@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.js
CHANGED
|
@@ -1,10 +1,236 @@
|
|
|
1
|
-
import { FileSystem, Logger, Prompts } from "@h3ravel/shared";
|
|
1
|
+
import { FileSystem, Logger, Prompts, importFile } from "@h3ravel/shared";
|
|
2
2
|
import { Argument, Command as Command$1, Option } from "commander";
|
|
3
3
|
import { build } from "tsdown";
|
|
4
4
|
import { glob } from "glob";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { createRequire } from "module";
|
|
7
7
|
import { mkdir } from "node:fs/promises";
|
|
8
|
+
//#region src/SignatureBuilder.ts
|
|
9
|
+
/**
|
|
10
|
+
* A fluent builder for constructing a command's signature programmatically,
|
|
11
|
+
* without authoring the string DSL by hand.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* buildSignature (sig: SignatureBuilder) {
|
|
15
|
+
* return sig
|
|
16
|
+
* .command('queue:work')
|
|
17
|
+
* .describe('Process jobs on the queue')
|
|
18
|
+
* .argument('connection', { description: 'The connection to work', required: false })
|
|
19
|
+
* .option('queue', { description: 'The queue to process', short: 'Q' })
|
|
20
|
+
* .option('once', { description: 'Process a single job and exit' })
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Musket consumes the builder directly (lossless) for normal commands, and can
|
|
25
|
+
* also reconstruct the equivalent signature string via {@link toString} for
|
|
26
|
+
* anything that still expects the DSL.
|
|
27
|
+
*/
|
|
28
|
+
var SignatureBuilder = class {
|
|
29
|
+
baseName = "";
|
|
30
|
+
commandDescription;
|
|
31
|
+
hidden = false;
|
|
32
|
+
args = [];
|
|
33
|
+
opts = [];
|
|
34
|
+
/**
|
|
35
|
+
* Set the command name, e.g. `make:model` or `queue:work`. A trailing `:`
|
|
36
|
+
* marks a namespace command.
|
|
37
|
+
*/
|
|
38
|
+
command(name) {
|
|
39
|
+
this.baseName = name.trim();
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Alias for {@link command}.
|
|
44
|
+
*
|
|
45
|
+
* @param name
|
|
46
|
+
* @returns
|
|
47
|
+
*/
|
|
48
|
+
name(name) {
|
|
49
|
+
return this.command(name);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Set the command description.
|
|
53
|
+
*
|
|
54
|
+
* @param name
|
|
55
|
+
* @returns
|
|
56
|
+
*/
|
|
57
|
+
describe(description) {
|
|
58
|
+
this.commandDescription = description;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Alias for {@link describe}.
|
|
63
|
+
*
|
|
64
|
+
* @param name
|
|
65
|
+
* @returns
|
|
66
|
+
*/
|
|
67
|
+
description(description) {
|
|
68
|
+
return this.describe(description);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Mark the command as hidden from help output.
|
|
72
|
+
*
|
|
73
|
+
* @param name
|
|
74
|
+
* @returns
|
|
75
|
+
*/
|
|
76
|
+
hide(hidden = true) {
|
|
77
|
+
this.hidden = hidden;
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Add a positional argument.
|
|
82
|
+
*
|
|
83
|
+
* @param name
|
|
84
|
+
* @returns
|
|
85
|
+
*/
|
|
86
|
+
argument(name, definition = {}) {
|
|
87
|
+
this.args.push({
|
|
88
|
+
name: name.trim(),
|
|
89
|
+
...definition
|
|
90
|
+
});
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Add an option/flag.
|
|
95
|
+
*
|
|
96
|
+
* @param name
|
|
97
|
+
* @returns
|
|
98
|
+
*/
|
|
99
|
+
option(name, definition = {}) {
|
|
100
|
+
this.opts.push({
|
|
101
|
+
name: name.replace(/^--?/, "").trim(),
|
|
102
|
+
...definition
|
|
103
|
+
});
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The configured command name (empty when unset).
|
|
108
|
+
*
|
|
109
|
+
* @param name
|
|
110
|
+
* @returns
|
|
111
|
+
*/
|
|
112
|
+
getName() {
|
|
113
|
+
return this.baseName;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The configured description, if any.
|
|
117
|
+
*
|
|
118
|
+
* @param name
|
|
119
|
+
* @returns
|
|
120
|
+
*/
|
|
121
|
+
getDescription() {
|
|
122
|
+
return this.commandDescription;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Whether the command is a namespace command (name ends with `:`).
|
|
126
|
+
*
|
|
127
|
+
* @param name
|
|
128
|
+
* @returns
|
|
129
|
+
*/
|
|
130
|
+
isNamespace() {
|
|
131
|
+
return this.baseName.endsWith(":");
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Whether anything has been configured on this builder.
|
|
135
|
+
*
|
|
136
|
+
* @param name
|
|
137
|
+
* @returns
|
|
138
|
+
*/
|
|
139
|
+
isEmpty() {
|
|
140
|
+
return this.baseName.length === 0;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Build the structured {@link ParsedCommand} consumed by the kernel. This is
|
|
144
|
+
* lossless — no string round-trip — and is the path used for normal commands.
|
|
145
|
+
*
|
|
146
|
+
* @param commandClass
|
|
147
|
+
* @returns
|
|
148
|
+
*/
|
|
149
|
+
toParsed(commandClass) {
|
|
150
|
+
const options = [...this.args.map((arg) => this.argumentToOption(arg)), ...this.opts.map((opt) => this.optionToOption(opt))];
|
|
151
|
+
return {
|
|
152
|
+
baseCommand: this.baseName,
|
|
153
|
+
isNamespaceCommand: false,
|
|
154
|
+
description: this.commandDescription ?? commandClass.getDescription(),
|
|
155
|
+
commandClass,
|
|
156
|
+
options,
|
|
157
|
+
isHidden: this.hidden
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
argumentToOption(arg) {
|
|
161
|
+
const required = arg.required ?? arg.default === void 0;
|
|
162
|
+
const multiple = arg.multiple ?? false;
|
|
163
|
+
let placeholder;
|
|
164
|
+
if (multiple) placeholder = required ? `<${arg.name}...>` : `[${arg.name}...]`;
|
|
165
|
+
return {
|
|
166
|
+
name: arg.name,
|
|
167
|
+
isFlag: false,
|
|
168
|
+
required,
|
|
169
|
+
multiple,
|
|
170
|
+
description: arg.description ?? "",
|
|
171
|
+
choices: arg.choices ?? [],
|
|
172
|
+
defaultValue: arg.default,
|
|
173
|
+
placeholder
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
optionToOption(opt) {
|
|
177
|
+
const long = `--${opt.name}`;
|
|
178
|
+
const flags = opt.short ? [`-${opt.short}`, long] : [long];
|
|
179
|
+
const takesValue = opt.default !== void 0 || opt.requiresValue === true || opt.optionalValue === true || (opt.choices?.length ?? 0) > 0;
|
|
180
|
+
let required = false;
|
|
181
|
+
let placeholder;
|
|
182
|
+
if (takesValue) if (opt.requiresValue === true && opt.default === void 0 && opt.optionalValue !== true) required = true;
|
|
183
|
+
else placeholder = `[${opt.name}]`;
|
|
184
|
+
return {
|
|
185
|
+
name: long,
|
|
186
|
+
flags,
|
|
187
|
+
isFlag: true,
|
|
188
|
+
required,
|
|
189
|
+
placeholder,
|
|
190
|
+
description: opt.description ?? "",
|
|
191
|
+
choices: opt.choices ?? [],
|
|
192
|
+
defaultValue: opt.default ?? (takesValue ? void 0 : false),
|
|
193
|
+
shared: opt.shared,
|
|
194
|
+
isHidden: opt.hidden
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Reconstruct the equivalent signature string in musket's DSL. Used by
|
|
199
|
+
* {@link Command.getSignature} so external consumers (and the namespace path)
|
|
200
|
+
* keep working unchanged.
|
|
201
|
+
*/
|
|
202
|
+
toString() {
|
|
203
|
+
const tokens = [this.baseName];
|
|
204
|
+
for (const arg of this.args) tokens.push(this.argumentToken(arg));
|
|
205
|
+
for (const opt of this.opts) tokens.push(this.optionToken(opt));
|
|
206
|
+
return tokens.join("\n ");
|
|
207
|
+
}
|
|
208
|
+
argumentToken(arg) {
|
|
209
|
+
const required = arg.required ?? arg.default === void 0;
|
|
210
|
+
const multiple = arg.multiple ?? false;
|
|
211
|
+
let token = arg.name;
|
|
212
|
+
if (arg.default !== void 0) token += `=${arg.default}`;
|
|
213
|
+
else if (!required && multiple) token += "?*";
|
|
214
|
+
else if (multiple) token += "*";
|
|
215
|
+
else if (!required) token += "?";
|
|
216
|
+
return this.wrap(token, arg.description, arg.choices);
|
|
217
|
+
}
|
|
218
|
+
optionToken(opt) {
|
|
219
|
+
const takesValue = opt.default !== void 0 || opt.requiresValue === true || opt.optionalValue === true;
|
|
220
|
+
let token = opt.short ? `--${opt.short}|${opt.name}` : `--${opt.name}`;
|
|
221
|
+
if (opt.default !== void 0) token += `=${opt.default}`;
|
|
222
|
+
else if (opt.requiresValue === true && opt.optionalValue !== true) token += "=";
|
|
223
|
+
else if (opt.optionalValue === true) token += "?";
|
|
224
|
+
return this.wrap(token, opt.description, opt.choices, takesValue);
|
|
225
|
+
}
|
|
226
|
+
wrap(token, description, choices, _value) {
|
|
227
|
+
let body = token;
|
|
228
|
+
if (description) body += ` : ${description}`;
|
|
229
|
+
if (choices?.length) body += ` : [${choices.join(", ")}]`;
|
|
230
|
+
return `{${body}}`;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
//#endregion
|
|
8
234
|
//#region src/Core/Command.ts
|
|
9
235
|
var Command = class {
|
|
10
236
|
app;
|
|
@@ -38,6 +264,11 @@ var Command = class {
|
|
|
38
264
|
*/
|
|
39
265
|
description;
|
|
40
266
|
/**
|
|
267
|
+
* Memoized result of {@link resolveBuilder}. `undefined` means "not yet
|
|
268
|
+
* resolved"; `null` means "resolved, no builder in use".
|
|
269
|
+
*/
|
|
270
|
+
resolvedBuilder;
|
|
271
|
+
/**
|
|
41
272
|
* The console command input.
|
|
42
273
|
*
|
|
43
274
|
* @var object
|
|
@@ -118,20 +349,70 @@ var Command = class {
|
|
|
118
349
|
return this;
|
|
119
350
|
}
|
|
120
351
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* @
|
|
352
|
+
* Define the command signature programmatically.
|
|
353
|
+
*
|
|
354
|
+
* Override this instead of (or in addition to) the {@link signature} string
|
|
355
|
+
* to describe the command, its arguments and its options through the fluent
|
|
356
|
+
* {@link SignatureBuilder} — no string DSL required. When the `signature`
|
|
357
|
+
* string is set it always takes precedence and this method is ignored.
|
|
358
|
+
*
|
|
359
|
+
* ```ts
|
|
360
|
+
* buildSignature (sig: SignatureBuilder) {
|
|
361
|
+
* return sig
|
|
362
|
+
* .command('queue:work')
|
|
363
|
+
* .describe('Process jobs on the queue')
|
|
364
|
+
* .argument('connection', { required: false })
|
|
365
|
+
* .option('queue', { short: 'Q', description: 'The queue to process' })
|
|
366
|
+
* .option('once', { description: 'Process a single job and exit' })
|
|
367
|
+
* }
|
|
368
|
+
* ```
|
|
369
|
+
*
|
|
370
|
+
* @param _builder A fresh builder to configure.
|
|
371
|
+
*/
|
|
372
|
+
buildSignature(_builder) {
|
|
373
|
+
/** Override to use. */
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Resolve (and memoize) the signature builder for this command, or `null`
|
|
377
|
+
* when the command uses the string {@link signature} (which always wins) or
|
|
378
|
+
* does not implement {@link buildSignature}.
|
|
379
|
+
*/
|
|
380
|
+
resolveBuilder() {
|
|
381
|
+
if (this.resolvedBuilder !== void 0) return this.resolvedBuilder;
|
|
382
|
+
if (this.signature) return this.resolvedBuilder = null;
|
|
383
|
+
const builder = new SignatureBuilder();
|
|
384
|
+
const result = this.buildSignature(builder);
|
|
385
|
+
const resolved = result instanceof SignatureBuilder ? result : builder;
|
|
386
|
+
return this.resolvedBuilder = resolved.isEmpty() ? null : resolved;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Get the command signature. Returns the string {@link signature} when set,
|
|
390
|
+
* otherwise reconstructs it from {@link buildSignature}.
|
|
391
|
+
*
|
|
392
|
+
* @returns
|
|
124
393
|
*/
|
|
125
394
|
getSignature() {
|
|
126
|
-
return this.signature;
|
|
395
|
+
if (this.signature) return this.signature;
|
|
396
|
+
return this.resolveBuilder()?.toString() ?? this.signature;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* The structured, lossless parsed command when {@link buildSignature} is used
|
|
400
|
+
* for a non-namespace command; `undefined` otherwise (callers then fall back
|
|
401
|
+
* to parsing {@link getSignature}). This avoids a lossy string round-trip.
|
|
402
|
+
*/
|
|
403
|
+
toParsedSignature() {
|
|
404
|
+
const builder = this.resolveBuilder();
|
|
405
|
+
if (!builder || builder.isNamespace()) return;
|
|
406
|
+
return builder.toParsed(this);
|
|
127
407
|
}
|
|
128
408
|
/**
|
|
129
409
|
* Get the command description
|
|
130
|
-
*
|
|
131
|
-
* @returns
|
|
410
|
+
*
|
|
411
|
+
* @returns
|
|
132
412
|
*/
|
|
133
413
|
getDescription() {
|
|
134
|
-
return this.description;
|
|
414
|
+
if (this.description) return this.description;
|
|
415
|
+
return this.resolveBuilder()?.getDescription() ?? this.description;
|
|
135
416
|
}
|
|
136
417
|
/**
|
|
137
418
|
* Get a specific option
|
|
@@ -481,11 +762,8 @@ var ListCommand = class ListCommand extends Command {
|
|
|
481
762
|
return Logger.describe(Logger.log(" " + e.name(), "green", false), e.description(), 25, false).join("");
|
|
482
763
|
});
|
|
483
764
|
const list = ListCommand.groupItems(commands);
|
|
484
|
-
/** Output the modules version */
|
|
485
|
-
const version = this.kernel.
|
|
486
|
-
const value = String(e.alias ?? e.name).split("/").pop().replace(/[-_]/g, " ").replace(/cli/gi, (match) => match === "cli" ? "CLI" : match).replace(/^./, (c) => c.toUpperCase());
|
|
487
|
-
return Logger.log([[`${value}:`, "white"], [e.version, "green"]], " ", false);
|
|
488
|
-
}).join(" | ");
|
|
765
|
+
/** Output the modules version (shared renderer, overridable via config) */
|
|
766
|
+
const version = this.kernel.getVersionString();
|
|
489
767
|
this.newLine();
|
|
490
768
|
console.log(version);
|
|
491
769
|
this.newLine();
|
|
@@ -736,6 +1014,13 @@ var Musket = class Musket {
|
|
|
736
1014
|
name = "musket";
|
|
737
1015
|
config = {};
|
|
738
1016
|
commands = [];
|
|
1017
|
+
/**
|
|
1018
|
+
* Keys (baseCommand, namespace-aware) of commands already registered, so the
|
|
1019
|
+
* same command surfacing from more than one source — e.g. discovered both as
|
|
1020
|
+
* built `dist/*.js` and as `src/*.ts` via the jiti loader — is only added
|
|
1021
|
+
* once. The first registration wins (base commands before discovered ones).
|
|
1022
|
+
*/
|
|
1023
|
+
registeredKeys = /* @__PURE__ */ new Set();
|
|
739
1024
|
program;
|
|
740
1025
|
constructor(app, kernel, baseCommands = [], resolver, tsDownConfig = {}) {
|
|
741
1026
|
this.app = app;
|
|
@@ -784,30 +1069,137 @@ var Musket = class Musket {
|
|
|
784
1069
|
* CLI Commands auto registration
|
|
785
1070
|
*/
|
|
786
1071
|
for await (const pth of glob.stream(paths)) {
|
|
787
|
-
const
|
|
1072
|
+
const file = pth.toString();
|
|
1073
|
+
const name = path.basename(file).replace(/\.(c|m)?(t|j)s$/, "");
|
|
788
1074
|
try {
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
1075
|
+
const mod = await this.importCommandModule(file);
|
|
1076
|
+
const CommandClass = this.resolveCommandClass(mod, name);
|
|
1077
|
+
if (!CommandClass) {
|
|
1078
|
+
this.warnDiscovery(`No command class export found in ${file}`);
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
commands.push(new CommandClass(this.app, this.kernel));
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
/**
|
|
1084
|
+
* Never swallow load failures silently — a command that throws on
|
|
1085
|
+
* import would otherwise just vanish from the CLI with no clue why.
|
|
1086
|
+
*/
|
|
1087
|
+
this.warnDiscovery(`Failed to load command ${file}: ${error?.message ?? String(error)}`);
|
|
1088
|
+
}
|
|
792
1089
|
}
|
|
793
1090
|
commands.forEach((e) => this.addCommand(e));
|
|
794
1091
|
}
|
|
795
1092
|
/**
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
1093
|
+
* Import a discovered command module with full TypeScript support.
|
|
1094
|
+
*
|
|
1095
|
+
* Native `import()` is attempted first: it loads built `.js`, and works
|
|
1096
|
+
* verbatim in TypeScript-aware runtimes/test loaders (vitest, tsx, Node with
|
|
1097
|
+
* type stripping) — which also keeps their module mocking and single module
|
|
1098
|
+
* registry intact. When native import can't load a TypeScript source (plain
|
|
1099
|
+
* Node throwing on a `.ts`/`.mts`/`.cts` file), it is transpiled on the fly
|
|
1100
|
+
* via jiti (`@h3ravel/shared`'s `importFile`) so commands are discovered
|
|
1101
|
+
* without a prior build. Consumers can bypass all of this with
|
|
1102
|
+
* {@link KernelConfig.importModule}.
|
|
1103
|
+
*
|
|
1104
|
+
* @param file Absolute or cwd-relative path to the command module.
|
|
1105
|
+
*/
|
|
1106
|
+
async importCommandModule(file) {
|
|
1107
|
+
if (this.config.importModule) return await this.config.importModule(file);
|
|
1108
|
+
try {
|
|
1109
|
+
return await import(file);
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
if (/\.(c|m)?ts$/.test(file)) return await importFile(file);
|
|
1112
|
+
throw error;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Resolve the command class out of an imported module. Prefers the export
|
|
1117
|
+
* named after the file, then a `default` export, then the first exported
|
|
1118
|
+
* constructor — because a file's name and its exported class name do not
|
|
1119
|
+
* always match.
|
|
1120
|
+
*
|
|
1121
|
+
* @param mod The imported module namespace.
|
|
1122
|
+
* @param name The file name without extension.
|
|
1123
|
+
*/
|
|
1124
|
+
resolveCommandClass(mod, name) {
|
|
1125
|
+
const named = mod[name];
|
|
1126
|
+
if (this.isCommandClass(named)) return named;
|
|
1127
|
+
if (this.isCommandClass(mod.default)) return mod.default;
|
|
1128
|
+
return Object.values(mod).find((value) => this.isCommandClass(value));
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Whether a value is a Command class (constructor), as opposed to any other
|
|
1132
|
+
* exported function/class.
|
|
1133
|
+
*
|
|
1134
|
+
* The check is structural — it looks for `getSignature` on the prototype
|
|
1135
|
+
* rather than using `instanceof Command` — so it stays correct when the
|
|
1136
|
+
* discovered command extends a Command from a different copy/version of
|
|
1137
|
+
* musket. Crucially, it rejects non-command exports such as the shared
|
|
1138
|
+
* bundler chunks tools like tsdown can emit alongside built commands (e.g. a
|
|
1139
|
+
* `Rebuilder` helper), which would otherwise be `new`-ed and then crash when
|
|
1140
|
+
* `getSignature()` is called on them.
|
|
1141
|
+
*
|
|
1142
|
+
* @param value The candidate export.
|
|
1143
|
+
*/
|
|
1144
|
+
isCommandClass(value) {
|
|
1145
|
+
return typeof value === "function" && typeof value.prototype?.getSignature === "function";
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Emit a non-fatal command-discovery warning.
|
|
1149
|
+
*
|
|
1150
|
+
* @param message
|
|
1151
|
+
*/
|
|
1152
|
+
warnDiscovery(message) {
|
|
1153
|
+
Logger.log([["[musket]", "yellow"], [message, "white"]], " ");
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Push a new command into the commands stack.
|
|
1157
|
+
*
|
|
1158
|
+
* Resolution prefers a command's structured signature (built via
|
|
1159
|
+
* {@link Command.buildSignature}) and falls back to parsing its signature
|
|
1160
|
+
* string. Commands that expose no usable signature are skipped with a warning
|
|
1161
|
+
* rather than crashing the CLI, and a command whose key was already
|
|
1162
|
+
* registered is ignored (de-duplication — see {@link registeredKeys}).
|
|
1163
|
+
*
|
|
1164
|
+
* @param command
|
|
799
1165
|
*/
|
|
800
1166
|
addCommand(command) {
|
|
801
|
-
|
|
1167
|
+
if (!command || typeof command.getSignature !== "function") {
|
|
1168
|
+
this.warnDiscovery(`Skipping ${this.describeCommand(command)}: not a valid command (no getSignature()).`);
|
|
1169
|
+
return this;
|
|
1170
|
+
}
|
|
1171
|
+
let parsed;
|
|
1172
|
+
try {
|
|
1173
|
+
parsed = command.toParsedSignature?.() ?? Signature.parseSignature(command.getSignature(), command);
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
this.warnDiscovery(`Skipping ${this.describeCommand(command)}: ${error?.message ?? String(error)}`);
|
|
1176
|
+
return this;
|
|
1177
|
+
}
|
|
1178
|
+
if (!parsed || !parsed.baseCommand) {
|
|
1179
|
+
this.warnDiscovery(`Skipping ${this.describeCommand(command)}: empty or unparsable signature.`);
|
|
1180
|
+
return this;
|
|
1181
|
+
}
|
|
1182
|
+
const key = parsed.isNamespaceCommand ? `${parsed.baseCommand}:` : parsed.baseCommand;
|
|
1183
|
+
if (this.registeredKeys.has(key)) return this;
|
|
1184
|
+
this.registeredKeys.add(key);
|
|
1185
|
+
this.commands.push(parsed);
|
|
802
1186
|
return this;
|
|
803
1187
|
}
|
|
804
1188
|
/**
|
|
1189
|
+
* A best-effort human label for a command, for diagnostics.
|
|
1190
|
+
*
|
|
1191
|
+
* @param command
|
|
1192
|
+
*/
|
|
1193
|
+
describeCommand(command) {
|
|
1194
|
+
return command?.constructor?.name ?? "command";
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
805
1197
|
* Push a list of new commands to commands stack
|
|
806
|
-
*
|
|
807
|
-
* @param command
|
|
1198
|
+
*
|
|
1199
|
+
* @param command
|
|
808
1200
|
*/
|
|
809
1201
|
registerCommands(commands) {
|
|
810
|
-
commands.forEach(this.addCommand);
|
|
1202
|
+
commands.forEach((e) => this.addCommand(e));
|
|
811
1203
|
return this;
|
|
812
1204
|
}
|
|
813
1205
|
/**
|
|
@@ -818,13 +1210,11 @@ var Musket = class Musket {
|
|
|
818
1210
|
}
|
|
819
1211
|
async initialize() {
|
|
820
1212
|
if (process.argv.includes("--help") || process.argv.includes("-h")) await this.rebuild("help");
|
|
821
|
-
/**
|
|
822
|
-
*
|
|
1213
|
+
/**
|
|
1214
|
+
* Render the provided packages versions (single source of truth shared
|
|
1215
|
+
* with ListCommand, fully overridable via KernelConfig).
|
|
823
1216
|
*/
|
|
824
|
-
const moduleVersions = this.kernel.
|
|
825
|
-
const value = String(e.alias ?? e.name).split("/").pop().replace(/[-_]/g, " ").replace(/cli/gi, (match) => match === "cli" ? "CLI" : match).replace(/^./, (c) => c.toUpperCase());
|
|
826
|
-
return Logger.parse([[`${value}:`, "white"], [e.version, "green"]], " ", false);
|
|
827
|
-
}).join(" | ");
|
|
1217
|
+
const moduleVersions = this.kernel.getVersionString();
|
|
828
1218
|
const additional = {
|
|
829
1219
|
quiet: ["-q, --quiet", "Do not output any message except errors and warnings"],
|
|
830
1220
|
silent: ["--silent", "Do not output any message"],
|
|
@@ -851,7 +1241,7 @@ var Musket = class Musket {
|
|
|
851
1241
|
* Load the root command here
|
|
852
1242
|
*/
|
|
853
1243
|
const root = new this.config.rootCommand(this.app, this.kernel);
|
|
854
|
-
const sign = Signature.parseSignature(root.getSignature(), root);
|
|
1244
|
+
const sign = root.toParsedSignature?.() ?? Signature.parseSignature(root.getSignature(), root);
|
|
855
1245
|
const cmd = this.program.name(sign.baseCommand).description(sign.description ?? sign.baseCommand).configureHelp({ showGlobalOptions: true }).action(async () => {
|
|
856
1246
|
root.setInput(this.program.opts(), this.program.args, this.program.registeredArguments, {}, this.program);
|
|
857
1247
|
await this.handle(root);
|
|
@@ -1153,13 +1543,18 @@ var Kernel = class Kernel {
|
|
|
1153
1543
|
for (let i = 0; i < this.packages.length; i++) try {
|
|
1154
1544
|
const item = this.packages[i];
|
|
1155
1545
|
const name = typeof item === "string" ? item : item.name;
|
|
1156
|
-
const alias = typeof item === "string" ? item : item.alias;
|
|
1546
|
+
const alias = typeof item === "string" ? item : item.alias ?? item.name;
|
|
1157
1547
|
const base = typeof item === "string" ? false : item.base;
|
|
1548
|
+
const label = typeof item === "string" ? void 0 : item.label;
|
|
1549
|
+
const versionOverride = typeof item === "string" ? void 0 : item.version;
|
|
1158
1550
|
const modulePath = FileSystem.findModulePkg(name, this.cwd) ?? "";
|
|
1159
1551
|
const pkg = require(path.join(modulePath, "package.json"));
|
|
1160
1552
|
pkg.alias = alias;
|
|
1161
1553
|
pkg.base = base;
|
|
1162
|
-
if (
|
|
1554
|
+
if (label) pkg.label = label;
|
|
1555
|
+
/** A per-package version wins, then the base-package override. */
|
|
1556
|
+
if (versionOverride) pkg.version = versionOverride;
|
|
1557
|
+
else if (base === true && version) pkg.version = version;
|
|
1163
1558
|
this.modules.push(pkg);
|
|
1164
1559
|
} catch {
|
|
1165
1560
|
this.modules.push({
|
|
@@ -1176,6 +1571,44 @@ var Kernel = class Kernel {
|
|
|
1176
1571
|
}
|
|
1177
1572
|
return this;
|
|
1178
1573
|
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Format a module's display label: strip the package scope, turn `-`/`_`
|
|
1576
|
+
* into spaces, normalise "cli" casing and capitalise. A module's explicit
|
|
1577
|
+
* `label` short-circuits all of this.
|
|
1578
|
+
*
|
|
1579
|
+
* @param module
|
|
1580
|
+
*/
|
|
1581
|
+
formatModuleLabel(module) {
|
|
1582
|
+
if (module.label) return module.label;
|
|
1583
|
+
return String(module.alias ?? module.name).split("/").pop().replace(/[-_]/g, " ").replace(/cli/gi, (match) => match === "cli" ? "CLI" : match).replace(/^./, (c) => c.toUpperCase());
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Render a single module as a colored `Label: version` segment, honoring
|
|
1587
|
+
* {@link KernelConfig.versionColors}.
|
|
1588
|
+
*
|
|
1589
|
+
* @param module
|
|
1590
|
+
*/
|
|
1591
|
+
renderModuleVersion(module) {
|
|
1592
|
+
const colors = this.config.versionColors ?? {};
|
|
1593
|
+
return Logger.parse([[`${this.formatModuleLabel(module)}:`, colors.label ?? "white"], [String(module.version), colors.version ?? "green"]], " ", false);
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Build the full version line shown for `--version` and atop the command
|
|
1597
|
+
* list. A single source of truth for both render sites.
|
|
1598
|
+
*
|
|
1599
|
+
* Honors {@link KernelConfig.versionFormatter} for complete control,
|
|
1600
|
+
* otherwise joins each module with {@link KernelConfig.versionSeparator}.
|
|
1601
|
+
*/
|
|
1602
|
+
getVersionString() {
|
|
1603
|
+
const modules = this.modules;
|
|
1604
|
+
const separator = this.config.versionSeparator ?? " | ";
|
|
1605
|
+
if (this.config.versionFormatter) return this.config.versionFormatter(modules, {
|
|
1606
|
+
format: (module) => this.renderModuleVersion(module),
|
|
1607
|
+
label: (module) => this.formatModuleLabel(module),
|
|
1608
|
+
separator
|
|
1609
|
+
});
|
|
1610
|
+
return modules.map((module) => this.renderModuleVersion(module)).join(separator);
|
|
1611
|
+
}
|
|
1179
1612
|
};
|
|
1180
1613
|
//#endregion
|
|
1181
|
-
export { Command, Kernel, Musket, Signature };
|
|
1614
|
+
export { Command, Kernel, Musket, Signature, SignatureBuilder };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h3ravel/musket",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Musket CLI is a framework-agnostic CLI framework designed to allow you build artisan-like CLI apps and for use in the H3ravel framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|