@crustjs/core 0.0.9 → 0.0.10
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/README.md +9 -10
- package/dist/index.d.ts +488 -200
- package/dist/index.js +1 -1
- package/dist/shared/chunk-hzhxdeqq.js +2 -0
- package/dist/shared/chunk-stffrp66.js +3 -0
- package/package.json +1 -1
- package/dist/shared/chunk-36287c08.js +0 -3
- package/dist/shared/chunk-n6bfy2wh.js +0 -2
package/README.md
CHANGED
|
@@ -13,21 +13,20 @@ bun add @crustjs/core
|
|
|
13
13
|
## Quick Example
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
|
-
import {
|
|
16
|
+
import { Crust } from "@crustjs/core";
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
meta
|
|
20
|
-
args
|
|
21
|
-
flags
|
|
18
|
+
const app = new Crust("greet")
|
|
19
|
+
.meta({ description: "Say hello" })
|
|
20
|
+
.args([{ name: "name", type: "string", default: "world" }] as const)
|
|
21
|
+
.flags({
|
|
22
22
|
loud: { type: "boolean", description: "Shout it", alias: "l" },
|
|
23
|
-
}
|
|
24
|
-
run({ args, flags }) {
|
|
23
|
+
})
|
|
24
|
+
.run(({ args, flags }) => {
|
|
25
25
|
const msg = `Hello, ${args.name}!`;
|
|
26
26
|
console.log(flags.loud ? msg.toUpperCase() : msg);
|
|
27
|
-
}
|
|
28
|
-
});
|
|
27
|
+
});
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
app.execute();
|
|
31
30
|
```
|
|
32
31
|
|
|
33
32
|
## Documentation
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The result of resolving a command from an argv array.
|
|
3
|
+
*
|
|
4
|
+
* Contains the resolved (sub)command and argv after subcommand
|
|
5
|
+
* resolution, and the full command path for help text rendering.
|
|
6
|
+
*/
|
|
7
|
+
interface CommandRoute {
|
|
8
|
+
/** The routed command (may be a subcommand of the original) */
|
|
9
|
+
command: CommandNode;
|
|
10
|
+
/** The argv after subcommand names have been consumed */
|
|
11
|
+
argv: string[];
|
|
12
|
+
/** The command path for help text (e.g. ["crust", "generate", "command"]) */
|
|
13
|
+
commandPath: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a command from an argv array by walking the subcommand tree.
|
|
17
|
+
*
|
|
18
|
+
* Subcommand matching happens BEFORE flag parsing, so:
|
|
19
|
+
* `crust build --entry src/cli.ts` first resolves "build" as a subcommand,
|
|
20
|
+
* then passes `["--entry", "src/cli.ts"]` to the build command's parser.
|
|
21
|
+
*
|
|
22
|
+
* Resolution rules:
|
|
23
|
+
* 1. If `argv[0]` matches a subcommand key, recurse into that subcommand
|
|
24
|
+
* 2. If no match and the current command has `run()`, return it (args passed to parser)
|
|
25
|
+
* 3. If no match and the current command has NO `run()`, it signals the caller
|
|
26
|
+
* should show help (the `showHelp` flag is set in the result)
|
|
27
|
+
* 4. Unknown subcommands produce a structured COMMAND_NOT_FOUND error
|
|
28
|
+
*
|
|
29
|
+
* @param command - The root command to resolve from
|
|
30
|
+
* @param argv - The argv array to resolve against
|
|
31
|
+
* @returns The resolved command, argv, and the command path
|
|
32
|
+
* @throws {CrustError} COMMAND_NOT_FOUND when an unknown subcommand is given and the parent has no run()
|
|
33
|
+
*/
|
|
34
|
+
declare function resolveCommand(command: CommandNode, argv: string[]): CommandRoute;
|
|
1
35
|
/** Supported type literals for args and flags */
|
|
2
36
|
type ValueType = "string" | "number" | "boolean";
|
|
3
37
|
/**
|
|
@@ -59,10 +93,14 @@ type ArgsDef = readonly ArgDef[];
|
|
|
59
93
|
interface FlagDefBase {
|
|
60
94
|
/** Human-readable description for help text */
|
|
61
95
|
description?: string;
|
|
62
|
-
/**
|
|
63
|
-
|
|
96
|
+
/** Single-character short alias (e.g. `"v"` → `-v`) */
|
|
97
|
+
short?: string;
|
|
98
|
+
/** Additional long aliases (e.g. `["out"]` → `--out`) */
|
|
99
|
+
aliases?: string[];
|
|
64
100
|
/** When `true`, the parser throws if the flag is not provided */
|
|
65
101
|
required?: true;
|
|
102
|
+
/** When `true`, the flag is inherited by subcommands */
|
|
103
|
+
inherit?: true;
|
|
66
104
|
}
|
|
67
105
|
/** Base for single-value flags — `multiple` must be omitted */
|
|
68
106
|
interface SingleFlagBase extends FlagDefBase {
|
|
@@ -119,7 +157,7 @@ interface BooleanMultiFlagDef extends MultiFlagBase {
|
|
|
119
157
|
* @example
|
|
120
158
|
* ```ts
|
|
121
159
|
* const flags = {
|
|
122
|
-
* verbose: { type: "boolean", description: "Enable verbose logging",
|
|
160
|
+
* verbose: { type: "boolean", description: "Enable verbose logging", short: "v" },
|
|
123
161
|
* port: { type: "number", description: "Port number", default: 3000 },
|
|
124
162
|
* files: { type: "string", multiple: true, default: ["index.ts"] },
|
|
125
163
|
* } satisfies FlagsDef;
|
|
@@ -129,18 +167,32 @@ type FlagDef = StringFlagDef | NumberFlagDef | BooleanFlagDef | StringMultiFlagD
|
|
|
129
167
|
/** Record mapping flag names to their definitions */
|
|
130
168
|
type FlagsDef = Record<string, FlagDef>;
|
|
131
169
|
/**
|
|
132
|
-
* Extract alias
|
|
170
|
+
* Extract the `short` alias literal from a flag definition.
|
|
171
|
+
* Resolves to `never` when no `short` field exists or when the type
|
|
172
|
+
* is the broad `string` (not a narrowed literal).
|
|
173
|
+
*/
|
|
174
|
+
type ExtractShort<F> = F extends {
|
|
175
|
+
short: infer S;
|
|
176
|
+
} ? S extends string ? string extends S ? never : S : never : never;
|
|
177
|
+
/**
|
|
178
|
+
* Extract alias string literals from the `aliases` array of a flag definition.
|
|
179
|
+
* Resolves to `never` when no `aliases` field exists or when the element type
|
|
180
|
+
* is the broad `string` (not narrowed literals).
|
|
181
|
+
*/
|
|
182
|
+
type ExtractLongAliases<F> = F extends {
|
|
183
|
+
aliases: infer A;
|
|
184
|
+
} ? A extends readonly string[] ? string extends A[number] ? never : A[number] : never : never;
|
|
185
|
+
/**
|
|
186
|
+
* Extract all alias identifiers (short + long) from a flag definition.
|
|
133
187
|
*
|
|
134
188
|
* Generalized to work with any shape (`FlagDef`, `FlagSpec`, etc.) —
|
|
135
|
-
* values without
|
|
189
|
+
* values without `short`/`aliases` fields resolve to `never`.
|
|
136
190
|
*
|
|
137
|
-
* Includes
|
|
191
|
+
* Includes `string extends ...` guards so non-narrowed types (e.g. the
|
|
138
192
|
* broad `string` type from a default generic) resolve to `never` instead
|
|
139
193
|
* of causing false-positive collisions.
|
|
140
194
|
*/
|
|
141
|
-
type
|
|
142
|
-
alias: infer A;
|
|
143
|
-
} ? A extends string ? string extends A ? never : A : A extends readonly string[] ? string extends A[number] ? never : A[number] : never : never;
|
|
195
|
+
type ExtractAllAliases<F> = ExtractShort<F> | ExtractLongAliases<F>;
|
|
144
196
|
/**
|
|
145
197
|
* Collects aliases from every flag *except* flag K.
|
|
146
198
|
* Used to detect alias→alias duplicates across different flags.
|
|
@@ -148,7 +200,7 @@ type ExtractAliases<F> = F extends {
|
|
|
148
200
|
type AliasesExcluding<
|
|
149
201
|
F extends Record<string, unknown>,
|
|
150
202
|
K extends keyof F & string
|
|
151
|
-
> = { [J in Exclude<keyof F & string, K>] :
|
|
203
|
+
> = { [J in Exclude<keyof F & string, K>] : ExtractAllAliases<F[J]> }[Exclude<keyof F & string, K>];
|
|
152
204
|
/**
|
|
153
205
|
* Per-flag collision detection: resolves to the alias literal(s) of flag K
|
|
154
206
|
* that collide with another flag's name or another flag's alias,
|
|
@@ -157,7 +209,7 @@ type AliasesExcluding<
|
|
|
157
209
|
type CollidingAliases<
|
|
158
210
|
F extends Record<string, unknown>,
|
|
159
211
|
K extends keyof F & string
|
|
160
|
-
> = (
|
|
212
|
+
> = (ExtractAllAliases<F[K]> & Exclude<keyof F & string, K>) | (ExtractAllAliases<F[K]> & AliasesExcluding<F, K>);
|
|
161
213
|
/**
|
|
162
214
|
* Per-flag validation mapped type. Resolves to `F` when no collisions exist.
|
|
163
215
|
* For flags with colliding aliases, adds a branded error property to the
|
|
@@ -167,30 +219,74 @@ type CollidingAliases<
|
|
|
167
219
|
* it with `FlagsDef`, the validate package uses it with `FlagShape`, etc.
|
|
168
220
|
*
|
|
169
221
|
* ```
|
|
170
|
-
* Property 'FIX_ALIAS_COLLISION' is missing in type '{ type: "string";
|
|
222
|
+
* Property 'FIX_ALIAS_COLLISION' is missing in type '{ type: "string"; short: "m" }'
|
|
171
223
|
* but required in type
|
|
172
|
-
* '{ readonly FIX_ALIAS_COLLISION: "Alias \"
|
|
224
|
+
* '{ readonly FIX_ALIAS_COLLISION: "Alias \"m\" collides with another flag name or alias" }'.
|
|
173
225
|
* ```
|
|
174
226
|
*/
|
|
175
227
|
type ValidateFlagAliases<F extends Record<string, unknown>> = { [K in keyof F & string] : CollidingAliases<F, K> extends never ? F[K] : F[K] & {
|
|
176
228
|
readonly FIX_ALIAS_COLLISION: `Alias "${CollidingAliases<F, K>}" collides with another flag name or alias`;
|
|
177
229
|
} };
|
|
178
230
|
/**
|
|
231
|
+
* Collects aliases from inherited flags, excluding those whose keys the
|
|
232
|
+
* child overrides (intentional override — child redefines a flag by name).
|
|
233
|
+
*/
|
|
234
|
+
type InheritedAliasesExcluding<
|
|
235
|
+
I extends Record<string, unknown>,
|
|
236
|
+
OverrideKeys extends string
|
|
237
|
+
> = { [K in Exclude<keyof I & string, OverrideKeys>] : ExtractAllAliases<I[K]> }[Exclude<keyof I & string, OverrideKeys>];
|
|
238
|
+
/**
|
|
239
|
+
* Per-flag cross-collision detection between a child flag K (from local
|
|
240
|
+
* flags F) and the inherited flag set I. Resolves to the colliding
|
|
241
|
+
* identifier, or `never` when no collision exists.
|
|
242
|
+
*
|
|
243
|
+
* Detects three collision classes:
|
|
244
|
+
* 1. Child alias → inherited flag name
|
|
245
|
+
* 2. Child alias → inherited flag alias
|
|
246
|
+
* 3. Child flag name → inherited flag alias
|
|
247
|
+
*
|
|
248
|
+
* Intentional name overrides (child defines a flag with the same key as
|
|
249
|
+
* an inherited flag) are excluded — those are handled by `MergeFlags`.
|
|
250
|
+
*/
|
|
251
|
+
type CrossCollision<
|
|
252
|
+
I extends Record<string, unknown>,
|
|
253
|
+
F extends Record<string, unknown>,
|
|
254
|
+
K extends keyof F & string
|
|
255
|
+
> = (ExtractAllAliases<F[K]> & Exclude<keyof I & string, keyof F & string>) | (ExtractAllAliases<F[K]> & InheritedAliasesExcluding<I, keyof F & string>) | (K & InheritedAliasesExcluding<I, keyof F & string>);
|
|
256
|
+
/**
|
|
257
|
+
* Per-flag validation mapped type for cross-collisions between inherited
|
|
258
|
+
* and local flags. Resolves to `F` when no collisions exist.
|
|
259
|
+
*
|
|
260
|
+
* When `Inherited` is the wide `FlagsDef` type (root commands with no
|
|
261
|
+
* parent), the validation is skipped to avoid false positives since
|
|
262
|
+
* `keyof FlagsDef` is `string`.
|
|
263
|
+
*
|
|
264
|
+
* ```
|
|
265
|
+
* Property 'FIX_INHERITED_COLLISION' is missing in type '{ type: "string"; aliases: ["verbose"] }'
|
|
266
|
+
* but required in type
|
|
267
|
+
* '{ readonly FIX_INHERITED_COLLISION: "\"verbose\" collides with inherited flag" }'.
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
type ValidateCrossCollisions<
|
|
271
|
+
I extends Record<string, unknown>,
|
|
272
|
+
F extends Record<string, unknown>
|
|
273
|
+
> = string extends keyof I ? F : { [K in keyof F & string] : CrossCollision<I, F, K> extends never ? F[K] : F[K] & {
|
|
274
|
+
readonly FIX_INHERITED_COLLISION: `"${CrossCollision<I, F, K> & string}" collides with inherited flag`;
|
|
275
|
+
} };
|
|
276
|
+
/**
|
|
179
277
|
* Detects whether a single alias literal starts with `"no-"`.
|
|
180
278
|
* Resolves to the offending alias, or `never` when it is clean.
|
|
181
279
|
*/
|
|
182
280
|
type NoPrefixedAlias<A> = A extends `no-${string}` ? A : never;
|
|
183
281
|
/**
|
|
184
282
|
* Collects all `"no-"`-prefixed alias literals from a flag definition.
|
|
185
|
-
*
|
|
283
|
+
* Checks both `short` and `aliases` fields.
|
|
186
284
|
* Non-narrowed `string` types resolve to `never` to avoid false positives.
|
|
187
285
|
*/
|
|
188
|
-
type NoPrefixedAliases<F> = F
|
|
189
|
-
alias: infer A;
|
|
190
|
-
} ? A extends string ? string extends A ? never : NoPrefixedAlias<A> : A extends readonly string[] ? string extends A[number] ? never : NoPrefixedAlias<A[number]> : never : never;
|
|
286
|
+
type NoPrefixedAliases<F> = NoPrefixedAlias<ExtractShort<F>> | NoPrefixedAlias<ExtractLongAliases<F>>;
|
|
191
287
|
/**
|
|
192
288
|
* Per-flag validation mapped type. Resolves to `F` when no `"no-"` prefixes
|
|
193
|
-
* exist on flag names or aliases. For flags with offending
|
|
289
|
+
* exist on flag names, short aliases, or long aliases. For flags with offending values,
|
|
194
290
|
* adds a branded error property causing a compile-time type error.
|
|
195
291
|
*
|
|
196
292
|
* The `"no-"` prefix is reserved for boolean flag negation (`--no-flag`).
|
|
@@ -229,6 +325,60 @@ type ValidateVariadicArgs<A extends readonly object[]> = A extends readonly [inf
|
|
|
229
325
|
readonly FIX_VARIADIC_POSITION: "Only the last positional argument can be variadic";
|
|
230
326
|
}, ...ValidateVariadicArgs<Tail>] : readonly [Head, ...ValidateVariadicArgs<Tail>] : readonly [Head] : A;
|
|
231
327
|
/**
|
|
328
|
+
* Picks only the flags from `F` that have `inherit: true`.
|
|
329
|
+
*
|
|
330
|
+
* Flags without `inherit` (or with `inherit` omitted) are excluded.
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```ts
|
|
334
|
+
* type Flags = {
|
|
335
|
+
* verbose: { type: "boolean"; inherit: true };
|
|
336
|
+
* port: { type: "number" };
|
|
337
|
+
* };
|
|
338
|
+
* type Result = InheritableFlags<Flags>;
|
|
339
|
+
* // Result = { verbose: { type: "boolean"; inherit: true } }
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
type InheritableFlags<F extends FlagsDef> = { [K in keyof F as F[K] extends {
|
|
343
|
+
inherit: true;
|
|
344
|
+
} ? K : never] : F[K] };
|
|
345
|
+
/**
|
|
346
|
+
* Merges parent flags with local flags, where local keys override parent keys.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```ts
|
|
350
|
+
* type Parent = { verbose: { type: "boolean" }; port: { type: "number" } };
|
|
351
|
+
* type Local = { port: { type: "string" } };
|
|
352
|
+
* type Result = MergeFlags<Parent, Local>;
|
|
353
|
+
* // Result = { verbose: { type: "boolean" }; port: { type: "string" } }
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
type MergeFlags<
|
|
357
|
+
Parent extends FlagsDef,
|
|
358
|
+
Local extends FlagsDef
|
|
359
|
+
> = Simplify<Omit<Parent, keyof Local> & Local>;
|
|
360
|
+
/**
|
|
361
|
+
* Computes the effective flags for a command by filtering the inherited flags
|
|
362
|
+
* (only those with `inherit: true`) and merging them with local flags.
|
|
363
|
+
*
|
|
364
|
+
* Local flags override inherited flags with the same key.
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* ```ts
|
|
368
|
+
* type Inherited = {
|
|
369
|
+
* verbose: { type: "boolean"; inherit: true };
|
|
370
|
+
* port: { type: "number" };
|
|
371
|
+
* };
|
|
372
|
+
* type Local = { output: { type: "string" } };
|
|
373
|
+
* type Result = EffectiveFlags<Inherited, Local>;
|
|
374
|
+
* // Result = { verbose: { type: "boolean"; inherit: true }; output: { type: "string" } }
|
|
375
|
+
* ```
|
|
376
|
+
*/
|
|
377
|
+
type EffectiveFlags<
|
|
378
|
+
Inherited extends FlagsDef,
|
|
379
|
+
Local extends FlagsDef
|
|
380
|
+
> = MergeFlags<InheritableFlags<Inherited>, Local>;
|
|
381
|
+
/**
|
|
232
382
|
* Infer the resolved type for a single ArgDef:
|
|
233
383
|
*
|
|
234
384
|
* - **variadic** → `primitive[]`
|
|
@@ -322,109 +472,351 @@ interface ParseResult<
|
|
|
322
472
|
/** Raw arguments that appeared after the `--` separator */
|
|
323
473
|
rawArgs: string[];
|
|
324
474
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
command
|
|
475
|
+
interface PluginState {
|
|
476
|
+
get<T = unknown>(key: string): T | undefined;
|
|
477
|
+
has(key: string): boolean;
|
|
478
|
+
set(key: string, value: unknown): void;
|
|
479
|
+
delete(key: string): boolean;
|
|
480
|
+
}
|
|
481
|
+
/** A command target accepted by SetupActions */
|
|
482
|
+
type CommandTarget = CommandNode;
|
|
483
|
+
interface SetupActions {
|
|
484
|
+
/**
|
|
485
|
+
* Inject a flag definition into a command's flags object.
|
|
486
|
+
*
|
|
487
|
+
* Use this in plugin `setup()` hooks to register plugin-specific flags
|
|
488
|
+
* (e.g. `--version`, `--help`) so they are recognized by the parser
|
|
489
|
+
* and rendered in help text.
|
|
490
|
+
*
|
|
491
|
+
* The flag is added to `effectiveFlags` only — `localFlags` is left
|
|
492
|
+
* unchanged to distinguish user-defined flags from plugin-injected ones.
|
|
493
|
+
*
|
|
494
|
+
* @param command - The command node to add the flag to
|
|
495
|
+
* @param name - The flag name (e.g. "version")
|
|
496
|
+
* @param def - The flag definition
|
|
497
|
+
*/
|
|
498
|
+
addFlag(command: CommandTarget, name: string, def: FlagDef): void;
|
|
499
|
+
/**
|
|
500
|
+
* Inject a subcommand into a command's `subCommands` record.
|
|
501
|
+
*
|
|
502
|
+
* Use this in plugin `setup()` hooks to register plugin-provided commands
|
|
503
|
+
* (e.g. a "skill" management command). If the parent already has a
|
|
504
|
+
* subcommand with the same name (user-defined), the call is silently
|
|
505
|
+
* skipped — user definitions always take priority over plugin injections.
|
|
506
|
+
*
|
|
507
|
+
* @param parent - The parent command node to add the subcommand to
|
|
508
|
+
* @param name - The subcommand name (used for routing)
|
|
509
|
+
* @param command - The subcommand node to register
|
|
510
|
+
*/
|
|
511
|
+
addSubCommand(parent: CommandTarget, name: string, command: CommandTarget): void;
|
|
512
|
+
}
|
|
513
|
+
/** Shared context fields available in both setup and middleware phases. */
|
|
514
|
+
interface BaseContext {
|
|
515
|
+
readonly argv: readonly string[];
|
|
516
|
+
readonly rootCommand: CommandNode;
|
|
517
|
+
readonly state: PluginState;
|
|
518
|
+
}
|
|
519
|
+
/** Context passed to plugin `setup()` hooks. */
|
|
520
|
+
interface SetupContext extends BaseContext {}
|
|
521
|
+
/** Context passed to plugin `middleware()` hooks. */
|
|
522
|
+
interface MiddlewareContext extends BaseContext {
|
|
523
|
+
route: Readonly<CommandRoute> | null;
|
|
524
|
+
input: ParseResult | null;
|
|
525
|
+
}
|
|
526
|
+
type Next = () => Promise<void>;
|
|
527
|
+
type PluginMiddleware = (context: MiddlewareContext, next: Next) => void | Promise<void>;
|
|
528
|
+
interface CrustPlugin {
|
|
529
|
+
name?: string;
|
|
530
|
+
setup?: (context: SetupContext, actions: SetupActions) => void | Promise<void>;
|
|
531
|
+
middleware?: PluginMiddleware;
|
|
336
532
|
}
|
|
337
533
|
/**
|
|
338
|
-
*
|
|
534
|
+
* Internal representation of a single node in the command tree.
|
|
339
535
|
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
* Compile-time validation for variadic args and flag alias collisions is
|
|
346
|
-
* enforced via parameter-level intersection in `defineCommand()`.
|
|
536
|
+
* Built by the `Crust` builder class; not part of the public API.
|
|
537
|
+
* Each node carries its own local flags, the pre-computed effective
|
|
538
|
+
* (inherited + local merged) flags, positional args, subcommands,
|
|
539
|
+
* plugins, and lifecycle handlers.
|
|
347
540
|
*/
|
|
348
|
-
interface
|
|
349
|
-
A extends ArgsDef = ArgsDef,
|
|
350
|
-
F extends FlagsDef = FlagsDef
|
|
351
|
-
> {
|
|
541
|
+
interface CommandNode {
|
|
352
542
|
/** Command metadata (name, description, usage) */
|
|
353
543
|
meta: CommandMeta;
|
|
544
|
+
/** Flags defined directly on this command via `.flags()` */
|
|
545
|
+
localFlags: FlagsDef;
|
|
546
|
+
/** Inherited flags merged with local flags (used by the parser) */
|
|
547
|
+
effectiveFlags: FlagsDef;
|
|
354
548
|
/** Positional argument definitions */
|
|
355
|
-
args
|
|
356
|
-
/**
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
|
|
549
|
+
args: ArgsDef | undefined;
|
|
550
|
+
/** Named subcommands keyed by name */
|
|
551
|
+
subCommands: Record<string, CommandNode>;
|
|
552
|
+
/** Plugins registered via `.use()` */
|
|
553
|
+
plugins: CrustPlugin[];
|
|
360
554
|
/** Called before `run()` — useful for initialization */
|
|
361
|
-
preRun
|
|
555
|
+
preRun?: (ctx: unknown) => void | Promise<void>;
|
|
362
556
|
/** The main command handler */
|
|
363
|
-
run
|
|
557
|
+
run?: (ctx: unknown) => void | Promise<void>;
|
|
364
558
|
/** Called after `run()` (even if it throws) — useful for teardown */
|
|
365
|
-
postRun
|
|
559
|
+
postRun?: (ctx: unknown) => void | Promise<void>;
|
|
366
560
|
}
|
|
367
|
-
/**
|
|
368
|
-
|
|
561
|
+
/**
|
|
562
|
+
* Creates a new `CommandNode` with all fields initialized to defaults.
|
|
563
|
+
*
|
|
564
|
+
* @param name - The command name.
|
|
565
|
+
* @returns A fresh `CommandNode` with empty flags, no args, no subcommands,
|
|
566
|
+
* no plugins, and no lifecycle handlers.
|
|
567
|
+
*/
|
|
568
|
+
declare function createCommandNode(name: string): CommandNode;
|
|
569
|
+
/**
|
|
570
|
+
* Merges inherited flags (only those with `inherit: true`) with local flags.
|
|
571
|
+
*
|
|
572
|
+
* Local flags override inherited flags with the same key. Non-inheritable
|
|
573
|
+
* flags from the parent are excluded entirely.
|
|
574
|
+
*
|
|
575
|
+
* This is the runtime counterpart of the `EffectiveFlags` utility type.
|
|
576
|
+
*
|
|
577
|
+
* @param inherited - The parent's effective flags (or any FlagsDef).
|
|
578
|
+
* @param local - The current command's locally-defined flags.
|
|
579
|
+
* @returns A new `FlagsDef` containing inherited+local merged flags.
|
|
580
|
+
*/
|
|
581
|
+
declare function computeEffectiveFlags(inherited: FlagsDef, local: FlagsDef): FlagsDef;
|
|
582
|
+
/**
|
|
583
|
+
* The runtime context object passed to `preRun()`, `run()`, and `postRun()`
|
|
584
|
+
* lifecycle hooks on the `Crust` builder.
|
|
585
|
+
*
|
|
586
|
+
* Generic parameters:
|
|
587
|
+
* - `A` — positional argument definitions tuple
|
|
588
|
+
* - `F` — the effective (inherited + local merged) flag definitions
|
|
589
|
+
*/
|
|
590
|
+
interface CrustCommandContext<
|
|
369
591
|
A extends ArgsDef = ArgsDef,
|
|
370
592
|
F extends FlagsDef = FlagsDef
|
|
371
593
|
> {
|
|
372
|
-
/**
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
|
|
380
|
-
/** Called before `run()` — useful for initialization */
|
|
381
|
-
preRun?(context: CommandContext<A, F>): void | Promise<void>;
|
|
382
|
-
/** The main command handler */
|
|
383
|
-
run?(context: CommandContext<A, F>): void | Promise<void>;
|
|
384
|
-
/** Called after `run()` (even if it throws) — useful for teardown */
|
|
385
|
-
postRun?(context: CommandContext<A, F>): void | Promise<void>;
|
|
594
|
+
/** Resolved positional arguments, keyed by arg name */
|
|
595
|
+
args: InferArgs<A>;
|
|
596
|
+
/** Resolved flags, keyed by flag name */
|
|
597
|
+
flags: InferFlags<F>;
|
|
598
|
+
/** Raw arguments that appeared after the `--` separator */
|
|
599
|
+
rawArgs: string[];
|
|
600
|
+
/** The resolved command node that is being executed */
|
|
601
|
+
command: CommandNode;
|
|
386
602
|
}
|
|
387
|
-
type AnyCommand = Command<any, any>;
|
|
388
603
|
/**
|
|
389
|
-
*
|
|
604
|
+
* Build-time validation protocol.
|
|
390
605
|
*
|
|
391
|
-
*
|
|
392
|
-
* `
|
|
393
|
-
*
|
|
606
|
+
* `crust build` spawns the user's entrypoint as a subprocess with
|
|
607
|
+
* `CRUST_INTERNAL_VALIDATE_ONLY=1`. When `.execute()` detects this env flag
|
|
608
|
+
* it runs validation, surfaces errors via stderr/exitCode, then force-exits.
|
|
609
|
+
*/
|
|
610
|
+
declare const VALIDATION_MODE_ENV = "CRUST_INTERNAL_VALIDATE_ONLY";
|
|
611
|
+
/**
|
|
612
|
+
* Chainable builder for defining CLI commands with full type inference.
|
|
394
613
|
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
614
|
+
* Generic parameters:
|
|
615
|
+
* - `Inherited` — flags inherited from a parent command (populated by `.command()`)
|
|
616
|
+
* - `Local` — flags defined on this command via `.flags()`
|
|
617
|
+
* - `A` — positional argument definitions
|
|
398
618
|
*
|
|
399
619
|
* @example
|
|
400
620
|
* ```ts
|
|
401
|
-
* const
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
* ]
|
|
406
|
-
* flags
|
|
407
|
-
*
|
|
408
|
-
* }
|
|
409
|
-
* run({ args, flags }) {
|
|
410
|
-
* // args.port is typed as number, flags.verbose is typed as boolean | undefined
|
|
411
|
-
* console.log(`Starting server on port ${args.port}`);
|
|
412
|
-
* },
|
|
413
|
-
* });
|
|
621
|
+
* const app = new Crust("my-cli")
|
|
622
|
+
* .flags({
|
|
623
|
+
* verbose: { type: "boolean", short: "v", inherit: true },
|
|
624
|
+
* })
|
|
625
|
+
* .args([{ name: "file", type: "string", required: true }])
|
|
626
|
+
* .run(({ args, flags }) => {
|
|
627
|
+
* console.log(args.file, flags.verbose);
|
|
628
|
+
* });
|
|
414
629
|
* ```
|
|
415
630
|
*/
|
|
416
|
-
declare
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
631
|
+
declare class Crust<
|
|
632
|
+
Inherited extends FlagsDef = FlagsDef,
|
|
633
|
+
Local extends FlagsDef = FlagsDef,
|
|
634
|
+
A extends ArgsDef = ArgsDef
|
|
635
|
+
> {
|
|
636
|
+
/** @internal — Phantom property exposing generic parameters for type-level testing */
|
|
637
|
+
readonly _types: {
|
|
638
|
+
inherited: Inherited;
|
|
639
|
+
local: Local;
|
|
640
|
+
args: A;
|
|
641
|
+
};
|
|
642
|
+
/** @internal */
|
|
643
|
+
readonly _node: CommandNode;
|
|
644
|
+
/** @internal — The inherited flags record (runtime counterpart of Inherited generic) */
|
|
645
|
+
readonly _inheritedFlags: FlagsDef;
|
|
646
|
+
/**
|
|
647
|
+
* Create a new root or standalone command builder.
|
|
648
|
+
*
|
|
649
|
+
* @param name - The command name.
|
|
650
|
+
* @throws {CrustError} `DEFINITION` if name is empty or whitespace-only
|
|
651
|
+
*/
|
|
652
|
+
constructor(name: string);
|
|
653
|
+
/**
|
|
654
|
+
* @internal — Create a child builder with pre-populated inherited flags.
|
|
655
|
+
* Used by `.command()` to propagate parent flags to the child.
|
|
656
|
+
*/
|
|
657
|
+
static _createChild<I extends FlagsDef>(name: string, inheritedFlags: FlagsDef): Crust<I, {}, []>;
|
|
658
|
+
/**
|
|
659
|
+
* @internal — Clone this builder with a new node, preserving generics.
|
|
660
|
+
*/
|
|
661
|
+
private _clone;
|
|
662
|
+
/**
|
|
663
|
+
* Set metadata (description, usage) for this command.
|
|
664
|
+
*
|
|
665
|
+
* The command name is already set via the constructor or `.command()`,
|
|
666
|
+
* so only `description` and `usage` can be provided here.
|
|
667
|
+
*
|
|
668
|
+
* Returns a new builder with updated metadata. The original builder
|
|
669
|
+
* is not mutated.
|
|
670
|
+
*
|
|
671
|
+
* @param meta - Metadata fields to set (description, usage)
|
|
672
|
+
* @returns A new `Crust` instance with updated metadata
|
|
673
|
+
*/
|
|
674
|
+
meta(meta: Omit<CommandMeta, "name">): Crust<Inherited, Local, A>;
|
|
675
|
+
/**
|
|
676
|
+
* Define local flags for this command.
|
|
677
|
+
*
|
|
678
|
+
* Returns a new builder with updated local flag types. The original
|
|
679
|
+
* builder is not mutated.
|
|
680
|
+
*
|
|
681
|
+
* @param defs - Flag definitions record
|
|
682
|
+
* @returns A new `Crust` instance with the given flags
|
|
683
|
+
* @throws {CrustError} `DEFINITION` if flag names/aliases violate constraints
|
|
684
|
+
*/
|
|
685
|
+
flags<const F extends FlagsDef>(defs: F & ValidateNoPrefixedFlags<ValidateFlagAliases<F>> & ValidateCrossCollisions<Inherited, F>): Crust<Inherited, F, A>;
|
|
686
|
+
/**
|
|
687
|
+
* Define positional arguments for this command.
|
|
688
|
+
*
|
|
689
|
+
* Returns a new builder with updated args types. The original
|
|
690
|
+
* builder is not mutated.
|
|
691
|
+
*
|
|
692
|
+
* @param defs - Ordered tuple of positional argument definitions
|
|
693
|
+
* @returns A new `Crust` instance with the given args
|
|
694
|
+
*/
|
|
695
|
+
args<const NewA extends ArgsDef>(defs: NewA & ValidateVariadicArgs<NewA>): Crust<Inherited, Local, NewA>;
|
|
696
|
+
/**
|
|
697
|
+
* Set the main command handler.
|
|
698
|
+
*
|
|
699
|
+
* The handler receives a {@link CrustCommandContext} with `args` typed from
|
|
700
|
+
* `.args()` and `flags` typed as `EffectiveFlags<Inherited, Local>` (inherited
|
|
701
|
+
* flags merged with local flags).
|
|
702
|
+
*
|
|
703
|
+
* Returns a new builder with the handler stored. The original builder is
|
|
704
|
+
* not mutated.
|
|
705
|
+
*
|
|
706
|
+
* @param handler - The main command handler function
|
|
707
|
+
* @returns A new `Crust` instance with the handler registered
|
|
708
|
+
*/
|
|
709
|
+
run(handler: (ctx: NoInfer<CrustCommandContext<A, EffectiveFlags<Inherited, Local>>>) => void | Promise<void>): Crust<Inherited, Local, A>;
|
|
710
|
+
/**
|
|
711
|
+
* Set the pre-run lifecycle hook.
|
|
712
|
+
*
|
|
713
|
+
* Called before `run()` — useful for initialization and setup.
|
|
714
|
+
* Receives the same {@link CrustCommandContext} as `run()`.
|
|
715
|
+
*
|
|
716
|
+
* @param handler - The pre-run handler function
|
|
717
|
+
* @returns A new `Crust` instance with the preRun handler registered
|
|
718
|
+
*/
|
|
719
|
+
preRun(handler: (ctx: NoInfer<CrustCommandContext<A, EffectiveFlags<Inherited, Local>>>) => void | Promise<void>): Crust<Inherited, Local, A>;
|
|
720
|
+
/**
|
|
721
|
+
* Set the post-run lifecycle hook.
|
|
722
|
+
*
|
|
723
|
+
* Called after `run()` (even if it throws) — useful for teardown and cleanup.
|
|
724
|
+
* Receives the same {@link CrustCommandContext} as `run()`.
|
|
725
|
+
*
|
|
726
|
+
* @param handler - The post-run handler function
|
|
727
|
+
* @returns A new `Crust` instance with the postRun handler registered
|
|
728
|
+
*/
|
|
729
|
+
postRun(handler: (ctx: NoInfer<CrustCommandContext<A, EffectiveFlags<Inherited, Local>>>) => void | Promise<void>): Crust<Inherited, Local, A>;
|
|
730
|
+
/**
|
|
731
|
+
* Register a plugin on this command.
|
|
732
|
+
*
|
|
733
|
+
* Plugins are collected during `.execute()` and their `setup()` hooks
|
|
734
|
+
* receive `SetupContext` and `SetupActions`. Middleware hooks run in
|
|
735
|
+
* registration order.
|
|
736
|
+
*
|
|
737
|
+
* Returns a new builder with the plugin appended. The original builder
|
|
738
|
+
* is not mutated.
|
|
739
|
+
*
|
|
740
|
+
* @param plugin - The plugin to register
|
|
741
|
+
* @returns A new `Crust` instance with the plugin registered
|
|
742
|
+
*/
|
|
743
|
+
use(plugin: CrustPlugin): Crust<Inherited, Local, A>;
|
|
744
|
+
/**
|
|
745
|
+
* Create a subcommand builder pre-typed with this command's inheritable flags.
|
|
746
|
+
*
|
|
747
|
+
* This is the factory method for the file-splitting pattern. The returned
|
|
748
|
+
* builder carries this command's effective flags (filtered for `inherit: true`)
|
|
749
|
+
* as its `Inherited` generic, enabling full type inference in split files
|
|
750
|
+
* without needing `Crust<any, any, any>`.
|
|
751
|
+
*
|
|
752
|
+
* Register the resulting builder with `.command(builder)` on the parent.
|
|
753
|
+
*
|
|
754
|
+
* @param name - Subcommand name (must be non-empty)
|
|
755
|
+
* @returns A new `Crust` builder pre-typed with inherited flags
|
|
756
|
+
* @throws {CrustError} `DEFINITION` if name is empty or whitespace-only
|
|
757
|
+
*
|
|
758
|
+
* @example
|
|
759
|
+
* ```ts
|
|
760
|
+
* // shared.ts
|
|
761
|
+
* const app = new Crust("my-cli")
|
|
762
|
+
* .flags({ verbose: { type: "boolean", inherit: true } });
|
|
763
|
+
*
|
|
764
|
+
* // commands/deploy.ts
|
|
765
|
+
* const deployCmd = app.sub("deploy")
|
|
766
|
+
* .flags({ env: { type: "string", required: true } })
|
|
767
|
+
* .run(({ flags }) => {
|
|
768
|
+
* flags.verbose; // boolean | undefined — typed!
|
|
769
|
+
* flags.env; // string — typed!
|
|
770
|
+
* });
|
|
771
|
+
*
|
|
772
|
+
* // cli.ts
|
|
773
|
+
* app.command(deployCmd).execute();
|
|
774
|
+
* ```
|
|
775
|
+
*/
|
|
776
|
+
sub<N extends string>(name: N): Crust<EffectiveFlags<Inherited, Local>, {}, []>;
|
|
777
|
+
/**
|
|
778
|
+
* Register a named subcommand via inline callback.
|
|
779
|
+
*
|
|
780
|
+
* The callback receives a fresh `Crust` builder pre-typed with this
|
|
781
|
+
* command's effective inheritable flags, enabling TypeScript contextual
|
|
782
|
+
* typing to flow inherited flag types into subcommand definitions.
|
|
783
|
+
*
|
|
784
|
+
* @param name - Subcommand name (must be non-empty, unique among siblings)
|
|
785
|
+
* @param cb - Callback that receives a child builder and returns the configured builder
|
|
786
|
+
* @returns A new `Crust` instance with the subcommand registered
|
|
787
|
+
* @throws {CrustError} `DEFINITION` if name is empty or already registered
|
|
788
|
+
*/
|
|
789
|
+
command<N extends string>(name: N, cb: (cmd: Crust<EffectiveFlags<Inherited, Local>, {}, []>) => Crust<any, any, any>): Crust<Inherited, Local, A>;
|
|
790
|
+
/**
|
|
791
|
+
* Register a pre-built subcommand builder (from `.sub()`).
|
|
792
|
+
*
|
|
793
|
+
* The builder's name (from its constructor or `.sub()`) is used as the
|
|
794
|
+
* subcommand name. This is the complement to `.sub()` for the
|
|
795
|
+
* file-splitting pattern.
|
|
796
|
+
*
|
|
797
|
+
* @param builder - A pre-configured `Crust` builder instance
|
|
798
|
+
* @returns A new `Crust` instance with the subcommand registered
|
|
799
|
+
* @throws {CrustError} `DEFINITION` if builder name is empty or already registered
|
|
800
|
+
*/
|
|
801
|
+
command(builder: Crust<any, any, any>): Crust<Inherited, Local, A>;
|
|
802
|
+
/**
|
|
803
|
+
* Parse `process.argv`, resolve subcommands, run plugins and middleware,
|
|
804
|
+
* and execute the matched command handler.
|
|
805
|
+
*
|
|
806
|
+
* This is the entry point for CLI execution — call it on the root builder.
|
|
807
|
+
*
|
|
808
|
+
* @param options - Optional overrides (e.g. custom `argv` for testing)
|
|
809
|
+
* @returns A promise that resolves when execution completes
|
|
810
|
+
*/
|
|
811
|
+
execute(options?: {
|
|
812
|
+
argv?: string[];
|
|
813
|
+
}): Promise<void>;
|
|
814
|
+
}
|
|
423
815
|
interface CommandNotFoundErrorDetails {
|
|
424
816
|
input: string;
|
|
425
817
|
available: string[];
|
|
426
818
|
commandPath: string[];
|
|
427
|
-
parentCommand:
|
|
819
|
+
parentCommand: CommandNode;
|
|
428
820
|
}
|
|
429
821
|
interface ValidationErrorDetails {
|
|
430
822
|
issues: readonly {
|
|
@@ -513,109 +905,5 @@ declare class CrustError<C extends CrustErrorCode = CrustErrorCode> extends Erro
|
|
|
513
905
|
declare function parseArgs<
|
|
514
906
|
A extends ArgsDef = ArgsDef,
|
|
515
907
|
F extends FlagsDef = FlagsDef
|
|
516
|
-
>(command:
|
|
517
|
-
|
|
518
|
-
* The result of resolving a command from an argv array.
|
|
519
|
-
*
|
|
520
|
-
* Contains the resolved (sub)command and argv after subcommand
|
|
521
|
-
* resolution, and the full command path for help text rendering.
|
|
522
|
-
*/
|
|
523
|
-
interface CommandRoute {
|
|
524
|
-
/** The routed command (may be a subcommand of the original) */
|
|
525
|
-
command: AnyCommand;
|
|
526
|
-
/** The argv after subcommand names have been consumed */
|
|
527
|
-
argv: string[];
|
|
528
|
-
/** The command path for help text (e.g. ["crust", "generate", "command"]) */
|
|
529
|
-
commandPath: string[];
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
|
-
* Resolve a command from an argv array by walking the subcommand tree.
|
|
533
|
-
*
|
|
534
|
-
* Subcommand matching happens BEFORE flag parsing, so:
|
|
535
|
-
* `crust build --entry src/cli.ts` first resolves "build" as a subcommand,
|
|
536
|
-
* then passes `["--entry", "src/cli.ts"]` to the build command's parser.
|
|
537
|
-
*
|
|
538
|
-
* Resolution rules:
|
|
539
|
-
* 1. If `argv[0]` matches a subcommand key, recurse into that subcommand
|
|
540
|
-
* 2. If no match and the current command has `run()`, return it (args passed to parser)
|
|
541
|
-
* 3. If no match and the current command has NO `run()`, it signals the caller
|
|
542
|
-
* should show help (the `showHelp` flag is set in the result)
|
|
543
|
-
* 4. Unknown subcommands produce a structured COMMAND_NOT_FOUND error
|
|
544
|
-
*
|
|
545
|
-
* @param command - The root command to resolve from
|
|
546
|
-
* @param argv - The argv array to resolve against
|
|
547
|
-
* @returns The resolved command, argv, and the command path
|
|
548
|
-
* @throws {CrustError} COMMAND_NOT_FOUND when an unknown subcommand is given and the parent has no run()
|
|
549
|
-
*/
|
|
550
|
-
declare function resolveCommand(command: AnyCommand, argv: string[]): CommandRoute;
|
|
551
|
-
interface PluginState {
|
|
552
|
-
get<T = unknown>(key: string): T | undefined;
|
|
553
|
-
has(key: string): boolean;
|
|
554
|
-
set(key: string, value: unknown): void;
|
|
555
|
-
delete(key: string): boolean;
|
|
556
|
-
}
|
|
557
|
-
interface SetupActions {
|
|
558
|
-
/**
|
|
559
|
-
* Inject a flag definition into a command's flags object.
|
|
560
|
-
*
|
|
561
|
-
* Use this in plugin `setup()` hooks to register plugin-specific flags
|
|
562
|
-
* (e.g. `--version`, `--help`) so they are recognized by the parser
|
|
563
|
-
* and rendered in help text.
|
|
564
|
-
*
|
|
565
|
-
* @param command - The command to add the flag to
|
|
566
|
-
* @param name - The flag name (e.g. "version")
|
|
567
|
-
* @param def - The flag definition
|
|
568
|
-
*/
|
|
569
|
-
addFlag(command: AnyCommand, name: string, def: FlagDef): void;
|
|
570
|
-
/**
|
|
571
|
-
* Inject a subcommand into a command's `subCommands` record.
|
|
572
|
-
*
|
|
573
|
-
* Use this in plugin `setup()` hooks to register plugin-provided commands
|
|
574
|
-
* (e.g. a "skill" management command). If the parent already has a
|
|
575
|
-
* subcommand with the same name (user-defined), the call is silently
|
|
576
|
-
* skipped — user definitions always take priority over plugin injections.
|
|
577
|
-
*
|
|
578
|
-
* @param parent - The parent command to add the subcommand to
|
|
579
|
-
* @param name - The subcommand name (used for routing)
|
|
580
|
-
* @param command - The subcommand to register
|
|
581
|
-
*/
|
|
582
|
-
addSubCommand(parent: AnyCommand, name: string, command: AnyCommand): void;
|
|
583
|
-
}
|
|
584
|
-
/** Shared context fields available in both setup and middleware phases. */
|
|
585
|
-
interface BaseContext {
|
|
586
|
-
readonly argv: readonly string[];
|
|
587
|
-
readonly rootCommand: AnyCommand;
|
|
588
|
-
readonly state: PluginState;
|
|
589
|
-
}
|
|
590
|
-
/** Context passed to plugin `setup()` hooks. */
|
|
591
|
-
interface SetupContext extends BaseContext {}
|
|
592
|
-
/** Context passed to plugin `middleware()` hooks. */
|
|
593
|
-
interface MiddlewareContext extends BaseContext {
|
|
594
|
-
route: Readonly<CommandRoute> | null;
|
|
595
|
-
input: ParseResult | null;
|
|
596
|
-
}
|
|
597
|
-
type Next = () => Promise<void>;
|
|
598
|
-
type PluginMiddleware = (context: MiddlewareContext, next: Next) => void | Promise<void>;
|
|
599
|
-
interface CrustPlugin {
|
|
600
|
-
name?: string;
|
|
601
|
-
setup?: (context: SetupContext, actions: SetupActions) => void | Promise<void>;
|
|
602
|
-
middleware?: PluginMiddleware;
|
|
603
|
-
}
|
|
604
|
-
/**
|
|
605
|
-
* Build-time validation protocol.
|
|
606
|
-
*
|
|
607
|
-
* `crust build` spawns the user's entrypoint as a subprocess with
|
|
608
|
-
* `CRUST_INTERNAL_VALIDATE_ONLY=1`. When `runMain` detects this env flag it
|
|
609
|
-
* runs validation, surfaces errors via stderr/exitCode, then force-exits.
|
|
610
|
-
*
|
|
611
|
-
* `VALIDATION_RESULT_GLOBAL_KEY` stores the result on `globalThis` for
|
|
612
|
-
* in-process consumers (tests that call `runMain` directly).
|
|
613
|
-
*/
|
|
614
|
-
declare const VALIDATION_MODE_ENV = "CRUST_INTERNAL_VALIDATE_ONLY";
|
|
615
|
-
interface RunOptions {
|
|
616
|
-
argv?: string[];
|
|
617
|
-
plugins?: CrustPlugin[];
|
|
618
|
-
}
|
|
619
|
-
declare function runCommand(command: AnyCommand, options?: RunOptions): Promise<void>;
|
|
620
|
-
declare function runMain(command: AnyCommand, options?: RunOptions): Promise<void>;
|
|
621
|
-
export { runMain, runCommand, resolveCommand, parseArgs, defineCommand, ValueType, ValidateVariadicArgs, ValidateNoPrefixedFlags, ValidateFlagAliases, VALIDATION_MODE_ENV, SetupContext, SetupActions, RunOptions, PluginMiddleware, ParseResult, MiddlewareContext, InferFlags, InferArgs, FlagsDef, FlagDef, CrustPlugin, CrustErrorCode, CrustError, CommandRoute, CommandMeta, CommandDef, CommandContext, Command, ArgsDef, ArgDef, AnyCommand };
|
|
908
|
+
>(command: CommandNode, argv: string[]): ParseResult<A, F>;
|
|
909
|
+
export { resolveCommand, parseArgs, createCommandNode, computeEffectiveFlags, ValueType, ValidateVariadicArgs, ValidateNoPrefixedFlags, ValidateFlagAliases, ValidateCrossCollisions, VALIDATION_MODE_ENV, SetupContext, SetupActions, PluginMiddleware, ParseResult, MiddlewareContext, MergeFlags, InheritableFlags, InferFlags, InferArgs, FlagsDef, FlagDef, EffectiveFlags, CrustPlugin, CrustErrorCode, CrustError, CrustCommandContext, Crust, CommandRoute, CommandNode, CommandMeta, ArgsDef, ArgDef };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{a as
|
|
2
|
+
import{a as P,b as Z,c as _}from"./shared/chunk-stffrp66.js";function I(j){return{meta:{name:j},localFlags:{},effectiveFlags:{},args:void 0,subCommands:{},plugins:[],preRun:void 0,run:void 0,postRun:void 0}}function z(j,q){let J={};for(let[Q,$]of Object.entries(j))if($.inherit===!0)J[Q]=$;for(let[Q,$]of Object.entries(q))J[Q]=$;return J}function R(j,q){let J=[j.meta.name],Q=j,$=q;while($.length>0){let G=Q.subCommands;if(!G||Object.keys(G).length===0)break;let X=$[0];if(!X||X.startsWith("-"))break;if(X in G&&G[X]){Q=G[X],J.push(X),$=$.slice(1);continue}if(Q.run)break;let U=Object.keys(G);throw new Z("COMMAND_NOT_FOUND",`Unknown command "${X}".`,{input:X,available:U,commandPath:[...J],parentCommand:Q})}return{command:Q,argv:$,commandPath:J}}function F(j){for(let[q,J]of Object.entries(j)){if(q.startsWith("no-")){let Q=q.slice(3);throw new Z("DEFINITION",`Flag "--${q}" must not use "no-" prefix; define "${Q}" and negate with "--no-${Q}"`)}if(J.short?.startsWith("no-"))throw new Z("DEFINITION",`Short alias "-${J.short}" on "--${q}" must not use "no-" prefix (reserved for negation)`);if(J.aliases){for(let Q of J.aliases)if(Q.startsWith("no-"))throw new Z("DEFINITION",`Alias "--${Q}" on "--${q}" must not use "no-" prefix (reserved for negation)`)}}}var S="CRUST_INTERNAL_VALIDATE_ONLY",O="__CRUST_VALIDATE_RESULT__";function y(){let j=new Map;return{get(q){return j.get(q)},has(q){return j.has(q)},set(q,J){j.set(q,J)},delete(q){return j.delete(q)}}}function A(j){return{addFlag(q,J,Q){if(J in q.effectiveFlags)j?.push(`Plugin flag "--${J}" on "${q.meta.name}" overrides existing flag`);q.effectiveFlags[J]=Q},addSubCommand(q,J,Q){if(!J.trim())throw new Z("DEFINITION","addSubCommand: name must be a non-empty string");if(q.subCommands[J]){j?.push(`Plugin subcommand "${J}" on "${q.meta.name}" skipped (already exists)`);return}q.subCommands[J]=Q}}}async function x(j,q,J){for(let Q of j){if(!Q.setup)continue;await Q.setup(q,J)}}async function L(j,q,J){let Q=j.map((X)=>X.middleware).filter((X)=>Boolean(X)),$=-1,G=async(X)=>{if(X<=$)throw new Z("DEFINITION","Plugin middleware called next() multiple times");if($=X,X===Q.length){await J();return}let U=Q[X];if(!U)throw new Z("DEFINITION","Plugin middleware stack is invalid");await U(q,()=>G(X+1))};await G(0)}function T(j){let q=[...j.plugins];for(let J of Object.values(j.subCommands))q.push(...T(J));return q}function B(j){if(Object.freeze(j),Object.freeze(j.localFlags),Object.freeze(j.effectiveFlags),Object.freeze(j.meta),Object.freeze(j.plugins),j.args)Object.freeze(j.args);for(let q of Object.values(j.subCommands))B(q);Object.freeze(j.subCommands)}class V{_node;_inheritedFlags;constructor(j){if(!j.trim())throw new Z("DEFINITION","meta.name must be a non-empty string");this._node=I(j),this._inheritedFlags={}}static _createChild(j,q){let J=new V(j);return J._inheritedFlags=q,J}_clone(j){let q=Object.create(Object.getPrototypeOf(this)),J={...this._node,localFlags:{...this._node.localFlags},effectiveFlags:{...this._node.effectiveFlags},subCommands:{...this._node.subCommands},plugins:[...this._node.plugins],meta:{...this._node.meta},args:this._node.args?[...this._node.args]:void 0,...j};return q._node=J,q._inheritedFlags=this._inheritedFlags,q}meta(j){return this._clone({meta:{...this._node.meta,...j}})}flags(j){F(j);let q={};for(let[J,Q]of Object.entries(j))q[J]={...Q};return this._clone({localFlags:q,effectiveFlags:z(this._inheritedFlags,q)})}args(j){let q=j.map((J)=>({...J}));return this._clone({args:q})}run(j){return this._clone({run:j})}preRun(j){return this._clone({preRun:j})}postRun(j){return this._clone({postRun:j})}use(j){return this._clone({plugins:[...this._node.plugins,j]})}sub(j){if(!j.trim())throw new Z("DEFINITION","Subcommand name must be a non-empty string");let q=z(this._inheritedFlags,this._node.localFlags);return V._createChild(j,q)}command(j,q){if(typeof j==="string"){let G=j;if(!q)throw new Z("DEFINITION","command(name, cb) requires a callback");if(!G.trim())throw new Z("DEFINITION","Subcommand name must be a non-empty string");if(this._node.subCommands[G])throw new Z("DEFINITION",`Subcommand "${G}" is already registered`);let X=z(this._inheritedFlags,this._node.localFlags),U=V._createChild(G,X),Y=q(U),W={...Y._node,effectiveFlags:z(Y._inheritedFlags,Y._node.localFlags)};return this._clone({subCommands:{...this._node.subCommands,[G]:W}})}let J=j,Q=J._node.meta.name;if(!Q.trim())throw new Z("DEFINITION","Subcommand name must be a non-empty string");if(this._node.subCommands[Q])throw new Z("DEFINITION",`Subcommand "${Q}" is already registered`);let $={...J._node,effectiveFlags:z(J._inheritedFlags,J._node.localFlags)};return this._clone({subCommands:{...this._node.subCommands,[Q]:$}})}async execute(j){let q=j?.argv??process.argv.slice(2),J=this._node,Q=T(J),$=[],G=y(),X={argv:[...q],rootCommand:J,state:G},U=A($);try{await x(Q,X,U)}catch(W){if(W instanceof Z){console.error(`Error: ${W.message}`),process.exitCode=1;return}let H=W instanceof Error?W.message:String(W);console.error(`Error: ${H}`),process.exitCode=1;return}if(B(J),process.env[S]==="1"){let W=(async()=>{try{let{validateCommandTree:H}=await import("./shared/chunk-hzhxdeqq.js");H(J);for(let K of $)console.warn(`Warning: ${K}`);return{ok:!0}}catch(H){let K=H instanceof Error?H.message:String(H);return console.error(K),process.exitCode=1,{ok:!1,error:H}}})();return globalThis[O]=W,await W,process.exit(process.exitCode??0)}for(let W of $)console.warn(`Warning: ${W}`);let Y={argv:[...q],rootCommand:J,state:G,route:null,input:null};try{let W,H;try{let K=R(J,[...q]);Y.route=K,W=K.command,H=_(W,K.argv),Y.input=H}catch(K){await L(Q,Y,async()=>{throw K});return}await L(Q,Y,async()=>{if(!W.run)return;let K={args:H.args,flags:H.flags,rawArgs:H.rawArgs,command:W},M;try{if(W.preRun)await W.preRun(K);await W.run(K)}catch(D){M=D}if(W.postRun)try{await W.postRun(K)}catch(D){if(!M)M=D;else console.error(`Error in postRun: ${D instanceof Error?D.message:String(D)}`)}if(M)throw M})}catch(W){if(W instanceof Z){console.error(`Error: ${W.message}`),process.exitCode=1;return}if(W instanceof Error){let H=new Z("EXECUTION",W.message).withCause(W);console.error(`Error: ${H.message}`),process.exitCode=1;return}console.error(`Error: ${String(W)}`),process.exitCode=1}}}export{R as resolveCommand,_ as parseArgs,I as createCommandNode,z as computeEffectiveFlags,S as VALIDATION_MODE_ENV,Z as CrustError,V as Crust};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{b as P,c as Q}from"./chunk-stffrp66.js";function M(B){switch(B.type){case"number":return"1";case"boolean":return"true";default:return"sample"}}function S(B){let z=[],K=B.effectiveFlags;for(let[j,G]of Object.entries(K)){if(G.required!==!0||G.default!==void 0)continue;if(z.push(`--${j}`),G.type!=="boolean")z.push(M(G))}let H=B.args;if(H)for(let j of H){if(j.required!==!0||j.default!==void 0)continue;z.push(M(j))}return z}function Y(B){let z=[{command:B,path:[B.meta.name]}],K=new Set;while(z.length>0){let H=z.pop();if(!H)break;let{command:j,path:G}=H;if(K.has(j))continue;K.add(j);try{Q(j,S(j))}catch(J){let L=J instanceof Error?J.message:"Unknown validation error";throw new P("DEFINITION",`Command "${G.join(" ")}" failed runtime validation: ${L}`).withCause(J)}for(let[J,L]of Object.entries(j.subCommands))z.push({command:L,path:[...G,J]})}}export{Y as validateCommandTree};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var A=import.meta.require;class W extends Error{code;details;cause;constructor(z,H,...J){super(H);this.name="CrustError",this.code=z,this.details=J[0]}is(z){return this.code===z}withCause(z){return this.cause=z,this}}import{parseArgs as q}from"util";function U(z){let H={},J={};if(!z)return{options:H,aliasToName:J};let B=new Map;for(let j of Object.keys(z))B.set(j,j);for(let[j,G]of Object.entries(z)){if(j.startsWith("no-")){let L=j.slice(3);throw new W("DEFINITION",`Flag "--${j}" must not use "no-" prefix; define "${L}" and negate with "--no-${L}"`)}let K=G.type==="boolean"?"boolean":"string",Q={type:K};if(G.multiple)Q.multiple=!0;if(G.short){if(G.short.startsWith("no-"))throw new W("DEFINITION",`Short alias "-${G.short}" on "--${j}" must not use "no-" prefix (reserved for negation)`);let L=B.get(G.short);if(L)throw new W("DEFINITION",`Alias collision: "-${G.short}" is used by both "--${L}" and "--${j}"`);B.set(G.short,j),J[G.short]=j,Q.short=G.short}if(G.aliases)for(let L of G.aliases){if(L.startsWith("no-"))throw new W("DEFINITION",`Alias "--${L}" on "--${j}" must not use "no-" prefix (reserved for negation)`);let Z=B.get(L);if(Z)throw new W("DEFINITION",`Alias collision: "${L.length===1?"-":"--"}${L}" is used by both "--${Z}" and "--${j}"`);B.set(L,j),J[L]=j;let _={type:K};if(G.multiple)_.multiple=!0;H[L]=_}H[j]=Q}return{options:H,aliasToName:J}}function $(z,H,J){if(H==="number"){let B=Number(z);if(Number.isNaN(B))throw new W("PARSE",`Expected number for ${J}, got "${z}"`);return B}if(H==="boolean")return z==="true"||z==="1";return z}function I(z,H){if(z.default!==void 0)return z.default;if(z.required===!0)throw new W("VALIDATION",`Missing required ${H}`);return}function M(z,H,J){let B=`--${z}`;if(H.multiple&&Array.isArray(J))return H.type==="boolean"?J.filter((j)=>typeof j==="boolean"):J.map((j)=>$(j,H.type,B));if(H.type==="boolean"){if(typeof J==="boolean")return J;throw new W("PARSE",`Expected boolean value for flag "${B}", got ${typeof J}`)}if(typeof J==="string")return $(J,H.type,B);if(J===!0)return H.default??void 0;return J}function h(z,H,J){let B={};for(let j in z){let G=H[j]??j;if(!(G in J))continue;let K=z[j],Q=B[G];if(Q!==void 0&&Array.isArray(Q)&&Array.isArray(K))Q.push(...K);else B[G]=K}return B}function P(z,H,J){if(!z)return{};let B=h(H,J,z),j={};for(let[G,K]of Object.entries(z)){let Q=B[G];if(Q!==void 0){j[G]=M(G,K,Q);continue}j[G]=K.default??void 0}return j}function S(z,H){if(!z)return;for(let[J,B]of Object.entries(z))if(B.required===!0&&B.default===void 0){if(H[J]===void 0)throw new W("VALIDATION",`Missing required flag "--${J}"`)}}function O(z,H){if(!z)return{};let J={},B=0;for(let j of z){let{name:G}=j,K=`argument "<${G}>"`;if(j.variadic){let Q=H.slice(B);if(j.required===!0&&Q.length===0)throw new W("VALIDATION",`Missing required ${K}`);J[G]=j.type==="string"?Q:Q.map((L)=>$(L,j.type,`<${G}>`)),B=H.length}else if(B<H.length)J[G]=$(H[B],j.type,`<${G}>`),B++;else J[G]=I(j,K)}return J}function R(z,H,J){if(!H)return;for(let B of z){if(B==="--")return;if(!B.startsWith("--no-"))continue;let j=B.indexOf("="),G=j===-1?B.slice(5):B.slice(5,j);if(!G)continue;let K=J[G];if(!K)continue;if(K===G)continue;if(H[K]?.type!=="boolean")continue;throw new W("PARSE",`Cannot negate alias "--no-${G}"; use "--no-${K}" instead`)}}function E(z,H){let{args:J,effectiveFlags:B}=z,{options:j,aliasToName:G}=U(B);R(H,B,G);let K;try{K=q({args:H,options:j,strict:!0,allowPositionals:!0,allowNegative:!0,tokens:!0})}catch(X){if(X instanceof Error){let Y=X.message.match(/Unknown option '(.+?)'/);if(Y)throw new W("PARSE",`Unknown flag "${Y[1]}"`).withCause(X)}throw new W("PARSE","Failed to parse command arguments").withCause(X)}let Q=[],L=[];if(K.tokens){let X=!1;for(let Y of K.tokens){if(Y.kind==="option-terminator"){X=!0;continue}if(Y.kind==="positional")(X?Q:L).push(Y.value??"")}}else L.push(...K.positionals);let Z=P(B,K.values,G),_=O(J,L);return S(B,Z),{args:_,flags:Z,rawArgs:Q}}
|
|
3
|
+
export{A as a,W as b,E as c};
|
package/package.json
CHANGED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
var A=import.meta.require;class W extends Error{code;details;cause;constructor(j,G,...H){super(G);this.name="CrustError",this.code=j,this.details=H[0]}is(j){return this.code===j}withCause(j){return this.cause=j,this}}import{parseArgs as q}from"util";function U(j){let G={},H={};if(!j)return{options:G,aliasToName:H};let z=new Map;for(let B of Object.keys(j))z.set(B,B);for(let[B,J]of Object.entries(j)){if(B.startsWith("no-")){let X=B.slice(3);throw new W("DEFINITION",`Flag "--${B}" must not use "no-" prefix; define "${X}" and negate with "--no-${X}"`)}let K=J.type==="boolean"?"boolean":"string",L={type:K};if(J.multiple)L.multiple=!0;if(J.alias){let X=Array.isArray(J.alias)?J.alias:[J.alias];for(let Q of X){if(Q.startsWith("no-"))throw new W("DEFINITION",`Alias "--${Q}" on "--${B}" must not use "no-" prefix (reserved for negation)`);let _=z.get(Q);if(_)throw new W("DEFINITION",`Alias collision: "${Q.length===1?"-":"--"}${Q}" is used by both "--${_}" and "--${B}"`);if(z.set(Q,B),H[Q]=B,Q.length===1&&!L.short)L.short=Q;else{let Y={type:K};if(J.multiple)Y.multiple=!0;G[Q]=Y}}}G[B]=L}return{options:G,aliasToName:H}}function $(j,G,H){if(G==="number"){let z=Number(j);if(Number.isNaN(z))throw new W("PARSE",`Expected number for ${H}, got "${j}"`);return z}if(G==="boolean")return j==="true"||j==="1";return j}function I(j,G){if(j.default!==void 0)return j.default;if(j.required===!0)throw new W("VALIDATION",`Missing required ${G}`);return}function M(j,G,H){let z=`--${j}`;if(G.multiple&&Array.isArray(H))return G.type==="boolean"?H.filter((B)=>typeof B==="boolean"):H.map((B)=>$(B,G.type,z));if(G.type==="boolean"){if(typeof H==="boolean")return H;throw new W("PARSE",`Expected boolean value for flag "${z}", got ${typeof H}`)}if(typeof H==="string")return $(H,G.type,z);if(H===!0)return G.default??void 0;return H}function R(j,G,H){let z={};for(let B in j){let J=G[B]??B;if(!(J in H))continue;let K=j[B],L=z[J];if(L!==void 0&&Array.isArray(L)&&Array.isArray(K))L.push(...K);else z[J]=K}return z}function h(j,G,H){if(!j)return{};let z=R(G,H,j),B={};for(let[J,K]of Object.entries(j)){let L=z[J];if(L!==void 0){B[J]=M(J,K,L);continue}B[J]=K.default??void 0}return B}function P(j,G){if(!j)return;for(let[H,z]of Object.entries(j))if(z.required===!0&&z.default===void 0){if(G[H]===void 0)throw new W("VALIDATION",`Missing required flag "--${H}"`)}}function S(j,G){if(!j)return{};let H={},z=0;for(let B of j){let{name:J}=B,K=`argument "<${J}>"`;if(B.variadic){let L=G.slice(z);if(B.required===!0&&L.length===0)throw new W("VALIDATION",`Missing required ${K}`);H[J]=B.type==="string"?L:L.map((X)=>$(X,B.type,`<${J}>`)),z=G.length}else if(z<G.length)H[J]=$(G[z],B.type,`<${J}>`),z++;else H[J]=I(B,K)}return H}function O(j,G,H){if(!G)return;for(let z of j){if(z==="--")return;if(!z.startsWith("--no-"))continue;let B=z.indexOf("="),J=B===-1?z.slice(5):z.slice(5,B);if(!J)continue;let K=H[J];if(!K)continue;if(K===J)continue;if(G[K]?.type!=="boolean")continue;throw new W("PARSE",`Cannot negate alias "--no-${J}"; use "--no-${K}" instead`)}}function x(j,G){let{args:H,flags:z}=j,{options:B,aliasToName:J}=U(z);O(G,z,J);let K;try{K=q({args:G,options:B,strict:!0,allowPositionals:!0,allowNegative:!0,tokens:!0})}catch(Y){if(Y instanceof Error){let Z=Y.message.match(/Unknown option '(.+?)'/);if(Z)throw new W("PARSE",`Unknown flag "${Z[1]}"`).withCause(Y)}throw new W("PARSE","Failed to parse command arguments").withCause(Y)}let L=[],X=[];if(K.tokens){let Y=!1;for(let Z of K.tokens){if(Z.kind==="option-terminator"){Y=!0;continue}if(Z.kind==="positional")(Y?L:X).push(Z.value??"")}}else X.push(...K.positionals);let Q=h(z,K.values,J),_=S(H,X);return P(z,Q),{args:_,flags:Q,rawArgs:L}}
|
|
3
|
-
export{A as a,W as b,x as c};
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
import{b as G,c as H}from"./chunk-36287c08.js";function F(j){switch(j.type){case"number":return"1";case"boolean":return"true";default:return"sample"}}function I(j){let q=[];if(j.flags)for(let[w,x]of Object.entries(j.flags)){if(x.required!==!0||x.default!==void 0)continue;if(q.push(`--${w}`),x.type!=="boolean")q.push(F(x))}if(j.args)for(let w of j.args){if(w.required!==!0||w.default!==void 0)continue;q.push(F(w))}return q}function L(j){let q=[{command:j,path:[j.meta.name]}],w=new Set;while(q.length>0){let x=q.pop();if(!x)break;let{command:y,path:D}=x;if(w.has(y))continue;w.add(y);try{H(y,I(y))}catch(z){let B=z instanceof Error?z.message:"Unknown validation error";throw new G("DEFINITION",`Command "${D.join(" ")}" failed runtime validation: ${B}`).withCause(z)}for(let[z,B]of Object.entries(y.subCommands))q.push({command:B,path:[...D,z]})}}export{L as validateCommandTree};
|