@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,1120 @@
1
+ /**
2
+ * Console kernel — registers commands and dispatches argv against them.
3
+ *
4
+ * Replaces the previous `CommandRunner`, which parsed argv without knowing the
5
+ * commands' declared inputs and therefore could not support `--flag value`,
6
+ * aliases, negation, typed flags, per-command help or required-input checks.
7
+ *
8
+ * The kernel accepts any class exposing the static contract in `types.ts` —
9
+ * extending `BaseCommand` is optional, which is what keeps agnostic packages
10
+ * free of a `@c9up/ream` dependency.
11
+ */
12
+ import { isReamError, ReamError } from '../errors/ReamError.js';
13
+ import { createAssertions, createSnapshot } from './assertions.js';
14
+ import { Ui } from './cliui.js';
15
+ import { buildFlag } from './decorators.js';
16
+ import { ExceptionHandler } from './ExceptionHandler.js';
17
+ import HelpCommand from './HelpCommand.js';
18
+ import ListCommand from './ListCommand.js';
19
+ import { assignParsedValues, parseArgv, validateParsed } from './parser.js';
20
+ import { Prompt } from './prompts.js';
21
+ import { commandExec } from './tracing.js';
22
+ import { isCommandClass, } from './types.js';
23
+ import { renderErrorWithSuggestions } from './utils.js';
24
+ import { colourise } from './ui.js';
25
+ export class Kernel {
26
+ #commands = new Map();
27
+ /**
28
+ * Commands a loader announced but whose class has not been imported yet.
29
+ *
30
+ * Booting reads metadata only, as Console does: listing the commands, answering
31
+ * `hasCommand()` or printing help must not run the module of a command
32
+ * nobody asked for.
33
+ */
34
+ #pending = new Map();
35
+ #aliases = new Map();
36
+ /**
37
+ * Commands the kernel registered on its own behalf, which an application may
38
+ * replace — see {@link register}.
39
+ */
40
+ #defaults = new Set();
41
+ /** Flags every command accepts — Console's global command. */
42
+ #globalFlags = [];
43
+ #flagListeners = new Map();
44
+ #loaders = [];
45
+ #hooks = { finding: [], loading: [], loaded: [], executing: [], executed: [] };
46
+ #state = 'idle';
47
+ #mainCommand;
48
+ #defaultCommand;
49
+ #executor;
50
+ /** The application a command was built with, when it asked for one. */
51
+ #apps = new WeakMap();
52
+ #startApp;
53
+ #onTerminate;
54
+ #prompt;
55
+ #binaryName;
56
+ /** Shared by the kernel and every command it runs. */
57
+ ui = new Ui();
58
+ logger = this.ui.logger;
59
+ /**
60
+ * The command a kernel runs when none is named. Replace it to ship a
61
+ * different landing command (Console `Kernel.defaultCommand`).
62
+ */
63
+ static defaultCommand = ListCommand;
64
+ /**
65
+ * How a command is built and run (Console `Kernel.commandExecutor`).
66
+ *
67
+ * The seam AdonisJS itself uses to add dependency injection; Ream's default
68
+ * already does it — the container builds the command and invokes its hooks,
69
+ * so `@inject()` works on both. Replace it on a subclass to drive commands
70
+ * some other way.
71
+ */
72
+ static commandExecutor = {
73
+ create(Command, parsed, kernel, context) {
74
+ return kernel.buildCommand(Command, parsed, context);
75
+ },
76
+ run(command, kernel) {
77
+ return kernel.runLifecycle(command);
78
+ },
79
+ };
80
+ /**
81
+ * What the CLI says about itself — binary name, versions. Printed above the
82
+ * command listing when it holds anything (Console `kernel.info`).
83
+ */
84
+ info = new Map();
85
+ /**
86
+ * Renders the failure of the command line (Console `kernel.errorHandler`).
87
+ *
88
+ * Replaceable: an application that wants its own reporting sets it here
89
+ * rather than wrapping `handle()`, which is what owns the process.
90
+ */
91
+ errorHandler = new ExceptionHandler();
92
+ /**
93
+ * The exit code the command line ended on (Console `kernel.exitCode`). Set once
94
+ * the dispatch is over: 1 for a failure, otherwise the command's own.
95
+ */
96
+ exitCode;
97
+ /**
98
+ * Console's factory — `Kernel.create()` reads better than `new Kernel()`.
99
+ *
100
+ * Built through `this`, so a subclass overriding `defaultCommand` or
101
+ * `commandExecutor` gets its own from the factory as well as from `new`.
102
+ */
103
+ static create(options = {}) {
104
+ return new this(options);
105
+ }
106
+ constructor(options = {}) {
107
+ this.#startApp = options.startApp;
108
+ this.#onTerminate = options.onTerminate;
109
+ this.#prompt = options.prompt;
110
+ this.#binaryName = options.binaryName ?? 'ream';
111
+ // `list` is a command, not a branch in the dispatcher — Console registers it the
112
+ // same way, which is what makes `hasCommand('list')`, `exec('list')` and
113
+ // `help list` true statements instead of special cases.
114
+ // Read off `new.target`, so a subclass overriding `static defaultCommand`
115
+ // gets its own (Console's customisation point).
116
+ this.#defaultCommand = new.target.defaultCommand;
117
+ this.#executor = new.target.commandExecutor;
118
+ this.register(this.#defaultCommand);
119
+ this.#defaults.add(this.#defaultCommand.commandName);
120
+ // `help` is a command for the same reason `list` is: everything the
121
+ // registry guarantees — introspection, `exec()`, replaceability — is false
122
+ // for a name only the dispatcher knows.
123
+ this.register(HelpCommand);
124
+ this.#defaults.add(HelpCommand.commandName);
125
+ // Console's `--help`, a global flag whose listener runs the help command.
126
+ this.defineFlag('help', {
127
+ type: 'boolean',
128
+ alias: ['h'],
129
+ description: 'Display help for the given command',
130
+ });
131
+ this.on('help', async (Command, kernel, parsed) => {
132
+ // Only the mention, not `--no-help`.
133
+ if (parsed.flags.help !== true)
134
+ return false;
135
+ // Whatever was resolved: the named command, or the default one for a bare
136
+ // `ream --help` — whose own help documents the listing.
137
+ await kernel.exec('help', [Command.commandName]);
138
+ return true;
139
+ });
140
+ // Console's `--ansi` / `--no-ansi`, declared as what they are: a global flag.
141
+ // Declaring them makes them visible in help and rejected nowhere, instead
142
+ // of being intercepted by a special case in the dispatcher.
143
+ this.defineFlag('ansi', {
144
+ type: 'boolean',
145
+ description: 'Force colours on, or off with --no-ansi',
146
+ showNegatedVariantInHelp: true,
147
+ });
148
+ this.on('ansi', (_command, _kernel, parsed) => {
149
+ if (parsed.flags.ansi === true) {
150
+ process.env.FORCE_COLOR = '1';
151
+ delete process.env.NO_COLOR;
152
+ return;
153
+ }
154
+ process.env.NO_COLOR = '1';
155
+ delete process.env.FORCE_COLOR;
156
+ });
157
+ }
158
+ /** Shown in usage lines — commands read it to print their own. */
159
+ get binaryName() {
160
+ return this.#binaryName;
161
+ }
162
+ /**
163
+ * Declare a flag every command accepts (Console `kernel.defineFlag`).
164
+ *
165
+ * Global flags steer the CLI, not the command: they are merged into the
166
+ * parser so no command has to redeclare them, and they are NOT assigned to
167
+ * the command's properties — a command reads them through `this.parsed.flags`
168
+ * or, more usually, does not read them at all.
169
+ */
170
+ defineFlag(name, options = {}) {
171
+ if (this.#state !== 'idle') {
172
+ throw new ReamError('E_CONSOLE_LATE_GLOBAL_FLAG', `Global flag "--${name}" was declared while the kernel was "${this.#state}".`, { hint: 'Declare global flags while wiring the kernel, before booting it.' });
173
+ }
174
+ this.#globalFlags.push(buildFlag(name, options));
175
+ return this;
176
+ }
177
+ /** The global flags — Console `kernel.flags`. */
178
+ get flags() {
179
+ return [...this.#globalFlags];
180
+ }
181
+ /**
182
+ * React to a global flag (Console `kernel.on`). One listener per flag, the last
183
+ * one registered winning, and only for the command invoked from the command
184
+ * line. Returning `true` stops there, without running the command — that is
185
+ * how `--help` short-circuits.
186
+ */
187
+ on(flagName, listener) {
188
+ this.#flagListeners.set(flagName, listener);
189
+ return this;
190
+ }
191
+ register(command) {
192
+ const existing = this.#commands.get(command.commandName);
193
+ if (existing !== undefined && existing !== command) {
194
+ // The kernel's own defaults are replaceable: one registry, and the
195
+ // application wins — an app shipping its own `list` gets it, the same
196
+ // rule the CLI applies to the built-in commands it shadows. Two
197
+ // APPLICATION commands claiming one name stays an error: there is
198
+ // nothing to arbitrate between them.
199
+ if (!this.#defaults.has(command.commandName)) {
200
+ throw new ReamError('E_CONSOLE_DUPLICATE_COMMAND', `Two commands claim the name "${command.commandName}".`, { hint: 'Rename one of them, or namespace it (e.g. "app:provision").' });
201
+ }
202
+ this.#defaults.delete(command.commandName);
203
+ }
204
+ this.#pending.delete(command.commandName);
205
+ this.#commands.set(command.commandName, command);
206
+ // `static aliases` travel with the command; rc-level `commandsAliases` are
207
+ // registered separately and may still override these.
208
+ for (const alias of command.aliases ?? []) {
209
+ this.#aliases.set(alias, [command.commandName]);
210
+ }
211
+ return this;
212
+ }
213
+ registerMany(commands) {
214
+ for (const command of commands)
215
+ this.register(command);
216
+ return this;
217
+ }
218
+ /**
219
+ * Register the default export of a loaded module when it is a command.
220
+ * Returns whether anything was registered, so the caller can report a module
221
+ * that exports the wrong shape instead of ignoring it.
222
+ */
223
+ registerModule(mod) {
224
+ if (typeof mod !== 'object' || mod === null)
225
+ return false;
226
+ const value = Reflect.get(mod, 'default');
227
+ if (!isCommandClass(value))
228
+ return false;
229
+ this.register(value);
230
+ return true;
231
+ }
232
+ /**
233
+ * Register a shorthand expanding to a command and, optionally, its flags —
234
+ * Console's `commandsAliases`. `resource` → `make:controller --resource` means
235
+ * `ream resource users` runs `ream make:controller --resource users`.
236
+ */
237
+ addAlias(alias, expansion) {
238
+ const tokens = tokeniseExpansion(expansion);
239
+ if (tokens.length === 0) {
240
+ throw new ReamError('E_CONSOLE_INVALID_ALIAS', `Alias "${alias}" expands to nothing.`, {
241
+ hint: 'Expected a command name, e.g. "make:controller --resource".',
242
+ });
243
+ }
244
+ this.#aliases.set(alias, tokens);
245
+ return this;
246
+ }
247
+ /**
248
+ * Resolve a command by name or alias — Console's `find()`.
249
+ *
250
+ * Async and throwing, as in Console: a name that resolves to nothing is a call
251
+ * error, and returning `undefined` pushes every caller into a check it
252
+ * usually forgets. `await` works even though Ream's registry answers
253
+ * immediately.
254
+ */
255
+ async find(name) {
256
+ const [resolved] = this.#expandAlias([name]);
257
+ const commandName = resolved ?? name;
258
+ await this.#emit(this.#hooks.finding, (handler) => handler(commandName));
259
+ const loaded = this.#commands.get(commandName);
260
+ if (loaded !== undefined) {
261
+ await this.#emit(this.#hooks.loading, (handler) => handler(this.#metadataFor(loaded)));
262
+ await this.#emit(this.#hooks.loaded, (handler) => handler(loaded));
263
+ return loaded;
264
+ }
265
+ const pending = this.#pending.get(commandName);
266
+ if (pending === undefined) {
267
+ throw new ReamError('E_CONSOLE_COMMAND_NOT_FOUND', `Unknown command "${name}".`, {
268
+ hint: 'Check the name with hasCommand() first, or list them with getCommands().',
269
+ });
270
+ }
271
+ await this.#emit(this.#hooks.loading, (handler) => handler(pending.metadata));
272
+ // Here is where a loader's module is imported — for the command that was
273
+ // actually asked for, and once.
274
+ const command = await pending.loader.getCommand(pending.metadata);
275
+ if (command === null) {
276
+ throw new ReamError('E_CONSOLE_COMMAND_NOT_FOUND', `The loader announced "${commandName}" but did not provide it.`, { hint: 'A loader must return the command it listed in its metadata.' });
277
+ }
278
+ this.register(command);
279
+ await this.#emit(this.#hooks.loaded, (handler) => handler(command));
280
+ return command;
281
+ }
282
+ /**
283
+ * Register a source of commands (Console `kernel.addLoader`).
284
+ *
285
+ * Consumed by {@link boot}, once. Unlike Console, the classes are resolved there
286
+ * and then rather than on first use: Ream's registry holds classes, which is
287
+ * what lets `find()` and `hasCommand()` answer without waiting on a loader.
288
+ */
289
+ addLoader(loader) {
290
+ if (this.#state !== 'idle') {
291
+ throw new ReamError('E_CONSOLE_LATE_LOADER', `A command loader was added while the kernel was "${this.#state}".`, { hint: 'Add loaders before booting the kernel.' });
292
+ }
293
+ this.#loaders.push(loader);
294
+ return this;
295
+ }
296
+ /**
297
+ * Load every registered loader's commands. Idempotent, and called by
298
+ * `handle()` — an application driving its own dispatch calls it itself.
299
+ */
300
+ async boot() {
301
+ if (this.#state !== 'idle')
302
+ return;
303
+ this.#state = 'booted';
304
+ for (const entry of this.#loaders) {
305
+ const loader = typeof entry === 'function' ? await entry() : entry;
306
+ for (const metadata of await loader.getMetaData()) {
307
+ // Metadata only — the class is imported by `find()`, when someone asks
308
+ // for the command. `loading` / `loaded` belong there too.
309
+ if (!this.#commands.has(metadata.commandName)) {
310
+ this.#pending.set(metadata.commandName, { metadata, loader });
311
+ }
312
+ // The metadata is what a manifest-style loader publishes; its aliases
313
+ // may be richer than the class's `static aliases`, and reading only the
314
+ // class would leave `hasCommand(alias)` answering false for a name the
315
+ // loader announced.
316
+ for (const alias of metadata.aliases)
317
+ this.addAlias(alias, metadata.commandName);
318
+ }
319
+ }
320
+ }
321
+ /** The kernel's lifecycle stage — Console `getState()`. */
322
+ getState() {
323
+ return this.#state;
324
+ }
325
+ /**
326
+ * The command run when none is named — the one this kernel was built with,
327
+ * or the application's replacement when it registered one under that name.
328
+ */
329
+ getDefaultCommand() {
330
+ return this.#commands.get(this.#defaultCommand.commandName) ?? this.#defaultCommand;
331
+ }
332
+ /** The command invoked from the command line, once it has been built. */
333
+ getMainCommand() {
334
+ return this.#mainCommand;
335
+ }
336
+ /** Before a command name is resolved (Console hook). */
337
+ finding(callback) {
338
+ this.#hooks.finding.push(callback);
339
+ return this;
340
+ }
341
+ /** Before a command class is read from its loader (Console hook). */
342
+ loading(callback) {
343
+ this.#hooks.loading.push(callback);
344
+ return this;
345
+ }
346
+ /** After a command class has been loaded (Console hook). */
347
+ loaded(callback) {
348
+ this.#hooks.loaded.push(callback);
349
+ return this;
350
+ }
351
+ /** Before a command runs. `isMain` distinguishes the CLI dispatch. */
352
+ executing(callback) {
353
+ this.#hooks.executing.push(callback);
354
+ return this;
355
+ }
356
+ /** After a command has run — not when it failed, as in Console. */
357
+ executed(callback) {
358
+ this.#hooks.executed.push(callback);
359
+ return this;
360
+ }
361
+ /**
362
+ * The metadata of every registered command, as Console exposes it.
363
+ *
364
+ * Metadata rather than the classes themselves: this is the introspection
365
+ * surface (help, completions, a command palette), and handing out the
366
+ * constructors invites callers to instantiate a command outside the kernel,
367
+ * where nothing injects its dependencies or runs its lifecycle. The class is
368
+ * still reachable through {@link find} when a caller genuinely needs it.
369
+ */
370
+ getCommands() {
371
+ return this.#allMetadata();
372
+ }
373
+ /** One command's metadata, or null — Console's `getCommand()`. */
374
+ getCommand(commandName) {
375
+ const command = this.#commands.get(commandName);
376
+ if (command !== undefined)
377
+ return this.#metadataFor(command);
378
+ const pending = this.#pending.get(commandName);
379
+ // Answered from the metadata a loader published: describing a command must
380
+ // not require importing it.
381
+ return pending === undefined
382
+ ? null
383
+ : { ...pending.metadata, aliases: this.#aliasesFor(commandName) };
384
+ }
385
+ /**
386
+ * The commands of one namespace. Called without one — or with an empty
387
+ * string — it answers with the commands that have no namespace at all, which
388
+ * is the group Console's own listing prints first.
389
+ */
390
+ getNamespaceCommands(namespace) {
391
+ const wanted = namespace === undefined || namespace === '' ? null : namespace;
392
+ return this.#allMetadata().filter((command) => command.namespace === wanted);
393
+ }
394
+ /** Every namespace in use, sorted. */
395
+ getNamespaces() {
396
+ const found = new Set();
397
+ for (const commandName of this.#names()) {
398
+ const namespace = namespaceOf(commandName);
399
+ if (namespace !== null)
400
+ found.add(namespace);
401
+ }
402
+ return [...found].sort((a, b) => a.localeCompare(b));
403
+ }
404
+ /** Every registered alias — `static aliases` and the rc file's alike. */
405
+ getAliases() {
406
+ return [...this.#aliases.keys()].sort((a, b) => a.localeCompare(b));
407
+ }
408
+ /** The command an alias stands for, or null when the alias is unknown. */
409
+ getAliasCommand(alias) {
410
+ const expansion = this.#aliases.get(alias);
411
+ if (expansion === undefined)
412
+ return null;
413
+ return this.getCommand(expansion[0] ?? '');
414
+ }
415
+ /** Every alias pointing at this command. */
416
+ getCommandAliases(commandName) {
417
+ return this.#aliasesFor(commandName);
418
+ }
419
+ /**
420
+ * Names close enough to this one to be what the user meant — Console's
421
+ * suggestions. Aliases are included: they are names one can legitimately
422
+ * type, so a typo on one deserves the same help.
423
+ */
424
+ getCommandSuggestions(name) {
425
+ return suggestFrom(name, [...this.#names(), ...this.#aliases.keys()]);
426
+ }
427
+ getNamespaceSuggestions(name) {
428
+ return suggestFrom(name, this.getNamespaces());
429
+ }
430
+ async handle(rawArgv, nodeArgs = []) {
431
+ if (this.#state === 'running') {
432
+ throw new ReamError('E_CONSOLE_KERNEL_RUNNING', 'A command is already running through this kernel.', { hint: 'One kernel drives one command line. Use exec() to call another command.' });
433
+ }
434
+ if (this.#state === 'completed') {
435
+ throw new ReamError('E_CONSOLE_KERNEL_COMPLETED', 'The kernel has finished; it cannot run another command line.', { hint: 'Create a fresh kernel.' });
436
+ }
437
+ await this.boot();
438
+ this.#state = 'running';
439
+ try {
440
+ return await this.#dispatch(rawArgv, nodeArgs);
441
+ }
442
+ catch (error) {
443
+ // The command line owns the process, so a failure is REPORTED here rather
444
+ // than thrown at a caller who has nothing better to do with it — Console's
445
+ // rule. `exec()` is the path that rethrows.
446
+ this.exitCode = 1;
447
+ process.exitCode = 1;
448
+ await this.errorHandler.render(error, this);
449
+ return { staysAlive: false };
450
+ }
451
+ finally {
452
+ // In `finally`, not after the call: `handle()` returns early on help, on
453
+ // an unknown command and on a short-circuiting flag listener. A state set
454
+ // on the success path alone lies on every other one.
455
+ this.#state = 'completed';
456
+ }
457
+ }
458
+ async #dispatch(rawArgv, nodeArgs) {
459
+ const argv = this.#expandAlias(this.#hoistGlobalFlags(rawArgv));
460
+ const [name, ...rest] = argv;
461
+ // Resolved through `find()`, like Console's own CLI path: it is what runs the
462
+ // finding/loading/loaded hooks, and a tool listening to them must see the
463
+ // command line, not only `exec()`.
464
+ //
465
+ // The default command goes through `find()` too: `ream` bare and
466
+ // `ream <command>` must run the same cycle, hooks included. Resolved by
467
+ // NAME, so an application that replaced it gets its own.
468
+ const target = name ?? this.getDefaultCommand().commandName;
469
+ let Command;
470
+ try {
471
+ Command = await this.find(target);
472
+ }
473
+ catch (error) {
474
+ // An unknown name gets the listing's help, not a stack trace: a typo is
475
+ // the most common way to reach this.
476
+ if (isReamError(error, 'E_CONSOLE_COMMAND_NOT_FOUND')) {
477
+ this.#reportUnknown(target);
478
+ return { staysAlive: false };
479
+ }
480
+ throw error;
481
+ }
482
+ const argvForCommand = name === undefined ? [] : rest;
483
+ const parsed = this.#parse(Command, argvForCommand, nodeArgs, true);
484
+ if (await this.#runFlagListeners(Command, parsed))
485
+ return { staysAlive: false };
486
+ this.#validate(Command, parsed);
487
+ const instance = await this.#runCommand(Command, parsed, true);
488
+ this.#mainCommand = instance;
489
+ const failure = Reflect.get(instance, 'error');
490
+ if (failure !== undefined)
491
+ throw failure;
492
+ const exitCode = Reflect.get(instance, 'exitCode');
493
+ if (typeof exitCode === 'number')
494
+ this.exitCode = exitCode;
495
+ if (typeof exitCode === 'number' && exitCode !== 0)
496
+ process.exitCode = exitCode;
497
+ return { staysAlive: Command.options?.staysAlive === true };
498
+ }
499
+ /**
500
+ * Parse, build and run one command through its full lifecycle.
501
+ *
502
+ * Shared by `handle()` (the CLI path, which rethrows) and `exec()` (the
503
+ * programmatic path, which reports through the returned instance). Errors are
504
+ * recorded on the instance rather than thrown, so both callers decide what to
505
+ * do with them instead of the logic being written twice.
506
+ */
507
+ /**
508
+ * Read argv against a command's declarations, global flags included.
509
+ *
510
+ * Separate from {@link #runCommand} because the main command's parsed input
511
+ * is needed BEFORE the command is built: Console's global-flag listeners see it,
512
+ * and one of them may end the dispatch.
513
+ */
514
+ #parse(Command, argv, nodeArgs, withGlobalFlags) {
515
+ const parsed = parseArgv(argv, {
516
+ args: Command.args,
517
+ // Global flags belong to the command line: they are merged for the
518
+ // command invoked there, and NOT for `exec()`, as in Console. A caller
519
+ // passing `--no-ansi` to exec() is passing a flag the command does not
520
+ // accept, and hearing so is the point.
521
+ flags: withGlobalFlags ? this.#flagsFor(Command) : (Command.flags ?? []),
522
+ allowUnknownFlags: Command.options?.allowUnknownFlags,
523
+ commandName: Command.commandName,
524
+ // Leniently: the required inputs are checked AFTER the global-flag
525
+ // listeners have had their say, which is what makes `--help` work on a
526
+ // command whose flags are required (Console's order).
527
+ validate: false,
528
+ });
529
+ parsed.nodeArgs = [...nodeArgs];
530
+ return parsed;
531
+ }
532
+ /** Hold the parsed input to the command's declarations — after the listeners. */
533
+ #validate(Command, parsed) {
534
+ validateParsed(parsed, {
535
+ args: Command.args,
536
+ flags: Command.flags,
537
+ allowUnknownFlags: Command.options?.allowUnknownFlags,
538
+ commandName: Command.commandName,
539
+ });
540
+ }
541
+ /**
542
+ * Run the listeners of the global flags that were passed. Returns whether one
543
+ * of them ended the dispatch.
544
+ *
545
+ * Console runs them for the main command only, and before it is built: a command
546
+ * that will not run must not boot the application first.
547
+ */
548
+ async #runFlagListeners(Command, parsed) {
549
+ for (const [flagName, listener] of this.#flagListeners) {
550
+ if (parsed.flags[flagName] === undefined)
551
+ continue;
552
+ // A command redeclaring the name owns the flag entirely — the value in
553
+ // `parsed.flags` is ITS value, and handing it to the global listener
554
+ // would act on something that was never the global flag.
555
+ if (Command.flags?.some((flag) => flag.flagName === flagName))
556
+ continue;
557
+ if ((await listener(Command, this, parsed)) === true)
558
+ return true;
559
+ }
560
+ return false;
561
+ }
562
+ /**
563
+ * Build a command: inject, plug in the plumbing, hydrate. Everything up to
564
+ * the lifecycle, shared by {@link create} and {@link #runCommand}.
565
+ */
566
+ async #build(Command, parsed, isMain, ui) {
567
+ // The application is resolved BEFORE the command is built: with one
568
+ // available, the command goes through the container so constructor
569
+ // dependencies are injected (Console parity), and its lifecycle hooks are
570
+ // invoked through `container.call` so `@inject()` works on them too.
571
+ const app = await this.#resolveApp(Command);
572
+ const instance = app === undefined ? new Command() : await app.container.make(Command);
573
+ Object.assign(instance, {
574
+ logger: ui.logger,
575
+ ui,
576
+ kernel: this,
577
+ colors: ui.colors,
578
+ // `handle()` is the command line; `exec()` is another caller.
579
+ isMain,
580
+ prompt: this.#prompt ?? new Prompt(),
581
+ // Console exposes every parsed input, not only the declared ones.
582
+ parsed,
583
+ onTerminate: this.#onTerminate,
584
+ });
585
+ this.#attachApp(instance, Command, app);
586
+ // Values after the plumbing, as Console does: the command's own `hydrate()`
587
+ // when it has one — a ported command may call it again, it is idempotent —
588
+ // and a plain assignment for a class declared structurally, which has none.
589
+ const hydrate = Reflect.get(instance, 'hydrate');
590
+ if (typeof hydrate === 'function')
591
+ Reflect.apply(hydrate, instance, []);
592
+ else
593
+ assignParsedValues(instance, Command.args ?? [], Command.flags ?? [], parsed);
594
+ // The application is handed back rather than resolved twice: booting it is
595
+ // not free, and `#resolveApp` is what starts it.
596
+ return { instance, app };
597
+ }
598
+ /**
599
+ * Build a command, ready to run — the default executor's `create`.
600
+ *
601
+ * @internal Public only because {@link commandExecutor} is a static object
602
+ * and cannot reach the kernel's privates.
603
+ */
604
+ async buildCommand(Command, parsed, context = { isMain: false, ui: this.ui }) {
605
+ const { instance, app } = await this.#build(Command, parsed, context.isMain, context.ui);
606
+ // Kept off the instance: `this.app` throws by design when the command did
607
+ // not ask for the application, and the lifecycle still needs to know
608
+ // whether there is a container to call through.
609
+ if (app !== undefined)
610
+ this.#apps.set(instance, app);
611
+ return instance;
612
+ }
613
+ /**
614
+ * Run `prepare`, `interact` and `run` — the default executor's `run`.
615
+ *
616
+ * `completed` is deliberately NOT here: it must run even when this throws,
617
+ * which is the kernel's business, not the executor's.
618
+ *
619
+ * @internal See {@link buildCommand}.
620
+ */
621
+ async runLifecycle(command) {
622
+ await this.#invoke(command, 'prepare');
623
+ await this.#invoke(command, 'interact');
624
+ return this.#invoke(command, 'run');
625
+ }
626
+ /** Call a lifecycle hook, through the container when there is one. */
627
+ async #invoke(command, hook) {
628
+ if (typeof command[hook] !== 'function')
629
+ return undefined;
630
+ const app = this.#apps.get(command);
631
+ if (app === undefined)
632
+ return command[hook]();
633
+ return app.container.call(command, hook);
634
+ }
635
+ async #runCommand(Command, parsed, isMain = false, ui = this.ui) {
636
+ const instance = await this.#executor.create(Command, parsed, this, { isMain, ui });
637
+ const invoke = (hook) => this.#invoke(instance, hook);
638
+ // Console's order: prepare → interact → run → completed. `completed` runs even
639
+ // when an earlier stage threw, and can mark the error handled.
640
+ let failure;
641
+ let result;
642
+ await this.#emit(this.#hooks.executing, (handler) => handler(instance, isMain));
643
+ try {
644
+ // Traced: an APM agent subscribes to `ream.command.exec` and sees every
645
+ // run without the kernel knowing it is watched. The payload is only built
646
+ // when somebody listens.
647
+ result = await (commandExec.hasSubscribers
648
+ ? commandExec.tracePromise(() => Promise.resolve(this.#executor.run(instance, this)), {
649
+ command: Command,
650
+ commandInstance: instance,
651
+ argv: parsed.args.map(String),
652
+ })
653
+ : this.#executor.run(instance, this));
654
+ }
655
+ catch (err) {
656
+ failure = err;
657
+ }
658
+ // Assigned before `completed()` so the hook can inspect them.
659
+ const ran = Object.assign(instance, { error: failure, result });
660
+ if (typeof ran.completed === 'function') {
661
+ try {
662
+ const handled = await invoke('completed');
663
+ if (handled === true)
664
+ ran.error = undefined;
665
+ }
666
+ catch (err) {
667
+ // `completed` is part of the lifecycle, so a throw from it is an
668
+ // execution failure like any other — it must not escape and turn
669
+ // `exec()` into a rejection. When an earlier stage had already failed,
670
+ // that first error is kept as the `cause` rather than lost.
671
+ ran.error =
672
+ ran.error === undefined || !(err instanceof Error)
673
+ ? err
674
+ : Object.assign(err, { cause: err.cause ?? ran.error });
675
+ }
676
+ }
677
+ // Console's convention: a command that failed without setting its own code
678
+ // exits 1.
679
+ const scored = Object.assign(ran, {
680
+ exitCode: typeof ran.exitCode === 'number' ? ran.exitCode : ran.error === undefined ? 0 : 1,
681
+ });
682
+ // Only when it finished: Console runs `executed` after the executor returns, so
683
+ // a throw skips it. A hook counting successes must not see a failure —
684
+ // and `completed()` marking the error handled IS a finish.
685
+ if (scored.error === undefined) {
686
+ await this.#emit(this.#hooks.executed, (handler) => handler(scored, isMain));
687
+ }
688
+ // Attached rather than inherited: a command declared structurally by an
689
+ // agnostic package never extends BaseCommand, and `exec()` promises these
690
+ // to every caller. A class that defines its own keeps it — `Object.assign`
691
+ // only fills what is missing here because we spread ours first.
692
+ return Object.assign(scored, createAssertions(scored, ui), {
693
+ toJSON: createSnapshot(scored, Command.commandName, Command.options ?? {}),
694
+ });
695
+ }
696
+ /**
697
+ * Run a command programmatically — Console's `consoleApp.exec()`.
698
+ *
699
+ * It REJECTS when the command fails, as Console does: the error is recorded on
700
+ * the command (`error`, `exitCode` 1) and then rethrown, so a caller cannot
701
+ * mistake a failure for a success by forgetting to look. `process.exitCode`
702
+ * is left alone — only the command line owns that.
703
+ *
704
+ * To inspect a failing command instead of catching, build it with
705
+ * {@link create} and drive it yourself, or run it through `handle()` and read
706
+ * {@link getMainCommand}.
707
+ */
708
+ async exec(commandName, argv = [], options = {}) {
709
+ if (this.#state === 'idle')
710
+ await this.boot();
711
+ if (this.#state === 'completed') {
712
+ throw new ReamError('E_CONSOLE_KERNEL_COMPLETED', `The kernel has finished; "${commandName}" cannot be run through it.`, { hint: 'Create a fresh kernel to execute more commands.' });
713
+ }
714
+ const expanded = this.#expandAlias([commandName, ...argv]);
715
+ const [name, ...rest] = expanded;
716
+ const Command = await this.find(name ?? commandName);
717
+ // A caller-supplied UI is what silences a command, or captures its output
718
+ // without touching the kernel's own (Console `exec(..., { ui })`).
719
+ const parsed = this.#parse(Command, rest, [], false);
720
+ this.#validate(Command, parsed);
721
+ const command = await this.#runCommand(Command, parsed, false, options.ui);
722
+ if (command.error !== undefined)
723
+ throw command.error;
724
+ return command;
725
+ }
726
+ /**
727
+ * Build a command instance without running it (Console `kernel.create`).
728
+ *
729
+ * Parsed, injected and hydrated — everything `exec()` does up to the
730
+ * lifecycle. The escape hatch for driving a command by hand: run it with
731
+ * `command.exec()` and inspect it whatever it does.
732
+ */
733
+ async create(Command, argv = []) {
734
+ if (this.#state === 'idle')
735
+ await this.boot();
736
+ const { instance } = await this.#build(Command, this.#parse(Command, argv, [], false), false, this.ui);
737
+ // The instance IS of the class handed in — built by `new Command()` or by
738
+ // the container from that same class. Typing it as the structural contract
739
+ // would hide the command's own API from the caller who named it.
740
+ return instance;
741
+ }
742
+ /** Is this name — or alias — dispatchable? Console's `consoleApp.hasCommand()`. */
743
+ hasCommand(name) {
744
+ const known = (commandName) => this.#commands.has(commandName) || this.#pending.has(commandName);
745
+ if (known(name))
746
+ return true;
747
+ const expansion = this.#aliases.get(name);
748
+ return expansion !== undefined && known(expansion[0] ?? '');
749
+ }
750
+ /**
751
+ * Give the command its application — booting first when it asked for it.
752
+ *
753
+ * When it did not, `this.app` is a throwing accessor rather than
754
+ * `undefined`: a command that resolves a service without declaring
755
+ * `startApp: true` gets a message naming the fix, instead of a
756
+ * "cannot read properties of undefined" ten frames deep.
757
+ */
758
+ /**
759
+ * Boot the application when the command asked for it, and hand it back so the
760
+ * command can be built through the container.
761
+ */
762
+ async #resolveApp(Command) {
763
+ if (Command.options?.startApp !== true)
764
+ return undefined;
765
+ if (this.#startApp === undefined) {
766
+ throw new ReamError('E_CONSOLE_NO_APP', `"${Command.commandName}" requires a booted application, but this kernel was created without one.`, { hint: 'Dispatch through the Ignitor console kernel rather than a bare Kernel instance.' });
767
+ }
768
+ return this.#startApp();
769
+ }
770
+ #attachApp(instance, Command, app) {
771
+ if (app === undefined) {
772
+ Object.defineProperty(instance, 'app', {
773
+ configurable: true,
774
+ get: () => {
775
+ throw new ReamError('E_CONSOLE_APP_NOT_STARTED', `"${Command.commandName}" accessed this.app without booting the application.`, { hint: `Add: static options: CommandOptions = { startApp: true }` });
776
+ },
777
+ });
778
+ return;
779
+ }
780
+ Object.assign(instance, { app });
781
+ }
782
+ /**
783
+ * Swap a leading alias for what it stands for. Only the first token is
784
+ * considered: an alias is a command shorthand, not a general macro.
785
+ */
786
+ #expandAlias(argv) {
787
+ const [first, ...rest] = argv;
788
+ if (first === undefined)
789
+ return argv;
790
+ const expansion = this.#aliases.get(first);
791
+ return expansion === undefined ? argv : [...expansion, ...rest];
792
+ }
793
+ /**
794
+ * Run a hook's handlers in registration order.
795
+ *
796
+ * The caller passes the invocation rather than the arguments, which keeps
797
+ * every hook's payload typed — a single variadic `emit` would need a cast to
798
+ * correlate the hook name with its parameters.
799
+ */
800
+ async #emit(handlers, invoke) {
801
+ for (const handler of handlers)
802
+ await invoke(handler);
803
+ }
804
+ /** The command's own flags, plus the global ones it did not redeclare. */
805
+ #flagsFor(Command) {
806
+ const own = [...(Command.flags ?? [])];
807
+ const declared = new Set(own.map((flag) => flag.flagName));
808
+ return [...own, ...this.#globalFlags.filter((flag) => !declared.has(flag.flagName))];
809
+ }
810
+ /**
811
+ * Move a global flag written BEFORE the command name to after it.
812
+ *
813
+ * `ream --no-ansi list` has to work — that is where a user naturally puts a
814
+ * CLI-wide switch — but the command name has to be the first token for the
815
+ * registry lookup. Moving them keeps ONE parser for global and command flags
816
+ * alike, instead of a second, hand-rolled scan.
817
+ */
818
+ #hoistGlobalFlags(argv) {
819
+ const leading = [];
820
+ const rest = [...argv];
821
+ while (rest.length > 0) {
822
+ const token = rest[0];
823
+ if (token === '--' || !token.startsWith('-'))
824
+ break;
825
+ const name = token.replace(/^--?(no-)?/, '').split('=')[0] ?? '';
826
+ const meta = this.#globalFlags.find((flag) => flag.flagName === name || flag.alias.includes(name));
827
+ if (meta === undefined)
828
+ break;
829
+ leading.push(rest.shift());
830
+ // A value-taking flag written apart from its value takes the next token
831
+ // with it, or the value would be read as the command name.
832
+ if (meta.type !== 'boolean' && !token.includes('=') && rest[1] !== undefined) {
833
+ leading.push(rest.shift());
834
+ }
835
+ }
836
+ if (leading.length === 0)
837
+ return [...argv];
838
+ const [command, ...args] = rest;
839
+ return command === undefined ? leading : [command, ...args, ...leading];
840
+ }
841
+ #reportUnknown(name) {
842
+ // Aliases are names the user can legitimately type, so a typo on one has to
843
+ // be suggestible too.
844
+ renderErrorWithSuggestions(this.logger, `Unknown command "${name}".`, [
845
+ ...this.getCommandSuggestions(name).slice(0, 1),
846
+ ]);
847
+ process.stderr.write(` Run "${this.#binaryName} list" to see available commands.\n`);
848
+ process.exitCode = 1;
849
+ }
850
+ /**
851
+ * `list` — commands grouped by namespace, as Console does.
852
+ *
853
+ * `namespaces` narrows the output to those groups; an empty list shows
854
+ * everything. `make` selects `make:*`, and the empty string the commands that
855
+ * have no namespace at all.
856
+ */
857
+ printList(asJson = false, namespaces = []) {
858
+ // Off the metadata, not the classes: listing the commands must not import
859
+ // the ones a loader has only announced.
860
+ const all = this.#allMetadata();
861
+ const commands = namespaces.length === 0
862
+ ? all
863
+ : all.filter((command) => namespaces.includes(command.namespace ?? ''));
864
+ if (asJson) {
865
+ // The full metadata contract, not a three-field summary: tooling reads
866
+ // this to build help, completions or a command palette.
867
+ this.logger.log(JSON.stringify(commands, null, 2));
868
+ return;
869
+ }
870
+ this.logger.log(`\n${colourise('Usage:', 'yellow')} ${this.#binaryName} <command> [options]\n`);
871
+ // What the CLI says about itself, when it was told anything (Console
872
+ // `kernel.info`): binary name, framework and app versions.
873
+ if (this.info.size > 0) {
874
+ for (const [label, value] of this.info) {
875
+ this.logger.log(` ${colourise(label, 'dim')} ${String(value)}`);
876
+ }
877
+ this.logger.log('');
878
+ }
879
+ if (commands.length === 0) {
880
+ this.logger.log(' No commands registered.\n');
881
+ return;
882
+ }
883
+ const width = Math.max(...commands.map((command) => command.commandName.length));
884
+ const groups = new Map();
885
+ for (const command of commands) {
886
+ const group = command.namespace ?? '';
887
+ const bucket = groups.get(group);
888
+ if (bucket === undefined)
889
+ groups.set(group, [command]);
890
+ else
891
+ bucket.push(command);
892
+ }
893
+ // Ungrouped commands first, then each namespace.
894
+ for (const [group, entries] of [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))) {
895
+ this.logger.log(group === '' ? colourise('Available commands', 'yellow') : colourise(group, 'yellow'));
896
+ for (const command of entries) {
897
+ const suffix = command.aliases.length === 0 ? '' : colourise(` [${command.aliases.join(', ')}]`, 'dim');
898
+ this.logger.log(` ${colourise(command.commandName.padEnd(width), 'green')} ${command.description}${suffix}`);
899
+ }
900
+ this.logger.log('');
901
+ }
902
+ }
903
+ /**
904
+ * Every alias pointing at this command — both `static aliases` and the rc
905
+ * file's `commandsAliases`, which only differ in where they were declared.
906
+ */
907
+ #aliasesFor(commandName) {
908
+ const found = [];
909
+ for (const [alias, expansion] of this.#aliases) {
910
+ if (expansion[0] === commandName)
911
+ found.push(alias);
912
+ }
913
+ return found.sort((a, b) => a.localeCompare(b));
914
+ }
915
+ /** Every command name the kernel knows, loaded or merely announced. */
916
+ #names() {
917
+ return [...new Set([...this.#commands.keys(), ...this.#pending.keys()])];
918
+ }
919
+ /**
920
+ * The metadata of every command, sorted by name.
921
+ *
922
+ * A loaded class wins over the announcement it came from: once imported, the
923
+ * class IS the contract, and a stale manifest must not describe it.
924
+ */
925
+ #allMetadata() {
926
+ const all = new Map();
927
+ for (const [commandName, pending] of this.#pending) {
928
+ all.set(commandName, { ...pending.metadata, aliases: this.#aliasesFor(commandName) });
929
+ }
930
+ for (const command of this.#commands.values()) {
931
+ all.set(command.commandName, this.#metadataFor(command));
932
+ }
933
+ return [...all.values()].sort((a, b) => a.commandName.localeCompare(b.commandName));
934
+ }
935
+ /**
936
+ * A command's metadata with the aliases the KERNEL knows about — a command
937
+ * declares its own, but the rc file can add more, and a consumer reading the
938
+ * metadata has no way to find those on its own.
939
+ */
940
+ #metadataFor(command) {
941
+ return { ...serializeCommand(command), aliases: this.#aliasesFor(command.commandName) };
942
+ }
943
+ /** `<command> --help` — usage, arguments and flags for one command. */
944
+ printCommandHelp(command) {
945
+ // Metadata is enough to describe a command, and it is what the `help`
946
+ // command holds: explaining a command must not import it.
947
+ const meta = typeof command === 'function' ? serializeCommand(command) : command;
948
+ const args = meta.args;
949
+ const flags = meta.flags;
950
+ const usage = [this.#binaryName, meta.commandName];
951
+ for (const arg of args) {
952
+ const label = arg.type === 'spread' ? `...${arg.argumentName}` : arg.argumentName;
953
+ usage.push(arg.required ? `<${label}>` : `[${label}]`);
954
+ }
955
+ if (flags.length > 0)
956
+ usage.push('[options]');
957
+ this.logger.log(`\n${colourise('Usage:', 'yellow')} ${usage.join(' ')}\n`);
958
+ this.logger.log(` ${meta.description}\n`);
959
+ const help = meta.help;
960
+ if (help !== undefined) {
961
+ // Console substitutes `{{ binaryName }}` so a help block can show a runnable
962
+ // example without hardcoding the binary's name.
963
+ for (const line of Array.isArray(help) ? help : [help]) {
964
+ this.logger.log(` ${line.replace(/\{\{\s*binaryName\s*\}\}/g, this.#binaryName)}`);
965
+ }
966
+ this.logger.log('');
967
+ }
968
+ if (args.length > 0) {
969
+ this.logger.log(colourise('Arguments', 'yellow'));
970
+ const width = Math.max(...args.map((arg) => arg.argumentName.length));
971
+ for (const arg of args) {
972
+ const notes = [
973
+ arg.required ? undefined : 'optional',
974
+ arg.default === undefined ? undefined : `default: ${String(arg.default)}`,
975
+ ].filter((note) => note !== undefined);
976
+ const suffix = notes.length > 0 ? colourise(` (${notes.join(', ')})`, 'dim') : '';
977
+ this.logger.log(` ${colourise(arg.argumentName.padEnd(width), 'green')} ${arg.description ?? ''}${suffix}`);
978
+ }
979
+ this.logger.log('');
980
+ }
981
+ // The command's own flags, then the global ones under their own heading —
982
+ // a flag every command accepts but that no help mentions is a flag nobody
983
+ // discovers.
984
+ this.#printFlags('Options', flags);
985
+ this.#printFlags('Global flags', this.#globalFlags);
986
+ }
987
+ #printFlags(heading, flags) {
988
+ if (flags.length > 0) {
989
+ this.logger.log(colourise(heading, 'yellow'));
990
+ const labels = flags.map((flag) => {
991
+ const aliases = flag.alias.map((alias) => `-${alias}`).join(', ');
992
+ // Booleans are always negatable; this only decides whether help says so.
993
+ const negated = flag.type === 'boolean' && flag.showNegatedVariantInHelp === true
994
+ ? ` | --no-${flag.flagName}`
995
+ : '';
996
+ return `${aliases === '' ? '' : `${aliases}, `}--${flag.flagName}${flag.type === 'boolean' ? '' : `=<${flag.type}>`}${negated}`;
997
+ });
998
+ const width = Math.max(...labels.map((label) => label.length));
999
+ flags.forEach((flag, index) => {
1000
+ const notes = [
1001
+ flag.required ? 'required' : undefined,
1002
+ flag.default === undefined ? undefined : `default: ${String(flag.default)}`,
1003
+ ].filter((note) => note !== undefined);
1004
+ const suffix = notes.length > 0 ? colourise(` (${notes.join(', ')})`, 'dim') : '';
1005
+ this.logger.log(` ${colourise((labels[index] ?? '').padEnd(width), 'green')} ${flag.description ?? ''}${suffix}`);
1006
+ });
1007
+ this.logger.log('');
1008
+ }
1009
+ }
1010
+ }
1011
+ /**
1012
+ * Split an alias expansion into argv tokens, honouring quotes.
1013
+ *
1014
+ * A plain `split(/\s+/)` breaks `make:controller --name "Blog Post"` into four
1015
+ * tokens instead of three, so any alias carrying a value with a space was
1016
+ * unrepresentable. Backslash escapes the next character.
1017
+ */
1018
+ function tokeniseExpansion(expansion) {
1019
+ const tokens = [];
1020
+ let current = '';
1021
+ let quote;
1022
+ let started = false;
1023
+ for (let i = 0; i < expansion.length; i++) {
1024
+ const char = expansion[i];
1025
+ if (char === '\\' && i + 1 < expansion.length) {
1026
+ current += expansion[++i];
1027
+ started = true;
1028
+ continue;
1029
+ }
1030
+ if (quote !== undefined) {
1031
+ if (char === quote)
1032
+ quote = undefined;
1033
+ else
1034
+ current += char;
1035
+ continue;
1036
+ }
1037
+ if (char === '"' || char === "'") {
1038
+ quote = char;
1039
+ started = true;
1040
+ continue;
1041
+ }
1042
+ if (/\s/.test(char)) {
1043
+ if (started) {
1044
+ tokens.push(current);
1045
+ current = '';
1046
+ started = false;
1047
+ }
1048
+ continue;
1049
+ }
1050
+ current += char;
1051
+ started = true;
1052
+ }
1053
+ if (started)
1054
+ tokens.push(current);
1055
+ return tokens;
1056
+ }
1057
+ /**
1058
+ * A command's metadata, whether or not it extends `BaseCommand`.
1059
+ *
1060
+ * A class declared structurally by an agnostic package has no `serialize()`, so
1061
+ * the fields are read off the statics it does carry.
1062
+ */
1063
+ export function serializeCommand(command) {
1064
+ const own = Reflect.get(command, 'serialize');
1065
+ if (typeof own === 'function') {
1066
+ const serialized = Reflect.apply(own, command, []);
1067
+ if (isSerialized(serialized))
1068
+ return serialized;
1069
+ }
1070
+ return {
1071
+ commandName: command.commandName,
1072
+ namespace: namespaceOf(command.commandName),
1073
+ description: command.description,
1074
+ help: command.help,
1075
+ aliases: [...(command.aliases ?? [])],
1076
+ options: { ...command.options },
1077
+ args: (command.args ?? []).map(({ parse: _p, ...rest }) => rest),
1078
+ flags: (command.flags ?? []).map(({ parse: _p, ...rest }) => rest),
1079
+ };
1080
+ }
1081
+ /** `make:controller` → `make`; a name without a colon has no namespace. */
1082
+ function namespaceOf(commandName) {
1083
+ const colon = commandName.indexOf(':');
1084
+ return colon === -1 ? null : commandName.slice(0, colon);
1085
+ }
1086
+ function isSerialized(value) {
1087
+ return typeof value === 'object' && value !== null && 'commandName' in value;
1088
+ }
1089
+ /**
1090
+ * The candidates close enough to `input` to be worth suggesting, nearest first.
1091
+ *
1092
+ * One ranking, used both by the "Did you mean?" line and by the public
1093
+ * suggestion methods — two implementations would eventually disagree about
1094
+ * what counts as close.
1095
+ */
1096
+ function suggestFrom(input, candidates) {
1097
+ const threshold = Math.max(2, Math.floor(input.length / 3));
1098
+ return candidates
1099
+ .map((candidate) => ({ candidate, score: distance(input, candidate) }))
1100
+ .filter(({ score }) => score <= threshold)
1101
+ .sort((a, b) => a.score - b.score || a.candidate.localeCompare(b.candidate))
1102
+ .map(({ candidate }) => candidate);
1103
+ }
1104
+ function distance(a, b) {
1105
+ const rows = a.length + 1;
1106
+ const cols = b.length + 1;
1107
+ let previous = Array.from({ length: cols }, (_, index) => index);
1108
+ for (let i = 1; i < rows; i++) {
1109
+ const current = [i];
1110
+ for (let j = 1; j < cols; j++) {
1111
+ const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
1112
+ const insertion = (current[j - 1] ?? 0) + 1;
1113
+ const deletion = (previous[j] ?? 0) + 1;
1114
+ current[j] = Math.min(substitution, insertion, deletion);
1115
+ }
1116
+ previous = current;
1117
+ }
1118
+ return previous[cols - 1] ?? 0;
1119
+ }
1120
+ //# sourceMappingURL=Kernel.js.map