Skip to the content
OrielDocs
0.1.0-alpha.10GitHub

Digging deeper

Oriel console

oriel is your app's command line. It runs the framework's commands, such as migrate and make:model, and the commands you write for your own app.

Introduction#

oriel is a script at the root of your app. Run it with Node, or with Bun. With no command, or with list, it lists every command it knows:

Terminal
node oriel listbun oriel list

list takes a group's name, the part of a command's name before its :, to list only that group:

Terminal
node oriel list make

Every command has a help screen, with its arguments and options. Give the command's name to help, or add --help (or -h) to the command:

Terminal
node oriel help migratenode oriel migrate --help

A name oriel doesn't know is an error, with the names closest to it: "Command "migrat" is not defined.", then "Did you mean one of these?" and up to three names.

On Node, the script loads your TypeScript with tsx, which a new app has among its dependencies; Bun runs it as it is. Either way, it creates your app from bootstrap/app.ts, boots it, runs the command, and shuts the app down, closing its database connections and writing out its logs, before it exits with the command's exit code.

Tinker (REPL)#

tinker opens Node's REPL with your app in it, as app:

Terminal
node oriel tinker

Type JavaScript at the oriel> prompt, await included. .exit (or Ctrl+D) leaves it.

Available commands#

These are the commands every app has. A new app adds one of its own, inspire, in routes/console.ts.

CommandWhat it does
aboutShows your app's name, the Oriel and Node versions, the environment, debug mode and URL
helpShows a command's help screen
listLists the commands, all of them or one group's
serveServes your app on Oriel's server; --watch restarts it when your code changes
testRuns your tests with Vitest
tinkerOpens Node's REPL with your app in it
db:seedSeeds the database, with DatabaseSeeder or the seeder you name
event:listLists your events and their listeners
ide:infoPrints your app's routes, commands, config keys, .env names, views, Live components, rules and Inertia pages as JSON, for editors (the OrielJS extension for VS Code reads it); secret values are left out
inertia:check-ssrChecks that the Inertia SSR server is up
inertia:middlewareWrites an Inertia middleware class, HandleInertiaRequests unless you name another
inertia:start-ssrStarts the Inertia SSR server
inertia:stop-ssrStops the Inertia SSR server
inertia:typesWrites the TypeScript types of your Inertia pages' props
install:apiWrites routes/api.ts and adds it to bootstrap/app.ts
key:generateSets APP_KEY in .env; --show prints a key instead
make:castWrites a custom cast class in app/Casts
make:commandWrites a console command class in app/Console/Commands
make:controllerWrites a controller in app/Http/Controllers
make:eventWrites an event class in app/Events
make:factoryWrites a model factory in database/factories
make:jobWrites a queued job in app/Jobs; --sync writes one that runs at once
make:listenerWrites an event listener in app/Listeners; --event names its event, --queued queues it
make:liveWrites a Live component; --test writes its test too
make:mailWrites a mailable in app/Mail, and its view
make:middlewareWrites a middleware class in app/Http/Middleware
make:migrationWrites a migration in database/migrations; --create or --table names its table
make:modelWrites a model in app/Models, and the files its options ask for
make:notificationWrites a notification in app/Notifications
make:observerWrites a model observer in app/Observers
make:policyWrites a policy in app/Policies; --model names its model
make:providerWrites a service provider in app/Providers
make:requestWrites a form request in app/Http/Requests
make:resourceWrites a resource class, whose toArray() shapes a model for JSON, in app/Http/Resources; --collection writes one for a list
make:ruleWrites a validation rule object in app/Rules
make:seederWrites a seeder in database/seeders
make:testWrites a test in tests/Feature, or in tests/Unit with --unit
migrateRuns the migrations that haven't run yet
migrate:freshDrops every table and runs every migration again
migrate:installCreates the table that records which migrations have run
migrate:refreshRolls back every migration and runs them all again
migrate:resetRolls back every migration
migrate:rollbackRolls back the last batch of migrations
migrate:statusShows whether each migration has run
model:typesWrites the TypeScript types of your models' attributes to types/models.d.ts
queue:clearDeletes the jobs waiting on a queue
queue:failedLists the failed jobs
queue:flushDeletes every failed job
queue:forgetDeletes one failed job
queue:listenRuns the queue's jobs, each in a fresh process
queue:restartTells every queue worker to stop after its current job
queue:retryPuts failed jobs back on their queue
queue:workRuns the queue's jobs until you stop it
route:listLists your routes
route:typesWrites the TypeScript types of your route names and parameters, and the route table for your pages
schedule:clear-cacheReleases the locks that withoutOverlapping() holds
schedule:interruptStops a running schedule:run from repeating its every-few-seconds tasks
schedule:listLists your scheduled tasks, with their cron expressions
schedule:runRuns the scheduled tasks that are due
schedule:workRuns the tasks that are due, every minute, until you stop it
storage:linkLinks public/storage to storage/app/public

