@crustjs/core 0.3.2 → 0.3.4

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.
@@ -536,6 +536,12 @@ interface CrustCommandContext<A extends ArgsDef = ArgsDef, F extends FlagsDef =
536
536
  ctx: ContextBag<Ctx>;
537
537
  /** Raw arguments that appeared after the `--` separator */
538
538
  rawArgs: string[];
539
+ /**
540
+ * Aborted when the caller cancels the invocation (`execute({ signal })`,
541
+ * `run(path, input, { signal })`, or the first `SIGINT` under `execute()`);
542
+ * see `ExecuteOptions.signal` for how the abort reason maps to the exit code.
543
+ */
544
+ signal: AbortSignal;
539
545
  /** Readonly, serializable snapshot of the resolved command */
540
546
  command: CommandSnapshot;
541
547
  /** Readonly snapshot of the application root, including Extension contributions */
@@ -580,18 +586,18 @@ type CompatibleRunValue<Expected, Actual> = Actual extends Expected ? Actual ext
580
586
  type CompatibleRunBranch<Expected, Actual> = Actual extends Expected ? Actual : Expected extends readonly (infer Item)[] ? JsonValue extends Item ? Actual extends (Expected extends readonly [unknown, ...unknown[]] ? readonly [unknown, ...unknown[]] : readonly unknown[]) ? Actual extends JsonCompatible<Actual> ? Actual : never : never : never : Actual extends object ? string extends keyof Expected ? { [K in keyof Actual]: CompatibleRunValue<Exclude<Expected[K & keyof Expected], undefined>, Actual[K]>; } : { [K in keyof Expected]: K extends keyof Actual ? CompatibleRunValue<Exclude<Expected[K], undefined>, Actual[K]> : Expected[K]; } & { [K in Exclude<keyof Actual, keyof Expected>]: never; } : never;
581
587
  type CompatibleRunInput<Shape extends CommandShape, Input> = CompatibleRunValue<RunInput<Shape>, Input>;
582
588
  type RunInputArguments<Shape extends CommandShape> = {} extends RunInput<Shape> ? readonly [input?: RunInput<Shape>] : readonly [input: RunInput<Shape>];
583
- type RunArguments<Shape extends CommandShape> = readonly [...RunInputArguments<Shape>, io?: Partial<InvocationIO>];
589
+ type RunArguments<Shape extends CommandShape> = readonly [...RunInputArguments<Shape>, options?: InvocationOptions];
584
590
  /**
585
591
  * Typed invoker bound to one command in an app, returned by {@link Crust.at}.
586
592
  *
587
- * `run` accepts the same structured input and IO as `Crust.run` with the path
588
- * already applied, so a handle can be re-exported as a plain typed function.
593
+ * `run` accepts the same structured input and options as `Crust.run` with the
594
+ * path already applied, so a handle can be re-exported as a plain typed function.
589
595
  */
590
596
  interface CommandHandle<Shape extends CommandShape> {
591
597
  /** The typed path this handle was created with (`[]` selects the root). */
592
598
  readonly path: readonly string[];
593
599
  run(...args: RunArguments<Shape>): Promise<RunOutcome<Shape["result"]>>;
594
- run<const Input>(input: Input, ...validation: [Input] extends [CompatibleRunInput<Shape, Input>] ? readonly [io?: Partial<InvocationIO>] : readonly [invalidInput: never]): Promise<RunOutcome<Shape["result"]>>;
600
+ run<const Input>(input: Input, ...validation: [Input] extends [CompatibleRunInput<Shape, Input>] ? readonly [options?: InvocationOptions] : readonly [invalidInput: never]): Promise<RunOutcome<Shape["result"]>>;
595
601
  }
596
602
  /** Static configuration for a reusable command definition. */
597
603
  interface CommandConfig extends Omit<CommandMeta, "name" | "sections" | "version"> {
@@ -924,8 +930,9 @@ declare class Crust<Flags extends FlagsDef = {}, A extends ArgsDef = [], Ctx ext
924
930
  *
925
931
  * @param path - Typed path to the command to invoke (`[]` selects the root)
926
932
  * @param input - Structured argument, flag, and raw values
927
- * @param io - Optional `stdout(text)` / `stderr(text)` callbacks, also
928
- * exposed to Command Actions and Extensions
933
+ * @param options - Optional `stdout(text)` / `stderr(text)` callbacks (also
934
+ * exposed to Command Actions and Extensions) and an
935
+ * `AbortSignal` that cancels the invocation
929
936
  */
930
937
  run<const Path extends CommandPath<Tree>>(this: {
931
938
  readonly _types: {
@@ -936,7 +943,7 @@ declare class Crust<Flags extends FlagsDef = {}, A extends ArgsDef = [], Ctx ext
936
943
  readonly _types: {
937
944
  readonly caps: "app";
938
945
  };
939
- }, 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"]>>;
946
+ }, path: Path & KnownCommandPath<Path, Tree>, input: Input, ...validation: [Input] extends [CompatibleRunInput<CommandShapeAt<CommandShape<A, Flags, Tree, Result>, NoInfer<Path>>, Input>] ? readonly [options?: InvocationOptions] : readonly [invalidInput: never]): Promise<RunOutcome<CommandShapeAt<CommandShape<A, Flags, Tree, Result>, Path>["result"]>>;
940
947
  /**
941
948
  * Bind a typed command path into a reusable {@link CommandHandle}.
942
949
  *
@@ -962,19 +969,16 @@ declare class Crust<Flags extends FlagsDef = {}, A extends ArgsDef = [], Ctx ext
962
969
  * Core's default renderer), sets `process.exitCode` (`1`, or
963
970
  * `130` for an `AbortError` cancellation), and resolves to the exit code.
964
971
  *
965
- * @param options - Optional overrides (e.g. custom `argv` and captured
966
- * `io` for in-process testing of exit codes and
967
- * rendered failures)
972
+ * @param options - Optional overrides: custom `argv` and captured `io` for
973
+ * in-process testing of exit codes and rendered failures,
974
+ * and a caller `signal` that cancels the invocation
968
975
  * @returns The terminal exit code (`0`, `1`, or `130` for cancellation)
969
976
  */
970
977
  execute(this: {
971
978
  readonly _types: {
972
979
  readonly caps: "app";
973
980
  };
974
- }, options?: {
975
- argv?: string[];
976
- io?: Partial<InvocationIO>;
977
- }): Promise<number>;
981
+ }, options?: ExecuteOptions): Promise<number>;
978
982
  }
979
983
  //#endregion
980
984
  //#region src/errors.d.ts
