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:
node oriel listbun oriel listlist takes a group's name, the part of a command's name before its :, to list only that group:
node oriel list makeEvery 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:
node oriel help migratenode oriel migrate --helpA 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/, 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:
node oriel tinkerType 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/.
| Command | What it does |
|---|---|
about | Shows your app's name, the Oriel and Node versions, the environment, debug mode and URL |
help | Shows a command's help screen |
list | Lists the commands, all of them or one group's |
serve | Serves your app on Oriel's server; --watch restarts it when your code changes |
test | Runs your tests with Vitest |
tinker | Opens Node's REPL with your app in it |
db:seed | Seeds the database, with DatabaseSeeder or the seeder you name |
event:list | Lists your events and their listeners |
ide:info | Prints 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-ssr | Checks that the Inertia SSR server is up |
inertia:middleware | Writes an Inertia middleware class, HandleInertiaRequests unless you name another |
inertia:start-ssr | Starts the Inertia SSR server |
inertia:stop-ssr | Stops the Inertia SSR server |
inertia:types | Writes the TypeScript types of your Inertia pages' props |
install:api | Writes routes/ and adds it to bootstrap/ |
key:generate | Sets APP_KEY in .env; --show prints a key instead |
make:cast | Writes a custom cast class in app/ |
make:command | Writes a console command class in app/ |
make:controller | Writes a controller in app/ |
make:event | Writes an event class in app/ |
make:factory | Writes a model factory in database/ |
make:job | Writes a queued job in app/; --sync writes one that runs at once |
make:listener | Writes an event listener in app/; --event names its event, --queued queues it |
make:live | Writes a Live component; --test writes its test too |
make:mail | Writes a mailable in app/, and its view |
make:middleware | Writes a middleware class in app/ |
make:migration | Writes a migration in database/; --create or --table names its table |
make:model | Writes a model in app/, and the files its options ask for |
make:notification | Writes a notification in app/ |
make:observer | Writes a model observer in app/ |
make:policy | Writes a policy in app/; --model names its model |
make:provider | Writes a service provider in app/ |
make:request | Writes a form request in app/ |
make:resource | Writes a resource class, whose toArray() shapes a model for JSON, in app/; --collection writes one for a list |
make:rule | Writes a validation rule object in app/ |
make:seeder | Writes a seeder in database/ |
make:test | Writes a test in tests/, or in tests/ with --unit |
migrate | Runs the migrations that haven't run yet |
migrate:fresh | Drops every table and runs every migration again |
migrate:install | Creates the table that records which migrations have run |
migrate:refresh | Rolls back every migration and runs them all again |
migrate:reset | Rolls back every migration |
migrate:rollback | Rolls back the last batch of migrations |
migrate:status | Shows whether each migration has run |
model:types | Writes the TypeScript types of your models' attributes to types/ |
queue:clear | Deletes the jobs waiting on a queue |
queue:failed | Lists the failed jobs |
queue:flush | Deletes every failed job |
queue:forget | Deletes one failed job |
queue:listen | Runs the queue's jobs, each in a fresh process |
queue:restart | Tells every queue worker to stop after its current job |
queue:retry | Puts failed jobs back on their queue |
queue:work | Runs the queue's jobs until you stop it |
route:list | Lists your routes |
route:types | Writes the TypeScript types of your route names and parameters, and the route table for your pages |
schedule:clear-cache | Releases the locks that withoutOverlapping() holds |
schedule:interrupt | Stops a running schedule:run from repeating its every-few-seconds tasks |
schedule:list | Lists your scheduled tasks, with their cron expressions |
schedule:run | Runs the scheduled tasks that are due |
schedule:work | Runs the tasks that are due, every minute, until you stop it |
storage:link | Links public/ to storage/ |
node oriel help <command> lists a command's options. A few that are worth knowing up front:
make:model Flight -mcrfsa --policywrites 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 theStoreFlightRequestandUpdateFlightRequestform requests the controller validates with. Short flags combine, as here.make:controllertakes--model=Photo,--invokable,--resource,--apiand--requests: Generating controllers shows what each one writes. A model whose name is a reserved word gets a parameter with aModelsuffix:--model=Casewritesshow(caseModel: Case).make:mail OrderShippedwrites the mailable and its view,resources/, which the mailable renders asviews/ mail/ order-shipped.tsx mail.order-shipped.--viewnames another view. A view that exists is left as it is, even with--force.serve --watchrestarts 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:migrationwrites a new file every time, its name starting with the date and time.
The schedule: commands run the tasks you schedule in routes/. 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/, where oriel finds
them by itself.
Generating commands#
make:command writes a new command class in app/, and creates the folder if your app doesn't have
it yet:
node oriel make:command SendEmailsThe 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:
node oriel make:command SendEmails --command=mail:sendCommand 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():
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/ with Artisan.command(), which takes the signature and a function:
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}node oriel mail:send 1 --queueOptions with values#
An = after the name makes an option take a value. When it isn't passed, it is undefined:
mail:send {user} {--queue=}node oriel mail:send 1 --queue=defaultnode oriel mail:send 1 --queue defaultA 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=}node oriel mail:send 1 -QdefaultSwitch 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*}node oriel mail:send 1 2Here 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=*}node oriel mail:send --id=1 --id=2When 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():
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/ and its subfolders: each exported class with a
handle() method and a signature. It then loads routes/, the file that withRouting() names as
console in bootstrap/, which defines your closure commands.
A command elsewhere is registered with Artisan.register(), in routes/ or in a service provider's
boot():
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:
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:
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.
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.