@c9up/ream 0.1.25 → 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.
- package/README.md +65 -0
- package/dist/ConfigLoader.d.ts +46 -5
- package/dist/ConfigLoader.d.ts.map +1 -1
- package/dist/ConfigLoader.js +60 -9
- package/dist/ConfigLoader.js.map +1 -1
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +46 -6
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +115 -7
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +254 -40
- package/dist/Ignitor.js.map +1 -1
- package/dist/console/BaseCommand.d.ts +217 -0
- package/dist/console/BaseCommand.d.ts.map +1 -0
- package/dist/console/BaseCommand.js +340 -0
- package/dist/console/BaseCommand.js.map +1 -0
- package/dist/console/Console.d.ts +86 -0
- package/dist/console/Console.d.ts.map +1 -0
- package/dist/console/Console.js +134 -0
- package/dist/console/Console.js.map +1 -0
- package/dist/console/ExceptionHandler.d.ts +28 -0
- package/dist/console/ExceptionHandler.d.ts.map +1 -0
- package/dist/console/ExceptionHandler.js +76 -0
- package/dist/console/ExceptionHandler.js.map +1 -0
- package/dist/console/HelpCommand.d.ts +16 -0
- package/dist/console/HelpCommand.d.ts.map +1 -0
- package/dist/console/HelpCommand.js +40 -0
- package/dist/console/HelpCommand.js.map +1 -0
- package/dist/console/IndexGenerator.d.ts +24 -0
- package/dist/console/IndexGenerator.d.ts.map +1 -0
- package/dist/console/IndexGenerator.js +65 -0
- package/dist/console/IndexGenerator.js.map +1 -0
- package/dist/console/Kernel.d.ts +298 -0
- package/dist/console/Kernel.d.ts.map +1 -0
- package/dist/console/Kernel.js +1120 -0
- package/dist/console/Kernel.js.map +1 -0
- package/dist/console/ListCommand.d.ts +18 -0
- package/dist/console/ListCommand.d.ts.map +1 -0
- package/dist/console/ListCommand.js +60 -0
- package/dist/console/ListCommand.js.map +1 -0
- package/dist/console/assertions.d.ts +20 -0
- package/dist/console/assertions.d.ts.map +1 -0
- package/dist/console/assertions.js +84 -0
- package/dist/console/assertions.js.map +1 -0
- package/dist/console/cliui.d.ts +248 -0
- package/dist/console/cliui.d.ts.map +1 -0
- package/dist/console/cliui.js +587 -0
- package/dist/console/cliui.js.map +1 -0
- package/dist/console/decorators.d.ts +90 -0
- package/dist/console/decorators.d.ts.map +1 -0
- package/dist/console/decorators.js +142 -0
- package/dist/console/decorators.js.map +1 -0
- package/dist/console/index.d.ts +32 -0
- package/dist/console/index.d.ts.map +1 -0
- package/dist/console/index.js +24 -0
- package/dist/console/index.js.map +1 -0
- package/dist/console/loaders.d.ts +52 -0
- package/dist/console/loaders.d.ts.map +1 -0
- package/dist/console/loaders.js +131 -0
- package/dist/console/loaders.js.map +1 -0
- package/dist/console/parser.d.ts +86 -0
- package/dist/console/parser.d.ts.map +1 -0
- package/dist/console/parser.js +440 -0
- package/dist/console/parser.js.map +1 -0
- package/dist/console/prompts.d.ts +124 -0
- package/dist/console/prompts.d.ts.map +1 -0
- package/dist/console/prompts.js +356 -0
- package/dist/console/prompts.js.map +1 -0
- package/dist/console/tracing.d.ts +17 -0
- package/dist/console/tracing.d.ts.map +1 -0
- package/dist/console/tracing.js +11 -0
- package/dist/console/tracing.js.map +1 -0
- package/dist/console/types.d.ts +255 -0
- package/dist/console/types.d.ts.map +1 -0
- package/dist/console/types.js +25 -0
- package/dist/console/types.js.map +1 -0
- package/dist/console/ui.d.ts +20 -0
- package/dist/console/ui.d.ts.map +1 -0
- package/dist/console/ui.js +31 -0
- package/dist/console/ui.js.map +1 -0
- package/dist/console/utils.d.ts +27 -0
- package/dist/console/utils.d.ts.map +1 -0
- package/dist/console/utils.js +56 -0
- package/dist/console/utils.js.map +1 -0
- package/dist/container/Container.d.ts +23 -3
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +177 -70
- package/dist/container/Container.js.map +1 -1
- package/dist/container/types.d.ts +14 -2
- package/dist/container/types.d.ts.map +1 -1
- package/dist/decorators/Service.d.ts +7 -0
- package/dist/decorators/Service.d.ts.map +1 -1
- package/dist/decorators/Service.js +23 -3
- package/dist/decorators/Service.js.map +1 -1
- package/dist/env/Env.d.ts +29 -4
- package/dist/env/Env.d.ts.map +1 -1
- package/dist/env/Env.js +27 -4
- package/dist/env/Env.js.map +1 -1
- package/dist/env/Secret.d.ts +11 -0
- package/dist/env/Secret.d.ts.map +1 -0
- package/dist/env/Secret.js +36 -0
- package/dist/env/Secret.js.map +1 -0
- package/dist/env/interpolate.d.ts +26 -0
- package/dist/env/interpolate.d.ts.map +1 -0
- package/dist/env/interpolate.js +48 -0
- package/dist/env/interpolate.js.map +1 -0
- package/dist/env/loadEnvFiles.d.ts.map +1 -1
- package/dist/env/loadEnvFiles.js +14 -4
- package/dist/env/loadEnvFiles.js.map +1 -1
- package/dist/env/schema.d.ts +23 -6
- package/dist/env/schema.d.ts.map +1 -1
- package/dist/env/schema.js +67 -33
- package/dist/env/schema.js.map +1 -1
- package/dist/errors/ReamError.d.ts +25 -0
- package/dist/errors/ReamError.d.ts.map +1 -1
- package/dist/errors/ReamError.js +5 -0
- package/dist/errors/ReamError.js.map +1 -1
- package/dist/errors/prettyPrintError.d.ts +9 -0
- package/dist/errors/prettyPrintError.d.ts.map +1 -0
- package/dist/errors/prettyPrintError.js +22 -0
- package/dist/errors/prettyPrintError.js.map +1 -0
- package/dist/events/Emitter.d.ts +1 -1
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +1 -1
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/EventsProvider.d.ts +2 -2
- package/dist/events/EventsProvider.d.ts.map +1 -1
- package/dist/events/EventsProvider.js +3 -3
- package/dist/events/EventsProvider.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts +1 -1
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +2 -2
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/graphql/GraphQLProvider.d.ts.map +1 -1
- package/dist/graphql/GraphQLProvider.js +7 -3
- package/dist/graphql/GraphQLProvider.js.map +1 -1
- package/dist/http/Exception.d.ts +63 -1
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +185 -48
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/HttpContext.d.ts +53 -7
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +56 -15
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/RedirectBuilder.d.ts.map +1 -1
- package/dist/http/RedirectBuilder.js +4 -1
- package/dist/http/RedirectBuilder.js.map +1 -1
- package/dist/http/Request.d.ts +141 -10
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +244 -34
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +176 -9
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +404 -16
- package/dist/http/Response.js.map +1 -1
- package/dist/index.d.ts +19 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -5
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.d.ts +29 -8
- package/dist/middleware/Pipeline.d.ts.map +1 -1
- package/dist/middleware/Pipeline.js +40 -2
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/openapi/OpenApiProvider.js +4 -4
- package/dist/openapi/OpenApiProvider.js.map +1 -1
- package/dist/router/Router.d.ts +223 -31
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +520 -102
- package/dist/router/Router.js.map +1 -1
- package/dist/rpc/RpcProvider.d.ts +1 -0
- package/dist/rpc/RpcProvider.d.ts.map +1 -1
- package/dist/rpc/RpcProvider.js +7 -3
- package/dist/rpc/RpcProvider.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts +2 -2
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +17 -5
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/ScheduleProvider.d.ts.map +1 -1
- package/dist/scheduler/ScheduleProvider.js +7 -15
- package/dist/scheduler/ScheduleProvider.js.map +1 -1
- package/dist/server/Server.d.ts +17 -4
- package/dist/server/Server.d.ts.map +1 -1
- package/dist/server/Server.js +41 -7
- package/dist/server/Server.js.map +1 -1
- package/dist/services/app.d.ts +1 -1
- package/dist/services/app.js +1 -1
- package/dist/services/console.d.ts +21 -0
- package/dist/services/console.d.ts.map +1 -0
- package/dist/services/console.js +35 -0
- package/dist/services/console.js.map +1 -0
- package/dist/session/SessionMiddleware.js +3 -3
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/storage/StaticProvider.js +1 -1
- package/dist/storage/StaticProvider.js.map +1 -1
- package/dist/testing/ApiResponse.d.ts +218 -0
- package/dist/testing/ApiResponse.d.ts.map +1 -0
- package/dist/testing/ApiResponse.js +576 -0
- package/dist/testing/ApiResponse.js.map +1 -0
- package/dist/testing/RequestBuilder.d.ts +155 -35
- package/dist/testing/RequestBuilder.d.ts.map +1 -1
- package/dist/testing/RequestBuilder.js +455 -109
- package/dist/testing/RequestBuilder.js.map +1 -1
- package/dist/testing/TestClient.d.ts +53 -37
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +84 -52
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/utils/Macroable.d.ts +30 -0
- package/dist/utils/Macroable.d.ts.map +1 -0
- package/dist/utils/Macroable.js +43 -0
- package/dist/utils/Macroable.js.map +1 -0
- package/dist/utils/inflect.d.ts +17 -0
- package/dist/utils/inflect.d.ts.map +1 -0
- package/dist/utils/inflect.js +32 -0
- package/dist/utils/inflect.js.map +1 -0
- package/dist/utils/objectPath.d.ts +57 -0
- package/dist/utils/objectPath.d.ts.map +1 -0
- package/dist/utils/objectPath.js +141 -0
- package/dist/utils/objectPath.js.map +1 -0
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +20 -2
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/ConfigLoader.ts +72 -9
- package/src/HttpKernel.ts +53 -15
- package/src/Ignitor.ts +364 -45
- package/src/console/BaseCommand.ts +467 -0
- package/src/console/Console.ts +174 -0
- package/src/console/ExceptionHandler.ts +91 -0
- package/src/console/HelpCommand.ts +37 -0
- package/src/console/IndexGenerator.ts +79 -0
- package/src/console/Kernel.ts +1382 -0
- package/src/console/ListCommand.ts +55 -0
- package/src/console/assertions.ts +114 -0
- package/src/console/cliui.ts +756 -0
- package/src/console/decorators.ts +199 -0
- package/src/console/index.ts +67 -0
- package/src/console/loaders.ts +145 -0
- package/src/console/parser.ts +598 -0
- package/src/console/prompts.ts +548 -0
- package/src/console/tracing.ts +22 -0
- package/src/console/types.ts +280 -0
- package/src/console/ui.ts +37 -0
- package/src/console/utils.ts +69 -0
- package/src/container/Container.ts +198 -72
- package/src/container/types.ts +15 -2
- package/src/decorators/Service.ts +28 -3
- package/src/env/Env.ts +49 -5
- package/src/env/Secret.ts +41 -0
- package/src/env/interpolate.ts +54 -0
- package/src/env/loadEnvFiles.ts +12 -4
- package/src/env/schema.ts +109 -35
- package/src/errors/ReamError.ts +31 -0
- package/src/errors/prettyPrintError.ts +20 -0
- package/src/events/Emitter.ts +2 -2
- package/src/events/EventsProvider.ts +5 -5
- package/src/graphql/GraphQLEngine.ts +4 -4
- package/src/graphql/GraphQLProvider.ts +7 -3
- package/src/http/Exception.ts +214 -37
- package/src/http/HttpContext.ts +87 -18
- package/src/http/RedirectBuilder.ts +4 -1
- package/src/http/Request.ts +264 -34
- package/src/http/Response.ts +478 -27
- package/src/index.ts +41 -6
- package/src/middleware/Pipeline.ts +77 -6
- package/src/openapi/OpenApiProvider.ts +4 -4
- package/src/router/Router.ts +631 -113
- package/src/rpc/RpcProvider.ts +6 -2
- package/src/rpc/RpcRouter.ts +22 -7
- package/src/scheduler/ScheduleProvider.ts +7 -15
- package/src/server/Server.ts +49 -10
- package/src/services/app.ts +1 -1
- package/src/services/console.ts +42 -0
- package/src/session/SessionMiddleware.ts +4 -4
- package/src/storage/StaticProvider.ts +1 -1
- package/src/testing/ApiResponse.ts +691 -0
- package/src/testing/RequestBuilder.ts +577 -137
- package/src/testing/TestClient.ts +110 -77
- package/src/utils/Macroable.ts +54 -0
- package/src/utils/inflect.ts +29 -0
- package/src/utils/objectPath.ts +142 -0
- package/dist/console/CommandRunner.d.ts +0 -18
- package/dist/console/CommandRunner.d.ts.map +0 -1
- package/dist/console/CommandRunner.js +0 -65
- package/dist/console/CommandRunner.js.map +0 -1
- package/dist/decorators/Lazy.d.ts +0 -19
- package/dist/decorators/Lazy.d.ts.map +0 -1
- package/dist/decorators/Lazy.js +0 -53
- package/dist/decorators/Lazy.js.map +0 -1
- package/src/console/CommandRunner.ts +0 -82
- package/src/decorators/Lazy.ts +0 -57
|
@@ -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
|