@h3ravel/musket 2.0.0 → 2.1.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
@@ -505,11 +505,8 @@ var ListCommand = class ListCommand extends Command {
505
505
  return _h3ravel_shared.Logger.describe(_h3ravel_shared.Logger.log(" " + e.name(), "green", false), e.description(), 25, false).join("");
506
506
  });
507
507
  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(" | ");
508
+ /** Output the modules version (shared renderer, overridable via config) */
509
+ const version = this.kernel.getVersionString();
513
510
  this.newLine();
514
511
  console.log(version);
515
512
  this.newLine();
@@ -808,15 +805,73 @@ var Musket = class Musket {
808
805
  * CLI Commands auto registration
809
806
  */
810
807
  for await (const pth of glob.glob.stream(paths)) {
811
- const name = node_path.default.basename(pth).replaceAll(/\.ts|\.js|\.mjs/g, "");
808
+ const file = pth.toString();
809
+ const name = node_path.default.basename(file).replace(/\.(c|m)?(t|j)s$/, "");
812
810
  try {
813
- const cmdClass = (await import(pth))[name];
814
- commands.push(new cmdClass(this.app, this.kernel));
815
- } catch {}
811
+ const mod = await this.importCommandModule(file);
812
+ const CommandClass = this.resolveCommandClass(mod, name);
813
+ if (!CommandClass) {
814
+ this.warnDiscovery(`No command class export found in ${file}`);
815
+ continue;
816
+ }
817
+ commands.push(new CommandClass(this.app, this.kernel));
818
+ } catch (error) {
819
+ /**
820
+ * Never swallow load failures silently — a command that throws on
821
+ * import would otherwise just vanish from the CLI with no clue why.
822
+ */
823
+ this.warnDiscovery(`Failed to load command ${file}: ${error?.message ?? String(error)}`);
824
+ }
816
825
  }
817
826
  commands.forEach((e) => this.addCommand(e));
818
827
  }
