@crustjs/core 0.2.1 → 0.3.1

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 CHANGED
@@ -5,7 +5,7 @@ Core library for the Crust CLI framework.
5
5
  ## Install
6
6
 
7
7
  ```sh
8
- bun add @crustjs/core
8
+ npm install @crustjs/core
9
9
  ```
10
10
 
11
11
  ## Documentation
@@ -92,6 +92,14 @@ type MergeProviders<A, B> = keyof A extends never ? B : keyof B extends never ?
92
92
  type DefName<T> = T extends {
93
93
  name: infer N extends string;
94
94
  } ? IsClosedName<N> extends true ? N : never : never;
95
+ /**
96
+ * Statically known `name` members of a definition (or definition union), for spelling grammar.
97
+ * Filtered per variant: combining names first would let an open variant's `string` absorb
98
+ * a literal sibling before {@link ClosedMembers} can see it.
99
+ */
100
+ type DefNameMembers<T> = T extends {
101
+ name: infer N extends string;
102
+ } ? ClosedMembers<N> : never;
95
103
  type UnionToIntersection<U> = (U extends unknown ? (x: U) => void : never) extends ((x: infer I) => void) ? I : never;
96
104
  type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
97
105
  /**
@@ -108,8 +116,14 @@ type HasClosedNames<Ds extends readonly unknown[]> = number extends Ds["length"]
108
116
  } ? IsUnion<N> extends true ? false : IsClosedName<N> : false; }[number] ? false : true;
109
117
  /** Brand statically known spelling collisions while allowing open names. */
110
118
  type CollisionBrand<S extends string, Existing extends string, Key extends string, Before extends string, After extends string> = string extends S | Existing ? {} : [S & Existing] extends [never] ? {} : { readonly [K in Key]: `${Before}"${S & Existing}"${After}`; };
111
- /** Brand a statically known empty literal while allowing widened and generic names. */
112
- type EmptyLiteralNameBrand<Name extends string, Err> = IsClosedName<Name> extends true ? ("" extends Name ? Err : {}) : {};
119
+ /**
120
+ * The statically known members of a name union; open members stay runtime-owned.
121
+ * Only for local spelling grammar: a literal beside an open template is still
122
+ * independently invalid, but must never become key or collision evidence.
123
+ */
124
+ type ClosedMembers<N extends string> = N extends unknown ? IsClosedName<N> extends true ? N : never : never;
125
+ /** Brand a statically known empty literal member while allowing widened and generic names. */
126
+ type EmptyLiteralNameBrand<Name extends string, Err> = "" extends ClosedMembers<Name> ? Err : {};
113
127
  /**
114
128
  * Brand a definition whose custom parser can return a Promise — parse results
115
129
  * are consumed synchronously during argv parsing. `Extract` keeps the check
@@ -600,7 +614,7 @@ type InferArgValue<A extends ArgDef> = A extends {
600
614
  } ? ResolveBaseType<A> : A extends {
601
615
  default: infer Default;
602
616
  } ? undefined extends Default ? ResolveBaseType<A> | undefined : ResolveBaseType<A> : ResolveBaseType<A> | undefined);
603
- type DuplicateArgNames<A extends readonly ArgDef[], Seen extends string = never> = A extends readonly [infer Head extends ArgDef, ...infer Tail extends readonly ArgDef[]] ? (Head["name"] & Seen) | DuplicateArgNames<Tail, Seen | Head["name"]> : never;
617
+ type DuplicateArgNames<A extends readonly ArgDef[], Seen extends string = never, Duplicates extends string = never> = A extends readonly [infer Head extends ArgDef, ...infer Tail extends readonly ArgDef[]] ? DuplicateArgNames<Tail, Seen | Head["name"], Duplicates | (Head["name"] & Seen)> : Duplicates;
604
618
  type InferDuplicateArgs<A extends readonly ArgDef[]> = A extends readonly [infer Head extends ArgDef, ...infer Tail extends readonly ArgDef[]] ? { [K in Head["name"]]: InferArgValue<Head>; } & InferDuplicateArgs<Tail> : {};
605
619
  /**
606
620
  * Convert a literal ArgsDef tuple into resolved values keyed by argument name.
@@ -825,4 +839,32 @@ interface ValidatedInput<A extends ArgsDef = ArgsDef, F extends FlagsDef = Flags
825
839
  flags: InferFlags<F>;
826
840
  }
827
841
  //#endregion
828
- export { CollisionBrand as A, UnionToIntersection as B, ValidatedInput as C, CommandSnapshot as D, ArgSnapshot as E, IsStaticTuple as F, defineExtensionId as H, IsUnion as I, LocalValueBrand as L, EmptyLiteralNameBrand as M, HasClosedNames as N, FlagSnapshot as O, IsClosedName as P, MergeContext as R, SectionConsumer as S, RunInputPayload as T, JsonCompatible as U, ExtensionId as V, JsonValue as W, ParseResult as _, CommandSectionInput as a, RuntimeCommandSectionInput as b, FlagsDef as c, InputArgs as d, InputFlags as f, NamedFlagsRecord as g, NamedFlagDef as h, CommandSection as i, DefName as j, Awaitable as k, InferArgs as l, MergeFlags as m, ArgsDef as n, DeclaredDefault as o, InvocationIO as p, CommandMeta as r, FlagDef as s, ArgDef as t, InferFlags as u, ParsedArgValue as v, ValueType as w, SectionAudience as x, ParsedFlagValue as y, MergeProviders as z };
842
+ //#region ../utils/src/artifacts.d.ts
843
+ /**
844
+ * Set by `crust build` while it prepares the Command Snapshot: the absolute
845
+ * entry-isolated directory Extension build hooks write into. Core reads it
846
+ * to run the hooks; `resolveArtifactDir` reads it so sections evaluated during
847
+ * that run see the artifacts being built instead of the wiped `.crust/root`.
848
+ */
849
+ declare const BUILD_OUT_DIR_ENV = "CRUST_INTERNAL_BUILD_OUT_DIR";
850
+ /**
851
+ * Absolute path of a build artifact or `crust.include` directory shipped with
852
+ * this CLI. `name` is a top-level directory name such as `"skills"`.
853
+ *
854
+ * The artifact path is computed from how the CLI is running — never probed:
855
+ * - Compiled executable (Bun or Deno): `<dir of the executable>/<name>`, which
856
+ * is a platform package's `bin/` or wherever the binary was placed.
857
+ * - Crust-built Node bundle: `<name>` next to the bundle's `bin/` directory,
858
+ * i.e. `.crust/root/<name>` in place and `<installed root>/<name>` after install.
859
+ * - Snapshot preparation inside `crust build`: `<build output dir>/<name>`, the
860
+ * artifacts earlier Extension build hooks wrote in this same build.
861
+ * - Source (`bun run`, `node`, `deno run`): `.crust/root/<name>` under the
862
+ * nearest package root of the real `process.argv[1]` entrypoint (following source
863
+ * links) — the output of the last `crust build`.
864
+ *
865
+ * @throws {Error} when `name` is not a single path segment, or in source mode
866
+ * when the entrypoint cannot be resolved or has no enclosing `package.json`.
867
+ */
868
+ declare function resolveArtifactDir(name: string): string;
869
+ //#endregion
870
+ export { FlagSnapshot as A, IsUnion as B, SectionAudience as C, RunInputPayload as D, ValueType as E, DefNameMembers as F, ExtensionId as G, MergeContext as H, EmptyLiteralNameBrand as I, JsonValue as J, defineExtensionId as K, HasClosedNames as L, ClosedMembers as M, CollisionBrand as N, ArgSnapshot as O, DefName as P, IsClosedName as R, RuntimeCommandSectionInput as S, ValidatedInput as T, MergeProviders as U, LocalValueBrand as V, UnionToIntersection as W, NamedFlagDef as _, CommandMeta as a, ParsedArgValue as b, DeclaredDefault as c, InferArgs as d, InferFlags as f, MergeFlags as g, InvocationIO as h, ArgsDef as i, Awaitable as j, CommandSnapshot as k, FlagDef as l, InputFlags as m, resolveArtifactDir as n, CommandSection as o, InputArgs as p, JsonCompatible as q, ArgDef as r, CommandSectionInput as s, BUILD_OUT_DIR_ENV as t, FlagsDef as u, NamedFlagsRecord as v, SectionConsumer as w, ParsedFlagValue as x, ParseResult as y, IsStaticTuple as z };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as CollisionBrand, B as UnionToIntersection, C as ValidatedInput, D as CommandSnapshot, E as ArgSnapshot, F as IsStaticTuple, H as defineExtensionId, I as IsUnion, L as LocalValueBrand, M as EmptyLiteralNameBrand, N as HasClosedNames, O as FlagSnapshot, P as IsClosedName, R as MergeContext, S as SectionConsumer, T as RunInputPayload, U as JsonCompatible, V as ExtensionId, W as JsonValue, _ as ParseResult, a as CommandSectionInput, b as RuntimeCommandSectionInput, c as FlagsDef, d as InputArgs, f as InputFlags, g as NamedFlagsRecord, h as NamedFlagDef, i as CommandSection, j as DefName, k as Awaitable, l as InferArgs, m as MergeFlags, n as ArgsDef, p as InvocationIO, r as CommandMeta, s as FlagDef, t as ArgDef, u as InferFlags, v as ParsedArgValue, w as ValueType, x as SectionAudience, y as ParsedFlagValue, z as MergeProviders } from "./types-DjMHz7M6.js";
1
+ import { A as FlagSnapshot, B as IsUnion, C as SectionAudience, D as RunInputPayload, E as ValueType, F as DefNameMembers, G as ExtensionId, H as MergeContext, I as EmptyLiteralNameBrand, J as JsonValue, K as defineExtensionId, L as HasClosedNames, M as ClosedMembers, N as CollisionBrand, O as ArgSnapshot, P as DefName, R as IsClosedName, S as RuntimeCommandSectionInput, T as ValidatedInput, U as MergeProviders, V as LocalValueBrand, W as UnionToIntersection, _ as NamedFlagDef, a as CommandMeta, b as ParsedArgValue, d as InferArgs, f as InferFlags, g as MergeFlags, h as InvocationIO, i as ArgsDef, j as Awaitable, k as CommandSnapshot, l as FlagDef, m as InputFlags, n as resolveArtifactDir, o as CommandSection, p as InputArgs, q as JsonCompatible, r as ArgDef, s as CommandSectionInput, u as FlagsDef, v as NamedFlagsRecord, w as SectionConsumer, x as ParsedFlagValue, y as ParseResult, z as IsStaticTuple } from "./artifacts-D8Jlug-a.js";
2
2
  //#region src/validation/args.brands.d.ts
