@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 +113 -17
- package/dist/index.d.ts +141 -9
- package/dist/index.js +114 -18
- package/package.json +1 -1
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.
|
|
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
|
|
808
|
+
const file = pth.toString();
|
|
809
|
+
const name = node_path.default.basename(file).replace(/\.(c|m)?(t|j)s$/, "");
|
|
812
810
|
try {
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
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
|
-
*
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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
|
|
784
|
+
const file = pth.toString();
|
|
785
|
+
const name = path.basename(file).replace(/\.(c|m)?(t|j)s$/, "");
|
|
788
786
|
try {
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
*
|
|
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.
|
|
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 (
|
|
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.
|
|
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",
|