@strav/cli 0.4.31 → 1.0.0-alpha.4

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 (63) hide show
  1. package/package.json +17 -41
  2. package/src/binder.ts +88 -0
  3. package/src/command.ts +297 -0
  4. package/src/config_list.ts +42 -0
  5. package/src/config_show.ts +50 -0
  6. package/src/console_provider.ts +46 -0
  7. package/src/exit_codes.ts +26 -0
  8. package/src/index.ts +60 -2
  9. package/src/key_generate.ts +66 -0
  10. package/src/make/index.ts +17 -0
  11. package/src/make/make_command_file.ts +27 -0
  12. package/src/make/make_controller.ts +24 -0
  13. package/src/make/make_factory.ts +25 -0
  14. package/src/make/make_job.ts +25 -0
  15. package/src/make/make_mail.ts +27 -0
  16. package/src/make/make_middleware.ts +23 -0
  17. package/src/make/make_migration.ts +48 -0
  18. package/src/make/make_model.ts +91 -0
  19. package/src/make/make_notification.ts +23 -0
  20. package/src/make/make_policy.ts +24 -0
  21. package/src/make/make_provider.ts +29 -0
  22. package/src/make/make_repository.ts +30 -0
  23. package/src/make/make_request.ts +24 -0
  24. package/src/make/make_seeder.ts +23 -0
  25. package/src/make/make_test.ts +22 -0
  26. package/src/make_command.ts +69 -0
  27. package/src/run_cli.ts +121 -0
  28. package/src/scaffold_console_provider.ts +45 -0
  29. package/src/signature.ts +171 -0
  30. package/src/subset_boot.ts +51 -0
  31. package/src/util_console_provider.ts +18 -0
  32. package/src/cli/bootstrap.ts +0 -82
  33. package/src/cli/command_loader.ts +0 -180
  34. package/src/cli/index.ts +0 -3
  35. package/src/cli/strav.ts +0 -13
  36. package/src/commands/db_seed.ts +0 -77
  37. package/src/commands/db_setup_roles.ts +0 -101
  38. package/src/commands/generate_api.ts +0 -93
  39. package/src/commands/generate_key.ts +0 -47
  40. package/src/commands/generate_models.ts +0 -49
  41. package/src/commands/generate_seeder.ts +0 -68
  42. package/src/commands/migration_compare.ts +0 -167
  43. package/src/commands/migration_fresh.ts +0 -148
  44. package/src/commands/migration_generate.ts +0 -84
  45. package/src/commands/migration_rollback.ts +0 -54
  46. package/src/commands/migration_run.ts +0 -45
  47. package/src/commands/package_install.ts +0 -161
  48. package/src/commands/queue_flush.ts +0 -35
  49. package/src/commands/queue_retry.ts +0 -34
  50. package/src/commands/queue_work.ts +0 -101
  51. package/src/commands/scheduler_work.ts +0 -46
  52. package/src/commands/tenant_create.ts +0 -35
  53. package/src/commands/tenant_delete.ts +0 -64
  54. package/src/commands/tenant_list.ts +0 -39
  55. package/src/config/loader.ts +0 -50
  56. package/src/generators/api_generator.ts +0 -1035
  57. package/src/generators/config.ts +0 -113
  58. package/src/generators/doc_generator.ts +0 -996
  59. package/src/generators/index.ts +0 -11
  60. package/src/generators/model_generator.ts +0 -596
  61. package/src/generators/route_generator.ts +0 -187
  62. package/src/generators/test_generator.ts +0 -1667
  63. package/tsconfig.json +0 -5
