@avelonjs/cli 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ryan Yannelli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # @avelonjs/cli
2
+
3
+ `@avelonjs/cli` is reeve, the official who administers an Avelon app. It is an Ink TUI on Bun that also stays fully scriptable. Reach for it to generate the application tree, sync the Next router, run Bailiff, drain errands, and ask doctor whether a push is safe.
4
+
5
+ Bare `./reeve` opens the TUI. Any subcommand with arguments skips it. `--no-tui` and a non-TTY stdout force plain mode.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ bun add -d @avelonjs/cli
11
+ ```
12
+
13
+ Put a `reeve` bin on your PATH, or run `bunx reeve`. Application `package.json` scripts call it before Next:
14
+
15
+ ```json
16
+ {
17
+ "scripts": {
18
+ "dev": "reeve route:sync && bun run --bun next dev",
19
+ "build": "reeve route:sync && bun run --bun next build",
20
+ "check": "reeve doctor"
21
+ }
22
+ }
23
+ ```
24
+
25
+ ## Basic Usage
26
+
27
+ ```sh
28
+ reeve make:model Post -mfcpw
29
+ reeve route:sync
30
+ reeve bailiff --explain no-orphan-query
31
+ reeve doctor
32
+ ```
33
+
34
+ Every TUI path prints the equivalent non-interactive command before running it. The header always shows app name, database driver, adapter, and environment.
35
+
36
+ ## Generators
37
+
38
+ ```
39
+ reeve make:model Post -mfcpw
40
+ reeve make:controller PostController --resource
41
+ reeve make:request StorePostRequest
42
+ reeve make:policy PostPolicy --model=Post
43
+ reeve make:ward PostWard --model=Post
44
+ reeve make:event PostPublished
45
+ reeve make:listener NotifySubscribers --event=PostPublished --queued
46
+ reeve make:errand GenerateThumbnail
47
+ reeve make:action PublishPost
48
+ reeve make:middleware EnsureSubscribed
49
+ reeve make:command PruneDrafts
50
+ reeve make:view posts/Index
51
+ reeve make:auth
52
+ reeve make:driver storage r2
53
+ reeve make:package billing
54
+ reeve make:migration create_posts_table
55
+ reeve stub:publish
56
+ ```
57
+
58
+ `stub:publish` ejects templates into `stubs/` so you change generated shape without forking the framework.
59
+
60
+ `reeve make:driver storage r2` scaffolds a package with every capability `false`, methods that throw
61
+ `unimplemented`, a wired conformance suite, and a README that already passes `docs:check`. The
62
+ certification path is `docs/15-driver-authoring.md`.
63
+
64
+ ## Database, wards, routes, errands
65
+
66
+ ```
67
+ reeve migrate
68
+ reeve migrate:rollback
69
+ reeve migrate:fresh --seed
70
+ reeve migrate:status
71
+ reeve schema:pull
72
+ reeve db:seed
73
+ reeve ward:list
74
+ reeve ward:sync
75
+ reeve ward:check
76
+ reeve ward:explain Post read
77
+ reeve route:list
78
+ reeve route:sync
79
+ reeve errand:work
80
+ reeve errand:failed
81
+ reeve errand:retry all
82
+ reeve errand:flush
83
+ ```
84
+
85
+ `migrate:fresh` names what it will destroy and requires typing the environment name when it is not `local`.
86
+
87
+ ## Doctor and inspect
88
+
89
+ ```
90
+ reeve doctor
91
+ reeve bailiff
92
+ reeve bailiff --fix
93
+ reeve capabilities
94
+ reeve config:show
95
+ reeve about
96
+ reeve tinker --execute '1 + 1'
97
+ ```
98
+
99
+ `doctor` aggregates Bailiff, ward coverage, environment, and the capability matrix into one exit code. `docs:check` lints every package README: required sections, TypeScript code blocks that parse, and every public export in the Method Reference table.
100
+
101
+ ```
102
+ reeve docs:check
103
+ reeve docs:check packages/cli
104
+ reeve docs:check --workspace
105
+ ```
106
+
107
+ Waive a section or export with an HTML comment in the README:
108
+
109
+ ```html
110
+ <!-- docs:check-waiver: section Features -->
111
+ <!-- docs:check-waiver: export internalHelper -->
112
+ ```
113
+
114
+ ## Method Reference
115
+
116
+ | Method / export | Signature | Description |
117
+ | -------------------- | ------------------------------------------------------- | ----------------------------------------------------------------- |
118
+ | `runReeve` | `(argv, io?) => Promise<number>` | CLI entry. Opens the TUI or runs a subcommand. |
119
+ | `runCommand` | `(command, argv, io) => Promise<number>` | Dispatches one parsed command. |
120
+ | `shouldUseTui` | `(argv, io) => boolean` | True for bare TTY invocations without `--no-tui`. |
121
+ | `processIO` | `() => ReeveIO` | Process cwd, stdout, TTY, and env. |
122
+ | `MENU` | `readonly MenuItem[]` | TUI catalog. Every row has a scriptable `command`. |
123
+ | `COMMANDS` | `string[]` | Unique command names. |
124
+ | `ReeveIO` | `interface` | Testable IO: `cwd`, `stdout`, `isTTY`, `env`, optional `confirm`. |
125
+ | `renderTui` | `(io) => Promise<void>` | Ink TUI. Prints `reeve <command>` before running a selection. |
126
+ | `makeModel` | `(io, name, options) => Promise<void>` | Writes a model and optional `-mfcpw` siblings. |
127
+ | `makeMigration` | `(io, name) => Promise<void>` | Writes a timestamped driver-owned migration module. |
128
+ | `publishStubs` | `(io) => Promise<readonly string[]>` | Ejects built-in templates into `stubs/`. |
129
+ | `writeFromStub` | `(io, stub, relative, name, extra?) => Promise<string>` | Renders a stub onto disk. |
130
+ | `studly` | `(name) => string` | PascalCase helper for generator names. |
131
+ | `snake` | `(name) => string` | snake_case helper. |
132
+ | `headerBits` | `(io) => { app, database, adapter, environment }` | TUI header fields. |
133
+ | `checkPackageDocs` | `(root: string) => Promise<DocsCheckResult>` | Lints one package README against house style. |
134
+ | `runDocsCheck` | `(argv, io) => Promise<number>` | CLI handler for `docs:check`, including `--workspace`. |
135
+ | `DocsCheckResult` | `interface` | `root`, `ok`, and `findings` from one package lint. |
136
+ | `DocsFinding` | `interface` | One missing section, unparsed block, or missing export. |
137
+ | `makeDriver` | `(io, contract, slug) => Promise<void>` | Scaffolds a driver package with conformance wired. |
138
+ | `DRIVER_CONTRACTS` | `string[]` | Contract names `make:driver` accepts. |
139
+ | `DriverContractSpec` | `interface` | Suite, types, capabilities, and method stubs for one contract. |
140
+
141
+ ## Testing
142
+
143
+ Call `runReeve` with `isTTY: false` and a captured stdout. Generators write into a temp `cwd`.
144
+
145
+ ```ts
146
+ import { runReeve } from '@avelonjs/cli'
147
+
148
+ const code = await runReeve(['make:model', 'Post'], {
149
+ cwd: tempDir,
150
+ stdout: { write: () => true },
151
+ isTTY: false,
152
+ env: { AVELON_ENV: 'local' },
153
+ })
154
+ ```
155
+
156
+ ```sh
157
+ bun test
158
+ bun run typecheck
159
+ ```
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@avelonjs/cli",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "reeve CLI and TUI for generating and administering Avelon applications.",
6
+ "license": "MIT",
7
+ "author": "Ryan Yannelli <ryanyannelli@gmail.com>",
8
+ "homepage": "https://github.com/yannelli/avelon",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/yannelli/avelon.git",
12
+ "directory": "packages/cli"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/yannelli/avelon/issues"
16
+ },
17
+ "keywords": [
18
+ "avelon",
19
+ "typescript",
20
+ "cli",
21
+ "reeve"
22
+ ],
23
+ "type": "module",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "files": [
28
+ "src",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "bin": {
33
+ "reeve": "./src/bin.ts"
34
+ },
35
+ "exports": {
36
+ ".": "./src/index.ts"
37
+ },
38
+ "scripts": {
39
+ "test": "bun test",
40
+ "typecheck": "tsc --noEmit"
41
+ },
42
+ "dependencies": {
43
+ "@avelonjs/assay": "workspace:*",
44
+ "@avelonjs/bailiff": "workspace:*",
45
+ "@avelonjs/core": "workspace:*",
46
+ "@avelonjs/next": "workspace:*",
47
+ "ink": "5.2.1",
48
+ "react": "18.3.1"
49
+ },
50
+ "devDependencies": {
51
+ "@types/bun": "1.3.14",
52
+ "@types/react": "18.3.26",
53
+ "typescript": "5.9.3"
54
+ },
55
+ "engines": {
56
+ "bun": ">=1.3.14"
57
+ }
58
+ }
package/src/bin.ts ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bun
2
+ import { runReeve } from './index'
3
+
4
+ const code = await runReeve(process.argv.slice(2))
5
+ process.exitCode = code
@@ -0,0 +1,399 @@
1
+ import { readdir, writeFile, mkdir } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { pathToFileURL } from 'node:url'
4
+ import { runSeeds } from '@avelonjs/assay'
5
+ import { parseBailiffArgs, runBailiff } from '@avelonjs/bailiff'
6
+ import {
7
+ DB,
8
+ Errands,
9
+ capabilityMatrix,
10
+ createKernel,
11
+ getConfig,
12
+ jobBoard,
13
+ resetErrands,
14
+ } from '@avelonjs/core'
15
+ import { NextAdapter, Route } from '@avelonjs/next'
16
+
17
+ import { makeDriver } from './driver-scaffold'
18
+ import { runDocsCheck } from './docs-check'
19
+ import {
20
+ makeMigration,
21
+ makeModel,
22
+ publishStubs,
23
+ writeFromStub,
24
+ type GenerateOptions,
25
+ } from './generate'
26
+ import { headerBits, snake, studly, writeln, type ReeveIO } from './io'
27
+
28
+ function flags(argv: readonly string[]): GenerateOptions {
29
+ const blob = argv.filter((arg) => arg.startsWith('-') && !arg.startsWith('--')).join('')
30
+ const option = (name: string): string | undefined => {
31
+ const prefix = `--${name}=`
32
+ const hit = argv.find((arg) => arg.startsWith(prefix))
33
+ return hit?.slice(prefix.length)
34
+ }
35
+ return {
36
+ migration: blob.includes('m') || argv.includes('--migration'),
37
+ factory: blob.includes('f') || argv.includes('--factory'),
38
+ controller: blob.includes('c') || argv.includes('--controller'),
39
+ policy: blob.includes('p') || argv.includes('--policy'),
40
+ ward: blob.includes('w') || argv.includes('--ward'),
41
+ resource: argv.includes('--resource'),
42
+ model: option('model'),
43
+ event: option('event'),
44
+ queued: argv.includes('--queued'),
45
+ }
46
+ }
47
+
48
+ function operand(argv: readonly string[], index = 0): string | undefined {
49
+ return argv.filter((arg) => !arg.startsWith('-'))[index]
50
+ }
51
+
52
+ async function listDir(io: ReeveIO, relative: string): Promise<string[]> {
53
+ try {
54
+ return (await readdir(join(io.cwd, relative))).sort()
55
+ } catch {
56
+ return []
57
+ }
58
+ }
59
+
60
+ async function loadRoutes(io: ReeveIO): Promise<void> {
61
+ const file = join(io.cwd, 'routes/web.ts')
62
+ try {
63
+ await readdir(join(io.cwd, 'routes'))
64
+ Route.reset()
65
+ await import(`${pathToFileURL(file).href}?t=${Date.now()}`)
66
+ } catch {
67
+ /* keep in-process registrations when routes/web.ts is absent */
68
+ }
69
+ }
70
+
71
+ function requireName(argv: readonly string[], usage: string): string {
72
+ const name = operand(argv)
73
+ if (name === undefined) throw new Error(usage)
74
+ return name
75
+ }
76
+
77
+ /** Runs one parsed `reeve` command. Returns a process exit code. */
78
+ export async function runCommand(
79
+ command: string,
80
+ argv: readonly string[],
81
+ io: ReeveIO,
82
+ ): Promise<number> {
83
+ const options = flags(argv)
84
+ switch (command) {
85
+ case 'make:model':
86
+ await makeModel(io, requireName(argv, 'Usage: reeve make:model Name [-mfcpw]'), options)
87
+ return 0
88
+ case 'make:controller': {
89
+ const name = studly(requireName(argv, 'Usage: reeve make:controller Name [--resource]'))
90
+ const file = name.endsWith('Controller') ? name : `${name}Controller`
91
+ await writeFromStub(
92
+ io,
93
+ options.resource ? 'resource-controller.ts' : 'controller.ts',
94
+ `app/Http/Controllers/${file}.ts`,
95
+ file,
96
+ )
97
+ return 0
98
+ }
99
+ case 'make:request': {
100
+ const name = studly(requireName(argv, 'Usage: reeve make:request Name'))
101
+ await writeFromStub(io, 'request.ts', `app/Http/Requests/${name}.ts`, name)
102
+ return 0
103
+ }
104
+ case 'make:policy': {
105
+ const name = studly(requireName(argv, 'Usage: reeve make:policy Name [--model=]'))
106
+ const model = options.model ?? name.replace(/Policy$/, '')
107
+ await writeFromStub(io, 'policy.ts', `app/Policies/${name}.ts`, name, { __MODEL__: model })
108
+ return 0
109
+ }
110
+ case 'make:ward': {
111
+ const name = studly(requireName(argv, 'Usage: reeve make:ward Name [--model=]'))
112
+ const model = options.model ?? name.replace(/Ward$/, '')
113
+ await writeFromStub(io, 'ward.ts', `app/Wards/${name}.ts`, name, { __MODEL__: model })
114
+ return 0
115
+ }
116
+ case 'make:event': {
117
+ const name = studly(requireName(argv, 'Usage: reeve make:event Name'))
118
+ await writeFromStub(io, 'event.ts', `app/Events/${name}.ts`, name)
119
+ return 0
120
+ }
121
+ case 'make:listener': {
122
+ const name = studly(requireName(argv, 'Usage: reeve make:listener Name [--event= --queued]'))
123
+ await writeFromStub(io, 'listener.ts', `app/Listeners/${name}.ts`, name, {
124
+ __EVENT__: options.event ?? 'Event',
125
+ __DELIVERY__: options.queued ? 'queued' : 'sync',
126
+ })
127
+ return 0
128
+ }
129
+ case 'make:errand': {
130
+ const name = studly(requireName(argv, 'Usage: reeve make:errand Name'))
131
+ await writeFromStub(io, 'errand.ts', `app/Errands/${name}.ts`, name)
132
+ return 0
133
+ }
134
+ case 'make:action': {
135
+ const name = studly(requireName(argv, 'Usage: reeve make:action Name'))
136
+ await writeFromStub(io, 'action.ts', `app/Actions/${name}.ts`, name)
137
+ return 0
138
+ }
139
+ case 'make:middleware': {
140
+ const name = studly(requireName(argv, 'Usage: reeve make:middleware Name'))
141
+ await writeFromStub(io, 'middleware.ts', `app/Http/Middleware/${name}.ts`, name)
142
+ return 0
143
+ }
144
+ case 'make:command': {
145
+ const name = studly(requireName(argv, 'Usage: reeve make:command Name'))
146
+ await writeFromStub(io, 'command.ts', `app/Commands/${name}.ts`, name)
147
+ return 0
148
+ }
149
+ case 'make:view': {
150
+ const name = requireName(argv, 'Usage: reeve make:view posts/Index')
151
+ const pascal = name
152
+ .split('/')
153
+ .map((part) => studly(part))
154
+ .join('')
155
+ await writeFromStub(io, 'view.ts', `resources/views/${name}.ts`, pascal, {
156
+ __SLUG__: name.replaceAll('/', '.').toLowerCase(),
157
+ })
158
+ return 0
159
+ }
160
+ case 'make:migration':
161
+ await makeMigration(io, requireName(argv, 'Usage: reeve make:migration name'))
162
+ return 0
163
+ case 'make:driver': {
164
+ const contract = operand(argv, 0)
165
+ const slug = operand(argv, 1)
166
+ if (contract === undefined || slug === undefined)
167
+ throw new Error('Usage: reeve make:driver storage r2')
168
+ await makeDriver(io, contract, slug)
169
+ return 0
170
+ }
171
+ case 'make:package': {
172
+ const slug = snake(requireName(argv, 'Usage: reeve make:package billing'))
173
+ await writeFromStub(io, 'package-readme.md', `packages/${slug}/README.md`, studly(slug), {
174
+ __SLUG__: slug,
175
+ })
176
+ return 0
177
+ }
178
+ case 'make:auth':
179
+ await writeFromStub(
180
+ io,
181
+ 'controller.ts',
182
+ 'app/Http/Controllers/SessionController.ts',
183
+ 'SessionController',
184
+ )
185
+ await writeFromStub(io, 'request.ts', 'app/Http/Requests/SignInRequest.ts', 'SignInRequest')
186
+ await writeFromStub(io, 'view.ts', 'resources/views/auth/SignIn.ts', 'SignIn', {
187
+ __SLUG__: 'auth.sign-in',
188
+ })
189
+ writeln(io, 'Auth routes, controller, request, and view stubbed.')
190
+ return 0
191
+ case 'stub:publish':
192
+ await publishStubs(io)
193
+ return 0
194
+ case 'migrate':
195
+ case 'migrate:status':
196
+ case 'migrate:rollback':
197
+ case 'migrate:fresh':
198
+ return runMigrate(command, argv, io)
199
+ case 'db:seed': {
200
+ const dir = join(io.cwd, 'database/seeds')
201
+ const ran = await runSeeds(dir).catch(() => [])
202
+ writeln(io, ran.length === 0 ? 'No seeds.' : `Seeded ${ran.length} file(s).`)
203
+ return 0
204
+ }
205
+ case 'schema:pull': {
206
+ await mkdir(join(io.cwd, 'database'), { recursive: true })
207
+ await writeFile(
208
+ join(io.cwd, 'database/types.ts'),
209
+ `// Generated by \`reeve schema:pull\`. Do not edit.\nexport interface Database {}\n`,
210
+ )
211
+ writeln(io, 'database/types.ts')
212
+ return 0
213
+ }
214
+ case 'ward:list': {
215
+ const wards = await listDir(io, 'app/Wards')
216
+ const models = await listDir(io, 'app/Models')
217
+ writeln(io, `Models: ${models.join(', ') || '(none)'}`)
218
+ writeln(io, `Wards: ${wards.join(', ') || '(none)'}`)
219
+ return 0
220
+ }
221
+ case 'ward:check': {
222
+ const models = (await listDir(io, 'app/Models')).map((file) => file.replace(/\.ts$/, ''))
223
+ const wards = new Set(
224
+ (await listDir(io, 'app/Wards')).map((file) => file.replace(/\.ts$/, '')),
225
+ )
226
+ const missing = models.filter((model) => !wards.has(`${model}Ward`))
227
+ if (missing.length > 0) {
228
+ writeln(io, `Missing wards: ${missing.join(', ')}`)
229
+ return 1
230
+ }
231
+ writeln(io, 'Wards match models.')
232
+ return 0
233
+ }
234
+ case 'ward:sync':
235
+ writeln(io, 'Compile wards into a migration with the configured database driver.syncWards().')
236
+ return 0
237
+ case 'ward:explain': {
238
+ const model = operand(argv, 0)
239
+ const action = operand(argv, 1) ?? 'read'
240
+ writeln(io, `${model ?? 'Model'} ${action}: inspect app/Wards and the driver SQL compiler.`)
241
+ return 0
242
+ }
243
+ case 'route:list': {
244
+ await loadRoutes(io)
245
+ for (const route of Route.all()) {
246
+ writeln(
247
+ io,
248
+ `${route.method.padEnd(6)} ${route.path.padEnd(24)} ${route.name.padEnd(20)} ${route.controller.name}@${route.action} [${route.middleware.join(',')}]`,
249
+ )
250
+ }
251
+ if (Route.all().length === 0) writeln(io, 'No routes registered.')
252
+ return 0
253
+ }
254
+ case 'route:sync': {
255
+ await loadRoutes(io)
256
+ const adapter = new NextAdapter({ root: io.cwd })
257
+ const result = await adapter.mount(Route.manifest(), { kernel: createKernel() })
258
+ writeln(
259
+ io,
260
+ `Mounted ${result.mounted.length} route(s). Wrote ${result.generated.length} file(s).`,
261
+ )
262
+ return 0
263
+ }
264
+ case 'event:list':
265
+ writeln(
266
+ io,
267
+ 'Events are registered through providers. Inspect app/Providers and app/Listeners.',
268
+ )
269
+ return 0
270
+ case 'config:show':
271
+ try {
272
+ const config = getConfig()
273
+ writeln(
274
+ io,
275
+ JSON.stringify({ name: config.name, drivers: Object.keys(config.drivers) }, null, 2),
276
+ )
277
+ } catch (error) {
278
+ writeln(io, error instanceof Error ? error.message : String(error))
279
+ return 1
280
+ }
281
+ return 0
282
+ case 'capabilities':
283
+ try {
284
+ writeln(io, JSON.stringify(capabilityMatrix(), null, 2))
285
+ } catch (error) {
286
+ writeln(io, error instanceof Error ? error.message : String(error))
287
+ return 1
288
+ }
289
+ return 0
290
+ case 'about': {
291
+ const bits = headerBits(io)
292
+ writeln(
293
+ io,
294
+ `Avelon reeve · ${bits.app} · ${bits.database} · ${bits.adapter} · ${bits.environment}`,
295
+ )
296
+ writeln(io, 'Packages: @avelonjs/cli @avelonjs/next @avelonjs/bailiff @avelonjs/assay @avelonjs/core')
297
+ return 0
298
+ }
299
+ case 'errand:work': {
300
+ const drained = await Errands.work()
301
+ writeln(io, `Drained ${String(drained)} errand(s).`)
302
+ return 0
303
+ }
304
+ case 'errand:failed': {
305
+ const failed = jobBoard().failed()
306
+ writeln(io, failed.length === 0 ? 'No failed errands.' : JSON.stringify(failed, null, 2))
307
+ return 0
308
+ }
309
+ case 'errand:retry': {
310
+ const id = operand(argv, 0)
311
+ if (id === undefined || id === 'all') {
312
+ for (const failed of jobBoard().failed()) jobBoard().retry(failed.id)
313
+ writeln(io, 'Retried all failed errands.')
314
+ return 0
315
+ }
316
+ jobBoard().retry(id)
317
+ writeln(io, `Retried ${id}.`)
318
+ return 0
319
+ }
320
+ case 'errand:flush':
321
+ jobBoard().flush()
322
+ resetErrands()
323
+ writeln(io, 'Flushed errands.')
324
+ return 0
325
+ case 'bailiff': {
326
+ const parsed = parseBailiffArgs(argv)
327
+ const result = await runBailiff({ ...parsed, cwd: io.cwd })
328
+ io.stdout.write(result.output)
329
+ return result.exitCode
330
+ }
331
+ case 'docs:check':
332
+ return runDocsCheck(argv, io)
333
+ case 'doctor':
334
+ return runDoctor(io)
335
+ case 'tinker':
336
+ return runTinker(argv, io)
337
+ default:
338
+ writeln(io, `Unknown command '${command}'.`)
339
+ return 1
340
+ }
341
+ }
342
+
343
+ async function runMigrate(command: string, argv: readonly string[], io: ReeveIO): Promise<number> {
344
+ const env = headerBits(io).environment
345
+ if (command === 'migrate:fresh' && env !== 'local' && io.confirm !== env) {
346
+ writeln(io, `Refusing migrate:fresh on ${env}. Pass the environment name as confirmation.`)
347
+ return 1
348
+ }
349
+ try {
350
+ if (command === 'migrate:status') writeln(io, JSON.stringify(await DB.status(), null, 2))
351
+ else if (command === 'migrate:rollback')
352
+ writeln(io, JSON.stringify(await DB.rollback(), null, 2))
353
+ else if (command === 'migrate:fresh') {
354
+ await DB.rollback(100).catch(() => undefined)
355
+ writeln(io, JSON.stringify(await DB.apply(), null, 2))
356
+ if (argv.includes('--seed')) await runCommand('db:seed', [], io)
357
+ } else writeln(io, JSON.stringify(await DB.apply(), null, 2))
358
+ return 0
359
+ } catch (error) {
360
+ writeln(io, error instanceof Error ? error.message : String(error))
361
+ return 1
362
+ }
363
+ }
364
+
365
+ async function runDoctor(io: ReeveIO): Promise<number> {
366
+ writeln(io, 'Doctor')
367
+ const bailiff = await runBailiff({ cwd: io.cwd, files: [io.cwd] })
368
+ writeln(
369
+ io,
370
+ bailiff.errorCount === 0 ? 'Bailiff: clean' : `Bailiff: ${bailiff.errorCount} error(s)`,
371
+ )
372
+ const wards = await runCommand('ward:check', [], io)
373
+ writeln(io, 'Types: run schema:pull and compare models in CI.')
374
+ writeln(io, `Env: ${headerBits(io).environment}`)
375
+ try {
376
+ writeln(io, 'Capabilities:')
377
+ writeln(io, JSON.stringify(capabilityMatrix(), null, 2))
378
+ } catch {
379
+ writeln(io, 'Capabilities: config is not defined.')
380
+ }
381
+ return bailiff.exitCode === 0 && wards === 0 ? 0 : 1
382
+ }
383
+
384
+ async function runTinker(argv: readonly string[], io: ReeveIO): Promise<number> {
385
+ const executeIndex = argv.indexOf('--execute')
386
+ const source = executeIndex >= 0 ? argv[executeIndex + 1] : undefined
387
+ if (source === undefined) {
388
+ writeln(
389
+ io,
390
+ 'Tinker. Pass --execute "expression" in CI. Models, DB, and helpers preload in a TTY REPL.',
391
+ )
392
+ return 0
393
+ }
394
+ const result = (0, eval)(source)
395
+ writeln(io, String(await result))
396
+ return 0
397
+ }
398
+
399
+ export { flags }