@@ -1195,6 +1199,12 @@ interface ExtensionContext<Defs extends readonly NamedExtensionFlagDef[] = [], D
1195
1199
  * @example `["--dry-run"]`
1196
1200
  */
1197
1201
  readonly rawArgs: readonly string[];
1202
+ /**
1203
+ * Aborted when the caller cancels the invocation (`execute({ signal })`,
1204
+ * `run(path, input, { signal })`, or the first `SIGINT` under `execute()`);
1205
+ * see `ExecuteOptions.signal` for how the abort reason maps to the exit code.
1206
+ */
1207
+ readonly signal: AbortSignal;
1198
1208
  /** Declared Contexts, constructed lazily on first property access. */
1199
1209
  readonly ctx: ContextBag<Deps>;
1200
1210
  /**
@@ -1333,11 +1343,17 @@ declare function defineExtension<Args extends readonly unknown[], const Defs ext
1333
1343
  declare function defineExtension<const Defs extends readonly NamedExtensionFlagDef[] = [], const Uses extends readonly AnyContextFactory[] = [], const Provides extends readonly AnyContextInstance[] = [], const Commands extends readonly CommandDefinition<any, any, any, any>[] = []>(id: ExtensionId, config?: ExtensionConfig<Defs, Uses, Provides, Commands> & ValidateExtensionConfig<Defs, Provides, Commands, Uses>): Extension<ContextDependencies<Uses> & ContextsDependencies<Provides> & CommandDefinitionsDependencies<Commands>, Provides, Defs, Commands, never, ContextDependencies<Uses>>;
1334
1344
  //#endregion
1335
1345
  //#region src/parsing/spellings.d.ts
1346
+ /**
1347
+ * One spelling of an effective flag. Entries are immutable and `def` is the
1348
+ * same object as `node.effectiveFlags[canonicalName]`, so node clones may
1349
+ * share entries and only copy the Map container. Writers (`registerFlag`)
1350
+ * always create fresh entries; never mutate one in place.
1351
+ */
1336
1352
  interface FlagSpelling {
1337
- canonicalName: string;
1338
- def: FlagDef;
1339
- kind: "canonical" | "short" | "alias";
1340
- negatable: boolean;
1353
+ readonly canonicalName: string;
1354
+ readonly def: FlagDef;
1355
+ readonly kind: "canonical" | "short" | "alias";
1356
+ readonly negatable: boolean;
1341
1357
  }
1342
1358
  //#endregion
1343
1359
  //#region src/command/node.d.ts
@@ -1396,6 +1412,29 @@ interface InvocationIO {
1396
1412
  /** Write a line of diagnostic output (injectable text callback) */
1397
1413
  stderr: (text: string) => void;
1398
1414
  }
1415
+ /** Caller options for one programmatic `run()` invocation: IO callbacks plus cancellation. */
1416
+ interface InvocationOptions extends Partial<InvocationIO> {
1417
+ /** Aborting it aborts the invocation's `ctx.signal` with the same reason. */
1418
+ signal?: AbortSignal;
1419
+ }
1420
+ /**
1421
+ * Caller options for the terminal `execute()` boundary. While an invocation
1422
+ * runs, the first `SIGINT` (Ctrl-C) aborts its `ctx.signal` with an `AbortError`
1423
+ * and a second `SIGINT` terminates unless another listener owns the signal.
1424
+ */
1425
+ interface ExecuteOptions {
1426
+ /** Arguments to parse; defaults to `process.argv.slice(2)`. */
1427
+ argv?: string[];
1428
+ /** Captured `stdout(text)` / `stderr(text)` callbacks (e.g. in-process tests). */
1429
+ io?: Partial<InvocationIO>;
1430
+ /**
1431
+ * Caller-owned cancellation; aborting it aborts the invocation's `ctx.signal`
1432
+ * with the same reason. If work throws that reason, an `AbortError` exits `130`
1433
+ * and other errors exit `1`. Effect signal interruption instead becomes an
1434
+ * `AbortError`, regardless of the caller's reason.
1435
+ */
1436
+ signal?: AbortSignal;
1437
+ }
1399
1438
  /**
1400
1439
  * Supported type literals for args and flags.
1401
1440
  *
@@ -1992,6 +2031,7 @@ type ValidateContextConfig<R extends ContextConfig> = {
1992
2031
  readonly uses?: R["uses"] extends readonly AnyContextFactory[] ? R["uses"] : "uses" extends keyof R ? {} : never;
1993
2032
  };
1994
2033
  interface ContextSetupInput<OF extends FlagsDef = FlagsDef> extends InvocationIO {
2034
+ readonly signal: AbortSignal;
1995
2035
  readonly flags: InferFlags<OF>;
1996
2036
  readonly ctx: ContextBag<ContextMap>;
1997
2037
  readonly defer: (cleanup: () => void | PromiseLike<void>) => void;
@@ -2015,6 +2055,8 @@ type AnyContextInstance = ContextInstance<string, unknown, any, any>;
2015
2055
  type ContextValue = Awaited<ReturnType<AnyContextInstance["setup"]>>;
2016
2056
  interface ContextSetup<Options, OF extends FlagsDef = {}, Deps extends ContextMap = {}> extends InvocationIO {
2017
2057
  readonly options: Options;
2058
+ /** The invocation's cancellation signal; pass it to cancellable setup work. */
2059
+ readonly signal: AbortSignal;
2018
2060
  readonly flags: InferFlags<OF>;
2019
2061
  readonly ctx: ContextBag<Deps>;