node oriel help <command> lists a command's options. A few that are worth knowing up front:

  • make:model Flight -mcrfsa --policy writes the model with every file it can have: its migration (-m), a resource controller typed with the model (-c, -r), its factory (-f), its seeder (-s) and its policy (--policy). -a (--all) asks for all of them, and for the StoreFlightRequest and UpdateFlightRequest form requests the controller validates with. Short flags combine, as here.
  • make:controller takes --model=Photo, --invokable, --resource, --api and --requests: Generating controllers shows what each one writes. A model whose name is a reserved word gets a parameter with a Model suffix: --model=Case writes show(caseModel: Case).
  • make:mail OrderShipped writes the mailable and its view, resources/views/mail/order-shipped.tsx, which the mailable renders as mail.order-shipped. --view names another view. A view that exists is left as it is, even with --force.
  • serve --watch restarts the server when a file it runs changes. Running your application says which files those are.
  • A make: command doesn't replace a file that exists, unless you pass --force. make:migration writes a new file every time, its name starting with the date and time.

The schedule: commands run the tasks you schedule in routes/console.ts. Your server's cron calls node oriel schedule:run every minute; while you work, node oriel schedule:work does it for you. Task scheduling covers them.

Writing commands#

Besides the framework's commands, you can write your own. They live in app/Console/Commands, where oriel finds them by itself.

Generating commands#

make:command writes a new command class in app/Console/Commands, and creates the folder if your app doesn't have it yet:

Terminal
node oriel make:command SendEmails

The class is named as you give it. Its signature is app: and the class's name in kebab case, here app:send-emails, unless you pass another with --command:

Terminal
node oriel make:command SendEmails --command=mail:send

Command structure#

Once the command is written, set its signature and its description, which list and help show. The signature is the command's name and the input it expects. handle() runs when the command is called.

Here is a command that sends a marketing email to a user. Its work is done by a service, which handle() takes from the container with inject():

app/Console/Commands/SendEmails.ts
import { Command, inject } from '@orieljs/framework'import { User } from '../../Models/User'import { DripEmailer } from '../../Support/DripEmailer'export class SendEmails extends Command {  readonly signature = 'mail:send {user}'  override description = 'Send a marketing email to a user'  async handle(): Promise<void> {    const user = await User.findOrFail(this.argument('user'))    await inject(DripEmailer).send(user)  }}

Each run of a command has its own scope, as a request does, so inject(), the helpers and the facades work inside handle(), and a scoped service lasts for that one run.

Exit codes#

A command that returns nothing exits with 0, which means success. Return a number to exit with another code:

export class SendEmails extends Command {  readonly signature = 'mail:send {user}'  handle(): number {    this.error('Something went wrong.')    return 1  }}

A command that throws exits with 1, and oriel prints the error with its stack.

Closure commands#

A closure command is to a command class what a route's closure is to a controller. Define them in routes/console.ts with Artisan.command(), which takes the signature and a function:

