@h3ravel/musket 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +466 -32
- package/dist/index.d.ts +379 -11
- package/dist/index.js +467 -34
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -29,6 +29,232 @@ let node_path = require("node:path");
|
|
|
29
29
|
node_path = __toESM(node_path, 1);
|
|
30
30
|
let module$1 = require("module");
|
|
31
31
|
let node_fs_promises = require("node:fs/promises");
|
|
32
|
+
//#region src/SignatureBuilder.ts
|
|
33
|
+
/**
|
|
34
|
+
* A fluent builder for constructing a command's signature programmatically,
|
|
35
|
+
* without authoring the string DSL by hand.
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* buildSignature (sig: SignatureBuilder) {
|
|
39
|
+
* return sig
|
|
40
|
+
* .command('queue:work')
|
|
41
|
+
* .describe('Process jobs on the queue')
|
|
42
|
+
* .argument('connection', { description: 'The connection to work', required: false })
|
|
43
|
+
* .option('queue', { description: 'The queue to process', short: 'Q' })
|
|
44
|
+
* .option('once', { description: 'Process a single job and exit' })
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* Musket consumes the builder directly (lossless) for normal commands, and can
|
|
49
|
+
* also reconstruct the equivalent signature string via {@link toString} for
|
|
50
|
+
* anything that still expects the DSL.
|
|
51
|
+
*/
|
|
52
|
+
var SignatureBuilder = class {
|
|
53
|
+
baseName = "";
|
|
54
|
+
commandDescription;
|
|
55
|
+
hidden = false;
|
|
56
|
+
args = [];
|
|
57
|
+
opts = [];
|
|
58
|
+
/**
|
|
59
|
+
* Set the command name, e.g. `make:model` or `queue:work`. A trailing `:`
|
|
60
|
+
* marks a namespace command.
|
|
61
|
+
*/
|
|
62
|
+
command(name) {
|
|
63
|
+
this.baseName = name.trim();
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Alias for {@link command}.
|
|
68
|
+
*
|
|
69
|
+
* @param name
|
|
70
|
+
* @returns
|
|
71
|
+
*/
|
|
72
|
+
name(name) {
|
|
73
|
+
return this.command(name);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Set the command description.
|
|
77
|
+
*
|
|
78
|
+
* @param name
|
|
79
|
+
* @returns
|
|
80
|
+
*/
|
|
81
|
+
describe(description) {
|
|
82
|
+
this.commandDescription = description;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Alias for {@link describe}.
|
|
87
|
+
*
|
|
88
|
+
* @param name
|
|
89
|
+
* @returns
|
|
90
|
+
*/
|
|
91
|
+
description(description) {
|
|
92
|
+
return this.describe(description);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Mark the command as hidden from help output.
|
|
96
|
+
*
|
|
97
|
+
* @param name
|
|
98
|
+
* @returns
|
|
99
|
+
*/
|
|
100
|
+
hide(hidden = true) {
|
|
101
|
+
this.hidden = hidden;
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Add a positional argument.
|
|
106
|
+
*
|
|
107
|
+
* @param name
|
|
108
|
+
* @returns
|
|
109
|
+
*/
|
|
110
|
+
argument(name, definition = {}) {
|
|
111
|
+
this.args.push({
|
|
112
|
+
name: name.trim(),
|
|
113
|
+
...definition
|
|
114
|
+
});
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Add an option/flag.
|
|
119
|
+
*
|
|
120
|
+
* @param name
|
|
121
|
+
* @returns
|
|
122
|
+
*/
|
|
123
|
+
option(name, definition = {}) {
|
|
124
|
+
this.opts.push({
|
|
125
|
+
name: name.replace(/^--?/, "").trim(),
|
|
126
|
+
...definition
|
|
127
|
+
});
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The configured command name (empty when unset).
|
|
132
|
+
*
|
|
133
|
+
* @param name
|
|
134
|
+
* @returns
|
|
135
|
+
*/
|
|
136
|
+
getName() {
|
|
137
|
+
return this.baseName;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The configured description, if any.
|
|
141
|
+
*
|
|
142
|
+
* @param name
|
|
143
|
+
* @returns
|
|
144
|
+
*/
|
|
145
|
+
getDescription() {
|
|
146
|
+
return this.commandDescription;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Whether the command is a namespace command (name ends with `:`).
|
|
150
|
+
*
|
|
151
|
+
* @param name
|
|
152
|
+
* @returns
|
|
153
|
+
*/
|
|
154
|
+
isNamespace() {
|
|
155
|
+
return this.baseName.endsWith(":");
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Whether anything has been configured on this builder.
|
|
159
|
+
*
|
|
160
|
+
* @param name
|
|
161
|
+
* @returns
|
|
162
|
+
*/
|
|
163
|
+
isEmpty() {
|
|
164
|
+
return this.baseName.length === 0;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Build the structured {@link ParsedCommand} consumed by the kernel. This is
|
|
168
|
+
* lossless — no string round-trip — and is the path used for normal commands.
|
|
169
|
+
*
|
|
170
|
+
* @param commandClass
|
|
171
|
+
* @returns
|
|
172
|
+
*/
|
|
173
|
+
toParsed(commandClass) {
|
|
174
|
+
const options = [...this.args.map((arg) => this.argumentToOption(arg)), ...this.opts.map((opt) => this.optionToOption(opt))];
|
|
175
|
+
return {
|
|
176
|
+
baseCommand: this.baseName,
|
|
177
|
+
isNamespaceCommand: false,
|
|
178
|
+
description: this.commandDescription ?? commandClass.getDescription(),
|
|
179
|
+
commandClass,
|
|
180
|
+
options,
|
|
181
|
+
isHidden: this.hidden
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
argumentToOption(arg) {
|
|
185
|
+
const required = arg.required ?? arg.default === void 0;
|
|
186
|
+
const multiple = arg.multiple ?? false;
|
|
187
|
+
let placeholder;
|
|
188
|
+
if (multiple) placeholder = required ? `<${arg.name}...>` : `[${arg.name}...]`;
|
|
189
|
+
return {
|
|
190
|
+
name: arg.name,
|
|
191
|
+
isFlag: false,
|
|
192
|
+
required,
|
|
193
|
+
multiple,
|
|
194
|
+
description: arg.description ?? "",
|
|
195
|
+
choices: arg.choices ?? [],
|
|
196
|
+
defaultValue: arg.default,
|
|
197
|
+
placeholder
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
optionToOption(opt) {
|
|
201
|
+
const long = `--${opt.name}`;
|
|
202
|
+
const flags = opt.short ? [`-${opt.short}`, long] : [long];
|
|
203
|
+
const takesValue = opt.default !== void 0 || opt.requiresValue === true || opt.optionalValue === true || (opt.choices?.length ?? 0) > 0;
|
|
204
|
+
let required = false;
|
|
205
|
+
let placeholder;
|
|
206
|
+
if (takesValue) if (opt.requiresValue === true && opt.default === void 0 && opt.optionalValue !== true) required = true;
|
|
207
|
+
else placeholder = `[${opt.name}]`;
|
|
208
|
+
return {
|
|
209
|
+
name: long,
|
|
210
|
+
flags,
|
|
211
|
+
isFlag: true,
|
|
212
|
+
required,
|
|
213
|
+
placeholder,
|
|
214
|
+
description: opt.description ?? "",
|
|
215
|
+
choices: opt.choices ?? [],
|
|
216
|
+
defaultValue: opt.default ?? (takesValue ? void 0 : false),
|
|
217
|
+
shared: opt.shared,
|
|
218
|
+
isHidden: opt.hidden
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Reconstruct the equivalent signature string in musket's DSL. Used by
|
|
223
|
+
* {@link Command.getSignature} so external consumers (and the namespace path)
|
|
224
|
+
* keep working unchanged.
|
|
225
|
+
*/
|
|
226
|
+
toString() {
|
|
227
|
+
const tokens = [this.baseName];
|
|
228
|
+
for (const arg of this.args) tokens.push(this.argumentToken(arg));
|
|
229
|
+
for (const opt of this.opts) tokens.push(this.optionToken(opt));
|
|
230
|
+
return tokens.join("\n ");
|
|
231
|
+
}
|
|
232
|
+
argumentToken(arg) {
|
|
233
|
+
const required = arg.required ?? arg.default === void 0;
|
|
234
|
+
const multiple = arg.multiple ?? false;
|
|
235
|
+
let token = arg.name;
|
|
236
|
+
if (arg.default !== void 0) token += `=${arg.default}`;
|
|
237
|
+
else if (!required && multiple) token += "?*";
|
|
238
|
+
else if (multiple) token += "*";
|
|
239
|
+
else if (!required) token += "?";
|
|
240
|
+
return this.wrap(token, arg.description, arg.choices);
|
|
241
|
+
}
|
|
242
|
+
optionToken(opt) {
|
|
243
|
+
const takesValue = opt.default !== void 0 || opt.requiresValue === true || opt.optionalValue === true;
|
|
244
|
+
let token = opt.short ? `--${opt.short}|${opt.name}` : `--${opt.name}`;
|
|
245
|
+
if (opt.default !== void 0) token += `=${opt.default}`;
|
|
246
|
+
else if (opt.requiresValue === true && opt.optionalValue !== true) token += "=";
|
|
247
|
+
else if (opt.optionalValue === true) token += "?";
|
|
248
|
+
return this.wrap(token, opt.description, opt.choices, takesValue);
|
|
249
|
+
}
|
|
250
|
+
wrap(token, description, choices, _value) {
|
|
251
|
+
let body = token;
|
|
252
|
+
if (description) body += ` : ${description}`;
|
|
253
|
+
if (choices?.length) body += ` : [${choices.join(", ")}]`;
|
|
254
|
+
return `{${body}}`;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
//#endregion
|
|
32
258
|
//#region src/Core/Command.ts
|
|
33
259
|
var Command = class {
|
|
34
260
|
app;
|
|
@@ -62,6 +288,11 @@ var Command = class {
|
|
|
62
288
|
*/
|
|
63
289
|
description;
|
|
64
290
|
/**
|
|
291
|
+
* Memoized result of {@link resolveBuilder}. `undefined` means "not yet
|
|
292
|
+
* resolved"; `null` means "resolved, no builder in use".
|
|
293
|
+
*/
|
|
294
|
+
resolvedBuilder;
|
|
295
|
+
/**
|
|
65
296
|
* The console command input.
|
|
66
297
|
*
|
|
67
298
|
* @var object
|
|
@@ -142,20 +373,70 @@ var Command = class {
|
|
|
142
373
|
return this;
|
|
143
374
|
}
|
|
144
375
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* @
|
|
376
|
+
* Define the command signature programmatically.
|
|
377
|
+
*
|
|
378
|
+
* Override this instead of (or in addition to) the {@link signature} string
|
|
379
|
+
* to describe the command, its arguments and its options through the fluent
|
|
380
|
+
* {@link SignatureBuilder} — no string DSL required. When the `signature`
|
|
381
|
+
* string is set it always takes precedence and this method is ignored.
|
|
382
|
+
*
|
|
383
|
+
* ```ts
|
|
384
|
+
* buildSignature (sig: SignatureBuilder) {
|
|
385
|
+
* return sig
|
|
386
|
+
* .command('queue:work')
|
|
387
|
+
* .describe('Process jobs on the queue')
|
|
388
|
+
* .argument('connection', { required: false })
|
|
389
|
+
* .option('queue', { short: 'Q', description: 'The queue to process' })
|
|
390
|
+
* .option('once', { description: 'Process a single job and exit' })
|
|
391
|
+
* }
|
|
392
|
+
* ```
|
|
393
|
+
*
|
|
394
|
+
* @param _builder A fresh builder to configure.
|
|
395
|
+
*/
|
|
396
|
+
buildSignature(_builder) {
|
|
397
|
+
/** Override to use. */
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Resolve (and memoize) the signature builder for this command, or `null`
|
|
401
|
+
* when the command uses the string {@link signature} (which always wins) or
|
|
402
|
+
* does not implement {@link buildSignature}.
|
|
403
|
+
*/
|
|
404
|
+
resolveBuilder() {
|
|
405
|
+
if (this.resolvedBuilder !== void 0) return this.resolvedBuilder;
|
|
406
|
+
if (this.signature) return this.resolvedBuilder = null;
|
|
407
|
+
const builder = new SignatureBuilder();
|
|
408
|
+
const result = this.buildSignature(builder);
|
|
409
|
+
const resolved = result instanceof SignatureBuilder ? result : builder;
|
|
410
|
+
return this.resolvedBuilder = resolved.isEmpty() ? null : resolved;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Get the command signature. Returns the string {@link signature} when set,
|
|
414
|
+
* otherwise reconstructs it from {@link buildSignature}.
|
|
415
|
+
*
|
|
416
|
+
* @returns
|
|
148
417
|
*/
|
|
149
418
|
getSignature() {
|
|
150
|
-
return this.signature;
|
|
419
|
+
if (this.signature) return this.signature;
|
|
420
|
+
return this.resolveBuilder()?.toString() ?? this.signature;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* The structured, lossless parsed command when {@link buildSignature} is used
|
|
424
|
+
* for a non-namespace command; `undefined` otherwise (callers then fall back
|
|
425
|
+
* to parsing {@link getSignature}). This avoids a lossy string round-trip.
|
|
426
|
+
*/
|
|
427
|
+
toParsedSignature() {
|
|
428
|
+
const builder = this.resolveBuilder();
|
|
429
|
+
if (!builder || builder.isNamespace()) return;
|
|
430
|
+
return builder.toParsed(this);
|
|
151
431
|
}
|
|
152
432
|
/**
|
|
153
433
|
* Get the command description
|
|
154
|
-
*
|
|
155
|
-
* @returns
|
|
434
|
+
*
|
|
435
|
+
* @returns
|
|
156
436
|
*/
|
|
157
437
|
getDescription() {
|
|
158
|
-
return this.description;
|
|
438
|
+
if (this.description) return this.description;
|
|
439
|
+
return this.resolveBuilder()?.getDescription() ?? this.description;
|
|
159
440
|
}
|
|
160
441
|
/**
|
|
161
442
|
* Get a specific option
|
|
@@ -505,11 +786,8 @@ var ListCommand = class ListCommand extends Command {
|
|
|
505
786
|
return _h3ravel_shared.Logger.describe(_h3ravel_shared.Logger.log(" " + e.name(), "green", false), e.description(), 25, false).join("");
|
|
506
787
|
});
|
|
507
788
|
const list = ListCommand.groupItems(commands);
|
|
508
|
-
/** Output the modules version */
|
|
509
|
-
const version = this.kernel.
|
|
510
|
-
const value = String(e.alias ?? e.name).split("/").pop().replace(/[-_]/g, " ").replace(/cli/gi, (match) => match === "cli" ? "CLI" : match).replace(/^./, (c) => c.toUpperCase());
|
|
511
|
-
return _h3ravel_shared.Logger.log([[`${value}:`, "white"], [e.version, "green"]], " ", false);
|
|
512
|
-
}).join(" | ");
|
|
789
|
+
/** Output the modules version (shared renderer, overridable via config) */
|
|
790
|
+
const version = this.kernel.getVersionString();
|
|
513
791
|
this.newLine();
|
|
514
792
|
console.log(version);
|
|
515
793
|
this.newLine();
|
|
@@ -760,6 +1038,13 @@ var Musket = class Musket {
|
|
|
760
1038
|
name = "musket";
|
|
761
1039
|
config = {};
|
|
762
1040
|
commands = [];
|
|
1041
|
+
/**
|
|
1042
|
+
* Keys (baseCommand, namespace-aware) of commands already registered, so the
|
|
1043
|
+
* same command surfacing from more than one source — e.g. discovered both as
|
|
1044
|
+
* built `dist/*.js` and as `src/*.ts` via the jiti loader — is only added
|
|
1045
|
+
* once. The first registration wins (base commands before discovered ones).
|
|
1046
|
+
*/
|
|
1047
|
+
registeredKeys = /* @__PURE__ */ new Set();
|
|
763
1048
|
program;
|
|
764
1049
|
constructor(app, kernel, baseCommands = [], resolver, tsDownConfig = {}) {
|
|
765
1050
|
this.app = app;
|
|
@@ -808,30 +1093,137 @@ var Musket = class Musket {
|
|
|
808
1093
|
* CLI Commands auto registration
|
|
809
1094
|
*/
|
|
810
1095
|
for await (const pth of glob.glob.stream(paths)) {
|
|
811
|
-
const
|
|
1096
|
+
const file = pth.toString();
|
|
1097
|
+
const name = node_path.default.basename(file).replace(/\.(c|m)?(t|j)s$/, "");
|
|
812
1098
|
try {
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
1099
|
+
const mod = await this.importCommandModule(file);
|
|
1100
|
+
const CommandClass = this.resolveCommandClass(mod, name);
|
|
1101
|
+
if (!CommandClass) {
|
|
1102
|
+
this.warnDiscovery(`No command class export found in ${file}`);
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
commands.push(new CommandClass(this.app, this.kernel));
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
/**
|
|
1108
|
+
* Never swallow load failures silently — a command that throws on
|
|
1109
|
+
* import would otherwise just vanish from the CLI with no clue why.
|
|
1110
|
+
*/
|
|
1111
|
+
this.warnDiscovery(`Failed to load command ${file}: ${error?.message ?? String(error)}`);
|
|
1112
|
+
}
|
|
816
1113
|
}
|
|
817
1114
|
commands.forEach((e) => this.addCommand(e));
|
|
818
1115
|
}
|
|
819
1116
|
/**
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
1117
|
+
* Import a discovered command module with full TypeScript support.
|
|
1118
|
+
*
|
|
1119
|
+
* Native `import()` is attempted first: it loads built `.js`, and works
|
|
1120
|
+
* verbatim in TypeScript-aware runtimes/test loaders (vitest, tsx, Node with
|
|
1121
|
+
* type stripping) — which also keeps their module mocking and single module
|
|
1122
|
+
* registry intact. When native import can't load a TypeScript source (plain
|
|
1123
|
+
* Node throwing on a `.ts`/`.mts`/`.cts` file), it is transpiled on the fly
|
|
1124
|
+
* via jiti (`@h3ravel/shared`'s `importFile`) so commands are discovered
|
|
1125
|
+
* without a prior build. Consumers can bypass all of this with
|
|
1126
|
+
* {@link KernelConfig.importModule}.
|
|
1127
|
+
*
|
|
1128
|
+
* @param file Absolute or cwd-relative path to the command module.
|
|
1129
|
+
*/
|
|
1130
|
+
async importCommandModule(file) {
|
|
1131
|
+
if (this.config.importModule) return await this.config.importModule(file);
|
|
1132
|
+
try {
|
|
1133
|
+
return await import(file);
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
if (/\.(c|m)?ts$/.test(file)) return await (0, _h3ravel_shared.importFile)(file);
|
|
1136
|
+
throw error;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Resolve the command class out of an imported module. Prefers the export
|
|
1141
|
+
* named after the file, then a `default` export, then the first exported
|
|
1142
|
+
* constructor — because a file's name and its exported class name do not
|
|
1143
|
+
* always match.
|
|
1144
|
+
*
|
|
1145
|
+
* @param mod The imported module namespace.
|
|
1146
|
+
* @param name The file name without extension.
|
|
1147
|
+
*/
|
|
1148
|
+
resolveCommandClass(mod, name) {
|
|
1149
|
+
const named = mod[name];
|
|
1150
|
+
if (this.isCommandClass(named)) return named;
|
|
1151
|
+
if (this.isCommandClass(mod.default)) return mod.default;
|
|
1152
|
+
return Object.values(mod).find((value) => this.isCommandClass(value));
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Whether a value is a Command class (constructor), as opposed to any other
|
|
1156
|
+
* exported function/class.
|
|
1157
|
+
*
|
|
1158
|
+
* The check is structural — it looks for `getSignature` on the prototype
|
|
1159
|
+
* rather than using `instanceof Command` — so it stays correct when the
|
|
1160
|
+
* discovered command extends a Command from a different copy/version of
|
|
1161
|
+
* musket. Crucially, it rejects non-command exports such as the shared
|
|
1162
|
+
* bundler chunks tools like tsdown can emit alongside built commands (e.g. a
|
|
1163
|
+
* `Rebuilder` helper), which would otherwise be `new`-ed and then crash when
|
|
1164
|
+
* `getSignature()` is called on them.
|
|
1165
|
+
*
|
|
1166
|
+
* @param value The candidate export.
|
|
1167
|
+
*/
|
|
1168
|
+
isCommandClass(value) {
|
|
1169
|
+
return typeof value === "function" && typeof value.prototype?.getSignature === "function";
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Emit a non-fatal command-discovery warning.
|
|
1173
|
+
*
|
|
1174
|
+
* @param message
|
|
1175
|
+
*/
|
|
1176
|
+
warnDiscovery(message) {
|
|
1177
|
+
_h3ravel_shared.Logger.log([["[musket]", "yellow"], [message, "white"]], " ");
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Push a new command into the commands stack.
|
|
1181
|
+
*
|
|
1182
|
+
* Resolution prefers a command's structured signature (built via
|
|
1183
|
+
* {@link Command.buildSignature}) and falls back to parsing its signature
|
|
1184
|
+
* string. Commands that expose no usable signature are skipped with a warning
|
|
1185
|
+
* rather than crashing the CLI, and a command whose key was already
|
|
1186
|
+
* registered is ignored (de-duplication — see {@link registeredKeys}).
|
|
1187
|
+
*
|
|
1188
|
+
* @param command
|
|
823
1189
|
*/
|
|
824
1190
|
addCommand(command) {
|
|
825
|
-
|
|
1191
|
+
if (!command || typeof command.getSignature !== "function") {
|
|
1192
|
+
this.warnDiscovery(`Skipping ${this.describeCommand(command)}: not a valid command (no getSignature()).`);
|
|
1193
|
+
return this;
|
|
1194
|
+
}
|
|
1195
|
+
let parsed;
|
|
1196
|
+
try {
|
|
1197
|
+
parsed = command.toParsedSignature?.() ?? Signature.parseSignature(command.getSignature(), command);
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
this.warnDiscovery(`Skipping ${this.describeCommand(command)}: ${error?.message ?? String(error)}`);
|
|
1200
|
+
return this;
|
|
1201
|
+
}
|
|
1202
|
+
if (!parsed || !parsed.baseCommand) {
|
|
1203
|
+
this.warnDiscovery(`Skipping ${this.describeCommand(command)}: empty or unparsable signature.`);
|
|
1204
|
+
return this;
|
|
1205
|
+
}
|
|
1206
|
+
const key = parsed.isNamespaceCommand ? `${parsed.baseCommand}:` : parsed.baseCommand;
|
|
1207
|
+
if (this.registeredKeys.has(key)) return this;
|
|
1208
|
+
this.registeredKeys.add(key);
|
|
1209
|
+
this.commands.push(parsed);
|
|
826
1210
|
return this;
|
|
827
1211
|
}
|
|
828
1212
|
/**
|
|
1213
|
+
* A best-effort human label for a command, for diagnostics.
|
|
1214
|
+
*
|
|
1215
|
+
* @param command
|
|
1216
|
+
*/
|
|
1217
|
+
describeCommand(command) {
|
|
1218
|
+
return command?.constructor?.name ?? "command";
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
829
1221
|
* Push a list of new commands to commands stack
|
|
830
|
-
*
|
|
831
|
-
* @param command
|
|
1222
|
+
*
|
|
1223
|
+
* @param command
|
|
832
1224
|
*/
|
|
833
1225
|
registerCommands(commands) {
|
|
834
|
-
commands.forEach(this.addCommand);
|
|
1226
|
+
commands.forEach((e) => this.addCommand(e));
|
|
835
1227
|
return this;
|
|
836
1228
|
}
|
|
837
1229
|
/**
|
|
@@ -842,13 +1234,11 @@ var Musket = class Musket {
|
|
|
842
1234
|
}
|
|
843
1235
|
async initialize() {
|
|
844
1236
|
if (process.argv.includes("--help") || process.argv.includes("-h")) await this.rebuild("help");
|
|
845
|
-
/**
|
|
846
|
-
*
|
|
1237
|
+
/**
|
|
1238
|
+
* Render the provided packages versions (single source of truth shared
|
|
1239
|
+
* with ListCommand, fully overridable via KernelConfig).
|
|
847
1240
|
*/
|
|
848
|
-
const moduleVersions = this.kernel.
|
|
849
|
-
const value = String(e.alias ?? e.name).split("/").pop().replace(/[-_]/g, " ").replace(/cli/gi, (match) => match === "cli" ? "CLI" : match).replace(/^./, (c) => c.toUpperCase());
|
|
850
|
-
return _h3ravel_shared.Logger.parse([[`${value}:`, "white"], [e.version, "green"]], " ", false);
|
|
851
|
-
}).join(" | ");
|
|
1241
|
+
const moduleVersions = this.kernel.getVersionString();
|
|
852
1242
|
const additional = {
|
|
853
1243
|
quiet: ["-q, --quiet", "Do not output any message except errors and warnings"],
|
|
854
1244
|
silent: ["--silent", "Do not output any message"],
|
|
@@ -875,7 +1265,7 @@ var Musket = class Musket {
|
|
|
875
1265
|
* Load the root command here
|
|
876
1266
|
*/
|
|
877
1267
|
const root = new this.config.rootCommand(this.app, this.kernel);
|
|
878
|
-
const sign = Signature.parseSignature(root.getSignature(), root);
|
|
1268
|
+
const sign = root.toParsedSignature?.() ?? Signature.parseSignature(root.getSignature(), root);
|
|
879
1269
|
const cmd = this.program.name(sign.baseCommand).description(sign.description ?? sign.baseCommand).configureHelp({ showGlobalOptions: true }).action(async () => {
|
|
880
1270
|
root.setInput(this.program.opts(), this.program.args, this.program.registeredArguments, {}, this.program);
|
|
881
1271
|
await this.handle(root);
|
|
@@ -1177,13 +1567,18 @@ var Kernel = class Kernel {
|
|
|
1177
1567
|
for (let i = 0; i < this.packages.length; i++) try {
|
|
1178
1568
|
const item = this.packages[i];
|
|
1179
1569
|
const name = typeof item === "string" ? item : item.name;
|
|
1180
|
-
const alias = typeof item === "string" ? item : item.alias;
|
|
1570
|
+
const alias = typeof item === "string" ? item : item.alias ?? item.name;
|
|
1181
1571
|
const base = typeof item === "string" ? false : item.base;
|
|
1572
|
+
const label = typeof item === "string" ? void 0 : item.label;
|
|
1573
|
+
const versionOverride = typeof item === "string" ? void 0 : item.version;
|
|
1182
1574
|
const modulePath = _h3ravel_shared.FileSystem.findModulePkg(name, this.cwd) ?? "";
|
|
1183
1575
|
const pkg = require(node_path.default.join(modulePath, "package.json"));
|
|
1184
1576
|
pkg.alias = alias;
|
|
1185
1577
|
pkg.base = base;
|
|
1186
|
-
if (
|
|
1578
|
+
if (label) pkg.label = label;
|
|
1579
|
+
/** A per-package version wins, then the base-package override. */
|
|
1580
|
+
if (versionOverride) pkg.version = versionOverride;
|
|
1581
|
+
else if (base === true && version) pkg.version = version;
|
|
1187
1582
|
this.modules.push(pkg);
|
|
1188
1583
|
} catch {
|
|
1189
1584
|
this.modules.push({
|
|
@@ -1200,9 +1595,48 @@ var Kernel = class Kernel {
|
|
|
1200
1595
|
}
|
|
1201
1596
|
return this;
|
|
1202
1597
|
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Format a module's display label: strip the package scope, turn `-`/`_`
|
|
1600
|
+
* into spaces, normalise "cli" casing and capitalise. A module's explicit
|
|
1601
|
+
* `label` short-circuits all of this.
|
|
1602
|
+
*
|
|
1603
|
+
* @param module
|
|
1604
|
+
*/
|
|
1605
|
+
formatModuleLabel(module) {
|
|
1606
|
+
if (module.label) return module.label;
|
|
1607
|
+
return String(module.alias ?? module.name).split("/").pop().replace(/[-_]/g, " ").replace(/cli/gi, (match) => match === "cli" ? "CLI" : match).replace(/^./, (c) => c.toUpperCase());
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Render a single module as a colored `Label: version` segment, honoring
|
|
1611
|
+
* {@link KernelConfig.versionColors}.
|
|
1612
|
+
*
|
|
1613
|
+
* @param module
|
|
1614
|
+
*/
|
|
1615
|
+
renderModuleVersion(module) {
|
|
1616
|
+
const colors = this.config.versionColors ?? {};
|
|
1617
|
+
return _h3ravel_shared.Logger.parse([[`${this.formatModuleLabel(module)}:`, colors.label ?? "white"], [String(module.version), colors.version ?? "green"]], " ", false);
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Build the full version line shown for `--version` and atop the command
|
|
1621
|
+
* list. A single source of truth for both render sites.
|
|
1622
|
+
*
|
|
1623
|
+
* Honors {@link KernelConfig.versionFormatter} for complete control,
|
|
1624
|
+
* otherwise joins each module with {@link KernelConfig.versionSeparator}.
|
|
1625
|
+
*/
|
|
1626
|
+
getVersionString() {
|
|
1627
|
+
const modules = this.modules;
|
|
1628
|
+
const separator = this.config.versionSeparator ?? " | ";
|
|
1629
|
+
if (this.config.versionFormatter) return this.config.versionFormatter(modules, {
|
|
1630
|
+
format: (module) => this.renderModuleVersion(module),
|
|
1631
|
+
label: (module) => this.formatModuleLabel(module),
|
|
1632
|
+
separator
|
|
1633
|
+
});
|
|
1634
|
+
return modules.map((module) => this.renderModuleVersion(module)).join(separator);
|
|
1635
|
+
}
|
|
1203
1636
|
};
|
|
1204
1637
|
//#endregion
|
|
1205
1638
|
exports.Command = Command;
|
|
1206
1639
|
exports.Kernel = Kernel;
|
|
1207
1640
|
exports.Musket = Musket;
|
|
1208
1641
|
exports.Signature = Signature;
|
|
1642
|
+
exports.SignatureBuilder = SignatureBuilder;
|