2020
2062
  /**
@@ -2056,4 +2098,4 @@ declare function defineContext<Name extends string, Value, Options = void>(name:
2056
2098
  declare function defineContext<Name extends string, const R extends ContextConfig, Value, Options = void>(name: Name, config: R & ValidateContextConfig<R> & ContextConfig, setup: (input: ContextSetup<Options, OwnedFlagsOf<R>, ContextDependencies<UsesOf<R>>>) => Awaitable<Value>): ContextFactory<Name, Options, Value, OwnedFlagsOf<R>, ContextDependencies<UsesOf<R>>>;
2057
2099
  type FactoryValueOf<F extends AnyContextFactory> = F extends ContextFactory<any, any, infer Value, any, any> ? Awaited<Value> : never;
2058
2100
  //#endregion
2059
- export { CrustErrorDetailsMap as $, ValueType as A, defineExtensionId as At, ExtensionFlagDef as B, NamedFlagDef as C, FlagSnapshot as Ct, SectionAudience as D, LocalValueBrand as Dt, ParsedFlagValue as E, EmptyArgNameBrand as Et, Extension as F, InvocationOutcome as G, ExtensionSectionContribution as H, ExtensionBuildContext as I, defineExtension as J, NamedExtensionFlagDef as K, ExtensionConfig as L, BuildFile as M, BuildReport as N, SectionConsumer as O, MergeContext as Ot, DefineExtensionWith as P, CrustErrorDetails as Q, ExtensionContext as R, MergeFlags as S, CommandSnapshot as St, ParsedArgValue as T, LocalFlagNameBrand as Tt, Finished as U, ExtensionHooks as V, InferExtensionFlags as W, CrustError as X, CommandNotFoundErrorDetails as Y, CrustErrorCode as Z, FlagDef as _, RunInput as _t, ContextInstance as a, CommandConfig as at, InputFlags as b, defineCommand as bt, FactoryValueOf as c, CommandHandle as ct, ArgDef as d, CommandShapeAt as dt, CrustErrorJson as et, ArgsDef as f, CommandTree as ft, DeclaredDefault as g, RunArguments as gt, CommandSectionInput as h, RootCommandMeta as ht, ContextFactory as i, AnyCrust as it, BuildArtifacts as j, ValidatedInput as k, ExtensionId as kt, contextSources as l, CommandPath as lt, CommandSection as m, CrustCommandContext as mt, ContextBag as n, ParseErrorDetails as nt, ContextMap as o, CommandDefinition as ot, CommandMeta as p, Crust as pt, RootMetaKey as q, ContextConfig as r, ValidationErrorDetails as rt, ContextSetup as s, CommandDefinitionBuilder as st, AnyContextFactory as t, DefinitionErrorDetails as tt, defineContext as u, CommandShape as ut, FlagsDef as v, RunInputArguments as vt, ParseResult as w, LocalFlagBrand as wt, InvocationIO as x, ArgSnapshot as xt, InputArgs as y, RunOutcome as yt, ExtensionFactory as z };
2101
+ export { CrustErrorCode as $, SectionConsumer as A, MergeContext as At, ExtensionContext as B, InvocationOptions as C, ArgSnapshot as Ct, ParsedArgValue as D, LocalFlagNameBrand as Dt, ParseResult as E, LocalFlagBrand as Et, BuildReport as F, Finished as G, ExtensionFlagDef as H, DefineExtensionWith as I, NamedExtensionFlagDef as J, InferExtensionFlags as K, Extension as L, ValueType as M, defineExtensionId as Mt, BuildArtifacts as N, ParsedFlagValue as O, EmptyArgNameBrand as Ot, BuildFile as P, CrustError as Q, ExtensionBuildContext as R, InvocationIO as S, defineCommand as St, NamedFlagDef as T, FlagSnapshot as Tt, ExtensionHooks as U, ExtensionFactory as V, ExtensionSectionContribution as W, defineExtension as X, RootMetaKey as Y, CommandNotFoundErrorDetails as Z, ExecuteOptions as _, RootCommandMeta as _t, ContextInstance as a, ValidationErrorDetails as at, InputArgs as b, RunInputArguments as bt, FactoryValueOf as c, CommandDefinition as ct, ArgDef as d, CommandPath as dt, CrustErrorDetails as et, ArgsDef as f, CommandShape as ft, DeclaredDefault as g, CrustCommandContext as gt, CommandSectionInput as h, Crust as ht, ContextFactory as i, ParseErrorDetails as it, ValidatedInput as j, ExtensionId as jt, SectionAudience as k, LocalValueBrand as kt, contextSources as l, CommandDefinitionBuilder as lt, CommandSection as m, CommandTree as mt, ContextBag as n, CrustErrorJson as nt, ContextMap as o, AnyCrust as ot, CommandMeta as p, CommandShapeAt as pt, InvocationOutcome as q, ContextConfig as r, DefinitionErrorDetails as rt, ContextSetup as s, CommandConfig as st, AnyContextFactory as t, CrustErrorDetailsMap as tt, defineContext as u, CommandHandle as ut, FlagDef as v, RunArguments as vt, MergeFlags as w, CommandSnapshot as wt, InputFlags as x, RunOutcome as xt, FlagsDef as y, RunInput as yt, ExtensionConfig as z };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as CrustErrorDetailsMap, A as ValueType, At as defineExtensionId, B as ExtensionFlagDef, C as NamedFlagDef, Ct as FlagSnapshot, D as SectionAudience, Dt as LocalValueBrand, E as ParsedFlagValue, Et as EmptyArgNameBrand, F as Extension, G as InvocationOutcome, H as ExtensionSectionContribution, I as ExtensionBuildContext, J as defineExtension, K as NamedExtensionFlagDef, L as ExtensionConfig, M as BuildFile, N as BuildReport, O as SectionConsumer, Ot as MergeContext, P as DefineExtensionWith, Q as CrustErrorDetails, R as ExtensionContext, S as MergeFlags, St as CommandSnapshot, T as ParsedArgValue, Tt as LocalFlagNameBrand, U as Finished, V as ExtensionHooks, W as InferExtensionFlags, X as CrustError, Y as CommandNotFoundErrorDetails, Z as CrustErrorCode, _ as FlagDef, _t as RunInput, a as ContextInstance, at as CommandConfig, b as InputFlags, bt as defineCommand, c as FactoryValueOf, ct as CommandHandle, d as ArgDef, dt as CommandShapeAt, et as CrustErrorJson, f as ArgsDef, ft as CommandTree, gt as RunArguments, h as CommandSectionInput, ht as RootCommandMeta, i as ContextFactory, it as AnyCrust, j as BuildArtifacts, k as ValidatedInput, kt as ExtensionId, l as contextSources, lt as CommandPath, m as CommandSection, mt as CrustCommandContext, n as ContextBag, nt as ParseErrorDetails, o as ContextMap, ot as CommandDefinition, p as CommandMeta, pt as Crust, q as RootMetaKey, r as ContextConfig, rt as ValidationErrorDetails, s as ContextSetup, st as CommandDefinitionBuilder, t as AnyContextFactory, tt as DefinitionErrorDetails, u as defineContext, ut as CommandShape, v as FlagsDef, vt as RunInputArguments, w as ParseResult, wt as LocalFlagBrand, x as InvocationIO, xt as ArgSnapshot, y as InputArgs, yt as RunOutcome, z as ExtensionFactory } from "./context-Cud9frNp.js";
1
+ import { $ as CrustErrorCode, A as SectionConsumer, At as MergeContext, B as ExtensionContext, C as InvocationOptions, Ct as ArgSnapshot, D as ParsedArgValue, Dt as LocalFlagNameBrand, E as ParseResult, Et as LocalFlagBrand, F as BuildReport, G as Finished, H as ExtensionFlagDef, I as DefineExtensionWith, J as NamedExtensionFlagDef, K as InferExtensionFlags, L as Extension, M as ValueType, Mt as defineExtensionId, N as BuildArtifacts, O as ParsedFlagValue, Ot as EmptyArgNameBrand, P as BuildFile, Q as CrustError, R as ExtensionBuildContext, S as InvocationIO, St as defineCommand, T as NamedFlagDef, Tt as FlagSnapshot, U as ExtensionHooks, V as ExtensionFactory, W as ExtensionSectionContribution, X as defineExtension, Y as RootMetaKey, Z as CommandNotFoundErrorDetails, _ as ExecuteOptions, _t as RootCommandMeta, a as ContextInstance, at as ValidationErrorDetails, b as InputArgs, bt as RunInputArguments, c as FactoryValueOf, ct as CommandDefinition, d as ArgDef, dt as CommandPath, et as CrustErrorDetails, f as ArgsDef, ft as CommandShape, gt as CrustCommandContext, h as CommandSectionInput, ht as Crust, i as ContextFactory, it as ParseErrorDetails, j as ValidatedInput, jt as ExtensionId, k as SectionAudience, kt as LocalValueBrand, l as contextSources, lt as CommandDefinitionBuilder, m as CommandSection, mt as CommandTree, n as ContextBag, nt as CrustErrorJson, o as ContextMap, ot as AnyCrust, p as CommandMeta, pt as CommandShapeAt, q as InvocationOutcome, r as ContextConfig, rt as DefinitionErrorDetails, s as ContextSetup, st as CommandConfig, t as AnyContextFactory, tt as CrustErrorDetailsMap, u as defineContext, ut as CommandHandle, v as FlagDef, vt as RunArguments, w as MergeFlags, wt as CommandSnapshot, x as InputFlags, xt as RunOutcome, y as FlagsDef, yt as RunInput, z as ExtensionConfig } from "./context-BPR700gY.js";
2
2
  import { resolveArtifactDir } from "@crustjs/utils/artifacts";
3
3
  //#region src/api/flags.d.ts
4
4
  /** Distribute `Omit<_, "name">` over the {@link ArgDef} union. */
