@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.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
- * Get the command signature
122
- *
123
- * @returns
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.modules.map((e) => {
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 name = path.basename(pth).replaceAll(/\.ts|\.js|\.mjs/g, "");
1072
+ const file = pth.toString();
1073
+ const name = path.basename(file).replace(/\.(c|m)?(t|j)s$/, "");
788
1074
  try {
789
- const cmdClass = (await import(pth))[name];
790
- commands.push(new cmdClass(this.app, this.kernel));
791
- } catch {}
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
- * Push a new command into the commands stack
797
- *
798
- * @param command
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
- this.commands.push(Signature.parseSignature(command.getSignature(), command));
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
- * Get the provided packages versions
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.modules.map((e) => {
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 (base === true && version) pkg.version = version;
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.0.0",
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",