@crustjs/core 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, join, posix, resolve, win32 } from "node:path";
3
- import { BUILD_OUT_DIR_ENV } from "@crustjs/utils/artifacts";
3
+ import { BUILD_OUT_DIR_ENV, isPackagedBuild } from "@crustjs/utils/artifacts";
4
4
  import { withAmbientTerminalIO } from "@crustjs/utils/terminal";
5
5
  import { parseArgs } from "node:util";
6
6
  import { isPromise } from "node:util/types";
@@ -170,6 +170,74 @@ function normalizeArg(def) {
170
170
  return ownDefinition(def);
171
171
  }
172
172
  //#endregion
173
+ //#region src/sections.ts
174
+ /** Whether a command belongs in user-facing listings. */
175
+ function isListed(command) {
176
+ return command.meta.hidden !== true;
177
+ }
178
+ /** Select and merge sections visible to the given consumer. */
179
+ function sectionsFor(sections, consumer) {
180
+ const visible = (sections ?? []).filter((section) => {
181
+ if (section.only) return section.only.includes(consumer);
182
+ if (section.except) return !section.except.includes(consumer);
183
+ return true;
184
+ });
185
+ const merged = /* @__PURE__ */ new Map();
186
+ for (const section of visible) {
187
+ const existing = merged.get(section.title);
188
+ merged.set(section.title, existing ? {
189
+ title: section.title,
190
+ body: `${existing.body}\n${section.body}`
191
+ } : section);
192
+ }
193
+ return [...merged.values()];
194
+ }
195
+ /** Collect section-bearing visible commands in canonical path order. The root path is `[]`. */
196
+ function visibleSectionsFor(snapshot, consumer) {
197
+ const groups = [];
198
+ function visit(command, path) {
199
+ const sections = sectionsFor(command.meta.sections, consumer);
200
+ if (sections.length > 0) groups.push({
201
+ path,
202
+ sections
203
+ });
204
+ for (const [name, child] of Object.entries(command.subCommands).sort(([a], [b]) => a.localeCompare(b))) if (isListed(child)) visit(child, [...path, name]);
205
+ }
206
+ visit(snapshot, []);
207
+ return groups;
208
+ }
209
+ const sectionOwnerLabels = {
210
+ command: "Command",
211
+ context: "Context",
212
+ extension: "Extension"
213
+ };
214
+ function invalidSections({ subject, name }) {
215
+ const label = sectionOwnerLabels[subject];
216
+ return new CrustError("DEFINITION", `${label} "${name}" contains invalid documentation sections`, {
217
+ subject,
218
+ name,
219
+ reason: "invalid-sections"
220
+ });
221
+ }
222
+ function normalizeSection(section, owner) {
223
+ const { title, body, only, except } = section;
224
+ if (!title.trim() || /[\r\n]/.test(title) || !body.trim() || only?.length === 0 || except?.length === 0 || only !== void 0 && except !== void 0) throw invalidSections(owner);
225
+ const audience = (ids) => {
226
+ return Object.freeze(ids.map((consumer) => typeof consumer === "string" ? consumer : consumer.id));
227
+ };
228
+ return Object.freeze({
229
+ title,
230
+ body,
231
+ ...only ? { only: audience(only) } : except ? { except: audience(except) } : {}
232
+ });
233
+ }
234
+ function validateCommandSections(name, sections, subject = "command") {
235
+ return sections.map((section) => normalizeSection(section, {
236
+ subject,
237
+ name
238
+ }));
239
+ }
240
+ //#endregion
173
241
  //#region src/api/context.ts
174
242
  const defining = Symbol("crust.defining");
175
243
  /** @internal */
@@ -178,7 +246,11 @@ function definingOf(value) {
178
246
  }
179
247
  /** @internal */
180
248
  function seal(value) {
181
- return Object.freeze(Object.assign(value, { [defining]: value }));
249
+ return sealHandle(value, value);
250
+ }
251
+ /** @internal Freeze a public handle whose defining data is a separate record. */
252
+ function sealHandle(handle, data) {
253
+ return Object.freeze(Object.assign(handle, { [defining]: data }));
182
254
  }
183
255
  /**
184
256
  * Adapter hook: the Context sources a bag was built from, exposed as a
@@ -198,87 +270,42 @@ function validateContextAvailability(contexts, sources) {
198
270
  name,
199
271
  reason: "missing-context"
200
272
  });
201
- for (const dependency of source.uses) visit(dependency);
273
+ for (const dependency of source.use) visit(dependency);
202
274
  };
203
275
  for (const source of sources) visit(source);
204
276
  }
205
- function isContextSetup(value) {
206
- return typeof value === "function";
277
+ function createContextBuilder(name, use, ownedFlags, sections) {
278
+ return Object.freeze({
279
+ use: (...factories) => createContextBuilder(name, Object.freeze([...use, ...factories.map(definingOf)]), ownedFlags, sections),
280
+ flags: (...defs) => createContextBuilder(name, use, Object.freeze(toFlagsRecord(defs, ownedFlags)), sections),
281
+ sections: (...inputs) => createContextBuilder(name, use, ownedFlags, Object.freeze([...sections, ...validateCommandSections(name, inputs, "context")])),
282
+ setup: (setup) => createContextFactory(name, use, ownedFlags, sections, setup)
283
+ });
207
284
  }
208
- function defineContext(name, configOrSetup, maybeSetup) {
209
- const hasConfig = !isContextSetup(configOrSetup);
210
- const config = hasConfig ? configOrSetup : {};
211
- const setup = hasConfig ? maybeSetup : configOrSetup;
212
- const ownedFlags = Object.freeze(toFlagsRecord(config.flags ?? []));
213
- const uses = Object.freeze((config.uses ?? []).map(definingOf));
214
- const instance = (instanceUses, run) => {
285
+ function createContextFactory(name, use, ownedFlags, sections, setup) {
286
+ const instance = (instanceUse, run) => {
215
287
  return seal({
216
288
  name,
217
289
  ownedFlags,
218
- uses: instanceUses,
290
+ sections,
291
+ use: instanceUse,
219
292
  factory: sealed,
220
293
  setup: run
221
294
  });
222
295
  };
223
- const factory = (options) => instance(uses, (input) => {
224
- return setup({
225
- options,
226
- ...input
227
- });
228
- });
296
+ const factory = (options) => instance(use, (input) => setup(input, options));
229
297
  factory.contextName = name;
230
- factory.uses = uses;
298
+ factory.use = use;
231
299
  factory.of = (value) => instance(Object.freeze([]), () => value);
232
300
  const sealed = seal(factory);
233
301
  return sealed;
234
302
  }
235
- function isDisposeCallback(value) {
236
- return typeof value === "function";
237
- }
238
303
  /**
239
- * Minimal `AsyncDisposableStack` stand-in for runtimes without the global
240
- * (Node 22): LIFO disposal of used resources and deferred callbacks,
241
- * preferring `Symbol.asyncDispose` over `Symbol.dispose`.
242
- *
243
- * ponytail: multiple disposal errors aggregate as `AggregateError` instead of
244
- * the native `SuppressedError` chain; delete this class when Node 22 leaves
245
- * the support floor.
246
- *
247
- * @internal Exported for unit testing and invocation wiring.
304
+ * Start an immutable fluent Context definition: a named, lazy command
305
+ * dependency. Chain `.use()`, `.flags()`, and `.sections()`, then `.setup()` for the factory.
248
306
  */
