@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
|
|
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.
|
|
273
|
+
for (const dependency of source.use) visit(dependency);
|
|
202
274
|
};
|
|
203
275
|
for (const source of sources) visit(source);
|
|
204
276
|
}
|
|
205
|
-
function
|
|
206
|
-
return
|
|
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
|
|
209
|
-
const
|
|
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
|
-
|
|
290
|
+
sections,
|
|
291
|
+
use: instanceUse,
|
|
219
292
|
factory: sealed,
|
|
220
293
|
setup: run
|
|
221
294
|
});
|
|
222
295
|
};
|
|
223
|
-
const factory = (options) => instance(
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
448
|
-
|
|
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
|
-
}))),
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
const
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
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
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2045
|
+
await writeFile(join(dirname(snapshotPath), "build-report.json"), JSON.stringify({ extensions }));
|
|
1924
2046
|
}
|
|
1925
|
-
await writeFile(
|
|
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
|
-
|
|
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 {
|
|
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 };
|