@crustjs/core 0.0.18 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1875 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { dirname, join, posix, resolve, win32 } from "node:path";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
4
+ import { Writable } from "node:stream";
5
+ import { parseArgs } from "node:util";
6
+ import { isPromise } from "node:util/types";
7
+ import { homedir } from "node:os";
8
+ //#region src/errors.ts
9
+ /**
10
+ * A typed error for runtime recipe, Extension, Context, documentation, argv, and validation failures.
11
+ *
12
+ * Every `CrustError` carries a {@link CrustErrorCode} that identifies the specific
13
+ * failure, enabling programmatic error handling without fragile message parsing.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { CrustError } from "@crustjs/core";
18
+ *
19
+ * const outcome = await app.run(["deploy"], { args: { target: "prod" } });
20
+ * if (outcome.status === "failed") {
21
+ * const err = outcome.error;
22
+ * if (err instanceof CrustError) {
23
+ * console.error(`[${err.code}] ${err.message}`);
24
+ * }
25
+ * }
26
+ * ```
27
+ */
28
+ var CrustError = class extends Error {
29
+ /** Machine-readable error code for programmatic handling */
30
+ code;
31
+ /** Structured payload for programmatic handling */
32
+ details;
33
+ /** Optional wrapped original error/value */
34
+ cause;
35
+ constructor(code, message, ...details) {
36
+ super(message);
37
+ this.name = "CrustError";
38
+ this.code = code;
39
+ this.details = details[0];
40
+ }
41
+ is(code) {
42
+ return Object.is(this.code, code);
43
+ }
44
+ withCause(cause) {
45
+ this.cause = cause;
46
+ return this;
47
+ }
48
+ toJSON() {
49
+ return {
50
+ code: this.code,
51
+ message: this.message,
52
+ details: this.details
53
+ };
54
+ }
55
+ };
56
+ //#endregion
57
+ //#region src/parsing/spellings.ts
58
+ function assertUsableSpelling(spelling, kind) {
59
+ if (spelling === "") throw new CrustError("DEFINITION", `Flag ${kind} spellings must be non-empty`, {
60
+ subject: "flag",
61
+ name: spelling,
62
+ reason: "empty-spelling"
63
+ });
64
+ if (kind !== "short" && spelling.startsWith("no-")) throw new CrustError("DEFINITION", `Flag ${kind} "${spelling}" must not start with "no-"; the prefix is reserved for boolean negation`, {
65
+ subject: "flag",
66
+ name: spelling,
67
+ reason: "reserved-no-prefix"
68
+ });
69
+ if (spelling === "__proto__") throw new CrustError("DEFINITION", `Flag ${kind} "__proto__" is a reserved spelling`, {
70
+ subject: "flag",
71
+ name: spelling,
72
+ reason: "reserved-spelling"
73
+ });
74
+ }
75
+ /** All canonical, short, and alias spellings carried by a flag definition. */
76
+ function flagSpellings(name, def) {
77
+ return [
78
+ name,
79
+ ...def.short === void 0 ? [] : [def.short],
80
+ ...def.aliases ?? []
81
+ ];
82
+ }
83
+ /** Whether a flag accepts generated `--no-` spellings. */
84
+ function isFlagNegatable(def) {
85
+ return def.type === "boolean" && def.noNegate !== true;
86
+ }
87
+ /** Convert named authoring definitions to the runtime flag record. */
88
+ function toFlagsRecord(definitions, initial = {}) {
89
+ const flags = { ...initial };
90
+ const seen = new Set(Object.entries(initial).flatMap(([name, def]) => flagSpellings(name, def)));
91
+ for (const definition of definitions) {
92
+ const { name, ...flag } = definition;
93
+ const normalized = normalizeFlag(name, flag);
94
+ for (const spelling of flagSpellings(name, normalized)) {
95
+ if (seen.has(spelling)) throw new CrustError("DEFINITION", `Flag "${name}" collides with an existing flag`, {
96
+ subject: "flag",
97
+ name,
98
+ reason: "flag-collision"
99
+ });
100
+ seen.add(spelling);
101
+ }
102
+ flags[name] = normalized;
103
+ }
104
+ return flags;
105
+ }
106
+ /** Copy only definition-owned collections, not JSON/URL/schema payloads. */
107
+ function ownDefinition(def) {
108
+ if (def.choices && def.default !== void 0) {
109
+ const values = "multiple" in def && def.multiple ? def.default : [def.default];
110
+ for (const value of values) if (!def.choices.includes(value)) throw new CrustError("DEFINITION", "default must be one of choices");
111
+ }
112
+ return Object.freeze({
113
+ ...def,
114
+ ..."aliases" in def && def.aliases ? { aliases: Object.freeze([...def.aliases]) } : {},
115
+ ...def.choices ? { choices: Object.freeze([...def.choices]) } : {},
116
+ ..."multiple" in def && def.multiple && Array.isArray(def.default) ? { default: Object.freeze([...def.default]) } : {}
117
+ });
118
+ }
119
+ function normalizeFlag(name, def) {
120
+ assertUsableSpelling(name, "canonical");
121
+ if (def.short !== void 0) assertUsableSpelling(def.short, "short");
122
+ for (const alias of def.aliases ?? []) assertUsableSpelling(alias, "alias");
123
+ const spellings = flagSpellings(name, def);
124
+ if (new Set(spellings).size !== spellings.length) throw new CrustError("DEFINITION", `Flag "${name}" repeats one of its own spellings`, {
125
+ subject: "flag",
126
+ name,
127
+ reason: "flag-collision"
128
+ });
129
+ if (def.short !== void 0 && def.short.length !== 1) throw new CrustError("DEFINITION", "Short flags must be one character");
130
+ return ownDefinition(def);
131
+ }
132
+ function normalizeArg(def) {
133
+ if (def.name === "") throw new CrustError("DEFINITION", "Argument names must be non-empty", {
134
+ subject: "argument",
135
+ name: def.name,
136
+ reason: "empty-name"
137
+ });
138
+ return ownDefinition(def);
139
+ }
140
+ //#endregion
141
+ //#region src/api/context.ts
142
+ const defining = Symbol("crust.defining");
143
+ /** @internal */
144
+ function definingOf(value) {
145
+ return value[defining];
146
+ }
147
+ /** @internal */
148
+ function seal(value) {
149
+ return Object.freeze(Object.assign(value, { [defining]: value }));
150
+ }
151
+ /** Check only declared availability; setup, callback values, and cycles belong to invocation. */
152
+ function validateContextAvailability(contexts, sources) {
153
+ const names = new Set(contexts.map((instance) => instance.name));
154
+ const visited = /* @__PURE__ */ new Set();
155
+ const visit = (source) => {
156
+ if (visited.has(source)) return;
157
+ visited.add(source);
158
+ const name = "contextName" in source ? source.contextName : source.name;
159
+ if (!names.has(name)) throw new CrustError("DEFINITION", `No provider for Context "${name}"`, {
160
+ subject: "context",
161
+ name,
162
+ reason: "missing-context"
163
+ });
164
+ for (const dependency of source.uses) visit(dependency);
165
+ };
166
+ for (const source of sources) visit(source);
167
+ }
168
+ function isContextSetup(value) {
169
+ return typeof value === "function";
170
+ }
171
+ function defineContext(name, configOrSetup, maybeSetup) {
172
+ const hasConfig = !isContextSetup(configOrSetup);
173
+ const config = hasConfig ? configOrSetup : {};
174
+ const setup = hasConfig ? maybeSetup : configOrSetup;
175
+ const ownedFlags = Object.freeze(toFlagsRecord(config.flags ?? []));
176
+ const uses = Object.freeze((config.uses ?? []).map(definingOf));
177
+ const instance = (instanceUses, run) => {
178
+ return seal({
179
+ name,
180
+ ownedFlags,
181
+ uses: instanceUses,
182
+ setup: run
183
+ });
184
+ };
185
+ const factory = (options) => instance(uses, (input) => {
186
+ return setup({
187
+ options,
188
+ ...input
189
+ });
190
+ });
191
+ factory.contextName = name;
192
+ factory.uses = uses;
193
+ factory.of = (value) => instance(Object.freeze([]), () => value);
194
+ return seal(factory);
195
+ }
196
+ function isDisposeCallback(value) {
197
+ return typeof value === "function";
198
+ }
199
+ /**
200
+ * Minimal `AsyncDisposableStack` stand-in for runtimes without the global
201
+ * (Node 22): LIFO disposal of used resources and deferred callbacks,
202
+ * preferring `Symbol.asyncDispose` over `Symbol.dispose`.
203
+ *
204
+ * ponytail: multiple disposal errors aggregate as `AggregateError` instead of
205
+ * the native `SuppressedError` chain; delete this class when Node 22 leaves
206
+ * the support floor.
207
+ *
208
+ * @internal Exported for unit testing and invocation wiring.
209
+ */
210
+ var FallbackAsyncDisposableStack = class {
211
+ #entries = [];
212
+ #disposed = false;
213
+ #assertPending() {
214
+ if (this.#disposed) throw new ReferenceError("AsyncDisposableStack is already disposed");
215
+ }
216
+ use(value) {
217
+ this.#assertPending();
218
+ const dispose = hasAsyncDispose(value) ? value[Symbol.asyncDispose] : value[Symbol.dispose];
219
+ this.#entries.push(() => dispose.call(value));
220
+ return value;
221
+ }
222
+ defer(onDisposeAsync) {
223
+ this.#assertPending();
224
+ if (!isDisposeCallback(onDisposeAsync)) throw new TypeError("defer callback is not callable");
225
+ this.#entries.push(onDisposeAsync);
226
+ }
227
+ async [Symbol.asyncDispose]() {
228
+ this.#disposed = true;
229
+ const errors = [];
230
+ for (let index = this.#entries.length - 1; index >= 0; index--) try {
231
+ await this.#entries[index]();
232
+ } catch (error) {
233
+ errors.push(error);
234
+ }
235
+ if (errors.length === 1) throw errors[0];
236
+ if (errors.length > 1) throw new AggregateError(errors, "Disposal failed");
237
+ }
238
+ };
239
+ const DisposalStack = globalThis.AsyncDisposableStack ?? FallbackAsyncDisposableStack;
240
+ function hasAsyncDispose(value) {
241
+ return typeof value[Symbol.asyncDispose] === "function";
242
+ }
243
+ function isDisposableValue(value) {
244
+ if (value === null || typeof value !== "object" && typeof value !== "function") return false;
245
+ const candidate = value;
246
+ return typeof candidate[Symbol.asyncDispose] === "function" || typeof candidate[Symbol.dispose] === "function";
247
+ }
248
+ function registerDisposable(value, disposal, registered) {
249
+ if (!isDisposableValue(value) || registered.has(value)) return;
250
+ registered.add(value);
251
+ disposal.use(value);
252
+ }
253
+ /** Internal lazy invocation container. Public consumers receive scoped Context bags. */
254
+ function createContextResolver(contexts, io, disposal) {
255
+ const byName = new Map(contexts.map((context) => [context.name, context]));
256
+ const entries = /* @__PURE__ */ new Map();
257
+ const registered = /* @__PURE__ */ new WeakSet();
258
+ let validatedFlags;
259
+ let disposed = false;
260
+ disposal.defer(() => {
261
+ disposed = true;
262
+ });
263
+ const pathTo = (from, target, visited = /* @__PURE__ */ new Set()) => {
264
+ if (from.name === target) return [from.name];
265
+ if (visited.has(from.name)) return void 0;
266
+ visited.add(from.name);
267
+ for (const name of from.waitingOn) {
268
+ const next = entries.get(name);
269
+ if (!next) continue;
270
+ const path = pathTo(next, target, visited);
271
+ if (path) return [from.name, ...path];
272
+ }
273
+ };
274
+ const isFlagValidationError = (error) => {
275
+ try {
276
+ const seen = /* @__PURE__ */ new Set();
277
+ for (let e = error; e != null && !seen.has(e); e = e.cause) {
278
+ seen.add(e);
279
+ if (e instanceof CrustError && e.code === "DEFINITION" && e.details?.reason === "flags-before-validation") return true;
280
+ }
281
+ return false;
282
+ } catch {
283
+ return false;
284
+ }
285
+ };
286
+ function handledRejection(reason) {
287
+ const rejection = Promise.reject(reason);
288
+ rejection.catch(() => {});
289
+ return rejection;
290
+ }
291
+ const makePull = (origin) => (name) => {
292
+ const originSuffix = origin ? ` (pulled while constructing Context "${origin.name}")` : "";
293
+ if (disposed) return handledRejection(new CrustError("DEFINITION", `Context "${name}" cannot be pulled from onError because invocation Contexts have already been disposed.`, {
294
+ subject: "context",
295
+ name,
296
+ reason: "context-after-disposal"
297
+ }));
298
+ const context = byName.get(name);
299
+ if (!context) return handledRejection(new CrustError("DEFINITION", `No provider for Context "${name}". Add .provide(${name}(...)) to the app or an ancestor command.${originSuffix}`, {
300
+ subject: "context",
301
+ name,
302
+ reason: "missing-context"
303
+ }));
304
+ 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.`, {
305
+ subject: "context",
306
+ name,
307
+ reason: "flags-before-validation"
308
+ }));
309
+ let entry = entries.get(name);
310
+ if (!entry) {
311
+ const deferred = Promise.withResolvers();
312
+ deferred.promise.catch(() => {});
313
+ entry = {
314
+ name,
315
+ promise: deferred.promise,
316
+ resolve: deferred.resolve,
317
+ reject: deferred.reject,
318
+ waitingOn: /* @__PURE__ */ new Set(),
319
+ settled: false
320
+ };
321
+ entries.set(name, entry);
322
+ const current = entry;
323
+ (async () => {
324
+ try {
325
+ const ownedFlags = Object.fromEntries(Object.keys(context.ownedFlags).map((flag) => [flag, validatedFlags?.[flag]]));
326
+ const value = await context.setup({
327
+ ...io,
328
+ flags: ownedFlags,
329
+ ctx: makeBag(context.uses, current),
330
+ defer(cleanup) {
331
+ if (current.settled) throw new CrustError("DEFINITION", `Context "${name}" cannot register cleanup after its setup has finished.`, {
332
+ subject: "context",
333
+ name,
334
+ reason: "context-defer-after-setup"
335
+ });
336
+ disposal.defer(cleanup);
337
+ }
338
+ });
339
+ registerDisposable(value, disposal, registered);
340
+ current.resolve(value);
341
+ } catch (error) {
342
+ if (isFlagValidationError(error)) entries.delete(name);
343
+ current.reject(error);
344
+ } finally {
345
+ current.settled = true;
346
+ }
347
+ })();
348
+ }
349
+ if (origin && !entry.settled) {
350
+ const path = pathTo(entry, origin.name);
351
+ if (path) return handledRejection(new CrustError("DEFINITION", `Context dependency cycle: ${[origin.name, ...path].map((part) => `"${part}"`).join(" -> ")}`, {
352
+ subject: "context",
353
+ name: origin.name,
354
+ reason: "context-cycle"
355
+ }));
356
+ origin.waitingOn.add(name);
357
+ return entry.promise.finally(() => origin.waitingOn.delete(name));
358
+ }
359
+ return entry.promise;
360
+ };
361
+ const makeBag = (sources, origin) => {
362
+ const bag = {};
363
+ const add = (source) => {
364
+ const name = "contextName" in source ? source.contextName : source.name;
365
+ if (Object.hasOwn(bag, name)) return;
366
+ Object.defineProperty(bag, name, {
367
+ enumerable: true,
368
+ get: () => makePull(origin)(name)
369
+ });
370
+ for (const dependency of source.uses ?? []) add(dependency);
371
+ };
372
+ for (const source of sources) add(source);
373
+ return Object.freeze(bag);
374
+ };
375
+ return {
376
+ bag: (sources) => makeBag(sources, null),
377
+ setValidatedFlags(flags) {
378
+ validatedFlags = flags;
379
+ },
380
+ async settle() {
381
+ for (;;) {
382
+ const pending = [...entries.values()].filter((entry) => !entry.settled);
383
+ if (pending.length === 0) return;
384
+ await Promise.allSettled(pending.map((entry) => entry.promise));
385
+ }
386
+ }
387
+ };
388
+ }
389
+ //#endregion
390
+ //#region src/api/extension.ts
391
+ const FINISHED = Object.freeze({ [Symbol("crust.finished")]: true });
392
+ /** @internal */
393
+ function finishInvocation() {
394
+ return FINISHED;
395
+ }
396
+ function isExtensionFactory(value) {
397
+ return typeof value === "function";
398
+ }
399
+ function defineExtension(id, config = {}) {
400
+ if (id === void 0) return defineExtension;
401
+ if (isExtensionFactory(config)) return Object.assign((...args) => defineExtension(id, config(...args)), { id });
402
+ const ownedFlags = Object.freeze(toFlagsRecord(config.flags ?? []));
403
+ toFlagsRecord((config.provides ?? []).flatMap((instance) => Object.entries(definingOf(instance).ownedFlags).map(([name, def]) => ({
404
+ ...def,
405
+ name
406
+ }))), ownedFlags);
407
+ return seal({
408
+ ...config,
409
+ uses: Object.freeze((config.uses ?? []).map(definingOf)),
410
+ ...config.provides ? { provides: Object.freeze(config.provides.map(definingOf)) } : {},
411
+ ...config.commands ? { commands: Object.freeze([...config.commands]) } : {},
412
+ id,
413
+ ...config.flags === void 0 ? {} : { flags: ownedFlags }
414
+ });
415
+ }
416
+ //#endregion
417
+ //#region src/command/node.ts
418
+ /**
419
+ * Creates a new `CommandNode` with all fields initialized to defaults.
420
+ *
421
+ * @param name - The command name.
422
+ * @returns A fresh `CommandNode` with empty flags, no args, no subcommands,
423
+ * no extensions, and no action.
424
+ */
425
+ function createCommandNode(name) {
426
+ return {
427
+ meta: { name },
428
+ localFlags: {},
429
+ ownedFlags: {},
430
+ effectiveFlags: {},
431
+ flagSpellings: /* @__PURE__ */ new Map(),
432
+ args: [],
433
+ subCommands: {},
434
+ contexts: [],
435
+ demands: [],
436
+ extensions: [],
437
+ run: void 0
438
+ };
439
+ }
440
+ /** Register one effective flag and its source-owned state with a single collision policy. */
441
+ function registerFlag(node, name, def, source) {
442
+ def = normalizeFlag(name, def);
443
+ const incomingSpellings = flagSpellings(name, def);
444
+ const existingName = Object.hasOwn(node.effectiveFlags, name) ? name : incomingSpellings.map((spelling) => node.flagSpellings.get(spelling)?.canonicalName).find((existing) => existing !== void 0);
445
+ if (existingName !== void 0) throw new CrustError("DEFINITION", `Flag "${name}" collides with existing flag "${existingName}" on command "${node.meta.name}"`, {
446
+ subject: "flag",
447
+ name,
448
+ reason: "flag-collision"
449
+ });
450
+ (source === "local" ? node.localFlags : node.ownedFlags)[name] = def;
451
+ node.effectiveFlags[name] = def;
452
+ const entry = {
453
+ canonicalName: name,
454
+ def,
455
+ negatable: isFlagNegatable(def)
456
+ };
457
+ node.flagSpellings.set(name, {
458
+ ...entry,
459
+ kind: "canonical"
460
+ });
461
+ if (def.short !== void 0) node.flagSpellings.set(def.short, {
462
+ ...entry,
463
+ kind: "short"
464
+ });
465
+ for (const alias of def.aliases ?? []) node.flagSpellings.set(alias, {
466
+ ...entry,
467
+ kind: "alias"
468
+ });
469
+ }
470
+ //#endregion
471
+ //#region src/command/extensions-install.ts
472
+ /** Inject an Extension-owned flag into a node and, when recursive, its descendants. */
473
+ function injectExtensionFlag(node, name, def, recursive) {
474
+ registerFlag(node, name, def, "owned");
475
+ if (!recursive) return;
476
+ for (const sub of Object.values(node.subCommands)) injectExtensionFlag(sub, name, def, true);
477
+ }
478
+ /** Attach one Extension's owned root commands to a cloned tree. */
479
+ function applyExtensionCommands(root, extension, materializeCommandDefinition) {
480
+ for (const definition of extension.commands ?? []) root.subCommands[definition.name] = materializeCommandDefinition(definition, root, extension.id);
481
+ }
482
+ /** Inject one Extension's owned flags across a cloned tree. */
483
+ function applyExtensionFlags(root, extension) {
484
+ for (const [name, defWithScope] of Object.entries(extension.flags ?? {})) {
485
+ const { recursive = true, ...def } = defWithScope;
486
+ injectExtensionFlag(root, name, def, recursive);
487
+ }
488
+ }
489
+ /** Deep-clone a command subtree without mutating the builder graph. */
490
+ function cloneCommandNode(node) {
491
+ const subCommands = {};
492
+ for (const [name, sub] of Object.entries(node.subCommands)) subCommands[name] = cloneCommandNode(sub);
493
+ return {
494
+ ...node,
495
+ meta: { ...node.meta },
496
+ localFlags: { ...node.localFlags },
497
+ ownedFlags: { ...node.ownedFlags },
498
+ effectiveFlags: { ...node.effectiveFlags },
499
+ flagSpellings: new Map([...node.flagSpellings].map(([spelling, entry]) => [spelling, {
500
+ ...entry,
501
+ def: node.effectiveFlags[entry.canonicalName]
502
+ }])),
503
+ args: [...node.args],
504
+ subCommands,
505
+ contexts: node.contexts.map((context) => ({ ...context })),
506
+ demands: [...node.demands],
507
+ extensions: [...node.extensions],
508
+ run: node.run
509
+ };
510
+ }
511
+ function invalidSections({ subject, name }) {
512
+ return new CrustError("DEFINITION", `${subject === "command" ? "Command" : "Extension"} "${name}" contains invalid documentation sections`, {
513
+ subject,
514
+ name,
515
+ reason: "invalid-sections"
516
+ });
517
+ }
518
+ function normalizeSection(section, owner) {
519
+ const { title, body, only, except } = section;
520
+ if (!title.trim() || /[\r\n]/.test(title) || !body.trim() || only?.length === 0 || except?.length === 0 || only !== void 0 && except !== void 0) throw invalidSections(owner);
521
+ const audience = (ids) => {
522
+ return Object.freeze(ids.map((consumer) => typeof consumer === "string" ? consumer : consumer.id));
523
+ };
524
+ return Object.freeze({
525
+ title,
526
+ body,
527
+ ...only ? { only: audience(only) } : except ? { except: audience(except) } : {}
528
+ });
529
+ }
530
+ function validateCommandSections(name, sections) {
531
+ return sections.map((section) => normalizeSection(section, {
532
+ subject: "command",
533
+ name
534
+ }));
535
+ }
536
+ function contributionTarget(root, command, extension) {
537
+ let target = root;
538
+ for (const segment of command) {
539
+ const next = Object.hasOwn(target.subCommands, segment) ? target.subCommands[segment] : void 0;
540
+ if (!next) throw new CrustError("DEFINITION", `Extension "${extension.id}" section target "${command.join(" ")}" is not a canonical command path`, {
541
+ subject: "extension",
542
+ name: extension.id,
543
+ reason: "invalid-section-path"
544
+ });
545
+ target = next;
546
+ }
547
+ return target;
548
+ }
549
+ function applyExtensionSections(root, extension, snapshot) {
550
+ if (!extension.sections) return;
551
+ const owner = {
552
+ subject: "extension",
553
+ name: extension.id
554
+ };
555
+ const contributions = extension.sections(snapshot);
556
+ for (const contribution of contributions) {
557
+ const section = normalizeSection(contribution, owner);
558
+ const target = contributionTarget(root, contribution.command, extension);
559
+ target.meta.sections = [...target.meta.sections ?? [], section];
560
+ }
561
+ }
562
+ function installExtensionContexts(node, extensions, reRegisteredIds) {
563
+ const cloned = cloneCommandNode(node);
564
+ const kept = new Set(extensions.flatMap((e) => !reRegisteredIds.has(e.id) && node.extensions.includes(e) ? [e.id] : []));
565
+ const prune = (target) => {
566
+ target.contexts = target.contexts.filter((context) => context.extensionId === void 0 || kept.has(context.extensionId));
567
+ const effectiveNames = Object.keys(target.effectiveFlags);
568
+ const localFlags = target.localFlags;
569
+ const ownedFlags = {};
570
+ for (const { instance } of target.contexts) Object.assign(ownedFlags, instance.ownedFlags);
571
+ target.localFlags = {};
572
+ target.ownedFlags = {};
573
+ target.effectiveFlags = {};
574
+ target.flagSpellings = /* @__PURE__ */ new Map();
575
+ for (const name of effectiveNames) {
576
+ const source = Object.hasOwn(ownedFlags, name) ? "owned" : "local";
577
+ const flags = source === "owned" ? ownedFlags : localFlags;
578
+ if (Object.hasOwn(flags, name)) registerFlag(target, name, flags[name], source);
579
+ }
580
+ for (const child of Object.values(target.subCommands)) prune(child);
581
+ };
582
+ prune(cloned);
583
+ for (const extension of extensions) {
584
+ if (kept.has(extension.id)) continue;
585
+ const instances = extension.provides ?? [];
586
+ if (instances.length === 0) continue;
587
+ const walk = (target, skip) => {
588
+ const installed = instances.filter((instance) => !skip.has(instance.name));
589
+ const registrations = installed.map((instance) => ({
590
+ instance,
591
+ extensionId: extension.id
592
+ }));
593
+ target.contexts.push(...registrations);
594
+ for (const instance of installed) for (const [name, def] of Object.entries(instance.ownedFlags)) registerFlag(target, name, def, "owned");
595
+ const inherited = new WeakSet(target.contexts.map(({ instance }) => instance));
596
+ for (const child of Object.values(target.subCommands)) {
597
+ const childSkip = new Set(skip);
598
+ for (const { instance } of child.contexts) if (!inherited.has(instance)) childSkip.add(instance.name);
599
+ walk(child, childSkip);
600
+ }
601
+ };
602
+ walk(cloned, /* @__PURE__ */ new Set());
603
+ }
604
+ return cloned;
605
+ }
606
+ //#endregion
607
+ //#region ../utils/src/terminal.ts
608
+ const STORAGE_KEY = Symbol.for("crustjs.terminal.io");
609
+ const AMBIENT_CALLBACKS_KEY = Symbol.for("crustjs.terminal.ambient-callbacks");
610
+ const globalWithStorage = globalThis;
611
+ const globalWithAmbientCallbacks = globalThis;
612
+ /** Bundled copies in Core, Prompts, and Progress share process-wide terminal state. */
613
+ const storage = globalWithStorage[STORAGE_KEY] ??= new AsyncLocalStorage();
614
+ const ambientCallbacks = globalWithAmbientCallbacks[AMBIENT_CALLBACKS_KEY] ??= /* @__PURE__ */ new WeakMap();
615
+ /** Run a function with terminal streams available in its async scope. */
616
+ function withTerminalIO(io, fn) {
617
+ const current = storage.getStore();
618
+ return storage.run({
619
+ input: io.input ?? current?.input,
620
+ output: io.output ?? current?.output
621
+ }, fn);
622
+ }
623
+ function lineBufferedOutput(io) {
624
+ let pending = "";
625
+ const output = new Writable({
626
+ decodeStrings: false,
627
+ write(chunk, _encoding, callback) {
628
+ pending += chunk.toString();
629
+ let newline = pending.indexOf("\n");
630
+ while (newline !== -1) {
631
+ io.stderr(pending.slice(0, newline));
632
+ pending = pending.slice(newline + 1);
633
+ newline = pending.indexOf("\n");
634
+ }
635
+ callback();
636
+ }
637
+ });
638
+ ambientCallbacks.set(output, io);
639
+ return output;
640
+ }
641
+ /** Run a function with Core's line-oriented output bridged into the terminal stream scope. */
642
+ function withAmbientTerminalIO(io, fn) {
643
+ const current = storage.getStore();
644
+ return withTerminalIO({
645
+ input: current?.input,
646
+ output: current?.output && !ambientCallbacks.has(current.output) ? current.output : lineBufferedOutput(io)
647
+ }, fn);
648
+ }
649
+ //#endregion
650
+ //#region ../utils/src/primitive.ts
651
+ /**
652
+ * Attempts to coerce a string to a number, returning `undefined` only when the
653
+ * result is `NaN`. Callers decide whether `undefined` means throw or fallback.
654
+ *
655
+ * @example
656
+ * ```ts
657
+ * tryCoerceNumber("42"); // 42
658
+ * tryCoerceNumber("abc"); // undefined
659
+ * tryCoerceNumber(""); // 0
660
+ * ```
661
+ */
662
+ function tryCoerceNumber(raw) {
663
+ const num = Number(raw);
664
+ return Number.isNaN(num) ? void 0 : num;
665
+ }
666
+ /**
667
+ * Coerces Crust boolean strings using the existing strict truthy spellings.
668
+ *
669
+ * @example
670
+ * ```ts
671
+ * coerceBooleanString("true"); // true
672
+ * coerceBooleanString("1"); // true
673
+ * coerceBooleanString("false"); // false
674
+ * ```
675
+ */
676
+ function coerceBooleanString(raw) {
677
+ return raw === "true" || raw === "1";
678
+ }
679
+ //#endregion
680
+ //#region src/parsing/coercers.ts
681
+ /**
682
+ * Coerce a raw argv string into a {@link URL} instance via the WHATWG
683
+ * `URL` parser. Any protocol accepted by `new URL()` (https, http, file,
684
+ * ftp, …) is allowed.
685
+ *
686
+ * Throws `CrustError("PARSE", …)` when the input is not a valid URL.
687
+ * The original input is echoed in the message. When the input clearly
688
+ * lacks a URL scheme, we append a hint reminding the user to include
689
+ * one — a common foot-gun on the command line.
690
+ */
691
+ function coerceUrl(raw) {
692
+ try {
693
+ return new URL(raw);
694
+ } catch {
695
+ throw new CrustError("PARSE", `Invalid URL "${raw}"${/^[a-z][a-z0-9+.-]*:/i.test(raw) ? "" : " (missing protocol — e.g. https://example.com)"}`);
696
+ }
697
+ }
698
+ /**
699
+ * Coerce a raw argv string into an absolute filesystem path.
700
+ *
701
+ * Steps:
702
+ * 1. Reject empty input.
703
+ * 2. Expand a leading `~` (followed by `/` or end-of-string) to the user's
704
+ * home directory. `~username` is intentionally NOT expanded.
705
+ * 3. Resolve against `process.cwd()` so the result is always absolute.
706
+ *
707
+ * Path-traversal (`..`) is allowed — coercion does not sandbox.
708
+ */
709
+ function coercePath(raw) {
710
+ if (raw === "") throw new CrustError("PARSE", "Path cannot be empty");
711
+ const expanded = raw.replace(/^~(?=\/|$)/, homedir());
712
+ return resolve(process.cwd(), expanded);
713
+ }
714
+ /**
715
+ * Coerce a raw argv string into a parsed JSON value (`JsonValue`) via
716
+ * `JSON.parse`. Any valid JSON document is accepted — objects, arrays,
717
+ * strings, numbers, booleans, null.
718
+ *
719
+ * Throws `CrustError("PARSE", …)` when the input is not valid JSON.
720
+ * The original `SyntaxError.message` is included plus a shell-quoting
721
+ * hint, since unquoted JSON on the command line is a common foot-gun.
722
+ *
723
+ * Note: `JSON.parse` loses precision on integers above `Number.MAX_SAFE_INTEGER`.
724
+ */
725
+ function coerceJson(raw) {
726
+ try {
727
+ return JSON.parse(raw);
728
+ } catch (err) {
729
+ throw new CrustError("PARSE", `Invalid JSON: ${err instanceof Error ? err.message : String(err)}. Tip: wrap JSON in single quotes on the command line, e.g. --flag '{"k":1}'`);
730
+ }
731
+ }
732
+ //#endregion
733
+ //#region src/parsing/parser.ts
734
+ /**
735
+ * 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
+ */
738
+ function buildParseArgsOptionDescriptor(spellings) {
739
+ const options = {};
740
+ const aliasToName = {};
741
+ for (const [spelling, entry] of spellings) {
742
+ const descriptor = { type: entry.def.type === "boolean" ? "boolean" : "string" };
743
+ if (entry.def.multiple) descriptor.multiple = true;
744
+ if (entry.kind === "canonical") {
745
+ if (entry.def.short) descriptor.short = entry.def.short;
746
+ options[spelling] = descriptor;
747
+ continue;
748
+ }
749
+ aliasToName[spelling] = entry.canonicalName;
750
+ if (entry.kind === "alias") options[spelling] = descriptor;
751
+ }
752
+ return {
753
+ options,
754
+ aliasToName
755
+ };
756
+ }
757
+ /**
758
+ * Coerce a string value to the expected type based on the type literal.
759
+ */
760
+ function coerceValue(value, type, label) {
761
+ if (type === "number") {
762
+ const num = tryCoerceNumber(value);
763
+ if (num === void 0) throw new CrustError("PARSE", `Expected number for ${label}, got "${value}"`);
764
+ return num;
765
+ }
766
+ if (type === "boolean") return coerceBooleanString(value);
767
+ if (type === "url") return coerceUrl(value);
768
+ if (type === "path") return coercePath(value);
769
+ if (type === "json") return coerceJson(value);
770
+ return value;
771
+ }
772
+ /**
773
+ * Validate a raw argv string against a flag/arg `choices` list. Throws
774
+ * `CrustError("PARSE", …)` when the value is not in the allowed set.
775
+ *
776
+ * Runs *before* any `parse` transform so the user-facing comparison is on
777
+ * the raw token, not the post-`parse` value.
778
+ */
779
+ function validateChoice(raw, choices, label) {
780
+ if (!choices.includes(raw)) throw new CrustError("PARSE", `Invalid value "${raw}" for ${label}. Expected one of: ${choices.join(", ")}`);
781
+ }
782
+ /** Invoke a user `parse` function on a raw token, wrapping errors. */
783
+ function invokeParse(parse, raw, label, index) {
784
+ const location = index === void 0 ? label : `${label} element [${index}]`;
785
+ let result;
786
+ try {
787
+ result = parse(raw);
788
+ if (isPromise(result)) {
789
+ result.catch(() => {});
790
+ throw new Error("parse must be synchronous");
791
+ }
792
+ } catch (err) {
793
+ throw new CrustError("PARSE", `Failed to parse ${location}: ${err instanceof Error ? err.message : String(err)}`).withCause(err);
794
+ }
795
+ return result;
796
+ }
797
+ /**
798
+ * Resolve a flag/arg default to its runtime value, mirroring the argv-side
799
+ * coercion pipeline so omitted-flag behavior matches user-supplied behavior:
800
+ *
801
+ * raw default → parse | coerce → result
802
+ *
803
+ * Without this, `{ type: "path", default: "./dist" }` returns the raw
804
+ * relative string while `--out ./dist` returns an absolute path.
805
+ *
806
+ * `parse` is preferred when present (matches the escape-hatch contract).
807
+ * `type: "path"` defaults are coerced through `coercePath` because their
808
+ * default field is a raw string per `PathFlagDef`/`PathArgDef`. `url` and
809
+ * `json` defaults are already in their resolved form (`URL` / `unknown`)
810
+ * per the variant interfaces, so they pass through unchanged.
811
+ */
812
+ function resolveDefault(def, label) {
813
+ const { default: defaultValue, parse } = def;
814
+ if (defaultValue === void 0) return void 0;
815
+ if (parse) {
816
+ if (Array.isArray(defaultValue)) return defaultValue.map((v, i) => invokeParse(parse, String(v), label, i));
817
+ return invokeParse(parse, String(defaultValue), label);
818
+ }
819
+ if (def.type === "path") {
820
+ if (Array.isArray(defaultValue)) return defaultValue.map((v) => coercePath(String(v)));
821
+ return coercePath(String(defaultValue));
822
+ }
823
+ return "multiple" in def && def.multiple && Array.isArray(defaultValue) ? [...defaultValue] : defaultValue;
824
+ }
825
+ /**
826
+ * Coerce a single flag's parsed value to its target type.
827
+ *
828
+ * Order on string-typed flags with `choices` and/or `parse`:
829
+ * raw token → choices validation → parse transform (if set) → result.
830
+ * For multi-value flags both steps run per element.
831
+ */
832
+ function coerceFlagValue(name, def, parsed) {
833
+ if (parsed.kind === "boolean") return parsed.value;
834
+ const label = `--${name}`;
835
+ const coerce = (value, index) => {
836
+ if (def.choices) validateChoice(value, def.choices, label);
837
+ if (def.parse) return invokeParse(def.parse, value, label, index);
838
+ return coerceValue(value, def.type, label);
839
+ };
840
+ return Array.isArray(parsed.value) ? parsed.value.map(coerce) : coerce(parsed.value);
841
+ }
842
+ /**
843
+ * Resolve parsed option tokens to canonical flag names, in argv order.
844
+ *
845
+ * Works from `parsed.tokens` rather than `parsed.values` because
846
+ * `util.parseArgs` groups values by option key: with aliases, the last *key*
847
+ * would win instead of the last *token* (`--verbose --no-loud --verbose`
848
+ * must be `true`), and `multiple` flags spread across aliases would lose
849
+ * their interleaved argv order.
850
+ */
851
+ function resolveAliases(tokens, aliasToName, flagsDef) {
852
+ const canonical = {};
853
+ for (const token of tokens) {
854
+ if (token.kind !== "option") continue;
855
+ const canonicalName = aliasToName[token.name] ?? token.name;
856
+ const def = flagsDef[canonicalName];
857
+ const existing = canonical[canonicalName];
858
+ if (def.type === "boolean") {
859
+ const value = !token.rawName.startsWith("--no-");
860
+ if (def.multiple && existing?.kind === "boolean" && Array.isArray(existing.value)) existing.value.push(value);
861
+ else canonical[canonicalName] = {
862
+ kind: "boolean",
863
+ value: def.multiple ? [value] : value
864
+ };
865
+ } else {
866
+ const value = token.value;
867
+ if (def.multiple && existing?.kind === "string" && Array.isArray(existing.value)) existing.value.push(value);
868
+ else canonical[canonicalName] = {
869
+ kind: "string",
870
+ value: def.multiple ? [value] : value
871
+ };
872
+ }
873
+ }
874
+ return canonical;
875
+ }
876
+ /**
877
+ * Resolve all flag definitions against the canonical parsed values.
878
+ * Handles coercion and default values.
879
+ */
880
+ function resolveFlags(flagsDef, values, coerce) {
881
+ const resolved = {};
882
+ for (const name of Object.keys(values)) if (!Object.hasOwn(flagsDef, name) && values[name] !== void 0) throw new CrustError("PARSE", `Unknown flag "--${name}"`, {
883
+ flag: name,
884
+ reason: "unknown-flag"
885
+ });
886
+ for (const [name, def] of Object.entries(flagsDef)) {
887
+ const parsedValue = Object.hasOwn(values, name) ? values[name] : void 0;
888
+ if (!(parsedValue === void 0 || def.multiple && Array.isArray(parsedValue) && parsedValue.length === 0)) {
889
+ resolved[name] = coerce(name, def, parsedValue);
890
+ continue;
891
+ }
892
+ resolved[name] = resolveDefault(def, `--${name}`);
893
+ }
894
+ return resolved;
895
+ }
896
+ /**
897
+ * Validate required flags against already-resolved flag values.
898
+ */
899
+ function validateRequiredFlags(flagsDef, resolvedFlags) {
900
+ for (const [name, def] of Object.entries(flagsDef)) if (def.required === true && def.default === void 0) {
901
+ if (resolvedFlags[name] === void 0) throw new CrustError("VALIDATION", `Missing required flag "--${name}"`);
902
+ }
903
+ }
904
+ function coerceArgToken(def, raw, label, index) {
905
+ if (def.schema) return raw;
906
+ if (def.choices) validateChoice(raw, def.choices, label);
907
+ if (def.parse) return invokeParse(def.parse, raw, label, index);
908
+ return coerceValue(raw, def.type, label);
909
+ }
910
+ function resolveArgs(argsDef, positionals, coerce) {
911
+ const resolved = {};
912
+ let index = 0;
913
+ for (const def of argsDef) {
914
+ const { name } = def;
915
+ const label = `<${name}>`;
916
+ if (def.variadic) {
917
+ resolved[name] = positionals.slice(index).map((v, i) => coerce(def, v, label, i));
918
+ index = positionals.length;
919
+ } else if (index < positionals.length) {
920
+ resolved[name] = coerce(def, positionals[index], label);
921
+ index++;
922
+ } else resolved[name] = resolveDefault(def, label);
923
+ }
924
+ return {
925
+ args: resolved,
926
+ consumed: index
927
+ };
928
+ }
929
+ /**
930
+ * Enforce `noNegate` at parse time.
931
+ *
932
+ * `--no-<spelling>` works for the canonical name and every long alias
933
+ * (an alias is a perfect synonym), but a boolean that
934
+ * opted out via `noNegate` rejects every negated spelling. Without this
935
+ * pre-scan, `util.parseArgs` (`allowNegative`) would silently accept it.
936
+ */
937
+ function validateNoNegateUsage(argv, spellings) {
938
+ for (const arg of argv) {
939
+ if (arg === "--") return;
940
+ if (!arg.startsWith("--no-")) continue;
941
+ const assignmentIndex = arg.indexOf("=");
942
+ const rawName = assignmentIndex === -1 ? arg.slice(5) : arg.slice(5, assignmentIndex);
943
+ const spelling = spellings.get(rawName);
944
+ if (!spelling || spelling.def.type !== "boolean" || spelling.negatable) continue;
945
+ throw new CrustError("PARSE", `Flag "--${spelling.canonicalName}" does not support negation ("--no-${rawName}")`);
946
+ }
947
+ }
948
+ function tokenizeArgv(command, argv) {
949
+ const spellings = command.flagSpellings;
950
+ const { options: parseOptions, aliasToName } = buildParseArgsOptionDescriptor(spellings);
951
+ validateNoNegateUsage(argv, spellings);
952
+ let parsed;
953
+ try {
954
+ parsed = parseArgs({
955
+ args: argv,
956
+ options: parseOptions,
957
+ strict: true,
958
+ allowPositionals: true,
959
+ allowNegative: true,
960
+ tokens: true
961
+ });
962
+ } catch (error) {
963
+ if (error instanceof Error) {
964
+ const unknownMatch = error.message.match(/Unknown option '(.+?)'/);
965
+ if (unknownMatch) throw new CrustError("PARSE", `Unknown flag "${unknownMatch[1]}"`).withCause(error);
966
+ if ("code" in error && error.code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE" && error.message.length > 0) throw new CrustError("PARSE", error.message).withCause(error);
967
+ }
968
+ throw new CrustError("PARSE", "Failed to parse command arguments").withCause(error);
969
+ }
970
+ const rawArgs = [];
971
+ const preSeparatorPositionals = [];
972
+ let afterSeparator = false;
973
+ for (const token of parsed.tokens) {
974
+ if (token.kind === "option-terminator") {
975
+ afterSeparator = true;
976
+ continue;
977
+ }
978
+ if (token.kind === "positional") (afterSeparator ? rawArgs : preSeparatorPositionals).push(token.value);
979
+ }
980
+ return {
981
+ positionals: preSeparatorPositionals,
982
+ flagValues: resolveAliases(parsed.tokens, aliasToName, command.effectiveFlags),
983
+ rawArgs
984
+ };
985
+ }
986
+ function validateStructuredValue(def, value, label) {
987
+ const type = def.schema ? def.type === "boolean" ? "boolean" : "string" : def.type;
988
+ let valid;
989
+ if (type === "url") valid = value instanceof URL;
990
+ else if (type === "json") {
991
+ const pending = [value];
992
+ const seen = /* @__PURE__ */ new Set();
993
+ valid = true;
994
+ while (pending.length > 0) {
995
+ const item = pending.pop();
996
+ if (item instanceof URL) {
997
+ valid = false;
998
+ break;
999
+ }
1000
+ if (Array.isArray(item) && !seen.has(item)) {
1001
+ seen.add(item);
1002
+ pending.push(...item);
1003
+ }
1004
+ }
1005
+ } else valid = typeof value === (type === "path" ? "string" : type);
1006
+ if (!valid) throw new CrustError("PARSE", `Expected ${type} for ${label}`);
1007
+ if (value === false && "noNegate" in def && def.noNegate) throw new CrustError("PARSE", `Flag "${label}" does not support negation`);
1008
+ }
1009
+ /** Validate supplied structured values before applying transforms. */
1010
+ function coerceStructuredValue(def, value, label, index) {
1011
+ validateStructuredValue(def, value, label);
1012
+ if (def.choices) validateChoice(String(value), def.choices, label);
1013
+ if (def.parse) return invokeParse(def.parse, String(value), label, index);
1014
+ if (def.type === "path") return coercePath(String(value));
1015
+ return value;
1016
+ }
1017
+ function coerceStructuredFlag(name, def, value) {
1018
+ const label = `--${name}`;
1019
+ if (def.multiple) {
1020
+ if (!Array.isArray(value)) throw new CrustError("PARSE", `Expected an occurrence array for ${label}`);
1021
+ return value.map((item, i) => coerceStructuredValue(def, item, label, i));
1022
+ }
1023
+ return coerceStructuredValue(def, value, label);
1024
+ }
1025
+ /** Both front doors share binding, defaults, and canonical flag validation. */
1026
+ function bind(command, positionals, flagValues, coerceArg, coerceFlag) {
1027
+ const flags = resolveFlags(command.effectiveFlags, flagValues, coerceFlag);
1028
+ return {
1029
+ ...resolveArgs(command.args, positionals, coerceArg),
1030
+ flags
1031
+ };
1032
+ }
1033
+ /**
1034
+ * Parse argv against a command's arg/flag definitions.
1035
+ *
1036
+ * Wraps Node's `util.parseArgs` with Crust's enhanced semantics:
1037
+ * positional arg mapping, type coercion, alias expansion, default values,
1038
+ * variadic args, and strict mode.
1039
+ *
1040
+ * This is a pure parse+coerce function — it never throws for missing required
1041
+ * values. Use {@link validateParsed} to enforce required constraints after
1042
+ * extensions have had a chance to finish an invocation (e.g. `--help`).
1043
+ *
1044
+ * @param command - The command whose arg/flag definitions drive the parsing
1045
+ * @param argv - The argv array to parse (typically `process.argv.slice(2)`)
1046
+ * @returns Parsed args, flags, excessArgs (positionals before `--` not consumed by a declared argument), and rawArgs (everything after `--`)
1047
+ * @throws {CrustError} On unknown flags or type coercion failure
1048
+ */
1049
+ function parseArgs$1(command, argv) {
1050
+ const { positionals, flagValues, rawArgs } = tokenizeArgv(command, argv);
1051
+ const { args, flags, consumed } = bind(command, positionals, flagValues, coerceArgToken, coerceFlagValue);
1052
+ return {
1053
+ args,
1054
+ flags,
1055
+ excessArgs: positionals.slice(consumed),
1056
+ rawArgs
1057
+ };
1058
+ }
1059
+ /** Bind typed input without producing argv; the path alone selects the command. */
1060
+ function parseStructured(command, input) {
1061
+ const { args: inputArgs, flags: inputFlags, raw } = input;
1062
+ const positionals = [];
1063
+ let omittedArgument;
1064
+ for (const definition of command.args) {
1065
+ const value = inputArgs && Object.hasOwn(inputArgs, definition.name) ? inputArgs[definition.name] : void 0;
1066
+ if (value === void 0) {
1067
+ omittedArgument = definition.name;
1068
+ continue;
1069
+ }
1070
+ if (omittedArgument !== void 0) throw new CrustError("PARSE", `Argument <${definition.name}> cannot be provided after omitted argument <${omittedArgument}>`, {
1071
+ argument: definition.name,
1072
+ reason: "positional-gap"
1073
+ });
1074
+ if (definition.variadic) {
1075
+ if (!Array.isArray(value)) throw new CrustError("PARSE", `Expected an occurrence array for <${definition.name}>`);
1076
+ positionals.push(...value);
1077
+ } else positionals.push(value);
1078
+ }
1079
+ for (const name of Object.keys(inputArgs ?? {})) if (!command.args.some((definition) => definition.name === name) && inputArgs?.[name] !== void 0) throw new CrustError("PARSE", `Unknown argument "${name}"`, {
1080
+ argument: name,
1081
+ reason: "unknown-argument"
1082
+ });
1083
+ const { args, flags } = bind(command, positionals, inputFlags ?? {}, coerceStructuredValue, coerceStructuredFlag);
1084
+ return {
1085
+ args,
1086
+ flags,
1087
+ excessArgs: [],
1088
+ rawArgs: [...raw ?? []]
1089
+ };
1090
+ }
1091
+ /**
1092
+ * Validate a parse result against its command's required-value constraints.
1093
+ *
1094
+ * Separated from {@link parseArgs} so that middleware (e.g. `--help`) can
1095
+ * inspect the parse result before validation errors are surfaced.
1096
+ *
1097
+ * @param command - The command whose definitions drive the validation
1098
+ * @param parsed - The parse result from {@link parseArgs}
1099
+ * @throws {CrustError} On missing required args or flags
1100
+ */
1101
+ function validateParsed(command, parsed) {
1102
+ const argsDef = command.args;
1103
+ const flagsDef = command.effectiveFlags;
1104
+ const args = parsed.args;
1105
+ const flags = parsed.flags;
1106
+ if (parsed.excessArgs.length > 0) throw new CrustError("VALIDATION", `Unexpected positional argument${parsed.excessArgs.length === 1 ? "" : "s"}: ${parsed.excessArgs.map((arg) => JSON.stringify(arg)).join(", ")}`);
1107
+ for (const def of argsDef) {
1108
+ const { name } = def;
1109
+ const label = `argument "<${name}>"`;
1110
+ const value = args[name];
1111
+ if (def.required === true && def.default === void 0) {
1112
+ if (def.variadic) {
1113
+ if (!Array.isArray(value) || value.length === 0) throw new CrustError("VALIDATION", `Missing required ${label}`);
1114
+ } else if (value === void 0) throw new CrustError("VALIDATION", `Missing required ${label}`);
1115
+ }
1116
+ }
1117
+ validateRequiredFlags(flagsDef, flags);
1118
+ }
1119
+ //#endregion
1120
+ //#region ../utils/src/schema.ts
1121
+ /**
1122
+ * Format an issue path into a dot-path string.
1123
+ *
1124
+ * - Numeric keys (array indexes) are rendered with bracket notation: `items[0]`
1125
+ * - String/symbol keys are joined with dots: `flags.verbose`
1126
+ * - An empty path array produces an empty string (root-level issue)
1127
+ *
1128
+ * @example
1129
+ * ```ts
1130
+ * formatPath(["flags", "verbose"]);
1131
+ * // => "flags.verbose"
1132
+ *
1133
+ * formatPath(["args", 0]);
1134
+ * // => "args[0]"
1135
+ *
1136
+ * formatPath([]);
1137
+ * // => ""
1138
+ * ```
1139
+ */
1140
+ function isNumericPathSegment(segment) {
1141
+ return typeof segment === "number";
1142
+ }
1143
+ function formatPath(path) {
1144
+ return path.map((segment, index) => isNumericPathSegment(segment) ? `[${segment}]` : index > 0 ? `.${String(segment)}` : String(segment)).join("");
1145
+ }
1146
+ /**
1147
+ * Normalize a Standard Schema issue path to an array of `PropertyKey`.
1148
+ *
1149
+ * Standard Schema paths contain either bare `PropertyKey` values or
1150
+ * `{ key: PropertyKey }` segment objects; both forms are flattened to
1151
+ * plain `PropertyKey`. Returns an empty array for a root-level issue
1152
+ * (`undefined`).
1153
+ */
1154
+ function isStandardPathSegment(segment) {
1155
+ return typeof segment === "object" && segment !== null && "key" in segment;
1156
+ }
1157
+ function normalizeStandardPath(path) {
1158
+ if (!path) return [];
1159
+ return path.map((segment) => isStandardPathSegment(segment) ? segment.key : segment);
1160
+ }
1161
+ /**
1162
+ * Normalize Standard Schema issues into canonical `ValidationIssue` objects.
1163
+ *
1164
+ * Applies an optional prefix (e.g. `["flags", "verbose"]`) to each issue
1165
+ * path, then formats each path to its canonical dot-path string.
1166
+ *
1167
+ * @param issues — Raw Standard Schema issues from a failed validation
1168
+ * @param prefix — Optional path segments prepended to each issue path
1169
+ */
1170
+ function normalizeStandardIssues(issues, prefix = []) {
1171
+ return issues.map((issue) => {
1172
+ const resolvedPath = normalizeStandardPath(issue.path);
1173
+ const fullPath = [...prefix, ...resolvedPath];
1174
+ return {
1175
+ message: issue.message,
1176
+ path: formatPath(fullPath)
1177
+ };
1178
+ });
1179
+ }
1180
+ //#endregion
1181
+ //#region src/parsing/schema.ts
1182
+ async function runSchema(schema, raw, path, issues) {
1183
+ let result = schema["~standard"].validate(raw);
1184
+ if (result instanceof Promise) result = await result;
1185
+ if (result.issues) {
1186
+ issues.push(...normalizeStandardIssues(result.issues, path));
1187
+ return { ok: false };
1188
+ }
1189
+ return {
1190
+ ok: true,
1191
+ value: result.value
1192
+ };
1193
+ }
1194
+ /**
1195
+ * Apply Standard Schemas declared on arg/flag definitions.
1196
+ *
1197
+ * Runs after syntax parsing and structural validation: each schema receives
1198
+ * the raw parsed value (`string | undefined` for args, the raw token value
1199
+ * for flags, arrays for variadic/multiple) and exclusively owns coercion,
1200
+ * defaults, requiredness, and validation. Returns transformed copies of
1201
+ * `args`/`flags`; definitions without a schema pass through unchanged.
1202
+ *
1203
+ * @throws {CrustError} `VALIDATION` aggregating every schema issue
1204
+ */
1205
+ async function applySchemas(node, parsed) {
1206
+ const issues = [];
1207
+ const args = new Map(Object.entries(parsed.args));
1208
+ const flags = new Map(Object.entries(parsed.flags));
1209
+ for (const def of node.args) {
1210
+ if (def.schema === void 0) continue;
1211
+ const result = await runSchema(def.schema, args.get(def.name), ["args", def.name], issues);
1212
+ if (result.ok) args.set(def.name, result.value);
1213
+ }
1214
+ for (const [name, def] of Object.entries(node.effectiveFlags)) {
1215
+ if (def.schema === void 0) continue;
1216
+ const result = await runSchema(def.schema, flags.get(name), ["flags", name], issues);
1217
+ if (result.ok) flags.set(name, result.value);
1218
+ }
1219
+ if (issues.length > 0) throw new CrustError("VALIDATION", `Invalid input:\n${issues.map((issue) => ` - ${issue.path}: ${issue.message}`).join("\n")}`, { issues });
1220
+ return {
1221
+ args: Object.fromEntries(args),
1222
+ flags: Object.fromEntries(flags)
1223
+ };
1224
+ }
1225
+ //#endregion
1226
+ //#region src/sections.ts
1227
+ /** Whether a command belongs in user-facing listings. */
1228
+ function isListed(command) {
1229
+ return command.meta.hidden !== true;
1230
+ }
1231
+ /** Select and merge sections visible to the given consumer. */
1232
+ function sectionsFor(sections, consumer) {
1233
+ const visible = (sections ?? []).filter((section) => {
1234
+ if (section.only) return section.only.includes(consumer);
1235
+ if (section.except) return !section.except.includes(consumer);
1236
+ return true;
1237
+ });
1238
+ const merged = /* @__PURE__ */ new Map();
1239
+ for (const section of visible) {
1240
+ const existing = merged.get(section.title);
1241
+ merged.set(section.title, existing ? {
1242
+ title: section.title,
1243
+ body: `${existing.body}\n${section.body}`
1244
+ } : section);
1245
+ }
1246
+ return [...merged.values()];
1247
+ }
1248
+ /** Collect section-bearing visible commands in canonical path order. The root path is `[]`. */
1249
+ function visibleSectionsFor(snapshot, consumer) {
1250
+ const groups = [];
1251
+ function visit(command, path) {
1252
+ const sections = sectionsFor(command.meta.sections, consumer);
1253
+ if (sections.length > 0) groups.push({
1254
+ path,
1255
+ sections
1256
+ });
1257
+ for (const [name, child] of Object.entries(command.subCommands).sort(([a], [b]) => a.localeCompare(b))) if (isListed(child)) visit(child, [...path, name]);
1258
+ }
1259
+ visit(snapshot, []);
1260
+ return groups;
1261
+ }
1262
+ //#endregion
1263
+ //#region src/command/snapshot.ts
1264
+ /** Drop keys with `undefined` values so snapshots serialize cleanly, then freeze. */
1265
+ function freezeCompact(obj) {
1266
+ for (const key of Object.keys(obj)) if (obj[key] === void 0) delete obj[key];
1267
+ return Object.freeze(obj);
1268
+ }
1269
+ function serializableDefault(value) {
1270
+ if (value instanceof URL) return value.href;
1271
+ if (Array.isArray(value)) return Object.freeze(value.map(serializableDefault));
1272
+ return value;
1273
+ }
1274
+ function snapshotArg(def) {
1275
+ return freezeCompact({
1276
+ name: def.name,
1277
+ type: def.type,
1278
+ description: def.description,
1279
+ required: def.required,
1280
+ variadic: def.variadic,
1281
+ choices: def.choices ? Object.freeze([...def.choices]) : void 0,
1282
+ default: serializableDefault(def.default)
1283
+ });
1284
+ }
1285
+ function snapshotFlag(def) {
1286
+ return freezeCompact({
1287
+ type: def.type,
1288
+ description: def.description,
1289
+ short: def.short,
1290
+ aliases: def.aliases ? Object.freeze([...def.aliases]) : void 0,
1291
+ required: def.required,
1292
+ multiple: def.multiple,
1293
+ negatable: isFlagNegatable(def),
1294
+ noNegate: "noNegate" in def ? def.noNegate : void 0,
1295
+ choices: def.choices ? Object.freeze([...def.choices]) : void 0,
1296
+ default: serializableDefault(def.default)
1297
+ });
1298
+ }
1299
+ /**
1300
+ * Project an internal command node (and its whole subtree) into a
1301
+ * {@link CommandSnapshot}.
1302
+ */
1303
+ function snapshotCommand(node) {
1304
+ const flags = {};
1305
+ for (const [name, def] of Object.entries(node.effectiveFlags)) flags[name] = snapshotFlag(def);
1306
+ const subCommands = {};
1307
+ for (const [name, sub] of Object.entries(node.subCommands)) subCommands[name] = snapshotCommand(sub);
1308
+ return Object.freeze({
1309
+ meta: freezeCompact({
1310
+ name: node.meta.name,
1311
+ description: node.meta.description,
1312
+ version: node.meta.version,
1313
+ usage: node.meta.usage,
1314
+ sections: node.meta.sections ? Object.freeze(node.meta.sections.map((section) => Object.freeze({ ...section }))) : void 0,
1315
+ aliases: node.meta.aliases ? Object.freeze([...node.meta.aliases]) : void 0,
1316
+ hidden: node.meta.hidden
1317
+ }),
1318
+ hasAction: node.run !== void 0,
1319
+ args: Object.freeze(node.args.map(snapshotArg)),
1320
+ flags: Object.freeze(flags),
1321
+ subCommands: Object.freeze(subCommands)
1322
+ });
1323
+ }
1324
+ //#endregion
1325
+ //#region src/command/router.ts
1326
+ /**
1327
+ * Find a sibling whose `meta.aliases` contains the given candidate. Returns
1328
+ * the canonical sibling key and node when matched, otherwise `null`.
1329
+ *
1330
+ * Resolution intentionally records the **canonical** key on the
1331
+ * `CommandRoute.commandPath`, never the alias the user typed. This is
1332
+ * load-bearing: error messages, help titles, and downstream extensions read
1333
+ * `commandPath` and assume canonical names.
1334
+ */
1335
+ function findAliasMatch(subCommands, candidate) {
1336
+ for (const [name, node] of Object.entries(subCommands)) {
1337
+ const aliases = node.meta.aliases;
1338
+ if (!aliases) continue;
1339
+ if (aliases.includes(candidate)) return {
1340
+ canonicalName: name,
1341
+ node
1342
+ };
1343
+ }
1344
+ return null;
1345
+ }
1346
+ /**
1347
+ * Match a dash token against the parser's shared spelling table.
1348
+ *
1349
+ * The token walk stays here because routing stops at unknown flags while
1350
+ * parsing reports them as errors; only spelling and negation policy is shared.
1351
+ */
1352
+ function matchKnownFlagToken(spellings, token) {
1353
+ if (token === "--") return null;
1354
+ if (token.startsWith("--")) {
1355
+ const eq = token.indexOf("=");
1356
+ const spelling = eq === -1 ? token.slice(2) : token.slice(2, eq);
1357
+ const entry = spellings.get(spelling);
1358
+ if (entry) return { consumesValue: entry.def.type !== "boolean" && eq === -1 };
1359
+ if (spelling.startsWith("no-")) {
1360
+ if (spellings.get(spelling.slice(3))?.negatable) return { consumesValue: false };
1361
+ }
1362
+ return null;
1363
+ }
1364
+ const chars = token.slice(1);
1365
+ if (chars.length === 0) return null;
1366
+ for (let index = 0; index < chars.length; index++) {
1367
+ const entry = spellings.get(chars[index]);
1368
+ if (!entry) return null;
1369
+ if (entry.def.type !== "boolean") return { consumesValue: index === chars.length - 1 };
1370
+ }
1371
+ return { consumesValue: false };
1372
+ }
1373
+ /**
1374
+ * Resolve a command from an argv array by walking the subcommand tree.
1375
+ *
1376
+ * 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.
1379
+ *
1380
+ * Resolution rules:
1381
+ * 1. If `argv[0]` matches a subcommand key, recurse into that subcommand
1382
+ * 2. If `argv[0]` matches a sibling's `meta.aliases` entry, recurse into
1383
+ * that sibling and record the **canonical** name in `commandPath`
1384
+ * 3. If no match and the current command has `run()`, return it (args passed to parser)
1385
+ * 3a. Known flags encountered before a subcommand name are set aside and
1386
+ * re-prepended for the resolved command's parser — but only if every
1387
+ * command routing descends into recognizes them with the same token
1388
+ * shape. A flag the subcommand cannot parse (e.g. a parent-local flag)
1389
+ * is a PARSE error at the descend, never a silent forward-then-fail.
1390
+ * 4. If no match and the current command has NO `run()`, it signals the caller
1391
+ * should show help (the `showHelp` flag is set in the result)
1392
+ * 5. Unknown subcommands produce a structured COMMAND_NOT_FOUND error whose
1393
+ * `details.available` lists visible canonical sibling names (aliases are
1394
+ * discoverable via `details.parentCommand.subCommands[name].meta.aliases`)
1395
+ *
1396
+ * Implementation: linear scan over siblings on miss. Command trees are small
1397
+ * and resolution runs once per invocation, so the cost is negligible compared
1398
+ * to building/freezing a parallel alias→canonical map. The scan does NOT
1399
+ * mutate `CommandNode`.
1400
+ *
1401
+ * @param command - The root command to resolve from
1402
+ * @param argv - The argv array to resolve against
1403
+ * @returns The resolved command, argv, and the command path
1404
+ * @throws {CrustError} COMMAND_NOT_FOUND when an unknown subcommand is given and the parent has no run()
1405
+ * @throws {CrustError} PARSE when a flag set aside during routing is not parseable by the subcommand being descended into
1406
+ */
1407
+ function resolveCommand(command, argv) {
1408
+ const path = [command.meta.name];
1409
+ let current = command;
1410
+ let routedArgv = argv;
1411
+ const skippedFlagTokens = [];
1412
+ const skippedFlagChecks = [];
1413
+ const assertFlagsForwardable = (child, candidate) => {
1414
+ if (skippedFlagChecks.length === 0) return;
1415
+ const childSpellings = child.flagSpellings;
1416
+ for (const { token, consumesValue } of skippedFlagChecks) {
1417
+ const match = matchKnownFlagToken(childSpellings, token);
1418
+ if (match !== null && match.consumesValue === consumesValue) continue;
1419
+ throw new CrustError("PARSE", `Flag "${token}" cannot be used before subcommand "${candidate}" because "${candidate}" does not accept it.`, {
1420
+ flag: token,
1421
+ reason: "flag-not-forwardable"
1422
+ });
1423
+ }
1424
+ };
1425
+ while (routedArgv.length > 0) {
1426
+ const subCommands = current.subCommands;
1427
+ if (Object.keys(subCommands).length === 0) break;
1428
+ const candidate = routedArgv[0];
1429
+ if (!candidate) break;
1430
+ if (candidate.startsWith("-")) {
1431
+ const match = matchKnownFlagToken(current.flagSpellings, candidate);
1432
+ if (!match) break;
1433
+ skippedFlagTokens.push(candidate);
1434
+ skippedFlagChecks.push({
1435
+ token: candidate,
1436
+ consumesValue: match.consumesValue
1437
+ });
1438
+ routedArgv = routedArgv.slice(1);
1439
+ const value = routedArgv[0];
1440
+ if (match.consumesValue && value !== void 0) {
1441
+ skippedFlagTokens.push(value);
1442
+ routedArgv = routedArgv.slice(1);
1443
+ }
1444
+ continue;
1445
+ }
1446
+ if (Object.hasOwn(subCommands, candidate) && subCommands[candidate]) {
1447
+ assertFlagsForwardable(subCommands[candidate], candidate);
1448
+ current = subCommands[candidate];
1449
+ path.push(candidate);
1450
+ routedArgv = routedArgv.slice(1);
1451
+ continue;
1452
+ }
1453
+ const aliasMatch = findAliasMatch(subCommands, candidate);
1454
+ if (aliasMatch) {
1455
+ assertFlagsForwardable(aliasMatch.node, candidate);
1456
+ current = aliasMatch.node;
1457
+ path.push(aliasMatch.canonicalName);
1458
+ routedArgv = routedArgv.slice(1);
1459
+ continue;
1460
+ }
1461
+ if (current.run) break;
1462
+ const parentCommand = snapshotCommand(current);
1463
+ throw new CrustError("COMMAND_NOT_FOUND", `Unknown command "${candidate}".`, {
1464
+ input: candidate,
1465
+ available: Object.entries(parentCommand.subCommands).flatMap(([name, child]) => isListed(child) ? [name] : []),
1466
+ commandPath: [...path],
1467
+ parentCommand
1468
+ });
1469
+ }
1470
+ return {
1471
+ command: current,
1472
+ argv: [...skippedFlagTokens, ...routedArgv],
1473
+ commandPath: path
1474
+ };
1475
+ }
1476
+ //#endregion
1477
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/usingCtx.js
1478
+ function _usingCtx() {
1479
+ var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
1480
+ var n = Error();
1481
+ return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
1482
+ }, e = {}, n = [];
1483
+ function using(r, e) {
1484
+ if (null != e) {
1485
+ if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
1486
+ if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
1487
+ if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
1488
+ if ("function" != typeof o) throw new TypeError("Object is not disposable.");
1489
+ t && (o = function o() {
1490
+ try {
1491
+ t.call(e);
1492
+ } catch (r) {
1493
+ return Promise.reject(r);
1494
+ }
1495
+ }), n.push({
1496
+ v: e,
1497
+ d: o,
1498
+ a: r
1499
+ });
1500
+ } else r && n.push({
1501
+ d: e,
1502
+ a: r
1503
+ });
1504
+ return e;
1505
+ }
1506
+ return {
1507
+ e,
1508
+ u: using.bind(null, !1),
1509
+ a: using.bind(null, !0),
1510
+ d: function d() {
1511
+ var o, t = this.e, s = 0;
1512
+ function next() {
1513
+ for (; o = n.pop();) try {
1514
+ if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
1515
+ if (o.d) {
1516
+ var r = o.d.call(o.v);
1517
+ if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
1518
+ } else s |= 1;
1519
+ } catch (r) {
1520
+ return err(r);
1521
+ }
1522
+ if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
1523
+ if (t !== e) throw t;
1524
+ }
1525
+ function err(n) {
1526
+ return t = t !== e ? new r(n, t) : n, next();
1527
+ }
1528
+ return next();
1529
+ }
1530
+ };
1531
+ }
1532
+ //#endregion
1533
+ //#region src/command/invocation.ts
1534
+ /** Terminal defaults: line-oriented writes to the process streams. */
1535
+ const DEFAULT_IO = {
1536
+ stdout: (text) => console.log(text),
1537
+ stderr: (text) => console.error(text)
1538
+ };
1539
+ /**
1540
+ * Snapshot subprocess protocol used by first-party build tooling.
1541
+ *
1542
+ * When set to a non-empty file path, `.execute()` prepares the command tree,
1543
+ * validates its documentation sections, optionally runs Extension build hooks when the
1544
+ * build output directory is set, writes its final JSON snapshot and Build Report, and exits
1545
+ * without dispatching a Command Action. In-process callers use `Crust.snapshot()`.
1546
+ */
1547
+ const SNAPSHOT_PATH_ENV = "CRUST_INTERNAL_SNAPSHOT_PATH";
1548
+ const BUILD_OUT_DIR_ENV = "CRUST_INTERNAL_BUILD_OUT_DIR";
1549
+ const EXIT_CODE_CANCELLED = 130;
1550
+ function isAbortError(error) {
1551
+ if (!(error instanceof Error)) return false;
1552
+ return error.name === "AbortError";
1553
+ }
1554
+ function freezeTree(node) {
1555
+ Object.freeze(node);
1556
+ Object.freeze(node.localFlags);
1557
+ Object.freeze(node.ownedFlags);
1558
+ Object.freeze(node.effectiveFlags);
1559
+ if (node.meta.sections) Object.freeze(node.meta.sections);
1560
+ Object.freeze(node.meta);
1561
+ Object.freeze(node.contexts);
1562
+ Object.freeze(node.extensions);
1563
+ Object.freeze(node.args);
1564
+ for (const sub of Object.values(node.subCommands)) freezeTree(sub);
1565
+ Object.freeze(node.subCommands);
1566
+ }
1567
+ function isSymbol(value) {
1568
+ return typeof value === "symbol";
1569
+ }
1570
+ function normalizeArtifactPath(path) {
1571
+ const normalized = posix.normalize(path.replaceAll("\\", "/"));
1572
+ const drivePrefix = /^[A-Za-z]:/;
1573
+ if (posix.isAbsolute(normalized) || win32.isAbsolute(path) || win32.isAbsolute(normalized) || drivePrefix.test(path) || drivePrefix.test(normalized)) throw new Error(`Artifact path "${path}" must be relative to outDir.`);
1574
+ if (normalized === ".." || normalized.startsWith("../")) throw new Error(`Artifact path "${path}" escapes outDir.`);
1575
+ if (normalized === "." || normalized === "./") throw new Error(`Artifact path "${path}" must name a file inside outDir.`);
1576
+ return normalized;
1577
+ }
1578
+ const preparedInvocations = /* @__PURE__ */ new WeakMap();
1579
+ /** Clone and apply Extension commands and flags; recipes run exactly once here. */
1580
+ function buildExtensionTree(node, materializeCommandDefinition) {
1581
+ const rootNode = cloneCommandNode(node);
1582
+ const extensions = Object.freeze([...node.extensions]);
1583
+ for (const extension of extensions) applyExtensionCommands(rootNode, extension, materializeCommandDefinition);
1584
+ for (const extension of extensions) applyExtensionFlags(rootNode, extension);
1585
+ return {
1586
+ rootNode,
1587
+ extensions
1588
+ };
1589
+ }
1590
+ /** Evaluate Extension section callbacks against current state and freeze the tree. */
1591
+ function applySectionsAndFreeze(rootNode, extensions) {
1592
+ const authoredSnapshot = snapshotCommand(rootNode);
1593
+ for (const extension of extensions) applyExtensionSections(rootNode, extension, authoredSnapshot);
1594
+ freezeTree(rootNode);
1595
+ return rootNode;
1596
+ }
1597
+ /** Clone, apply Extensions and sections, freeze, and cache ordinary invocation preparation. */
1598
+ function prepareInvocation(node, materializeCommandDefinition) {
1599
+ const cached = preparedInvocations.get(node);
1600
+ if (cached) return cached;
1601
+ const prepared = buildExtensionTree(node, materializeCommandDefinition);
1602
+ applySectionsAndFreeze(prepared.rootNode, prepared.extensions);
1603
+ preparedInvocations.set(node, prepared);
1604
+ return prepared;
1605
+ }
1606
+ function resolveArgvInput(root, argv) {
1607
+ const route = resolveCommand(root, [...argv]);
1608
+ return {
1609
+ argv,
1610
+ route,
1611
+ parsed: parseArgs$1(route.command, route.argv)
1612
+ };
1613
+ }
1614
+ function resolveStructuredInput(root, path, input) {
1615
+ const route = resolveCommand(root, [...path]);
1616
+ if (route.argv.length > 0) {
1617
+ const candidate = route.argv[0];
1618
+ const parentCommand = snapshotCommand(route.command);
1619
+ throw new CrustError("COMMAND_NOT_FOUND", `Unknown command "${candidate}".`, {
1620
+ input: candidate,
1621
+ available: Object.entries(parentCommand.subCommands).flatMap(([name, child]) => isListed(child) ? [name] : []),
1622
+ commandPath: route.commandPath,
1623
+ parentCommand
1624
+ });
1625
+ }
1626
+ return {
1627
+ argv: path,
1628
+ route,
1629
+ parsed: parseStructured(route.command, input)
1630
+ };
1631
+ }
1632
+ /** Resolve, parse, and run one invocation without rendering failures. */
1633
+ async function dispatch(input, prepared, io, onExtensionContext, onFailure) {
1634
+ try {
1635
+ var _usingCtx$1 = _usingCtx();
1636
+ const { rootNode, extensions } = prepared;
1637
+ const { argv, route, parsed } = "argv" in input ? resolveArgvInput(rootNode, input.argv) : resolveStructuredInput(rootNode, input.path, input.input);
1638
+ const resolvedNode = route.command;
1639
+ const disposal = _usingCtx$1.a(new DisposalStack());
1640
+ const contexts = resolvedNode.contexts.map(({ instance }) => instance);
1641
+ const resolver = createContextResolver(contexts, io, disposal);
1642
+ const rootSnapshot = snapshotCommand(rootNode);
1643
+ const extensionContext = Object.freeze({
1644
+ argv: [...argv],
1645
+ rootCommand: rootSnapshot,
1646
+ command: resolvedNode === rootNode ? rootSnapshot : snapshotCommand(resolvedNode),
1647
+ commandPath: Object.freeze([...route.commandPath]),
1648
+ args: parsed.args,
1649
+ flags: parsed.flags,
1650
+ rawArgs: parsed.rawArgs,
1651
+ ctx: resolver.bag(extensions.flatMap((extension) => extension.uses ?? [])),
1652
+ finish: finishInvocation,
1653
+ stdout: io.stdout,
1654
+ stderr: io.stderr
1655
+ });
1656
+ onExtensionContext?.(extensionContext);
1657
+ const terminal = async () => {
1658
+ validateParsed(resolvedNode, parsed);
1659
+ const validated = await applySchemas(resolvedNode, parsed);
1660
+ resolver.setValidatedFlags(validated.flags);
1661
+ if (!resolvedNode.run) return;
1662
+ const context = {
1663
+ args: validated.args,
1664
+ flags: validated.flags,
1665
+ ctx: resolver.bag(contexts),
1666
+ rawArgs: parsed.rawArgs,
1667
+ command: extensionContext.command,
1668
+ rootCommand: rootSnapshot,
1669
+ stdout: io.stdout,
1670
+ stderr: io.stderr
1671
+ };
1672
+ return await resolvedNode.run(context);
1673
+ };
1674
+ let result;
1675
+ let outcome = { status: "completed" };
1676
+ try {
1677
+ try {
1678
+ for (const extension of extensions) if (await extension.hooks?.preRun?.(extensionContext) === finishInvocation()) {
1679
+ outcome = {
1680
+ status: "finished",
1681
+ by: extension.id
1682
+ };
1683
+ break;
1684
+ }
1685
+ if (outcome.status !== "finished") result = await terminal();
1686
+ } catch (error) {
1687
+ const by = await onFailure?.(error, extensionContext);
1688
+ outcome = {
1689
+ status: "failed",
1690
+ error,
1691
+ ...by === void 0 ? {} : { by }
1692
+ };
1693
+ }
1694
+ Object.freeze(outcome);
1695
+ let postRunFailed = false;
1696
+ let postRunError;
1697
+ for (const extension of extensions.toReversed()) try {
1698
+ await extension.hooks?.postRun?.(extensionContext, outcome);
1699
+ } catch (error) {
1700
+ if (outcome.status !== "failed" && !postRunFailed) {
1701
+ postRunFailed = true;
1702
+ postRunError = error;
1703
+ }
1704
+ }
1705
+ if (outcome.status === "failed") throw outcome.error;
1706
+ if (postRunFailed) throw postRunError;
1707
+ } finally {
1708
+ await resolver.settle();
1709
+ }
1710
+ return outcome.status === "finished" ? {
1711
+ status: "finished",
1712
+ by: outcome.by
1713
+ } : {
1714
+ status: "completed",
1715
+ result
1716
+ };
1717
+ } catch (_) {
1718
+ _usingCtx$1.e = _;
1719
+ } finally {
1720
+ await _usingCtx$1.d();
1721
+ }
1722
+ }
1723
+ /** Render one failure through Extension onError hooks, ending in Core's default renderer. */
1724
+ async function renderFailure(error, argv, prepared, io, extensionContext, silentDefault = false) {
1725
+ const renderDefault = () => {
1726
+ if (silentDefault) return;
1727
+ const message = error instanceof Error ? error.message : String(error);
1728
+ io.stderr(`Error: ${message}`);
1729
+ };
1730
+ function unavailable(property) {
1731
+ return Promise.reject(new CrustError("DEFINITION", `Context "${String(property)}" cannot be pulled from onError because invocation Contexts have already been disposed.`, {
1732
+ subject: "context",
1733
+ name: String(property),
1734
+ reason: "context-after-disposal"
1735
+ }));
1736
+ }
1737
+ const unavailableContext = new Proxy({}, { get: (_, property) => property === "then" || isSymbol(property) ? void 0 : unavailable(property) });
1738
+ const context = extensionContext ?? Object.freeze({
1739
+ argv: [...argv],
1740
+ rootCommand: snapshotCommand(prepared.rootNode),
1741
+ command: snapshotCommand(prepared.rootNode),
1742
+ commandPath: Object.freeze([prepared.rootNode.meta.name]),
1743
+ args: Object.freeze({}),
1744
+ flags: Object.freeze({}),
1745
+ rawArgs: [],
1746
+ finish: finishInvocation,
1747
+ stdout: io.stdout,
1748
+ stderr: io.stderr,
1749
+ ctx: unavailableContext
1750
+ });
1751
+ try {
1752
+ for (const extension of prepared.extensions) if (await extension.hooks?.onError?.(error, context)) return extension.id;
1753
+ } catch {}
1754
+ renderDefault();
1755
+ }
1756
+ /** Explicitly injected IO opts an invocation into the ambient terminal scope. */
1757
+ function hasInjectedIO(io) {
1758
+ return io !== void 0 && Object.keys(io).length > 0;
1759
+ }
1760
+ /** Quiet programmatic boundary: capture output and the failure escaping the complete lifecycle. */
1761
+ async function runInvocation(node, input, io, materializeCommandDefinition) {
1762
+ const stdout = [];
1763
+ const stderr = [];
1764
+ try {
1765
+ const sinks = { ...io };
1766
+ const resolvedIO = {
1767
+ stdout(text) {
1768
+ stdout.push(text);
1769
+ sinks.stdout?.(text);
1770
+ },
1771
+ stderr(text) {
1772
+ stderr.push(text);
1773
+ sinks.stderr?.(text);
1774
+ }
1775
+ };
1776
+ return {
1777
+ ...await withAmbientTerminalIO(resolvedIO, async () => {
1778
+ return await dispatch(input, prepareInvocation(node, materializeCommandDefinition), resolvedIO);
1779
+ }),
1780
+ stdout: stdout.join("\n"),
1781
+ stderr: stderr.join("\n")
1782
+ };
1783
+ } catch (error) {
1784
+ return {
1785
+ status: "failed",
1786
+ error,
1787
+ stdout: stdout.join("\n"),
1788
+ stderr: stderr.join("\n")
1789
+ };
1790
+ }
1791
+ }
1792
+ /** Terminal CLI boundary: render failures and set the process exit status. */
1793
+ async function executeInvocation(node, options, materializeCommandDefinition) {
1794
+ const argv = options?.argv ?? process.argv.slice(2);
1795
+ const io = {
1796
+ ...DEFAULT_IO,
1797
+ ...options?.io
1798
+ };
1799
+ const snapshotPath = process.env[SNAPSHOT_PATH_ENV];
1800
+ if (snapshotPath) {
1801
+ try {
1802
+ const base = buildExtensionTree(node, materializeCommandDefinition);
1803
+ const takeSnapshot = () => snapshotCommand(applySectionsAndFreeze(cloneCommandNode(base.rootNode), base.extensions));
1804
+ let snapshot = takeSnapshot();
1805
+ const buildOutDir = process.env[BUILD_OUT_DIR_ENV];
1806
+ if (buildOutDir) {
1807
+ const extensions = [];
1808
+ for (const extension of base.extensions) {
1809
+ if (!extension.build) continue;
1810
+ try {
1811
+ const artifacts = await extension.build({
1812
+ snapshot,
1813
+ outDir: buildOutDir
1814
+ });
1815
+ extensions.push({
1816
+ id: extension.id,
1817
+ files: artifacts === void 0 ? "unknown" : artifacts.map(normalizeArtifactPath)
1818
+ });
1819
+ } catch (error) {
1820
+ const message = error instanceof Error ? error.message : String(error);
1821
+ throw new Error(`Extension "${extension.id}" build failed: ${message}`, { cause: error });
1822
+ }
1823
+ snapshot = takeSnapshot();
1824
+ }
1825
+ await writeFile(join(dirname(snapshotPath), "build-report.json"), JSON.stringify({ extensions }));
1826
+ }
1827
+ await writeFile(snapshotPath, JSON.stringify(snapshot));
1828
+ } catch (error) {
1829
+ const message = error instanceof Error ? error.message : String(error);
1830
+ console.error(message);
1831
+ return process.exit(1);
1832
+ }
1833
+ return process.exit(0);
1834
+ }
1835
+ const invoke = async () => {
1836
+ let prepared;
1837
+ try {
1838
+ prepared = prepareInvocation(node, materializeCommandDefinition);
1839
+ } catch (error) {
1840
+ if (isAbortError(error)) {
1841
+ process.exitCode = EXIT_CODE_CANCELLED;
1842
+ return EXIT_CODE_CANCELLED;
1843
+ }
1844
+ const message = error instanceof Error ? error.message : String(error);
1845
+ io.stderr(`Error: ${message}`);
1846
+ process.exitCode = 1;
1847
+ return 1;
1848
+ }
1849
+ let extensionContext;
1850
+ let renderedInDispatch = false;
1851
+ try {
1852
+ await dispatch({ argv }, prepared, io, (context) => {
1853
+ extensionContext = context;
1854
+ }, async (error, context) => {
1855
+ renderedInDispatch = true;
1856
+ const cancelled = isAbortError(error);
1857
+ process.exitCode = cancelled ? EXIT_CODE_CANCELLED : 1;
1858
+ return renderFailure(error, argv, prepared, io, context, cancelled);
1859
+ });
1860
+ } catch (error) {
1861
+ if (isAbortError(error)) {
1862
+ if (!renderedInDispatch) await renderFailure(error, argv, prepared, io, extensionContext, true);
1863
+ process.exitCode = EXIT_CODE_CANCELLED;
1864
+ return EXIT_CODE_CANCELLED;
1865
+ }
1866
+ process.exitCode = 1;
1867
+ if (!renderedInDispatch) await renderFailure(error, argv, prepared, io, extensionContext);
1868
+ return 1;
1869
+ }
1870
+ return 0;
1871
+ };
1872
+ return await (hasInjectedIO(options?.io) ? withAmbientTerminalIO(io, invoke) : invoke());
1873
+ }
1874
+ //#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 };