package/package.json CHANGED
@@ -1,52 +1,28 @@
1
1
  {
2
2
  "name": "@strav/cli",
3
- "version": "0.4.31",
3
+ "version": "1.0.0-alpha.4",
4
+ "description": "Strav CLI layer — Command base, signature parser, ConsoleProvider, interactive prompts on top of @strav/kernel's ConsoleKernel",
4
5
  "type": "module",
5
- "description": "CLI framework and code generators for the Strav framework",
6
- "license": "MIT",
7
- "keywords": [
8
- "bun",
9
- "framework",
10
- "typescript",
11
- "strav",
12
- "cli"
13
- ],
14
- "strav": {
15
- "commands": "src/commands"
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts"
16
10
  },
17
11
  "files": [
18
- "src/",
19
- "package.json",
20
- "tsconfig.json",
21
- "CHANGELOG.md"
12
+ "src",
13
+ "README.md"
22
14
  ],
23
- "exports": {
24
- ".": "./src/index.ts",
25
- "./cli": "./src/cli/index.ts",
26
- "./cli/*": "./src/cli/*.ts",
27
- "./commands": "./src/commands/index.ts",
28
- "./commands/*": "./src/commands/*.ts",
29
- "./config/*": "./src/config/*.ts",
30
- "./generators": "./src/generators/index.ts",
31
- "./generators/*": "./src/generators/*.ts"
15
+ "engines": {
16
+ "bun": ">=1.3.14"
32
17
  },
33
- "bin": {
34
- "strav": "./src/cli/strav.ts"
35
- },
36
- "peerDependencies": {
37
- "@strav/kernel": "0.4.31",
38
- "@strav/http": "0.4.31",
39
- "@strav/database": "0.4.31",
40
- "@strav/queue": "0.4.31",
41
- "@strav/signal": "0.4.31"
18
+ "publishConfig": {
19
+ "access": "public"
42
20
  },
43
21
  "dependencies": {
44
- "chalk": "^5.6.2",
45
- "commander": "^14.0.3",
46
- "prettier": "^3.8.1"
22
+ "@strav/kernel": "1.0.0-alpha.4"
23
+ },
24
+ "peerDependencies": {
25
+ "@types/bun": ">=1.3.14"
47
26
  },
48
- "scripts": {
49
- "test": "bun test tests/",
50
- "typecheck": "tsc --noEmit"
51
- }
27
+ "devDependencies": null
52
28
  }
package/src/binder.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Argv binder — given a parsed `Signature` and the kernel's `ParsedArgv`,
3
+ * produce the `{ args, flags }` shape `Command.execute()` receives.
4
+ *
5
+ * Validation is loud-fail (`UsageError`) so wrong invocations land an exit
6
+ * code 2 with a clear message rather than a `TypeError` from `args.foo`:
7
+ * - Required positional missing → "missing argument: <name>".
8
+ * - Extra positional after all declared → "unexpected argument: <value>".
9
+ * - String flag with no value (bare `--output`) → "flag --output requires a value".
10
+ *
11
+ * Unknown flags (passed but not declared) are *retained* in the output —
12
+ * commands may inspect them via `flags[<unknown>]`, which is the escape
13
+ * hatch for ad-hoc flags without growing the signature. They never error.
14
+ */
15
+
16
+ import { type ParsedArgv, StravError, type StravErrorOptions } from '@strav/kernel'
17
+ import type { Signature } from './signature.ts'
18
+
19
+ /**
20
+ * Thrown when argv doesn't match a `Signature` — missing required positional,
21
+ * unexpected extra positional, or a value-flag with no value. Status 2 mirrors
22
+ * the POSIX "usage error" exit-code convention; the CliConsoleKernel surfaces
23
+ * it as exit code 2.
24
+ */
25
+ export class UsageError extends StravError {
26
+ constructor(message: string, options: StravErrorOptions = {}) {
27
+ super(message, { code: 'cli.usage', status: 2 }, options)
28
+ }
29
+ }
30
+
31
+ export interface BoundArgv {
32
+ args: Record<string, string | undefined>
33
+ flags: Record<string, string | boolean>
34
+ }
35
+
36
+ export function bindArgv(signature: Signature, parsed: ParsedArgv): BoundArgv {
37
+ const args: Record<string, string | undefined> = {}
38
+ const flags: Record<string, string | boolean> = {}
39
+
40
+ // ─── positionals ────────────────────────────────────────────────────────────
41
+ for (let i = 0; i < signature.args.length; i++) {
42
+ const spec = signature.args[i]
43
+ if (!spec) continue
44
+ const value = parsed.args[i]
45
+ if (value === undefined) {
46
+ if (!spec.optional) {
47
+ throw new UsageError(`missing argument: <${spec.name}>`)
48
+ }
49
+ args[spec.name] = undefined
50
+ } else {
51
+ args[spec.name] = value
52
+ }
53
+ }
54
+
55
+ if (parsed.args.length > signature.args.length) {
56
+ const extra = parsed.args[signature.args.length]
57
+ throw new UsageError(`unexpected argument: "${extra}"`)
58
+ }
59
+
60
+ // ─── declared flags ─────────────────────────────────────────────────────────
61
+ const declaredFlagNames = new Set<string>()
62
+ for (const spec of signature.flags) {
63
+ declaredFlagNames.add(spec.name)
64
+ const raw = parsed.flags[spec.name]
65
+ if (raw === undefined) {
66
+ flags[spec.name] = spec.default
67
+ continue
68
+ }
69
+ if (spec.kind === 'boolean') {
70
+ // Bare `--flag` parses as `true`; an explicit `--flag=foo` also flips
71
+ // it to true (the value is meaningless for a boolean — we ignore it
72
+ // rather than error so `--verbose=1` from CI scripts doesn't surprise).
73
+ flags[spec.name] = raw !== false
74
+ } else {
75
+ if (raw === true) {
76
+ throw new UsageError(`flag --${spec.name} requires a value`)
77
+ }
78
+ flags[spec.name] = raw
79
+ }
80
+ }
81
+
82
+ // ─── undeclared flags (pass through) ────────────────────────────────────────
83
+ for (const [name, value] of Object.entries(parsed.flags)) {
84
+ if (!declaredFlagNames.has(name)) flags[name] = value
85
+ }
86
+
87
+ return { args, flags }
88
+ }
package/src/command.ts ADDED
@@ -0,0 +1,297 @@
1
+ /**
2
+ * `Command` — base class for CLI commands.
3
+ *
4
+ * Wraps `@strav/kernel`'s minimal `Command` (which exposes `handle(ctx)` with
5
+ * positional `args: readonly string[]`) and adds:
6
+ * - The signature DSL (`'cmd {arg} {arg?} {--flag=default}'`), parsed once
7
+ * at registration time. `execute({ args, flags })` receives the bound
8
+ * values keyed by name.
9
+ * - Output helpers as instance methods (`this.info` / `this.warn` / …) so
10
+ * command bodies stay declarative without threading the writer around.
11
+ * - Interactive prompts (`this.confirm`, `this.ask`, `this.choice`,
12
+ * `this.table`) backed by Bun's stdin reader.
13
+ * - `static providers?: string[]` — names of providers to boot. Resolved
14
+ * by the `CliConsoleKernel` against the default list (transitive deps
15
+ * auto-included). Omit for "boot everything"; `[]` for "boot nothing".
16
+ *
17
+ * Subclasses implement `execute(args)` and return a number (exit code) or
18
+ * void (treated as 0). Errors thrown from `execute()` bubble up to the
19
+ * kernel, which surfaces them via stderr + returns exit 1.
20
+ */
21
+
22
+ import {
23
+ type Application,
24
+ type CommandContext,
25
+ type CommandResult,
26
+ Command as KernelCommand,
27
+ } from '@strav/kernel'
28
+ import { bindArgv, UsageError } from './binder.ts'
29
+ import { ExitCode } from './exit_codes.ts'
30
+ import { parseSignature, type Signature } from './signature.ts'
31
+
32
+ export interface ExecuteArgs {
33
+ /** Positional args, keyed by name (from `{name}` in the signature). */
34
+ args: Record<string, string | undefined>
35
+ /**
36
+ * Parsed flags, keyed by name. Boolean flags resolve to `false` by default;
37
+ * string flags to their declared default. Undeclared flags pass through.
38
+ */
39
+ flags: Record<string, string | boolean>
40
+ }
41
+
42
+ /**
43
+ * Static metadata every CLI command must declare. The richer `signature` is
44
+ * the source of truth for argv binding — the bare command name is derived
45
+ * from its first token. `description` is the one-liner shown by `list`.
46
+ */
47
+ export interface CliCommandMeta {
48
+ readonly signature: string
49
+ readonly description: string
50
+ /**
51
+ * Subset of provider names to boot for this command. Omitted = full default
52
+ * list. `[]` = none. See `docs/cli/guides/subset-boot.md`.
53
+ */
54
+ readonly providers?: readonly string[]
55
+ }
56
+
57
+ export abstract class Command extends KernelCommand {
58
+ /** Lazily-built `Signature` from `static signature`. Re-used across handle() calls. */
59
+ private static signatureCache: WeakMap<typeof Command, Signature> = new WeakMap()
60
+
61
+ static parsedSignature(): Signature {
62
+ // `this` here refers to the SUBCLASS that called .parsedSignature(), which
63
+ // is what we want — the cache keys per-subclass so each command parses its
64
+ // signature once. Using the literal `Command` name would collapse all
65
+ // subclasses to the same key.
66
+ // biome-ignore lint/complexity/noThisInStatic: intentional subclass-aware cache
67
+ const Class = this as typeof Command
68
+ const cached = Command.signatureCache.get(Class)
69
+ if (cached) return cached
70
+ const raw = (Class as { signature?: unknown }).signature
71
+ if (typeof raw !== 'string') {
72
+ throw new Error(
73
+ `${Class.name}: missing static \`signature\` string — every Command needs one`,
74
+ )
75
+ }
76
+ const parsed = parseSignature(raw)
77
+ Command.signatureCache.set(Class, parsed)
78
+ return parsed
79
+ }
80
+
81
+ /** Kernel calls this with positional argv; we re-bind via the signature and call `execute()`. */
82
+ // biome-ignore lint/suspicious/noConfusingVoidType: matches kernel's CommandResult — void means "treat as 0"
83
+ override async handle(ctx: CommandContext): Promise<number | void> {
84
+ const Class = this.constructor as typeof Command
85
+ const signature = Class.parsedSignature()
86
+ this.output = ctx.out
87
+ this.app = ctx.app
88
+
89
+ // `<cmd> --help` short-circuits to per-command help.
90
+ if (ctx.flags.help === true || ctx.flags.h === true) {
91
+ this.printHelp()
92
+ return 0
93
+ }
94
+
95
+ // UsageError → POSIX exit code 2 + per-command usage line on stderr.
96
+ // The catch covers BOTH the binder (signature mismatch) AND execute()
97
+ // itself — commands often validate flag values after binding (e.g.,
98
+ // `--batch=not-a-number`) and throw UsageError. Other exceptions still
99
+ // get the kernel's generic exit-1 + stack-trace treatment.
100
+ try {
101
+ const bound = bindArgv(signature, {
102
+ command: signature.name,
103
+ args: [...ctx.args],
104
+ flags: { ...ctx.flags },
105
+ })
106
+ return await this.execute(bound)
107
+ } catch (err) {
108
+ if (err instanceof UsageError) {
109
+ this.error(err.message)
110
+ this.error(`Usage: ${formatUsage(signature)}`)
111
+ return ExitCode.UsageError
112
+ }
113
+ throw err
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Print the help message for this command. Composed from `static description`
119
+ * + the parsed signature + the optional `help()` override.
120
+ */
121
+ protected printHelp(): void {
122
+ const Class = this.constructor as typeof Command
123
+ const signature = Class.parsedSignature()
124
+ const description = (Class as { description?: unknown }).description
125
+ if (typeof description === 'string' && description.length > 0) {
126
+ this.line(description)
127
+ this.line()
128
+ }
129
+ this.line(`Usage: ${formatUsage(signature)}`)
130
+ if (signature.args.length > 0) {
131
+ this.line()
132
+ this.line('Arguments:')
133
+ for (const arg of signature.args) {
134
+ this.line(` ${arg.name}${arg.optional ? '?' : ''}`)
135
+ }
136
+ }
137
+ if (signature.flags.length > 0) {
138
+ this.line()
139
+ this.line('Flags:')
140
+ for (const flag of signature.flags) {
141
+ const suffix = flag.kind === 'string' ? `=<value> (default: ${flag.default})` : ' (boolean)'
142
+ this.line(` --${flag.name}${suffix}`)
143
+ }
144
+ }
145
+ const extra = this.help()
146
+ if (extra) {
147
+ this.line()
148
+ this.line(extra)
149
+ }
150
+ }
151
+
152
+ /** Set by `handle()` before calling `execute()`. */
153
+ protected output!: CommandContext['out']
154
+ /**
155
+ * The booted `Application` — set by `handle()` before `execute()` runs.
156
+ * Use it to resolve services that don't fit through constructor injection
157
+ * (e.g., when a command's deps are dynamic, or when subset boot binds a
158
+ * service that the command's static `@inject()` doesn't know about).
159
+ */
160
+ protected app!: Application
161
+
162
+ /** Implement the command. Return a number for an explicit exit code, void for 0. */
163
+ abstract execute(argv: ExecuteArgs): CommandResult
164
+
165
+ // ─── output helpers — thin wrappers around ConsoleOutput ─────────────────────
166
+
167
+ protected info(msg: string): void {
168
+ this.output.info(msg)
169
+ }
170
+ protected success(msg: string): void {
171
+ this.output.success(msg)
172
+ }
173
+ protected warn(msg: string): void {
174
+ this.output.warn(msg)
175
+ }
176
+ protected error(msg: string): void {
177
+ this.output.error(msg)
178
+ }
179
+ protected line(msg = ''): void {
180
+ this.output.line(msg)
181
+ }
182
+
183
+ /**
184
+ * Print a table with aligned columns. Headers + every row are stringified;
185
+ * column widths derived from the widest cell. Apps that need fancier
186
+ * tables write to `this.output` directly.
187
+ */
188
+ protected table(headers: readonly string[], rows: readonly (readonly string[])[]): void {
189
+ const widths = headers.map((h, i) =>
190
+ Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length)),
191
+ )
192
+ const fmt = (cells: readonly string[]) =>
193
+ cells.map((c, i) => (c ?? '').padEnd(widths[i] ?? 0)).join(' ')
194
+ this.line(fmt(headers))
195
+ this.line(widths.map((w) => '-'.repeat(w)).join(' '))
196
+ for (const row of rows) this.line(fmt(row))
197
+ }
198
+
199
+ /**
200
+ * Yes/no prompt. Accepts `y`/`yes` (case-insensitive) as `true`; anything
201
+ * else (including empty input) as `false`. Use `defaultYes: true` to flip
202
+ * empty input to `true`.
203
+ */
204
+ protected async confirm(question: string, opts: { defaultYes?: boolean } = {}): Promise<boolean> {
205
+ const suffix = opts.defaultYes ? ' [Y/n] ' : ' [y/N] '
206
+ this.output.write(`${question}${suffix}`)
207
+ const answer = (await readStdinLine()).trim().toLowerCase()
208
+ if (answer === '') return opts.defaultYes ?? false
209
+ return answer === 'y' || answer === 'yes'
210
+ }
211
+
212
+ /** Free-text prompt. Returns the default when the user just hits enter. */
213
+ protected async ask(question: string, defaultValue?: string): Promise<string> {
214
+ const suffix = defaultValue !== undefined ? ` [${defaultValue}] ` : ' '
215
+ this.output.write(`${question}${suffix}`)
216
+ const answer = (await readStdinLine()).trim()
217
+ if (answer === '') return defaultValue ?? ''
218
+ return answer
219
+ }
220
+
221
+ /**
222
+ * Multiple-choice prompt — prints options as a numbered list, accepts
223
+ * either the number or the option text. Re-prompts on bad input.
224
+ */
225
+ protected async choice<T extends string>(
226
+ question: string,
227
+ options: readonly T[],
228
+ defaultValue?: T,
229
+ ): Promise<T> {
230
+ if (options.length === 0) {
231
+ throw new Error('choice(): options must be non-empty')
232
+ }
233
+ while (true) {
234
+ this.line(question)
235
+ for (let i = 0; i < options.length; i++) {
236
+ const marker = options[i] === defaultValue ? ' (default)' : ''
237
+ this.line(` ${i + 1}. ${options[i]}${marker}`)
238
+ }
239
+ this.output.write('> ')
240
+ const raw = (await readStdinLine()).trim()
241
+ if (raw === '' && defaultValue !== undefined) return defaultValue
242
+ const asNum = Number.parseInt(raw, 10)
243
+ if (Number.isInteger(asNum) && asNum >= 1 && asNum <= options.length) {
244
+ return options[asNum - 1] as T
245
+ }
246
+ const match = options.find((o) => o === raw)
247
+ if (match !== undefined) return match
248
+ this.warn(`Invalid choice: "${raw}"`)
249
+ }
250
+ }
251
+
252
+ /** Optional per-command help text. Override to provide examples / extra detail. */
253
+ help(): string | undefined {
254
+ return undefined
255
+ }
256
+ }
257
+
258
+ /** Render `cmd {arg} {arg?} [--flag=…]` from a parsed signature for help output. */
259
+ function formatUsage(signature: Signature): string {
260
+ const parts: string[] = [signature.name]
261
+ for (const arg of signature.args) parts.push(arg.optional ? `[${arg.name}]` : `<${arg.name}>`)
262
+ for (const flag of signature.flags) {
263
+ parts.push(flag.kind === 'string' ? `[--${flag.name}=…]` : `[--${flag.name}]`)
264
+ }
265
+ return parts.join(' ')
266
+ }
267
+
268
+ /**
269
+ * Read one line from stdin. Returns the empty string at EOF. Decoded as UTF-8.
270
+ *
271
+ * Bun's `console.readLine()` blocks the event loop; this uses the `for await`
272
+ * stream interface so other async work (e.g., a spinner) can run alongside.
273
+ */
274
+ async function readStdinLine(): Promise<string> {
275
+ let buf = ''
276
+ const decoder = new TextDecoder()
277
+ for await (const chunk of Bun.stdin.stream()) {
278
+ buf += decoder.decode(chunk, { stream: true })
279
+ const newline = buf.indexOf('\n')
280
+ if (newline !== -1) {
281
+ return buf.slice(0, newline).replace(/\r$/, '')
282
+ }
283
+ }
284
+ buf += decoder.decode()
285
+ return buf.replace(/\r$/, '')
286
+ }
287
+
288
+ /**
289
+ * Constructor type the `CliConsoleKernel` accepts. Combines the runtime
290
+ * constructor (so the container can `make()` it) with the static metadata
291
+ * the kernel reads at registration / dispatch time.
292
+ */
293
+ export type CliCommandClass<T extends Command = Command> = (new (
294
+ // biome-ignore lint/suspicious/noExplicitAny: structural ctor — Command subclasses vary in injected deps
295
+ ...args: any[]
296
+ ) => T) &
297
+ CliCommandMeta
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `bun strav config:list` — list every top-level config namespace.
3
+ *
4
+ * Useful for sanity-checking what `ConfigProvider` actually loaded —
5
+ * e.g., confirming that `config.auth` is present after wiring a new
6
+ * provider, or seeing which app sections are populated in production
7
+ * vs. development.
8
+ *
9
+ * Pairs with `config:show <key>` for drilling in. Outputs one
10
+ * namespace per line, alphabetically sorted, with a trailing `(empty)`
11
+ * marker for namespaces whose value is `undefined` / `null` / an empty
12
+ * object.
13
+ */
14
+
15
+ import { ConfigRepository } from '@strav/kernel'
16
+ import { Command } from './command.ts'
17
+ import { ExitCode } from './exit_codes.ts'
18
+
19
+ export class ConfigList extends Command {
20
+ static signature = 'config:list'
21
+ static description = 'List top-level config namespaces (app, auth, database, …).'
22
+ static providers = ['config']
23
+
24
+ override execute(): number {
25
+ const all = this.app.resolve(ConfigRepository).all()
26
+ const keys = Object.keys(all).sort()
27
+
28
+ if (keys.length === 0) {
29
+ this.info('No config namespaces are loaded.')
30
+ return ExitCode.Success
31
+ }
32
+
33
+ for (const key of keys) {
34
+ const value = all[key]
35
+ const empty =
36
+ value == null ||
37
+ (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0)
38
+ this.line(empty ? `${key} (empty)` : key)
39
+ }
40
+ return ExitCode.Success
41
+ }
42
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `bun strav config:show <key>` — print a single config value by dot-path.
3
+ *
4
+ * Resolves the value through `ConfigRepository.get`, which means apps see
5
+ * the same lookup semantics here as in their providers — including any
6
+ * env-var indirection / config decorators applied at boot.
7
+ *
8
+ * `--json` prints a compact JSON encoding (objects, arrays, numbers, …).
9
+ * The default formatter handles scalars and objects sensibly so common
10
+ * lookups (`config:show app.url`) "just work" without `--json`.
11
+ *
12
+ * Secret values: this command does **no** redaction. Apps that want to
13
+ * audit secret access from the CLI should wrap config access in their
14
+ * own provider or rely on the logger's redaction config when piping the
15
+ * output to logs.
16
+ */
17
+
18
+ import { ConfigRepository } from '@strav/kernel'
19
+ import { Command, type ExecuteArgs } from './command.ts'
20
+ import { ExitCode } from './exit_codes.ts'
21
+
22
+ export class ConfigShow extends Command {
23
+ static signature = 'config:show {key} {--json}'
24
+ static description = 'Print a config value by dot-path (e.g. config:show app.url).'
25
+ static providers = ['config']
26
+
27
+ override execute({ args, flags }: ExecuteArgs): number {
28
+ const key = args.key as string
29
+ const value = this.app.resolve(ConfigRepository).get(key)
30
+
31
+ if (value === undefined) {
32
+ this.error(`Config key not set: ${key}`)
33
+ return ExitCode.DataError
34
+ }
35
+
36
+ if (flags.json === true) {
37
+ this.line(JSON.stringify(value))
38
+ return ExitCode.Success
39
+ }
40
+
41
+ if (value === null || typeof value !== 'object') {
42
+ this.line(String(value))
43
+ return ExitCode.Success
44
+ }
45
+
46
+ // Objects / arrays — pretty-print so nested config is readable.
47
+ this.line(JSON.stringify(value, null, 2))
48
+ return ExitCode.Success
49
+ }
50
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * `ConsoleProvider` — declares a set of console `Command` classes the app
3
+ * registers with the `CliConsoleKernel`.
4
+ *
5
+ * Apps subclass it once in `app/providers/console_provider.ts`:
6
+ *
7
+ * ```ts
8
+ * import { ConsoleProvider } from '@strav/cli'
9
+ * import { TenantBackup } from '../console/commands/tenant_backup.ts'
10
+ *
11
+ * export class AppConsoleProvider extends ConsoleProvider {
12
+ * override readonly name = 'console.app'
13
+ * override readonly commands = [TenantBackup] as const
14
+ * }
15
+ * ```
16
+ *
17
+ * `runCli({ defaultProviders })` walks the provider list once, pulls every
18
+ * `commands` array off subclasses, and hands the union to the
19
+ * `CliConsoleKernel`. Apps don't have to wire commands a second time.
20
+ *
21
+ * The provider doesn't do anything at `register()` — command collection
22
+ * happens before the app boots so subset-boot (`static providers`) can
23
+ * pre-filter the provider list.
24
+ */
25
+
26
+ import { ServiceProvider } from '@strav/kernel'
27
+ import type { CliCommandClass } from './command.ts'
28
+
29
+ export abstract class ConsoleProvider extends ServiceProvider {
30
+ /** Commands this provider contributes. Override in subclasses. */
31
+ readonly commands: readonly CliCommandClass[] = []
32
+ }
33
+
34
+ /**
35
+ * Collect every command across an ordered provider list. Order matches the
36
+ * iteration; duplicate-by-signature is caught later by `CliConsoleKernel`.
37
+ */
38
+ export function collectCommands(providers: readonly ServiceProvider[]): CliCommandClass[] {
39
+ const out: CliCommandClass[] = []
40
+ for (const provider of providers) {
41
+ if (provider instanceof ConsoleProvider) {
42
+ out.push(...provider.commands)
43
+ }
44
+ }
45
+ return out
46
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * POSIX-aligned exit-code constants. Commands return one of these (or any
3
+ * number ≥ 100 for command-specific failures); the framework defaults to
4
+ * 0 when `execute()` returns `void` and 1 when an error escapes.
5
+ *
6
+ * Apps reach for these via `import { ExitCode } from '@strav/cli'` so the
7
+ * call site reads `return ExitCode.UsageError` instead of a magic `2`.
8
+ */
9
+
10
+ export const ExitCode = {
11
+ /** Success. */
12
+ Success: 0,
13
+ /** Unspecified failure. The kernel uses this when an exception escapes. */
14
+ GenericFailure: 1,
15
+ /**
16
+ * Argv didn't match the command's signature — missing positional, extra
17
+ * positional, value-flag with no value. Thrown by the binder as `UsageError`.
18
+ */
19
+ UsageError: 2,
20
+ /** Configuration is invalid or missing (e.g., `APP_KEY` unset). */
21
+ ConfigError: 64,
22
+ /** Data dependency unavailable (DB unreachable, file missing). */
23
+ DataError: 65,
24
+ } as const
25
+
26
+ export type ExitCodeValue = (typeof ExitCode)[keyof typeof ExitCode]
package/src/index.ts CHANGED
@@ -1,2 +1,60 @@
1
- export * from './cli/index.ts'
2
- export * from './generators/index.ts'
1
+ // Public API of @strav/cli.
2
+ //
3
+ // Adds the signature DSL (`'cmd {arg} {arg?} {--flag=default}'`), a richer
4
+ // `Command` base with output helpers + interactive prompts, `ConsoleProvider`
5
+ // for command registration, subset boot (`static providers = [...]`), and
6
+ // `runCli` as the entry point for `bin/strav.ts`.
7
+ //
8
+ // Built on @strav/kernel's `Command` / `ConsoleKernel` — those stay as the
9
+ // minimal infrastructure; @strav/cli is the productive developer surface.
10
+ //
11
+ // Deferred to post-M4 slices (each lands when its underlying package does):
12
+ // - `cache:*` — needs `@strav/cache` (not yet a package)
13
+ // - `tenant:*` — needs a generic tenant-CRUD convention or app-side hooks
14
+ // - `plugin:install` — needs `package.json#strav` metadata convention
15
+
16
+ export { type BoundArgv, bindArgv, UsageError } from './binder.ts'
17
+ export {
18
+ type CliCommandClass,
19
+ type CliCommandMeta,
20
+ Command,
21
+ type ExecuteArgs,
22
+ } from './command.ts'
23
+ export { ConsoleProvider, collectCommands } from './console_provider.ts'
24
+ export { ConfigList } from './config_list.ts'
25
+ export { ConfigShow } from './config_show.ts'
26
+ export { ExitCode, type ExitCodeValue } from './exit_codes.ts'
27
+ export { KeyGenerate } from './key_generate.ts'
28
+ export {
29
+ MakeCommandFile,
30
+ MakeController,
31
+ MakeFactory,
32
+ MakeJob,
33
+ MakeMail,
34
+ MakeMiddleware,
35
+ MakeMigration,
36
+ MakeModel,
37
+ MakeNotification,
38
+ MakePolicy,
39
+ MakeProvider,
40
+ MakeRepository,
41
+ MakeRequest,
42
+ MakeSeeder,
43
+ MakeTest,
44
+ } from './make/index.ts'
45
+ export {
46
+ camel,
47
+ MakeCommand as MakeCommandBase,
48
+ pascal,
49
+ snake,
50
+ } from './make_command.ts'
51
+ export { type RunCliOptions, runCli } from './run_cli.ts'
52
+ export { ScaffoldConsoleProvider } from './scaffold_console_provider.ts'
53
+ export {
54
+ type FlagSpec,
55
+ type PositionalArg,
56
+ parseSignature,
57
+ type Signature,
58
+ } from './signature.ts'
59
+ export { selectProviders } from './subset_boot.ts'
60
+ export { UtilConsoleProvider } from './util_console_provider.ts'