3
3
  type ArgNames<A extends readonly object[]> = DefName<A[number]>;
4
4
  type DuplicateArgBrand<A, Existing extends string> = CollisionBrand<DefName<A>, Existing, "FIX_DUPLICATE_ARG", "Argument name ", " is already defined">;
@@ -7,7 +7,7 @@ type EmptyArgNameError = {
7
7
  };
8
8
  /** Reject empty argument names, including empty members of a name union. */
9
9
  type EmptyArgNameBrand<Name extends string> = EmptyLiteralNameBrand<Name, EmptyArgNameError>;
10
- type EmptyArgDefinitionNameBrand<A> = "" extends DefName<A> ? EmptyArgNameError : {};
10
+ type EmptyArgDefinitionNameBrand<A> = EmptyArgNameBrand<DefNameMembers<A>>;
11
11
  type ArgChecks<A, Existing extends string> = A & DuplicateArgBrand<A, Existing> & LocalValueBrand<A> & EmptyArgDefinitionNameBrand<A>;
12
12
  /**
13
13
  * Per-arg validation tuple type. Resolves to `A` when the constraints are
@@ -44,8 +44,9 @@ type AliasesOf<C> = C extends {
44
44
  readonly aliases: infer A extends readonly string[];
45
45
  } ? A : "aliases" extends keyof C ? readonly string[] : readonly [];
46
46
  type NarrowAliases<A extends readonly string[]> = IsClosedName<A[number]> extends true ? A[number] : never;
47
+ type AliasMembers<A extends readonly string[]> = ClosedMembers<A[number]>;
47
48
  type AliasShapeError<Name extends string, Alias extends string> = Alias extends "" ? `Subcommand "${Name}" has an invalid alias: must be a non-empty string` : Alias extends `${string} ${string}` | `${string}\t${string}` | `${string}\n${string}` | `${string}\r${string}` | `${string}\v${string}` | `${string}\f${string}` ? `Subcommand "${Name}" alias "${Alias}" must not contain whitespace` : Alias extends `-${string}` ? `Subcommand "${Name}" alias "${Alias}" must not start with "-" (reserved for flags)` : string extends Name ? never : Alias extends Name ? `Subcommand "${Name}" alias "${Alias}" must not equal its own canonical name` : never;
48
- type AliasShapeErrors<Name extends string, C> = NarrowAliases<AliasesOf<C>> extends (infer Alias) ? Alias extends string ? AliasShapeError<Name, Alias> : never : never;
49
+ type AliasShapeErrors<Name extends string, C> = AliasMembers<AliasesOf<C>> extends (infer Alias) ? Alias extends string ? AliasShapeError<Name, Alias> : never : never;
49
50
  type AliasShapeBrand<Name extends string, C> = [AliasShapeErrors<Name, C>] extends [never] ? {} : {
50
51
  readonly FIX_ALIAS_SHAPE: AliasShapeErrors<Name, C>;
51
52
  };
@@ -59,8 +60,8 @@ type EmptyNameError = {
59
60
  };
60
61
  type TrimWhitespace = " " | "\t" | "\n" | "\r" | "\v" | "\f" | " " | " " | " " | " " | " " | " " | " " | " " | " " | " " | " " | " " | " " | "
" | "
" | " " | " " | " " | "";
61
62
  type BlankName<Name extends string> = Name extends `${TrimWhitespace}${infer Tail}` ? BlankName<Tail> : Name extends "" ? true : false;
62
- /** Runtime checks own open names; provably invalid literal members remain errors. */
63
- type CommandNameBrand<Name extends string> = IsClosedName<Name> extends false ? {} : true extends BlankName<Name> ? EmptyNameError : "__proto__" extends Name ? {
63
+ /** Runtime checks own open members; every statically known member is validated, even beside an open one. */
64
+ type CommandNameBrand<Name extends string> = true extends BlankName<ClosedMembers<Name>> ? EmptyNameError : "__proto__" extends ClosedMembers<Name> ? {
64
65
  readonly FIX_RESERVED_NAME: "Command name \"__proto__\" is reserved";
65
66
  } : {};
66
67
  type DefinitionAliases<D> = CommandDefinitionData<D> extends {
@@ -70,9 +71,9 @@ type DefinitionAliases<D> = CommandDefinitionData<D> extends {
70
71
  type CommandDefinitionSpellings<D> = D extends unknown ? D extends {
71
72
  name: infer N extends string;
72
73
  } ? IsUnion<N> extends true ? never : DefName<D> extends (infer Name extends string) ? [Name] extends [never] ? never : Name | NarrowAliases<DefinitionAliases<D>> : never : never : never;
73
- type SelfAliasBrand<D> = DefName<D> & NarrowAliases<DefinitionAliases<D>> extends (infer Dup extends string) ? [Dup] extends [never] ? {} : {
74
+ type SelfAliasBrand<D> = UnionToIntersection<D extends unknown ? DefNameMembers<D> & AliasMembers<DefinitionAliases<D>> extends (infer Dup extends string) ? [Dup] extends [never] ? {} : {
74
75
  readonly FIX_ALIAS_SHAPE: `Command "${Dup}" must not list its own canonical name as an alias`;
75
- } : never;
76
+ } : never : never>;
76
77
  type CommandCollisionBrand<Spellings extends string, Existing extends string> = CollisionBrand<Spellings, Existing, "FIX_COMMAND_COLLISION", "Command name or alias ", " collides with a sibling command">;
77
78
  /**
78
79
  * Validate definitions against existing siblings and definitions earlier in
@@ -80,7 +81,7 @@ type CommandCollisionBrand<Spellings extends string, Existing extends string> =
80
81
  * not statically knowable; their literal aliases opt out with them
81
82
  * (see {@link CommandDefinitionSpellings}).
82
83
  */
83
- type ValidateCommandDefinitions<Ds extends readonly unknown[], Existing extends string = never> = Ds extends readonly [infer Head, ...infer Tail] ? CommandDefinitionSpellings<Head> extends (infer Spellings extends string) ? readonly [Head & CommandCollisionBrand<Spellings, Existing> & CommandNameBrand<DefName<Head>> & SelfAliasBrand<Head>, ...ValidateCommandDefinitions<Tail, Existing | Spellings>] : never : Ds;
84
+ type ValidateCommandDefinitions<Ds extends readonly unknown[], Existing extends string = never> = Ds extends readonly [infer Head, ...infer Tail] ? CommandDefinitionSpellings<Head> extends (infer Spellings extends string) ? readonly [Head & CommandCollisionBrand<Spellings, Existing> & CommandNameBrand<DefNameMembers<Head>> & SelfAliasBrand<Head>, ...ValidateCommandDefinitions<Tail, Existing | Spellings>] : never : Ds;
84
85
  type ExtensionCommandDefs<E> = [E] extends [never] ? readonly [] : DefiningOf<E> extends {
85
86
  readonly commands?: infer Cs extends readonly unknown[];
86
87
  } ? Cs : readonly [];
@@ -180,17 +181,29 @@ type KnownContextInstances<Cs extends readonly AnyContextInstance[]> = { [I in k
180
181
  //#region src/validation/flags.brands.d.ts
181
182
  /** Brand an incoming definition when one of its spellings is already claimed. */
182
183
  type ExistingFlagCollisionBrand<F, Existing extends string> = CollisionBrand<DefName<F> | ExtractAllAliases<F>, Existing, "FIX_ALIAS_COLLISION", "Flag spelling ", " collides with an existing flag">;
184
+ /**
185
+ * Every statically known canonical, short, and long-alias member, including literals
186
+ * beside an open member. Spelling grammar only: collision evidence stays with
187
+ * {@link ExtractAllAliases}, which must not leak literals out of an open domain.
188
+ * Each field is filtered on its own so a `string`-typed field cannot absorb a
189
+ * sibling field's invalid literal before the filter runs.
190
+ */
191
+ type SpellingMembers<F> = DefNameMembers<F> | ClosedMembers<F extends {
192
+ short: infer S extends string;
193
+ } ? S : never> | ClosedMembers<F extends {
194
+ aliases: infer A extends readonly string[];
195
+ } ? A[number] : never>;
183
196
  /** Reject `__proto__`, which mutates the prototype of plain-object flag registries. */
184
- type ReservedSpellingBrand<F> = "__proto__" extends DefName<F> | ExtractAllAliases<F> ? {
197
+ type ReservedSpellingBrand<F> = "__proto__" extends SpellingMembers<F> ? {
185
198
  readonly FIX_RESERVED_SPELLING: "Flag spelling \"__proto__\" is reserved";
186
199
  } : {};
187
200
  type EmptySpellingError = {
188
201
  readonly FIX_EMPTY_SPELLING: "Flag names and aliases must be non-empty strings";
189
202
  };
190
- /** Reject empty flag names, including empty members of a name union. */
203
+ /** Reject empty flag names, including an empty member of a name union beside an open member. */
191
204
  type EmptyFlagSpellingBrand<Name extends string> = EmptyLiteralNameBrand<Name, EmptySpellingError>;
192
205
  /** Reject empty spellings: their CLI tokens (`--`, `-`) are unparseable, so the flag can never be supplied. */
193
- type EmptySpellingBrand<F> = "" extends DefName<F> | ExtractAllAliases<F> ? EmptySpellingError : {};
206
+ type EmptySpellingBrand<F> = "" extends SpellingMembers<F> ? EmptySpellingError : {};
194
207
  type RepeatedAliases<Aliases extends readonly string[], Seen extends string> = Aliases extends readonly [infer Head extends string, ...infer Tail extends readonly string[]] ? (Head & Seen) | RepeatedAliases<Tail, Seen | Head> : never;
195
208
  type OwnAliasesBrand<F> = F extends {
196
209
  aliases: infer Aliases extends readonly string[];
@@ -298,7 +311,7 @@ type HasClosedFlagSpellings<F> = F extends {
298
311
  aliases: infer A extends readonly string[];
299
312
  } ? false extends IsStaticTuple<A> | IsClosedName<A[number]> ? false : true : false : true) ? false : true : false;
300
313
  type LocalFlagBrand<F> = UnionToIntersection<F extends unknown ? LocalFlagBranchBrand<F> : never>;
301
- type LocalFlagBranchBrand<F> = LocalValueBrand<F> & OwnAliasesBrand<F> & ShortLengthBrand<F> & ReservedSpellingBrand<F> & EmptySpellingBrand<F> & NoPrefixBrand<DefName<F> | ExtractAllAliases<F>> & ([DefName<F> & ExtractAllAliases<F>] extends [never] ? {} : {
314
+ type LocalFlagBranchBrand<F> = LocalValueBrand<F> & OwnAliasesBrand<F> & ShortLengthBrand<F> & ReservedSpellingBrand<F> & EmptySpellingBrand<F> & NoPrefixBrand<SpellingMembers<F>> & ([DefName<F> & ExtractAllAliases<F>] extends [never] ? {} : {
302
315
  readonly FIX_ALIAS_COLLISION: "Flag repeats one of its own spellings";
303
316
  });
304
317
  /** Validate provable local fields and destination relations without inventing names for open inputs. */
@@ -379,6 +392,18 @@ type CompatibleRunBranch<Expected, Actual> = Actual extends Expected ? Actual :
379
392
  type CompatibleRunInput<Shape extends CommandShape, Input> = CompatibleRunValue<RunInput<Shape>, Input>;
380
393
  type RunInputArguments<Shape extends CommandShape> = {} extends RunInput<Shape> ? readonly [input?: RunInput<Shape>] : readonly [input: RunInput<Shape>];
381
394
  type RunArguments<Shape extends CommandShape> = readonly [...RunInputArguments<Shape>, io?: Partial<InvocationIO>];
395
+ /**
396
+ * Typed invoker bound to one command in an app, returned by {@link Crust.at}.
397
+ *
398
+ * `run` accepts the same structured input and IO as `Crust.run` with the path
399
+ * already applied, so a handle can be re-exported as a plain typed function.
400
+ */
401
+ interface CommandHandle<Shape extends CommandShape> {
402
+ /** The typed path this handle was created with (`[]` selects the root). */
403
+ readonly path: readonly string[];
404
+ run(...args: RunArguments<Shape>): Promise<RunOutcome<Shape["result"]>>;
405
+ run<const Input>(input: Input, ...validation: [Input] extends [CompatibleRunInput<Shape, Input>] ? readonly [io?: Partial<InvocationIO>] : readonly [invalidInput: never]): Promise<RunOutcome<Shape["result"]>>;
406
+ }
382
407
  /** Static configuration for a reusable command definition. */
383
408
  interface CommandConfig extends Omit<CommandMeta, "name" | "sections" | "version"> {
384
409
  /** Plain-text sections rendered after built-in command documentation. */
@@ -435,9 +460,9 @@ type DepsOfBuilder<B> = UnionToIntersection<B extends {
435
460
  deps: infer Deps extends ContextMap;
436
461
  };
437
462
  } ? Deps : {}> extends (infer Merged extends ContextMap) ? Merged : {};
438
- type DefinitionShapeForSpelling<D, Spelling extends string> = CommandDefinitionData<D> extends {
463
+ type DefinitionShapeForSpelling<D, Spelling extends string> = D extends unknown ? CommandDefinitionData<D> extends {
439
464
  readonly _shape?: infer Shape extends CommandShape;
440
- } ? Spelling extends CommandDefinitionSpellings<D> ? Shape : never : never;
465
+ } ? Spelling extends CommandDefinitionSpellings<D> ? Shape : never : never : never;
441
466
  type ShapeWithInheritedFlags<S, CF extends FlagsDef> = keyof CF extends never ? S : S extends CommandShape<infer SA, infer SF, infer SC, infer SR, infer P> ? CommandShape<SA, MergeFlags<CF, SF>, { [K in keyof SC]: ShapeWithInheritedFlags<SC[K], CF>; }, SR, P> : never;
442
467
  type DefinitionsTree<Ds extends readonly CommandDefinition<any, any, any, any>[], CtxFlags extends FlagsDef = {}> = string extends AttachedCommandSpellings<Ds> ? Record<string, CommandShape> & KnownDefinitionTree<Ds, CtxFlags> : { [K in CommandDefinitionSpellings<Ds[number]>]: ShapeWithInheritedFlags<DefinitionShapeForSpelling<Ds[number], K>, CtxFlags>; };
443
468
  type KnownDefinitionTree<Ds extends readonly unknown[], CF extends FlagsDef> = IsUnion<Ds> extends true ? {} : Ds extends readonly [infer H, ...infer T] ? (IsUnion<H> extends true ? {} : { [K in CommandDefinitionSpellings<H>]: ShapeWithInheritedFlags<DefinitionShapeForSpelling<H, K>, CF>; }) & KnownDefinitionTree<T, CF> : {};
@@ -502,7 +527,8 @@ export declare function defineCommand<const Name extends string, const C extends
502
527
  * });
503
528
  * ```
504
529
  */
505
- type CollisionSpellings<Extensions extends string = never, Tree extends string = never, Demands extends ContextMap = {}, Pending extends string = never, RecipeDeps extends ContextMap = {}, Providers extends Record<string, ContextValue> = {}> = {
530
+ type CollisionSpellings<Extensions extends string = never, Tree extends string = never, Demands extends ContextMap = {}, Pending extends string = never, RecipeDeps extends ContextMap = {}, Providers extends Record<string, ContextValue> = {}, ActionDeps extends ContextMap = {}> = {
531
+ readonly actionDeps: ActionDeps;
506
532
  readonly pending: Pending;
507
533
  readonly demands: Demands;
508
534
  readonly extension: Extensions;
@@ -511,6 +537,7 @@ type CollisionSpellings<Extensions extends string = never, Tree extends string =
511
537
  readonly providers: Providers;
512
538
  };
513
539
  type AnyCollisionSpellings = {
540
+ readonly actionDeps?: ContextMap;
514
541
  readonly pending: string;
515
542
  readonly demands: ContextMap;
516
543
  readonly extension: string;
@@ -524,13 +551,19 @@ type RecipeDepsOf<S extends AnyCollisionSpellings> = S extends {
524
551
  type ProvidersOf<S extends AnyCollisionSpellings> = S extends {
525
552
  readonly providers: infer Providers extends Record<string, ContextValue>;
526
553
  } ? Providers : {};
554
+ /** Obligations of this node's stored action, never inherited by descendants. */
555
+ type ActionDepsOf<S extends AnyCollisionSpellings> = S extends {
556
+ readonly actionDeps: infer Deps extends ContextMap;
557
+ } ? Deps : {};
527
558
  type AfterFlags<Flags extends FlagsDef, A extends ArgsDef, Ctx extends ContextMap, Sibs extends string, Sp extends string, Tree extends object, CtxFlags extends FlagsDef, CollisionSp extends AnyCollisionSpellings, Result, Defs extends readonly NamedFlagDef[], Meta extends RootCommandMeta | undefined, Caps extends "app" | "recipe"> = Crust<MergeFlags<Flags, AttachedFlags<Defs>>, A, Ctx, Sibs, Sp | AttachedSpellings<Defs>, Tree, CtxFlags, CollisionSp, Result, Meta, string, Caps>;
528
559
  type AfterArgs<Flags extends FlagsDef, A extends ArgsDef, Ctx extends ContextMap, Sibs extends string, Sp extends string, Tree extends object, CtxFlags extends FlagsDef, CollisionSp extends AnyCollisionSpellings, Result, NewA extends ArgsDef, Meta extends RootCommandMeta | undefined, Caps extends "app" | "recipe"> = Crust<Flags, AppendedArgs<A, NewA>, Ctx, Sibs, Sp, Tree, CtxFlags, CollisionSp, Result, Meta, string, Caps>;
529
- type AfterUse<Flags extends FlagsDef, A extends ArgsDef, Ctx extends ContextMap, Sibs extends string, Sp extends string, Tree extends object, CtxFlags extends FlagsDef, CollisionSp extends AnyCollisionSpellings, Result, Fs extends readonly AnyContextFactory[], Meta extends RootCommandMeta | undefined> = Crust<Flags, A, MergeContext<Ctx, ContextDependencies<Fs>>, Sibs, Sp, Tree, CtxFlags, CollisionSpellings<CollisionSp["extension"], CollisionSp["tree"], CollisionSp["demands"], CollisionSp["pending"], MergeContext<RecipeDepsOf<CollisionSp>, ContextDependencies<Fs>>, ProvidersOf<CollisionSp>>, Result, Meta, string, "recipe">;
530
- type AfterProvide<Flags extends FlagsDef, A extends ArgsDef, Ctx extends ContextMap, Sibs extends string, Sp extends string, Tree extends object, CtxFlags extends FlagsDef, CollisionSp extends AnyCollisionSpellings, Result, Cs extends readonly AnyContextInstance[], Meta extends RootCommandMeta | undefined, Caps extends "app" | "recipe"> = Crust<MergeFlags<Flags, ContextsOwnedFlags<Cs>>, A, MergeProviders<Ctx, ContextsOutput<Cs>>, Sibs, Sp | LocalSpellingsOf<ContextsOwnedFlags<Cs>>, Tree, MergeFlags<CtxFlags, ContextsOwnedFlags<Cs>>, CollisionSpellings<CollisionSp["extension"], CollisionSp["tree"], CollisionSp["demands"], CollisionSp["pending"], RecipeDepsOf<CollisionSp>, MergeProviders<ProvidersOf<CollisionSp>, ContextsOutput<Cs>>>, Result, Meta, string, Caps>;
531
- type AfterAction<Flags extends FlagsDef, A extends ArgsDef, Ctx extends ContextMap, Sibs extends string, Sp extends string, Tree extends object, CtxFlags extends FlagsDef, CollisionSp extends AnyCollisionSpellings, R, Meta extends RootCommandMeta | undefined, Caps extends "app" | "recipe"> = Crust<Flags, A, Ctx, Sibs, Sp, Tree, CtxFlags, CollisionSp, Awaited<R>, Meta, string, Caps>;
532
- type AfterExtend<Flags extends FlagsDef, A extends ArgsDef, Ctx extends ContextMap, Sibs extends string, Sp extends string, Tree extends object, CtxFlags extends FlagsDef, CollisionSp extends AnyCollisionSpellings, Result, Es extends readonly AnyExtension[], Meta extends RootCommandMeta | undefined, Caps extends "app" | "recipe"> = Crust<MergeFlags<Flags, ExtensionFlags<Es>>, A, MergeProviders<Ctx, ExtensionsProvidesOutput<Es>>, Sibs | ExtensionsCommandSpellings<Es>, Sp | ExtensionsSpellings<Es>, ExtendedTree<Tree, Es, RecursiveExtensionFlags<Es>, CtxFlags>, MergeFlags<CtxFlags, RecursiveExtensionFlags<Es>>, CollisionSpellings<CollisionSp["extension"] | ExtensionsSpellings<Es>, CollisionSp["tree"] | DefinitionTreeSpellings<ExtensionCommands<Es>>, CollisionSp["demands"] & ExtensionDemandValues<Es>, CollisionSp["pending"] | DefinitionTreeSpellings<ExtensionCommands<Es>>, RecipeDepsOf<CollisionSp>, MergeProviders<ProvidersOf<CollisionSp>, ExtensionsProvidesOutput<Es>>>, Result, Meta, string, Caps>;
533
- type AfterAdd<Flags extends FlagsDef, A extends ArgsDef, Ctx extends ContextMap, Sibs extends string, Sp extends string, Tree extends object, CtxFlags extends FlagsDef, CollisionSp extends AnyCollisionSpellings, Result, Ds extends readonly CommandDefinition<any, any, any, any>[], Meta extends RootCommandMeta | undefined, Caps extends "app" | "recipe"> = Crust<Flags, A, Ctx, Sibs | AttachedCommandSpellings<Ds>, Sp, Tree & DefinitionsTree<Ds, CtxFlags>, CtxFlags, CollisionSpellings<CollisionSp["extension"], CollisionSp["tree"] | DefinitionTreeSpellings<Ds>, CollisionSp["demands"], CollisionSp["pending"], RecipeDepsOf<CollisionSp>, ProvidersOf<CollisionSp>>, Result, Meta, string, Caps>;
560
+ type AfterUse<Flags extends FlagsDef, A extends ArgsDef, Ctx extends ContextMap, Sibs extends string, Sp extends string, Tree extends object, CtxFlags extends FlagsDef, CollisionSp extends AnyCollisionSpellings, Result, Fs extends readonly AnyContextFactory[], Meta extends RootCommandMeta | undefined> = Crust<Flags, A, MergeContext<Ctx, ContextDependencies<Fs>>, Sibs, Sp, Tree, CtxFlags, CollisionSpellings<CollisionSp["extension"], CollisionSp["tree"], CollisionSp["demands"], CollisionSp["pending"], MergeContext<RecipeDepsOf<CollisionSp>, ContextDependencies<Fs>>, ProvidersOf<CollisionSp>, ActionDepsOf<CollisionSp>>, Result, Meta, string, "recipe">;
561
+ type AfterProvide<Flags extends FlagsDef, A extends ArgsDef, Ctx extends ContextMap, Sibs extends string, Sp extends string, Tree extends object, CtxFlags extends FlagsDef, CollisionSp extends AnyCollisionSpellings, Result, Cs extends readonly AnyContextInstance[], Meta extends RootCommandMeta | undefined, Caps extends "app" | "recipe"> = Crust<MergeFlags<Flags, ContextsOwnedFlags<Cs>>, A, MergeProviders<Ctx, ContextsOutput<Cs>>, Sibs, Sp | LocalSpellingsOf<ContextsOwnedFlags<Cs>>, Tree, MergeFlags<CtxFlags, ContextsOwnedFlags<Cs>>, CollisionSpellings<CollisionSp["extension"], CollisionSp["tree"], CollisionSp["demands"], CollisionSp["pending"], RecipeDepsOf<CollisionSp>, MergeProviders<ProvidersOf<CollisionSp>, ContextsOutput<Cs>>, ActionDepsOf<CollisionSp>>, Result, Meta, string, Caps>;
562
+ type AfterAction<Flags extends FlagsDef, A extends ArgsDef, Ctx extends ContextMap, Sibs extends string, Sp extends string, Tree extends object, CtxFlags extends FlagsDef, CollisionSp extends AnyCollisionSpellings, R, Meta extends RootCommandMeta | undefined, Caps extends "app" | "recipe"> = Crust<Flags, A, Ctx, Sibs, Sp, Tree, CtxFlags, Omit<CollisionSp, "actionDeps"> & {
563
+ readonly actionDeps: Ctx;
564
+ }, Awaited<R>, Meta, string, Caps>;
565
+ type AfterExtend<Flags extends FlagsDef, A extends ArgsDef, Ctx extends ContextMap, Sibs extends string, Sp extends string, Tree extends object, CtxFlags extends FlagsDef, CollisionSp extends AnyCollisionSpellings, Result, Es extends readonly AnyExtension[], Meta extends RootCommandMeta | undefined, Caps extends "app" | "recipe"> = Crust<MergeFlags<Flags, ExtensionFlags<Es>>, A, MergeProviders<Ctx, ExtensionsProvidesOutput<Es>>, Sibs | ExtensionsCommandSpellings<Es>, Sp | ExtensionsSpellings<Es>, ExtendedTree<Tree, Es, RecursiveExtensionFlags<Es>, CtxFlags>, MergeFlags<CtxFlags, RecursiveExtensionFlags<Es>>, CollisionSpellings<CollisionSp["extension"] | ExtensionsSpellings<Es>, CollisionSp["tree"] | DefinitionTreeSpellings<ExtensionCommands<Es>>, CollisionSp["demands"] & ExtensionDemandValues<Es>, CollisionSp["pending"] | DefinitionTreeSpellings<ExtensionCommands<Es>>, RecipeDepsOf<CollisionSp>, MergeProviders<ProvidersOf<CollisionSp>, ExtensionsProvidesOutput<Es>>, ActionDepsOf<CollisionSp>>, Result, Meta, string, Caps>;
566
+ type AfterAdd<Flags extends FlagsDef, A extends ArgsDef, Ctx extends ContextMap, Sibs extends string, Sp extends string, Tree extends object, CtxFlags extends FlagsDef, CollisionSp extends AnyCollisionSpellings, Result, Ds extends readonly CommandDefinition<any, any, any, any>[], Meta extends RootCommandMeta | undefined, Caps extends "app" | "recipe"> = Crust<Flags, A, Ctx, Sibs | AttachedCommandSpellings<Ds>, Sp, Tree & DefinitionsTree<Ds, CtxFlags>, CtxFlags, CollisionSpellings<CollisionSp["extension"], CollisionSp["tree"] | DefinitionTreeSpellings<Ds>, CollisionSp["demands"], CollisionSp["pending"], RecipeDepsOf<CollisionSp>, ProvidersOf<CollisionSp>, ActionDepsOf<CollisionSp>>, Result, Meta, string, Caps>;
534
567
  type ExtensionDemandValues<Es extends readonly AnyExtension[]> = UnionToIntersection<DefiningOf<Es[number]> extends {
535
568
  readonly _hookDeps?: infer H extends ContextMap;
536
569
  } ? H : Record<string, ContextValue>> extends (infer D extends ContextMap) ? D : {};
@@ -588,7 +621,7 @@ export declare class Crust<Flags extends FlagsDef = {}, A extends ArgsDef = [],
588
621
  * @param defs - Named flag definitions
589
622
  * @returns A new `Crust` instance with the given flags
590
623
  */
591
- flags<const Defs extends readonly NamedFlagDef[]>(...defs: ValidateLocalFlagDefs<Defs, Sp>): AfterFlags<Flags, A, Ctx, Sibs, Sp, Tree, CtxFlags, CollisionSp, Result, Defs, Meta, Caps>;
624
+ flags<const Defs extends readonly NamedFlagDef[], _Union extends this = never>(...defs: ValidateLocalFlagDefs<Defs, Sp>): AfterFlags<Flags, A, Ctx, Sibs, Sp, Tree, CtxFlags, CollisionSp, Result, Defs, Meta, Caps>;
592
625
  /**
593
626
  * Define positional arguments for this command; argument order is the
594
627
  * order they are passed (created with `defineArg(name, def)` or written
@@ -600,12 +633,12 @@ export declare class Crust<Flags extends FlagsDef = {}, A extends ArgsDef = [],
600
633
  * @param defs - Positional argument definitions, in positional order
601
634
  * @returns A new `Crust` instance with the combined args
602
635
  */
603
- args<const NewA extends ArgsDef>(...defs: NewA & AppendArgsChecks<A, NewA>): AfterArgs<Flags, A, Ctx, Sibs, Sp, Tree, CtxFlags, CollisionSp, Result, NewA, Meta, Caps>;
636
+ args<const NewA extends ArgsDef, _Union extends this = never>(...defs: NewA & AppendArgsChecks<A, NewA>): AfterArgs<Flags, A, Ctx, Sibs, Sp, Tree, CtxFlags, CollisionSp, Result, NewA, Meta, Caps>;
604
637
  /**
605
638
  * Declare Contexts this command consumes without supplying their values.
606
639
  * Factory references are retained while setup stays lazy.
607
640
  */
608
- use<const Fs extends readonly [AnyContextFactory, ...AnyContextFactory[]]>(this: {
641
+ use<const Fs extends readonly [AnyContextFactory, ...AnyContextFactory[]] = never>(this: {
609
642
  readonly _types: {
610
643
  readonly caps: "recipe";
611
644
  };
@@ -620,7 +653,7 @@ export declare class Crust<Flags extends FlagsDef = {}, A extends ArgsDef = [],
620
653
  * Extension commands. Consuming operations throw `DEFINITION` for actual collisions.
621
654
  *
622
655
  */
623
- provide<const Cs extends readonly AnyContextInstance[]>(...instances: KnownContextInstances<Cs> & ProvideChecks<Sp | CollisionSp["pending"], Cs> & ValidateContextNames<Caps extends "recipe" ? ProvidersOf<CollisionSp> : Ctx, Cs> & ValidateContextDeps<Ctx, Cs> & DeclaredDependencyValuesBrand<CollisionSp["demands"] & RecipeDepsOf<CollisionSp>, ContextsOutput<NoInfer<Cs>>>): AfterProvide<Flags, A, Ctx, Sibs, Sp, Tree, CtxFlags, CollisionSp, Result, Cs, Meta, Caps>;
656
+ provide<const Cs extends readonly AnyContextInstance[] = never>(...instances: KnownContextInstances<Cs> & ProvideChecks<Sp | CollisionSp["pending"], Cs> & ValidateContextNames<Caps extends "recipe" ? ProvidersOf<CollisionSp> : Ctx, Cs> & ValidateContextDeps<Ctx, Cs> & DeclaredDependencyValuesBrand<CollisionSp["demands"] & RecipeDepsOf<CollisionSp> & ActionDepsOf<CollisionSp>, ContextsOutput<NoInfer<Cs>>>): AfterProvide<Flags, A, Ctx, Sibs, Sp, Tree, CtxFlags, CollisionSp, Result, Cs, Meta, Caps>;
624
657
  /**
625
658
  * Define the Command Action — the function that implements this
626
659
  * command's behavior after its inputs are ready.
@@ -628,8 +661,9 @@ export declare class Crust<Flags extends FlagsDef = {}, A extends ArgsDef = [],
628
661
  * The action receives a {@link CrustCommandContext} with `args` typed from
629
662
  * `.args()` and `flags` typed from the accumulated `Flags`.
630
663
  *
631
- * Calling `.action()` again replaces the command behavior on the new builder.
632
- * The original builder is not mutated.
664
+ * Calling `.action()` again replaces the command behavior and its Context obligations
665
+ * on the new builder. Later local providers must satisfy the bound Context value types;
666
+ * descendants may shadow independently. The original builder is not mutated.
633
667
  *
634
668
  * @param action - The Command Action function
635
669
  * @returns A new `Crust` instance with the action registered
@@ -650,7 +684,7 @@ export declare class Crust<Flags extends FlagsDef = {}, A extends ArgsDef = [],
650
684
  * metadata by TypeScript, not at runtime.
651
685
  * Recipe builders cannot call this root-only method.
652
686
  */
653
- extend<const Es extends readonly Extension<any, any, any, any, DefinedRootMetaKeys<Meta>>[]>(this: {
687
+ extend<const Es extends readonly Extension<any, any, any, any, DefinedRootMetaKeys<Meta>>[], _Union extends this = never>(this: {
654
688
  readonly _types: {
655
689
  readonly caps: "app";
656
690
  };
@@ -662,7 +696,7 @@ export declare class Crust<Flags extends FlagsDef = {}, A extends ArgsDef = [],
662
696
  * under its own carried name (use `.as(name)` to rename).
663
697
  *
664
698
  */
665
- add<const Ds extends readonly CommandDefinition<any, any, any, any>[]>(...definitions: Ds & ValidateCommandDefinitions<Ds, Sibs> & ValidateDeclaredDeps<Ctx, Ds> & DefinitionDescendantValuesBrand<Ds, CollisionSp["demands"]> & ValidateDefinitionFlags<Ds, CollisionSp["extension"] | LocalSpellingsOf<CtxFlags>>): AfterAdd<Flags, A, Ctx, Sibs, Sp, Tree, CtxFlags, CollisionSp, Result, Ds, Meta, Caps>;
699
+ add<const Ds extends readonly CommandDefinition<any, any, any, any>[] = never>(...definitions: Ds & ValidateCommandDefinitions<Ds, Sibs> & ValidateDeclaredDeps<Ctx, Ds> & DefinitionDescendantValuesBrand<Ds, CollisionSp["demands"]> & ValidateDefinitionFlags<Ds, CollisionSp["extension"] | LocalSpellingsOf<CtxFlags>>): AfterAdd<Flags, A, Ctx, Sibs, Sp, Tree, CtxFlags, CollisionSp, Result, Ds, Meta, Caps>;
666
700
  /**
667
701
  * Define an app-local leaf subcommand inline (root-only sugar for
668
702
  * `.add(defineCommand(name, recipe))`).
@@ -714,6 +748,22 @@ export declare class Crust<Flags extends FlagsDef = {}, A extends ArgsDef = [],
714
748
  readonly caps: "app";
715
749
  };
716
750
  }, path: Path & KnownCommandPath<Path, Tree>, input: Input, ...validation: [Input] extends [CompatibleRunInput<CommandShapeAt<CommandShape<A, Flags, Tree, Result>, NoInfer<Path>>, Input>] ? readonly [io?: Partial<InvocationIO>] : readonly [invalidInput: never]): Promise<RunOutcome<CommandShapeAt<CommandShape<A, Flags, Tree, Result>, Path>["result"]>>;
751
+ /**
752
+ * Bind a typed command path into a reusable {@link CommandHandle}.
753
+ *
754
+ * Validates the path eagerly against the prepared command tree, so an
755
+ * unknown path throws `COMMAND_NOT_FOUND` here rather than on the first
756
+ * `handle.run()`. The handle is bound to this builder's tree; commands added
757
+ * afterwards belong to the new builder returned by `.add()`.
758
+ *
759
+ * @param path - Typed path to the command to bind (`[]` selects the root)
760
+ * @throws {CrustError} COMMAND_NOT_FOUND when the path does not name a command
761
+ */
762
+ at<const Path extends CommandPath<Tree>>(this: {
763
+ readonly _types: {
764
+ readonly caps: "app";
765
+ };
766
+ }, path: Path & KnownCommandPath<Path, Tree>): CommandHandle<CommandShapeAt<CommandShape<A, Flags, Tree, Result>, Path>>;
717
767
  /**
718
768
  * Parse `process.argv`, resolve subcommands, run Extension hooks, and
719
769
  * execute the matched Command Action.
@@ -867,24 +917,31 @@ type RootMetaKey = keyof RootCommandMeta;
867
917
  type RootCommandSnapshot<K extends RootMetaKey> = CommandSnapshot & {
868
918
  readonly meta: Readonly<Required<Pick<CommandMeta, K>>>;
869
919
  };
870
- /** Artifact paths relative to `outDir`. */
871
- type BuildArtifacts = readonly string[];
872
- /** Artifacts reported by Extension build hooks, in hook execution order. */
920
+ /** One file a build hook produces; `path` is POSIX-relative to the build output directory. */
921
+ interface BuildFile {
922
+ readonly path: string;
923
+ readonly content: string | Uint8Array;
924
+ }
925
+ /** Files returned by a build hook; build tooling writes them into the build output directory. */
926
+ type BuildArtifacts = readonly BuildFile[];
927
+ /** Files written for each Extension build hook, in hook execution order. */
873
928
  interface BuildReport {
874
929
  readonly extensions: readonly {
875
930
  readonly id: ExtensionId;
876
- readonly files: readonly string[] | "unknown";
931
+ readonly files: readonly string[];
877
932
  }[];
878
933
  }
879
- /** Build-time context passed to an Extension's artifact generator. */
934
+ /**
935
+ * Build-time context passed to an Extension's artifact generator. It deliberately
936
+ * carries no output directory: build tooling owns that tree, so every shipped file
937
+ * is a returned {@link BuildFile} and the {@link BuildReport} is exact.
938
+ */
880
939
  interface ExtensionBuildContext<MetaKeys extends RootMetaKey = never> {
881
940
  /**
882
941
  * Frozen snapshot prepared before this hook starts. It does not include this hook's own
883
942
  * outputs; later-registered hooks receive refreshed snapshots.
884
943
  */
885
944
  readonly snapshot: RootCommandSnapshot<MetaKeys>;
886
- /** Resolved absolute output directory. */
887
- readonly outDir: string;
888
945
  }
889
946
  /**
890
947
  * Readonly invocation view passed to Extension hooks.
@@ -998,15 +1055,14 @@ type ExtensionFlagDef = FlagDef & {
998
1055
  type NamedExtensionFlagDef = NamedFlagDef & {
999
1056
  readonly recursive?: boolean;
1000
1057
  };
1058
+ type SchemaToken<F> = F extends {
1059
+ type: "boolean";
1060
+ } ? boolean : string;
1001
1061
  type InferPreSchemaExtensionFlag<F extends ExtensionFlagDef> = F extends {
1002
1062
  schema: unknown;
1003
- } ? F extends {
1063
+ } ? (F extends {
1004
1064
  multiple: true;
1005
- } ? F extends {
1006
- type: "boolean";
1007
- } ? boolean[] | undefined : string[] | undefined : F extends {
1008
- type: "boolean";
1009
- } ? boolean | undefined : string | undefined : F extends {
1065
+ } ? never : SchemaToken<F>) | ("multiple" extends keyof F ? true extends F["multiple"] ? SchemaToken<F>[] : never : never) | undefined : F extends {
1010
1066
  required: true;
1011
1067
  } ? F extends {
1012
1068
  default: unknown;
@@ -1017,11 +1073,9 @@ type InferPreSchemaExtensionFlag<F extends ExtensionFlagDef> = F extends {
1017
1073
  }>["value"] | undefined : InferFlags<{
1018
1074
  value: F;
1019
1075
  }>["value"];
1020
- type InferExtensionFlag<F> = F extends ExtensionFlagDef ? F extends {
1021
- recursive: false;
1022
- } ? InferPreSchemaExtensionFlag<F> | undefined : InferPreSchemaExtensionFlag<F> : never;
1023
- /** Infer the syntax-parsed values visible to an Extension's hooks. */
1024
- type InferExtensionFlags<Defs extends readonly NamedExtensionFlagDef[]> = { [K in keyof NamedFlagsRecord<Defs>]: InferExtensionFlag<NamedFlagsRecord<Defs>[K]>; };
1076
+ type InferExtensionFlag<F> = F extends ExtensionFlagDef ? InferPreSchemaExtensionFlag<F> | ("recursive" extends keyof F ? (false extends F["recursive"] ? undefined : never) : never) : never;
1077
+ /** Infer pre-validation hook values; uncertain collections claim no typed ownership. */
1078
+ type InferExtensionFlags<Defs extends readonly NamedExtensionFlagDef[]> = IsStaticTuple<Defs> extends true ? HasClosedNames<Defs> extends true ? { [K in keyof NamedFlagsRecord<Defs>]: InferExtensionFlag<NamedFlagsRecord<Defs>[K]>; } : Record<string, ParsedFlagValue> : Record<string, ParsedFlagValue>;
1025
1079
  /** A documentation section an Extension contributes to one command path. */
1026
1080
  type ExtensionSectionContribution = RuntimeCommandSectionInput & {
1027
1081
  readonly command: readonly string[];
@@ -1033,7 +1087,7 @@ interface ExtensionConfig<Defs extends readonly NamedExtensionFlagDef[] = readon
1033
1087
  readonly uses?: Uses;
1034
1088
  readonly provides?: Provides;
1035
1089
  readonly sections?: (snapshot: RootCommandSnapshot<MetaKeys>) => readonly ExtensionSectionContribution[];
1036
- readonly build?: (ctx: ExtensionBuildContext<MetaKeys>) => BuildArtifacts | void | Promise<BuildArtifacts | void>;
1090
+ readonly build?: (ctx: ExtensionBuildContext<MetaKeys>) => Awaitable<BuildArtifacts>;
1037
1091
  readonly hooks?: ExtensionHooks<Defs, ContextDependencies<Uses>, MetaKeys>;
1038
1092
  }
1039
1093
  type ValidateExtensionConfig<Defs extends readonly NamedExtensionFlagDef[], Provides extends readonly AnyContextInstance[], Commands extends readonly CommandDefinition<any, any, any, any>[], Uses extends readonly AnyContextFactory[]> = {
@@ -1055,7 +1109,7 @@ interface Extension<Deps extends ContextMap = ContextMap, Provides extends reado
1055
1109
  readonly uses: readonly AnyContextFactory[];
1056
1110
  readonly provides?: Provides;
1057
1111
  readonly sections?: (snapshot: RootCommandSnapshot<MetaKeys>) => readonly ExtensionSectionContribution[];
1058
- readonly build?: (ctx: ExtensionBuildContext<MetaKeys>) => BuildArtifacts | void | Promise<BuildArtifacts | void>;
1112
+ readonly build?: (ctx: ExtensionBuildContext<MetaKeys>) => Awaitable<BuildArtifacts>;
1059
1113
  readonly hooks?: ExtensionHooks<any, Deps, MetaKeys>;
1060
1114
  readonly _deps?: Deps;
1061
1115
  }
@@ -1148,8 +1202,15 @@ interface Defining<T> {
1148
1202
  }
1149
1203
  /** @internal */
1150
1204
  type DefiningOf<T> = T extends Defining<unknown> ? T[typeof defining] : T;
1205
+ /**
1206
+ * Adapter hook: the Context sources a bag was built from, exposed as a
1207
+ * non-enumerable symbol property. Reading it never starts construction.
1208
+ */
1209
+ export declare const contextSources: unique symbol;
1151
1210
  /** Lazy, invocation-scoped Context values. Reading a property starts construction. */
1152
- type ContextBag<Deps extends ContextMap = {}> = { readonly [K in keyof Deps]: Promise<Deps[K]>; };
1211
+ type ContextBag<Deps extends ContextMap = {}> = { readonly [K in keyof Deps]: Promise<Deps[K]>; } & {
1212
+ readonly [contextSources]?: readonly (AnyContextFactory | AnyContextInstance)[];
1213
+ };
1153
1214
  interface ContextConfig {
1154
1215
  readonly flags?: readonly NamedFlagDef[];
1155
1216
  readonly uses?: readonly AnyContextFactory[];
@@ -1169,6 +1230,8 @@ interface ContextInstance<Name extends string = string, Value = unknown, OF exte
1169
1230
  readonly ownedFlags: FlagsDef;
1170
1231
  /** @internal — declared direct dependency factories */
1171
1232
  readonly uses: readonly AnyContextFactory[];
1233
+ /** @internal defining factory, for adapters that identify Contexts by factory */
1234
+ readonly factory: AnyContextFactory;
1172
1235
  setup(input: ContextSetupInput<OF>): Awaitable<Value>;
1173
1236
  readonly _ownedFlags?: OF;
1174
1237
  /** @internal — phantom carrying the transitive dependency closure */
@@ -1241,4 +1304,4 @@ export declare function defineFlag<const N extends string, const D extends FlagD
1241
1304
  /** Define and own one positional argument; layout belongs to its consuming command. */
1242
1305
  export declare function defineArg<const N extends string, const D extends UnnamedArgDef>(name: N & EmptyArgNameBrand<N>, def: D & LocalValueBrand<D>): Named<N, D>;
1243
1306
  //#endregion
1244
- export { type AnyContextFactory, type AnyCrust, type ArgDef, type ArgSnapshot, type ArgsDef, type BuildArtifacts, type BuildReport, type CommandConfig, type CommandDefinition, type CommandDefinitionBuilder, type CommandMeta, type CommandNotFoundErrorDetails, type CommandPath, type CommandSection, type CommandSectionInput, type CommandShape, type CommandShapeAt, type CommandSnapshot, type CommandTree, type ContextBag, type ContextConfig, type ContextFactory, type ContextInstance, type ContextMap, type ContextSetup, type CrustCommandContext, type CrustErrorCode, type CrustErrorDetails, type CrustErrorDetailsMap, type CrustErrorJson, type DefineExtensionWith, type DefinitionErrorDetails, type Extension, type ExtensionBuildContext, type ExtensionConfig, type ExtensionContext, type ExtensionFactory, type ExtensionFlagDef, type ExtensionHooks, type ExtensionId, type ExtensionSectionContribution, type FactoryValueOf, type Finished, type FlagDef, type FlagSnapshot, type FlagsDef, type InferExtensionFlags, type InputArgs, type InputFlags, type InvocationIO, type InvocationOutcome, type MergeContext, type MergeFlags, type NamedExtensionFlagDef, type NamedFlagDef, type ParseErrorDetails, type ParseResult, type ParsedArgValue, type ParsedFlagValue, type RootCommandMeta, type RootMetaKey, type RunArguments, type RunInput, type RunInputArguments, type RunOutcome, type SectionAudience, type SectionConsumer, type UnnamedArgDef, type ValidatedInput, type ValidationErrorDetails, type ValueType, defineExtensionId };
1307
+ export { type AnyContextFactory, type AnyCrust, type ArgDef, type ArgSnapshot, type ArgsDef, type BuildArtifacts, type BuildFile, type BuildReport, type CommandConfig, type CommandDefinition, type CommandDefinitionBuilder, type CommandHandle, type CommandMeta, type CommandNotFoundErrorDetails, type CommandPath, type CommandSection, type CommandSectionInput, type CommandShape, type CommandShapeAt, type CommandSnapshot, type CommandTree, type ContextBag, type ContextConfig, type ContextFactory, type ContextInstance, type ContextMap, type ContextSetup, type CrustCommandContext, type CrustErrorCode, type CrustErrorDetails, type CrustErrorDetailsMap, type CrustErrorJson, type DefineExtensionWith, type DefinitionErrorDetails, type Extension, type ExtensionBuildContext, type ExtensionConfig, type ExtensionContext, type ExtensionFactory, type ExtensionFlagDef, type ExtensionHooks, type ExtensionId, type ExtensionSectionContribution, type FactoryValueOf, type Finished, type FlagDef, type FlagSnapshot, type FlagsDef, type InferExtensionFlags, type InputArgs, type InputFlags, type InvocationIO, type InvocationOutcome, type MergeContext, type MergeFlags, type NamedExtensionFlagDef, type NamedFlagDef, type ParseErrorDetails, type ParseResult, type ParsedArgValue, type ParsedFlagValue, type RootCommandMeta, type RootMetaKey, type RunArguments, type RunInput, type RunInputArguments, type RunOutcome, type SectionAudience, type SectionConsumer, type UnnamedArgDef, type ValidatedInput, type ValidationErrorDetails, type ValueType, defineExtensionId, resolveArtifactDir };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as definingOf, a as runInvocation, b as normalizeFlag, d as installExtensionContexts, f as validateCommandSections, g as defineContext, h as defineExtension, i as prepareInvocation, m as registerFlag, o as snapshotCommand, p as createCommandNode, r as executeInvocation, u as cloneCommandNode, v as validateContextAvailability, x as CrustError, y as normalizeArg } from "./invocation-DcA5FqF7.js";
1
+ import { C as normalizeFlag, S as normalizeArg, _ as defineExtension, a as runInvocation, b as definingOf, d as resolveArtifactDir, f as cloneCommandNode, g as registerFlag, h as createCommandNode, i as resolveTypedPath, m as validateCommandSections, n as executeInvocation, o as snapshotCommand, p as installExtensionContexts, r as prepareInvocation, v as contextSources, w as CrustError, x as validateContextAvailability, y as defineContext } from "./invocation-CIvHShiW.js";
2
2
  //#region src/identity.ts
3
3
  /** Mint an Extension identity from any non-blank, trimmed string. */
4
4
  function defineExtensionId(id) {
@@ -173,8 +173,9 @@ var Crust = class {
173
173
  * The action receives a {@link CrustCommandContext} with `args` typed from
174
174
  * `.args()` and `flags` typed from the accumulated `Flags`.
175
175
  *
176
- * Calling `.action()` again replaces the command behavior on the new builder.
177
- * The original builder is not mutated.
176
+ * Calling `.action()` again replaces the command behavior and its Context obligations
177
+ * on the new builder. Later local providers must satisfy the bound Context value types;
178
+ * descendants may shadow independently. The original builder is not mutated.
178
179
  *
179
180
  * @param action - The Command Action function
180
181
  * @returns A new `Crust` instance with the action registered
@@ -224,9 +225,25 @@ var Crust = class {
224
225
  input: structuredInput
225
226
  }, io, materializeCommandDefinition);
226
227
  }
228
+ at(path) {
229
+ resolveTypedPath(prepareInvocation(this._node, materializeCommandDefinition).rootNode, path);
230
+ const node = this._node;
231
+ const boundPath = Object.freeze([...path]);
232
+ return {
233
+ path: boundPath,
234
+ async run(...args) {
235
+ const structuredInput = args[0] ?? {};
236
+ const io = args[1];
237
+ return await runInvocation(node, {
238
+ path: boundPath,
239
+ input: structuredInput
240
+ }, io, materializeCommandDefinition);
241
+ }
242
+ };
243
+ }
227
244
  async execute(options) {
228
245
  return await executeInvocation(this._node, options, materializeCommandDefinition);
229
246
  }
230
247
  };
231
248
  //#endregion
232
- export { Crust, CrustError, defineArg, defineCommand, defineContext, defineExtension, defineExtensionId, defineFlag };
249
+ export { Crust, CrustError, contextSources, defineArg, defineCommand, defineContext, defineExtension, defineExtensionId, defineFlag, resolveArtifactDir };
@@ -1,5 +1,7 @@
1
- import { writeFile } from "node:fs/promises";
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, join, posix, resolve, win32 } from "node:path";
3
+ import { existsSync, realpathSync, statSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
3
5
  import { AsyncLocalStorage } from "node:async_hooks";
4
6
  import { Writable } from "node:stream";
5
7
  import { parseArgs } from "node:util";
@@ -148,6 +150,11 @@ function definingOf(value) {
148
150
  function seal(value) {
149
151
  return Object.freeze(Object.assign(value, { [defining]: value }));
150
152
  }
153
+ /**
154
+ * Adapter hook: the Context sources a bag was built from, exposed as a
155
+ * non-enumerable symbol property. Reading it never starts construction.
156
+ */
157
+ const contextSources = Symbol.for("crust.contextSources");
151
158
  /** Check only declared availability; setup, callback values, and cycles belong to invocation. */
152
159
  function validateContextAvailability(contexts, sources) {
153
160
  const names = new Set(contexts.map((instance) => instance.name));
@@ -179,6 +186,7 @@ function defineContext(name, configOrSetup, maybeSetup) {
179
186
  name,
180
187
  ownedFlags,
181
188
  uses: instanceUses,
189
+ factory: sealed,
182
190
  setup: run
183
191
  });
184
192
  };
@@ -191,7 +199,8 @@ function defineContext(name, configOrSetup, maybeSetup) {
191
199
  factory.contextName = name;
192
200
  factory.uses = uses;
193
201
  factory.of = (value) => instance(Object.freeze([]), () => value);
194
- return seal(factory);
202
+ const sealed = seal(factory);
203
+ return sealed;
195
204
  }
196
205
  function isDisposeCallback(value) {
197
206
  return typeof value === "function";
@@ -370,6 +379,7 @@ function createContextResolver(contexts, io, disposal) {
370
379
  for (const dependency of source.uses ?? []) add(dependency);
371
380
  };
372
381
  for (const source of sources) add(source);
382
+ Object.defineProperty(bag, contextSources, { value: Object.freeze([...sources]) });
373
383
  return Object.freeze(bag);
374
384
  };
375
385
  return {
@@ -604,6 +614,83 @@ function installExtensionContexts(node, extensions, reRegisteredIds) {
604
614
  return cloned;
605
615
  }
606
616
  //#endregion
617
+ //#region ../utils/src/source.ts
618
+ /**
619
+ * Find the nearest directory containing `package.json` by walking up the
620
+ * filesystem from `startPath`.
621
+ *
622
+ * If `startPath` points at an existing file, its parent directory is used as
623
+ * the starting point. The directory walk uses `path.resolve()` (lexical), not
624
+ * `fs.realpath()`, so a symlink's parent chain is walked rather than the
625
+ * symlink target's parent chain.
626
+ *
627
+ * @param startPath - Directory or file path to start walking from.
628
+ * @returns The absolute path of the nearest enclosing directory containing
629
+ * `package.json`, or `null` if the filesystem root is reached first.
630
+ *
631
+ * @internal Shared by {@link resolveSourceDir} and `resolveArtifactDir`.
632
+ */
633
+ function findNearestPackageRoot(startPath) {
634
+ let current = resolve(startPath);
635
+ if (existsSync(current) && !statSync(current).isDirectory()) current = dirname(current);
636
+ while (true) {
637
+ if (existsSync(join(current, "package.json"))) return current;
638
+ const parent = dirname(current);
639
+ if (parent === current) return null;
640
+ current = parent;
641
+ }
642
+ }
643
+ //#endregion
644
+ //#region ../utils/src/artifacts.ts
645
+ /**
646
+ * Set by `crust build` while it prepares the Command Snapshot: the absolute
647
+ * entry-isolated directory Extension build hooks write into. Core reads it
648
+ * to run the hooks; `resolveArtifactDir` reads it so sections evaluated during
649
+ * that run see the artifacts being built instead of the wiped `.crust/root`.
650
+ */
651
+ const BUILD_OUT_DIR_ENV = "CRUST_INTERNAL_BUILD_OUT_DIR";
652
+ /** True inside a `bun build --compile` or `deno compile` executable. */
653
+ function isCompiledExecutable() {
654
+ const { Bun, Deno } = globalThis;
655
+ const bunMain = Bun?.main ?? "";
656
+ return bunMain.startsWith("/$bunfs/") || /^[A-Za-z]:[\\/]~BUN[\\/]/.test(bunMain) || Deno?.build?.standalone === true;
657
+ }
658
+ /**
659
+ * Absolute path of a build artifact or `crust.include` directory shipped with
660
+ * this CLI. `name` is a top-level directory name such as `"skills"`.
661
+ *
662
+ * The artifact path is computed from how the CLI is running — never probed:
663
+ * - Compiled executable (Bun or Deno): `<dir of the executable>/<name>`, which
664
+ * is a platform package's `bin/` or wherever the binary was placed.
665
+ * - Crust-built Node bundle: `<name>` next to the bundle's `bin/` directory,
666
+ * i.e. `.crust/root/<name>` in place and `<installed root>/<name>` after install.
667
+ * - Snapshot preparation inside `crust build`: `<build output dir>/<name>`, the
668
+ * artifacts earlier Extension build hooks wrote in this same build.
669
+ * - Source (`bun run`, `node`, `deno run`): `.crust/root/<name>` under the
670
+ * nearest package root of the real `process.argv[1]` entrypoint (following source
671
+ * links) — the output of the last `crust build`.
672
+ *
673
+ * @throws {Error} when `name` is not a single path segment, or in source mode
674
+ * when the entrypoint cannot be resolved or has no enclosing `package.json`.
675
+ */
676
+ function resolveArtifactDir(name) {
677
+ if (name === "" || name === "." || name === ".." || /[\\/]/.test(name)) throw new Error(`Artifact name must be a single directory name, got ${JSON.stringify(name)}.`);
678
+ if (isCompiledExecutable()) return join(dirname(process.execPath), name);
679
+ if (process.env.CRUST_INTERNAL_BUILD === "1") return resolve(fileURLToPath(import.meta.url), "..", "..", name);
680
+ const buildOutDir = process.env[BUILD_OUT_DIR_ENV];
681
+ if (buildOutDir) return join(buildOutDir, name);
682
+ const entrypoint = process.argv[1];
683
+ let sourceEntrypoint = entrypoint;
684
+ if (entrypoint) try {
685
+ sourceEntrypoint = realpathSync(entrypoint);
686
+ } catch (cause) {
687
+ throw new Error(`Could not resolve artifact "${name}": could not resolve source entrypoint "${entrypoint}".`, { cause });
688
+ }
689
+ const packageRoot = sourceEntrypoint ? findNearestPackageRoot(sourceEntrypoint) : null;
690
+ if (!packageRoot) throw new Error(`Could not resolve artifact "${name}": no package.json was found above ${entrypoint ? `entrypoint "${entrypoint}"` : "process.argv[1] (unset)"}.`);
691
+ return join(packageRoot, ".crust", "root", name);
692
+ }
693
+ //#endregion
607
694
  //#region ../utils/src/terminal.ts
608
695
  const STORAGE_KEY = Symbol.for("crustjs.terminal.io");
609
696
  const AMBIENT_CALLBACKS_KEY = Symbol.for("crustjs.terminal.ambient-callbacks");
@@ -733,11 +820,9 @@ function coerceJson(raw) {
733
820
  //#region src/parsing/parser.ts
734
821
  /**
735
822
  * Build the options config for `util.parseArgs` from the shared spelling table.
736
- * Also returns a reverse alias→name mapping for resolving parsed results.
737
823
  */
738
824
  function buildParseArgsOptionDescriptor(spellings) {
739
825
  const options = {};
740
- const aliasToName = {};
741
826
  for (const [spelling, entry] of spellings) {
742
827
  const descriptor = { type: entry.def.type === "boolean" ? "boolean" : "string" };
743
828
  if (entry.def.multiple) descriptor.multiple = true;
@@ -746,13 +831,9 @@ function buildParseArgsOptionDescriptor(spellings) {
746
831
  options[spelling] = descriptor;
747
832
  continue;
748
833
  }
749
- aliasToName[spelling] = entry.canonicalName;
750
834
  if (entry.kind === "alias") options[spelling] = descriptor;
751
835
  }
752
- return {
753
- options,
754
- aliasToName
755
- };
836
+ return options;
756
837
  }
757
838
  /**
758
839
  * Coerce a string value to the expected type based on the type literal.
@@ -848,12 +929,11 @@ function coerceFlagValue(name, def, parsed) {
848
929
  * must be `true`), and `multiple` flags spread across aliases would lose
849
930
  * their interleaved argv order.
850
931
  */
851
- function resolveAliases(tokens, aliasToName, flagsDef) {
932
+ function resolveAliases(tokens, spellings) {
852
933
  const canonical = {};
853
934
  for (const token of tokens) {
854
935
  if (token.kind !== "option") continue;
855
- const canonicalName = aliasToName[token.name] ?? token.name;
856
- const def = flagsDef[canonicalName];
936
+ const { canonicalName, def } = spellings.get(token.name);
857
937
  const existing = canonical[canonicalName];
858
938
  if (def.type === "boolean") {
859
939
  const value = !token.rawName.startsWith("--no-");
@@ -913,6 +993,12 @@ function resolveArgs(argsDef, positionals, coerce) {
913
993
  for (const def of argsDef) {
914
994
  const { name } = def;
915
995
  const label = `<${name}>`;
996
+ Object.defineProperty(resolved, name, {
997
+ value: void 0,
998
+ writable: true,
999
+ enumerable: true,
1000
+ configurable: true
1001
+ });
916
1002
  if (def.variadic) {
917
1003
  resolved[name] = positionals.slice(index).map((v, i) => coerce(def, v, label, i));
918
1004
  index = positionals.length;
@@ -947,7 +1033,7 @@ function validateNoNegateUsage(argv, spellings) {
947
1033
  }
948
1034
  function tokenizeArgv(command, argv) {
949
1035
  const spellings = command.flagSpellings;
950
- const { options: parseOptions, aliasToName } = buildParseArgsOptionDescriptor(spellings);
1036
+ const parseOptions = buildParseArgsOptionDescriptor(spellings);
951
1037
  validateNoNegateUsage(argv, spellings);
952
1038
  let parsed;
953
1039
  try {
@@ -979,7 +1065,7 @@ function tokenizeArgv(command, argv) {
979
1065
  }
980
1066
  return {
981
1067
  positionals: preSeparatorPositionals,
982
- flagValues: resolveAliases(parsed.tokens, aliasToName, command.effectiveFlags),
1068
+ flagValues: resolveAliases(parsed.tokens, spellings),
983
1069
  rawArgs
984
1070
  };
985
1071
  }
@@ -1180,8 +1266,7 @@ function normalizeStandardIssues(issues, prefix = []) {
1180
1266
  //#endregion
1181
1267
  //#region src/parsing/schema.ts
1182
1268
  async function runSchema(schema, raw, path, issues) {
1183
- let result = schema["~standard"].validate(raw);
1184
- if (result instanceof Promise) result = await result;
1269
+ const result = await schema["~standard"].validate(raw);
1185
1270
  if (result.issues) {
1186
1271
  issues.push(...normalizeStandardIssues(result.issues, path));
1187
1272
  return { ok: false };
@@ -1374,8 +1459,8 @@ function matchKnownFlagToken(spellings, token) {
1374
1459
  * Resolve a command from an argv array by walking the subcommand tree.
1375
1460
  *
1376
1461
  * Subcommand matching happens BEFORE flag parsing, so:
1377
- * `crust build --entry src/cli.ts` first resolves "build" as a subcommand,
1378
- * then passes `["--entry", "src/cli.ts"]` to the build command's parser.
1462
+ * `crust build --target host` first resolves "build" as a subcommand,
1463
+ * then passes `["--target", "host"]` to the build command's parser.
1379
1464
  *
1380
1465
  * Resolution rules:
1381
1466
  * 1. If `argv[0]` matches a subcommand key, recurse into that subcommand
@@ -1545,7 +1630,6 @@ const DEFAULT_IO = {
1545
1630
  * without dispatching a Command Action. In-process callers use `Crust.snapshot()`.
1546
1631
  */
1547
1632
  const SNAPSHOT_PATH_ENV = "CRUST_INTERNAL_SNAPSHOT_PATH";
1548
- const BUILD_OUT_DIR_ENV = "CRUST_INTERNAL_BUILD_OUT_DIR";
1549
1633
  const EXIT_CODE_CANCELLED = 130;
1550
1634
  function isAbortError(error) {
1551
1635
  if (!(error instanceof Error)) return false;
@@ -1611,7 +1695,8 @@ function resolveArgvInput(root, argv) {
1611
1695
  parsed: parseArgs$1(route.command, route.argv)
1612
1696
  };
1613
1697
  }
1614
- function resolveStructuredInput(root, path, input) {
1698
+ /** Resolve a typed path, rejecting any element the router cannot consume as a command. */
1699
+ function resolveTypedPath(root, path) {
1615
1700
  const route = resolveCommand(root, [...path]);
1616
1701
  if (route.argv.length > 0) {
1617
1702
  const candidate = route.argv[0];
@@ -1623,6 +1708,10 @@ function resolveStructuredInput(root, path, input) {
1623
1708
  parentCommand
1624
1709
  });
1625
1710
  }
1711
+ return route;
1712
+ }
1713
+ function resolveStructuredInput(root, path, input) {
1714
+ const route = resolveTypedPath(root, path);
1626
1715
  return {
1627
1716
  argv: path,
1628
1717
  route,
@@ -1805,16 +1894,31 @@ async function executeInvocation(node, options, materializeCommandDefinition) {
1805
1894
  const buildOutDir = process.env[BUILD_OUT_DIR_ENV];
1806
1895
  if (buildOutDir) {
1807
1896
  const extensions = [];
1897
+ const owners = /* @__PURE__ */ new Map();
1808
1898
  for (const extension of base.extensions) {
1809
1899
  if (!extension.build) continue;
1810
1900
  try {
1811
- const artifacts = await extension.build({
1812
- snapshot,
1813
- outDir: buildOutDir
1901
+ const files = (await extension.build({ snapshot })).map((file) => {
1902
+ const path = normalizeArtifactPath(file.path);
1903
+ const key = path.toLowerCase();
1904
+ for (const [ownedKey, owner] of owners) if (ownedKey === key || ownedKey.startsWith(`${key}/`) || key.startsWith(`${ownedKey}/`)) throw new Error(`Artifact path "${path}" collides with "${owner.path}" produced by Extension "${owner.id}".`);
1905
+ owners.set(key, {
1906
+ id: extension.id,
1907
+ path
1908
+ });
1909
+ return {
1910
+ path,
1911
+ content: file.content
1912
+ };
1814
1913
  });
1914
+ for (const file of files) {
1915
+ const target = join(buildOutDir, file.path);
1916
+ await mkdir(dirname(target), { recursive: true });
1917
+ await writeFile(target, file.content);
1918
+ }
1815
1919
  extensions.push({
1816
1920
  id: extension.id,
1817
- files: artifacts === void 0 ? "unknown" : artifacts.map(normalizeArtifactPath)
1921
+ files: files.map((file) => file.path)
1818
1922
  });
1819
1923
  } catch (error) {
1820
1924
  const message = error instanceof Error ? error.message : String(error);
@@ -1872,4 +1976,4 @@ async function executeInvocation(node, options, materializeCommandDefinition) {
1872
1976
  return await (hasInjectedIO(options?.io) ? withAmbientTerminalIO(io, invoke) : invoke());
1873
1977
  }
1874
1978
  //#endregion
1875
- export { definingOf as _, runInvocation as a, normalizeFlag as b, sectionsFor as c, installExtensionContexts as d, validateCommandSections as f, defineContext as g, defineExtension as h, prepareInvocation as i, visibleSectionsFor as l, registerFlag as m, SNAPSHOT_PATH_ENV as n, snapshotCommand as o, createCommandNode as p, executeInvocation as r, isListed as s, BUILD_OUT_DIR_ENV as t, cloneCommandNode as u, validateContextAvailability as v, CrustError as x, normalizeArg as y };
1979
+ export { normalizeFlag as C, normalizeArg as S, defineExtension as _, runInvocation as a, definingOf as b, sectionsFor as c, resolveArtifactDir as d, cloneCommandNode as f, registerFlag as g, createCommandNode as h, resolveTypedPath as i, visibleSectionsFor as l, validateCommandSections as m, executeInvocation as n, snapshotCommand as o, installExtensionContexts as p, prepareInvocation as r, isListed as s, SNAPSHOT_PATH_ENV as t, BUILD_OUT_DIR_ENV as u, contextSources as v, CrustError as w, validateContextAvailability as x, defineContext as y };
package/dist/tooling.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { D as CommandSnapshot, O as FlagSnapshot, V as ExtensionId, i as CommandSection, o as DeclaredDefault } from "./types-DjMHz7M6.js";
1
+ import { A as FlagSnapshot, G as ExtensionId, c as DeclaredDefault, k as CommandSnapshot, o as CommandSection, t as BUILD_OUT_DIR_ENV } from "./artifacts-D8Jlug-a.js";
2
2
  //#region src/command/invocation.d.ts
3
3
  /**
4
4
  * Snapshot subprocess protocol used by first-party build tooling.
@@ -9,7 +9,6 @@ import { D as CommandSnapshot, O as FlagSnapshot, V as ExtensionId, i as Command
9
9
  * without dispatching a Command Action. In-process callers use `Crust.snapshot()`.
10
10
  */
11
11
  export declare const SNAPSHOT_PATH_ENV = "CRUST_INTERNAL_SNAPSHOT_PATH";
12
- export declare const BUILD_OUT_DIR_ENV = "CRUST_INTERNAL_BUILD_OUT_DIR";
13
12
  //#endregion
14
13
  //#region src/command/documentation.d.ts
15
14
  /** Format a declared default value for command documentation. */
@@ -157,4 +156,4 @@ export declare function visibleSectionsFor(snapshot: CommandSnapshot, consumer:
157
156
  readonly sections: readonly CommandSection[];
158
157
  }[];
159
158
  //#endregion
160
- export type { CommandDocumentation, CommandSnapshot, DocumentationArg, DocumentationFlag, UsageSegment };
159
+ export { BUILD_OUT_DIR_ENV, type CommandDocumentation, type CommandSnapshot, type DocumentationArg, type DocumentationFlag, type UsageSegment };
package/dist/tooling.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c as sectionsFor, l as visibleSectionsFor, n as SNAPSHOT_PATH_ENV, s as isListed, t as BUILD_OUT_DIR_ENV } from "./invocation-DcA5FqF7.js";
1
+ import { c as sectionsFor, l as visibleSectionsFor, s as isListed, t as SNAPSHOT_PATH_ENV, u as BUILD_OUT_DIR_ENV } from "./invocation-CIvHShiW.js";
2
2
  //#region src/command/documentation.ts
3
3
  function isNonFiniteNumber(value) {
4
4
  return typeof value === "number" && !Number.isFinite(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crustjs/core",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Core library for the Crust CLI framework",
5
5
  "type": "module",
6
6
  "sideEffects": false,