249
- var FallbackAsyncDisposableStack = class {
250
- #entries = [];
251
- #disposed = false;
252
- #assertPending() {
253
- if (this.#disposed) throw new ReferenceError("AsyncDisposableStack is already disposed");
254
- }
255
- use(value) {
256
- this.#assertPending();
257
- const dispose = hasAsyncDispose(value) ? value[Symbol.asyncDispose] : value[Symbol.dispose];
258
- this.#entries.push(() => dispose.call(value));
259
- return value;
260
- }
261
- defer(onDisposeAsync) {
262
- this.#assertPending();
263
- if (!isDisposeCallback(onDisposeAsync)) throw new TypeError("defer callback is not callable");
264
- this.#entries.push(onDisposeAsync);
265
- }
266
- async [Symbol.asyncDispose]() {
267
- if (this.#disposed) return;
268
- this.#disposed = true;
269
- const errors = [];
270
- for (let index = this.#entries.length - 1; index >= 0; index--) try {
271
- await this.#entries[index]();
272
- } catch (error) {
273
- errors.push(error);
274
- }
275
- if (errors.length === 1) throw errors[0];
276
- if (errors.length > 1) throw new AggregateError(errors, "Disposal failed");
277
- }
278
- };
279
- const DisposalStack = globalThis.AsyncDisposableStack ?? FallbackAsyncDisposableStack;
280
- function hasAsyncDispose(value) {
281
- return typeof value[Symbol.asyncDispose] === "function";
307
+ function defineContext(name) {
308
+ return createContextBuilder(name, Object.freeze([]), Object.freeze({}), Object.freeze([]));
282
309
  }
283
310
  function isDisposableValue(value) {
284
311
  if (value === null || typeof value !== "object" && typeof value !== "function") return false;
@@ -373,7 +400,7 @@ function createContextResolver(contexts, io, disposal, signal) {
373
400
  ...io,
374
401
  signal,
375
402
  flags: ownedFlags,
376
- ctx: makeBag(context.uses, current),
403
+ ctx: makeBag(context.use, current),
377
404
  defer(cleanup) {
378
405
  if (current.settled) throw new CrustError("DEFINITION", `Context "${name}" cannot register cleanup after its setup has finished.`, {
379
406
  subject: "context",
@@ -414,7 +441,7 @@ function createContextResolver(contexts, io, disposal, signal) {
414
441
  enumerable: true,
415
442
  get: () => makePull(origin)(name)
416
443
  });
417
- for (const dependency of source.uses ?? []) add(dependency);
444
+ for (const dependency of source.use ?? []) add(dependency);
418
445
  };
419
446
  for (const source of sources) add(source);
420
447
  Object.defineProperty(bag, contextSources, { value: Object.freeze([...sources]) });
@@ -444,24 +471,75 @@ const FINISHED = Object.freeze({ [Symbol("crust.finished")]: true });
444
471
  function finishInvocation() {
445
472
  return FINISHED;
446
473
  }
447
- function isExtensionFactory(value) {
448
- return typeof value === "function";
449
- }
450
- function defineExtension(id, config = {}) {
451
- if (id === void 0) return defineExtension;
452
- if (isExtensionFactory(config)) return Object.assign((...args) => defineExtension(id, config(...args)), { id });
453
- const ownedFlags = Object.freeze(toFlagsRecord(config.flags ?? []));
454
- toFlagsRecord((config.provides ?? []).flatMap((instance) => Object.entries(definingOf(instance).ownedFlags).map(([name, def]) => ({
474
+ function createExtension(state) {
475
+ const flags = Object.freeze(toFlagsRecord(state.flags));
476
+ toFlagsRecord(state.provide.flatMap((instance) => Object.entries(instance.ownedFlags).map(([name, def]) => ({
455
477
  ...def,
456
478
  name
457
- }))), ownedFlags);
458
- return seal({
459
- ...config,
460
- uses: Object.freeze((config.uses ?? []).map(definingOf)),
461
- ...config.provides ? { provides: Object.freeze(config.provides.map(definingOf)) } : {},
462
- ...config.commands ? { commands: Object.freeze([...config.commands]) } : {},
479
+ }))), flags);
480
+ const data = Object.freeze({
481
+ id: state.id,
482
+ flags,
483
+ commands: Object.freeze([...state.commands]),
484
+ use: Object.freeze([...state.use]),
485
+ provide: Object.freeze([...state.provide]),
486
+ hooks: Object.freeze({ ...state.hooks }),
487
+ ...state.sections ? { sections: state.sections } : {},
488
+ ...state.build ? { build: state.build } : {}
489
+ });
490
+ const next = (change) => createExtension({
491
+ ...state,
492
+ ...change
493
+ });
494
+ const sealed = sealHandle({
495
+ id: state.id,
496
+ use: (...factories) => next({ use: [...state.use, ...factories.map(definingOf)] }),
497
+ provide: (...instances) => next({ provide: [...state.provide, ...instances.map(definingOf)] }),
498
+ flags: (...defs) => next({ flags: [...state.flags, ...defs.map(ownDefinition)] }),
499
+ add: (...definitions) => next({ commands: [...state.commands, ...definitions] }),
500
+ preRun: (preRun) => next({ hooks: {
501
+ ...state.hooks,
502
+ preRun
503
+ } }),
504
+ postRun: (postRun) => next({ hooks: {
505
+ ...state.hooks,
506
+ postRun
507
+ } }),
508
+ onError: (onError) => next({ hooks: {
509
+ ...state.hooks,
510
+ onError
511
+ } }),
512
+ sections: (sections) => next({ sections }),
513
+ build: (build) => next({ build }),
514
+ factory: (define) => Object.assign((...args) => {
515
+ const extension = define(sealed, ...args);
516
+ const returned = definingOf(extension).id;
517
+ if (returned !== state.id) throw new CrustError("DEFINITION", `Extension factory "${state.id}" returned Extension "${returned}"; return an Extension built from the handle it receives`, {
518
+ subject: "extension",
519
+ name: state.id,
520
+ reason: "factory-id-mismatch"
521
+ });
522
+ return extension;
523
+ }, { id: state.id })
524
+ }, data);
525
+ return sealed;
526
+ }
527
+ /**
528
+ * Start an immutable fluent Extension definition.
529
+ *
530
+ * Extensions apply to the whole application and own the flags and commands
531
+ * they contribute. `defineExtension<"version">(id)` requires root metadata keys
532
+ * without preventing inference of flags, Contexts, or commands; chain
533
+ * `.factory()` for a configurable Extension whose identity is a section consumer.
534
+ */
535
+ function defineExtension(id) {
536
+ return createExtension({
463
537
  id,
464
- ...config.flags === void 0 ? {} : { flags: ownedFlags }
538
+ use: [],
539
+ provide: [],
540
+ flags: [],
541
+ commands: [],
542
+ hooks: {}
465
543
  });
466
544
  }
467
545
  //#endregion
@@ -483,6 +561,7 @@ function createCommandNode(name) {
483
561
  args: [],
484
562
  subCommands: {},
485
563
  contexts: [],
564
+ providedContexts: [],
486
565
  demands: [],
487
566
  extensions: [],
488
567
  run: void 0
@@ -528,11 +607,11 @@ function injectExtensionFlag(node, name, def, recursive) {
528
607
  }
529
608
  /** Attach one Extension's owned root commands to a cloned tree. */
530
609
  function applyExtensionCommands(root, extension, materializeCommandDefinition) {
531
- for (const definition of extension.commands ?? []) root.subCommands[definition.name] = materializeCommandDefinition(definition, root, extension.id);
610
+ for (const definition of extension.commands) root.subCommands[definition.name] = materializeCommandDefinition(definition, root, extension.id);
532
611
  }
533
612
  /** Inject one Extension's owned flags across a cloned tree. */
534
613
  function applyExtensionFlags(root, extension) {
535
- for (const [name, defWithScope] of Object.entries(extension.flags ?? {})) {
614
+ for (const [name, defWithScope] of Object.entries(extension.flags)) {
536
615
  const { recursive = true, ...def } = defWithScope;
537
616
  injectExtensionFlag(root, name, def, recursive);
538
617
  }
@@ -557,36 +636,12 @@ function cloneCommandNode(node) {
557
636
  args: [...node.args],
558
637
  subCommands,
559
638
  contexts: node.contexts.map((context) => ({ ...context })),
639
+ providedContexts: [...node.providedContexts],
560
640
  demands: [...node.demands],
561
641
  extensions: [...node.extensions],
562
642
  run: node.run
563
643
  };
564
644
  }
565
- function invalidSections({ subject, name }) {
566
- return new CrustError("DEFINITION", `${subject === "command" ? "Command" : "Extension"} "${name}" contains invalid documentation sections`, {
567
- subject,
568
- name,
569
- reason: "invalid-sections"
570
- });
571
- }
572
- function normalizeSection(section, owner) {
573
- const { title, body, only, except } = section;
574
- if (!title.trim() || /[\r\n]/.test(title) || !body.trim() || only?.length === 0 || except?.length === 0 || only !== void 0 && except !== void 0) throw invalidSections(owner);
575
- const audience = (ids) => {
576
- return Object.freeze(ids.map((consumer) => typeof consumer === "string" ? consumer : consumer.id));
577
- };
578
- return Object.freeze({
579
- title,
580
- body,
581
- ...only ? { only: audience(only) } : except ? { except: audience(except) } : {}
582
- });
583
- }
584
- function validateCommandSections(name, sections) {
585
- return sections.map((section) => normalizeSection(section, {
586
- subject: "command",
587
- name
588
- }));
589
- }
590
645
  function contributionTarget(root, command, extension) {
591
646
  let target = root;
592
647
  for (const segment of command) {
@@ -613,6 +668,30 @@ function applyExtensionSections(root, extension, snapshot) {
613
668
  target.meta.sections = [...target.meta.sections ?? [], section];
614
669
  }
615
670
  }
671
+ /**
672
+ * Install Context sections where each Context is provided: `.provide()` on its own
673
+ * command only, Extension providers once on the root. Unlike Extension section
674
+ * contributions, a title already on the command is a `DEFINITION` error.
675
+ */
676
+ function applyContextSections(root, extensions) {
677
+ const visit = (node, path, instances) => {
678
+ const effective = [...new Map(instances.map((instance) => [instance.name, instance])).values()];
679
+ if (effective.some((instance) => instance.sections.length > 0)) {
680
+ const titles = new Set((node.meta.sections ?? []).map(({ title }) => title));
681
+ for (const instance of effective) {
682
+ for (const { title } of instance.sections) if (titles.has(title)) throw new CrustError("DEFINITION", `Context "${instance.name}" section "${title}" duplicates a section on command "${path}"`, {
683
+ subject: "context",
684
+ name: instance.name,
685
+ reason: "duplicate-section"
686
+ });
687
+ for (const { title } of instance.sections) titles.add(title);
688
+ }
689
+ node.meta.sections = [...node.meta.sections ?? [], ...effective.flatMap((instance) => instance.sections)];
690
+ }
691
+ for (const [name, child] of Object.entries(node.subCommands)) visit(child, `${path} ${name}`, child.providedContexts);
692
+ };
693
+ visit(root, root.meta.name, [...root.providedContexts, ...extensions.flatMap((extension) => extension.provide)]);
694
+ }
616
695
  function installExtensionContexts(node, extensions, reRegisteredIds) {
617
696
  const cloned = cloneCommandNode(node);
618
697
  const kept = new Set(extensions.flatMap((e) => !reRegisteredIds.has(e.id) && node.extensions.includes(e) ? [e.id] : []));
@@ -636,7 +715,7 @@ function installExtensionContexts(node, extensions, reRegisteredIds) {
636
715
  prune(cloned);
637
716
  for (const extension of extensions) {
638
717
  if (kept.has(extension.id)) continue;
639
- const instances = extension.provides ?? [];
718
+ const instances = extension.provide;
640
719
  if (instances.length === 0) continue;
641
720
  const walk = (target, skip) => {
642
721
  const installed = instances.filter((instance) => !skip.has(instance.name));
@@ -711,6 +790,50 @@ function coerceJson(raw) {
711
790
  }
712
791
  }
713
792
  //#endregion
793
+ //#region src/parsing/schema.ts
794
+ async function runSchema(schema, raw, path, issues) {
795
+ const result = await schema["~standard"].validate(raw);
796
+ if (result.issues) {
797
+ issues.push(...normalizeStandardIssues(result.issues, path));
798
+ return { ok: false };
799
+ }
800
+ return {
801
+ ok: true,
802
+ value: result.value
803
+ };
804
+ }
805
+ /**
806
+ * Apply Standard Schemas declared on arg/flag definitions.
807
+ *
808
+ * Runs after syntax parsing and structural validation: each schema receives
809
+ * the raw parsed value (`string | undefined` for args, the raw token value
810
+ * for flags, arrays for variadic/multiple) and exclusively owns coercion,
811
+ * defaults, requiredness, and validation. Returns transformed copies of
812
+ * `args`/`flags`; definitions without a schema pass through unchanged.
813
+ *
814
+ * @throws {CrustError} `VALIDATION` aggregating every schema issue
815
+ */
816
+ async function applySchemas(node, parsed) {
817
+ const issues = [];
818
+ const args = new Map(Object.entries(parsed.args));
819
+ const flags = new Map(Object.entries(parsed.flags));
820
+ for (const def of node.args) {
821
+ if (def.schema === void 0) continue;
822
+ const result = await runSchema(def.schema, args.get(def.name), ["args", def.name], issues);
823
+ if (result.ok) args.set(def.name, result.value);
824
+ }
825
+ for (const [name, def] of Object.entries(node.effectiveFlags)) {
826
+ if (def.schema === void 0) continue;
827
+ const result = await runSchema(def.schema, flags.get(name), ["flags", name], issues);
828
+ if (result.ok) flags.set(name, result.value);
829
+ }
830
+ if (issues.length > 0) throw new CrustError("VALIDATION", `Invalid input:\n${issues.map((issue) => ` - ${issue.path}: ${issue.message}`).join("\n")}`, { issues });
831
+ return {
832
+ args: Object.fromEntries(args),
833
+ flags: Object.fromEntries(flags)
834
+ };
835
+ }
836
+ //#endregion
714
837
  //#region src/parsing/parser.ts
715
838
  /**
716
839
  * Build the options config for `util.parseArgs` from the shared spelling table.
@@ -945,7 +1068,8 @@ function resolveArgs(argsDef, positionals, coerce) {
945
1068
  configurable: true
946
1069
  });
947
1070
  if (def.variadic) {
948
- resolved[name] = positionals.slice(index).map((v, i) => coerce(def, v, label, i));
1071
+ const remaining = positionals.slice(index);
1072
+ resolved[name] = remaining.length === 0 && def.default !== void 0 ? [resolveDefault(def, label)] : remaining.map((v, i) => coerce(def, v, label, i));
949
1073
  index = positionals.length;
950
1074
  } else if (index < positionals.length) {
951
1075
  resolved[name] = coerce(def, positionals[index], label);
@@ -1088,6 +1212,31 @@ function parseArgs$1(command, argv, env = process.env) {
1088
1212
  rawArgs
1089
1213
  };
1090
1214
  }
1215
+ /**
1216
+ * Resolve flags from their `env` bindings alone, as {@link parseArgs} does for an
1217
+ * empty argv: `env.delimiter` splitting, `choices`, `parse`, built-in coercion,
1218
+ * defaults, requiredness, then Standard Schemas. Flags without `env` resolve
1219
+ * to their defaults.
1220
+ *
1221
+ * @param flags - Flag definitions keyed by result name; each `env.name` selects its variable
1222
+ * @param env - Raw variable text; only own properties are read
1223
+ * @throws {CrustError} `DEFINITION` for a definition `defineFlag` rejects (e.g. a
1224
+ * `__proto__` key or a default outside `choices`); `PARSE` or `VALIDATION` on the first
1225
+ * coercion or requiredness failure, or `VALIDATION` aggregating schema issues. Messages
1226
+ * may quote raw values.
1227
+ */
1228
+ async function parseFlagValues(flags, env) {
1229
+ for (const [name, def] of Object.entries(flags)) normalizeFlag(name, def);
1230
+ const resolved = resolveFlags(flags, applyEnvAndDelimiter(flags, {}, env), coerceFlagValue);
1231
+ validateRequiredFlags(flags, resolved);
1232
+ return (await applySchemas({
1233
+ args: [],
1234
+ effectiveFlags: flags
1235
+ }, {
1236
+ args: {},
1237
+ flags: resolved
1238
+ })).flags;
1239
+ }
1091
1240
  /** Bind typed input without producing argv; the path alone selects the command. */
1092
1241
  function parseStructured(command, input) {
1093
1242
  const { args: inputArgs, flags: inputFlags, raw } = input;
@@ -1149,87 +1298,6 @@ function validateParsed(command, parsed) {
1149
1298
  validateRequiredFlags(flagsDef, flags);
1150
1299
  }
1151
1300
  //#endregion
1152
- //#region src/parsing/schema.ts
1153
- async function runSchema(schema, raw, path, issues) {
1154
- const result = await schema["~standard"].validate(raw);
1155
- if (result.issues) {
1156
- issues.push(...normalizeStandardIssues(result.issues, path));
1157
- return { ok: false };
1158
- }
1159
- return {
1160
- ok: true,
1161
- value: result.value
1162
- };
1163
- }
1164
- /**
1165
- * Apply Standard Schemas declared on arg/flag definitions.
1166
- *
1167
- * Runs after syntax parsing and structural validation: each schema receives
1168
- * the raw parsed value (`string | undefined` for args, the raw token value
1169
- * for flags, arrays for variadic/multiple) and exclusively owns coercion,
1170
- * defaults, requiredness, and validation. Returns transformed copies of
1171
- * `args`/`flags`; definitions without a schema pass through unchanged.
1172
- *
1173
- * @throws {CrustError} `VALIDATION` aggregating every schema issue
1174
- */
1175
- async function applySchemas(node, parsed) {
1176
- const issues = [];
1177
- const args = new Map(Object.entries(parsed.args));
1178
- const flags = new Map(Object.entries(parsed.flags));
1179
- for (const def of node.args) {
1180
- if (def.schema === void 0) continue;
1181
- const result = await runSchema(def.schema, args.get(def.name), ["args", def.name], issues);
1182
- if (result.ok) args.set(def.name, result.value);
1183
- }
1184
- for (const [name, def] of Object.entries(node.effectiveFlags)) {
1185
- if (def.schema === void 0) continue;
1186
- const result = await runSchema(def.schema, flags.get(name), ["flags", name], issues);
1187
- if (result.ok) flags.set(name, result.value);
1188
- }
1189
- if (issues.length > 0) throw new CrustError("VALIDATION", `Invalid input:\n${issues.map((issue) => ` - ${issue.path}: ${issue.message}`).join("\n")}`, { issues });
1190
- return {
1191
- args: Object.fromEntries(args),
1192
- flags: Object.fromEntries(flags)
1193
- };
1194
- }
1195
- //#endregion
1196
- //#region src/sections.ts
1197
- /** Whether a command belongs in user-facing listings. */
1198
- function isListed(command) {
1199
- return command.meta.hidden !== true;
1200
- }
1201
- /** Select and merge sections visible to the given consumer. */
1202
- function sectionsFor(sections, consumer) {
1203
- const visible = (sections ?? []).filter((section) => {
1204
- if (section.only) return section.only.includes(consumer);
1205
- if (section.except) return !section.except.includes(consumer);
1206
- return true;
1207
- });
1208
- const merged = /* @__PURE__ */ new Map();
1209
- for (const section of visible) {
1210
- const existing = merged.get(section.title);
1211
- merged.set(section.title, existing ? {
1212
- title: section.title,
1213
- body: `${existing.body}\n${section.body}`
1214
- } : section);
1215
- }
1216
- return [...merged.values()];
1217
- }
1218
- /** Collect section-bearing visible commands in canonical path order. The root path is `[]`. */
1219
- function visibleSectionsFor(snapshot, consumer) {
1220
- const groups = [];
1221
- function visit(command, path) {
1222
- const sections = sectionsFor(command.meta.sections, consumer);
1223
- if (sections.length > 0) groups.push({
1224
- path,
1225
- sections
1226
- });
1227
- for (const [name, child] of Object.entries(command.subCommands).sort(([a], [b]) => a.localeCompare(b))) if (isListed(child)) visit(child, [...path, name]);
1228
- }
1229
- visit(snapshot, []);
1230
- return groups;
1231
- }
1232
- //#endregion
1233
1301
  //#region src/command/snapshot.ts
1234
1302
  /** Drop keys with `undefined` values so snapshots serialize cleanly, then freeze. */
1235
1303
  function freezeCompact(obj) {
@@ -1449,62 +1517,6 @@ function resolveCommand(command, argv) {
1449
1517
  };
1450
1518
  }
1451
1519
  //#endregion
1452
- //#region \0@oxc-project+runtime@0.148.0/helpers/esm/usingCtx.js
1453
- function _usingCtx() {
1454
- var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
1455
- var n = Error();
1456
- return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
1457
- }, e = {}, n = [];
1458
- function using(r, e) {
1459
- if (null != e) {
1460
- if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
1461
- if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
1462
- if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
1463
- if ("function" != typeof o) throw new TypeError("Object is not disposable.");
1464
- t && (o = function o() {
1465
- try {
1466
- t.call(e);
1467
- } catch (r) {
1468
- return Promise.reject(r);
1469
- }
1470
- }), n.push({
1471
- v: e,
1472
- d: o,
1473
- a: r
1474
- });
1475
- } else r && n.push({
1476
- d: e,
1477
- a: r
1478
- });
1479
- return e;
1480
- }
1481
- return {
1482
- e,
1483
- u: using.bind(null, !1),
1484
- a: using.bind(null, !0),
1485
- d: function d() {
1486
- var o, t = this.e, s = 0;
1487
- function next() {
1488
- for (; o = n.pop();) try {
1489
- if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
1490
- if (o.d) {
1491
- var r = o.d.call(o.v);
1492
- if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
1493
- } else s |= 1;
1494
- } catch (r) {
1495
- return err(r);
1496
- }
1497
- if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
1498
- if (t !== e) throw t;
1499
- }
1500
- function err(n) {
1501
- return t = t !== e ? new r(n, t) : n, next();
1502
- }
1503
- return next();
1504
- }
1505
- };
1506
- }
1507
- //#endregion
1508
1520
  //#region src/command/invocation.ts
1509
1521
  const ignoreStreamError = () => {};
1510
1522
  /**
@@ -1545,7 +1557,9 @@ const DEFAULT_IO = {
1545
1557
  * without dispatching a Command Action. In-process callers use `Crust.snapshot()`.
1546
1558
  *
1547
1559
  * Only source entries run by `crust build` honor it: finished Bun/Node bundles carry
1548
- * `process.env.CRUST_INTERNAL_BUILD === "1"` as a literal and compile the protocol out.
1560
+ * `process.env.CRUST_INTERNAL_BUILD === "1"` as a literal and compile the protocol out;
1561
+ * packaged Deno bundles set the `isPackagedBuild()` marker and skip it without
1562
+ * reading the environment.
1549
1563
  */
1550
1564
  const SNAPSHOT_PATH_ENV = "CRUST_INTERNAL_SNAPSHOT_PATH";
1551
1565
  const EXIT_CODE_CANCELLED = 130;
@@ -1554,7 +1568,7 @@ function isAbortError(error) {
1554
1568
  return error.name === "AbortError";
1555
1569
  }
1556
1570
  /**
1557
- * Structural: Node 22 bundles down-level `await using` to a plain `Error`
1571
+ * Structural: down-level bundles can implement `await using` with a plain `Error`
1558
1572
  * carrying `error`/`suppressed`, so `instanceof SuppressedError` cannot be used.
1559
1573
  */
1560
1574
  function isSuppressedError(error) {
@@ -1655,8 +1669,9 @@ function buildExtensionTree(node, materializeCommandDefinition) {
1655
1669
  extensions
1656
1670
  };
1657
1671
  }
1658
- /** Evaluate Extension section callbacks against current state and freeze the tree. */
1672
+ /** Install Context sections, evaluate Extension section callbacks, and freeze the tree. */
1659
1673
  function applySectionsAndFreeze(rootNode, extensions) {
1674
+ applyContextSections(rootNode, extensions);
1660
1675
  if (extensions.some((extension) => extension.sections !== void 0)) {
1661
1676
  const authoredSnapshot = snapshotCommand(rootNode);
1662
1677
  for (const extension of extensions) applyExtensionSections(rootNode, extension, authoredSnapshot);
@@ -1706,98 +1721,91 @@ function resolveStructuredInput(root, path, input) {
1706
1721
  }
1707
1722
  /** Resolve, parse, and run one invocation without rendering failures. */
1708
1723
  async function dispatch(input, prepared, io, signal, onExtensionContext, onFailure) {
1709
- try {
1710
- var _usingCtx$1 = _usingCtx();
1711
- const { rootNode, extensions } = prepared;
1712
- const { argv, route, parsed } = "argv" in input ? resolveArgvInput(rootNode, input.argv) : resolveStructuredInput(rootNode, input.path, input.input);
1713
- const resolvedNode = route.command;
1714
- const disposal = _usingCtx$1.a(new DisposalStack());
1715
- const contexts = resolvedNode.contexts.map(({ instance }) => instance);
1716
- const resolver = createContextResolver(contexts, io, disposal, signal);
1717
- const rootSnapshot = snapshotCommand(rootNode);
1718
- let commandSnapshot = rootSnapshot;
1719
- for (const name of route.commandPath.slice(1)) commandSnapshot = commandSnapshot.subCommands[name];
1720
- const extensionContext = Object.freeze({
1721
- argv: [...argv],
1722
- rootCommand: rootSnapshot,
1723
- command: commandSnapshot,
1724
- commandPath: Object.freeze([...route.commandPath]),
1725
- args: parsed.args,
1726
- flags: parsed.flags,
1724
+ const { rootNode, extensions } = prepared;
1725
+ const { argv, route, parsed } = "argv" in input ? resolveArgvInput(rootNode, input.argv) : resolveStructuredInput(rootNode, input.path, input.input);
1726
+ const resolvedNode = route.command;
1727
+ await using disposal = new AsyncDisposableStack();
1728
+ const contexts = resolvedNode.contexts.map(({ instance }) => instance);
1729
+ const resolver = createContextResolver(contexts, io, disposal, signal);
1730
+ const rootSnapshot = snapshotCommand(rootNode);
1731
+ let commandSnapshot = rootSnapshot;
1732
+ for (const name of route.commandPath.slice(1)) commandSnapshot = commandSnapshot.subCommands[name];
1733
+ const extensionContext = Object.freeze({
1734
+ argv: [...argv],
1735
+ rootCommand: rootSnapshot,
1736
+ command: commandSnapshot,
1737
+ commandPath: Object.freeze([...route.commandPath]),
1738
+ args: parsed.args,
1739
+ flags: parsed.flags,
1740
+ rawArgs: parsed.rawArgs,
1741
+ signal,
1742
+ ctx: resolver.bag(extensions.flatMap((extension) => extension.use)),
1743
+ finish: finishInvocation,
1744
+ stdout: io.stdout,
1745
+ stderr: io.stderr
1746
+ });
1747
+ onExtensionContext?.(extensionContext);
1748
+ const terminal = async () => {
1749
+ validateParsed(resolvedNode, parsed);
1750
+ const validated = await applySchemas(resolvedNode, parsed);
1751
+ resolver.setValidatedFlags(validated.flags);
1752
+ if (!resolvedNode.run) return;
1753
+ const context = {
1754
+ args: validated.args,
1755
+ flags: validated.flags,
1756
+ ctx: resolver.bag(contexts),
1727
1757
  rawArgs: parsed.rawArgs,
1728
1758
  signal,
1729
- ctx: resolver.bag(extensions.flatMap((extension) => extension.uses ?? [])),
1730
- finish: finishInvocation,
1759
+ command: extensionContext.command,
1760
+ rootCommand: rootSnapshot,
1731
1761
  stdout: io.stdout,
1732
1762
  stderr: io.stderr
1733
- });
1734
- onExtensionContext?.(extensionContext);
1735
- const terminal = async () => {
1736
- validateParsed(resolvedNode, parsed);
1737
- const validated = await applySchemas(resolvedNode, parsed);
1738
- resolver.setValidatedFlags(validated.flags);
1739
- if (!resolvedNode.run) return;
1740
- const context = {
1741
- args: validated.args,
1742
- flags: validated.flags,
1743
- ctx: resolver.bag(contexts),
1744
- rawArgs: parsed.rawArgs,
1745
- signal,
1746
- command: extensionContext.command,
1747
- rootCommand: rootSnapshot,
1748
- stdout: io.stdout,
1749
- stderr: io.stderr
1750
- };
1751
- return await resolvedNode.run(context);
1752
1763
  };
1753
- let result;
1754
- let outcome = { status: "completed" };
1764
+ return await resolvedNode.run(context);
1765
+ };
1766
+ let result;
1767
+ let outcome = { status: "completed" };
1768
+ try {
1755
1769
  try {
1756
- try {
1757
- for (const extension of extensions) if (await extension.hooks?.preRun?.(extensionContext) === finishInvocation()) {
1758
- outcome = {
1759
- status: "finished",
1760
- by: extension.id
1761
- };
1762
- break;
1763
- }
1764
- if (outcome.status !== "finished") result = await terminal();
1765
- } catch (error) {
1766
- const by = await onFailure?.(error, extensionContext);
1770
+ for (const extension of extensions) if (await extension.hooks?.preRun?.(extensionContext) === finishInvocation()) {
1767
1771
  outcome = {
1768
- status: "failed",
1769
- error,
1770
- ...by === void 0 ? {} : { by }
1772
+ status: "finished",
1773
+ by: extension.id
1771
1774
  };
1775
+ break;
1772
1776
  }
1773
- Object.freeze(outcome);
1774
- let postRunFailed = false;
1775
- let postRunError;
1776
- for (const extension of extensions.toReversed()) try {
1777
- await extension.hooks?.postRun?.(extensionContext, outcome);
1778
- } catch (error) {
1779
- if (outcome.status !== "failed" && !postRunFailed) {
1780
- postRunFailed = true;
1781
- postRunError = error;
1782
- }
1777
+ if (outcome.status !== "finished") result = await terminal();
1778
+ } catch (error) {
1779
+ const by = await onFailure?.(error, extensionContext);
1780
+ outcome = {
1781
+ status: "failed",
1782
+ error,
1783
+ ...by === void 0 ? {} : { by }
1784
+ };
1785
+ }
1786
+ Object.freeze(outcome);
1787
+ let postRunFailed = false;
1788
+ let postRunError;
1789
+ for (const extension of extensions.toReversed()) try {
1790
+ await extension.hooks?.postRun?.(extensionContext, outcome);
1791
+ } catch (error) {
1792
+ if (outcome.status !== "failed" && !postRunFailed) {
1793
+ postRunFailed = true;
1794
+ postRunError = error;
1783
1795
  }
1784
- if (outcome.status === "failed") throw outcome.error;
1785
- if (postRunFailed) throw postRunError;
1786
- } finally {
1787
- await resolver.settle();
1788
1796
  }
1789
- return outcome.status === "finished" ? {
1790
- status: "finished",
1791
- by: outcome.by
1792
- } : {
1793
- status: "completed",
1794
- result
1795
- };
1796
- } catch (_) {
1797
- _usingCtx$1.e = _;
1797
+ if (outcome.status === "failed") throw outcome.error;
1798
+ if (postRunFailed) throw postRunError;
1798
1799
  } finally {
1799
- await _usingCtx$1.d();
1800
+ await resolver.settle();
1800
1801
  }
1802
+ return outcome.status === "finished" ? {
1803
+ status: "finished",
1804
+ by: outcome.by
1805
+ } : {
1806
+ status: "completed",
1807
+ result
1808
+ };
1801
1809
  }
1802
1810
  /** Render one failure through Extension onError hooks, ending in Core's default renderer. */
1803
1811
  async function renderFailure(error, argv, prepared, io, signal, extensionContext, silentDefault = false) {
@@ -1881,56 +1889,58 @@ async function executeInvocation(node, options, materializeCommandDefinition) {
1881
1889
  };
1882
1890
  const controller = new AbortController();
1883
1891
  const signal = options?.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
1884
- const snapshotPath = process.env.CRUST_INTERNAL_BUILD === "1" ? void 0 : process.env[SNAPSHOT_PATH_ENV];
1885
- if (snapshotPath) {
1886
- try {
1887
- const base = buildExtensionTree(node, materializeCommandDefinition);
1888
- const takeSnapshot = () => snapshotCommand(applySectionsAndFreeze(cloneCommandNode(base.rootNode), base.extensions));
1889
- let snapshot = takeSnapshot();
1890
- const buildOutDir = process.env[BUILD_OUT_DIR_ENV];
1891
- if (buildOutDir) {
1892
- const extensions = [];
1893
- const owners = /* @__PURE__ */ new Map();
1894
- for (const extension of base.extensions) {
1895
- if (!extension.build) continue;
1896
- try {
1897
- const files = (await extension.build({ snapshot })).map((file) => {
1898
- const path = normalizeArtifactPath(file.path);
1899
- const key = path.toLowerCase();
1900
- for (const [ownedKey, owner] of owners) if (ownedKey === key || ownedKey.startsWith(`${key}/`) || key.startsWith(`${ownedKey}/`)) throw new Error(`Artifact path "${path}" collides with "${owner.path}" produced by Extension "${owner.id}".`);
1901
- owners.set(key, {
1892
+ if (!isPackagedBuild()) {
1893
+ const snapshotPath = process.env.CRUST_INTERNAL_BUILD === "1" ? void 0 : process.env[SNAPSHOT_PATH_ENV];
1894
+ if (snapshotPath) {
1895
+ try {
1896
+ const base = buildExtensionTree(node, materializeCommandDefinition);
1897
+ const takeSnapshot = () => snapshotCommand(applySectionsAndFreeze(cloneCommandNode(base.rootNode), base.extensions));
1898
+ let snapshot = takeSnapshot();
1899
+ const buildOutDir = process.env[BUILD_OUT_DIR_ENV];
1900
+ if (buildOutDir) {
1901
+ const extensions = [];
1902
+ const owners = /* @__PURE__ */ new Map();
1903
+ for (const extension of base.extensions) {
1904
+ if (!extension.build) continue;
1905
+ try {
1906
+ const files = (await extension.build({ snapshot })).map((file) => {
1907
+ const path = normalizeArtifactPath(file.path);
1908
+ const key = path.toLowerCase();
1909
+ for (const [ownedKey, owner] of owners) if (ownedKey === key || ownedKey.startsWith(`${key}/`) || key.startsWith(`${ownedKey}/`)) throw new Error(`Artifact path "${path}" collides with "${owner.path}" produced by Extension "${owner.id}".`);
1910
+ owners.set(key, {
1911
+ id: extension.id,
1912
+ path
1913
+ });
1914
+ return {
1915
+ path,
1916
+ content: file.content
1917
+ };
1918
+ });
1919
+ for (const file of files) {
1920
+ const target = join(buildOutDir, file.path);
1921
+ await mkdir(dirname(target), { recursive: true });
1922
+ await writeFile(target, file.content);
1923
+ }
1924
+ extensions.push({
1902
1925
  id: extension.id,
1903
- path
1926
+ files: files.map((file) => file.path)
1904
1927
  });
1905
- return {
1906
- path,
1907
- content: file.content
1908
- };
1909
- });
1910
- for (const file of files) {
1911
- const target = join(buildOutDir, file.path);
1912
- await mkdir(dirname(target), { recursive: true });
1913
- await writeFile(target, file.content);
1928
+ } catch (error) {
1929
+ const message = error instanceof Error ? error.message : String(error);
1930
+ throw new Error(`Extension "${extension.id}" build failed: ${message}`, { cause: error });
1914
1931
  }
1915
- extensions.push({
1916
- id: extension.id,
1917
- files: files.map((file) => file.path)
1918
- });
1919
- } catch (error) {
1920
- const message = error instanceof Error ? error.message : String(error);
1921
- throw new Error(`Extension "${extension.id}" build failed: ${message}`, { cause: error });
1932
+ snapshot = takeSnapshot();
1922
1933
  }
1923
- snapshot = takeSnapshot();
1934
+ await writeFile(join(dirname(snapshotPath), "build-report.json"), JSON.stringify({ extensions }));
1924
1935
  }
1925
- await writeFile(join(dirname(snapshotPath), "build-report.json"), JSON.stringify({ extensions }));
1936
+ await writeFile(snapshotPath, JSON.stringify(snapshot));
1937
+ } catch (error) {
1938
+ const message = error instanceof Error ? error.message : String(error);
1939
+ console.error(message);
1940
+ return process.exit(1);
1926
1941
  }
1927
- await writeFile(snapshotPath, JSON.stringify(snapshot));
1928
- } catch (error) {
1929
- const message = error instanceof Error ? error.message : String(error);
1930
- console.error(message);
1931
- return process.exit(1);
1942
+ return process.exit(0);
1932
1943
  }
1933
- return process.exit(0);
1934
1944
  }
1935
1945
  const invoke = async () => {
1936
1946
  let prepared;
@@ -1989,4 +1999,4 @@ async function executeInvocation(node, options, materializeCommandDefinition) {
1989
1999
  return await (hasInjectedIO(options?.io) ? withAmbientTerminalIO(io, invoke) : invoke());
1990
2000
  }
1991
2001
  //#endregion
1992
- 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 };
2002
+ export { normalizeFlag as C, normalizeArg as S, validateContextAvailability as _, runInvocation as a, validateCommandSections as b, cloneCommandNode as c, createCommandNode as d, registerFlag as f, definingOf as g, defineContext as h, resolveTypedPath as i, cloneFlagRegistry as l, contextSources as m, executeInvocation as n, snapshotCommand as o, defineExtension as p, prepareInvocation as r, parseFlagValues as s, SNAPSHOT_PATH_ENV as t, installExtensionContexts as u, isListed as v, CrustError as w, visibleSectionsFor as x, sectionsFor as y };