@crustjs/core 0.4.1 → 0.5.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.
@@ -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,40 +270,43 @@ 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
  }
303
+ /**
304
+ * Start an immutable fluent Context definition: a named, lazy command
305
+ * dependency. Chain `.use()`, `.flags()`, and `.sections()`, then `.setup()` for the factory.
306
+ */
307
+ function defineContext(name) {
308
+ return createContextBuilder(name, Object.freeze([]), Object.freeze({}), Object.freeze([]));
309
+ }
235
310
  function isDisposeCallback(value) {
236
311
  return typeof value === "function";
237
312
  }
@@ -373,7 +448,7 @@ function createContextResolver(contexts, io, disposal, signal) {
373
448
  ...io,
374
449
  signal,
375
450
  flags: ownedFlags,
376
- ctx: makeBag(context.uses, current),
451
+ ctx: makeBag(context.use, current),
377
452
  defer(cleanup) {
378
453
  if (current.settled) throw new CrustError("DEFINITION", `Context "${name}" cannot register cleanup after its setup has finished.`, {
379
454
  subject: "context",
@@ -414,7 +489,7 @@ function createContextResolver(contexts, io, disposal, signal) {
414
489
  enumerable: true,
415
490
  get: () => makePull(origin)(name)
416
491
  });
417
- for (const dependency of source.uses ?? []) add(dependency);
492
+ for (const dependency of source.use ?? []) add(dependency);
418
493
  };
419
494
  for (const source of sources) add(source);
420
495
  Object.defineProperty(bag, contextSources, { value: Object.freeze([...sources]) });
@@ -444,24 +519,75 @@ const FINISHED = Object.freeze({ [Symbol("crust.finished")]: true });
444
519
  function finishInvocation() {
445
520
  return FINISHED;
446
521
  }
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]) => ({
522
+ function createExtension(state) {
523
+ const flags = Object.freeze(toFlagsRecord(state.flags));
524
+ toFlagsRecord(state.provide.flatMap((instance) => Object.entries(instance.ownedFlags).map(([name, def]) => ({
455
525
  ...def,
456
526
  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]) } : {},
527
+ }))), flags);
528
+ const data = Object.freeze({
529
+ id: state.id,
530
+ flags,
531
+ commands: Object.freeze([...state.commands]),
532
+ use: Object.freeze([...state.use]),
533
+ provide: Object.freeze([...state.provide]),
534
+ hooks: Object.freeze({ ...state.hooks }),
535
+ ...state.sections ? { sections: state.sections } : {},
536
+ ...state.build ? { build: state.build } : {}
537
+ });
538
+ const next = (change) => createExtension({
539
+ ...state,
540
+ ...change
541
+ });
542
+ const sealed = sealHandle({
543
+ id: state.id,
544
+ use: (...factories) => next({ use: [...state.use, ...factories.map(definingOf)] }),
545
+ provide: (...instances) => next({ provide: [...state.provide, ...instances.map(definingOf)] }),
546
+ flags: (...defs) => next({ flags: [...state.flags, ...defs.map(ownDefinition)] }),
547
+ add: (...definitions) => next({ commands: [...state.commands, ...definitions] }),
548
+ preRun: (preRun) => next({ hooks: {
549
+ ...state.hooks,
550
+ preRun
551
+ } }),
552
+ postRun: (postRun) => next({ hooks: {
553
+ ...state.hooks,
554
+ postRun
555
+ } }),
556
+ onError: (onError) => next({ hooks: {
557
+ ...state.hooks,
558
+ onError
559
+ } }),
560
+ sections: (sections) => next({ sections }),
561
+ build: (build) => next({ build }),
562
+ factory: (define) => Object.assign((...args) => {
563
+ const extension = define(sealed, ...args);
564
+ const returned = definingOf(extension).id;
565
+ if (returned !== state.id) throw new CrustError("DEFINITION", `Extension factory "${state.id}" returned Extension "${returned}"; return an Extension built from the handle it receives`, {
566
+ subject: "extension",
567
+ name: state.id,
568
+ reason: "factory-id-mismatch"
569
+ });
570
+ return extension;
571
+ }, { id: state.id })
572
+ }, data);
573
+ return sealed;
574
+ }
575
+ /**
576
+ * Start an immutable fluent Extension definition.
577
+ *
578
+ * Extensions apply to the whole application and own the flags and commands
579
+ * they contribute. `defineExtension<"version">(id)` requires root metadata keys
580
+ * without preventing inference of flags, Contexts, or commands; chain
581
+ * `.factory()` for a configurable Extension whose identity is a section consumer.
582
+ */
583
+ function defineExtension(id) {
584
+ return createExtension({
463
585
  id,
464
- ...config.flags === void 0 ? {} : { flags: ownedFlags }
586
+ use: [],
587
+ provide: [],
588
+ flags: [],
589
+ commands: [],
590
+ hooks: {}
465
591
  });
466
592
  }