@@ -20,4 +20,4 @@ export declare function defineFlag<const N extends string, const D extends FlagD
20
20
  /** Define and own one positional argument; layout belongs to its consuming command. */
21
21
  export declare function defineArg<const N extends string, const D extends UnnamedArgDef>(name: N & EmptyArgNameBrand<N>, def: D & LocalValueBrand<D>): Named<N, D>;
22
22
  //#endregion
23
- 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, Crust, type CrustCommandContext, CrustError, 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, contextSources, defineCommand, defineContext, defineExtension, defineExtensionId, resolveArtifactDir };
23
+ 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, Crust, type CrustCommandContext, CrustError, type CrustErrorCode, type CrustErrorDetails, type CrustErrorDetailsMap, type CrustErrorJson, type DefineExtensionWith, type DefinitionErrorDetails, type ExecuteOptions, 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 InvocationOptions, 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, contextSources, defineCommand, defineContext, defineExtension, defineExtensionId, resolveArtifactDir };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { S as CrustError, _ as defineContext, a as runInvocation, b as normalizeArg, d as installExtensionContexts, f as validateCommandSections, g as contextSources, h as defineExtension, i as resolveTypedPath, m as registerFlag, n as executeInvocation, o as snapshotCommand, p as createCommandNode, r as prepareInvocation, u as cloneCommandNode, v as definingOf, x as normalizeFlag, y as validateContextAvailability } from "./invocation-B17RvTLb.js";
1
+ import { C as CrustError, S as normalizeFlag, _ as contextSources, a as runInvocation, b as validateContextAvailability, d as cloneFlagRegistry, f as installExtensionContexts, g as defineExtension, h as registerFlag, i as resolveTypedPath, m as createCommandNode, n as executeInvocation, o as snapshotCommand, p as validateCommandSections, r as prepareInvocation, u as cloneCommandNode, v as defineContext, x as normalizeArg, y as definingOf } from "./invocation-BaWf5POY.js";
2
2
  import { resolveArtifactDir } from "@crustjs/utils/artifacts";
3
3
  //#region src/identity.ts
4
4
  /** Mint an Extension identity from any non-blank, trimmed string. */
@@ -121,18 +121,14 @@ var Crust = class {
121
121
  _clone(nodeOverrides) {
122
122
  const cloned = Object.create(Object.getPrototypeOf(this));
123
123
  cloned._node = {
124
- ...cloneCommandNode({
125
- ...this._node,
126
- subCommands: {}
127
- }),
128
- subCommands: { ...this._node.subCommands },
124
+ ...this._node,
129
125
  ...nodeOverrides
130
126
  };
131
127
  cloned._ancestorOwnedFlags = this._ancestorOwnedFlags;
132
128
  return cloned;
133
129
  }
134
130
  flags(...defs) {
135
- const cloned = this._clone({});
131
+ const cloned = this._clone(cloneFlagRegistry(this._node));
136
132
  for (const def of defs) {
137
133
  const { name, ...rest } = def;
138
134
  registerFlag(cloned._node, name, rest, "local");
@@ -163,7 +159,10 @@ var Crust = class {
163
159
  provide(...inputs) {
164
160
  const instances = inputs.map(definingOf);
165
161
  validateContextAvailability([...this._node.contexts.map(({ instance }) => instance), ...instances], instances);
166
- const cloned = this._clone({ contexts: [...this._node.contexts, ...instances.map((instance) => ({ instance }))] });
162
+ const cloned = this._clone({
163
+ ...cloneFlagRegistry(this._node),
164
+ contexts: [...this._node.contexts, ...instances.map((instance) => ({ instance }))]
165
+ });
167
166
  for (const instance of instances) for (const [name, definition] of Object.entries(instance.ownedFlags)) registerFlag(cloned._node, name, definition, "owned");
168
167
  return cloned;
169
168
  }
@@ -220,11 +219,11 @@ var Crust = class {
220
219
  }
221
220
  async run(path, ...args) {
222
221
  const structuredInput = args[0] ?? {};
223
- const io = args[1];
222
+ const options = args[1];
224
223
  return await runInvocation(this._node, {
225
224
  path,
226
225
  input: structuredInput
227
- }, io, materializeCommandDefinition);
226
+ }, options, materializeCommandDefinition);
228
227
  }
229
228
  at(path) {
230
229
  resolveTypedPath(prepareInvocation(this._node, materializeCommandDefinition).rootNode, path);
@@ -234,11 +233,11 @@ var Crust = class {
234
233
  path: boundPath,
235
234
  async run(...args) {
236
235
  const structuredInput = args[0] ?? {};
237
- const io = args[1];
236
+ const options = args[1];
238
237
  return await runInvocation(node, {
239
238
  path: boundPath,
240
239
  input: structuredInput
241
- }, io, materializeCommandDefinition);
240
+ }, options, materializeCommandDefinition);
242
241
  }
243
242
  };
244
243
  }
