@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 +21 -0
- package/README.md +159 -0
- package/package.json +58 -0
- package/src/bin.ts +5 -0
- package/src/commands.ts +399 -0
- package/src/docs-check.ts +415 -0
- package/src/driver-scaffold.ts +524 -0
- package/src/generate.ts +306 -0
- package/src/index.ts +42 -0
- package/src/io.ts +65 -0
- package/src/menu.ts +53 -0
- package/src/tui.tsx +77 -0
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
package/src/commands.ts
ADDED
|
@@ -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 }
|