@orieljs/framework 0.1.0-alpha.6 → 0.1.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-ZOEPKBVF.js → chunk-4MWG3B7G.js} +1347 -428
  5. package/dist/chunk-4MWG3B7G.js.map +1 -0
  6. package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
  7. package/dist/chunk-7ZQZYDOC.js.map +1 -0
  8. package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
  9. package/dist/chunk-GVNLZZTN.js.map +1 -0
  10. package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
  11. package/dist/chunk-VOYW3R5J.js.map +1 -0
  12. package/dist/contracts.d.ts +515 -17
  13. package/dist/contracts.js +1 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +964 -152
  16. package/dist/index.js +4279 -866
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
  19. package/dist/jsx-runtime.d.ts +2 -2
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
  22. package/dist/testing.d.ts +2 -2
  23. package/dist/testing.js +3 -3
  24. package/docs/authentication.md +487 -0
  25. package/docs/authorization.md +435 -0
  26. package/docs/cache.md +647 -0
  27. package/docs/collections.md +2270 -0
  28. package/docs/configuration.md +334 -0
  29. package/docs/console.md +651 -0
  30. package/docs/container.md +430 -0
  31. package/docs/controllers.md +406 -0
  32. package/docs/csrf.md +126 -0
  33. package/docs/database-testing.md +144 -0
  34. package/docs/database.md +236 -0
  35. package/docs/deployment.md +276 -0
  36. package/docs/eloquent-collections.md +218 -0
  37. package/docs/eloquent-factories.md +255 -0
  38. package/docs/eloquent-mutators.md +254 -0
  39. package/docs/eloquent-observers.md +145 -0
  40. package/docs/eloquent-relationships.md +557 -0
  41. package/docs/eloquent.md +494 -0
  42. package/docs/encryption.md +104 -0
  43. package/docs/errors.md +318 -0
  44. package/docs/events.md +336 -0
  45. package/docs/facades.md +278 -0
  46. package/docs/hashing.md +118 -0
  47. package/docs/helpers.md +1281 -0
  48. package/docs/http-tests.md +386 -0
  49. package/docs/inertia.md +485 -0
  50. package/docs/installation.md +209 -0
  51. package/docs/lifecycle.md +234 -0
  52. package/docs/live-tests.md +226 -0
  53. package/docs/live.md +910 -0
  54. package/docs/logging.md +255 -0
  55. package/docs/mail.md +619 -0
  56. package/docs/middleware.md +384 -0
  57. package/docs/migrations.md +416 -0
  58. package/docs/mocking.md +264 -0
  59. package/docs/navigation.json +273 -0
  60. package/docs/notifications.md +225 -0
  61. package/docs/pagination.md +247 -0
  62. package/docs/passwords.md +207 -0
  63. package/docs/providers.md +187 -0
  64. package/docs/queries.md +600 -0
  65. package/docs/queues.md +330 -0
  66. package/docs/requests.md +533 -0
  67. package/docs/responses.md +444 -0
  68. package/docs/routing.md +738 -0
  69. package/docs/scheduling.md +277 -0
  70. package/docs/seeding.md +122 -0
  71. package/docs/session.md +409 -0
  72. package/docs/starter-kits.md +396 -0
  73. package/docs/storage.md +192 -0
  74. package/docs/structure.md +264 -0
  75. package/docs/testing.md +165 -0
  76. package/docs/time-travel.md +138 -0
  77. package/docs/validation.md +1792 -0
  78. package/docs/verification.md +152 -0
  79. package/docs/views.md +566 -0
  80. package/docs/vite.md +322 -0
  81. package/package.json +34 -32
  82. package/dist/chunk-I63ZFMX6.js.map +0 -1
  83. package/dist/chunk-MFTZQMZM.js.map +0 -1
  84. package/dist/chunk-POEP4B2M.js.map +0 -1
  85. package/dist/chunk-ZOEPKBVF.js.map +0 -1
  86. package/dist/index.d-BJyJx6U1.d.ts +0 -319