routes/console.ts
import { Artisan } from '@orieljs/framework'import type { CommandContext } from '@orieljs/framework/contracts'Artisan.command('mail:send {user}', function (this: CommandContext, user: unknown) {  this.info(`Sending email to: ${String(user)}!`)})

The function receives the command's arguments, in the order the signature lists them, and its this is the command, with a command class's methods for its input and output: this.option(), this.info(), this.ask() and the rest. Write a function, not an arrow function, so that this is the command. The arguments are typed unknown: an argument is a string, or a list of strings for an array argument.

Closure command descriptions#

purpose() gives a closure command its description, which list and help show:

Artisan.command('mail:send {user}', function (this: CommandContext, user: unknown) {  // ...}).purpose('Send a marketing email to a user')

Isolatable commands#

Laravel's Isolatable commands aren't in Oriel. To keep a command from running twice at the same time, take a cache lock in its handle(), and return when another run holds it.

Defining input expectations#

The signature says what your command takes from the command line: its name, then its arguments and options, each in braces.

Arguments#

This command takes one required argument, user:

export class SendEmails extends Command {  readonly signature = 'mail:send {user}'  handle(): void {    // ...  }}

A ? makes an argument optional, and = gives it a default:

mail:send {user?}mail:send {user=foo}

Run without a required argument, the command stops before handle(), with "Not enough arguments (missing: "user")." and exit code 1.

Options#

An option is passed with two hyphens. One without a value is a switch: it is true when it is passed and false when it isn't.

mail:send {user} {--queue}
Terminal
node oriel mail:send 1 --queue

Options with values#

An = after the name makes an option take a value. When it isn't passed, it is undefined:

mail:send {user} {--queue=}
Terminal
node oriel mail:send 1 --queue=defaultnode oriel mail:send 1 --queue default

A value after the = is the default, used when the option isn't passed:

mail:send {user} {--queue=default}

Option shortcuts#

A letter and a | before the name give an option a shortcut, passed with one hyphen. The value follows the letter, with no =:

mail:send {user} {--Q|queue=}
Terminal
node oriel mail:send 1 -Qdefault

Switch shortcuts combine: -mfs is -m -f -s.

An option the signature doesn't declare is an error: "The "--bogus" option does not exist.".

Input arrays#

A * makes an argument take every value that follows it, as a list, so put it last:

mail:send {user*}
Terminal
node oriel mail:send 1 2

Here user is ['1', '2']. With ?*, the list may be empty:

mail:send {user?*}

Option arrays#

An option that takes several values is declared with =*. Pass it once for each value:

mail:send {--id=*}
Terminal
node oriel mail:send --id=1 --id=2

When it isn't passed, it is an empty list.

Input descriptions#

Describe an argument or an option after a colon, with a space on each side. help shows the descriptions. A signature can run over several lines:

export class SendEmails extends Command {  readonly signature = `mail:send    {user : The ID of the user}    {--queue : Whether the job should be queued}`  handle(): void {    // ...  }}

Command I/O#

Retrieving input#

argument() and option() read the command's input. arguments() and options() return all of them:

const userId = command.argument('user')const all = command.arguments()const queueName = command.option('queue')const options = command.options()

Arguments and option values are strings, as they arrive from the command line: --tries=3 is '3', so convert a number with Number(). An argument is a string, a list for an array argument, or undefined when an optional one is missing. An option is true or false for a switch, a string or undefined for an option with a value, and a list for an option array. An argument or an option that the signature doesn't declare is undefined.

Prompting for input#

ask() asks a question and returns the answer. A second argument is the default, shown in parentheses and returned when the answer is empty:

const name = await command.ask('What is your name?')const nameOrDefault = await command.ask('What is your name?', 'Taylor')

secret() asks for a password or another secret:

const password = await command.secret('What is the password?')

Asking for confirmation#

confirm() asks a yes or no question. It returns true when the answer is y or yes, in any case, and false for any other answer:

if (await command.confirm('Do you wish to continue?')) {  // ...}

An empty answer gives the default, false unless you pass true:

if (await command.confirm('Do you wish to continue?', true)) {  // ...}

Multiple choice questions#

choice() lists the choices, numbered from 0, and returns the one picked by its number or its text. The third argument is the index of the default, returned for an empty answer:

const name = await command.choice('What is your name?', ['Taylor', 'Dayle'], 0)

An answer that is neither stops the command, with ""Tom" is not a valid choice." and exit code 1.

Writing output#

info(), comment() and line() print a line to the standard output, and warn() and error() print one to the standard error. newLine() prints a blank line, or as many as you ask for:

command.info('The command was successful!')command.error('Something went wrong!')command.line('Display this on the screen')command.newLine()command.newLine(3)

Tables#

table() takes the column headings and the rows, each a list of strings, and sizes the columns to fit. The rows are a plain array, so end a collection's map() with all():

app/Console/Commands/ListUsers.ts
import { Command } from '@orieljs/framework'import { User } from '../../Models/User'export class ListUsers extends Command {  readonly signature = 'users:list'  override description = 'List the users'  async handle(): Promise<void> {    const users = await User.all()    this.table(      ['Name', 'Email'],      users.map((user) => [user.name, user.email]).all(),    )  }}

Laravel's progress bars aren't in Oriel.

Registering commands#

oriel registers every command class in app/Console/Commands and its subfolders: each exported class with a handle() method and a signature. It then loads routes/console.ts, the file that withRouting() names as console in bootstrap/app.ts, which defines your closure commands.

A command elsewhere is registered with Artisan.register(), in routes/console.ts or in a service provider's boot():

routes/console.ts
import { Artisan } from '@orieljs/framework'import { SendEmails } from '../app/Domain/Orders/Commands/SendEmails'Artisan.register(SendEmails)

A command class has no constructor arguments: oriel makes it with new. Two commands with the same name are one command, the one registered last, so a command of yours can take the place of one of the framework's.

Programmatically executing commands#

Artisan.call() runs a command from your code, such as a route or a job. It takes the command's name and its arguments and options, with each option's name written with its --, and returns the exit code:

routes/web.ts
import { Artisan, Route } from '@orieljs/framework'Route.post('/user/{user}/mail', async (user: string) => {  const exitCode = await Artisan.call('mail:send', { user, '--queue': 'default' })  return { exitCode, output: Artisan.output() }})

The command's output isn't printed: Artisan.output() returns what the last call printed, the standard output then the standard error. A command that throws makes Artisan.call() throw, and so does a name that no command has.

A command run by Artisan.call() has no one to answer its questions: ask(), confirm() and the others throw.

Passing array values#

An option array or an array argument takes a list:

await Artisan.call('mail:send', { '--id': [5, 13] })

Passing boolean values#

A switch takes true to pass it. false leaves it off:

await Artisan.call('migrate:refresh', { '--force': true })

Calling commands from other commands#

Inside a command class, this.call() runs another command. Its output goes to your command's output, and it returns the exit code:

app/Console/Commands/SendEmails.ts
import { Command } from '@orieljs/framework'export class SendEmails extends Command {  readonly signature = 'mail:send {user} {--queue=}'  async handle(): Promise<void> {    await this.call('mail:send-receipt', { user: this.argument('user'), '--queue': 'default' })  }}

Laravel's callSilently() isn't in Oriel: call Artisan.call(), whose output isn't printed.

Testing commands#

Your feature tests run a command with artisan(), and check its output and its exit code. Await the result: the command runs then, and not before.

tests/Feature/SendEmails.test.ts
import { test } from 'vitest'import { TestCase } from '../TestCase'const t = TestCase.boot()test('the mail is sent', async () => {  await t.artisan('mail:send', { user: 1 }).expectsOutput('Sending email to: 1!').assertExitCode(0)})

expectsOutput() wants a line of the output to equal its text, and expectsOutputToContain() the output to contain it; doesntExpectOutput() and doesntExpectOutputToContain() want the opposite. assertSuccessful() and assertFailed() check the exit code is 0, or isn't.

The Testing pages cover the rest.

Privacy policy ·

esc