@@ -234,6 +234,7 @@ var FallbackAsyncDisposableStack = class {
234
234
  this.#entries.push(onDisposeAsync);
235
235
  }
236
236
  async [Symbol.asyncDispose]() {
237
+ if (this.#disposed) return;
237
238
  this.#disposed = true;
238
239
  const errors = [];
239
240
  for (let index = this.#entries.length - 1; index >= 0; index--) try {
@@ -260,11 +261,12 @@ function registerDisposable(value, disposal, registered) {
260
261
  disposal.use(value);
261
262
  }
262
263
  /** Internal lazy invocation container. Public consumers receive scoped Context bags. */
263
- function createContextResolver(contexts, io, disposal) {
264
+ function createContextResolver(contexts, io, disposal, signal) {
264
265
  const byName = new Map(contexts.map((context) => [context.name, context]));
265
266
  const entries = /* @__PURE__ */ new Map();
266
267
  const registered = /* @__PURE__ */ new WeakSet();
267
268
  let validatedFlags;
269
+ let constructing = true;
268
270
  let disposed = false;
269
271
  disposal.defer(() => {
270
272
  disposed = true;
@@ -310,6 +312,11 @@ function createContextResolver(contexts, io, disposal) {
310
312
  name,
311
313
  reason: "missing-context"
312
314
  }));
315
+ if (!constructing && !entries.has(name)) return handledRejection(new CrustError("DEFINITION", `Context "${name}" cannot be constructed during invocation cleanup. Read it while setting up the Context whose cleanup needs it.`, {
316
+ subject: "context",
317
+ name,
318
+ reason: "context-during-disposal"
319
+ }));
313
320
  if (validatedFlags === void 0 && Object.keys(context.ownedFlags).length > 0) return handledRejection(new CrustError("DEFINITION", `Context "${name}" owns flags and cannot be pulled before flag validation${originSuffix}. Pull it from an action or a postRun hook after a validated invocation.`, {
314
321
  subject: "context",
315
322
  name,
@@ -334,6 +341,7 @@ function createContextResolver(contexts, io, disposal) {
334
341
  const ownedFlags = Object.fromEntries(Object.keys(context.ownedFlags).map((flag) => [flag, validatedFlags?.[flag]]));
335
342
  const value = await context.setup({
336
343
  ...io,
344
+ signal,
337
345
  flags: ownedFlags,
338
346
  ctx: makeBag(context.uses, current),
339
347
  defer(cleanup) {
@@ -390,7 +398,10 @@ function createContextResolver(contexts, io, disposal) {
390
398
  async settle() {
391
399
  for (;;) {
392
400
  const pending = [...entries.values()].filter((entry) => !entry.settled);
393
- if (pending.length === 0) return;
401
+ if (pending.length === 0) {
402
+ constructing = false;
403
+ return;
404
+ }
394
405
  await Promise.allSettled(pending.map((entry) => entry.promise));
395
406
  }
396
407
  }
@@ -496,6 +507,15 @@ function applyExtensionFlags(root, extension) {
496
507
  injectExtensionFlag(root, name, def, recursive);
497
508
  }
498
509
  }
510
+ /** Copy the flag registry so `registerFlag` on the copy leaves `node` untouched. */
511
+ function cloneFlagRegistry(node) {
512
+ return {
513
+ localFlags: { ...node.localFlags },
514
+ ownedFlags: { ...node.ownedFlags },
515
+ effectiveFlags: { ...node.effectiveFlags },
516
+ flagSpellings: new Map(node.flagSpellings)
517
+ };
518
+ }
499
519
  /** Deep-clone a command subtree without mutating the builder graph. */
500
520
  function cloneCommandNode(node) {
501
521
  const subCommands = {};
@@ -503,13 +523,7 @@ function cloneCommandNode(node) {
503
523
  return {
504
524
  ...node,
505
525
  meta: { ...node.meta },
506
- localFlags: { ...node.localFlags },
507
- ownedFlags: { ...node.ownedFlags },
508
- effectiveFlags: { ...node.effectiveFlags },
509
- flagSpellings: new Map([...node.flagSpellings].map(([spelling, entry]) => [spelling, {
510
- ...entry,
511
- def: node.effectiveFlags[entry.canonicalName]
512
- }])),
526
+ ...cloneFlagRegistry(node),
513
527
  args: [...node.args],
514
528
  subCommands,
515
529
  contexts: node.contexts.map((context) => ({ ...context })),
@@ -674,14 +688,11 @@ function coerceJson(raw) {
674
688
  function buildParseArgsOptionDescriptor(spellings) {
675
689
  const options = {};
676
690
  for (const [spelling, entry] of spellings) {
691
+ if (entry.kind === "short") continue;
677
692
  const descriptor = { type: entry.def.type === "boolean" ? "boolean" : "string" };
678
693
  if (entry.def.multiple) descriptor.multiple = true;
679
- if (entry.kind === "canonical") {
680
- if (entry.def.short) descriptor.short = entry.def.short;
681
- options[spelling] = descriptor;
682
- continue;
683
- }
684
- if (entry.kind === "alias") options[spelling] = descriptor;
694
+ if (entry.kind === "canonical" && entry.def.short) descriptor.short = entry.def.short;
695
+ options[spelling] = descriptor;
685
696
  }
686
697
  return options;
687
698
  }
@@ -1405,10 +1416,35 @@ function _usingCtx() {
1405
1416
  }
1406
1417
  //#endregion
1407
1418
  //#region src/command/invocation.ts
1408
- /** Terminal defaults: line-oriented writes to the process streams. */
1419
+ const ignoreStreamError = () => {};
1420
+ /**
1421
+ * Line-oriented write to a process stream, with `console.log`'s error tolerance: a
1422
+ * downstream that closed early (`cli | head`) surfaces as EPIPE, synchronously or as a
1423
+ * later `error` event, and must not crash the CLI. Mirrors Node's Console
1424
+ * (`ignoreErrors: true`): a noop listener absorbs the event when nobody else listens.
1425
+ */
1426
+ function writeLine(stream, text) {
1427
+ const absorb = () => {
1428
+ if (stream.listenerCount("error") === 0) stream.once("error", ignoreStreamError);
1429
+ };
1430
+ try {
1431
+ absorb();
1432
+ stream.write(`${text}\n`, (error) => {
1433
+ if (error) absorb();
1434
+ });
1435
+ } catch {} finally {
1436
+ stream.removeListener("error", ignoreStreamError);
1437
+ }
1438
+ }
1439
+ /**
1440
+ * Terminal defaults: stream writes rather than `console.log`. Once anything materializes
1441
+ * `process.stdout` (a platform layer, a color library probing `isTTY`), Bun's native
1442
+ * console writer silently drops output past the 64 KiB pipe buffer at exit
1443
+ * (oven-sh/bun#36419); the stream writer flushes it.
1444
+ */
1409
1445
  const DEFAULT_IO = {
1410
- stdout: (text) => console.log(text),
1411
- stderr: (text) => console.error(text)
1446
+ stdout: (text) => writeLine(process.stdout, text),
1447
+ stderr: (text) => writeLine(process.stderr, text)
1412
1448
  };
1413
1449
  /**
1414
1450
  * Snapshot subprocess protocol used by first-party build tooling.
@@ -1417,6 +1453,9 @@ const DEFAULT_IO = {
1417
1453
  * validates its documentation sections, optionally runs Extension build hooks when the
1418
1454
  * build output directory is set, writes its final JSON snapshot and Build Report, and exits
1419
1455
  * without dispatching a Command Action. In-process callers use `Crust.snapshot()`.
1456
+ *
1457
+ * Only source entries run by `crust build` honor it: finished Bun/Node bundles carry
1458
+ * `process.env.CRUST_INTERNAL_BUILD === "1"` as a literal and compile the protocol out.
1420
1459
  */
1421
1460
  const SNAPSHOT_PATH_ENV = "CRUST_INTERNAL_SNAPSHOT_PATH";
1422
1461
  const EXIT_CODE_CANCELLED = 130;
@@ -1424,6 +1463,72 @@ function isAbortError(error) {
1424
1463
  if (!(error instanceof Error)) return false;
1425
1464
  return error.name === "AbortError";
1426
1465
  }
1466
+ /**
1467
+ * Structural: Node 22 bundles down-level `await using` to a plain `Error`
1468
+ * carrying `error`/`suppressed`, so `instanceof SuppressedError` cannot be used.
1469
+ */
1470
+ function isSuppressedError(error) {
1471
+ return isObjectLike(error) && "error" in error && "suppressed" in error && error.name === "SuppressedError";
1472
+ }
1473
+ function isObjectLike(value) {
1474
+ return typeof value === "object" && value !== null;
1475
+ }
1476
+ /**
1477
+ * One `label: message` line per underlying failure. `SuppressedError` chains and
1478
+ * `AggregateError`s contribute their members, not their own boilerplate message,
1479
+ * so a disposal failure never renders as a bare `Error: `.
1480
+ */
1481
+ function describeFailure(error, label = "Error") {
1482
+ const messages = [];
1483
+ const seen = /* @__PURE__ */ new Set();
1484
+ let remaining = 256;
1485
+ let truncated = false;
1486
+ const visitMember = (read) => {
1487
+ if (truncated) return;
1488
+ if (remaining === 0) {
1489
+ truncated = true;
1490
+ messages.push("[additional failures omitted]");
1491
+ return;
1492
+ }
1493
+ remaining--;
1494
+ let member;
1495
+ try {
1496
+ member = read();
1497
+ } catch {
1498
+ messages.push("[unreadable failure]");
1499
+ return;
1500
+ }
1501
+ visit(member);
1502
+ };
1503
+ const visit = (value) => {
1504
+ try {
1505
+ if (isObjectLike(value)) {
1506
+ if (seen.has(value)) return;
1507
+ seen.add(value);
1508
+ }
1509
+ const before = messages.length;
1510
+ if (isSuppressedError(value)) {
1511
+ visitMember(() => value.error);
1512
+ visitMember(() => value.suppressed);
1513
+ if (messages.length > before) return;
1514
+ } else if (value instanceof AggregateError) {
1515
+ const members = value.errors;
1516
+ if (Array.isArray(members)) {
1517
+ for (let index = 0; index < members.length; index++) {
1518
+ visitMember(() => members[index]);
1519
+ if (truncated) break;
1520
+ }
1521
+ if (messages.length > before) return;
1522
+ }
1523
+ }
1524
+ messages.push(String(value instanceof Error && value.message || value));
1525
+ } catch {
1526
+ messages.push("[unreadable failure]");
1527
+ }
1528
+ };
1529
+ visitMember(() => error);
1530
+ return messages.map((message) => `${label}: ${message}`).join("\n");
1531
+ }
1427
1532
  function freezeTree(node) {
1428
1533
  Object.freeze(node);
1429
1534
  Object.freeze(node.localFlags);
@@ -1462,8 +1567,10 @@ function buildExtensionTree(node, materializeCommandDefinition) {
1462
1567
  }
1463
1568
  /** Evaluate Extension section callbacks against current state and freeze the tree. */
1464
1569
  function applySectionsAndFreeze(rootNode, extensions) {
1465
- const authoredSnapshot = snapshotCommand(rootNode);
1466
- for (const extension of extensions) applyExtensionSections(rootNode, extension, authoredSnapshot);
1570
+ if (extensions.some((extension) => extension.sections !== void 0)) {
1571
+ const authoredSnapshot = snapshotCommand(rootNode);
1572
+ for (const extension of extensions) applyExtensionSections(rootNode, extension, authoredSnapshot);
1573
+ }
1467
1574
  freezeTree(rootNode);
1468
1575
  return rootNode;
1469
1576
  }
@@ -1508,7 +1615,7 @@ function resolveStructuredInput(root, path, input) {
1508
1615
  };
1509
1616
  }
1510
1617
  /** Resolve, parse, and run one invocation without rendering failures. */
1511
- async function dispatch(input, prepared, io, onExtensionContext, onFailure) {
1618
+ async function dispatch(input, prepared, io, signal, onExtensionContext, onFailure) {
1512
1619
  try {
1513
1620
  var _usingCtx$1 = _usingCtx();
1514
1621
  const { rootNode, extensions } = prepared;
@@ -1516,16 +1623,19 @@ async function dispatch(input, prepared, io, onExtensionContext, onFailure) {
1516
1623
  const resolvedNode = route.command;
1517
1624
  const disposal = _usingCtx$1.a(new DisposalStack());
1518
1625
  const contexts = resolvedNode.contexts.map(({ instance }) => instance);
1519
- const resolver = createContextResolver(contexts, io, disposal);
1626
+ const resolver = createContextResolver(contexts, io, disposal, signal);
1520
1627
  const rootSnapshot = snapshotCommand(rootNode);
1628
+ let commandSnapshot = rootSnapshot;
1629
+ for (const name of route.commandPath.slice(1)) commandSnapshot = commandSnapshot.subCommands[name];
1521
1630
  const extensionContext = Object.freeze({
1522
1631
  argv: [...argv],
1523
1632
  rootCommand: rootSnapshot,
1524
- command: resolvedNode === rootNode ? rootSnapshot : snapshotCommand(resolvedNode),
1633
+ command: commandSnapshot,
1525
1634
  commandPath: Object.freeze([...route.commandPath]),
1526
1635
  args: parsed.args,
1527
1636
  flags: parsed.flags,
1528
1637
  rawArgs: parsed.rawArgs,
1638
+ signal,
1529
1639
  ctx: resolver.bag(extensions.flatMap((extension) => extension.uses ?? [])),
1530
1640
  finish: finishInvocation,
1531
1641
  stdout: io.stdout,
@@ -1542,6 +1652,7 @@ async function dispatch(input, prepared, io, onExtensionContext, onFailure) {
1542
1652
  flags: validated.flags,
1543
1653
  ctx: resolver.bag(contexts),
1544
1654
  rawArgs: parsed.rawArgs,
1655
+ signal,
1545
1656
  command: extensionContext.command,
1546
1657
  rootCommand: rootSnapshot,
1547
1658
  stdout: io.stdout,
@@ -1599,11 +1710,10 @@ async function dispatch(input, prepared, io, onExtensionContext, onFailure) {
1599
1710
  }
1600
1711
  }
1601
1712
  /** Render one failure through Extension onError hooks, ending in Core's default renderer. */
1602
- async function renderFailure(error, argv, prepared, io, extensionContext, silentDefault = false) {
1713
+ async function renderFailure(error, argv, prepared, io, signal, extensionContext, silentDefault = false) {
1603
1714
  const renderDefault = () => {
1604
1715
  if (silentDefault) return;
1605
- const message = error instanceof Error ? error.message : String(error);
1606
- io.stderr(`Error: ${message}`);
1716
+ io.stderr(describeFailure(error));
1607
1717
  };
1608
1718
  function unavailable(property) {
1609
1719
  return Promise.reject(new CrustError("DEFINITION", `Context "${String(property)}" cannot be pulled from onError because invocation Contexts have already been disposed.`, {
@@ -1613,19 +1723,24 @@ async function renderFailure(error, argv, prepared, io, extensionContext, silent
1613
1723
  }));
1614
1724
  }
1615
1725
  const unavailableContext = new Proxy({}, { get: (_, property) => property === "then" || isSymbol(property) ? void 0 : unavailable(property) });
1616
- const context = extensionContext ?? Object.freeze({
1617
- argv: [...argv],
1618
- rootCommand: snapshotCommand(prepared.rootNode),
1619
- command: snapshotCommand(prepared.rootNode),
1620
- commandPath: Object.freeze([prepared.rootNode.meta.name]),
1621
- args: Object.freeze({}),
1622
- flags: Object.freeze({}),
1623
- rawArgs: [],
1624
- finish: finishInvocation,
1625
- stdout: io.stdout,
1626
- stderr: io.stderr,
1627
- ctx: unavailableContext
1628
- });
1726
+ let context = extensionContext;
1727
+ if (!context) {
1728
+ const rootSnapshot = snapshotCommand(prepared.rootNode);
1729
+ context = Object.freeze({
1730
+ argv: [...argv],
1731
+ rootCommand: rootSnapshot,
1732
+ command: rootSnapshot,
1733
+ commandPath: Object.freeze([prepared.rootNode.meta.name]),
1734
+ args: Object.freeze({}),
1735
+ flags: Object.freeze({}),
1736
+ rawArgs: [],
1737
+ signal,
1738
+ finish: finishInvocation,
1739
+ stdout: io.stdout,
1740
+ stderr: io.stderr,
1741
+ ctx: unavailableContext
1742
+ });
1743
+ }
1629
1744
  try {
1630
1745
  for (const extension of prepared.extensions) if (await extension.hooks?.onError?.(error, context)) return extension.id;
1631
1746
  } catch {}
@@ -1636,11 +1751,11 @@ function hasInjectedIO(io) {
1636
1751
  return io !== void 0 && Object.keys(io).length > 0;
1637
1752
  }
1638
1753
  /** Quiet programmatic boundary: capture output and the failure escaping the complete lifecycle. */
1639
- async function runInvocation(node, input, io, materializeCommandDefinition) {
1754
+ async function runInvocation(node, input, options, materializeCommandDefinition) {
1640
1755
  const stdout = [];
1641
1756
  const stderr = [];
1642
1757
  try {
1643
- const sinks = { ...io };
1758
+ const { signal = new AbortController().signal, ...sinks } = options ?? {};
1644
1759
  const resolvedIO = {
1645
1760
  stdout(text) {
1646
1761
  stdout.push(text);
@@ -1653,7 +1768,7 @@ async function runInvocation(node, input, io, materializeCommandDefinition) {
1653
1768
  };
1654
1769
  return {
1655
1770
  ...await withAmbientTerminalIO(resolvedIO, async () => {
1656
- return await dispatch(input, prepareInvocation(node, materializeCommandDefinition), resolvedIO);
1771
+ return await dispatch(input, prepareInvocation(node, materializeCommandDefinition), resolvedIO, signal);
1657
1772
  }),
1658
1773
  stdout: stdout.join("\n"),
1659
1774
  stderr: stderr.join("\n")
@@ -1674,7 +1789,9 @@ async function executeInvocation(node, options, materializeCommandDefinition) {
1674
1789
  ...DEFAULT_IO,
1675
1790
  ...options?.io
1676
1791
  };
1677
- const snapshotPath = process.env[SNAPSHOT_PATH_ENV];
1792
+ const controller = new AbortController();
1793
+ const signal = options?.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
1794
+ const snapshotPath = process.env.CRUST_INTERNAL_BUILD === "1" ? void 0 : process.env[SNAPSHOT_PATH_ENV];
1678
1795
  if (snapshotPath) {
1679
1796
  try {
1680
1797
  const base = buildExtensionTree(node, materializeCommandDefinition);
@@ -1734,35 +1851,52 @@ async function executeInvocation(node, options, materializeCommandDefinition) {
1734
1851
  process.exitCode = EXIT_CODE_CANCELLED;
1735
1852
  return EXIT_CODE_CANCELLED;
1736
1853
  }
1737
- const message = error instanceof Error ? error.message : String(error);
1738
- io.stderr(`Error: ${message}`);
1854
+ io.stderr(describeFailure(error));
1739
1855
  process.exitCode = 1;
1740
1856
  return 1;
1741
1857
  }
1858
+ const onSigint = () => {
1859
+ if (!controller.signal.aborted) {
1860
+ controller.abort(new DOMException("Interrupted by SIGINT.", "AbortError"));
1861
+ return;
1862
+ }
1863
+ process.removeListener("SIGINT", onSigint);
1864
+ if (process.listenerCount("SIGINT") === 0) process.kill(process.pid, "SIGINT");
1865
+ };
1866
+ process.on("SIGINT", onSigint);
1742
1867
  let extensionContext;
1743
1868
  let renderedInDispatch = false;
1869
+ let renderedError;
1744
1870
  try {
1745
- await dispatch({ argv }, prepared, io, (context) => {
1871
+ await dispatch({ argv }, prepared, io, signal, (context) => {
1746
1872
  extensionContext = context;
1747
1873
  }, async (error, context) => {
1748
1874
  renderedInDispatch = true;
1875
+ renderedError = error;
1749
1876
  const cancelled = isAbortError(error);
1750
1877
  process.exitCode = cancelled ? EXIT_CODE_CANCELLED : 1;
1751
- return renderFailure(error, argv, prepared, io, context, cancelled);
1878
+ return renderFailure(error, argv, prepared, io, signal, context, cancelled);
1752
1879
  });
1753
1880
  } catch (error) {
1881
+ let cleanupFailure;
1882
+ try {
1883
+ if (renderedInDispatch && !Object.is(error, renderedError) && isSuppressedError(error) && Object.is(error.suppressed, renderedError)) cleanupFailure = { error: error.error };
1884
+ } catch {}
1885
+ if (cleanupFailure) io.stderr(describeFailure(cleanupFailure.error, "Cleanup failed"));
1754
1886
  if (isAbortError(error)) {
1755
- if (!renderedInDispatch) await renderFailure(error, argv, prepared, io, extensionContext, true);
1887
+ if (!renderedInDispatch) await renderFailure(error, argv, prepared, io, signal, extensionContext, true);
1756
1888
  process.exitCode = EXIT_CODE_CANCELLED;
1757
1889
  return EXIT_CODE_CANCELLED;
1758
1890
  }
1759
1891
  process.exitCode = 1;
1760
- if (!renderedInDispatch) await renderFailure(error, argv, prepared, io, extensionContext);
1892
+ if (!renderedInDispatch) await renderFailure(error, argv, prepared, io, signal, extensionContext);
1761
1893
  return 1;
1894
+ } finally {
1895
+ process.removeListener("SIGINT", onSigint);
1762
1896
  }
1763
1897
  return 0;
1764
1898
  };
1765
1899
  return await (hasInjectedIO(options?.io) ? withAmbientTerminalIO(io, invoke) : invoke());
1766
1900
  }
1767
1901
  //#endregion
1768
- export { CrustError as S, defineContext as _, runInvocation as a, normalizeArg as b, sectionsFor as c, installExtensionContexts as d, validateCommandSections as f, contextSources as g, defineExtension as h, resolveTypedPath as i, visibleSectionsFor as l, registerFlag as m, executeInvocation as n, snapshotCommand as o, createCommandNode as p, prepareInvocation as r, isListed as s, SNAPSHOT_PATH_ENV as t, cloneCommandNode as u, definingOf as v, normalizeFlag as x, validateContextAvailability as y };
1902
+ export { CrustError as C, normalizeFlag as S, contextSources as _, runInvocation as a, validateContextAvailability as b, sectionsFor as c, cloneFlagRegistry as d, installExtensionContexts as f, defineExtension as g, registerFlag as h, resolveTypedPath as i, visibleSectionsFor as l, createCommandNode as m, executeInvocation as n, snapshotCommand as o, validateCommandSections as p, prepareInvocation as r, isListed as s, SNAPSHOT_PATH_ENV as t, cloneCommandNode as u, defineContext as v, normalizeArg as x, definingOf as y };
package/dist/tooling.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Ct as FlagSnapshot, St as CommandSnapshot, g as DeclaredDefault, kt as ExtensionId, m as CommandSection } from "./context-Cud9frNp.js";
1
+ import { Tt as FlagSnapshot, g as DeclaredDefault, jt as ExtensionId, m as CommandSection, wt as CommandSnapshot } from "./context-BPR700gY.js";
2
2
  import { BUILD_OUT_DIR_ENV } from "@crustjs/utils/artifacts";
3
3
  //#region src/command/invocation.d.ts
4
4
  /**
@@ -8,6 +8,9 @@ import { BUILD_OUT_DIR_ENV } from "@crustjs/utils/artifacts";
8
8
  * validates its documentation sections, optionally runs Extension build hooks when the
9
9
  * build output directory is set, writes its final JSON snapshot and Build Report, and exits
10
10
  * without dispatching a Command Action. In-process callers use `Crust.snapshot()`.
11
+ *
12
+ * Only source entries run by `crust build` honor it: finished Bun/Node bundles carry
13
+ * `process.env.CRUST_INTERNAL_BUILD === "1"` as a literal and compile the protocol out.
11
14
  */
12
15
  export declare const SNAPSHOT_PATH_ENV = "CRUST_INTERNAL_SNAPSHOT_PATH";
13
16
  //#endregion
package/dist/tooling.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c as sectionsFor, l as visibleSectionsFor, s as isListed, t as SNAPSHOT_PATH_ENV } from "./invocation-B17RvTLb.js";
1
+ import { c as sectionsFor, l as visibleSectionsFor, s as isListed, t as SNAPSHOT_PATH_ENV } from "./invocation-BaWf5POY.js";
2
2
  import { BUILD_OUT_DIR_ENV } from "@crustjs/utils/artifacts";
3
3
  //#region src/command/documentation.ts
4
4
  function isNonFiniteNumber(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crustjs/core",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Core library for the Crust CLI framework",
5
5
  "type": "module",
6
6
  "sideEffects": false,