@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 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
- * Get the command signature
146
- *
147
- * @returns
376
+ * Define the command signature programmatically.
377
+ *
378
+ * Override this instead of (or in addition to) the {@link signature} string
379
+ * to describe the command, its arguments and its options through the fluent
380
+ * {@link SignatureBuilder} — no string DSL required. When the `signature`
381
+ * string is set it always takes precedence and this method is ignored.
382
+ *
383
+ * ```ts
384
+ * buildSignature (sig: SignatureBuilder) {
385
+ * return sig
386
+ * .command('queue:work')
387
+ * .describe('Process jobs on the queue')
388
+ * .argument('connection', { required: false })
389
+ * .option('queue', { short: 'Q', description: 'The queue to process' })
390
+ * .option('once', { description: 'Process a single job and exit' })
391
+ * }
392
+ * ```
393
+ *
394
+ * @param _builder A fresh builder to configure.
395
+ */
396
+ buildSignature(_builder) {
397
+ /** Override to use. */
398
+ }
399
+ /**
400
+ * Resolve (and memoize) the signature builder for this command, or `null`
401
+ * when the command uses the string {@link signature} (which always wins) or
402
+ * does not implement {@link buildSignature}.
403
+ */
404
+ resolveBuilder() {
405
+ if (this.resolvedBuilder !== void 0) return this.resolvedBuilder;
406
+ if (this.signature) return this.resolvedBuilder = null;
407
+ const builder = new SignatureBuilder();
408
+ const result = this.buildSignature(builder);
409
+ const resolved = result instanceof SignatureBuilder ? result : builder;
410
+ return this.resolvedBuilder = resolved.isEmpty() ? null : resolved;
411
+ }
412
+ /**
413
+ * Get the command signature. Returns the string {@link signature} when set,
414
+ * otherwise reconstructs it from {@link buildSignature}.
415
+ *
416
+ * @returns
148
417
  */
149
418
  getSignature() {
150
- return this.signature;
419
+ if (this.signature) return this.signature;
420
+ return this.resolveBuilder()?.toString() ?? this.signature;
421
+ }
422
+ /**
423
+ * The structured, lossless parsed command when {@link buildSignature} is used
424
+ * for a non-namespace command; `undefined` otherwise (callers then fall back
425
+ * to parsing {@link getSignature}). This avoids a lossy string round-trip.
426
+ */
427
+ toParsedSignature() {
428
+ const builder = this.resolveBuilder();
429
+ if (!builder || builder.isNamespace()) return;
430
+ return builder.toParsed(this);
151
431
  }
152
432
  /**
153
433
  * Get the command description
154
- *
155
- * @returns
434
+ *
435
+ * @returns
156
436
  */
157
437
  getDescription() {
158
- return this.description;
438
+ if (this.description) return this.description;
439
+ return this.resolveBuilder()?.getDescription() ?? this.description;
159
440
  }
160
441
  /**
161
442
  * Get a specific option
@@ -505,11 +786,8 @@ var ListCommand = class ListCommand extends Command {
505
786
  return _h3ravel_shared.Logger.describe(_h3ravel_shared.Logger.log(" " + e.name(), "green", false), e.description(), 25, false).join("");
506
787
  });
507
788
  const list = ListCommand.groupItems(commands);
508
- /** Output the modules version */
509
- const version = this.kernel.modules.map((e) => {
510
- const value = String(e.alias ?? e.name).split("/").pop().replace(/[-_]/g, " ").replace(/cli/gi, (match) => match === "cli" ? "CLI" : match).replace(/^./, (c) => c.toUpperCase());
511
- return _h3ravel_shared.Logger.log([[`${value}:`, "white"], [e.version, "green"]], " ", false);
512
- }).join(" | ");
789
+ /** Output the modules version (shared renderer, overridable via config) */
790
+ const version = this.kernel.getVersionString();
513
791
  this.newLine();
514
792
  console.log(version);
515
793
  this.newLine();