@@ -0,0 +1,651 @@
1
+ # Oriel console
2
+
3
+ `oriel` is your app's command line. It runs the framework's commands, such as `migrate` and `make:model`, and the
4
+ commands you write for your own app.
5
+
6
+ ## Introduction
7
+
8
+ `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
9
+ every command it knows:
10
+
11
+ ```sh
12
+ node oriel list
13
+ bun oriel list
14
+ ```
15
+
16
+ `list` takes a group's name, the part of a command's name before its `:`, to list only that group:
17
+
18
+ ```sh
19
+ node oriel list make
20
+ ```
21
+
22
+ Every command has a help screen, with its arguments and options. Give the command's name to `help`, or add `--help`
23
+ (or `-h`) to the command:
24
+
25
+ ```sh
26
+ node oriel help migrate
27
+ node oriel migrate --help
28
+ ```
29
+
30
+ A name `oriel` doesn't know is an error, with the names closest to it: "Command "migrat" is not defined.", then "Did
31
+ you mean one of these?" and up to three names.
32
+
33
+ On Node, the script loads your TypeScript with `tsx`, which a new app has among its dependencies; Bun runs it as it
34
+ is. Either way, it creates your app from `bootstrap/app.ts`, boots it, runs the command, and shuts the app down,
35
+ closing its database connections and writing out its logs, before it exits with the command's exit code.
36
+
37
+ ### Tinker (REPL)
38
+
39
+ `tinker` opens Node's REPL with your app in it, as `app`:
40
+
41
+ ```sh
42
+ node oriel tinker
43
+ ```
44
+
45
+ Type JavaScript at the `oriel>` prompt, `await` included. `.exit` (or Ctrl+D) leaves it.
46
+
47
+ > [!NOTE]
48
+ > Laravel's Tinker is PsySH, with your models and facades at hand and a config file of its own. Oriel's `tinker` is
49
+ > Node's own REPL, with `app` in its context and nothing else, and it has no settings.
50
+
51
+ ## Available commands
52
+
53
+ These are the commands every app has. A new app adds one of its own, `inspire`, in `routes/console.ts`.
54
+
55
+ | Command | What it does |
56
+ | --- | --- |
57
+ | `about` | Shows your app's name, the Oriel and Node versions, the environment, debug mode and URL |
58
+ | `help` | Shows a command's help screen |
59
+ | `list` | Lists the commands, all of them or one group's |
60
+ | `serve` | Serves your app on Oriel's server; `--watch` restarts it when your code changes |
61
+ | `test` | Runs your tests with Vitest |
62
+ | `tinker` | Opens Node's REPL with your app in it |
63
+ | `db:seed` | Seeds the database, with `DatabaseSeeder` or the seeder you name |
64
+ | `event:list` | Lists your events and their listeners |
65
+ | `inertia:check-ssr` | Checks that the Inertia SSR server is up |
66
+ | `inertia:middleware` | Writes an Inertia middleware class, `HandleInertiaRequests` unless you name another |
67
+ | `inertia:start-ssr` | Starts the Inertia SSR server |
68
+ | `inertia:stop-ssr` | Stops the Inertia SSR server |
69
+ | `inertia:types` | Writes the TypeScript types of your Inertia pages' props |
70
+ | `install:api` | Writes `routes/api.ts` and adds it to `bootstrap/app.ts` |
71
+ | `key:generate` | Sets `APP_KEY` in `.env`; `--show` prints a key instead |
72
+ | `make:cast` | Writes a custom cast class in `app/Casts` |
73
+ | `make:command` | Writes a console command class in `app/Console/Commands` |
74
+ | `make:controller` | Writes a controller in `app/Http/Controllers` |
75
+ | `make:event` | Writes an event class in `app/Events` |
76
+ | `make:factory` | Writes a model factory in `database/factories` |
77
+ | `make:job` | Writes a queued job in `app/Jobs`; `--sync` writes one that runs at once |
78
+ | `make:listener` | Writes an event listener in `app/Listeners`; `--event` names its event, `--queued` queues it |
79
+ | `make:live` | Writes a Live component; `--test` writes its test too |
80
+ | `make:mail` | Writes a mailable in `app/Mail`, and its view |
81
+ | `make:middleware` | Writes a middleware class in `app/Http/Middleware` |
82
+ | `make:migration` | Writes a migration in `database/migrations`; `--create` or `--table` names its table |
83
+ | `make:model` | Writes a model in `app/Models`, and the files its options ask for |
84
+ | `make:notification` | Writes a notification in `app/Notifications` |
85
+ | `make:observer` | Writes a model observer in `app/Observers` |
86
+ | `make:policy` | Writes a policy in `app/Policies`; `--model` names its model |
87
+ | `make:provider` | Writes a service provider in `app/Providers` |
88
+ | `make:request` | Writes a form request in `app/Http/Requests` |
89
+ | `make:resource` | Writes a resource class, whose `toArray()` shapes a model for JSON, in `app/Http/Resources`; `--collection` writes one for a list |
90
+ | `make:rule` | Writes a validation rule object in `app/Rules` |
91
+ | `make:seeder` | Writes a seeder in `database/seeders` |
92
+ | `make:test` | Writes a test in `tests/Feature`, or in `tests/Unit` with `--unit` |
93
+ | `migrate` | Runs the migrations that haven't run yet |
94
+ | `migrate:fresh` | Drops every table and runs every migration again |
95
+ | `migrate:install` | Creates the table that records which migrations have run |
96
+ | `migrate:refresh` | Rolls back every migration and runs them all again |
97
+ | `migrate:reset` | Rolls back every migration |
98
+ | `migrate:rollback` | Rolls back the last batch of migrations |
99
+ | `migrate:status` | Shows whether each migration has run |
100
+ | `model:types` | Writes the TypeScript types of your models' attributes to `types/models.d.ts` |
101
+ | `queue:clear` | Deletes the jobs waiting on a queue |
102
+ | `queue:failed` | Lists the failed jobs |
103
+ | `queue:flush` | Deletes every failed job |
104
+ | `queue:forget` | Deletes one failed job |
105
+ | `queue:listen` | Runs the queue's jobs, each in a fresh process |
106
+ | `queue:restart` | Tells every queue worker to stop after its current job |
107
+ | `queue:retry` | Puts failed jobs back on their queue |
108
+ | `queue:work` | Runs the queue's jobs until you stop it |
109
+ | `route:list` | Lists your routes |
110
+ | `route:types` | Writes the TypeScript types of your route names and parameters, and the route table for your pages |
111
+ | `schedule:clear-cache` | Releases the locks that `withoutOverlapping()` holds |
112
+ | `schedule:interrupt` | Stops a running `schedule:run` from repeating its every-few-seconds tasks |
113
+ | `schedule:list` | Lists your scheduled tasks, with their cron expressions |
114
+ | `schedule:run` | Runs the scheduled tasks that are due |
115
+ | `schedule:work` | Runs the tasks that are due, every minute, until you stop it |
116
+ | `storage:link` | Links `public/storage` to `storage/app/public` |
117
+
118
+ `node oriel help <command>` lists a command's options. A few that are worth knowing up front:
119
+
120
+ - `make:model Flight -mcrfsa --policy` writes the model with every file it can have: its migration (`-m`), a resource
121
+ controller typed with the model (`-c`, `-r`), its factory (`-f`), its seeder (`-s`) and its policy (`--policy`).
122
+ `-a` (`--all`) asks for all of them, and for the `StoreFlightRequest` and `UpdateFlightRequest` form requests the
123
+ controller validates with. Short flags combine, as here.
124
+ - `make:controller` takes `--model=Photo`, `--invokable`, `--resource`, `--api` and `--requests`:
125
+ [Generating controllers](controllers.md#generating-controllers) shows what each one writes. A model whose name is a
126
+ reserved word gets a parameter with a `Model` suffix: `--model=Case` writes `show(caseModel: Case)`.
127
+ - `make:mail OrderShipped` writes the mailable and its view, `resources/views/mail/order-shipped.tsx`, which the
128
+ mailable renders as `mail.order-shipped`. `--view` names another view. A view that exists is left as it is, even
129
+ with `--force`.
130
+ - `serve --watch` restarts the server when a file it runs changes. [Running your
131
+ application](installation.md#running-your-application) says which files those are.
132
+ - A `make:` command doesn't replace a file that exists, unless you pass `--force`. `make:migration` writes a new file
133
+ every time, its name starting with the date and time.
134
+
135
+ The `schedule:` commands run the tasks you schedule in `routes/console.ts`. Your server's cron calls
136
+ `node oriel schedule:run` every minute; while you work, `node oriel schedule:work` does it for you.
137
+ [Task scheduling](scheduling.md) covers them.
138
+
139
+ ## Writing commands
140
+
141
+ Besides the framework's commands, you can write your own. They live in `app/Console/Commands`, where `oriel` finds
142
+ them by itself.
143
+
144
+ ### Generating commands
145
+
146
+ `make:command` writes a new command class in `app/Console/Commands`, and creates the folder if your app doesn't have
147
+ it yet:
148
+
149
+ ```sh
150
+ node oriel make:command SendEmails
151
+ ```
152
+
153
+ The class is named as you give it. Its signature is `app:` and the class's name in kebab case, here `app:send-emails`,
154
+ unless you pass another with `--command`:
155
+
156
+ ```sh
157
+ node oriel make:command SendEmails --command=mail:send
158
+ ```
159
+
160
+ ### Command structure
161
+
162
+ Once the command is written, set its `signature` and its `description`, which `list` and `help` show. The signature
163
+ is the command's name and the [input it expects](#defining-input-expectations). `handle()` runs when the command is
164
+ called.
165
+
166
+ Here is a command that sends a marketing email to a user. Its work is done by a service, which `handle()` takes from
167
+ the container with `inject()`:
168
+
169
+ ```ts title="app/Console/Commands/SendEmails.ts"
170
+ // @filename: app/Support/DripEmailer.ts
171
+ import type { User } from '../Models/User'
172
+ export declare class DripEmailer {
173
+ send(user: User): Promise<void>
174
+ }
175
+ // ---cut---
176
+ import { Command, inject } from '@orieljs/framework'
177
+ import { User } from '../../Models/User'
178
+ import { DripEmailer } from '../../Support/DripEmailer'
179
+
180
+ export class SendEmails extends Command {
181
+ readonly signature = 'mail:send {user}'
182
+ override description = 'Send a marketing email to a user'
183
+
184
+ async handle(): Promise<void> {
185
+ const user = await User.findOrFail(this.argument('user'))
186
+
187
+ await inject(DripEmailer).send(user)
188
+ }
189
+ }
190
+ ```
191
+
192
+ Each run of a command has its own scope, as a request does, so `inject()`, the helpers and the facades work inside
193
+ `handle()`, and a scoped service lasts for that one run.
194
+
195
+ > [!NOTE]
196
+ > Laravel injects the services that `handle()` type-hints. TypeScript types are gone when your app runs, so Oriel
197
+ > calls `handle()` with no arguments: take a service with `inject()` or its facade, inside `handle()`.
198
+
199
+ #### Exit codes
200
+
201
+ A command that returns nothing exits with `0`, which means success. Return a number to exit with another code:
202
+
203
+ ```ts
204
+ export class SendEmails extends Command {
205
+ readonly signature = 'mail:send {user}'
206
+
207
+ handle(): number {
208
+ this.error('Something went wrong.')
209
+
210
+ return 1
211
+ }
212
+ }
213
+ ```
214
+
215
+ A command that throws exits with `1`, and `oriel` prints the error with its stack.
216
+
217
+ > [!NOTE]
218
+ > Laravel's `$this->fail()` isn't in Oriel. Print the message with `this.error()` and return `1`, or throw.
219
+
220
+ ### Closure commands
221
+
222
+ A closure command is to a command class what a route's closure is to a controller. Define them in
223
+ `routes/console.ts` with `Artisan.command()`, which takes the signature and a function:
224
+
225
+ ```ts title="routes/console.ts"
226
+ import { Artisan } from '@orieljs/framework'
227
+ import type { CommandContext } from '@orieljs/framework/contracts'
228
+
229
+ Artisan.command('mail:send {user}', function (this: CommandContext, user: unknown) {
230
+ this.info(`Sending email to: ${String(user)}!`)
231
+ })
232
+ ```
233
+
234
+ The function receives the command's arguments, in the order the signature lists them, and its `this` is the command,
235
+ with a command class's methods for its input and output: `this.option()`, `this.info()`, `this.ask()` and the rest.
236
+ Write a `function`, not an arrow function, so that `this` is the command. The arguments are typed `unknown`: an argument
237
+ is a string, or a list of strings for an [array argument](#input-arrays).
238
+
239
+ > [!NOTE]
240
+ > Laravel matches a closure's parameters to the arguments and options by name, and injects any service it
241
+ > type-hints. Oriel passes the arguments only, by position: read an option with `this.option()`, and a service with
242
+ > `inject()`.
243
+
244
+ #### Closure command descriptions
245
+
246
+ `purpose()` gives a closure command its description, which `list` and `help` show:
247
+
248
+ ```ts
249
+ Artisan.command('mail:send {user}', function (this: CommandContext, user: unknown) {
250
+ // ...
251
+ }).purpose('Send a marketing email to a user')
252
+ ```
253
+
254
+ ### Isolatable commands
255
+
256
+ Laravel's `Isolatable` commands aren't in Oriel. To keep a command from running twice at the same time, take a
257
+ [cache lock](cache.md#atomic-locks) in its `handle()`, and return when another run holds it.
258
+
259
+ ## Defining input expectations
260
+
261
+ The signature says what your command takes from the command line: its name, then its arguments and options, each in
262
+ braces.
263
+
264
+ ### Arguments
265
+
266
+ This command takes one required argument, `user`:
267
+
268
+ ```ts
269
+ export class SendEmails extends Command {
270
+ readonly signature = 'mail:send {user}'
271
+
272
+ handle(): void {
273
+ // ...
274
+ }
275
+ }
276
+ ```
277
+
278
+ A `?` makes an argument optional, and `=` gives it a default:
279
+
280
+ ```text
281
+ mail:send {user?}
282
+ mail:send {user=foo}
283
+ ```
284
+
285
+ Run without a required argument, the command stops before `handle()`, with "Not enough arguments (missing:
286
+ "user")." and exit code `1`.
287
+
288
+ ### Options
289
+
290
+ An option is passed with two hyphens. One without a value is a switch: it is `true` when it is passed and `false`
291
+ when it isn't.
292
+
293
+ ```text
294
+ mail:send {user} {--queue}
295
+ ```
296
+
297
+ ```sh
298
+ node oriel mail:send 1 --queue
299
+ ```
300
+
301
+ #### Options with values
302
+
303
+ An `=` after the name makes an option take a value. When it isn't passed, it is `undefined`:
304
+
305
+ ```text
306
+ mail:send {user} {--queue=}
307
+ ```
308
+
309
+ ```sh
310
+ node oriel mail:send 1 --queue=default
311
+ node oriel mail:send 1 --queue default
312
+ ```
313
+
314
+ A value after the `=` is the default, used when the option isn't passed:
315
+
316
+ ```text
317
+ mail:send {user} {--queue=default}
318
+ ```
319
+
320
+ #### Option shortcuts
321
+
322
+ A letter and a `|` before the name give an option a shortcut, passed with one hyphen. The value follows the letter,
323
+ with no `=`:
324
+
325
+ ```text
326
+ mail:send {user} {--Q|queue=}
327
+ ```
328
+
329
+ ```sh
330
+ node oriel mail:send 1 -Qdefault
331
+ ```
332
+
333
+ Switch shortcuts combine: `-mfs` is `-m -f -s`.
334
+
335
+ An option the signature doesn't declare is an error: "The "--bogus" option does not exist.".
336
+
337
+ ### Input arrays
338
+
339
+ A `*` makes an argument take every value that follows it, as a list, so put it last:
340
+
341
+ ```text
342
+ mail:send {user*}
343
+ ```
344
+
345
+ ```sh
346
+ node oriel mail:send 1 2
347
+ ```
348
+
349
+ Here `user` is `['1', '2']`. With `?*`, the list may be empty:
350
+
351
+ ```text
352
+ mail:send {user?*}
353
+ ```
354
+
355
+ #### Option arrays
356
+
357
+ An option that takes several values is declared with `=*`. Pass it once for each value:
358
+
359
+ ```text
360
+ mail:send {--id=*}
361
+ ```
362
+
363
+ ```sh
364
+ node oriel mail:send --id=1 --id=2
365
+ ```
366
+
367
+ When it isn't passed, it is an empty list.
368
+
369
+ ### Input descriptions
370
+
371
+ Describe an argument or an option after a colon, with a space on each side. `help` shows the descriptions. A
372
+ signature can run over several lines:
373
+
374
+ ```ts
375
+ export class SendEmails extends Command {
376
+ readonly signature = `mail:send
377
+ {user : The ID of the user}
378
+ {--queue : Whether the job should be queued}`
379
+
380
+ handle(): void {
381
+ // ...
382
+ }
383
+ }
384
+ ```
385
+
386
+ > [!NOTE]
387
+ > Laravel can prompt for a missing required argument (`PromptsForMissingInput`). Oriel stops with an error instead:
388
+ > ask for it with [`ask()`](#prompting-for-input) when an optional argument is missing.
389
+
390
+ ## Command I/O
391
+
392
+ ### Retrieving input
393
+
394
+ `argument()` and `option()` read the command's input. `arguments()` and `options()` return all of them:
395
+
396
+ ```ts
397
+ declare const command: Command
398
+ // ---cut---
399
+ const userId = command.argument('user')
400
+
401
+ const all = command.arguments()
402
+
403
+ const queueName = command.option('queue')
404
+
405
+ const options = command.options()
406
+ ```
407
+
408
+ Arguments and option values are strings, as they arrive from the command line: `--tries=3` is `'3'`, so convert a
409
+ number with `Number()`. An argument is a string, a list for an array argument, or `undefined` when an optional one is
410
+ missing. An option is `true` or `false` for a switch, a string or `undefined` for an option with a value, and a list
411
+ for an option array. An argument or an option that the signature doesn't declare is `undefined`.
412
+
413
+ ### Prompting for input
414
+
415
+ `ask()` asks a question and returns the answer. A second argument is the default, shown in parentheses and returned
416
+ when the answer is empty:
417
+
418
+ ```ts
419
+ declare const command: Command
420
+ // ---cut---
421
+ const name = await command.ask('What is your name?')
422
+
423
+ const nameOrDefault = await command.ask('What is your name?', 'Taylor')
424
+ ```
425
+
426
+ `secret()` asks for a password or another secret:
427
+
428
+ ```ts
429
+ declare const command: Command
430
+ // ---cut---
431
+ const password = await command.secret('What is the password?')
432
+ ```
433
+
434
+ > [!WARNING]
435
+ > Laravel hides what the user types for `secret()`. Oriel's `secret()` reads the answer as `ask()` does, and the
436
+ > terminal shows it as it is typed.
437
+
438
+ #### Asking for confirmation
439
+
440
+ `confirm()` asks a yes or no question. It returns `true` when the answer is `y` or `yes`, in any case, and `false`
441
+ for any other answer:
442
+
443
+ ```ts
444
+ declare const command: Command
445
+ // ---cut---
446
+ if (await command.confirm('Do you wish to continue?')) {
447
+ // ...
448
+ }
449
+ ```
450
+
451
+ An empty answer gives the default, `false` unless you pass `true`:
452
+
453
+ ```ts
454
+ declare const command: Command
455
+ // ---cut---
456
+ if (await command.confirm('Do you wish to continue?', true)) {
457
+ // ...
458
+ }
459
+ ```
460
+
461
+ #### Multiple choice questions
462
+
463
+ `choice()` lists the choices, numbered from 0, and returns the one picked by its number or its text. The third
464
+ argument is the index of the default, returned for an empty answer:
465
+
466
+ ```ts
467
+ declare const command: Command
468
+ // ---cut---
469
+ const name = await command.choice('What is your name?', ['Taylor', 'Dayle'], 0)
470
+ ```
471
+
472
+ An answer that is neither stops the command, with ""Tom" is not a valid choice." and exit code `1`.
473
+
474
+ > [!NOTE]
475
+ > Laravel's `choice()` asks again after a wrong answer, and can take several answers; `anticipate()`, which suggests
476
+ > answers as you type, and Laravel Prompts aren't in Oriel.
477
+
478
+ ### Writing output
479
+
480
+ `info()`, `comment()` and `line()` print a line to the standard output, and `warn()` and `error()` print one to the
481
+ standard error. `newLine()` prints a blank line, or as many as you ask for:
482
+
483
+ ```ts
484
+ declare const command: Command
485
+ // ---cut---
486
+ command.info('The command was successful!')
487
+
488
+ command.error('Something went wrong!')
489
+
490
+ command.line('Display this on the screen')
491
+
492
+ command.newLine()
493
+ command.newLine(3)
494
+ ```
495
+
496
+ > [!NOTE]
497
+ > Laravel prints each kind of message in its own colour. Oriel prints them as plain text; `warn()` and `error()` go to
498
+ > the standard error, so a script that runs your command can tell them apart.
499
+
500
+ #### Tables
501
+
502
+ `table()` takes the column headings and the rows, each a list of strings, and sizes the columns to fit. The rows are
503
+ a plain array, so end a collection's `map()` with `all()`:
504
+
505
+ ```ts title="app/Console/Commands/ListUsers.ts"
506
+ import { Command } from '@orieljs/framework'
507
+ import { User } from '../../Models/User'
508
+
509
+ export class ListUsers extends Command {
510
+ readonly signature = 'users:list'
511
+ override description = 'List the users'
512
+
513
+ async handle(): Promise<void> {
514
+ const users = await User.all()
515
+
516
+ this.table(
517
+ ['Name', 'Email'],
518
+ users.map((user) => [user.name, user.email]).all(),
519
+ )
520
+ }
521
+ }
522
+ ```
523
+
524
+ Laravel's progress bars aren't in Oriel.
525
+
526
+ ## Registering commands
527
+
528
+ `oriel` registers every command class in `app/Console/Commands` and its subfolders: each exported class with a
529
+ `handle()` method and a `signature`. It then loads `routes/console.ts`, the file that `withRouting()` names as
530
+ `console` in `bootstrap/app.ts`, which defines your closure commands.
531
+
532
+ A command elsewhere is registered with `Artisan.register()`, in `routes/console.ts` or in a service provider's
533
+ `boot()`:
534
+
535
+ ```ts title="routes/console.ts"
536
+ // @filename: app/Domain/Orders/Commands/SendEmails.ts
537
+ import { Command } from '@orieljs/framework'
538
+ export class SendEmails extends Command {
539
+ readonly signature = 'orders:send-emails'
540
+ handle(): void {}
541
+ }
542
+ // ---cut---
543
+ import { Artisan } from '@orieljs/framework'
544
+ import { SendEmails } from '../app/Domain/Orders/Commands/SendEmails'
545
+
546
+ Artisan.register(SendEmails)
547
+ ```
548
+
549
+ A command class has no constructor arguments: `oriel` makes it with `new`. Two commands with the same name are one
550
+ command, the one registered last, so a command of yours can take the place of one of the framework's.
551
+
552
+ > [!WARNING]
553
+ > Your commands in `app/Console/Commands` and `routes/console.ts` are loaded only when your app runs in the console:
554
+ > under `oriel`, which `node oriel serve` is too, and in your tests. An app served by `public/index.ts` doesn't load
555
+ > them, so `Artisan.call()` from one of its routes can't find them. A command registered in a service provider's
556
+ > `boot()` is there in both.
557
+
558
+ ## Programmatically executing commands
559
+
560
+ `Artisan.call()` runs a command from your code, such as a route or a job. It takes the command's name and its
561
+ arguments and options, with each option's name written with its `--`, and returns the exit code:
562
+
563
+ ```ts title="routes/web.ts"
564
+ import { Artisan, Route } from '@orieljs/framework'
565
+
566
+ Route.post('/user/{user}/mail', async (user: string) => {
567
+ const exitCode = await Artisan.call('mail:send', { user, '--queue': 'default' })
568
+
569
+ return { exitCode, output: Artisan.output() }
570
+ })
571
+ ```
572
+
573
+ The command's output isn't printed: `Artisan.output()` returns what the last call printed, the standard output then
574
+ the standard error. A command that throws makes `Artisan.call()` throw, and so does a name that no command has.
575
+
576
+ > [!NOTE]
577
+ > Laravel's `Artisan::call()` also takes the whole command as one string, `'mail:send 1 --queue=default'`. Oriel's
578
+ > takes the name only, with the input as an object. `Artisan::queue()` isn't in Oriel: dispatch a
579
+ > [job](queues.md) that calls the command.
580
+
581
+ A command run by `Artisan.call()` has no one to answer its questions: `ask()`, `confirm()` and the others throw.
582
+
583
+ #### Passing array values
584
+
585
+ An option array or an array argument takes a list:
586
+
587
+ ```ts
588
+ await Artisan.call('mail:send', { '--id': [5, 13] })
589
+ ```
590
+
591
+ #### Passing boolean values
592
+
593
+ A switch takes `true` to pass it. `false` leaves it off:
594
+
595
+ ```ts
596
+ await Artisan.call('migrate:refresh', { '--force': true })
597
+ ```
598
+
599
+ ### Calling commands from other commands
600
+
601
+ Inside a command class, `this.call()` runs another command. Its output goes to your command's output, and it returns
602
+ the exit code:
603
+
604
+ ```ts title="app/Console/Commands/SendEmails.ts"
605
+ import { Command } from '@orieljs/framework'
606
+
607
+ export class SendEmails extends Command {
608
+ readonly signature = 'mail:send {user} {--queue=}'
609
+
610
+ async handle(): Promise<void> {
611
+ await this.call('mail:send-receipt', { user: this.argument('user'), '--queue': 'default' })
612
+ }
613
+ }
614
+ ```
615
+
616
+ > [!WARNING]
617
+ > `this.call()` with a name that no command has does nothing, and returns `undefined`. Laravel throws there. It lets
618
+ > a command call another that a part of the framework your app may not use would register, as `make:model -m` calls
619
+ > `make:migration`.
620
+
621
+ Laravel's `callSilently()` isn't in Oriel: call `Artisan.call()`, whose output isn't printed.
622
+
623
+ ## Testing commands
624
+
625
+ Your feature tests run a command with `artisan()`, and check its output and its exit code. Await the result: the
626
+ command runs then, and not before.
627
+
628
+ ```ts title="tests/Feature/SendEmails.test.ts"
629
+ // @filename: tests/TestCase.ts
630
+ import { TestCase as BaseTestCase } from '@orieljs/framework/testing'
631
+ export class TestCase extends BaseTestCase {}
632
+ // ---cut---
633
+ import { test } from 'vitest'
634
+ import { TestCase } from '../TestCase'
635
+
636
+ const t = TestCase.boot()
637
+
638
+ test('the mail is sent', async () => {
639
+ await t.artisan('mail:send', { user: 1 }).expectsOutput('Sending email to: 1!').assertExitCode(0)
640
+ })
641
+ ```
642
+
643
+ `expectsOutput()` wants a line of the output to equal its text, and `expectsOutputToContain()` the output to contain
644
+ it; `doesntExpectOutput()` and `doesntExpectOutputToContain()` want the opposite. `assertSuccessful()` and
645
+ `assertFailed()` check the exit code is `0`, or isn't.
646
+
647
+ > [!NOTE]
648
+ > Laravel's `expectsQuestion()` and `expectsConfirmation()` aren't in Oriel: a command that asks a question fails in
649
+ > a test. Give it the answer as an argument or an option instead.
650
+
651
+ The [Testing](testing.md) pages cover the rest.