@c9up/ream 0.1.26 → 0.1.27

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 (179) hide show
  1. package/README.md +65 -0
  2. package/dist/Ignitor.d.ts +115 -7
  3. package/dist/Ignitor.d.ts.map +1 -1
  4. package/dist/Ignitor.js +233 -37
  5. package/dist/Ignitor.js.map +1 -1
  6. package/dist/console/BaseCommand.d.ts +217 -0
  7. package/dist/console/BaseCommand.d.ts.map +1 -0
  8. package/dist/console/BaseCommand.js +340 -0
  9. package/dist/console/BaseCommand.js.map +1 -0
  10. package/dist/console/Console.d.ts +86 -0
  11. package/dist/console/Console.d.ts.map +1 -0
  12. package/dist/console/Console.js +134 -0
  13. package/dist/console/Console.js.map +1 -0
  14. package/dist/console/ExceptionHandler.d.ts +28 -0
  15. package/dist/console/ExceptionHandler.d.ts.map +1 -0
  16. package/dist/console/ExceptionHandler.js +76 -0
  17. package/dist/console/ExceptionHandler.js.map +1 -0
  18. package/dist/console/HelpCommand.d.ts +16 -0
  19. package/dist/console/HelpCommand.d.ts.map +1 -0
  20. package/dist/console/HelpCommand.js +40 -0
  21. package/dist/console/HelpCommand.js.map +1 -0
  22. package/dist/console/IndexGenerator.d.ts +24 -0
  23. package/dist/console/IndexGenerator.d.ts.map +1 -0
  24. package/dist/console/IndexGenerator.js +65 -0
  25. package/dist/console/IndexGenerator.js.map +1 -0
  26. package/dist/console/Kernel.d.ts +298 -0
  27. package/dist/console/Kernel.d.ts.map +1 -0
  28. package/dist/console/Kernel.js +1120 -0
  29. package/dist/console/Kernel.js.map +1 -0
  30. package/dist/console/ListCommand.d.ts +18 -0
  31. package/dist/console/ListCommand.d.ts.map +1 -0
  32. package/dist/console/ListCommand.js +60 -0
  33. package/dist/console/ListCommand.js.map +1 -0
  34. package/dist/console/assertions.d.ts +20 -0
  35. package/dist/console/assertions.d.ts.map +1 -0
  36. package/dist/console/assertions.js +84 -0
  37. package/dist/console/assertions.js.map +1 -0
  38. package/dist/console/cliui.d.ts +248 -0
  39. package/dist/console/cliui.d.ts.map +1 -0
  40. package/dist/console/cliui.js +587 -0
  41. package/dist/console/cliui.js.map +1 -0
  42. package/dist/console/decorators.d.ts +90 -0
  43. package/dist/console/decorators.d.ts.map +1 -0
  44. package/dist/console/decorators.js +142 -0
  45. package/dist/console/decorators.js.map +1 -0
  46. package/dist/console/index.d.ts +32 -0
  47. package/dist/console/index.d.ts.map +1 -0
  48. package/dist/console/index.js +24 -0
  49. package/dist/console/index.js.map +1 -0
  50. package/dist/console/loaders.d.ts +52 -0
  51. package/dist/console/loaders.d.ts.map +1 -0
  52. package/dist/console/loaders.js +131 -0
  53. package/dist/console/loaders.js.map +1 -0
  54. package/dist/console/parser.d.ts +86 -0
  55. package/dist/console/parser.d.ts.map +1 -0
  56. package/dist/console/parser.js +440 -0
  57. package/dist/console/parser.js.map +1 -0
  58. package/dist/console/prompts.d.ts +124 -0
  59. package/dist/console/prompts.d.ts.map +1 -0
  60. package/dist/console/prompts.js +356 -0
  61. package/dist/console/prompts.js.map +1 -0
  62. package/dist/console/tracing.d.ts +17 -0
  63. package/dist/console/tracing.d.ts.map +1 -0
  64. package/dist/console/tracing.js +11 -0
  65. package/dist/console/tracing.js.map +1 -0
  66. package/dist/console/types.d.ts +255 -0
  67. package/dist/console/types.d.ts.map +1 -0
  68. package/dist/console/types.js +25 -0
  69. package/dist/console/types.js.map +1 -0
  70. package/dist/console/ui.d.ts +20 -0
  71. package/dist/console/ui.d.ts.map +1 -0
  72. package/dist/console/ui.js +31 -0
  73. package/dist/console/ui.js.map +1 -0
  74. package/dist/console/utils.d.ts +27 -0
  75. package/dist/console/utils.d.ts.map +1 -0
  76. package/dist/console/utils.js +56 -0
  77. package/dist/console/utils.js.map +1 -0
  78. package/dist/container/Container.d.ts.map +1 -1
  79. package/dist/container/Container.js +40 -12
  80. package/dist/container/Container.js.map +1 -1
  81. package/dist/container/types.d.ts +14 -2
  82. package/dist/container/types.d.ts.map +1 -1
  83. package/dist/decorators/Service.d.ts +7 -0
  84. package/dist/decorators/Service.d.ts.map +1 -1
  85. package/dist/decorators/Service.js +23 -3
  86. package/dist/decorators/Service.js.map +1 -1
  87. package/dist/errors/ReamError.d.ts +25 -0
  88. package/dist/errors/ReamError.d.ts.map +1 -1
  89. package/dist/errors/ReamError.js +5 -0
  90. package/dist/errors/ReamError.js.map +1 -1
  91. package/dist/errors/prettyPrintError.d.ts +9 -0
  92. package/dist/errors/prettyPrintError.d.ts.map +1 -0
  93. package/dist/errors/prettyPrintError.js +22 -0
  94. package/dist/errors/prettyPrintError.js.map +1 -0
  95. package/dist/index.d.ts +14 -4
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +16 -2
  98. package/dist/index.js.map +1 -1
  99. package/dist/middleware/Pipeline.d.ts +29 -8
  100. package/dist/middleware/Pipeline.d.ts.map +1 -1
  101. package/dist/middleware/Pipeline.js +39 -1
  102. package/dist/middleware/Pipeline.js.map +1 -1
  103. package/dist/rpc/RpcRouter.d.ts.map +1 -1
  104. package/dist/rpc/RpcRouter.js +13 -2
  105. package/dist/rpc/RpcRouter.js.map +1 -1
  106. package/dist/services/app.d.ts +1 -1
  107. package/dist/services/app.js +1 -1
  108. package/dist/services/console.d.ts +21 -0
  109. package/dist/services/console.d.ts.map +1 -0
  110. package/dist/services/console.js +35 -0
  111. package/dist/services/console.js.map +1 -0
  112. package/dist/testing/ApiResponse.d.ts +218 -0
  113. package/dist/testing/ApiResponse.d.ts.map +1 -0
  114. package/dist/testing/ApiResponse.js +576 -0
  115. package/dist/testing/ApiResponse.js.map +1 -0
  116. package/dist/testing/RequestBuilder.d.ts +132 -72
  117. package/dist/testing/RequestBuilder.d.ts.map +1 -1
  118. package/dist/testing/RequestBuilder.js +389 -222
  119. package/dist/testing/RequestBuilder.js.map +1 -1
  120. package/dist/testing/TestClient.d.ts +34 -39
  121. package/dist/testing/TestClient.d.ts.map +1 -1
  122. package/dist/testing/TestClient.js +48 -56
  123. package/dist/testing/TestClient.js.map +1 -1
  124. package/dist/utils/Macroable.js +3 -3
  125. package/dist/utils/Macroable.js.map +1 -1
  126. package/events.darwin-arm64.node +0 -0
  127. package/events.darwin-x64.node +0 -0
  128. package/events.linux-arm64-gnu.node +0 -0
  129. package/events.linux-x64-gnu.node +0 -0
  130. package/events.win32-x64-msvc.node +0 -0
  131. package/index.darwin-arm64.node +0 -0
  132. package/index.darwin-x64.node +0 -0
  133. package/index.linux-arm64-gnu.node +0 -0
  134. package/index.linux-x64-gnu.node +0 -0
  135. package/index.win32-x64-msvc.node +0 -0
  136. package/package.json +15 -2
  137. package/scheduler.darwin-arm64.node +0 -0
  138. package/scheduler.darwin-x64.node +0 -0
  139. package/scheduler.linux-arm64-gnu.node +0 -0
  140. package/scheduler.linux-x64-gnu.node +0 -0
  141. package/scheduler.win32-x64-msvc.node +0 -0
  142. package/src/Ignitor.ts +340 -42
  143. package/src/console/BaseCommand.ts +467 -0
  144. package/src/console/Console.ts +174 -0
  145. package/src/console/ExceptionHandler.ts +91 -0
  146. package/src/console/HelpCommand.ts +37 -0
  147. package/src/console/IndexGenerator.ts +79 -0
  148. package/src/console/Kernel.ts +1382 -0
  149. package/src/console/ListCommand.ts +55 -0
  150. package/src/console/assertions.ts +114 -0
  151. package/src/console/cliui.ts +756 -0
  152. package/src/console/decorators.ts +199 -0
  153. package/src/console/index.ts +67 -0
  154. package/src/console/loaders.ts +145 -0
  155. package/src/console/parser.ts +598 -0
  156. package/src/console/prompts.ts +548 -0
  157. package/src/console/tracing.ts +22 -0
  158. package/src/console/types.ts +280 -0
  159. package/src/console/ui.ts +37 -0
  160. package/src/console/utils.ts +69 -0
  161. package/src/container/Container.ts +55 -13
  162. package/src/container/types.ts +15 -2
  163. package/src/decorators/Service.ts +28 -3
  164. package/src/errors/ReamError.ts +31 -0
  165. package/src/errors/prettyPrintError.ts +20 -0
  166. package/src/index.ts +30 -4
  167. package/src/middleware/Pipeline.ts +76 -5
  168. package/src/rpc/RpcRouter.ts +16 -2
  169. package/src/services/app.ts +1 -1
  170. package/src/services/console.ts +42 -0
  171. package/src/testing/ApiResponse.ts +691 -0
  172. package/src/testing/RequestBuilder.ts +498 -275
  173. package/src/testing/TestClient.ts +63 -86
  174. package/src/utils/Macroable.ts +3 -3
  175. package/dist/console/CommandRunner.d.ts +0 -18
  176. package/dist/console/CommandRunner.d.ts.map +0 -1
  177. package/dist/console/CommandRunner.js +0 -65
  178. package/dist/console/CommandRunner.js.map +0 -1
  179. package/src/console/CommandRunner.ts +0 -82