819
828
  /**
829
+ * Import a discovered command module with full TypeScript support.
830
+ *
831
+ * Native `import()` is attempted first: it loads built `.js`, and works
832
+ * verbatim in TypeScript-aware runtimes/test loaders (vitest, tsx, Node with
833
+ * type stripping) — which also keeps their module mocking and single module
834
+ * registry intact. When native import can't load a TypeScript source (plain
835
+ * Node throwing on a `.ts`/`.mts`/`.cts` file), it is transpiled on the fly
836
+ * via jiti (`@h3ravel/shared`'s `importFile`) so commands are discovered
837
+ * without a prior build. Consumers can bypass all of this with
838
+ * {@link KernelConfig.importModule}.
839
+ *
840
+ * @param file Absolute or cwd-relative path to the command module.
841
+ */
842
+ async importCommandModule(file) {
843
+ if (this.config.importModule) return await this.config.importModule(file);
844
+ try {
845
+ return await import(file);
846
+ } catch (error) {
847
+ if (/\.(c|m)?ts$/.test(file)) return await (0, _h3ravel_shared.importFile)(file);
848
+ throw error;
849
+ }
850
+ }
851
+ /**
852
+ * Resolve the command class out of an imported module. Prefers the export
853
+ * named after the file, then a `default` export, then the first exported
854
+ * constructor — because a file's name and its exported class name do not
855
+ * always match.
856
+ *
857
+ * @param mod The imported module namespace.
858
+ * @param name The file name without extension.
859
+ */
860
+ resolveCommandClass(mod, name) {
861
+ const named = mod[name];
862
+ if (typeof named === "function") return named;
863
+ if (typeof mod.default === "function") return mod.default;
864
+ return Object.values(mod).find((value) => typeof value === "function");
865
+ }
866
+ /**
867
+ * Emit a non-fatal command-discovery warning.
868
+ *
869
+ * @param message
870
+ */
871
+ warnDiscovery(message) {
872
+ _h3ravel_shared.Logger.log([["[musket]", "yellow"], [message, "white"]], " ");
873
+ }
874
+ /**
820
875
  * Push a new command into the commands stack
821
876
  *
822
877
  * @param command
@@ -842,13 +897,11 @@ var Musket = class Musket {
842
897
  }
843
898
  async initialize() {
844
899
  if (process.argv.includes("--help") || process.argv.includes("-h")) await this.rebuild("help");
845
- /**
846
- * Get the provided packages versions
900
+ /**
901
+ * Render the provided packages versions (single source of truth shared
902
+ * with ListCommand, fully overridable via KernelConfig).
847
903
  */
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(" | ");
904
+ const moduleVersions = this.kernel.getVersionString();
852
905
  const additional = {
853
906
  quiet: ["-q, --quiet", "Do not output any message except errors and warnings"],
854
907
  silent: ["--silent", "Do not output any message"],
@@ -1177,13 +1230,18 @@ var Kernel = class Kernel {
1177
1230
  for (let i = 0; i < this.packages.length; i++) try {
1178
1231
  const item = this.packages[i];
1179
1232
  const name = typeof item === "string" ? item : item.name;
1180
- const alias = typeof item === "string" ? item : item.alias;
1233
+ const alias = typeof item === "string" ? item : item.alias ?? item.name;
1181
1234
  const base = typeof item === "string" ? false : item.base;
1235
+ const label = typeof item === "string" ? void 0 : item.label;
1236
+ const versionOverride = typeof item === "string" ? void 0 : item.version;
1182
1237
  const modulePath = _h3ravel_shared.FileSystem.findModulePkg(name, this.cwd) ?? "";
1183
1238
  const pkg = require(node_path.default.join(modulePath, "package.json"));
1184
1239
  pkg.alias = alias;
1185
1240
  pkg.base = base;
1186
- if (base === true && version) pkg.version = version;
1241
+ if (label) pkg.label = label;
1242
+ /** A per-package version wins, then the base-package override. */
1243
+ if (versionOverride) pkg.version = versionOverride;
1244
+ else if (base === true && version) pkg.version = version;
1187
1245
  this.modules.push(pkg);
1188
1246
  } catch {
1189
1247
  this.modules.push({
@@ -1200,6 +1258,44 @@ var Kernel = class Kernel {
1200
1258
  }
1201
1259
  return this;
1202
1260
  }
1261
+ /**
1262
+ * Format a module's display label: strip the package scope, turn `-`/`_`
1263
+ * into spaces, normalise "cli" casing and capitalise. A module's explicit
1264
+ * `label` short-circuits all of this.
1265
+ *
1266
+ * @param module
1267
+ */
1268
+ formatModuleLabel(module) {
1269
+ if (module.label) return module.label;
1270
+ return String(module.alias ?? module.name).split("/").pop().replace(/[-_]/g, " ").replace(/cli/gi, (match) => match === "cli" ? "CLI" : match).replace(/^./, (c) => c.toUpperCase());
1271
+ }
1272
+ /**
1273
+ * Render a single module as a colored `Label: version` segment, honoring
1274
+ * {@link KernelConfig.versionColors}.
1275
+ *
1276
+ * @param module
1277
+ */
1278
+ renderModuleVersion(module) {
1279
+ const colors = this.config.versionColors ?? {};
1280
+ return _h3ravel_shared.Logger.parse([[`${this.formatModuleLabel(module)}:`, colors.label ?? "white"], [String(module.version), colors.version ?? "green"]], " ", false);
1281
+ }
1282
+ /**
1283
+ * Build the full version line shown for `--version` and atop the command
1284
+ * list. A single source of truth for both render sites.
1285
+ *
1286
+ * Honors {@link KernelConfig.versionFormatter} for complete control,
1287
+ * otherwise joins each module with {@link KernelConfig.versionSeparator}.
1288
+ */
1289
+ getVersionString() {
1290
+ const modules = this.modules;
1291
+ const separator = this.config.versionSeparator ?? " | ";
1292
+ if (this.config.versionFormatter) return this.config.versionFormatter(modules, {
1293
+ format: (module) => this.renderModuleVersion(module),
1294
+ label: (module) => this.formatModuleLabel(module),
1295
+ separator
1296
+ });
1297
+ return modules.map((module) => this.renderModuleVersion(module)).join(separator);
1298
+ }
1203
1299
  };
1204
1300
  //#endregion
1205
1301
  exports.Command = Command;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ChoiceOrSeparatorArray, Choices, Spinner } from "@h3ravel/shared";
1
+ import { ChoiceOrSeparatorArray, Choices, LoggerChalk, Spinner } from "@h3ravel/shared";
2
2
  import { Argument, Command as Command$1 } from "commander";
3
3
  import { UserConfig } from "tsdown";
4
4
 
@@ -52,9 +52,45 @@ type ParsedCommand<A extends Application = Application> = {
52
52
  options?: CommandOption[];
53
53
  };
54
54
  type PackageMeta = string | {
55
+ /**
56
+ * The package name to resolve version info from (its `package.json`).
57
+ */
55
58
  name: string;
56
- alias: string;
59
+ /**
60
+ * Display alias; defaults to `name`. Still passes through label formatting.
61
+ */
62
+ alias?: string;
63
+ /**
64
+ * Mark as the base package whose version `KernelConfig.version` overrides.
65
+ */
57
66
  base?: boolean;
67
+ /**
68
+ * Exact display label, bypassing the default name formatting entirely.
69
+ */
70
+ label?: string;
71
+ /**
72
+ * Hardcoded version string, bypassing `package.json` resolution.
73
+ */
74
+ version?: string;
75
+ };
76
+ /**
77
+ * Resolved metadata for a single module shown in the CLI version line.
78
+ */
79
+ type ModuleMeta = {
80
+ name: string;
81
+ version: string;
82
+ alias?: string;
83
+ label?: string;
84
+ base?: boolean;
85
+ };
86
+ /**
87
+ * Helpers handed to {@link KernelConfig.versionFormatter} so a custom renderer
88
+ * can reuse the default per-module formatting while controlling the layout.
89
+ */
90
+ type VersionRenderHelpers = {
91
+ /** Default per-module renderer: the colored `Label: version` segment. */format: (module: ModuleMeta) => string; /** Default label formatter (scope-stripped, spaced, capitalized). */
92
+ label: (module: ModuleMeta) => string; /** The separator that would be used between modules. */
93
+ separator: string;
58
94
  };
59
95
  type CommandMethodResolver = <X extends Command>(cmd: X, met: any) => Promise<X>;
60
96
  interface KernelConfig<A extends Application = Application> {
@@ -74,6 +110,42 @@ interface KernelConfig<A extends Application = Application> {
74
110
  * @default musket
75
111
  */
76
112
  version?: string;
113
+ /**
114
+ * Separator rendered between modules in the version line.
115
+ *
116
+ * @default ' | '
117
+ */
118
+ versionSeparator?: string;
119
+ /**
120
+ * Colors used by the default version renderer. Ignored when
121
+ * {@link versionFormatter} is supplied.
122
+ */
123
+ versionColors?: {
124
+ /**
125
+ * Color of each module's label.
126
+ *
127
+ * @default 'white'
128
+ */
129
+ label?: LoggerChalk;
130
+ /**
131
+ * Color of each module's version.
132
+ *
133
+ * @default 'green'
134
+ */
135
+ version?: LoggerChalk;
136
+ };
137
+ /**
138
+ * Fully override how the version line is rendered. Receives the resolved
139
+ * modules plus helpers (default per-module formatter, label formatter and
140
+ * separator) and must return the final string shown for `--version` and
141
+ * atop the command list. When omitted, modules are joined with
142
+ * {@link versionSeparator} using the default colored `Label: version`
143
+ * layout.
144
+ *
145
+ * @param modules The resolved module metadata.
146
+ * @param helpers Default formatters so layout can be customized cheaply.
147
+ */
148
+ versionFormatter?: (modules: ModuleMeta[], helpers: VersionRenderHelpers) => string;
77
149
  /**
78
150
  * Don't parse the command, usefull for testing or manual control
79
151
  */
@@ -124,6 +196,17 @@ interface KernelConfig<A extends Application = Application> {
124
196
  * @example ['Console/Commands/*.js', 'src/app/Commands/*.js']
125
197
  */
126
198
  discoveryPaths?: string | string[];
199
+ /**
200
+ * Optional override for how discovered command modules are imported.
201
+ * Receives the matched file path and must resolve the module namespace.
202
+ *
203
+ * When omitted, musket loads TypeScript sources via jiti (`@h3ravel/shared`'s
204
+ * `importFile`) and JavaScript natively, so `.ts` commands are discovered
205
+ * without a prior build.
206
+ *
207
+ * @param filePath The matched command file path.
208
+ */
209
+ importModule?: (filePath: string) => Promise<Record<string, unknown>>;
127
210
  }
128
211
  //#endregion
129
212
  //#region src/Contracts/Utils.d.ts
@@ -140,12 +223,7 @@ declare class Kernel<A extends Application = Application> {
140
223
  */
141
224
  private cwd;
142
225
  output: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function";
143
- modules: XGeneric<{
144
- version: string;
145
- name: string;
146
- base?: boolean;
147
- alias?: string;
148
- }>[];
226
+ modules: XGeneric<ModuleMeta>[];
149
227
  /**
150
228
  * The base path for the CLI app
151
229
  */
@@ -224,6 +302,29 @@ declare class Kernel<A extends Application = Application> {
224
302
  * Prepares the CLI for execution
225
303
  */
226
304
  bootstrap(): this;
305
+ /**
306
+ * Format a module's display label: strip the package scope, turn `-`/`_`
307
+ * into spaces, normalise "cli" casing and capitalise. A module's explicit
308
+ * `label` short-circuits all of this.
309
+ *
310
+ * @param module
311
+ */
312
+ formatModuleLabel(module: ModuleMeta): string;
313
+ /**
314
+ * Render a single module as a colored `Label: version` segment, honoring
315
+ * {@link KernelConfig.versionColors}.
316
+ *
317
+ * @param module
318
+ */
319
+ renderModuleVersion(module: ModuleMeta): string;
320
+ /**
321
+ * Build the full version line shown for `--version` and atop the command
322
+ * list. A single source of truth for both render sites.
323
+ *
324
+ * Honors {@link KernelConfig.versionFormatter} for complete control,
325
+ * otherwise joins each module with {@link KernelConfig.versionSeparator}.
326
+ */
327
+ getVersionString(): string;
227
328
  }
228
329
  //#endregion
229
330
  //#region src/Core/Command.d.ts
@@ -548,6 +649,37 @@ declare class Musket<A extends Application = Application> {
548
649
  */
549
650
  discoverCommandsFrom(paths: string | string[]): this;
550
651
  private loadDiscoveredCommands;
652
+ /**
653
+ * Import a discovered command module with full TypeScript support.
654
+ *
655
+ * Native `import()` is attempted first: it loads built `.js`, and works
656
+ * verbatim in TypeScript-aware runtimes/test loaders (vitest, tsx, Node with
657
+ * type stripping) — which also keeps their module mocking and single module
658
+ * registry intact. When native import can't load a TypeScript source (plain
659
+ * Node throwing on a `.ts`/`.mts`/`.cts` file), it is transpiled on the fly
660
+ * via jiti (`@h3ravel/shared`'s `importFile`) so commands are discovered
661
+ * without a prior build. Consumers can bypass all of this with
662
+ * {@link KernelConfig.importModule}.
663
+ *
664
+ * @param file Absolute or cwd-relative path to the command module.
665
+ */
666
+ private importCommandModule;
667
+ /**
668
+ * Resolve the command class out of an imported module. Prefers the export
669
+ * named after the file, then a `default` export, then the first exported
670
+ * constructor — because a file's name and its exported class name do not
671
+ * always match.
672
+ *
673
+ * @param mod The imported module namespace.
674
+ * @param name The file name without extension.
675
+ */
676
+ private resolveCommandClass;
677
+ /**
678
+ * Emit a non-fatal command-discovery warning.
679
+ *
680
+ * @param message
681
+ */
682
+ private warnDiscovery;
551
683
  /**
552
684
  * Push a new command into the commands stack
553
685
  *
@@ -604,4 +736,4 @@ declare class Signature {
604
736
  static parseSignature<A extends Application = Application>(signature: string, commandClass: Command<A>): ParsedCommand<A>;
605
737
  }
606
738
  //#endregion
607
- export { Application, Command, CommandMethodResolver, CommandOption, Kernel, KernelConfig, Musket, PackageMeta, ParsedCommand, Signature, TGeneric, XGeneric };
739
+ export { Application, Command, CommandMethodResolver, CommandOption, Kernel, KernelConfig, ModuleMeta, Musket, PackageMeta, ParsedCommand, Signature, TGeneric, VersionRenderHelpers, XGeneric };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
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";
@@ -481,11 +481,8 @@ var ListCommand = class ListCommand extends Command {
481
481
  return Logger.describe(Logger.log(" " + e.name(), "green", false), e.description(), 25, false).join("");
482
482
  });
483
483
  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(" | ");
484
+ /** Output the modules version (shared renderer, overridable via config) */
485
+ const version = this.kernel.getVersionString();
489
486
  this.newLine();
490
487
  console.log(version);
491
488
  this.newLine();
@@ -784,15 +781,73 @@ var Musket = class Musket {
784
781
  * CLI Commands auto registration
785
782
  */
786
783
  for await (const pth of glob.stream(paths)) {
787
- const name = path.basename(pth).replaceAll(/\.ts|\.js|\.mjs/g, "");
784
+ const file = pth.toString();
785
+ const name = path.basename(file).replace(/\.(c|m)?(t|j)s$/, "");
788
786
  try {
789
- const cmdClass = (await import(pth))[name];
790
- commands.push(new cmdClass(this.app, this.kernel));
791
- } catch {}
787
+ const mod = await this.importCommandModule(file);
788
+ const CommandClass = this.resolveCommandClass(mod, name);
789
+ if (!CommandClass) {
790
+ this.warnDiscovery(`No command class export found in ${file}`);
791
+ continue;
792
+ }
793
+ commands.push(new CommandClass(this.app, this.kernel));
794
+ } catch (error) {
795
+ /**
796
+ * Never swallow load failures silently — a command that throws on
797
+ * import would otherwise just vanish from the CLI with no clue why.
798
+ */
799
+ this.warnDiscovery(`Failed to load command ${file}: ${error?.message ?? String(error)}`);
800
+ }
792
801
  }
793
802
  commands.forEach((e) => this.addCommand(e));
794
803
  }
795
804
  /**
805
+ * Import a discovered command module with full TypeScript support.
806
+ *
807
+ * Native `import()` is attempted first: it loads built `.js`, and works
808
+ * verbatim in TypeScript-aware runtimes/test loaders (vitest, tsx, Node with
809
+ * type stripping) — which also keeps their module mocking and single module
810
+ * registry intact. When native import can't load a TypeScript source (plain
811
+ * Node throwing on a `.ts`/`.mts`/`.cts` file), it is transpiled on the fly
812
+ * via jiti (`@h3ravel/shared`'s `importFile`) so commands are discovered
813
+ * without a prior build. Consumers can bypass all of this with
814
+ * {@link KernelConfig.importModule}.
815
+ *
816
+ * @param file Absolute or cwd-relative path to the command module.
817
+ */
818
+ async importCommandModule(file) {
819
+ if (this.config.importModule) return await this.config.importModule(file);
820
+ try {
821
+ return await import(file);
822
+ } catch (error) {
823
+ if (/\.(c|m)?ts$/.test(file)) return await importFile(file);
824
+ throw error;
825
+ }
826
+ }
827
+ /**
828
+ * Resolve the command class out of an imported module. Prefers the export
829
+ * named after the file, then a `default` export, then the first exported
830
+ * constructor — because a file's name and its exported class name do not
831
+ * always match.
832
+ *
833
+ * @param mod The imported module namespace.
834
+ * @param name The file name without extension.
835
+ */
836
+ resolveCommandClass(mod, name) {
837
+ const named = mod[name];
838
+ if (typeof named === "function") return named;
839
+ if (typeof mod.default === "function") return mod.default;
840
+ return Object.values(mod).find((value) => typeof value === "function");
841
+ }
842
+ /**
843
+ * Emit a non-fatal command-discovery warning.
844
+ *
845
+ * @param message
846
+ */
847
+ warnDiscovery(message) {
848
+ Logger.log([["[musket]", "yellow"], [message, "white"]], " ");
849
+ }
850
+ /**
796
851
  * Push a new command into the commands stack
797
852
  *
798
853
  * @param command
@@ -818,13 +873,11 @@ var Musket = class Musket {
818
873
  }
819
874
  async initialize() {
820
875
  if (process.argv.includes("--help") || process.argv.includes("-h")) await this.rebuild("help");
821
- /**
822
- * Get the provided packages versions
876
+ /**
877
+ * Render the provided packages versions (single source of truth shared
878
+ * with ListCommand, fully overridable via KernelConfig).
823
879
  */
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(" | ");
880
+ const moduleVersions = this.kernel.getVersionString();
828
881
  const additional = {
829
882
  quiet: ["-q, --quiet", "Do not output any message except errors and warnings"],
830
883
  silent: ["--silent", "Do not output any message"],
@@ -1153,13 +1206,18 @@ var Kernel = class Kernel {
1153
1206
  for (let i = 0; i < this.packages.length; i++) try {
1154
1207
  const item = this.packages[i];
1155
1208
  const name = typeof item === "string" ? item : item.name;
1156
- const alias = typeof item === "string" ? item : item.alias;
1209
+ const alias = typeof item === "string" ? item : item.alias ?? item.name;
1157
1210
  const base = typeof item === "string" ? false : item.base;
1211
+ const label = typeof item === "string" ? void 0 : item.label;
1212
+ const versionOverride = typeof item === "string" ? void 0 : item.version;
1158
1213
  const modulePath = FileSystem.findModulePkg(name, this.cwd) ?? "";
1159
1214
  const pkg = require(path.join(modulePath, "package.json"));
1160
1215
  pkg.alias = alias;
1161
1216
  pkg.base = base;
1162
- if (base === true && version) pkg.version = version;
1217
+ if (label) pkg.label = label;
1218
+ /** A per-package version wins, then the base-package override. */
1219
+ if (versionOverride) pkg.version = versionOverride;
1220
+ else if (base === true && version) pkg.version = version;
1163
1221
  this.modules.push(pkg);
1164
1222
  } catch {
1165
1223
  this.modules.push({
@@ -1176,6 +1234,44 @@ var Kernel = class Kernel {
1176
1234
  }
1177
1235
  return this;
1178
1236
  }
1237
+ /**
1238
+ * Format a module's display label: strip the package scope, turn `-`/`_`
1239
+ * into spaces, normalise "cli" casing and capitalise. A module's explicit
1240
+ * `label` short-circuits all of this.
1241
+ *
1242
+ * @param module
1243
+ */
1244
+ formatModuleLabel(module) {
1245
+ if (module.label) return module.label;
1246
+ return String(module.alias ?? module.name).split("/").pop().replace(/[-_]/g, " ").replace(/cli/gi, (match) => match === "cli" ? "CLI" : match).replace(/^./, (c) => c.toUpperCase());
1247
+ }
1248
+ /**
1249
+ * Render a single module as a colored `Label: version` segment, honoring
1250
+ * {@link KernelConfig.versionColors}.
1251
+ *
1252
+ * @param module
1253
+ */
1254
+ renderModuleVersion(module) {
1255
+ const colors = this.config.versionColors ?? {};
1256
+ return Logger.parse([[`${this.formatModuleLabel(module)}:`, colors.label ?? "white"], [String(module.version), colors.version ?? "green"]], " ", false);
1257
+ }
1258
+ /**
1259
+ * Build the full version line shown for `--version` and atop the command
1260
+ * list. A single source of truth for both render sites.
1261
+ *
1262
+ * Honors {@link KernelConfig.versionFormatter} for complete control,
1263
+ * otherwise joins each module with {@link KernelConfig.versionSeparator}.
1264
+ */
1265
+ getVersionString() {
1266
+ const modules = this.modules;
1267
+ const separator = this.config.versionSeparator ?? " | ";
1268
+ if (this.config.versionFormatter) return this.config.versionFormatter(modules, {
1269
+ format: (module) => this.renderModuleVersion(module),
1270
+ label: (module) => this.formatModuleLabel(module),
1271
+ separator
1272
+ });
1273
+ return modules.map((module) => this.renderModuleVersion(module)).join(separator);
1274
+ }
1179
1275
  };
1180
1276
  //#endregion
1181
1277
  export { Command, Kernel, Musket, Signature };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@h3ravel/musket",
3
- "version": "2.0.0",
3
+ "version": "2.1.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",