@@ -760,6 +1038,13 @@ var Musket = class Musket {
760
1038
  name = "musket";
761
1039
  config = {};
762
1040
  commands = [];
1041
+ /**
1042
+ * Keys (baseCommand, namespace-aware) of commands already registered, so the
1043
+ * same command surfacing from more than one source — e.g. discovered both as
1044
+ * built `dist/*.js` and as `src/*.ts` via the jiti loader — is only added
1045
+ * once. The first registration wins (base commands before discovered ones).
1046
+ */
1047
+ registeredKeys = /* @__PURE__ */ new Set();
763
1048
  program;
764
1049
  constructor(app, kernel, baseCommands = [], resolver, tsDownConfig = {}) {
765
1050
  this.app = app;
@@ -808,30 +1093,137 @@ var Musket = class Musket {
808
1093
  * CLI Commands auto registration
809
1094
  */
810
1095
  for await (const pth of glob.glob.stream(paths)) {
811
- const name = node_path.default.basename(pth).replaceAll(/\.ts|\.js|\.mjs/g, "");
1096
+ const file = pth.toString();
1097
+ const name = node_path.default.basename(file).replace(/\.(c|m)?(t|j)s$/, "");
812
1098
  try {
813
- const cmdClass = (await import(pth))[name];
814
- commands.push(new cmdClass(this.app, this.kernel));
815
- } catch {}
1099
+ const mod = await this.importCommandModule(file);
1100
+ const CommandClass = this.resolveCommandClass(mod, name);
1101
+ if (!CommandClass) {
1102
+ this.warnDiscovery(`No command class export found in ${file}`);
1103
+ continue;
1104
+ }
1105
+ commands.push(new CommandClass(this.app, this.kernel));
1106
+ } catch (error) {
1107
+ /**
1108
+ * Never swallow load failures silently — a command that throws on
1109
+ * import would otherwise just vanish from the CLI with no clue why.
1110
+ */
1111
+ this.warnDiscovery(`Failed to load command ${file}: ${error?.message ?? String(error)}`);
1112
+ }
816
1113
  }
817
1114
  commands.forEach((e) => this.addCommand(e));
818
1115
  }
819
1116
  /**
820
- * Push a new command into the commands stack
821
- *
822
- * @param command
1117
+ * Import a discovered command module with full TypeScript support.
1118
+ *
1119
+ * Native `import()` is attempted first: it loads built `.js`, and works
1120
+ * verbatim in TypeScript-aware runtimes/test loaders (vitest, tsx, Node with
1121
+ * type stripping) — which also keeps their module mocking and single module
1122
+ * registry intact. When native import can't load a TypeScript source (plain
1123
+ * Node throwing on a `.ts`/`.mts`/`.cts` file), it is transpiled on the fly
1124
+ * via jiti (`@h3ravel/shared`'s `importFile`) so commands are discovered
1125
+ * without a prior build. Consumers can bypass all of this with
1126
+ * {@link KernelConfig.importModule}.
1127
+ *
1128
+ * @param file Absolute or cwd-relative path to the command module.
1129
+ */
1130
+ async importCommandModule(file) {
1131
+ if (this.config.importModule) return await this.config.importModule(file);
1132
+ try {
1133
+ return await import(file);
1134
+ } catch (error) {
1135
+ if (/\.(c|m)?ts$/.test(file)) return await (0, _h3ravel_shared.importFile)(file);
1136
+ throw error;
1137
+ }
1138
+ }
1139
+ /**
1140
+ * Resolve the command class out of an imported module. Prefers the export
1141
+ * named after the file, then a `default` export, then the first exported
1142
+ * constructor — because a file's name and its exported class name do not
1143
+ * always match.
1144
+ *
1145
+ * @param mod The imported module namespace.
1146
+ * @param name The file name without extension.
1147
+ */
1148
+ resolveCommandClass(mod, name) {
1149
+ const named = mod[name];
1150
+ if (this.isCommandClass(named)) return named;
1151
+ if (this.isCommandClass(mod.default)) return mod.default;
1152
+ return Object.values(mod).find((value) => this.isCommandClass(value));
1153
+ }
1154
+ /**
1155
+ * Whether a value is a Command class (constructor), as opposed to any other
1156
+ * exported function/class.
1157
+ *
1158
+ * The check is structural — it looks for `getSignature` on the prototype
1159
+ * rather than using `instanceof Command` — so it stays correct when the
1160
+ * discovered command extends a Command from a different copy/version of
1161
+ * musket. Crucially, it rejects non-command exports such as the shared
1162
+ * bundler chunks tools like tsdown can emit alongside built commands (e.g. a
1163
+ * `Rebuilder` helper), which would otherwise be `new`-ed and then crash when
1164
+ * `getSignature()` is called on them.
1165
+ *
1166
+ * @param value The candidate export.
1167
+ */
1168
+ isCommandClass(value) {
1169
+ return typeof value === "function" && typeof value.prototype?.getSignature === "function";
1170
+ }
1171
+ /**
1172
+ * Emit a non-fatal command-discovery warning.
1173
+ *
1174
+ * @param message
1175
+ */
1176
+ warnDiscovery(message) {
1177
+ _h3ravel_shared.Logger.log([["[musket]", "yellow"], [message, "white"]], " ");
1178
+ }
1179
+ /**
1180
+ * Push a new command into the commands stack.
1181
+ *
1182
+ * Resolution prefers a command's structured signature (built via
1183
+ * {@link Command.buildSignature}) and falls back to parsing its signature
1184
+ * string. Commands that expose no usable signature are skipped with a warning
1185
+ * rather than crashing the CLI, and a command whose key was already
1186
+ * registered is ignored (de-duplication — see {@link registeredKeys}).
1187
+ *
1188
+ * @param command
823
1189
  */
824
1190
  addCommand(command) {
825
- this.commands.push(Signature.parseSignature(command.getSignature(), command));
1191
+ if (!command || typeof command.getSignature !== "function") {
1192
+ this.warnDiscovery(`Skipping ${this.describeCommand(command)}: not a valid command (no getSignature()).`);
1193
+ return this;
1194
+ }
1195
+ let parsed;
1196
+ try {
1197
+ parsed = command.toParsedSignature?.() ?? Signature.parseSignature(command.getSignature(), command);
1198
+ } catch (error) {
1199
+ this.warnDiscovery(`Skipping ${this.describeCommand(command)}: ${error?.message ?? String(error)}`);
1200
+ return this;
1201
+ }
1202
+ if (!parsed || !parsed.baseCommand) {
1203
+ this.warnDiscovery(`Skipping ${this.describeCommand(command)}: empty or unparsable signature.`);
1204
+ return this;
1205
+ }
1206
+ const key = parsed.isNamespaceCommand ? `${parsed.baseCommand}:` : parsed.baseCommand;
1207
+ if (this.registeredKeys.has(key)) return this;
1208
+ this.registeredKeys.add(key);
1209
+ this.commands.push(parsed);
826
1210
  return this;
827
1211
  }
828
1212
  /**
1213
+ * A best-effort human label for a command, for diagnostics.
1214
+ *
1215
+ * @param command
1216
+ */
1217
+ describeCommand(command) {
1218
+ return command?.constructor?.name ?? "command";
1219
+ }
1220
+ /**
829
1221
  * Push a list of new commands to commands stack
830
- *
831
- * @param command
1222
+ *
1223
+ * @param command
832
1224
  */
833
1225
  registerCommands(commands) {
834
- commands.forEach(this.addCommand);
1226
+ commands.forEach((e) => this.addCommand(e));
835
1227
  return this;
836
1228
  }
837
1229
  /**
@@ -842,13 +1234,11 @@ var Musket = class Musket {
842
1234
  }
843
1235
  async initialize() {
844
1236
  if (process.argv.includes("--help") || process.argv.includes("-h")) await this.rebuild("help");
845
- /**
846
- * Get the provided packages versions
1237
+ /**
1238
+ * Render the provided packages versions (single source of truth shared
1239
+ * with ListCommand, fully overridable via KernelConfig).
847
1240
  */
848
- const moduleVersions = this.kernel.modules.map((e) => {
849
- const value = String(e.alias ?? e.name).split("/").pop().replace(/[-_]/g, " ").replace(/cli/gi, (match) => match === "cli" ? "CLI" : match).replace(/^./, (c) => c.toUpperCase());
850
- return _h3ravel_shared.Logger.parse([[`${value}:`, "white"], [e.version, "green"]], " ", false);
851
- }).join(" | ");
1241
+ const moduleVersions = this.kernel.getVersionString();
852
1242
  const additional = {
853
1243
  quiet: ["-q, --quiet", "Do not output any message except errors and warnings"],
854
1244
  silent: ["--silent", "Do not output any message"],
@@ -875,7 +1265,7 @@ var Musket = class Musket {
875
1265
  * Load the root command here
876
1266
  */
877
1267
  const root = new this.config.rootCommand(this.app, this.kernel);
878
- const sign = Signature.parseSignature(root.getSignature(), root);
1268
+ const sign = root.toParsedSignature?.() ?? Signature.parseSignature(root.getSignature(), root);
879
1269
  const cmd = this.program.name(sign.baseCommand).description(sign.description ?? sign.baseCommand).configureHelp({ showGlobalOptions: true }).action(async () => {
880
1270
  root.setInput(this.program.opts(), this.program.args, this.program.registeredArguments, {}, this.program);
881
1271
  await this.handle(root);
@@ -1177,13 +1567,18 @@ var Kernel = class Kernel {
1177
1567
  for (let i = 0; i < this.packages.length; i++) try {
1178
1568
  const item = this.packages[i];
1179
1569
  const name = typeof item === "string" ? item : item.name;
1180
- const alias = typeof item === "string" ? item : item.alias;
1570
+ const alias = typeof item === "string" ? item : item.alias ?? item.name;
1181
1571
  const base = typeof item === "string" ? false : item.base;
1572
+ const label = typeof item === "string" ? void 0 : item.label;
1573
+ const versionOverride = typeof item === "string" ? void 0 : item.version;
1182
1574
  const modulePath = _h3ravel_shared.FileSystem.findModulePkg(name, this.cwd) ?? "";
1183
1575
  const pkg = require(node_path.default.join(modulePath, "package.json"));
1184
1576
  pkg.alias = alias;
1185
1577
  pkg.base = base;
1186
- if (base === true && version) pkg.version = version;
1578
+ if (label) pkg.label = label;
1579
+ /** A per-package version wins, then the base-package override. */
1580
+ if (versionOverride) pkg.version = versionOverride;
1581
+ else if (base === true && version) pkg.version = version;
1187
1582
  this.modules.push(pkg);
1188
1583
  } catch {
1189
1584
  this.modules.push({
@@ -1200,9 +1595,48 @@ var Kernel = class Kernel {
1200
1595
  }
1201
1596
  return this;
1202
1597
  }
1598
+ /**
1599
+ * Format a module's display label: strip the package scope, turn `-`/`_`
1600
+ * into spaces, normalise "cli" casing and capitalise. A module's explicit
1601
+ * `label` short-circuits all of this.
1602
+ *
1603
+ * @param module
1604
+ */
1605
+ formatModuleLabel(module) {
1606
+ if (module.label) return module.label;
1607
+ return String(module.alias ?? module.name).split("/").pop().replace(/[-_]/g, " ").replace(/cli/gi, (match) => match === "cli" ? "CLI" : match).replace(/^./, (c) => c.toUpperCase());
1608
+ }
1609
+ /**
1610
+ * Render a single module as a colored `Label: version` segment, honoring
1611
+ * {@link KernelConfig.versionColors}.
1612
+ *
1613
+ * @param module
1614
+ */
1615
+ renderModuleVersion(module) {
1616
+ const colors = this.config.versionColors ?? {};
1617
+ return _h3ravel_shared.Logger.parse([[`${this.formatModuleLabel(module)}:`, colors.label ?? "white"], [String(module.version), colors.version ?? "green"]], " ", false);
1618
+ }
1619
+ /**
1620
+ * Build the full version line shown for `--version` and atop the command
1621
+ * list. A single source of truth for both render sites.
1622
+ *
1623
+ * Honors {@link KernelConfig.versionFormatter} for complete control,
1624
+ * otherwise joins each module with {@link KernelConfig.versionSeparator}.
1625
+ */
1626
+ getVersionString() {
1627
+ const modules = this.modules;
1628
+ const separator = this.config.versionSeparator ?? " | ";
1629
+ if (this.config.versionFormatter) return this.config.versionFormatter(modules, {
1630
+ format: (module) => this.renderModuleVersion(module),
1631
+ label: (module) => this.formatModuleLabel(module),
1632
+ separator
1633
+ });
1634
+ return modules.map((module) => this.renderModuleVersion(module)).join(separator);
1635
+ }
1203
1636
  };
1204
1637
  //#endregion
1205
1638
  exports.Command = Command;
1206
1639
  exports.Kernel = Kernel;
1207
1640
  exports.Musket = Musket;
1208
1641
  exports.Signature = Signature;
1642
+ exports.SignatureBuilder = SignatureBuilder;