467
593
  //#endregion
@@ -483,6 +609,7 @@ function createCommandNode(name) {
483
609
  args: [],
484
610
  subCommands: {},
485
611
  contexts: [],
612
+ providedContexts: [],
486
613
  demands: [],
487
614
  extensions: [],
488
615
  run: void 0
@@ -528,11 +655,11 @@ function injectExtensionFlag(node, name, def, recursive) {
528
655
  }
529
656
  /** Attach one Extension's owned root commands to a cloned tree. */
530
657
  function applyExtensionCommands(root, extension, materializeCommandDefinition) {
531
- for (const definition of extension.commands ?? []) root.subCommands[definition.name] = materializeCommandDefinition(definition, root, extension.id);
658
+ for (const definition of extension.commands) root.subCommands[definition.name] = materializeCommandDefinition(definition, root, extension.id);
532
659
  }
533
660
  /** Inject one Extension's owned flags across a cloned tree. */
534
661
  function applyExtensionFlags(root, extension) {
535
- for (const [name, defWithScope] of Object.entries(extension.flags ?? {})) {
662
+ for (const [name, defWithScope] of Object.entries(extension.flags)) {
536
663
  const { recursive = true, ...def } = defWithScope;
537
664
  injectExtensionFlag(root, name, def, recursive);
538
665
  }
@@ -557,36 +684,12 @@ function cloneCommandNode(node) {
557
684
  args: [...node.args],
558
685
  subCommands,
559
686
  contexts: node.contexts.map((context) => ({ ...context })),
687
+ providedContexts: [...node.providedContexts],
560
688
  demands: [...node.demands],
561
689
  extensions: [...node.extensions],
562
690
  run: node.run
563
691
  };
564
692
  }
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
693
  function contributionTarget(root, command, extension) {
591
694
  let target = root;
592
695
  for (const segment of command) {
@@ -613,6 +716,30 @@ function applyExtensionSections(root, extension, snapshot) {
613
716
  target.meta.sections = [...target.meta.sections ?? [], section];
614
717
  }
615
718
  }
719
+ /**
720
+ * Install Context sections where each Context is provided: `.provide()` on its own
721
+ * command only, Extension providers once on the root. Unlike Extension section
722
+ * contributions, a title already on the command is a `DEFINITION` error.
723
+ */
724
+ function applyContextSections(root, extensions) {
725
+ const visit = (node, path, instances) => {
726
+ const effective = [...new Map(instances.map((instance) => [instance.name, instance])).values()];
727
+ if (effective.some((instance) => instance.sections.length > 0)) {
728
+ const titles = new Set((node.meta.sections ?? []).map(({ title }) => title));
729
+ for (const instance of effective) {
730
+ 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}"`, {
731
+ subject: "context",
732
+ name: instance.name,
733
+ reason: "duplicate-section"
734
+ });
735
+ for (const { title } of instance.sections) titles.add(title);
736
+ }
737
+ node.meta.sections = [...node.meta.sections ?? [], ...effective.flatMap((instance) => instance.sections)];
738
+ }
739
+ for (const [name, child] of Object.entries(node.subCommands)) visit(child, `${path} ${name}`, child.providedContexts);
740
+ };
741
+ visit(root, root.meta.name, [...root.providedContexts, ...extensions.flatMap((extension) => extension.provide)]);
742
+ }
616
743
  function installExtensionContexts(node, extensions, reRegisteredIds) {
617
744
  const cloned = cloneCommandNode(node);
618
745
  const kept = new Set(extensions.flatMap((e) => !reRegisteredIds.has(e.id) && node.extensions.includes(e) ? [e.id] : []));
@@ -636,7 +763,7 @@ function installExtensionContexts(node, extensions, reRegisteredIds) {
636
763
  prune(cloned);
637
764
  for (const extension of extensions) {
638
765
  if (kept.has(extension.id)) continue;
639
- const instances = extension.provides ?? [];
766
+ const instances = extension.provide;
640
767
  if (instances.length === 0) continue;
641
768
  const walk = (target, skip) => {
642
769
  const installed = instances.filter((instance) => !skip.has(instance.name));
@@ -711,6 +838,50 @@ function coerceJson(raw) {
711
838
  }
712
839
  }
713
840
  //#endregion
841
+ //#region src/parsing/schema.ts
842
+ async function runSchema(schema, raw, path, issues) {
843
+ const result = await schema["~standard"].validate(raw);
844
+ if (result.issues) {
845
+ issues.push(...normalizeStandardIssues(result.issues, path));
846
+ return { ok: false };
847
+ }
848
+ return {
849
+ ok: true,
850
+ value: result.value
851
+ };
852
+ }
853
+ /**
854
+ * Apply Standard Schemas declared on arg/flag definitions.
855
+ *
856
+ * Runs after syntax parsing and structural validation: each schema receives
857
+ * the raw parsed value (`string | undefined` for args, the raw token value
858
+ * for flags, arrays for variadic/multiple) and exclusively owns coercion,
859
+ * defaults, requiredness, and validation. Returns transformed copies of
860
+ * `args`/`flags`; definitions without a schema pass through unchanged.
861
+ *
862
+ * @throws {CrustError} `VALIDATION` aggregating every schema issue
863
+ */
864
+ async function applySchemas(node, parsed) {
865
+ const issues = [];
866
+ const args = new Map(Object.entries(parsed.args));
867
+ const flags = new Map(Object.entries(parsed.flags));
868
+ for (const def of node.args) {
869
+ if (def.schema === void 0) continue;
870
+ const result = await runSchema(def.schema, args.get(def.name), ["args", def.name], issues);
871
+ if (result.ok) args.set(def.name, result.value);
872
+ }
873
+ for (const [name, def] of Object.entries(node.effectiveFlags)) {
874
+ if (def.schema === void 0) continue;
875
+ const result = await runSchema(def.schema, flags.get(name), ["flags", name], issues);
876
+ if (result.ok) flags.set(name, result.value);
877
+ }
878
+ if (issues.length > 0) throw new CrustError("VALIDATION", `Invalid input:\n${issues.map((issue) => ` - ${issue.path}: ${issue.message}`).join("\n")}`, { issues });
879
+ return {
880
+ args: Object.fromEntries(args),
881
+ flags: Object.fromEntries(flags)
882
+ };
883
+ }
884
+ //#endregion
714
885
  //#region src/parsing/parser.ts
715
886
  /**
716
887
  * Build the options config for `util.parseArgs` from the shared spelling table.
@@ -945,7 +1116,8 @@ function resolveArgs(argsDef, positionals, coerce) {
945
1116
  configurable: true
946
1117
  });
947
1118
  if (def.variadic) {
948
- resolved[name] = positionals.slice(index).map((v, i) => coerce(def, v, label, i));
1119
+ const remaining = positionals.slice(index);
1120
+ resolved[name] = remaining.length === 0 && def.default !== void 0 ? [resolveDefault(def, label)] : remaining.map((v, i) => coerce(def, v, label, i));
949
1121
  index = positionals.length;
950
1122
  } else if (index < positionals.length) {
951
1123
  resolved[name] = coerce(def, positionals[index], label);
@@ -1088,6 +1260,31 @@ function parseArgs$1(command, argv, env = process.env) {
1088
1260
  rawArgs
1089
1261
  };
1090
1262
  }
1263
+ /**
1264
+ * Resolve flags from their `env` bindings alone, as {@link parseArgs} does for an
1265
+ * empty argv: `env.delimiter` splitting, `choices`, `parse`, built-in coercion,
1266
+ * defaults, requiredness, then Standard Schemas. Flags without `env` resolve
1267
+ * to their defaults.
1268
+ *
1269
+ * @param flags - Flag definitions keyed by result name; each `env.name` selects its variable
1270
+ * @param env - Raw variable text; only own properties are read
1271
+ * @throws {CrustError} `DEFINITION` for a definition `defineFlag` rejects (e.g. a
1272
+ * `__proto__` key or a default outside `choices`); `PARSE` or `VALIDATION` on the first
1273
+ * coercion or requiredness failure, or `VALIDATION` aggregating schema issues. Messages
1274
+ * may quote raw values.
1275
+ */
1276
+ async function parseFlagValues(flags, env) {
1277
+ for (const [name, def] of Object.entries(flags)) normalizeFlag(name, def);
1278
+ const resolved = resolveFlags(flags, applyEnvAndDelimiter(flags, {}, env), coerceFlagValue);
1279
+ validateRequiredFlags(flags, resolved);
1280
+ return (await applySchemas({
1281
+ args: [],
1282
+ effectiveFlags: flags
1283
+ }, {
1284
+ args: {},
1285
+ flags: resolved
1286
+ })).flags;
1287
+ }
1091
1288
  /** Bind typed input without producing argv; the path alone selects the command. */
1092
1289
  function parseStructured(command, input) {
1093
1290
  const { args: inputArgs, flags: inputFlags, raw } = input;
@@ -1149,87 +1346,6 @@ function validateParsed(command, parsed) {
1149
1346
  validateRequiredFlags(flagsDef, flags);
1150
1347
  }
1151
1348
  //#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
1349
  //#region src/command/snapshot.ts
1234
1350
  /** Drop keys with `undefined` values so snapshots serialize cleanly, then freeze. */
1235
1351
  function freezeCompact(obj) {
@@ -1449,7 +1565,7 @@ function resolveCommand(command, argv) {
1449
1565
  };
1450
1566
  }
1451
1567
  //#endregion
1452
- //#region \0@oxc-project+runtime@0.148.0/helpers/esm/usingCtx.js
1568
+ //#region \0@oxc-project+runtime@0.150.0/helpers/esm/usingCtx.js
1453
1569
  function _usingCtx() {
1454
1570
  var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
1455
1571
  var n = Error();
@@ -1545,7 +1661,9 @@ const DEFAULT_IO = {
1545
1661
  * without dispatching a Command Action. In-process callers use `Crust.snapshot()`.
1546
1662
  *
1547
1663
  * 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.
1664
+ * `process.env.CRUST_INTERNAL_BUILD === "1"` as a literal and compile the protocol out;
1665
+ * packaged Deno bundles set the `isPackagedBuild()` marker and skip it without
1666
+ * reading the environment.
1549
1667
  */
1550
1668
  const SNAPSHOT_PATH_ENV = "CRUST_INTERNAL_SNAPSHOT_PATH";
1551
1669
  const EXIT_CODE_CANCELLED = 130;
@@ -1655,8 +1773,9 @@ function buildExtensionTree(node, materializeCommandDefinition) {
1655
1773
  extensions
1656
1774
  };
1657
1775
  }
1658
- /** Evaluate Extension section callbacks against current state and freeze the tree. */
1776
+ /** Install Context sections, evaluate Extension section callbacks, and freeze the tree. */
1659
1777
  function applySectionsAndFreeze(rootNode, extensions) {
1778
+ applyContextSections(rootNode, extensions);
1660
1779
  if (extensions.some((extension) => extension.sections !== void 0)) {
1661
1780
  const authoredSnapshot = snapshotCommand(rootNode);
1662
1781
  for (const extension of extensions) applyExtensionSections(rootNode, extension, authoredSnapshot);
@@ -1726,7 +1845,7 @@ async function dispatch(input, prepared, io, signal, onExtensionContext, onFailu
1726
1845
  flags: parsed.flags,
1727
1846
  rawArgs: parsed.rawArgs,
1728
1847
  signal,
1729
- ctx: resolver.bag(extensions.flatMap((extension) => extension.uses ?? [])),
1848
+ ctx: resolver.bag(extensions.flatMap((extension) => extension.use)),
1730
1849
  finish: finishInvocation,
1731
1850
  stdout: io.stdout,
1732
1851
  stderr: io.stderr
@@ -1881,56 +2000,58 @@ async function executeInvocation(node, options, materializeCommandDefinition) {
1881
2000
  };
1882
2001
  const controller = new AbortController();
1883
2002
  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, {
2003
+ if (!isPackagedBuild()) {
2004
+ const snapshotPath = process.env.CRUST_INTERNAL_BUILD === "1" ? void 0 : process.env[SNAPSHOT_PATH_ENV];
2005
+ if (snapshotPath) {
2006
+ try {
2007
+ const base = buildExtensionTree(node, materializeCommandDefinition);
2008
+ const takeSnapshot = () => snapshotCommand(applySectionsAndFreeze(cloneCommandNode(base.rootNode), base.extensions));
2009
+ let snapshot = takeSnapshot();
2010
+ const buildOutDir = process.env[BUILD_OUT_DIR_ENV];
2011
+ if (buildOutDir) {
2012
+ const extensions = [];
2013
+ const owners = /* @__PURE__ */ new Map();
2014
+ for (const extension of base.extensions) {
2015
+ if (!extension.build) continue;
2016
+ try {
2017
+ const files = (await extension.build({ snapshot })).map((file) => {
2018
+ const path = normalizeArtifactPath(file.path);
2019
+ const key = path.toLowerCase();
2020
+ 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}".`);
2021
+ owners.set(key, {
2022
+ id: extension.id,
2023
+ path
2024
+ });
2025
+ return {
2026
+ path,
2027
+ content: file.content
2028
+ };
2029
+ });
2030
+ for (const file of files) {
2031
+ const target = join(buildOutDir, file.path);
2032
+ await mkdir(dirname(target), { recursive: true });
2033
+ await writeFile(target, file.content);
2034
+ }
2035
+ extensions.push({
1902
2036
  id: extension.id,
1903
- path
2037
+ files: files.map((file) => file.path)
1904
2038
  });
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);
2039
+ } catch (error) {
2040
+ const message = error instanceof Error ? error.message : String(error);
2041
+ throw new Error(`Extension "${extension.id}" build failed: ${message}`, { cause: error });
1914
2042
  }
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 });
2043
+ snapshot = takeSnapshot();
1922
2044
  }
1923
- snapshot = takeSnapshot();
2045
+ await writeFile(join(dirname(snapshotPath), "build-report.json"), JSON.stringify({ extensions }));
1924
2046
  }
1925
- await writeFile(join(dirname(snapshotPath), "build-report.json"), JSON.stringify({ extensions }));
2047
+ await writeFile(snapshotPath, JSON.stringify(snapshot));
2048
+ } catch (error) {
2049
+ const message = error instanceof Error ? error.message : String(error);
2050
+ console.error(message);
2051
+ return process.exit(1);
1926
2052
  }
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);
2053
+ return process.exit(0);
1932
2054
  }
1933
- return process.exit(0);
1934
2055
  }
1935
2056
  const invoke = async () => {
1936
2057
  let prepared;
@@ -1989,4 +2110,4 @@ async function executeInvocation(node, options, materializeCommandDefinition) {
1989
2110
  return await (hasInjectedIO(options?.io) ? withAmbientTerminalIO(io, invoke) : invoke());
1990
2111
  }
1991
2112
  //#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 };
2113
+ 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 };