@@ -0,0 +1,467 @@
1
+ /**
2
+ * BaseCommand — the class application commands extend (Console parity).
3
+ *
4
+ * Everything the kernel needs sits on the STATIC side (`commandName`,
5
+ * `description`, `options`, and the `args` / `flags` metadata written by the
6
+ * decorators), so extending this class is a convenience, not a requirement:
7
+ * it supplies `this.logger`, `this.prompt`, `this.app` and the `exitCode`
8
+ * contract. Packages that must not depend on `@c9up/ream` declare the same
9
+ * statics on a plain class instead.
10
+ *
11
+ * export default class Provision extends BaseCommand {
12
+ * static commandName = 'provision'
13
+ * static description = 'Create the owner account'
14
+ * static options: CommandOptions = { startApp: true }
15
+ *
16
+ * @flags.string({ description: 'Owner email', required: true })
17
+ * declare email: string
18
+ *
19
+ * async run() {
20
+ * const users = await this.app.container.resolve('users')
21
+ * await users.createOwner(this.email)
22
+ * this.logger.success(`Owner created: ${this.email}`)
23
+ * }
24
+ * }
25
+ */
26
+
27
+ import type { Application } from '../Application.js'
28
+ import { createAssertions } from './assertions.js'
29
+ import type { Colors, Logger, Ui } from './cliui.js'
30
+ import { assertArgumentOrder, buildArgument, buildFlag } from './decorators.js'
31
+ import type { Kernel } from './Kernel.js'
32
+ import { assignParsedValues, validateParsed } from './parser.js'
33
+ import type { Prompt } from './prompts.js'
34
+ import type {
35
+ ArgumentMetaData,
36
+ ArgumentsParserOptions,
37
+ CommandOptions,
38
+ CommandSnapshot,
39
+ FlagMetaData,
40
+ FlagsParserOptions,
41
+ ParsedInput,
42
+ SerializedCommand,
43
+ } from './types.js'
44
+
45
+ export abstract class BaseCommand {
46
+ /** Name typed on the command line, e.g. `provision` or `make:controller`. */
47
+ static commandName = ''
48
+
49
+ /** One-line summary shown by `list`. */
50
+ static description = ''
51
+
52
+ /**
53
+ * Alternative names this command answers to (Console `static aliases`). Unlike
54
+ * `commandsAliases` in the rc file, these travel with the command itself.
55
+ */
56
+ static aliases: readonly string[] = []
57
+
58
+ /** Longer text shown under the usage line by `<command> --help`. */
59
+ static help?: string | string[]
60
+
61
+ static options: CommandOptions = {}
62
+
63
+ /**
64
+ * Written by the `@args` / `@flags` decorators. Declared here so a subclass
65
+ * that uses neither still satisfies the dispatch contract, and so the
66
+ * decorators have an inherited list to copy from.
67
+ */
68
+ static args: readonly ArgumentMetaData[] = []
69
+ static flags: readonly FlagMetaData[] = []
70
+
71
+ /**
72
+ * Assigned by the kernel before `run()`. Declared with `declare` so no class
73
+ * field is emitted — under `target: ES2022` a real field would be
74
+ * re-initialised to `undefined` at construction and wipe the assignment.
75
+ */
76
+ declare logger: Logger
77
+ /** Tables, stickers, instructions, tasks — and the raw-mode switch. */
78
+ declare ui: Ui
79
+ /** Chainable ANSI helpers: `this.colors.green('DONE')`. */
80
+ declare colors: Colors
81
+ declare prompt: Prompt
82
+
83
+ /**
84
+ * The booted application. Only meaningful when the command sets
85
+ * `options.startApp` — otherwise the kernel never boots, and reading this is
86
+ * a programming error the kernel reports rather than a silent `undefined`.
87
+ */
88
+ declare app: Application
89
+
90
+ /**
91
+ * The kernel that dispatched this command (Console parity).
92
+ *
93
+ * Assigned for every command, booted app or not: it is the registry, and a
94
+ * command that inspects or lists its siblings — `list` is the built-in
95
+ * example — has nowhere else to read them from.
96
+ */
97
+ declare kernel: Kernel
98
+
99
+ /**
100
+ * Every parsed input, as Console exposes it: `this.parsed.args` /
101
+ * `this.parsed.flags`. The declared ones are also assigned to their
102
+ * properties; this is the whole picture, including flags accepted through
103
+ * `allowUnknownFlags`.
104
+ */
105
+ declare parsed: ParsedInput
106
+
107
+ /** Set it to make the process exit non-zero without throwing. */
108
+ exitCode?: number
109
+
110
+ /** What `run()` returned — filled by the kernel (Console `command.result`). */
111
+ result?: unknown
112
+
113
+ /**
114
+ * The error thrown by `prepare` / `interact` / `run`, assigned before
115
+ * `completed()` runs so the hook can inspect it (Console parity).
116
+ */
117
+ error?: unknown
118
+
119
+ /** Assigned by the kernel — see {@link terminate}. */
120
+ protected declare onTerminate?: () => void | Promise<void>
121
+
122
+ /**
123
+ * Lifecycle hooks, run in this order around `run()`: `prepare`, `interact`,
124
+ * `run`, then `completed` — which runs even when one of the earlier stages
125
+ * threw. Returning `true` from `completed()` marks the error handled, so the
126
+ * kernel stops propagating it.
127
+ */
128
+ // Parameters are allowed: the container injects them (`@inject()` on a
129
+ // lifecycle method), and a no-arg signature rejects those commands.
130
+ prepare?(...args: never[]): void | Promise<void>
131
+ interact?(...args: never[]): void | Promise<void>
132
+ // biome-ignore lint/suspicious/noConfusingVoidType: Console contract — returning `true` marks the error handled, returning nothing is the normal case
133
+ completed?(...args: never[]): boolean | void | Promise<boolean | void>
134
+
135
+ /**
136
+ * Shut down a `staysAlive` command. Without it a long-running command has no
137
+ * way to hand control back — the kernel deliberately does not tear the app
138
+ * down for those.
139
+ */
140
+ async terminate(): Promise<void> {
141
+ await this.onTerminate?.()
142
+ }
143
+
144
+ /** Has {@link boot} run for THIS class — not for one of its ancestors. */
145
+ static booted = false
146
+
147
+ /**
148
+ * Give this class its OWN copy of the inherited static declarations
149
+ * (Console `boot`).
150
+ *
151
+ * Without it, `Child.args.push(...)` would append to the array the parent
152
+ * declared, and every command in the hierarchy would share one list. Called
153
+ * by everything that reads or writes those statics, so it is normally
154
+ * invisible; it is public because ported Console code calls it directly.
155
+ */
156
+ static boot(): void {
157
+ if (Object.hasOwn(this, 'booted') && this.booted === true) return
158
+ this.booted = true
159
+ // Only the mutable declarations are copied. Console also re-assigns the scalar
160
+ // statics (name, description, help); for a string, an own copy and an
161
+ // inherited one are indistinguishable — the line would read as a bug.
162
+ this.args = [...this.args]
163
+ this.flags = [...this.flags]
164
+ this.aliases = [...this.aliases]
165
+ this.options = { staysAlive: false, allowUnknownFlags: false, ...this.options }
166
+ }
167
+
168
+ /**
169
+ * Declare an argument without a decorator (Console `defineArgument`).
170
+ *
171
+ * The decorators are the ergonomic form, but a command built at runtime — or
172
+ * one in a package that must not import the framework — needs a plain call.
173
+ */
174
+ static defineArgument(name: string, options: Partial<ArgumentMetaData> = {}): void {
175
+ this.boot()
176
+ const list: ArgumentMetaData[] = [...this.args]
177
+ // Built by the same function the decorators use: a field added on one path
178
+ // and forgotten on the other is a silent divergence, not a compile error.
179
+ const argument = buildArgument(name, options)
180
+
181
+ // Console's two ordering rules, shared with the decorators — see
182
+ // `assertArgumentOrder`. They are not pedantry: a spread argument eats the
183
+ // tail, so nothing after it can ever be filled, and a required argument
184
+ // behind an optional one can only be reached by passing the optional one,
185
+ // which makes "required" a lie.
186
+ assertArgumentOrder(list, argument)
187
+
188
+ list.push(argument)
189
+ this.args = list
190
+ }
191
+
192
+ /** Declare a flag without a decorator (Console `defineFlag`). */
193
+ static defineFlag(name: string, options: Partial<FlagMetaData> = {}): void {
194
+ this.boot()
195
+ const list: FlagMetaData[] = [...this.flags]
196
+ list.push(buildFlag(name, options))
197
+ this.flags = list
198
+ }
199
+
200
+ /**
201
+ * The command's metadata as plain data (Console `serialize`).
202
+ *
203
+ * What `ream list --json` and any tooling reads, without having to know how
204
+ * the decorators stored it.
205
+ */
206
+ static serialize(): SerializedCommand {
207
+ this.boot()
208
+ // `parse` is a function: keeping it would make this object look JSON-safe
209
+ // while quietly losing a field the moment it is stringified.
210
+ const withoutParse = <T extends { parse?: unknown }>(entry: T): Omit<T, 'parse'> => {
211
+ const { parse: _dropped, ...rest } = entry
212
+ return rest
213
+ }
214
+
215
+ const colon = this.commandName.indexOf(':')
216
+
217
+ return {
218
+ commandName: this.commandName,
219
+ namespace: colon === -1 ? null : this.commandName.slice(0, colon),
220
+ description: this.description,
221
+ help: this.help,
222
+ aliases: [...this.aliases],
223
+ options: { ...this.options },
224
+ args: this.args.map(withoutParse),
225
+ flags: this.flags.map(withoutParse),
226
+ }
227
+ }
228
+
229
+ /**
230
+ * How this command's inputs are parsed, grouped by type (Console
231
+ * `getParserOptions`).
232
+ *
233
+ * Ream's parser reads the declarations directly, so nothing internal calls
234
+ * this; it exists because Console code inspects the shape — and it is derived
235
+ * from the same metadata, so it cannot describe something the parser would
236
+ * not do.
237
+ */
238
+ static getParserOptions(options?: Partial<FlagsParserOptions>): {
239
+ flagsParserOptions: FlagsParserOptions
240
+ argumentsParserOptions: ArgumentsParserOptions[]
241
+ } {
242
+ this.boot()
243
+
244
+ const flagsParserOptions: FlagsParserOptions = {
245
+ all: [],
246
+ string: [],
247
+ boolean: [],
248
+ number: [],
249
+ array: [],
250
+ alias: {},
251
+ count: [],
252
+ coerce: {},
253
+ default: {},
254
+ ...options,
255
+ }
256
+
257
+ for (const flag of this.flags) {
258
+ flagsParserOptions.all.push(flag.flagName)
259
+ if (flag.alias.length > 0) flagsParserOptions.alias[flag.flagName] = [...flag.alias]
260
+ if (flag.parse !== undefined) {
261
+ flagsParserOptions.coerce[flag.flagName] = flag.parse as (value: never) => unknown
262
+ }
263
+ if (flag.default !== undefined) flagsParserOptions.default[flag.flagName] = flag.default
264
+ flagsParserOptions[flag.type].push(flag.flagName)
265
+ }
266
+
267
+ return {
268
+ flagsParserOptions,
269
+ argumentsParserOptions: this.args.map((arg) => ({
270
+ type: arg.type,
271
+ default: arg.default,
272
+ parse: arg.parse,
273
+ })),
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Check a parsed input against this command's declarations (Console `validate`).
279
+ *
280
+ * The parser enforces the same rules while parsing, so calling this on its
281
+ * output always passes. It is here for the case Console documents: an input built
282
+ * by hand, which has never been through a parser.
283
+ *
284
+ * Positional values are accepted as a list (Console's shape) or keyed by property
285
+ * name (what `this.parsed.args` holds), so the same call works from both.
286
+ */
287
+ static validate(parsed: {
288
+ args: readonly unknown[] | Record<string, unknown>
289
+ flags?: Record<string, unknown>
290
+ unknownFlags?: readonly string[]
291
+ }): void {
292
+ this.boot()
293
+ // The kernel applies the same rules between parsing and running — see
294
+ // `validateParsed`. One implementation, so a hand-built input is held to
295
+ // exactly what the command line is held to.
296
+ validateParsed(parsed, {
297
+ args: this.args,
298
+ flags: this.flags,
299
+ allowUnknownFlags: this.options.allowUnknownFlags,
300
+ commandName: this.commandName,
301
+ })
302
+ }
303
+
304
+ /**
305
+ * Assign the parsed values to their properties (Console `hydrate`).
306
+ *
307
+ * Idempotent: the kernel calls it before `run()`, and a ported command may
308
+ * call it again without the values being recomputed.
309
+ */
310
+ hydrate(): void {
311
+ if (this.hydrated) return
312
+ const commandClass = Object.getPrototypeOf(this).constructor
313
+ if (!isDeclaring(commandClass)) {
314
+ throw new Error('hydrate() requires a command class carrying the static contract')
315
+ }
316
+
317
+ // One hydration path, shared with the kernel — see `assignParsedValues`.
318
+ // By declaration, not by copying the parsed bag: positionals are read BY
319
+ // POSITION and flags by their CLI name, so `this.name` is filled whichever
320
+ // shape the input came in.
321
+ assignParsedValues(this, commandClass.args, commandClass.flags, this.parsed)
322
+
323
+ this.hydrated = true
324
+ }
325
+
326
+ /** Set once {@link hydrate} has run. */
327
+ protected hydrated = false
328
+
329
+ /**
330
+ * Hydrate, then run — Console's instance-level `exec()`.
331
+ *
332
+ * Deliberately NOT the kernel's `exec()`: this one runs `run()` alone and
333
+ * rethrows, while the kernel drives the whole lifecycle (`prepare`,
334
+ * `interact`, `completed`) and reports the failure on the command instead.
335
+ */
336
+ async exec(): Promise<unknown> {
337
+ this.hydrate()
338
+ try {
339
+ this.result = await this.run()
340
+ this.exitCode = this.exitCode ?? 0
341
+ return this.result
342
+ } catch (error) {
343
+ this.error = error
344
+ this.exitCode = this.exitCode ?? 1
345
+ throw error
346
+ }
347
+ }
348
+
349
+ /** The statics, readable from the instance (Console parity). */
350
+ get commandName(): string {
351
+ return Object.getPrototypeOf(this).constructor.commandName
352
+ }
353
+
354
+ get options(): CommandOptions {
355
+ return Object.getPrototypeOf(this).constructor.options
356
+ }
357
+
358
+ get args(): readonly ArgumentMetaData[] {
359
+ return Object.getPrototypeOf(this).constructor.args
360
+ }
361
+
362
+ get flags(): readonly FlagMetaData[] {
363
+ return Object.getPrototypeOf(this).constructor.flags
364
+ }
365
+
366
+ /**
367
+ * A snapshot of THIS execution (Console `toJSON`): what the command received and
368
+ * what it produced.
369
+ *
370
+ * Distinct from the static {@link serialize}, which describes the command's
371
+ * contract. Returning the metadata here would drop `exitCode`, `result` and
372
+ * `error` — exactly what a test or an integration reads after running it.
373
+ */
374
+ toJSON(): CommandSnapshot {
375
+ // Not named `constructor`: that shadows the global and biome rejects it.
376
+ const commandClass = Object.getPrototypeOf(this).constructor
377
+ if (!isSerializable(commandClass)) {
378
+ throw new Error('toJSON() requires a command class carrying the static contract')
379
+ }
380
+ const meta = commandClass.serialize()
381
+
382
+ return {
383
+ commandName: meta.commandName,
384
+ options: meta.options,
385
+ // Positional VALUES as a list (Console's shape); flags stay keyed.
386
+ args: [...(this.parsed?.args ?? [])],
387
+ flags: this.parsed?.flags ?? {},
388
+ error: this.error,
389
+ result: this.result,
390
+ exitCode: this.exitCode,
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Was this command the one invoked on the command line, rather than one
396
+ * another command called through `consoleApp.exec()`? Assigned by the kernel.
397
+ */
398
+ declare isMain: boolean
399
+
400
+ abstract run(...args: never[]): unknown
401
+
402
+ // ─── Test assertions (Console parity) ───────────────────────────
403
+ //
404
+ // They live on the command because that is where a test holds the result,
405
+ // but the implementation is the kernel's: a command the kernel ran carries
406
+ // assertions ATTACHED by it (a class declared structurally never extends
407
+ // this one), and two copies of "did the table print this row?" is how the
408
+ // two answers end up disagreeing — which is exactly what happened.
409
+
410
+ /** The command finished with exit code 0. */
411
+ assertSucceeded(): void {
412
+ createAssertions(this, this.ui).assertSucceeded()
413
+ }
414
+
415
+ /** The command finished with a non-zero exit code. */
416
+ assertFailed(): void {
417
+ createAssertions(this, this.ui).assertFailed()
418
+ }
419
+
420
+ assertExitCode(expected: number): void {
421
+ createAssertions(this, this.ui).assertExitCode(expected)
422
+ }
423
+
424
+ assertNotExitCode(unexpected: number): void {
425
+ createAssertions(this, this.ui).assertNotExitCode(unexpected)
426
+ }
427
+
428
+ /**
429
+ * A line equal to `message` was logged. Pass a stream to require it went
430
+ * there — a warning belongs on stderr, and asserting that is the point.
431
+ */
432
+ assertLog(message: string, stream?: 'stdout' | 'stderr'): void {
433
+ createAssertions(this, this.ui).assertLog(message, stream)
434
+ }
435
+
436
+ assertLogMatches(pattern: RegExp, stream?: 'stdout' | 'stderr'): void {
437
+ createAssertions(this, this.ui).assertLogMatches(pattern, stream)
438
+ }
439
+
440
+ /** Every expected row was rendered — the header counts as one (Console). */
441
+ assertTableRows(expected: readonly (readonly string[])[]): void {
442
+ createAssertions(this, this.ui).assertTableRows(expected)
443
+ }
444
+ }
445
+
446
+ /**
447
+ * The class carries the declarations `hydrate()` reads. Checked rather than
448
+ * assumed: a command may be declared structurally, and reading `args` off
449
+ * something that has none would silently hydrate nothing.
450
+ */
451
+ function isDeclaring(
452
+ value: unknown,
453
+ ): value is { args: readonly ArgumentMetaData[]; flags: readonly FlagMetaData[] } {
454
+ return (
455
+ typeof value === 'function' &&
456
+ Array.isArray(Reflect.get(value, 'args')) &&
457
+ Array.isArray(Reflect.get(value, 'flags'))
458
+ )
459
+ }
460
+
461
+ function isSerializable(value: unknown): value is { serialize(): SerializedCommand } {
462
+ return (
463
+ typeof value === 'function' &&
464
+ 'serialize' in value &&
465
+ typeof Reflect.get(value, 'serialize') === 'function'
466
+ )
467
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * `consoleApp` — the programmatic face of the console.
3
+ *
4
+ * The CLI is not the only caller: a test wants to run a command and inspect the
5
+ * result, a controller may want to trigger one, a script may want to check a
6
+ * command exists before calling it. Spawning a process for that is both slow
7
+ * and untestable.
8
+ *
9
+ * import consoleApp from '@c9up/ream/services/console'
10
+ *
11
+ * const command = await consoleApp.exec('make:controller', ['user', '--resource'])
12
+ * command.exitCode // 0 on success, 1 on an unhandled failure
13
+ * command.result // whatever run() returned
14
+ * command.error // the failure, when one survived completed()
15
+ */
16
+
17
+ import { ReamError } from '../errors/ReamError.js'
18
+ import type { Ui } from './cliui.js'
19
+ import type { Kernel, KernelState } from './Kernel.js'
20
+ import type { CommandClass, CommandInstance, ExecutedCommand, SerializedCommand } from './types.js'
21
+
22
+ export interface ConsoleOptions {
23
+ kernel: Kernel
24
+ /**
25
+ * Loads the application's commands into the kernel. Called once, on first
26
+ * use — discovery reads the filesystem and imports modules, which is too much
27
+ * to do at every `hasCommand()`.
28
+ */
29
+ load: () => Promise<void>
30
+ }
31
+
32
+ export class Console {
33
+ readonly #kernel: Kernel
34
+ readonly #load: () => Promise<void>
35
+ #booting: Promise<void> | undefined
36
+ #booted = false
37
+
38
+ constructor(options: ConsoleOptions) {
39
+ this.#kernel = options.kernel
40
+ this.#load = options.load
41
+ }
42
+
43
+ /**
44
+ * Load the commands if that has not happened yet.
45
+ *
46
+ * Idempotent, and safe under concurrent calls: the in-flight promise is
47
+ * shared, so two parallel `exec()` calls do not each import every command.
48
+ */
49
+ async boot(): Promise<void> {
50
+ this.#booting ??= this.#load().then(() => {
51
+ this.#booted = true
52
+ })
53
+ await this.#booting
54
+ }
55
+
56
+ /** Run a command and return it, carrying `exitCode`, `result` and `error`. */
57
+ async exec(
58
+ commandName: string,
59
+ argv: readonly string[] = [],
60
+ options: { ui?: Ui } = {},
61
+ ): Promise<ExecutedCommand> {
62
+ await this.boot()
63
+ return this.#kernel.exec(commandName, argv, options)
64
+ }
65
+
66
+ /**
67
+ * Does a command (or alias) by this name exist?
68
+ *
69
+ * Synchronous, as in Console — `await consoleApp.boot()` then `if (consoleApp.hasCommand(…))`.
70
+ * An async version would be a trap: a Promise is always truthy, so Adonis
71
+ * code copied over would take every branch.
72
+ *
73
+ * Calling it before `boot()` throws rather than answering `false`: an
74
+ * unloaded registry cannot tell "no such command" from "not looked yet", and
75
+ * a silent `false` is exactly the failure this signature exists to avoid.
76
+ */
77
+ hasCommand(name: string): boolean {
78
+ this.#assertBooted('hasCommand')
79
+ return this.#kernel.hasCommand(name)
80
+ }
81
+
82
+ /** The metadata of every registered command. Requires {@link boot} too. */
83
+ getCommands(): SerializedCommand[] {
84
+ this.#assertBooted('getCommands')
85
+ return this.#kernel.getCommands()
86
+ }
87
+
88
+ /**
89
+ * The rest of Console's introspection surface, relayed as-is.
90
+ *
91
+ * All of it reads the registry, so all of it needs {@link boot} first —
92
+ * answering from an empty registry would be worse than the error, since
93
+ * "no such command" and "commands not loaded yet" are indistinguishable to
94
+ * the caller.
95
+ */
96
+ getCommand(commandName: string): SerializedCommand | null {
97
+ this.#assertBooted('getCommand')
98
+ return this.#kernel.getCommand(commandName)
99
+ }
100
+
101
+ getNamespaceCommands(namespace?: string): SerializedCommand[] {
102
+ this.#assertBooted('getNamespaceCommands')
103
+ return this.#kernel.getNamespaceCommands(namespace)
104
+ }
105
+
106
+ getNamespaces(): string[] {
107
+ this.#assertBooted('getNamespaces')
108
+ return this.#kernel.getNamespaces()
109
+ }
110
+
111
+ getAliases(): string[] {
112
+ this.#assertBooted('getAliases')
113
+ return this.#kernel.getAliases()
114
+ }
115
+
116
+ getAliasCommand(alias: string): SerializedCommand | null {
117
+ this.#assertBooted('getAliasCommand')
118
+ return this.#kernel.getAliasCommand(alias)
119
+ }
120
+
121
+ getCommandAliases(commandName: string): string[] {
122
+ this.#assertBooted('getCommandAliases')
123
+ return this.#kernel.getCommandAliases(commandName)
124
+ }
125
+
126
+ getCommandSuggestions(name: string): string[] {
127
+ this.#assertBooted('getCommandSuggestions')
128
+ return this.#kernel.getCommandSuggestions(name)
129
+ }
130
+
131
+ getNamespaceSuggestions(name: string): string[] {
132
+ this.#assertBooted('getNamespaceSuggestions')
133
+ return this.#kernel.getNamespaceSuggestions(name)
134
+ }
135
+
136
+ /** The kernel's lifecycle stage — Console `getState()`. */
137
+ getState(): KernelState {
138
+ return this.#kernel.getState()
139
+ }
140
+
141
+ /** The command run when none is named. */
142
+ getDefaultCommand(): CommandClass {
143
+ this.#assertBooted('getDefaultCommand')
144
+ return this.#kernel.getDefaultCommand()
145
+ }
146
+
147
+ /** The command invoked from the command line, once it has been built. */
148
+ getMainCommand(): CommandInstance | undefined {
149
+ return this.#kernel.getMainCommand()
150
+ }
151
+
152
+ /**
153
+ * Resolve a command class by name or alias, throwing when there is none.
154
+ * Loads the commands first, like {@link exec}.
155
+ */
156
+ async find(commandName: string): Promise<CommandClass> {
157
+ await this.boot()
158
+ return this.#kernel.find(commandName)
159
+ }
160
+
161
+ #assertBooted(method: string): void {
162
+ if (this.#booted) return
163
+ throw new ReamError(
164
+ 'E_CONSOLE_NOT_BOOTED',
165
+ `consoleApp.${method}() was called before the commands were loaded.`,
166
+ { hint: 'Await consoleApp.boot() first — consoleApp.exec() does it for you.' },
167
+ )
168
+ }
169
+
170
+ /** The underlying kernel, for anything the façade does not cover. */
171
+ get kernel(): Kernel {
172
+ return this.#kernel
173
+ }
174
+ }