@h3ravel/musket 2.1.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 +356 -18
- package/dist/index.d.ts +239 -3
- package/dist/index.js +356 -19
- 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
|
|
@@ -757,6 +1038,13 @@ var Musket = class Musket {
|
|
|
757
1038
|
name = "musket";
|
|
758
1039
|
config = {};
|
|
759
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();
|
|
760
1048
|
program;
|
|
761
1049
|
constructor(app, kernel, baseCommands = [], resolver, tsDownConfig = {}) {
|
|
762
1050
|
this.app = app;
|
|
@@ -859,9 +1147,26 @@ var Musket = class Musket {
|
|
|
859
1147
|
*/
|
|
860
1148
|
resolveCommandClass(mod, name) {
|
|
861
1149
|
const named = mod[name];
|
|
862
|
-
if (
|
|
863
|
-
if (
|
|
864
|
-
return Object.values(mod).find((value) =>
|
|
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";
|
|
865
1170
|
}
|
|
866
1171
|
/**
|
|
867
1172
|
* Emit a non-fatal command-discovery warning.
|
|
@@ -872,21 +1177,53 @@ var Musket = class Musket {
|
|
|
872
1177
|
_h3ravel_shared.Logger.log([["[musket]", "yellow"], [message, "white"]], " ");
|
|
873
1178
|
}
|
|
874
1179
|
/**
|
|
875
|
-
* Push a new command into the commands stack
|
|
876
|
-
*
|
|
877
|
-
*
|
|
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
|
|
878
1189
|
*/
|
|
879
1190
|
addCommand(command) {
|
|
880
|
-
|
|
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);
|
|
881
1210
|
return this;
|
|
882
1211
|
}
|
|
883
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
|
+
/**
|
|
884
1221
|
* Push a list of new commands to commands stack
|
|
885
|
-
*
|
|
886
|
-
* @param command
|
|
1222
|
+
*
|
|
1223
|
+
* @param command
|
|
887
1224
|
*/
|
|
888
1225
|
registerCommands(commands) {
|
|
889
|
-
commands.forEach(this.addCommand);
|
|
1226
|
+
commands.forEach((e) => this.addCommand(e));
|
|
890
1227
|
return this;
|
|
891
1228
|
}
|
|
892
1229
|
/**
|
|
@@ -928,7 +1265,7 @@ var Musket = class Musket {
|
|
|
928
1265
|
* Load the root command here
|
|
929
1266
|
*/
|
|
930
1267
|
const root = new this.config.rootCommand(this.app, this.kernel);
|
|
931
|
-
const sign = Signature.parseSignature(root.getSignature(), root);
|
|
1268
|
+
const sign = root.toParsedSignature?.() ?? Signature.parseSignature(root.getSignature(), root);
|
|
932
1269
|
const cmd = this.program.name(sign.baseCommand).description(sign.description ?? sign.baseCommand).configureHelp({ showGlobalOptions: true }).action(async () => {
|
|
933
1270
|
root.setInput(this.program.opts(), this.program.args, this.program.registeredArguments, {}, this.program);
|
|
934
1271
|
await this.handle(root);
|
|
@@ -1302,3 +1639,4 @@ exports.Command = Command;
|
|
|
1302
1639
|
exports.Kernel = Kernel;
|
|
1303
1640
|
exports.Musket = Musket;
|
|
1304
1641
|
exports.Signature = Signature;
|
|
1642
|
+
exports.SignatureBuilder = SignatureBuilder;
|
package/dist/index.d.ts
CHANGED
|
@@ -327,6 +327,168 @@ declare class Kernel<A extends Application = Application> {
|
|
|
327
327
|
getVersionString(): string;
|
|
328
328
|
}
|
|
329
329
|
//#endregion
|
|
330
|
+
//#region src/SignatureBuilder.d.ts
|
|
331
|
+
type SignatureValue = string | number | boolean | string[] | undefined;
|
|
332
|
+
/**
|
|
333
|
+
* Definition of a positional argument added via {@link SignatureBuilder.argument}.
|
|
334
|
+
*/
|
|
335
|
+
interface ArgumentDefinition {
|
|
336
|
+
/** Human readable description. */
|
|
337
|
+
description?: string;
|
|
338
|
+
/** Whether the argument must be provided. Defaults to `true`. */
|
|
339
|
+
required?: boolean;
|
|
340
|
+
/** A default value. Implies the argument is optional. */
|
|
341
|
+
default?: SignatureValue;
|
|
342
|
+
/** Whether the argument accepts multiple (variadic) values. */
|
|
343
|
+
multiple?: boolean;
|
|
344
|
+
/** Restrict the accepted values to this list. */
|
|
345
|
+
choices?: string[];
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Definition of an option/flag added via {@link SignatureBuilder.option}.
|
|
349
|
+
*
|
|
350
|
+
* By default an option is a boolean flag. Give it a `default`, set
|
|
351
|
+
* `requiresValue`, or set `optionalValue` to make it take a value.
|
|
352
|
+
*/
|
|
353
|
+
interface OptionDefinition {
|
|
354
|
+
/** Human readable description. */
|
|
355
|
+
description?: string;
|
|
356
|
+
/** A single character short alias, e.g. `Q` for `-Q`. */
|
|
357
|
+
short?: string;
|
|
358
|
+
/** A default value. Implies the option takes an optional value. */
|
|
359
|
+
default?: SignatureValue;
|
|
360
|
+
/** The option requires a value (`--queue <value>`). */
|
|
361
|
+
requiresValue?: boolean;
|
|
362
|
+
/** The option takes an optional value (`--queue [value]`). */
|
|
363
|
+
optionalValue?: boolean;
|
|
364
|
+
/** Restrict the accepted values to this list. */
|
|
365
|
+
choices?: string[];
|
|
366
|
+
/** Share this option with sub-commands of a namespace command. */
|
|
367
|
+
shared?: boolean;
|
|
368
|
+
/** Hide this option from help output. */
|
|
369
|
+
hidden?: boolean;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* A fluent builder for constructing a command's signature programmatically,
|
|
373
|
+
* without authoring the string DSL by hand.
|
|
374
|
+
*
|
|
375
|
+
* ```ts
|
|
376
|
+
* buildSignature (sig: SignatureBuilder) {
|
|
377
|
+
* return sig
|
|
378
|
+
* .command('queue:work')
|
|
379
|
+
* .describe('Process jobs on the queue')
|
|
380
|
+
* .argument('connection', { description: 'The connection to work', required: false })
|
|
381
|
+
* .option('queue', { description: 'The queue to process', short: 'Q' })
|
|
382
|
+
* .option('once', { description: 'Process a single job and exit' })
|
|
383
|
+
* }
|
|
384
|
+
* ```
|
|
385
|
+
*
|
|
386
|
+
* Musket consumes the builder directly (lossless) for normal commands, and can
|
|
387
|
+
* also reconstruct the equivalent signature string via {@link toString} for
|
|
388
|
+
* anything that still expects the DSL.
|
|
389
|
+
*/
|
|
390
|
+
declare class SignatureBuilder {
|
|
391
|
+
private baseName;
|
|
392
|
+
private commandDescription?;
|
|
393
|
+
private hidden;
|
|
394
|
+
private args;
|
|
395
|
+
private opts;
|
|
396
|
+
/**
|
|
397
|
+
* Set the command name, e.g. `make:model` or `queue:work`. A trailing `:`
|
|
398
|
+
* marks a namespace command.
|
|
399
|
+
*/
|
|
400
|
+
command(name: string): this;
|
|
401
|
+
/**
|
|
402
|
+
* Alias for {@link command}.
|
|
403
|
+
*
|
|
404
|
+
* @param name
|
|
405
|
+
* @returns
|
|
406
|
+
*/
|
|
407
|
+
name(name: string): this;
|
|
408
|
+
/**
|
|
409
|
+
* Set the command description.
|
|
410
|
+
*
|
|
411
|
+
* @param name
|
|
412
|
+
* @returns
|
|
413
|
+
*/
|
|
414
|
+
describe(description: string): this;
|
|
415
|
+
/**
|
|
416
|
+
* Alias for {@link describe}.
|
|
417
|
+
*
|
|
418
|
+
* @param name
|
|
419
|
+
* @returns
|
|
420
|
+
*/
|
|
421
|
+
description(description: string): this;
|
|
422
|
+
/**
|
|
423
|
+
* Mark the command as hidden from help output.
|
|
424
|
+
*
|
|
425
|
+
* @param name
|
|
426
|
+
* @returns
|
|
427
|
+
*/
|
|
428
|
+
hide(hidden?: boolean): this;
|
|
429
|
+
/**
|
|
430
|
+
* Add a positional argument.
|
|
431
|
+
*
|
|
432
|
+
* @param name
|
|
433
|
+
* @returns
|
|
434
|
+
*/
|
|
435
|
+
argument(name: string, definition?: ArgumentDefinition): this;
|
|
436
|
+
/**
|
|
437
|
+
* Add an option/flag.
|
|
438
|
+
*
|
|
439
|
+
* @param name
|
|
440
|
+
* @returns
|
|
441
|
+
*/
|
|
442
|
+
option(name: string, definition?: OptionDefinition): this;
|
|
443
|
+
/**
|
|
444
|
+
* The configured command name (empty when unset).
|
|
445
|
+
*
|
|
446
|
+
* @param name
|
|
447
|
+
* @returns
|
|
448
|
+
*/
|
|
449
|
+
getName(): string;
|
|
450
|
+
/**
|
|
451
|
+
* The configured description, if any.
|
|
452
|
+
*
|
|
453
|
+
* @param name
|
|
454
|
+
* @returns
|
|
455
|
+
*/
|
|
456
|
+
getDescription(): string | undefined;
|
|
457
|
+
/**
|
|
458
|
+
* Whether the command is a namespace command (name ends with `:`).
|
|
459
|
+
*
|
|
460
|
+
* @param name
|
|
461
|
+
* @returns
|
|
462
|
+
*/
|
|
463
|
+
isNamespace(): boolean;
|
|
464
|
+
/**
|
|
465
|
+
* Whether anything has been configured on this builder.
|
|
466
|
+
*
|
|
467
|
+
* @param name
|
|
468
|
+
* @returns
|
|
469
|
+
*/
|
|
470
|
+
isEmpty(): boolean;
|
|
471
|
+
/**
|
|
472
|
+
* Build the structured {@link ParsedCommand} consumed by the kernel. This is
|
|
473
|
+
* lossless — no string round-trip — and is the path used for normal commands.
|
|
474
|
+
*
|
|
475
|
+
* @param commandClass
|
|
476
|
+
* @returns
|
|
477
|
+
*/
|
|
478
|
+
toParsed<A extends Application = Application>(commandClass: Command<A>): ParsedCommand<A>;
|
|
479
|
+
private argumentToOption;
|
|
480
|
+
private optionToOption;
|
|
481
|
+
/**
|
|
482
|
+
* Reconstruct the equivalent signature string in musket's DSL. Used by
|
|
483
|
+
* {@link Command.getSignature} so external consumers (and the namespace path)
|
|
484
|
+
* keep working unchanged.
|
|
485
|
+
*/
|
|
486
|
+
toString(): string;
|
|
487
|
+
private argumentToken;
|
|
488
|
+
private optionToken;
|
|
489
|
+
private wrap;
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
330
492
|
//#region src/Core/Command.d.ts
|
|
331
493
|
declare class Command<A extends Application = Application> {
|
|
332
494
|
protected app: A;
|
|
@@ -356,6 +518,11 @@ declare class Command<A extends Application = Application> {
|
|
|
356
518
|
* @var string
|
|
357
519
|
*/
|
|
358
520
|
protected description?: string;
|
|
521
|
+
/**
|
|
522
|
+
* Memoized result of {@link resolveBuilder}. `undefined` means "not yet
|
|
523
|
+
* resolved"; `null` means "resolved, no builder in use".
|
|
524
|
+
*/
|
|
525
|
+
private resolvedBuilder?;
|
|
359
526
|
/**
|
|
360
527
|
* The console command input.
|
|
361
528
|
*
|
|
@@ -409,11 +576,46 @@ declare class Command<A extends Application = Application> {
|
|
|
409
576
|
*/
|
|
410
577
|
setProgram(program: Command$1): this;
|
|
411
578
|
/**
|
|
412
|
-
*
|
|
579
|
+
* Define the command signature programmatically.
|
|
580
|
+
*
|
|
581
|
+
* Override this instead of (or in addition to) the {@link signature} string
|
|
582
|
+
* to describe the command, its arguments and its options through the fluent
|
|
583
|
+
* {@link SignatureBuilder} — no string DSL required. When the `signature`
|
|
584
|
+
* string is set it always takes precedence and this method is ignored.
|
|
585
|
+
*
|
|
586
|
+
* ```ts
|
|
587
|
+
* buildSignature (sig: SignatureBuilder) {
|
|
588
|
+
* return sig
|
|
589
|
+
* .command('queue:work')
|
|
590
|
+
* .describe('Process jobs on the queue')
|
|
591
|
+
* .argument('connection', { required: false })
|
|
592
|
+
* .option('queue', { short: 'Q', description: 'The queue to process' })
|
|
593
|
+
* .option('once', { description: 'Process a single job and exit' })
|
|
594
|
+
* }
|
|
595
|
+
* ```
|
|
596
|
+
*
|
|
597
|
+
* @param _builder A fresh builder to configure.
|
|
598
|
+
*/
|
|
599
|
+
protected buildSignature(_builder: SignatureBuilder): SignatureBuilder | void;
|
|
600
|
+
/**
|
|
601
|
+
* Resolve (and memoize) the signature builder for this command, or `null`
|
|
602
|
+
* when the command uses the string {@link signature} (which always wins) or
|
|
603
|
+
* does not implement {@link buildSignature}.
|
|
604
|
+
*/
|
|
605
|
+
private resolveBuilder;
|
|
606
|
+
/**
|
|
607
|
+
* Get the command signature. Returns the string {@link signature} when set,
|
|
608
|
+
* otherwise reconstructs it from {@link buildSignature}.
|
|
413
609
|
*
|
|
414
610
|
* @returns
|
|
415
611
|
*/
|
|
416
612
|
getSignature(): string;
|
|
613
|
+
/**
|
|
614
|
+
* The structured, lossless parsed command when {@link buildSignature} is used
|
|
615
|
+
* for a non-namespace command; `undefined` otherwise (callers then fall back
|
|
616
|
+
* to parsing {@link getSignature}). This avoids a lossy string round-trip.
|
|
617
|
+
*/
|
|
618
|
+
toParsedSignature(): ParsedCommand<A> | undefined;
|
|
417
619
|
/**
|
|
418
620
|
* Get the command description
|
|
419
621
|
*
|
|
@@ -626,6 +828,13 @@ declare class Musket<A extends Application = Application> {
|
|
|
626
828
|
name: string;
|
|
627
829
|
private config;
|
|
628
830
|
private commands;
|
|
831
|
+
/**
|
|
832
|
+
* Keys (baseCommand, namespace-aware) of commands already registered, so the
|
|
833
|
+
* same command surfacing from more than one source — e.g. discovered both as
|
|
834
|
+
* built `dist/*.js` and as `src/*.ts` via the jiti loader — is only added
|
|
835
|
+
* once. The first registration wins (base commands before discovered ones).
|
|
836
|
+
*/
|
|
837
|
+
private registeredKeys;
|
|
629
838
|
private program;
|
|
630
839
|
constructor(app: A, kernel: Kernel<A>, baseCommands?: Command<A>[], resolver?: CommandMethodResolver | undefined, tsDownConfig?: UserConfig);
|
|
631
840
|
build(): Promise<Command$1>;
|
|
@@ -674,6 +883,21 @@ declare class Musket<A extends Application = Application> {
|
|
|
674
883
|
* @param name The file name without extension.
|
|
675
884
|
*/
|
|
676
885
|
private resolveCommandClass;
|
|
886
|
+
/**
|
|
887
|
+
* Whether a value is a Command class (constructor), as opposed to any other
|
|
888
|
+
* exported function/class.
|
|
889
|
+
*
|
|
890
|
+
* The check is structural — it looks for `getSignature` on the prototype
|
|
891
|
+
* rather than using `instanceof Command` — so it stays correct when the
|
|
892
|
+
* discovered command extends a Command from a different copy/version of
|
|
893
|
+
* musket. Crucially, it rejects non-command exports such as the shared
|
|
894
|
+
* bundler chunks tools like tsdown can emit alongside built commands (e.g. a
|
|
895
|
+
* `Rebuilder` helper), which would otherwise be `new`-ed and then crash when
|
|
896
|
+
* `getSignature()` is called on them.
|
|
897
|
+
*
|
|
898
|
+
* @param value The candidate export.
|
|
899
|
+
*/
|
|
900
|
+
private isCommandClass;
|
|
677
901
|
/**
|
|
678
902
|
* Emit a non-fatal command-discovery warning.
|
|
679
903
|
*
|
|
@@ -681,11 +905,23 @@ declare class Musket<A extends Application = Application> {
|
|
|
681
905
|
*/
|
|
682
906
|
private warnDiscovery;
|
|
683
907
|
/**
|
|
684
|
-
* Push a new command into the commands stack
|
|
908
|
+
* Push a new command into the commands stack.
|
|
909
|
+
*
|
|
910
|
+
* Resolution prefers a command's structured signature (built via
|
|
911
|
+
* {@link Command.buildSignature}) and falls back to parsing its signature
|
|
912
|
+
* string. Commands that expose no usable signature are skipped with a warning
|
|
913
|
+
* rather than crashing the CLI, and a command whose key was already
|
|
914
|
+
* registered is ignored (de-duplication — see {@link registeredKeys}).
|
|
685
915
|
*
|
|
686
916
|
* @param command
|
|
687
917
|
*/
|
|
688
918
|
addCommand(command: Command<A>): this;
|
|
919
|
+
/**
|
|
920
|
+
* A best-effort human label for a command, for diagnostics.
|
|
921
|
+
*
|
|
922
|
+
* @param command
|
|
923
|
+
*/
|
|
924
|
+
private describeCommand;
|
|
689
925
|
/**
|
|
690
926
|
* Push a list of new commands to commands stack
|
|
691
927
|
*
|
|
@@ -736,4 +972,4 @@ declare class Signature {
|
|
|
736
972
|
static parseSignature<A extends Application = Application>(signature: string, commandClass: Command<A>): ParsedCommand<A>;
|
|
737
973
|
}
|
|
738
974
|
//#endregion
|
|
739
|
-
export { Application, Command, CommandMethodResolver, CommandOption, Kernel, KernelConfig, ModuleMeta, Musket, PackageMeta, ParsedCommand, Signature, TGeneric, VersionRenderHelpers, XGeneric };
|
|
975
|
+
export { Application, ArgumentDefinition, Command, CommandMethodResolver, CommandOption, Kernel, KernelConfig, ModuleMeta, Musket, OptionDefinition, PackageMeta, ParsedCommand, Signature, SignatureBuilder, SignatureValue, TGeneric, VersionRenderHelpers, XGeneric };
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,232 @@ 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
|
|
@@ -733,6 +1014,13 @@ var Musket = class Musket {
|
|
|
733
1014
|
name = "musket";
|
|
734
1015
|
config = {};
|
|
735
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();
|
|
736
1024
|
program;
|
|
737
1025
|
constructor(app, kernel, baseCommands = [], resolver, tsDownConfig = {}) {
|
|
738
1026
|
this.app = app;
|
|
@@ -835,9 +1123,26 @@ var Musket = class Musket {
|
|
|
835
1123
|
*/
|
|
836
1124
|
resolveCommandClass(mod, name) {
|
|
837
1125
|
const named = mod[name];
|
|
838
|
-
if (
|
|
839
|
-
if (
|
|
840
|
-
return Object.values(mod).find((value) =>
|
|
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";
|
|
841
1146
|
}
|
|
842
1147
|
/**
|
|
843
1148
|
* Emit a non-fatal command-discovery warning.
|
|
@@ -848,21 +1153,53 @@ var Musket = class Musket {
|
|
|
848
1153
|
Logger.log([["[musket]", "yellow"], [message, "white"]], " ");
|
|
849
1154
|
}
|
|
850
1155
|
/**
|
|
851
|
-
* Push a new command into the commands stack
|
|
852
|
-
*
|
|
853
|
-
*
|
|
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
|
|
854
1165
|
*/
|
|
855
1166
|
addCommand(command) {
|
|
856
|
-
|
|
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);
|
|
857
1186
|
return this;
|
|
858
1187
|
}
|
|
859
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
|
+
/**
|
|
860
1197
|
* Push a list of new commands to commands stack
|
|
861
|
-
*
|
|
862
|
-
* @param command
|
|
1198
|
+
*
|
|
1199
|
+
* @param command
|
|
863
1200
|
*/
|
|
864
1201
|
registerCommands(commands) {
|
|
865
|
-
commands.forEach(this.addCommand);
|
|
1202
|
+
commands.forEach((e) => this.addCommand(e));
|
|
866
1203
|
return this;
|
|
867
1204
|
}
|
|
868
1205
|
/**
|
|
@@ -904,7 +1241,7 @@ var Musket = class Musket {
|
|
|
904
1241
|
* Load the root command here
|
|
905
1242
|
*/
|
|
906
1243
|
const root = new this.config.rootCommand(this.app, this.kernel);
|
|
907
|
-
const sign = Signature.parseSignature(root.getSignature(), root);
|
|
1244
|
+
const sign = root.toParsedSignature?.() ?? Signature.parseSignature(root.getSignature(), root);
|
|
908
1245
|
const cmd = this.program.name(sign.baseCommand).description(sign.description ?? sign.baseCommand).configureHelp({ showGlobalOptions: true }).action(async () => {
|
|
909
1246
|
root.setInput(this.program.opts(), this.program.args, this.program.registeredArguments, {}, this.program);
|
|
910
1247
|
await this.handle(root);
|
|
@@ -1274,4 +1611,4 @@ var Kernel = class Kernel {
|
|
|
1274
1611
|
}
|
|
1275
1612
|
};
|
|
1276
1613
|
//#endregion
|
|
1277
|
-
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",
|