@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.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,10 +52,46 @@ 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
+ */
58
+ name: 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
+ */
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 = {
55
80
  name: string;
56
- alias: string;
81
+ version: string;
82
+ alias?: string;
83
+ label?: string;
57
84
  base?: boolean;
58
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;
94
+ };
59
95
  type CommandMethodResolver = <X extends Command>(cmd: X, met: any) => Promise<X>;
60
96
  interface KernelConfig<A extends Application = Application> {
61
97
  /**
@@ -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,191 @@ 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;
328
+ }
329
+ //#endregion
330
+ //#region src/SignatureBuilder.d.ts
331
+ type SignatureValue = string | number | boolean | string[] | undefined;
332
+ /**
333
+ * Definition of a positional argument added via {@link SignatureBuilder.argument}.
334
+ */
335
+ interface ArgumentDefinition {
336
+ /** Human readable description. */
337
+ description?: string;
338
+ /** Whether the argument must be provided. Defaults to `true`. */
339
+ required?: boolean;
340
+ /** A default value. Implies the argument is optional. */
341
+ default?: SignatureValue;
342
+ /** Whether the argument accepts multiple (variadic) values. */
343
+ multiple?: boolean;
344
+ /** Restrict the accepted values to this list. */
345
+ choices?: string[];
346
+ }
347
+ /**
348
+ * Definition of an option/flag added via {@link SignatureBuilder.option}.
349
+ *
350
+ * By default an option is a boolean flag. Give it a `default`, set
351
+ * `requiresValue`, or set `optionalValue` to make it take a value.
352
+ */
353
+ interface OptionDefinition {
354
+ /** Human readable description. */
355
+ description?: string;
356
+ /** A single character short alias, e.g. `Q` for `-Q`. */
357
+ short?: string;
358
+ /** A default value. Implies the option takes an optional value. */
359
+ default?: SignatureValue;
360
+ /** The option requires a value (`--queue <value>`). */
361
+ requiresValue?: boolean;
362
+ /** The option takes an optional value (`--queue [value]`). */
363
+ optionalValue?: boolean;
364
+ /** Restrict the accepted values to this list. */
365
+ choices?: string[];
366
+ /** Share this option with sub-commands of a namespace command. */
367
+ shared?: boolean;
368
+ /** Hide this option from help output. */
369
+ hidden?: boolean;
370
+ }
371
+ /**
372
+ * A fluent builder for constructing a command's signature programmatically,
373
+ * without authoring the string DSL by hand.
374
+ *
375
+ * ```ts
376
+ * buildSignature (sig: SignatureBuilder) {
377
+ * return sig
378
+ * .command('queue:work')
379
+ * .describe('Process jobs on the queue')
380
+ * .argument('connection', { description: 'The connection to work', required: false })
381
+ * .option('queue', { description: 'The queue to process', short: 'Q' })
382
+ * .option('once', { description: 'Process a single job and exit' })
383
+ * }
384
+ * ```
385
+ *
386
+ * Musket consumes the builder directly (lossless) for normal commands, and can
387
+ * also reconstruct the equivalent signature string via {@link toString} for
388
+ * anything that still expects the DSL.
389
+ */
390
+ declare class SignatureBuilder {
391
+ private baseName;
392
+ private commandDescription?;
393
+ private hidden;
394
+ private args;
395
+ private opts;
396
+ /**
397
+ * Set the command name, e.g. `make:model` or `queue:work`. A trailing `:`
398
+ * marks a namespace command.
399
+ */
400
+ command(name: string): this;
401
+ /**
402
+ * Alias for {@link command}.
403
+ *
404
+ * @param name
405
+ * @returns
406
+ */
407
+ name(name: string): this;
408
+ /**
409
+ * Set the command description.
410
+ *
411
+ * @param name
412
+ * @returns
413
+ */
414
+ describe(description: string): this;
415
+ /**
416
+ * Alias for {@link describe}.
417
+ *
418
+ * @param name
419
+ * @returns
420
+ */
421
+ description(description: string): this;
422
+ /**
423
+ * Mark the command as hidden from help output.
424
+ *
425
+ * @param name
426
+ * @returns
427
+ */
428
+ hide(hidden?: boolean): this;
429
+ /**
430
+ * Add a positional argument.
431
+ *
432
+ * @param name
433
+ * @returns
434
+ */
435
+ argument(name: string, definition?: ArgumentDefinition): this;
436
+ /**
437
+ * Add an option/flag.
438
+ *
439
+ * @param name
440
+ * @returns
441
+ */
442
+ option(name: string, definition?: OptionDefinition): this;
443
+ /**
444
+ * The configured command name (empty when unset).
445
+ *
446
+ * @param name
447
+ * @returns
448
+ */
449
+ getName(): string;
450
+ /**
451
+ * The configured description, if any.
452
+ *
453
+ * @param name
454
+ * @returns
455
+ */
456
+ getDescription(): string | undefined;
457
+ /**
458
+ * Whether the command is a namespace command (name ends with `:`).
459
+ *
460
+ * @param name
461
+ * @returns
462
+ */
463
+ isNamespace(): boolean;
464
+ /**
465
+ * Whether anything has been configured on this builder.
466
+ *
467
+ * @param name
468
+ * @returns
469
+ */
470
+ isEmpty(): boolean;
471
+ /**
472
+ * Build the structured {@link ParsedCommand} consumed by the kernel. This is
473
+ * lossless — no string round-trip — and is the path used for normal commands.
474
+ *
475
+ * @param commandClass
476
+ * @returns
477
+ */
478
+ toParsed<A extends Application = Application>(commandClass: Command<A>): ParsedCommand<A>;
479
+ private argumentToOption;
480
+ private optionToOption;
481
+ /**
482
+ * Reconstruct the equivalent signature string in musket's DSL. Used by
483
+ * {@link Command.getSignature} so external consumers (and the namespace path)
484
+ * keep working unchanged.
485
+ */
486
+ toString(): string;
487
+ private argumentToken;
488
+ private optionToken;
489
+ private wrap;
227
490
  }
228
491
  //#endregion
229
492
  //#region src/Core/Command.d.ts
@@ -255,6 +518,11 @@ declare class Command<A extends Application = Application> {
255
518
  * @var string
256
519
  */
257
520
  protected description?: string;
521
+ /**
522
+ * Memoized result of {@link resolveBuilder}. `undefined` means "not yet
523
+ * resolved"; `null` means "resolved, no builder in use".
524
+ */
525
+ private resolvedBuilder?;
258
526
  /**
259
527
  * The console command input.
260
528
  *
@@ -308,11 +576,46 @@ declare class Command<A extends Application = Application> {
308
576
  */
309
577
  setProgram(program: Command$1): this;
310
578
  /**
311
- * Get the command signature
579
+ * Define the command signature programmatically.
580
+ *
581
+ * Override this instead of (or in addition to) the {@link signature} string
582
+ * to describe the command, its arguments and its options through the fluent
583
+ * {@link SignatureBuilder} — no string DSL required. When the `signature`
584
+ * string is set it always takes precedence and this method is ignored.
585
+ *
586
+ * ```ts
587
+ * buildSignature (sig: SignatureBuilder) {
588
+ * return sig
589
+ * .command('queue:work')
590
+ * .describe('Process jobs on the queue')
591
+ * .argument('connection', { required: false })
592
+ * .option('queue', { short: 'Q', description: 'The queue to process' })
593
+ * .option('once', { description: 'Process a single job and exit' })
594
+ * }
595
+ * ```
596
+ *
597
+ * @param _builder A fresh builder to configure.
598
+ */
599
+ protected buildSignature(_builder: SignatureBuilder): SignatureBuilder | void;
600
+ /**
601
+ * Resolve (and memoize) the signature builder for this command, or `null`
602
+ * when the command uses the string {@link signature} (which always wins) or
603
+ * does not implement {@link buildSignature}.
604
+ */
605
+ private resolveBuilder;
606
+ /**
607
+ * Get the command signature. Returns the string {@link signature} when set,
608
+ * otherwise reconstructs it from {@link buildSignature}.
312
609
  *
313
610
  * @returns
314
611
  */
315
612
  getSignature(): string;
613
+ /**
614
+ * The structured, lossless parsed command when {@link buildSignature} is used
615
+ * for a non-namespace command; `undefined` otherwise (callers then fall back
616
+ * to parsing {@link getSignature}). This avoids a lossy string round-trip.
617
+ */
618
+ toParsedSignature(): ParsedCommand<A> | undefined;
316
619
  /**
317
620
  * Get the command description
318
621
  *
@@ -525,6 +828,13 @@ declare class Musket<A extends Application = Application> {
525
828
  name: string;
526
829
  private config;
527
830
  private commands;
831
+ /**
832
+ * Keys (baseCommand, namespace-aware) of commands already registered, so the
833
+ * same command surfacing from more than one source — e.g. discovered both as
834
+ * built `dist/*.js` and as `src/*.ts` via the jiti loader — is only added
835
+ * once. The first registration wins (base commands before discovered ones).
836
+ */
837
+ private registeredKeys;
528
838
  private program;
529
839
  constructor(app: A, kernel: Kernel<A>, baseCommands?: Command<A>[], resolver?: CommandMethodResolver | undefined, tsDownConfig?: UserConfig);
530
840
  build(): Promise<Command$1>;
@@ -549,11 +859,69 @@ declare class Musket<A extends Application = Application> {
549
859
  discoverCommandsFrom(paths: string | string[]): this;
550
860
  private loadDiscoveredCommands;
551
861
  /**
552
- * Push a new command into the commands stack
862
+ * Import a discovered command module with full TypeScript support.
863
+ *
864
+ * Native `import()` is attempted first: it loads built `.js`, and works
865
+ * verbatim in TypeScript-aware runtimes/test loaders (vitest, tsx, Node with
866
+ * type stripping) — which also keeps their module mocking and single module
867
+ * registry intact. When native import can't load a TypeScript source (plain
868
+ * Node throwing on a `.ts`/`.mts`/`.cts` file), it is transpiled on the fly
869
+ * via jiti (`@h3ravel/shared`'s `importFile`) so commands are discovered
870
+ * without a prior build. Consumers can bypass all of this with
871
+ * {@link KernelConfig.importModule}.
872
+ *
873
+ * @param file Absolute or cwd-relative path to the command module.
874
+ */
875
+ private importCommandModule;
876
+ /**
877
+ * Resolve the command class out of an imported module. Prefers the export
878
+ * named after the file, then a `default` export, then the first exported
879
+ * constructor — because a file's name and its exported class name do not
880
+ * always match.
881
+ *
882
+ * @param mod The imported module namespace.
883
+ * @param name The file name without extension.
884
+ */
885
+ private resolveCommandClass;
886
+ /**
887
+ * Whether a value is a Command class (constructor), as opposed to any other
888
+ * exported function/class.
889
+ *
890
+ * The check is structural — it looks for `getSignature` on the prototype
891
+ * rather than using `instanceof Command` — so it stays correct when the
892
+ * discovered command extends a Command from a different copy/version of
893
+ * musket. Crucially, it rejects non-command exports such as the shared
894
+ * bundler chunks tools like tsdown can emit alongside built commands (e.g. a
895
+ * `Rebuilder` helper), which would otherwise be `new`-ed and then crash when
896
+ * `getSignature()` is called on them.
897
+ *
898
+ * @param value The candidate export.
899
+ */
900
+ private isCommandClass;
901
+ /**
902
+ * Emit a non-fatal command-discovery warning.
903
+ *
904
+ * @param message
905
+ */
906
+ private warnDiscovery;
907
+ /**
908
+ * Push a new command into the commands stack.
909
+ *
910
+ * Resolution prefers a command's structured signature (built via
911
+ * {@link Command.buildSignature}) and falls back to parsing its signature
912
+ * string. Commands that expose no usable signature are skipped with a warning
913
+ * rather than crashing the CLI, and a command whose key was already
914
+ * registered is ignored (de-duplication — see {@link registeredKeys}).
553
915
  *
554
916
  * @param command
555
917
  */
556
918
  addCommand(command: Command<A>): this;
919
+ /**
920
+ * A best-effort human label for a command, for diagnostics.
921
+ *
922
+ * @param command
923
+ */
924
+ private describeCommand;
557
925
  /**
558
926
  * Push a list of new commands to commands stack
559
927
  *
@@ -604,4 +972,4 @@ declare class Signature {
604
972
  static parseSignature<A extends Application = Application>(signature: string, commandClass: Command<A>): ParsedCommand<A>;
605
973
  }
606
974
  //#endregion
607
- export { Application, Command, CommandMethodResolver, CommandOption, Kernel, KernelConfig, Musket, PackageMeta, ParsedCommand, Signature, TGeneric, XGeneric };
975
+ export { Application, ArgumentDefinition, Command, CommandMethodResolver, CommandOption, Kernel, KernelConfig, ModuleMeta, Musket, OptionDefinition, PackageMeta, ParsedCommand, Signature, SignatureBuilder, SignatureValue, TGeneric, VersionRenderHelpers, XGeneric };