@crustjs/crust-linux-arm64-musl 0.3.6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3163 @@
1
+ import { accessSync, constants, copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { spawn } from "node:child_process";
7
+ import { randomBytes } from "node:crypto";
8
+ import { once } from "node:events";
9
+ import { text } from "node:stream/consumers";
10
+ //#region ../utils/dist/artifacts.js
11
+ /**
12
+ * Set by `crust build` while it prepares the Command Snapshot: the absolute
13
+ * entry-isolated directory Extension build hooks write into. Core reads it
14
+ * to run the hooks; `resolveArtifactDir` reads it so sections evaluated during
15
+ * that run see the artifacts being built instead of the wiped `.crust/root`.
16
+ */
17
+ const BUILD_OUT_DIR_ENV = "CRUST_INTERNAL_BUILD_OUT_DIR";
18
+ //#endregion
19
+ //#region ../core/dist/invocation-ka-BNJKZ.js
20
+ /**
21
+ * A typed error for runtime recipe, Extension, Context, documentation, argv, and validation failures.
22
+ *
23
+ * Every `CrustError` carries a {@link CrustErrorCode} that identifies the specific
24
+ * failure, enabling programmatic error handling without fragile message parsing.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * import { CrustError } from "@crustjs/core";
29
+ *
30
+ * const outcome = await app.run(["deploy"], { args: { target: "prod" } });
31
+ * if (outcome.status === "failed") {
32
+ * const err = outcome.error;
33
+ * if (err instanceof CrustError) {
34
+ * console.error(`[${err.code}] ${err.message}`);
35
+ * }
36
+ * }
37
+ * ```
38
+ */
39
+ var CrustError = class extends Error {
40
+ /** Machine-readable error code for programmatic handling */
41
+ code;
42
+ /** Structured payload for programmatic handling */
43
+ details;
44
+ /** Optional wrapped original error/value */
45
+ cause;
46
+ constructor(code, message, ...details) {
47
+ super(message);
48
+ this.name = "CrustError";
49
+ this.code = code;
50
+ this.details = details[0];
51
+ }
52
+ is(code) {
53
+ return Object.is(this.code, code);
54
+ }
55
+ withCause(cause) {
56
+ this.cause = cause;
57
+ return this;
58
+ }
59
+ toJSON() {
60
+ return {
61
+ code: this.code,
62
+ message: this.message,
63
+ details: this.details
64
+ };
65
+ }
66
+ };
67
+ globalThis.AsyncDisposableStack;
68
+ Object.freeze({ [Symbol("crust.finished")]: true });
69
+ function invalidSections({ subject, name }) {
70
+ return new CrustError("DEFINITION", `${subject === "command" ? "Command" : "Extension"} "${name}" contains invalid documentation sections`, {
71
+ subject,
72
+ name,
73
+ reason: "invalid-sections"
74
+ });
75
+ }
76
+ function normalizeSection(section, owner) {
77
+ const { title, body, only, except } = section;
78
+ if (!title.trim() || /[\r\n]/.test(title) || !body.trim() || only?.length === 0 || except?.length === 0 || only !== void 0 && except !== void 0) throw invalidSections(owner);
79
+ const audience = (ids) => {
80
+ return Object.freeze(ids.map((consumer) => typeof consumer === "string" ? consumer : consumer.id));
81
+ };
82
+ return Object.freeze({
83
+ title,
84
+ body,
85
+ ...only ? { only: audience(only) } : except ? { except: audience(except) } : {}
86
+ });
87
+ }
88
+ function validateCommandSections(name, sections) {
89
+ return sections.map((section) => normalizeSection(section, {
90
+ subject: "command",
91
+ name
92
+ }));
93
+ }
94
+ /**
95
+ * Snapshot subprocess protocol used by first-party build tooling.
96
+ *
97
+ * When set to a non-empty file path, `.execute()` prepares the command tree,
98
+ * validates its documentation sections, optionally runs Extension build hooks when the
99
+ * build output directory is set, writes its final JSON snapshot and Build Report, and exits
100
+ * without dispatching a Command Action. In-process callers use `Crust.snapshot()`.
101
+ *
102
+ * Only source entries run by `crust build` honor it: finished Bun/Node bundles carry
103
+ * `process.env.CRUST_INTERNAL_BUILD === "1"` as a literal and compile the protocol out.
104
+ */
105
+ const SNAPSHOT_PATH_ENV = "CRUST_INTERNAL_SNAPSHOT_PATH";
106
+ //#endregion
107
+ //#region ../core/dist/index.js
108
+ /** Mint an Extension identity from any non-blank, trimmed string. */
109
+ function defineExtensionId(id) {
110
+ if (!id.trim() || id !== id.trim()) throw new CrustError("DEFINITION", "Extension id must be a non-empty, trimmed string", {
111
+ subject: "extension",
112
+ reason: "empty-id"
113
+ });
114
+ return id;
115
+ }
116
+ const commandDefinitionInternal = Symbol.for("crust.commandDefinition");
117
+ function resolveCommandName(name, aliases = []) {
118
+ if (name.trim() === "") throw new CrustError("DEFINITION", "Command name must be a non-empty string", {
119
+ subject: "command",
120
+ name,
121
+ reason: "empty-name"
122
+ });
123
+ if (name === "__proto__") throw new CrustError("DEFINITION", "Command name \"__proto__\" is reserved", {
124
+ subject: "command",
125
+ name,
126
+ reason: "reserved-name"
127
+ });
128
+ if (aliases.includes(name)) throw new CrustError("DEFINITION", `Command "${name}" must not list its own canonical name as an alias`, {
129
+ subject: "command",
130
+ name,
131
+ reason: "alias-collision"
132
+ });
133
+ return name;
134
+ }
135
+ function isCommandRecipe(value) {
136
+ return typeof value === "function";
137
+ }
138
+ function defineCommand(nameInput, configOrRecipe, maybeRecipe) {
139
+ const hasConfig = !isCommandRecipe(configOrRecipe);
140
+ const config = hasConfig ? configOrRecipe : {};
141
+ const recipe = hasConfig ? maybeRecipe : configOrRecipe;
142
+ const name = resolveCommandName(nameInput, config.aliases);
143
+ for (const alias of config.aliases ?? []) if (alias === "" || /[ \t\n\r\v\f]/.test(alias) || alias.startsWith("-")) throw new CrustError("DEFINITION", `Command "${name}" has an invalid alias "${alias}"`);
144
+ const { sections, version: _rootVersion, ...metaRest } = config;
145
+ const meta = {
146
+ ...metaRest,
147
+ ...config.aliases ? { aliases: [...config.aliases] } : {},
148
+ ...sections ? { sections: validateCommandSections(name, sections) } : {}
149
+ };
150
+ const internal = {
151
+ name,
152
+ recipe,
153
+ meta
154
+ };
155
+ const named = (defName) => {
156
+ return Object.freeze({
157
+ name: defName,
158
+ as: (newName) => named(resolveCommandName(newName, meta.aliases)),
159
+ [commandDefinitionInternal]: Object.freeze({
160
+ ...internal,
161
+ name: defName
162
+ })
163
+ });
164
+ };
165
+ return named(name);
166
+ }
167
+ //#endregion
168
+ //#region ../style/dist/index.js
169
+ var __defProp = Object.defineProperty;
170
+ var __exportAll = (all, no_symbols) => {
171
+ let target = {};
172
+ for (var name in all) __defProp(target, name, {
173
+ get: all[name],
174
+ enumerable: true
175
+ });
176
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
177
+ return target;
178
+ };
179
+ new Intl.Segmenter(void 0, { granularity: "grapheme" });
180
+ new RegExp([
181
+ String.raw`(?:\x1b\[|\x9b)[^\x1b\x18\x1a\x9c\x40-\x7e]*[\x40-\x7e\x18\x1a\x9c]?`,
182
+ String.raw`(?:\x1b\]|\x9d)[^\x07\x1b\x18\x1a\x9c]*(?:\x07|\x1b\\|[\x18\x1a\x9c])?`,
183
+ String.raw`(?:\x1b[PX^_]|[\x90\x98\x9e\x9f])[^\x1b\x18\x1a\x9c]*(?:\x1b\\|[\x18\x1a\x9c])?`,
184
+ String.raw`\x1b[\x20-\x2f][^\x1b]?`,
185
+ String.raw`\x1b[\x18\x1a\x9c\x30-\x7e]?`
186
+ ].join("|"), "g");
187
+ var ansiCodes_exports = /* @__PURE__ */ __exportAll({
188
+ bgBlack: () => bgBlack$1,
189
+ bgBlue: () => bgBlue$1,
190
+ bgBrightBlack: () => bgBrightBlack$1,
191
+ bgBrightBlue: () => bgBrightBlue$1,
192
+ bgBrightCyan: () => bgBrightCyan$1,
193
+ bgBrightGreen: () => bgBrightGreen$1,
194
+ bgBrightMagenta: () => bgBrightMagenta$1,
195
+ bgBrightRed: () => bgBrightRed$1,
196
+ bgBrightWhite: () => bgBrightWhite$1,
197
+ bgBrightYellow: () => bgBrightYellow$1,
198
+ bgCyan: () => bgCyan$1,
199
+ bgGreen: () => bgGreen$1,
200
+ bgMagenta: () => bgMagenta$1,
201
+ bgRed: () => bgRed$1,
202
+ bgWhite: () => bgWhite$1,
203
+ bgYellow: () => bgYellow$1,
204
+ black: () => black$1,
205
+ blue: () => blue$1,
206
+ bold: () => bold$1,
207
+ brightBlue: () => brightBlue$1,
208
+ brightCyan: () => brightCyan$1,
209
+ brightGreen: () => brightGreen$1,
210
+ brightMagenta: () => brightMagenta$1,
211
+ brightRed: () => brightRed$1,
212
+ brightWhite: () => brightWhite$1,
213
+ brightYellow: () => brightYellow$1,
214
+ cyan: () => cyan$1,
215
+ dim: () => dim$1,
216
+ gray: () => gray$1,
217
+ green: () => green$1,
218
+ hidden: () => hidden$1,
219
+ inverse: () => inverse$1,
220
+ isModifierName: () => isModifierName,
221
+ italic: () => italic$1,
222
+ magenta: () => magenta$1,
223
+ red: () => red$1,
224
+ strikethrough: () => strikethrough$1,
225
+ styleMethodNames: () => styleMethodNames,
226
+ underline: () => underline$1,
227
+ white: () => white$1,
228
+ yellow: () => yellow$1
229
+ });
230
+ function pair(open, close) {
231
+ return {
232
+ open: `\x1b[${open}m`,
233
+ close: `\x1b[${close}m`
234
+ };
235
+ }
236
+ /** Bold / increased intensity. */
237
+ const bold$1 = pair(1, 22);
238
+ /** Dim / decreased intensity. */
239
+ const dim$1 = pair(2, 22);
240
+ /** Italic. */
241
+ const italic$1 = pair(3, 23);
242
+ /** Underline. */
243
+ const underline$1 = pair(4, 24);
244
+ /** Inverse / reverse video. */
245
+ const inverse$1 = pair(7, 27);
246
+ /** Hidden / conceal. */
247
+ const hidden$1 = pair(8, 28);
248
+ /** Strikethrough / crossed out. */
249
+ const strikethrough$1 = pair(9, 29);
250
+ const black$1 = pair(30, 39);
251
+ const red$1 = pair(31, 39);
252
+ const green$1 = pair(32, 39);
253
+ const yellow$1 = pair(33, 39);
254
+ const blue$1 = pair(34, 39);
255
+ const magenta$1 = pair(35, 39);
256
+ const cyan$1 = pair(36, 39);
257
+ const white$1 = pair(37, 39);
258
+ /** Bright black (gray). */
259
+ const gray$1 = pair(90, 39);
260
+ const brightRed$1 = pair(91, 39);
261
+ const brightGreen$1 = pair(92, 39);
262
+ const brightYellow$1 = pair(93, 39);
263
+ const brightBlue$1 = pair(94, 39);
264
+ const brightMagenta$1 = pair(95, 39);
265
+ const brightCyan$1 = pair(96, 39);
266
+ const brightWhite$1 = pair(97, 39);
267
+ const bgBlack$1 = pair(40, 49);
268
+ const bgRed$1 = pair(41, 49);
269
+ const bgGreen$1 = pair(42, 49);
270
+ const bgYellow$1 = pair(43, 49);
271
+ const bgBlue$1 = pair(44, 49);
272
+ const bgMagenta$1 = pair(45, 49);
273
+ const bgCyan$1 = pair(46, 49);
274
+ const bgWhite$1 = pair(47, 49);
275
+ const bgBrightBlack$1 = pair(100, 49);
276
+ const bgBrightRed$1 = pair(101, 49);
277
+ const bgBrightGreen$1 = pair(102, 49);
278
+ const bgBrightYellow$1 = pair(103, 49);
279
+ const bgBrightBlue$1 = pair(104, 49);
280
+ const bgBrightMagenta$1 = pair(105, 49);
281
+ const bgBrightCyan$1 = pair(106, 49);
282
+ const bgBrightWhite$1 = pair(107, 49);
283
+ const styleMethodNames = Object.freeze([
284
+ "bold",
285
+ "dim",
286
+ "italic",
287
+ "underline",
288
+ "inverse",
289
+ "hidden",
290
+ "strikethrough",
291
+ "black",
292
+ "red",
293
+ "green",
294
+ "yellow",
295
+ "blue",
296
+ "magenta",
297
+ "cyan",
298
+ "white",
299
+ "gray",
300
+ "brightRed",
301
+ "brightGreen",
302
+ "brightYellow",
303
+ "brightBlue",
304
+ "brightMagenta",
305
+ "brightCyan",
306
+ "brightWhite",
307
+ "bgBlack",
308
+ "bgRed",
309
+ "bgGreen",
310
+ "bgYellow",
311
+ "bgBlue",
312
+ "bgMagenta",
313
+ "bgCyan",
314
+ "bgWhite",
315
+ "bgBrightBlack",
316
+ "bgBrightRed",
317
+ "bgBrightGreen",
318
+ "bgBrightYellow",
319
+ "bgBrightBlue",
320
+ "bgBrightMagenta",
321
+ "bgBrightCyan",
322
+ "bgBrightWhite"
323
+ ]);
324
+ const modifierNameSet = /* @__PURE__ */ new Set([
325
+ "bold",
326
+ "dim",
327
+ "italic",
328
+ "underline",
329
+ "inverse",
330
+ "hidden",
331
+ "strikethrough"
332
+ ]);
333
+ function isModifierName(name) {
334
+ return modifierNameSet.has(name);
335
+ }
336
+ function read(overrides, key, environment) {
337
+ return overrides !== void 0 && key in overrides ? overrides[key] : environment;
338
+ }
339
+ function readTTY(overrides) {
340
+ return read(overrides, "isTTY", process.stdout?.isTTY) ?? false;
341
+ }
342
+ function readForceColor(overrides) {
343
+ return read(overrides, "forceColor", process.env.FORCE_COLOR);
344
+ }
345
+ /** `FORCE_COLOR=0` / `FORCE_COLOR=false` mean "force off"; any other value forces on. */
346
+ function forceColorDisables(forceColor) {
347
+ return forceColor === "0" || forceColor === "false";
348
+ }
349
+ function isTrueColorTerm(term) {
350
+ const lower = term.toLowerCase();
351
+ return lower.includes("24bit") || lower.includes("truecolor") || lower.endsWith("-direct");
352
+ }
353
+ function detectsTruecolor(colorTerm, term) {
354
+ if (colorTerm !== void 0) {
355
+ const lower = colorTerm.toLowerCase();
356
+ if (lower === "truecolor" || lower === "24bit") return true;
357
+ }
358
+ return term !== void 0 && isTrueColorTerm(term);
359
+ }
360
+ function detectDepth(colorTerm, term) {
361
+ if (detectsTruecolor(colorTerm, term)) return "truecolor";
362
+ if (term !== void 0 && term.toLowerCase().includes("256color")) return "256";
363
+ return "16";
364
+ }
365
+ /**
366
+ * Resolve the color depth a terminal can emit.
367
+ *
368
+ * Resolution rules:
369
+ * - `"never"` → `"none"`.
370
+ * - `"always"` → `"truecolor"`.
371
+ * - `"auto"`:
372
+ * 1. `FORCE_COLOR` set → decides unconditionally (overrides TTY and
373
+ * `NO_COLOR`, chalk convention): `0` / `false` → `"none"`; `1` →
374
+ * `"16"`; `2` → `"256"`; `3` → `"truecolor"`; any other value
375
+ * (including empty) → forced on at the `COLORTERM` / `TERM`
376
+ * detected depth.
377
+ * 2. Not a TTY OR `NO_COLOR` set non-empty → `"none"`.
378
+ * 3. `COLORTERM` is `"truecolor"` or `"24bit"` (case-insensitive) →
379
+ * `"truecolor"`.
380
+ * 4. `TERM` ends with `-direct` OR contains `truecolor` / `24bit` →
381
+ * `"truecolor"`.
382
+ * 5. `TERM === "dumb"` → `"none"`.
383
+ * 6. `TERM` contains `256color` → `"256"`.
384
+ * 7. Any other TTY value → `"16"`.
385
+ *
386
+ * Detection follows the ecosystem `FORCE_COLOR` / `NO_COLOR` / `COLORTERM`
387
+ * / `TERM` conventions; no bespoke environment variables are introduced.
388
+ *
389
+ * @param mode - The color emission mode.
390
+ * @param overrides - Optional overrides for deterministic testing.
391
+ * @returns The resolved {@link ColorDepth} tier.
392
+ *
393
+ * @example
394
+ * ```ts
395
+ * resolveColorDepth("always"); // "truecolor"
396
+ * resolveColorDepth("never"); // "none"
397
+ * resolveColorDepth("auto", {
398
+ * isTTY: true,
399
+ * noColor: undefined,
400
+ * colorTerm: "truecolor",
401
+ * }); // "truecolor"
402
+ * resolveColorDepth("auto", {
403
+ * isTTY: true,
404
+ * noColor: undefined,
405
+ * colorTerm: undefined,
406
+ * term: "xterm-256color",
407
+ * }); // "256"
408
+ * ```
409
+ */
410
+ function resolveColorDepth(mode, overrides) {
411
+ if (mode === "never") return "none";
412
+ if (mode === "always") return "truecolor";
413
+ const colorTerm = read(overrides, "colorTerm", process.env.COLORTERM);
414
+ const term = read(overrides, "term", process.env.TERM);
415
+ const forceColor = readForceColor(overrides);
416
+ if (forceColor !== void 0) {
417
+ if (forceColorDisables(forceColor)) return "none";
418
+ if (forceColor === "1") return "16";
419
+ if (forceColor === "2") return "256";
420
+ if (forceColor === "3") return "truecolor";
421
+ return detectDepth(colorTerm, term);
422
+ }
423
+ if (!readTTY(overrides)) return "none";
424
+ const noColor = read(overrides, "noColor", process.env.NO_COLOR);
425
+ if (noColor !== void 0 && noColor !== "") return "none";
426
+ if (term !== void 0 && term.toLowerCase() === "dumb" && !detectsTruecolor(colorTerm, term)) return "none";
427
+ return detectDepth(colorTerm, term);
428
+ }
429
+ /**
430
+ * Resolve whether non-color ANSI modifiers should be emitted.
431
+ *
432
+ * In `"auto"` mode, modifiers are enabled when stdout is a TTY, but are
433
+ * **not** affected by `NO_COLOR` (which only controls color output).
434
+ * `FORCE_COLOR`, when set, decides unconditionally — it is the all-ANSI
435
+ * switch, while `NO_COLOR` is the colors-only switch.
436
+ *
437
+ * @internal Exported only for use by {@link createStyle}; not part of the
438
+ * public surface of `@crustjs/style`.
439
+ */
440
+ function resolveModifierCapability(mode, overrides) {
441
+ if (mode === "always") return true;
442
+ if (mode === "never") return false;
443
+ const forceColor = readForceColor(overrides);
444
+ if (forceColor !== void 0) return !forceColorDisables(forceColor);
445
+ return readTTY(overrides);
446
+ }
447
+ const namedColorValues = {
448
+ aliceblue: [
449
+ 240,
450
+ 248,
451
+ 255
452
+ ],
453
+ antiquewhite: [
454
+ 250,
455
+ 235,
456
+ 215
457
+ ],
458
+ aqua: [
459
+ 0,
460
+ 255,
461
+ 255
462
+ ],
463
+ aquamarine: [
464
+ 127,
465
+ 255,
466
+ 212
467
+ ],
468
+ azure: [
469
+ 240,
470
+ 255,
471
+ 255
472
+ ],
473
+ beige: [
474
+ 245,
475
+ 245,
476
+ 220
477
+ ],
478
+ bisque: [
479
+ 255,
480
+ 228,
481
+ 196
482
+ ],
483
+ black: [
484
+ 0,
485
+ 0,
486
+ 0
487
+ ],
488
+ blanchedalmond: [
489
+ 255,
490
+ 235,
491
+ 205
492
+ ],
493
+ blue: [
494
+ 0,
495
+ 0,
496
+ 255
497
+ ],
498
+ blueviolet: [
499
+ 138,
500
+ 43,
501
+ 226
502
+ ],
503
+ brown: [
504
+ 165,
505
+ 42,
506
+ 42
507
+ ],
508
+ burlywood: [
509
+ 222,
510
+ 184,
511
+ 135
512
+ ],
513
+ cadetblue: [
514
+ 95,
515
+ 158,
516
+ 160
517
+ ],
518
+ chartreuse: [
519
+ 127,
520
+ 255,
521
+ 0
522
+ ],
523
+ chocolate: [
524
+ 210,
525
+ 105,
526
+ 30
527
+ ],
528
+ coral: [
529
+ 255,
530
+ 127,
531
+ 80
532
+ ],
533
+ cornflowerblue: [
534
+ 100,
535
+ 149,
536
+ 237
537
+ ],
538
+ cornsilk: [
539
+ 255,
540
+ 248,
541
+ 220
542
+ ],
543
+ crimson: [
544
+ 220,
545
+ 20,
546
+ 60
547
+ ],
548
+ cyan: [
549
+ 0,
550
+ 255,
551
+ 255
552
+ ],
553
+ darkblue: [
554
+ 0,
555
+ 0,
556
+ 139
557
+ ],
558
+ darkcyan: [
559
+ 0,
560
+ 139,
561
+ 139
562
+ ],
563
+ darkgoldenrod: [
564
+ 184,
565
+ 134,
566
+ 11
567
+ ],
568
+ darkgray: [
569
+ 169,
570
+ 169,
571
+ 169
572
+ ],
573
+ darkgreen: [
574
+ 0,
575
+ 100,
576
+ 0
577
+ ],
578
+ darkgrey: [
579
+ 169,
580
+ 169,
581
+ 169
582
+ ],
583
+ darkkhaki: [
584
+ 189,
585
+ 183,
586
+ 107
587
+ ],
588
+ darkmagenta: [
589
+ 139,
590
+ 0,
591
+ 139
592
+ ],
593
+ darkolivegreen: [
594
+ 85,
595
+ 107,
596
+ 47
597
+ ],
598
+ darkorange: [
599
+ 255,
600
+ 140,
601
+ 0
602
+ ],
603
+ darkorchid: [
604
+ 153,
605
+ 50,
606
+ 204
607
+ ],
608
+ darkred: [
609
+ 139,
610
+ 0,
611
+ 0
612
+ ],
613
+ darksalmon: [
614
+ 233,
615
+ 150,
616
+ 122
617
+ ],
618
+ darkseagreen: [
619
+ 143,
620
+ 188,
621
+ 143
622
+ ],
623
+ darkslateblue: [
624
+ 72,
625
+ 61,
626
+ 139
627
+ ],
628
+ darkslategray: [
629
+ 47,
630
+ 79,
631
+ 79
632
+ ],
633
+ darkslategrey: [
634
+ 47,
635
+ 79,
636
+ 79
637
+ ],
638
+ darkturquoise: [
639
+ 0,
640
+ 206,
641
+ 209
642
+ ],
643
+ darkviolet: [
644
+ 148,
645
+ 0,
646
+ 211
647
+ ],
648
+ deeppink: [
649
+ 255,
650
+ 20,
651
+ 147
652
+ ],
653
+ deepskyblue: [
654
+ 0,
655
+ 191,
656
+ 255
657
+ ],
658
+ dimgray: [
659
+ 105,
660
+ 105,
661
+ 105
662
+ ],
663
+ dimgrey: [
664
+ 105,
665
+ 105,
666
+ 105
667
+ ],
668
+ dodgerblue: [
669
+ 30,
670
+ 144,
671
+ 255
672
+ ],
673
+ firebrick: [
674
+ 178,
675
+ 34,
676
+ 34
677
+ ],
678
+ floralwhite: [
679
+ 255,
680
+ 250,
681
+ 240
682
+ ],
683
+ forestgreen: [
684
+ 34,
685
+ 139,
686
+ 34
687
+ ],
688
+ fuchsia: [
689
+ 255,
690
+ 0,
691
+ 255
692
+ ],
693
+ gainsboro: [
694
+ 220,
695
+ 220,
696
+ 220
697
+ ],
698
+ ghostwhite: [
699
+ 248,
700
+ 248,
701
+ 255
702
+ ],
703
+ gold: [
704
+ 255,
705
+ 215,
706
+ 0
707
+ ],
708
+ goldenrod: [
709
+ 218,
710
+ 165,
711
+ 32
712
+ ],
713
+ gray: [
714
+ 128,
715
+ 128,
716
+ 128
717
+ ],
718
+ green: [
719
+ 0,
720
+ 128,
721
+ 0
722
+ ],
723
+ greenyellow: [
724
+ 173,
725
+ 255,
726
+ 47
727
+ ],
728
+ grey: [
729
+ 128,
730
+ 128,
731
+ 128
732
+ ],
733
+ honeydew: [
734
+ 240,
735
+ 255,
736
+ 240
737
+ ],
738
+ hotpink: [
739
+ 255,
740
+ 105,
741
+ 180
742
+ ],
743
+ indianred: [
744
+ 205,
745
+ 92,
746
+ 92
747
+ ],
748
+ indigo: [
749
+ 75,
750
+ 0,
751
+ 130
752
+ ],
753
+ ivory: [
754
+ 255,
755
+ 255,
756
+ 240
757
+ ],
758
+ khaki: [
759
+ 240,
760
+ 230,
761
+ 140
762
+ ],
763
+ lavender: [
764
+ 230,
765
+ 230,
766
+ 250
767
+ ],
768
+ lavenderblush: [
769
+ 255,
770
+ 240,
771
+ 245
772
+ ],
773
+ lawngreen: [
774
+ 124,
775
+ 252,
776
+ 0
777
+ ],
778
+ lemonchiffon: [
779
+ 255,
780
+ 250,
781
+ 205
782
+ ],
783
+ lightblue: [
784
+ 173,
785
+ 216,
786
+ 230
787
+ ],
788
+ lightcoral: [
789
+ 240,
790
+ 128,
791
+ 128
792
+ ],
793
+ lightcyan: [
794
+ 224,
795
+ 255,
796
+ 255
797
+ ],
798
+ lightgoldenrodyellow: [
799
+ 250,
800
+ 250,
801
+ 210
802
+ ],
803
+ lightgray: [
804
+ 211,
805
+ 211,
806
+ 211
807
+ ],
808
+ lightgreen: [
809
+ 144,
810
+ 238,
811
+ 144
812
+ ],
813
+ lightgrey: [
814
+ 211,
815
+ 211,
816
+ 211
817
+ ],
818
+ lightpink: [
819
+ 255,
820
+ 182,
821
+ 193
822
+ ],
823
+ lightsalmon: [
824
+ 255,
825
+ 160,
826
+ 122
827
+ ],
828
+ lightseagreen: [
829
+ 32,
830
+ 178,
831
+ 170
832
+ ],
833
+ lightskyblue: [
834
+ 135,
835
+ 206,
836
+ 250
837
+ ],
838
+ lightslategray: [
839
+ 119,
840
+ 136,
841
+ 153
842
+ ],
843
+ lightslategrey: [
844
+ 119,
845
+ 136,
846
+ 153
847
+ ],
848
+ lightsteelblue: [
849
+ 176,
850
+ 196,
851
+ 222
852
+ ],
853
+ lightyellow: [
854
+ 255,
855
+ 255,
856
+ 224
857
+ ],
858
+ lime: [
859
+ 0,
860
+ 255,
861
+ 0
862
+ ],
863
+ limegreen: [
864
+ 50,
865
+ 205,
866
+ 50
867
+ ],
868
+ linen: [
869
+ 250,
870
+ 240,
871
+ 230
872
+ ],
873
+ magenta: [
874
+ 255,
875
+ 0,
876
+ 255
877
+ ],
878
+ maroon: [
879
+ 128,
880
+ 0,
881
+ 0
882
+ ],
883
+ mediumaquamarine: [
884
+ 102,
885
+ 205,
886
+ 170
887
+ ],
888
+ mediumblue: [
889
+ 0,
890
+ 0,
891
+ 205
892
+ ],
893
+ mediumorchid: [
894
+ 186,
895
+ 85,
896
+ 211
897
+ ],
898
+ mediumpurple: [
899
+ 147,
900
+ 112,
901
+ 219
902
+ ],
903
+ mediumseagreen: [
904
+ 60,
905
+ 179,
906
+ 113
907
+ ],
908
+ mediumslateblue: [
909
+ 123,
910
+ 104,
911
+ 238
912
+ ],
913
+ mediumspringgreen: [
914
+ 0,
915
+ 250,
916
+ 154
917
+ ],
918
+ mediumturquoise: [
919
+ 72,
920
+ 209,
921
+ 204
922
+ ],
923
+ mediumvioletred: [
924
+ 199,
925
+ 21,
926
+ 133
927
+ ],
928
+ midnightblue: [
929
+ 25,
930
+ 25,
931
+ 112
932
+ ],
933
+ mintcream: [
934
+ 245,
935
+ 255,
936
+ 250
937
+ ],
938
+ mistyrose: [
939
+ 255,
940
+ 228,
941
+ 225
942
+ ],
943
+ moccasin: [
944
+ 255,
945
+ 228,
946
+ 181
947
+ ],
948
+ navajowhite: [
949
+ 255,
950
+ 222,
951
+ 173
952
+ ],
953
+ navy: [
954
+ 0,
955
+ 0,
956
+ 128
957
+ ],
958
+ oldlace: [
959
+ 253,
960
+ 245,
961
+ 230
962
+ ],
963
+ olive: [
964
+ 128,
965
+ 128,
966
+ 0
967
+ ],
968
+ olivedrab: [
969
+ 107,
970
+ 142,
971
+ 35
972
+ ],
973
+ orange: [
974
+ 255,
975
+ 165,
976
+ 0
977
+ ],
978
+ orangered: [
979
+ 255,
980
+ 69,
981
+ 0
982
+ ],
983
+ orchid: [
984
+ 218,
985
+ 112,
986
+ 214
987
+ ],
988
+ palegoldenrod: [
989
+ 238,
990
+ 232,
991
+ 170
992
+ ],
993
+ palegreen: [
994
+ 152,
995
+ 251,
996
+ 152
997
+ ],
998
+ paleturquoise: [
999
+ 175,
1000
+ 238,
1001
+ 238
1002
+ ],
1003
+ palevioletred: [
1004
+ 219,
1005
+ 112,
1006
+ 147
1007
+ ],
1008
+ papayawhip: [
1009
+ 255,
1010
+ 239,
1011
+ 213
1012
+ ],
1013
+ peachpuff: [
1014
+ 255,
1015
+ 218,
1016
+ 185
1017
+ ],
1018
+ peru: [
1019
+ 205,
1020
+ 133,
1021
+ 63
1022
+ ],
1023
+ pink: [
1024
+ 255,
1025
+ 192,
1026
+ 203
1027
+ ],
1028
+ plum: [
1029
+ 221,
1030
+ 160,
1031
+ 221
1032
+ ],
1033
+ powderblue: [
1034
+ 176,
1035
+ 224,
1036
+ 230
1037
+ ],
1038
+ purple: [
1039
+ 128,
1040
+ 0,
1041
+ 128
1042
+ ],
1043
+ rebeccapurple: [
1044
+ 102,
1045
+ 51,
1046
+ 153
1047
+ ],
1048
+ red: [
1049
+ 255,
1050
+ 0,
1051
+ 0
1052
+ ],
1053
+ rosybrown: [
1054
+ 188,
1055
+ 143,
1056
+ 143
1057
+ ],
1058
+ royalblue: [
1059
+ 65,
1060
+ 105,
1061
+ 225
1062
+ ],
1063
+ saddlebrown: [
1064
+ 139,
1065
+ 69,
1066
+ 19
1067
+ ],
1068
+ salmon: [
1069
+ 250,
1070
+ 128,
1071
+ 114
1072
+ ],
1073
+ sandybrown: [
1074
+ 244,
1075
+ 164,
1076
+ 96
1077
+ ],
1078
+ seagreen: [
1079
+ 46,
1080
+ 139,
1081
+ 87
1082
+ ],
1083
+ seashell: [
1084
+ 255,
1085
+ 245,
1086
+ 238
1087
+ ],
1088
+ sienna: [
1089
+ 160,
1090
+ 82,
1091
+ 45
1092
+ ],
1093
+ silver: [
1094
+ 192,
1095
+ 192,
1096
+ 192
1097
+ ],
1098
+ skyblue: [
1099
+ 135,
1100
+ 206,
1101
+ 235
1102
+ ],
1103
+ slateblue: [
1104
+ 106,
1105
+ 90,
1106
+ 205
1107
+ ],
1108
+ slategray: [
1109
+ 112,
1110
+ 128,
1111
+ 144
1112
+ ],
1113
+ slategrey: [
1114
+ 112,
1115
+ 128,
1116
+ 144
1117
+ ],
1118
+ snow: [
1119
+ 255,
1120
+ 250,
1121
+ 250
1122
+ ],
1123
+ springgreen: [
1124
+ 0,
1125
+ 255,
1126
+ 127
1127
+ ],
1128
+ steelblue: [
1129
+ 70,
1130
+ 130,
1131
+ 180
1132
+ ],
1133
+ tan: [
1134
+ 210,
1135
+ 180,
1136
+ 140
1137
+ ],
1138
+ teal: [
1139
+ 0,
1140
+ 128,
1141
+ 128
1142
+ ],
1143
+ thistle: [
1144
+ 216,
1145
+ 191,
1146
+ 216
1147
+ ],
1148
+ tomato: [
1149
+ 255,
1150
+ 99,
1151
+ 71
1152
+ ],
1153
+ turquoise: [
1154
+ 64,
1155
+ 224,
1156
+ 208
1157
+ ],
1158
+ violet: [
1159
+ 238,
1160
+ 130,
1161
+ 238
1162
+ ],
1163
+ wheat: [
1164
+ 245,
1165
+ 222,
1166
+ 179
1167
+ ],
1168
+ white: [
1169
+ 255,
1170
+ 255,
1171
+ 255
1172
+ ],
1173
+ whitesmoke: [
1174
+ 245,
1175
+ 245,
1176
+ 245
1177
+ ],
1178
+ yellow: [
1179
+ 255,
1180
+ 255,
1181
+ 0
1182
+ ],
1183
+ yellowgreen: [
1184
+ 154,
1185
+ 205,
1186
+ 50
1187
+ ]
1188
+ };
1189
+ /**
1190
+ * Apply an ANSI style pair to a string with nesting-safe composition.
1191
+ *
1192
+ * When the input already contains the close sequence for the applied style
1193
+ * (e.g. from a nested style call that shares the same close code), the engine
1194
+ * reopens the outer style after each inner close to prevent style bleed.
1195
+ *
1196
+ * @param text - The string to style.
1197
+ * @param style - The ANSI pair to apply.
1198
+ * @returns The styled string, or the original string if empty.
1199
+ *
1200
+ * @example
1201
+ * ```ts
1202
+ * import { applyStyle } from "./styleEngine.ts";
1203
+ * import { bold, red } from "./ansiCodes.ts";
1204
+ *
1205
+ * // Simple usage
1206
+ * applyStyle("hello", bold); // "\x1b[1mhello\x1b[22m"
1207
+ *
1208
+ * // Nesting: bold wraps a red segment — bold reopens after red's close
1209
+ * const inner = applyStyle("world", red);
1210
+ * applyStyle(`hello ${inner}!`, bold);
1211
+ * ```
1212
+ */
1213
+ function applyStyle(text, style) {
1214
+ if (text == null) return "";
1215
+ let result = String(text);
1216
+ if (result === "") return "";
1217
+ const { open, close } = style;
1218
+ if (result.includes(close)) result = result.replaceAll(close, close + open);
1219
+ return open + result + close;
1220
+ }
1221
+ /** Foreground close: matches the close of every static fg color (`\x1b[39m`). */
1222
+ const FG_CLOSE = "\x1B[39m";
1223
+ /** Background close: matches the close of every static bg color (`\x1b[49m`). */
1224
+ const BG_CLOSE = "\x1B[49m";
1225
+ /** Foreground/background extended-color SGR introducers. */
1226
+ const FG_INTRODUCER = "\x1B[38;";
1227
+ const BG_INTRODUCER = "\x1B[48;";
1228
+ /** Quote supported inputs, falling back to `String()` for hostile runtime values. */
1229
+ function describeInput(input) {
1230
+ try {
1231
+ return JSON.stringify(input) ?? String(input);
1232
+ } catch {
1233
+ return String(input);
1234
+ }
1235
+ }
1236
+ function isNamedColor(value) {
1237
+ return Object.hasOwn(namedColorValues, value);
1238
+ }
1239
+ /** Parse a supported color into an RGB triple. */
1240
+ function parseRgb(input) {
1241
+ if (Array.isArray(input)) {
1242
+ if (input.length === 3 && input.every((channel) => Number.isInteger(channel) && channel >= 0 && channel <= 255)) return [
1243
+ input[0],
1244
+ input[1],
1245
+ input[2]
1246
+ ];
1247
+ } else if (typeof input === "string") {
1248
+ const value = input.toLowerCase();
1249
+ if (isNamedColor(value)) return namedColorValues[value];
1250
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value)?.[1];
1251
+ if (hex) {
1252
+ const expanded = hex.length === 3 ? hex.split("").map((digit) => digit + digit).join("") : hex;
1253
+ return [
1254
+ Number.parseInt(expanded.slice(0, 2), 16),
1255
+ Number.parseInt(expanded.slice(2, 4), 16),
1256
+ Number.parseInt(expanded.slice(4, 6), 16)
1257
+ ];
1258
+ }
1259
+ const rgb = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/.exec(value) ?? /^rgb\(\s*(\d{1,3})\s+(\d{1,3})\s+(\d{1,3})\s*\)$/.exec(value);
1260
+ if (rgb) {
1261
+ const channels = [
1262
+ Number(rgb[1]),
1263
+ Number(rgb[2]),
1264
+ Number(rgb[3])
1265
+ ];
1266
+ if (channels.every((channel) => channel <= 255)) return channels;
1267
+ }
1268
+ }
1269
+ throw new TypeError(`Invalid color input: ${describeInput(input)}`);
1270
+ }
1271
+ function rgbToAnsi256(r, g, b) {
1272
+ const levels = [
1273
+ 0,
1274
+ 95,
1275
+ 135,
1276
+ 175,
1277
+ 215,
1278
+ 255
1279
+ ];
1280
+ const nearest = (channel) => levels.reduce((best, value, index) => Math.abs(value - channel) < Math.abs(levels[best] - channel) ? index : best, 0);
1281
+ const [ir, ig, ib] = [
1282
+ nearest(r),
1283
+ nearest(g),
1284
+ nearest(b)
1285
+ ];
1286
+ const grayIndex = Math.min(23, Math.max(0, Math.round(((r + g + b) / 3 - 8) / 10)));
1287
+ const gray = 8 + grayIndex * 10;
1288
+ const distance = (cr, cg, cb) => (r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2;
1289
+ return distance(gray, gray, gray) < distance(levels[ir], levels[ig], levels[ib]) ? 232 + grayIndex : 16 + 36 * ir + 6 * ig + ib;
1290
+ }
1291
+ /**
1292
+ * Quantize `[r, g, b]` to a foreground SGR parameter (`30`–`37`, `90`–`97`).
1293
+ * Same algorithm as `ansi-styles` / `chalk`: bucket each channel at 50%,
1294
+ * pack into a 3-bit base color, then add 60 for bright when the max
1295
+ * channel rounds up. Call sites add `+10` for backgrounds.
1296
+ *
1297
+ * @internal
1298
+ */
1299
+ function rgbToAnsi16Param(r, g, b) {
1300
+ const brightnessBucket = Math.round(Math.max(r, g, b) / 127.5);
1301
+ if (brightnessBucket === 0) return 30;
1302
+ let ansi = 30 + ((Math.round(b / 255) << 2 | Math.round(g / 255) << 1 | Math.round(r / 255)) & 7);
1303
+ if (brightnessBucket === 2) ansi += 60;
1304
+ return ansi;
1305
+ }
1306
+ /**
1307
+ * Foreground SGR open sequence at `depth`.
1308
+ *
1309
+ * @internal
1310
+ */
1311
+ function fgOpen(input, depth) {
1312
+ const [r, g, b] = parseRgb(input);
1313
+ if (depth === "16") return `\x1b[${rgbToAnsi16Param(r, g, b)}m`;
1314
+ if (depth === "256") return `\x1b[38;5;${rgbToAnsi256(r, g, b)}m`;
1315
+ return `\x1b[38;2;${r};${g};${b}m`;
1316
+ }
1317
+ /**
1318
+ * Background SGR open sequence at `depth`. For `truecolor` / `256`,
1319
+ * derived from {@link fgOpen} by swapping the `\x1b[38;` introducer for
1320
+ * `\x1b[48;` (both the truecolor and 256-color forms use it). For `16`,
1321
+ * quantized directly to a real background SGR.
1322
+ *
1323
+ * @internal
1324
+ */
1325
+ function bgOpen(input, depth) {
1326
+ if (depth === "16") {
1327
+ const [r, g, b] = parseRgb(input);
1328
+ return `\x1b[${rgbToAnsi16Param(r, g, b) + 10}m`;
1329
+ }
1330
+ return fgOpen(input, depth).replace(FG_INTRODUCER, BG_INTRODUCER);
1331
+ }
1332
+ function colorPair(kind, input, depth) {
1333
+ if (depth === "none") {
1334
+ parseRgb(input);
1335
+ return {
1336
+ open: "",
1337
+ close: ""
1338
+ };
1339
+ }
1340
+ return {
1341
+ open: kind === "fg" ? fgOpen(input, depth) : bgOpen(input, depth),
1342
+ close: kind === "fg" ? FG_CLOSE : BG_CLOSE
1343
+ };
1344
+ }
1345
+ /**
1346
+ * Depth-aware foreground `AnsiPair` for chain composition. `depth: "none"`
1347
+ * returns an empty pair (still validates input). Used by `createStyle()`
1348
+ * to back `chainable.fg(input)`.
1349
+ *
1350
+ * @throws {TypeError} If `input` is not a recognized color.
1351
+ * @internal
1352
+ */
1353
+ function fgPairAtDepth(input, depth) {
1354
+ return colorPair("fg", input, depth);
1355
+ }
1356
+ /**
1357
+ * Depth-aware background `AnsiPair` for chain composition. Mirrors
1358
+ * {@link fgPairAtDepth}.
1359
+ *
1360
+ * @throws {TypeError} If `input` is not a recognized color.
1361
+ * @internal
1362
+ */
1363
+ function bgPairAtDepth(input, depth) {
1364
+ return colorPair("bg", input, depth);
1365
+ }
1366
+ function paint(kind, text, input, depth) {
1367
+ const pair = colorPair(kind, input, depth);
1368
+ return depth === "none" ? text : applyStyle(text, pair);
1369
+ }
1370
+ /**
1371
+ * Apply a foreground color to `text` from any {@link ColorInput}. `depth`
1372
+ * selects the output format (`"truecolor"` default, `"256"`, `"16"`, or
1373
+ * `"none"`). `"none"` returns `text` unchanged but still validates
1374
+ * `input`. Empty `text` short-circuits to `""`.
1375
+ *
1376
+ * @throws {TypeError} If `input` is not a recognized color.
1377
+ *
1378
+ * @example
1379
+ * ```ts
1380
+ * fg("error", "#ff0000");
1381
+ * fg("ocean", "rgb(0, 128, 255)");
1382
+ * fg("custom", [255, 127, 80]);
1383
+ * fg("256-only", "#ff0000", "256"); // \x1b[38;5;196m...
1384
+ * ```
1385
+ */
1386
+ function fg$1(text, input, depth = "truecolor") {
1387
+ return paint("fg", text, input, depth);
1388
+ }
1389
+ /**
1390
+ * Apply a background color to `text`. Mirrors {@link fg}.
1391
+ *
1392
+ * @throws {TypeError} If `input` is not a recognized color.
1393
+ *
1394
+ * @example
1395
+ * ```ts
1396
+ * bg("warning", "#ff8800");
1397
+ * bg("info", "rgb(0, 128, 255)");
1398
+ * ```
1399
+ */
1400
+ function bg$1(text, input, depth = "truecolor") {
1401
+ return paint("bg", text, input, depth);
1402
+ }
1403
+ const OSC = "\x1B]";
1404
+ const ST = "\x1B\\";
1405
+ const HYPERLINK_CLOSE = `${OSC}8;;${ST}`;
1406
+ function assertMatches(value, label, pattern, suffix = "") {
1407
+ if (!pattern.test(value)) throw new TypeError(`Invalid ${label}: ${JSON.stringify(value)} must contain only printable ASCII characters${suffix}.`);
1408
+ }
1409
+ function serializeParams(options) {
1410
+ const id = options?.id;
1411
+ if (id === void 0 || id === "") return "";
1412
+ assertMatches(id, "hyperlink id", /^[\x20-\x7e]*$/);
1413
+ if (id.includes(":") || id.includes(";")) throw new TypeError("Invalid hyperlink id: \":\" and \";\" are reserved by the OSC 8 format.");
1414
+ return `id=${id}`;
1415
+ }
1416
+ /**
1417
+ * Create an OSC 8 hyperlink escape pair for `url`. Backs {@link link} and
1418
+ * the `link` method on style instances; also used to validate URLs when
1419
+ * hyperlink emission is disabled.
1420
+ *
1421
+ * @param url - Target URL. Must contain only printable ASCII characters
1422
+ * and no spaces (per the OSC 8 spec). URL-encode characters outside
1423
+ * that range before passing.
1424
+ * @param options - Optional {@link HyperlinkOptions} — currently just
1425
+ * `id` for grouping multi-segment links.
1426
+ * @returns An {@link AnsiPair} whose `open` carries the URL and `close`
1427
+ * terminates the hyperlink.
1428
+ * @throws {TypeError} If `url` contains spaces or non-printable ASCII,
1429
+ * or if `options.id` contains `":"` / `";"` (reserved by OSC 8) or
1430
+ * non-printable characters.
1431
+ *
1432
+ * @internal
1433
+ */
1434
+ function linkCode(url, options) {
1435
+ assertMatches(url, "hyperlink URL", /^[\x21-\x7e]*$/, " without spaces");
1436
+ const params = serializeParams(options);
1437
+ return {
1438
+ open: `${OSC}8;${params};${url}${ST}`,
1439
+ close: HYPERLINK_CLOSE
1440
+ };
1441
+ }
1442
+ /**
1443
+ * Wrap `text` in OSC 8 hyperlink escape sequences.
1444
+ *
1445
+ * @param text - The visible label.
1446
+ * @param url - Target URL. Must contain only printable ASCII characters
1447
+ * and no spaces (per the OSC 8 spec); URL-encode anything outside that
1448
+ * range before passing.
1449
+ * @param options - Optional {@link HyperlinkOptions}.
1450
+ * @returns The styled string.
1451
+ * @throws {TypeError} If `url` contains spaces or non-printable ASCII,
1452
+ * or if `options.id` contains `":"` / `";"` (reserved by OSC 8) or
1453
+ * non-printable characters.
1454
+ *
1455
+ * @example
1456
+ * ```ts
1457
+ * import { link } from "@crustjs/style";
1458
+ *
1459
+ * console.log(link("docs", "https://crustjs.dev"));
1460
+ * console.log(link("page 1", "https://example.com/p1", { id: "intro" }));
1461
+ * ```
1462
+ */
1463
+ function link$1(text, url, options) {
1464
+ return applyStyle(text, linkCode(url, options));
1465
+ }
1466
+ const dynamicColorKinds = [[
1467
+ "fg",
1468
+ fgPairAtDepth,
1469
+ fg$1
1470
+ ], [
1471
+ "bg",
1472
+ bgPairAtDepth,
1473
+ bg$1
1474
+ ]];
1475
+ const styleMethodPairs = ansiCodes_exports;
1476
+ function stepIsModifier(step) {
1477
+ return step.kind === "named" && isModifierName(step.name);
1478
+ }
1479
+ function stepPair(step, colorDepth) {
1480
+ if (step.kind === "named") return styleMethodPairs[step.name];
1481
+ return step.kind === "fg" ? fgPairAtDepth(step.input, colorDepth) : bgPairAtDepth(step.input, colorDepth);
1482
+ }
1483
+ function applyChain(text, steps, resolveCapabilities) {
1484
+ if (text == null) return "";
1485
+ let result = String(text);
1486
+ if (result === "") return result;
1487
+ const capabilities = resolveCapabilities();
1488
+ const { modifiersEnabled, colorsEnabled } = capabilities;
1489
+ for (let i = steps.length - 1; i >= 0; i--) {
1490
+ const step = steps[i];
1491
+ if (step === void 0) continue;
1492
+ if (stepIsModifier(step) ? !modifiersEnabled : !colorsEnabled) continue;
1493
+ result = applyStyle(result, stepPair(step, capabilities.colorDepth));
1494
+ }
1495
+ return result;
1496
+ }
1497
+ function isTemplateStringsArray(value) {
1498
+ return Array.isArray(value) && "raw" in value && Array.isArray(value.raw);
1499
+ }
1500
+ function buildChainableStyleFactory(resolveCapabilities, runtime) {
1501
+ const cache = /* @__PURE__ */ new Map();
1502
+ const pairDepth = runtime ? "truecolor" : resolveCapabilities().colorDepth;
1503
+ function createChainableStyle(steps) {
1504
+ const key = steps.map((step) => step.kind === "named" ? step.name : `~${stepPair(step, "truecolor").open}`).join("|");
1505
+ const cached = cache.get(key);
1506
+ if (cached) return cached;
1507
+ const styleFn = ((first, ...rest) => {
1508
+ if (isTemplateStringsArray(first)) {
1509
+ let text = "";
1510
+ for (let i = 0; i < first.length; i++) {
1511
+ text += first[i] ?? "";
1512
+ if (i < rest.length) text += String(rest[i]);
1513
+ }
1514
+ return applyChain(text, steps, resolveCapabilities);
1515
+ }
1516
+ return applyChain(first, steps, resolveCapabilities);
1517
+ });
1518
+ if (cache.size >= 1024) cache.delete(cache.keys().next().value);
1519
+ cache.set(key, styleFn);
1520
+ for (const name of styleMethodNames) Object.defineProperty(styleFn, name, {
1521
+ configurable: false,
1522
+ enumerable: true,
1523
+ get() {
1524
+ return createChainableStyle([...steps, {
1525
+ kind: "named",
1526
+ name
1527
+ }]);
1528
+ }
1529
+ });
1530
+ for (const [kind, pairAtDepth] of dynamicColorKinds) Object.defineProperty(styleFn, kind, {
1531
+ configurable: false,
1532
+ enumerable: true,
1533
+ value: (input) => {
1534
+ pairAtDepth(input, "truecolor");
1535
+ return createChainableStyle([...steps, {
1536
+ kind,
1537
+ input
1538
+ }]);
1539
+ },
1540
+ writable: false
1541
+ });
1542
+ let open = "";
1543
+ let close = "";
1544
+ for (const step of steps) {
1545
+ const pair = stepPair(step, pairDepth);
1546
+ open += pair.open;
1547
+ close = pair.close + close;
1548
+ }
1549
+ Object.defineProperty(styleFn, "open", {
1550
+ value: open,
1551
+ writable: false,
1552
+ configurable: false,
1553
+ enumerable: true
1554
+ });
1555
+ Object.defineProperty(styleFn, "close", {
1556
+ value: close,
1557
+ writable: false,
1558
+ configurable: false,
1559
+ enumerable: true
1560
+ });
1561
+ return Object.freeze(styleFn);
1562
+ }
1563
+ return createChainableStyle;
1564
+ }
1565
+ function buildStyleMethods(createChainableStyle) {
1566
+ const methods = {};
1567
+ for (const methodName of styleMethodNames) methods[methodName] = createChainableStyle([{
1568
+ kind: "named",
1569
+ name: methodName
1570
+ }]);
1571
+ return methods;
1572
+ }
1573
+ /**
1574
+ * Create a configured style instance with mode-aware styling functions.
1575
+ *
1576
+ * The returned instance provides the full set of modifier, foreground color,
1577
+ * and background color functions. In `"never"` mode, all functions return
1578
+ * plain text without ANSI codes. In `"always"` mode, ANSI codes are always
1579
+ * emitted. In `"auto"` mode, color methods respect `stdout.isTTY` and
1580
+ * `NO_COLOR`, non-color modifiers (bold, italic, etc.) follow TTY only,
1581
+ * and `FORCE_COLOR`, when set, decides unconditionally for both.
1582
+ *
1583
+ * @param options - Configuration options. Defaults to `{ mode: "auto" }`.
1584
+ * @returns A frozen {@link StyleInstance} with all styling functions.
1585
+ *
1586
+ * @example
1587
+ * ```ts
1588
+ * // Auto-detect terminal capabilities
1589
+ * const s = createStyle();
1590
+ * console.log(s.bold("hello"));
1591
+ *
1592
+ * // Force color output
1593
+ * const color = createStyle({ mode: "always" });
1594
+ * console.log(color.red("error"));
1595
+ * console.log(color.bold.red("critical"));
1596
+ *
1597
+ * // Disable all styling
1598
+ * const plain = createStyle({ mode: "never" });
1599
+ * console.log(plain.red("error")); // "error"
1600
+ *
1601
+ * // Deterministic testing
1602
+ * const test = createStyle({
1603
+ * mode: "auto",
1604
+ * overrides: { isTTY: true, noColor: undefined },
1605
+ * });
1606
+ * ```
1607
+ */
1608
+ function resolveStyleCapabilities(options) {
1609
+ const mode = options?.mode ?? "auto";
1610
+ const modifiersEnabled = resolveModifierCapability(mode, options?.overrides);
1611
+ const colorDepth = resolveColorDepth(mode, options?.overrides);
1612
+ return {
1613
+ modifiersEnabled,
1614
+ colorDepth,
1615
+ colorsEnabled: colorDepth !== "none",
1616
+ trueColorEnabled: colorDepth === "truecolor"
1617
+ };
1618
+ }
1619
+ function createStyleInstance(options, runtime) {
1620
+ const resolveCapabilities = runtime ? resolveStyleCapabilities : (() => {
1621
+ const capabilities = resolveStyleCapabilities(options);
1622
+ return () => capabilities;
1623
+ })();
1624
+ const createChainableStyle = buildChainableStyleFactory(resolveCapabilities, runtime);
1625
+ const methods = buildStyleMethods(createChainableStyle);
1626
+ const instance = {
1627
+ get enabled() {
1628
+ const { modifiersEnabled, colorsEnabled } = resolveCapabilities();
1629
+ return modifiersEnabled || colorsEnabled;
1630
+ },
1631
+ get colorsEnabled() {
1632
+ return resolveCapabilities().colorsEnabled;
1633
+ },
1634
+ get trueColorEnabled() {
1635
+ return resolveCapabilities().trueColorEnabled;
1636
+ },
1637
+ get colorDepth() {
1638
+ return resolveCapabilities().colorDepth;
1639
+ },
1640
+ link(text, url, hyperlinkOptions) {
1641
+ if (resolveCapabilities().modifiersEnabled) return link$1(text, url, hyperlinkOptions);
1642
+ linkCode(url, hyperlinkOptions);
1643
+ return text;
1644
+ },
1645
+ ...Object.fromEntries(dynamicColorKinds.map(([kind, pairAtDepth, paint]) => [kind, (...args) => {
1646
+ const resolved = resolveCapabilities();
1647
+ if (args.length === 1) {
1648
+ pairAtDepth(args[0], "truecolor");
1649
+ return createChainableStyle([{
1650
+ kind,
1651
+ input: args[0]
1652
+ }]);
1653
+ }
1654
+ return paint(args[0], args[1], resolved.colorDepth);
1655
+ }])),
1656
+ ...methods
1657
+ };
1658
+ return Object.freeze(instance);
1659
+ }
1660
+ /**
1661
+ * Default style instance using `"auto"` mode.
1662
+ *
1663
+ * Emits color ANSI codes when stdout is a TTY and `NO_COLOR` is not set.
1664
+ * Non-color modifiers (bold, italic, etc.) follow TTY only; `FORCE_COLOR`,
1665
+ * when set, decides unconditionally for both.
1666
+ * Import this for convenient access without explicit configuration.
1667
+ *
1668
+ * @example
1669
+ * ```ts
1670
+ * import { style } from "@crustjs/style";
1671
+ *
1672
+ * console.log(style.bold("hello"));
1673
+ * console.log(style.red("error"));
1674
+ * ```
1675
+ */
1676
+ const style = createStyleInstance(void 0, true);
1677
+ style.black;
1678
+ style.red;
1679
+ const green = style.green;
1680
+ const yellow = style.yellow;
1681
+ style.blue;
1682
+ style.magenta;
1683
+ const cyan = style.cyan;
1684
+ style.white;
1685
+ style.gray;
1686
+ style.brightRed;
1687
+ style.brightGreen;
1688
+ style.brightYellow;
1689
+ style.brightBlue;
1690
+ style.brightMagenta;
1691
+ style.brightCyan;
1692
+ style.brightWhite;
1693
+ style.bgBlack;
1694
+ style.bgRed;
1695
+ style.bgGreen;
1696
+ style.bgYellow;
1697
+ style.bgBlue;
1698
+ style.bgMagenta;
1699
+ style.bgCyan;
1700
+ style.bgWhite;
1701
+ style.bgBrightBlack;
1702
+ style.bgBrightRed;
1703
+ style.bgBrightGreen;
1704
+ style.bgBrightYellow;
1705
+ style.bgBrightBlue;
1706
+ style.bgBrightMagenta;
1707
+ style.bgBrightCyan;
1708
+ style.bgBrightWhite;
1709
+ const bold = style.bold;
1710
+ const dim = style.dim;
1711
+ style.italic;
1712
+ style.underline;
1713
+ style.inverse;
1714
+ style.hidden;
1715
+ style.strikethrough;
1716
+ style.link;
1717
+ style.fg;
1718
+ style.bg;
1719
+ //#endregion
1720
+ //#region ../utils/dist/json.js
1721
+ /** Narrow a JSON document root to an object (not array/scalar/null). */
1722
+ function isJsonObject(value) {
1723
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1724
+ }
1725
+ //#endregion
1726
+ //#region ../utils/dist/path.js
1727
+ /** Returns whether child is parent itself or lies below it. */
1728
+ function isWithin(parent, child) {
1729
+ const path = relative(parent, child);
1730
+ return path === "" || !isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`);
1731
+ }
1732
+ //#endregion
1733
+ //#region ../utils/dist/error.js
1734
+ /** Narrows a caught Node.js system error to its stable errno contract. */
1735
+ function isErrnoException(error) {
1736
+ return error instanceof Error && "code" in error && typeof error.code === "string";
1737
+ }
1738
+ //#endregion
1739
+ //#region ../utils/dist/process.js
1740
+ const WINDOWS_SHELL_UNSAFE = /[\0\r\n"%!^`<>&|]/;
1741
+ const WINDOWS_SHELL_META = /([()\][%!^"`<>&|;, *?])/g;
1742
+ function escapeWindowsShellArgument(value) {
1743
+ let escaped = value.replace(/(?=(\\+?)?)\1"/g, "$1$1\\\"");
1744
+ escaped = escaped.replace(/(?=(\\+?)?)\1$/g, "$1$1");
1745
+ escaped = `"${escaped}"`.replace(WINDOWS_SHELL_META, "^$1");
1746
+ return escaped.replace(WINDOWS_SHELL_META, "^$1");
1747
+ }
1748
+ /** @internal Build Node's escaped Windows command-shim workaround. */
1749
+ function getWindowsShimCommand(command, args, shell, platform = process.platform) {
1750
+ if (shell || platform !== "win32" || !/\.(cmd|bat)$/i.test(command)) return null;
1751
+ const shimCommand = win32.normalize(command);
1752
+ for (const [index, value] of [shimCommand, ...args].entries()) if (WINDOWS_SHELL_UNSAFE.test(value)) {
1753
+ const label = index === 0 ? "command" : `argument ${index}`;
1754
+ throw new Error(`Windows command shim ${label} ${JSON.stringify(value)} contains unsafe shell characters`);
1755
+ }
1756
+ const commandLine = [shimCommand.replace(WINDOWS_SHELL_META, "^$1"), ...args.map(escapeWindowsShellArgument)].join(" ");
1757
+ return {
1758
+ command: process.env.ComSpec ?? "cmd.exe",
1759
+ args: [
1760
+ "/d",
1761
+ "/s",
1762
+ "/c",
1763
+ `"${commandLine}"`
1764
+ ],
1765
+ windowsVerbatimArguments: true
1766
+ };
1767
+ }
1768
+ /**
1769
+ * Spawn a process and wait for it to exit without imposing an error policy.
1770
+ *
1771
+ * On Windows, the automatic `.cmd`/`.bat` workaround escapes arguments for
1772
+ * `cmd.exe`; unsafe shell-expansion characters throw before spawning. Explicit
1773
+ * `shell: true` calls are passed through unchanged and own their shell escaping.
1774
+ */
1775
+ async function runProcess(command, args = [], options = {}) {
1776
+ const windowsShimCommand = getWindowsShimCommand(command, args, options.shell);
1777
+ const collect = (options.stdio ?? "collect") === "collect";
1778
+ const collectStdout = collect && options.stdout !== "ignore";
1779
+ const proc = spawn(windowsShimCommand?.command ?? command, windowsShimCommand?.args ?? args, {
1780
+ cwd: options.cwd,
1781
+ env: options.env,
1782
+ shell: options.shell,
1783
+ stdio: collect ? [
1784
+ "ignore",
1785
+ collectStdout ? "pipe" : "ignore",
1786
+ "pipe"
1787
+ ] : "inherit",
1788
+ windowsVerbatimArguments: windowsShimCommand?.windowsVerbatimArguments
1789
+ });
1790
+ const [stdout, stderr, [exitCode]] = await Promise.all([
1791
+ collectStdout ? text(proc.stdout) : "",
1792
+ collect ? text(proc.stderr) : "",
1793
+ once(proc, "close")
1794
+ ]);
1795
+ return {
1796
+ exitCode,
1797
+ stdout,
1798
+ stderr
1799
+ };
1800
+ }
1801
+ /** Resolve a bare executable name from PATH (and PATHEXT on Windows). */
1802
+ function which(command) {
1803
+ if (command.includes(sep) || command.includes("/")) {
1804
+ try {
1805
+ accessSync(command, constants.X_OK);
1806
+ if (statSync(command).isFile()) return command;
1807
+ } catch {}
1808
+ return null;
1809
+ }
1810
+ const extensions = process.platform === "win32" && !extname(command) ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
1811
+ for (const directory of process.env.PATH?.split(delimiter) ?? []) for (const extension of extensions) {
1812
+ const candidate = resolve(join(directory, command + extension));
1813
+ try {
1814
+ accessSync(candidate, constants.X_OK);
1815
+ if (statSync(candidate).isFile()) return candidate;
1816
+ } catch {}
1817
+ }
1818
+ return null;
1819
+ }
1820
+ //#endregion
1821
+ //#region src/utils/build-helpers.ts
1822
+ const BUILD_RUNTIMES = [
1823
+ "bun",
1824
+ "deno",
1825
+ "node"
1826
+ ];
1827
+ const BUN_TARGETS = {
1828
+ runtime: "Bun",
1829
+ targets: [
1830
+ "bun-linux-x64",
1831
+ "bun-linux-arm64",
1832
+ "bun-linux-x64-musl",
1833
+ "bun-linux-arm64-musl",
1834
+ "bun-darwin-x64",
1835
+ "bun-darwin-arm64",
1836
+ "bun-windows-x64",
1837
+ "bun-windows-arm64"
1838
+ ],
1839
+ info: {
1840
+ "bun-linux-x64": {
1841
+ alias: "linux-x64",
1842
+ platformKey: "linux-x64",
1843
+ os: "linux",
1844
+ cpu: "x64",
1845
+ libc: "glibc"
1846
+ },
1847
+ "bun-linux-arm64": {
1848
+ alias: "linux-arm64",
1849
+ platformKey: "linux-arm64",
1850
+ os: "linux",
1851
+ cpu: "arm64",
1852
+ libc: "glibc"
1853
+ },
1854
+ "bun-linux-x64-musl": {
1855
+ alias: "linux-x64-musl",
1856
+ platformKey: "linux-x64-musl",
1857
+ os: "linux",
1858
+ cpu: "x64",
1859
+ libc: "musl"
1860
+ },
1861
+ "bun-linux-arm64-musl": {
1862
+ alias: "linux-arm64-musl",
1863
+ platformKey: "linux-arm64-musl",
1864
+ os: "linux",
1865
+ cpu: "arm64",
1866
+ libc: "musl"
1867
+ },
1868
+ "bun-darwin-x64": {
1869
+ alias: "darwin-x64",
1870
+ platformKey: "darwin-x64",
1871
+ os: "darwin",
1872
+ cpu: "x64"
1873
+ },
1874
+ "bun-darwin-arm64": {
1875
+ alias: "darwin-arm64",
1876
+ platformKey: "darwin-arm64",
1877
+ os: "darwin",
1878
+ cpu: "arm64"
1879
+ },
1880
+ "bun-windows-x64": {
1881
+ alias: "windows-x64",
1882
+ platformKey: "win32-x64",
1883
+ os: "win32",
1884
+ cpu: "x64"
1885
+ },
1886
+ "bun-windows-arm64": {
1887
+ alias: "windows-arm64",
1888
+ platformKey: "win32-arm64",
1889
+ os: "win32",
1890
+ cpu: "arm64"
1891
+ }
1892
+ }
1893
+ };
1894
+ const DENO_TARGETS = {
1895
+ runtime: "Deno",
1896
+ targets: [
1897
+ "x86_64-unknown-linux-gnu",
1898
+ "aarch64-unknown-linux-gnu",
1899
+ "x86_64-apple-darwin",
1900
+ "aarch64-apple-darwin",
1901
+ "x86_64-pc-windows-msvc",
1902
+ "aarch64-pc-windows-msvc"
1903
+ ],
1904
+ info: {
1905
+ "x86_64-unknown-linux-gnu": {
1906
+ alias: "linux-x64",
1907
+ platformKey: "linux-x64",
1908
+ os: "linux",
1909
+ cpu: "x64",
1910
+ libc: "glibc"
1911
+ },
1912
+ "aarch64-unknown-linux-gnu": {
1913
+ alias: "linux-arm64",
1914
+ platformKey: "linux-arm64",
1915
+ os: "linux",
1916
+ cpu: "arm64",
1917
+ libc: "glibc"
1918
+ },
1919
+ "x86_64-apple-darwin": {
1920
+ alias: "darwin-x64",
1921
+ platformKey: "darwin-x64",
1922
+ os: "darwin",
1923
+ cpu: "x64"
1924
+ },
1925
+ "aarch64-apple-darwin": {
1926
+ alias: "darwin-arm64",
1927
+ platformKey: "darwin-arm64",
1928
+ os: "darwin",
1929
+ cpu: "arm64"
1930
+ },
1931
+ "x86_64-pc-windows-msvc": {
1932
+ alias: "windows-x64",
1933
+ platformKey: "win32-x64",
1934
+ os: "win32",
1935
+ cpu: "x64"
1936
+ },
1937
+ "aarch64-pc-windows-msvc": {
1938
+ alias: "windows-arm64",
1939
+ platformKey: "win32-arm64",
1940
+ os: "win32",
1941
+ cpu: "arm64"
1942
+ }
1943
+ }
1944
+ };
1945
+ /** `--target` value that stands for this machine's canonical target. */
1946
+ const HOST_TARGET = "host";
1947
+ /**
1948
+ * Canonical targets for `--target` inputs, deduplicated in input order. No
1949
+ * inputs means every target of the table; `host` means this machine's target.
1950
+ */
1951
+ function resolveTargets(table, targetFlags) {
1952
+ if (!targetFlags?.length) return [...table.targets];
1953
+ const targets = targetFlags.map((input) => {
1954
+ if (input === "host") {
1955
+ const host = hostTarget(table);
1956
+ if (host === null) throw new Error(`No ${table.runtime} target matches this machine (${hostPlatformKey()}).\n Valid targets: ${table.targets.join(", ")}`);
1957
+ return host;
1958
+ }
1959
+ const exact = table.targets.find((target) => target === input);
1960
+ if (exact) return exact;
1961
+ const canonical = table.targets.find((target) => table.info[target].alias === input);
1962
+ const hint = canonical ? ` Did you mean "${canonical}"?` : "";
1963
+ const runtime = table.runtime === "Bun" ? "" : `${table.runtime} `;
1964
+ throw new Error(`Unknown ${runtime}target "${input}". Targets must use canonical ${table.runtime} names.${hint}\n Valid targets: ${table.targets.join(", ")}`);
1965
+ });
1966
+ return [...new Set(targets)];
1967
+ }
1968
+ /** True on musl-based Linux (Alpine, Void, …). Mirrors the check in Bun's own npm installer. */
1969
+ function isMuslHost() {
1970
+ if (process.platform !== "linux") return false;
1971
+ try {
1972
+ const report = process.report?.getReport();
1973
+ if (report?.header) return report.header.glibcVersionRuntime === void 0;
1974
+ } catch {}
1975
+ return existsSync("/etc/alpine-release");
1976
+ }
1977
+ function hostPlatformKey() {
1978
+ return `${process.platform}-${process.arch}${isMuslHost() ? "-musl" : ""}`;
1979
+ }
1980
+ function hostTarget(table) {
1981
+ const platformKey = hostPlatformKey();
1982
+ return table.targets.find((target) => table.info[target].platformKey === platformKey) ?? null;
1983
+ }
1984
+ function readUserPackageJson(cwd) {
1985
+ const packageJsonPath = join(cwd, "package.json");
1986
+ if (!existsSync(packageJsonPath)) return void 0;
1987
+ try {
1988
+ return JSON.parse(readFileSync(packageJsonPath, "utf8"));
1989
+ } catch (error) {
1990
+ throw new Error(`Failed to parse package.json in ${cwd}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1991
+ }
1992
+ }
1993
+ function toBunEnvFileArgs(envFiles) {
1994
+ return envFiles.flatMap((envFile) => ["--env-file", envFile]);
1995
+ }
1996
+ /**
1997
+ * Resolve the safest executable to run `bun build`.
1998
+ *
1999
+ * Prefer the real Bun binary when it is available on PATH, because invoking
2000
+ * standalone compilation from inside a compiled Crust executable can trigger
2001
+ * Bun runtime bugs on some host/target combinations.
2002
+ *
2003
+ * Fall back to the current executable with `BUN_BE_BUN=1` so packaged Crust
2004
+ * binaries still work in environments without a separate Bun install. Only a
2005
+ * Bun process can stand in for bun: the library `build()` may run under Node,
2006
+ * where the fallback would spawn node with Bun flags, so that case is an error.
2007
+ */
2008
+ function resolveBunBuildRunner(runningUnderBun = process.versions.bun !== void 0) {
2009
+ const bunPath = which("bun");
2010
+ if (bunPath) return {
2011
+ command: bunPath,
2012
+ env: { ...process.env }
2013
+ };
2014
+ if (!runningUnderBun) throw new Error("bun was not found on PATH.\n crust build compiles with the Bun bundler and prepares Command Snapshots with bun; install Bun (https://bun.sh) or run the build under bun.");
2015
+ return {
2016
+ command: process.execPath,
2017
+ env: {
2018
+ ...process.env,
2019
+ BUN_BE_BUN: "1"
2020
+ }
2021
+ };
2022
+ }
2023
+ /**
2024
+ * Bun's `-baseline` spelling of an x64 target, or null for arm64.
2025
+ *
2026
+ * Bun 1.4 ships one x64 build, so the alias yields the same executable (Bun
2027
+ * 1.4.2 downloads a `bun-<target>-baseline-v1.4.2` base that is byte-identical
2028
+ * to the plain one; checked for linux-x64-musl and darwin-x64), but `baseline:
2029
+ * true` is never Bun's compiled-in default, so it is never the self-copy target.
2030
+ * arm64 spellings with the suffix resolve to the plain aarch64 base instead.
2031
+ */
2032
+ function bunBaselineAlias(target) {
2033
+ return BUN_TARGETS.info[target].cpu === "x64" ? `${target}-baseline` : null;
2034
+ }
2035
+ /**
2036
+ * Target string passed to Bun: the `-baseline` alias when the `BUN_BE_BUN`
2037
+ * fallback runner would otherwise compile `target` by copying itself.
2038
+ */
2039
+ function bunCompileTarget(target, runner, host = hostTarget(BUN_TARGETS)) {
2040
+ if (target !== host || runner.env.BUN_BE_BUN !== "1") return target;
2041
+ return bunBaselineAlias(target) ?? target;
2042
+ }
2043
+ /**
2044
+ * Refuse the host target when only the `BUN_BE_BUN` fallback runner is
2045
+ * available and no `-baseline` alias can stand in for it (arm64 hosts).
2046
+ */
2047
+ function assertTargetsBuildableWithoutBun(targets, host = hostTarget(BUN_TARGETS)) {
2048
+ if (host === null || !targets.includes(host) || bunBaselineAlias(host) !== null || which("bun") !== null) return;
2049
+ const others = targets.filter((target) => target !== host);
2050
+ const alternative = others.length > 0 ? `pass --target with the other targets (e.g. ${others.map((target) => `--target ${target}`).join(" ")})` : "build a different target";
2051
+ throw new Error(`Cannot build ${host} without a separate bun executable on PATH.\n Bun reuses the running crust executable as the base for its own platform, which yields a binary that crashes on start.
2052
+ Install Bun (https://bun.sh), or ${alternative}.`);
2053
+ }
2054
+ /**
2055
+ * Module source the generated driver imports for a `crust.bunPlugins` specifier.
2056
+ *
2057
+ * Paths become file URLs resolved against the project; bare specifiers are
2058
+ * imported as written so they resolve from the project's own node_modules.
2059
+ */
2060
+ function resolveBunPluginSource(specifier, cwd) {
2061
+ return specifier.startsWith(".") || isAbsolute(specifier) ? pathToFileURL(resolve(cwd, specifier)).href : specifier;
2062
+ }
2063
+ /**
2064
+ * Marks every Bun/Node bundle crust produces so `resolveArtifactDir` (core)
2065
+ * can tell a staged bundle from source at runtime. Not a `PUBLIC_*` variable:
2066
+ * it is internal to crust, never user-set. Deno compile has no define, but
2067
+ * standalone Deno binaries are detected directly.
2068
+ */
2069
+ const CRUST_BUILD_DEFINE = { "process.env.CRUST_INTERNAL_BUILD": "\"1\"" };
2070
+ const CRUST_BUILD_DEFINE_ARG = "process.env.CRUST_INTERNAL_BUILD=\"1\"";
2071
+ /**
2072
+ * Script that runs `Bun.build` with the project's bundler plugins.
2073
+ *
2074
+ * `bun build` has no plugin flag, so plugins are loaded by a generated script
2075
+ * placed in the project root (project module resolution) and run with the
2076
+ * same runner as the CLI path. Every value is embedded via JSON.stringify.
2077
+ */
2078
+ function createBunPluginDriverScript(options) {
2079
+ return `// Generated by crust build for crust.bunPlugins; deleted when the build finishes.
2080
+ const options = ${JSON.stringify(options)};
2081
+ const plugins = [];
2082
+ for (const { specifier, source } of options.plugins) {
2083
+ let plugin;
2084
+ try {
2085
+ plugin = (await import(source)).default;
2086
+ } catch (error) {
2087
+ // Bun names this (soon deleted) driver as the importer; the project root is the useful location.
2088
+ const message = (error instanceof Error ? error.message : String(error)).replace(\` imported from \${import.meta.path}\`, "");
2089
+ console.error(\`crust.bunPlugins entry \${specifier} could not be imported from \${process.cwd()}: \${message}\`);
2090
+ process.exit(1);
2091
+ }
2092
+ if (typeof plugin !== "object" || plugin === null || typeof plugin.name !== "string" || typeof plugin.setup !== "function") {
2093
+ console.error(\`crust.bunPlugins entry \${specifier} must default-export a Bun bundler plugin ({ name, setup }). Wrap a plugin factory in a module that default-exports the created plugin.\`);
2094
+ process.exit(1);
2095
+ }
2096
+ plugins.push(plugin);
2097
+ }
2098
+ const result = await Bun.build({ ...options.build, define: ${JSON.stringify(CRUST_BUILD_DEFINE)}, plugins, throw: false });
2099
+ if (!result.success) {
2100
+ for (const log of result.logs) console.error(log);
2101
+ process.exit(1);
2102
+ }
2103
+ if (options.build.target === "node") {
2104
+ if (result.outputs.length !== 1) {
2105
+ // Same refusal as \`bun build --outfile\`: a file-type asset import yields entry + asset.
2106
+ console.error("error: cannot write multiple output files without an output directory");
2107
+ process.exit(1);
2108
+ }
2109
+ await Bun.write(options.outfile, result.outputs[0]);
2110
+ }
2111
+ `;
2112
+ }
2113
+ async function runBunPluginDriver(build, outfilePath, bunPlugins, envFiles, cwd) {
2114
+ const driverPath = join(cwd, `.crust-build-${randomBytes(6).toString("hex")}.ts`);
2115
+ await writeFile(driverPath, createBunPluginDriverScript({
2116
+ plugins: bunPlugins.map((specifier) => ({
2117
+ specifier,
2118
+ source: resolveBunPluginSource(specifier, cwd)
2119
+ })),
2120
+ build,
2121
+ outfile: outfilePath
2122
+ }));
2123
+ try {
2124
+ await runBuildProcess(resolveBunBuildRunner(), [...toBunEnvFileArgs(envFiles), driverPath], outfilePath, cwd);
2125
+ } finally {
2126
+ await rm(driverPath, { force: true });
2127
+ }
2128
+ }
2129
+ /**
2130
+ * Compile a single entry file to a standalone executable.
2131
+ *
2132
+ * Uses `bun build --compile` as a subprocess so the standalone compiler runs
2133
+ * in Bun's CLI process rather than inside the current Crust runtime.
2134
+ * This avoids in-process compiler issues seen on some host/target
2135
+ * combinations while still supporting env-file loading natively.
2136
+ *
2137
+ * @param entryPath - Absolute path to the entry file
2138
+ * @param outfilePath - Absolute path to the output binary
2139
+ * @param minify - Whether to enable minification
2140
+ * @param target - Bun compile target
2141
+ * @param envFiles - Optional env files to load during build
2142
+ * @param bunPlugins - Bun bundler plugin specifiers; when present the build
2143
+ * runs through the generated `Bun.build` driver instead of `bun build`
2144
+ * @throws {Error} If the build fails
2145
+ */
2146
+ async function execBuild(entryPath, outfilePath, minify, target, envFiles, cwd, bunPlugins = []) {
2147
+ const runner = resolveBunBuildRunner();
2148
+ const compileTarget = bunCompileTarget(target, runner);
2149
+ if (bunPlugins.length > 0) {
2150
+ await runBunPluginDriver({
2151
+ entrypoints: [entryPath],
2152
+ minify,
2153
+ env: "PUBLIC_*",
2154
+ target: "bun",
2155
+ compile: {
2156
+ target: compileTarget,
2157
+ outfile: outfilePath,
2158
+ autoloadBunfig: false
2159
+ }
2160
+ }, outfilePath, bunPlugins, envFiles, cwd);
2161
+ return;
2162
+ }
2163
+ await runBuildProcess(runner, createBunCompileArgs(entryPath, outfilePath, minify, compileTarget, envFiles), outfilePath, cwd);
2164
+ }
2165
+ function createBunCompileArgs(entryPath, outfilePath, minify, target, envFiles = []) {
2166
+ return [
2167
+ "build",
2168
+ "--compile",
2169
+ "--no-compile-autoload-bunfig",
2170
+ ...toBunEnvFileArgs(envFiles),
2171
+ "--env=PUBLIC_*",
2172
+ "--define",
2173
+ CRUST_BUILD_DEFINE_ARG,
2174
+ "--outfile",
2175
+ outfilePath,
2176
+ ...minify ? ["--minify"] : [],
2177
+ "--target",
2178
+ target,
2179
+ entryPath
2180
+ ];
2181
+ }
2182
+ function createDenoCompileArgs(entryPath, outfilePath, target) {
2183
+ return [
2184
+ "compile",
2185
+ "-A",
2186
+ "--output",
2187
+ outfilePath,
2188
+ "--target",
2189
+ target,
2190
+ entryPath
2191
+ ];
2192
+ }
2193
+ async function runBuildProcess(runner, args, outfilePath, cwd) {
2194
+ const { exitCode, stdout, stderr } = await runProcess(runner.command, args, {
2195
+ env: runner.env,
2196
+ cwd,
2197
+ stdio: "collect"
2198
+ });
2199
+ if (exitCode !== 0) {
2200
+ const output = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
2201
+ throw new Error(`Build failed for ${outfilePath}${output ? `:\n${output}` : ""}`);
2202
+ }
2203
+ }
2204
+ async function execNodeBuild(entryPath, outfilePath, minify, envFiles, cwd, bunPlugins = []) {
2205
+ if (bunPlugins.length > 0) await runBunPluginDriver({
2206
+ entrypoints: [entryPath],
2207
+ minify,
2208
+ env: "PUBLIC_*",
2209
+ target: "node",
2210
+ format: "esm"
2211
+ }, outfilePath, bunPlugins, envFiles, cwd);
2212
+ else await runBuildProcess(resolveBunBuildRunner(), [
2213
+ "build",
2214
+ ...toBunEnvFileArgs(envFiles),
2215
+ "--env=PUBLIC_*",
2216
+ "--define",
2217
+ CRUST_BUILD_DEFINE_ARG,
2218
+ "--target",
2219
+ "node",
2220
+ "--format",
2221
+ "esm",
2222
+ "--outfile",
2223
+ outfilePath,
2224
+ ...minify ? ["--minify"] : [],
2225
+ entryPath
2226
+ ], outfilePath, cwd);
2227
+ const output = await readFile(outfilePath, "utf8");
2228
+ await writeFile(outfilePath, "#!/usr/bin/env node\n" + output.replace(/^#![^\n]*(?:\n|$)/, ""));
2229
+ if (process.platform !== "win32") await chmod(outfilePath, 493);
2230
+ }
2231
+ async function execDenoBuild(entryPath, outfilePath, target, cwd) {
2232
+ const denoPath = which("deno");
2233
+ if (!denoPath) throw new Error("Deno is required for the deno runtime but was not found on PATH.\n Install Deno from https://deno.com/ and try again.");
2234
+ await runBuildProcess({
2235
+ command: denoPath,
2236
+ env: { ...process.env }
2237
+ }, createDenoCompileArgs(entryPath, outfilePath, target), outfilePath, cwd);
2238
+ }
2239
+ /**
2240
+ * Prepare a CLI entry's Command Snapshot in the user's project context.
2241
+ *
2242
+ * The entry runs as a subprocess with `CRUST_INTERNAL_SNAPSHOT_PATH` pointing
2243
+ * to a temporary file. `.execute()` validates and writes the command graph and
2244
+ * adjacent Build Report, then exits before any following entrypoint code can run.
2245
+ *
2246
+ * Runs with the same bun as compilation (`resolveBunBuildRunner`): bun on
2247
+ * PATH, or a compiled standalone crust executable as `BUN_BE_BUN=1`, so
2248
+ * arbitrary `.ts` entries run without a separate `bun` install.
2249
+ */
2250
+ const SNAPSHOT_TIMEOUT_MS = 3e4;
2251
+ function isBuildReport(value) {
2252
+ return isJsonObject(value) && Array.isArray(value.extensions) && value.extensions.every((extension) => isJsonObject(extension) && typeof extension.id === "string" && Array.isArray(extension.files) && extension.files.every((file) => typeof file === "string"));
2253
+ }
2254
+ async function buildEntrypoint(entryPath, outDir, envFiles, io, cwd) {
2255
+ const absoluteEntry = resolve(entryPath);
2256
+ const snapshotDir = await mkdtemp(join(tmpdir(), "crust-snapshot-"));
2257
+ const snapshotPath = join(snapshotDir, "command.json");
2258
+ const buildReportPath = join(snapshotDir, "build-report.json");
2259
+ try {
2260
+ const runner = resolveBunBuildRunner();
2261
+ const spawnedAt = Date.now();
2262
+ const proc = spawn(runner.command, [...toBunEnvFileArgs(envFiles), absoluteEntry], {
2263
+ env: {
2264
+ ...runner.env,
2265
+ [SNAPSHOT_PATH_ENV]: snapshotPath,
2266
+ [BUILD_OUT_DIR_ENV]: resolve(outDir)
2267
+ },
2268
+ cwd,
2269
+ stdio: [
2270
+ "ignore",
2271
+ "ignore",
2272
+ "pipe"
2273
+ ],
2274
+ timeout: SNAPSHOT_TIMEOUT_MS
2275
+ });
2276
+ const stderrPromise = text(proc.stderr);
2277
+ const [exitCode] = await once(proc, "close");
2278
+ const stderr = (await stderrPromise).trim();
2279
+ if (proc.signalCode !== null) {
2280
+ if (Date.now() - spawnedAt >= SNAPSHOT_TIMEOUT_MS) throw new Error(`Command Snapshot preparation timed out after ${SNAPSHOT_TIMEOUT_MS / 1e3}s.\n An Extension build hook may be hanging. Use --no-validate to skip entry preparation and build hooks.`);
2281
+ throw new Error(`Command Snapshot preparation was killed by ${proc.signalCode}.${stderr ? `\n${stderr}` : ""}`);
2282
+ }
2283
+ if (exitCode !== 0) throw new Error(stderr || "Command Snapshot preparation failed");
2284
+ if (stderr) {
2285
+ const styled = stderr.split("\n").map((line) => line.startsWith("Warning:") ? `${yellow("Warning:")}${line.slice(8)}` : line).join("\n");
2286
+ io.stderr(styled);
2287
+ }
2288
+ let serialized;
2289
+ try {
2290
+ serialized = await readFile(snapshotPath, "utf8");
2291
+ } catch (error) {
2292
+ if (isErrnoException(error) && error.code === "ENOENT") throw new Error(`Entry exited without producing a Command Snapshot.\n Ensure ${absoluteEntry} calls await app.execute() and uses a compatible @crustjs/core version.`, { cause: error });
2293
+ throw error;
2294
+ }
2295
+ let snapshot;
2296
+ try {
2297
+ snapshot = JSON.parse(serialized);
2298
+ } catch (error) {
2299
+ throw new Error(`Entry produced an invalid Command Snapshot.\n Ensure ${absoluteEntry} uses a compatible @crustjs/core version.`, { cause: error });
2300
+ }
2301
+ let serializedBuild;
2302
+ try {
2303
+ serializedBuild = await readFile(buildReportPath, "utf8");
2304
+ } catch (error) {
2305
+ if (isErrnoException(error) && error.code === "ENOENT") throw new Error(`Entry produced a Command Snapshot without a Build Report.\n Ensure ${absoluteEntry} uses a compatible @crustjs/core version.`, { cause: error });
2306
+ throw error;
2307
+ }
2308
+ try {
2309
+ const build = JSON.parse(serializedBuild);
2310
+ if (!isBuildReport(build)) throw new Error("Expected extensions with string ids and files arrays.");
2311
+ for (const extension of build.extensions) {
2312
+ defineExtensionId(extension.id);
2313
+ for (const file of extension.files) {
2314
+ const path = resolve(outDir, file);
2315
+ if (file === "." || file.includes("\\") || posix.normalize(file) !== file || win32.isAbsolute(file) || /^[A-Za-z]:/.test(file) || !isWithin(resolve(outDir), path) || !isWithin(realpathSync(outDir), realpathSync(path)) || !lstatSync(path).isFile()) throw new Error(`Reported artifact must be a normalized, contained regular file: ${file}`);
2316
+ }
2317
+ }
2318
+ return {
2319
+ snapshot,
2320
+ build
2321
+ };
2322
+ } catch (error) {
2323
+ throw new Error(`Entry produced an invalid Build Report.\n Ensure ${absoluteEntry} uses a compatible @crustjs/core version; upgrade Core, @crustjs/crust, and build-hook Extensions together to the pure-return build API.`, { cause: error });
2324
+ }
2325
+ } finally {
2326
+ await rm(snapshotDir, {
2327
+ recursive: true,
2328
+ force: true,
2329
+ maxRetries: 3,
2330
+ retryDelay: 100
2331
+ });
2332
+ }
2333
+ }
2334
+ //#endregion
2335
+ //#region src/utils/distribute.ts
2336
+ /** Project-relative directory that `crust build` owns: wiped per build, read by `crust publish`. */
2337
+ const CRUST_DIR = ".crust";
2338
+ const MAX_PACKAGE_NAME_LENGTH = 214;
2339
+ const METADATA_KEYS = [
2340
+ "description",
2341
+ "license",
2342
+ "author",
2343
+ "homepage",
2344
+ "bugs",
2345
+ "repository",
2346
+ "keywords",
2347
+ "publishConfig",
2348
+ "funding",
2349
+ "engines"
2350
+ ];
2351
+ function readPackageJson(cwd, packageJson) {
2352
+ if (packageJson === void 0) throw new Error(`package.json not found in ${cwd}\n crust build requires a package.json with name and version fields.`);
2353
+ if (!isJsonObject(packageJson)) throw new Error(`package.json in ${cwd} must contain a JSON object.`);
2354
+ validatePackageIdentity(packageJson, "package.json");
2355
+ return packageJson;
2356
+ }
2357
+ /** Only identity is interpreted here; this is not a complete npm schema validator. */
2358
+ function validatePackageIdentity(value, source) {
2359
+ if (value === void 0 || !isJsonObject(value)) throw new Error(`${source} must contain a JSON object.`);
2360
+ if (typeof value.name !== "string" || value.name.trim() === "") throw new Error(`${source} name field must be a non-empty string.`);
2361
+ if (typeof value.version !== "string" || value.version.trim() === "") throw new Error(`${source} version field must be a non-empty string.`);
2362
+ }
2363
+ function isString$1(value) {
2364
+ return typeof value === "string";
2365
+ }
2366
+ function derivePlatformPackageName(rootPackageName, targetAlias) {
2367
+ const [scope, name] = rootPackageName.startsWith("@") ? rootPackageName.split("/") : [void 0, rootPackageName];
2368
+ const suffixedName = `${name}-${targetAlias}`;
2369
+ return scope ? `${scope}/${suffixedName}` : suffixedName;
2370
+ }
2371
+ function getPackagePathSegment(packageName) {
2372
+ return packageName.startsWith("@") ? packageName.split("/")[1] ?? packageName : packageName;
2373
+ }
2374
+ /** Platform binary filename: `<command>-<target>`, `.exe` on Windows. */
2375
+ function binaryFilename(command, target) {
2376
+ return `${command}-${target.target}${target.os === "win32" ? ".exe" : ""}`;
2377
+ }
2378
+ function platformBinMap(commands, target) {
2379
+ return Object.fromEntries(commands.map((command) => [command, `bin/${binaryFilename(command, target)}`]));
2380
+ }
2381
+ function buildDistributionRootPackageJson(metadata, commands, targets, { artifactDirs, manPages }) {
2382
+ return {
2383
+ ...metadata.rootPackageJson,
2384
+ name: metadata.rootPackageName,
2385
+ version: metadata.version,
2386
+ type: "module",
2387
+ files: ["bin", ...artifactDirs],
2388
+ bin: Object.fromEntries(commands.map((command) => [command, `bin/${command}.js`])),
2389
+ ...targets.length > 0 ? { optionalDependencies: Object.fromEntries(targets.map((target) => [target.packageName, metadata.version])) } : {},
2390
+ ...manPages.length > 0 ? { man: manPages.map((page) => `./man/${page}`) } : {},
2391
+ exports: metadata.exports,
2392
+ peerDependencies: metadata.peerDependencies,
2393
+ peerDependenciesMeta: metadata.peerDependenciesMeta
2394
+ };
2395
+ }
2396
+ /**
2397
+ * Node rejects export targets whose segments are `.`, `..`, `node_modules`, or
2398
+ * empty (`ERR_INVALID_PACKAGE_TARGET`), also percent-encoded, even when the path
2399
+ * normalizes to a staged file, so filesystem checks alone would pass a target
2400
+ * consumers cannot import.
2401
+ */
2402
+ function hasNodeInvalidSegment(target) {
2403
+ return target.slice(2).split(/[\\/]/).some((segment) => {
2404
+ let decoded = segment;
2405
+ try {
2406
+ decoded = decodeURIComponent(segment);
2407
+ } catch {}
2408
+ return /^(\.\.?|node_modules|)$/i.test(decoded);
2409
+ });
2410
+ }
2411
+ /**
2412
+ * Checks a package.json `exports` map against the staged root package: every
2413
+ * target must be a `./`-relative path to a file that staging copied there (a
2414
+ * `crust.include` directory or Extension artifact), so the published root
2415
+ * package resolves exactly what the project's own `exports` promises. `null`
2416
+ * targets (blocked subpaths) and nested condition objects are allowed;
2417
+ * fallback arrays and `*` patterns are rejected rather than half-checked.
2418
+ */
2419
+ function validateStagedExports(exports, rootDir, sourceType) {
2420
+ const fail = (detail) => {
2421
+ throw new Error(`package.json exports ${detail}\n crust build stages only bin/, Extension artifacts, and crust.include directories into the root package; point exports at a crust.include directory or remove the field.`);
2422
+ };
2423
+ const checkTarget = (target, at) => {
2424
+ if (target === null) return;
2425
+ if (isString$1(target)) {
2426
+ const staged = resolve(rootDir, target);
2427
+ if (!target.startsWith("./") || !isWithin(rootDir, staged)) fail(`target ${JSON.stringify(target)} (${at}) must be a ./-relative path inside the package.`);
2428
+ if (target.includes("*")) fail(`target ${JSON.stringify(target)} (${at}) uses a pattern, which crust build does not support.`);
2429
+ if (hasNodeInvalidSegment(target)) fail(`target ${JSON.stringify(target)} (${at}) contains a path segment Node rejects (".", "..", "node_modules", or empty).`);
2430
+ if (!existsSync(staged) || !statSync(staged).isFile()) fail(`target ${JSON.stringify(target)} (${at}) is not a staged file: ${staged}`);
2431
+ if (sourceType !== "module" && (target.endsWith(".js") || target.endsWith(".d.ts"))) {
2432
+ let scope = dirname(staged);
2433
+ while (scope !== rootDir && !existsSync(join(scope, "package.json"))) scope = dirname(scope);
2434
+ if (scope === rootDir) fail(`target ${JSON.stringify(target)} (${at}) would change module format under the staged root's type: "module". Use .cjs/.d.cts for CommonJS, or include a nested package.json declaring the library's type.`);
2435
+ }
2436
+ return;
2437
+ }
2438
+ if (!isJsonObject(target)) fail(`${at} must be a path, null, or a conditions object, not ${JSON.stringify(target)}.`);
2439
+ for (const [condition, value] of Object.entries(target)) {
2440
+ if (condition.startsWith(".")) fail(`${at} mixes subpath ${JSON.stringify(condition)} into a conditions object.`);
2441
+ checkTarget(value, `${at} -> ${condition}`);
2442
+ }
2443
+ };
2444
+ if (isJsonObject(exports) && Object.keys(exports).some((key) => key.startsWith("."))) {
2445
+ for (const [subpath, target] of Object.entries(exports)) {
2446
+ if (!subpath.startsWith(".")) fail(`mixes condition ${JSON.stringify(subpath)} with subpath keys.`);
2447
+ checkTarget(target, subpath);
2448
+ }
2449
+ return;
2450
+ }
2451
+ checkTarget(exports, "\".\"");
2452
+ }
2453
+ /**
2454
+ * Carries `peerDependencies` (and `peerDependenciesMeta`) into the root package
2455
+ * so a library `exports` entry can declare what its published types import.
2456
+ * Ranges must be publishable as written: the staged manifests go to npm
2457
+ * directly, so `workspace:` and `catalog:` ranges would leak into the registry.
2458
+ */
2459
+ function validatePeerDependencies(peerDependencies, peerDependenciesMeta) {
2460
+ if (!isJsonObject(peerDependencies)) throw new Error("package.json peerDependencies must be an object of package names to ranges.");
2461
+ const ranges = {};
2462
+ for (const [name, range] of Object.entries(peerDependencies)) {
2463
+ if (!isString$1(range) || /^(workspace|catalog):/.test(range)) throw new Error(`package.json peerDependencies[${JSON.stringify(name)}] must be a publishable range, not ${JSON.stringify(range)}.\n crust build publishes the staged root package as written; workspace: and catalog: ranges are never rewritten.`);
2464
+ ranges[name] = range;
2465
+ }
2466
+ if (peerDependenciesMeta === void 0) return { peerDependencies: ranges };
2467
+ if (!isJsonObject(peerDependenciesMeta)) throw new Error("package.json peerDependenciesMeta must be an object keyed by peer name.");
2468
+ for (const name of Object.keys(peerDependenciesMeta)) if (!Object.hasOwn(ranges, name)) throw new Error(`package.json peerDependenciesMeta[${JSON.stringify(name)}] has no matching peerDependencies entry.`);
2469
+ return {
2470
+ peerDependencies: ranges,
2471
+ peerDependenciesMeta
2472
+ };
2473
+ }
2474
+ function buildDistributionPlatformPackageJson(metadata, commands, target) {
2475
+ return {
2476
+ ...metadata.rootPackageJson,
2477
+ name: target.packageName,
2478
+ version: metadata.version,
2479
+ files: ["bin"],
2480
+ bin: platformBinMap(commands, target),
2481
+ os: [target.os],
2482
+ cpu: [target.cpu],
2483
+ ...target.libc ? { libc: [target.libc] } : {}
2484
+ };
2485
+ }
2486
+ function pickRootMetadata(pkgJson) {
2487
+ const metadata = {
2488
+ name: pkgJson.name,
2489
+ version: pkgJson.version
2490
+ };
2491
+ for (const key of METADATA_KEYS) {
2492
+ const value = pkgJson[key];
2493
+ if (value !== void 0) Object.assign(metadata, { [key]: value });
2494
+ }
2495
+ return metadata;
2496
+ }
2497
+ function validatePackageNameLength(packageName) {
2498
+ if (packageName.length > MAX_PACKAGE_NAME_LENGTH) throw new Error(`Generated package name is too long for npm: ${packageName}\n Keep package names at or below ${MAX_PACKAGE_NAME_LENGTH} characters after the platform suffix is added.`);
2499
+ }
2500
+ function resolveDistributionMetadata(cwd, userPackageJson) {
2501
+ const pkgJson = readPackageJson(cwd, userPackageJson);
2502
+ validatePackageNameLength(pkgJson.name);
2503
+ return {
2504
+ rootPackageName: pkgJson.name,
2505
+ version: pkgJson.version,
2506
+ rootPackageJson: pickRootMetadata(pkgJson),
2507
+ sourceType: pkgJson.type,
2508
+ ...pkgJson.exports !== void 0 ? { exports: pkgJson.exports } : {},
2509
+ ...pkgJson.peerDependencies !== void 0 ? validatePeerDependencies(pkgJson.peerDependencies, pkgJson.peerDependenciesMeta) : {}
2510
+ };
2511
+ }
2512
+ function resolveDistributionTarget(table, stageDir, rootPackageName, target) {
2513
+ const info = table.info[target];
2514
+ const packageName = derivePlatformPackageName(rootPackageName, info.alias);
2515
+ validatePackageNameLength(packageName);
2516
+ return {
2517
+ target,
2518
+ platformKey: info.platformKey,
2519
+ targetAlias: info.alias,
2520
+ packageName,
2521
+ packagePathSegment: getPackagePathSegment(packageName),
2522
+ packageDir: resolve(stageDir, info.alias),
2523
+ os: info.os,
2524
+ cpu: info.cpu,
2525
+ ...info.libc ? { libc: info.libc } : {}
2526
+ };
2527
+ }
2528
+ function generateDistributionJsResolver(command, targets) {
2529
+ const targetMap = Object.fromEntries(targets.map((target) => [target.platformKey, {
2530
+ packagePathSegment: target.packagePathSegment,
2531
+ packageName: target.packageName,
2532
+ targetAlias: target.targetAlias,
2533
+ binaryFilename: binaryFilename(command, target)
2534
+ }]));
2535
+ const supportedPlatforms = targets.map((target) => target.targetAlias).join(", ");
2536
+ return `#!/usr/bin/env node
2537
+ // Auto-generated by crust build -- do not edit
2538
+ import { spawn } from "node:child_process";
2539
+ import { chmodSync, existsSync } from "node:fs";
2540
+ import { dirname, resolve } from "node:path";
2541
+ import process from "node:process";
2542
+ import { fileURLToPath } from "node:url";
2543
+
2544
+ const NAME = ${JSON.stringify(command)};
2545
+ const PLATFORMS = ${JSON.stringify(targetMap, null, " ")};
2546
+ const dir = dirname(fileURLToPath(import.meta.url));
2547
+
2548
+ // Same check as Bun's own npm installer: glibc reports its version, musl does not.
2549
+ function isMusl() {
2550
+ try {
2551
+ const report = process.report?.getReport();
2552
+ if (report?.header) return report.header.glibcVersionRuntime === undefined;
2553
+ } catch {}
2554
+ return existsSync("/etc/alpine-release");
2555
+ }
2556
+
2557
+ const platformKey =
2558
+ \`\${process.platform}-\${process.arch}\` + (process.platform === "linux" && isMusl() ? "-musl" : "");
2559
+ const target = PLATFORMS[platformKey];
2560
+
2561
+ if (!target) {
2562
+ \tconsole.error("[" + NAME + "] Unsupported platform: " + platformKey);
2563
+ \tconsole.error("[" + NAME + "] Supported platforms: ${supportedPlatforms}");
2564
+ \tprocess.exit(1);
2565
+ }
2566
+
2567
+ const candidates = [
2568
+ \t// Hoisted install: the platform package beside this one in node_modules.
2569
+ \tresolve(dir, "..", "..", target.packagePathSegment, "bin", target.binaryFilename),
2570
+ \t// Nested install: the platform package under this package's node_modules.
2571
+ \tresolve(dir, "..", "node_modules", target.packageName, "bin", target.binaryFilename),
2572
+ \t// In place: the .crust/ tree crust build staged, before any install.
2573
+ \tresolve(dir, "..", "..", target.targetAlias, "bin", target.binaryFilename),
2574
+ ];
2575
+ const binPath = candidates.find((candidate) => existsSync(candidate));
2576
+
2577
+ if (!binPath) {
2578
+ \tconsole.error("[" + NAME + "] Missing platform package for " + platformKey);
2579
+ \tconsole.error("[" + NAME + "] Tried:");
2580
+ \tfor (const candidate of candidates) console.error(" " + candidate);
2581
+ \tconsole.error(
2582
+ \t\t"[" + NAME + "] Reinstall dependencies on this platform and ensure optional dependencies are enabled.",
2583
+ \t);
2584
+ \tprocess.exit(1);
2585
+ }
2586
+
2587
+ if (process.platform !== "win32") {
2588
+ \ttry {
2589
+ \t\tchmodSync(binPath, 0o755);
2590
+ \t} catch {
2591
+ \t\t// Ignore permission adjustment failures and let spawn surface real errors.
2592
+ \t}
2593
+ }
2594
+
2595
+ const child = spawn(binPath, process.argv.slice(2), {
2596
+ \tstdio: "inherit",
2597
+ });
2598
+
2599
+ child.on("error", (error) => {
2600
+ \tconsole.error("[" + NAME + "] Failed to launch binary: " + error.message);
2601
+ \tprocess.exit(1);
2602
+ });
2603
+
2604
+ child.on("exit", (code, signal) => {
2605
+ \tif (signal) {
2606
+ \t\ttry {
2607
+ \t\t\tprocess.kill(process.pid, signal);
2608
+ \t\t} catch {
2609
+ \t\t\tprocess.exit(1);
2610
+ \t\t}
2611
+ \t\treturn;
2612
+ \t}
2613
+
2614
+ \tprocess.exit(code ?? 0);
2615
+ });
2616
+ `;
2617
+ }
2618
+ function writeJson(path, value) {
2619
+ writeFileSync(path, `${JSON.stringify(value, null, " ")}\n`);
2620
+ }
2621
+ function copyRootReadme(cwd, rootDir) {
2622
+ const readmePath = join(cwd, "README.md");
2623
+ if (existsSync(readmePath)) copyFileSync(readmePath, join(rootDir, "README.md"));
2624
+ }
2625
+ function copyLicense(cwd, packageDirs) {
2626
+ const licenseName = [
2627
+ "LICENSE",
2628
+ "LICENSE.md",
2629
+ "LICENCE",
2630
+ "LICENCE.md"
2631
+ ].find((name) => existsSync(join(cwd, name)));
2632
+ if (!licenseName) return;
2633
+ const licensePath = join(cwd, licenseName);
2634
+ for (const packageDir of packageDirs) copyFileSync(licensePath, join(packageDir, licenseName));
2635
+ }
2636
+ function writeDistributionManifest(stageDir, metadata, commands, targets, build) {
2637
+ const manifest = {
2638
+ version: metadata.version,
2639
+ root: {
2640
+ name: metadata.rootPackageName,
2641
+ dir: "root",
2642
+ bins: [...commands]
2643
+ },
2644
+ packages: targets.map((target) => ({
2645
+ target: target.targetAlias,
2646
+ name: target.packageName,
2647
+ dir: target.targetAlias,
2648
+ os: target.os,
2649
+ cpu: target.cpu,
2650
+ ...target.libc ? { libc: target.libc } : {},
2651
+ bins: platformBinMap(commands, target)
2652
+ })),
2653
+ publishOrder: [...targets.map((target) => target.targetAlias), "root"],
2654
+ ...build ? { build } : {}
2655
+ };
2656
+ writeJson(join(stageDir, "manifest.json"), manifest);
2657
+ return manifest;
2658
+ }
2659
+ function stageDistributionPackages(cwd, stageDir, metadata, commands, targets, options) {
2660
+ const rootDir = join(stageDir, "root");
2661
+ const rootBinDir = join(rootDir, "bin");
2662
+ mkdirSync(rootBinDir, { recursive: true });
2663
+ writeJson(join(rootDir, "package.json"), buildDistributionRootPackageJson(metadata, commands, targets, options));
2664
+ copyRootReadme(cwd, rootDir);
2665
+ for (const target of targets) {
2666
+ mkdirSync(join(target.packageDir, "bin"), { recursive: true });
2667
+ writeJson(join(target.packageDir, "package.json"), buildDistributionPlatformPackageJson(metadata, commands, target));
2668
+ }
2669
+ copyLicense(cwd, [rootDir, ...targets.map((target) => target.packageDir)]);
2670
+ }
2671
+ /**
2672
+ * Stages the npm tree in `plan.stageDir`. `build` is each command's Extension
2673
+ * build hook report, recorded in `manifest.json`; omit it when the hooks did not run.
2674
+ * Returns every generated package.json, launcher, and compiled command in staging order.
2675
+ */
2676
+ async function runDistributeBuild(plan, distribution, io, build) {
2677
+ const metadata = resolveDistributionMetadata(plan.cwd, plan.userPackageJson);
2678
+ const commands = plan.entries.map((entry) => entry.command);
2679
+ const table = distribution.table;
2680
+ const distributionTargets = table ? distribution.targets.map((target) => resolveDistributionTarget(table, plan.stageDir, metadata.rootPackageName, target)) : [];
2681
+ io.stdout(table ? `Staging ${bold(`${distributionTargets.length}`)} distribution target(s) in ${dim(plan.stageDir)}...` : `Staging a root-only npm package in ${dim(plan.stageDir)}...`);
2682
+ const artifactOutDir = plan.validate ? plan.outDir : void 0;
2683
+ const artifacts = collectArtifacts(artifactOutDir);
2684
+ const includeDirs = collectIncludeDirs(plan.cwd, plan.stageDir, plan.include, artifacts.names);
2685
+ stageDistributionPackages(plan.cwd, plan.stageDir, metadata, commands, distributionTargets, {
2686
+ artifactDirs: [...artifacts.names, ...includeDirs],
2687
+ manPages: artifacts.manPages
2688
+ });
2689
+ const rootDir = join(plan.stageDir, "root");
2690
+ const produced = [{
2691
+ kind: "package-json",
2692
+ path: join(rootDir, "package.json")
2693
+ }, ...distributionTargets.map((targetPackage) => ({
2694
+ kind: "package-json",
2695
+ path: join(targetPackage.packageDir, "package.json"),
2696
+ target: targetPackage.target
2697
+ }))];
2698
+ const copies = [...artifactOutDir ? artifacts.names.map((name) => ({
2699
+ name,
2700
+ sourceDir: join(artifactOutDir, name),
2701
+ dereference: false
2702
+ })) : [], ...includeDirs.map((name) => ({
2703
+ name,
2704
+ sourceDir: join(plan.cwd, name),
2705
+ dereference: true
2706
+ }))];
2707
+ for (const { name, sourceDir, dereference } of copies) {
2708
+ const options = {
2709
+ recursive: true,
2710
+ dereference,
2711
+ verbatimSymlinks: !dereference
2712
+ };
2713
+ cpSync(sourceDir, join(rootDir, name), options);
2714
+ for (const targetPackage of distributionTargets) cpSync(sourceDir, join(targetPackage.packageDir, "bin", name), options);
2715
+ }
2716
+ if (metadata.exports !== void 0) validateStagedExports(metadata.exports, rootDir, metadata.sourceType);
2717
+ const rootBinDir = join(rootDir, "bin");
2718
+ if (table) {
2719
+ for (const { command } of plan.entries) {
2720
+ const launcherPath = join(rootBinDir, `${command}.js`);
2721
+ writeFileSync(launcherPath, generateDistributionJsResolver(command, distributionTargets), { mode: 493 });
2722
+ produced.push({
2723
+ kind: "launcher",
2724
+ path: launcherPath,
2725
+ command
2726
+ });
2727
+ }
2728
+ for (const targetPackage of distributionTargets) for (const { command, entryPath } of plan.entries) {
2729
+ const outfilePath = join(targetPackage.packageDir, "bin", binaryFilename(command, targetPackage));
2730
+ io.stdout(` ${cyan("→")} ${bold(targetPackage.targetAlias)}: ${dim(outfilePath)}`);
2731
+ await distribution.execute(entryPath, outfilePath, targetPackage.target);
2732
+ produced.push({
2733
+ kind: "executable",
2734
+ path: outfilePath,
2735
+ command,
2736
+ target: targetPackage.target
2737
+ });
2738
+ }
2739
+ } else for (const { command, entryPath } of plan.entries) {
2740
+ const rootBinPath = join(rootBinDir, `${command}.js`);
2741
+ io.stdout(` ${cyan("→")} ${bold("root")}: ${dim(rootBinPath)}`);
2742
+ await distribution.execute(entryPath, rootBinPath);
2743
+ produced.push({
2744
+ kind: "bundle",
2745
+ path: rootBinPath,
2746
+ command
2747
+ });
2748
+ }
2749
+ writeDistributionManifest(plan.stageDir, metadata, commands, distributionTargets, build);
2750
+ const manifestPath = join(plan.stageDir, "manifest.json");
2751
+ io.stdout(`\n${green("✓")} Staged ${bold(`${distributionTargets.length + 1}`)} npm package(s) successfully:`);
2752
+ io.stdout(` ${rootDir}`);
2753
+ for (const targetPackage of distributionTargets) io.stdout(` ${targetPackage.packageDir}`);
2754
+ io.stdout(`\n${dim("Manifest:")} ${manifestPath}`);
2755
+ return produced;
2756
+ }
2757
+ /**
2758
+ * `crust.include` directories normalized to cwd-relative POSIX names. They are
2759
+ * staged exactly like Extension artifacts.
2760
+ */
2761
+ function collectIncludeDirs(cwd, stageDir, include, artifactNames) {
2762
+ const names = [...artifactNames];
2763
+ const includeDirs = [];
2764
+ for (const entry of include) {
2765
+ const dir = resolve(cwd, entry);
2766
+ const name = relative(cwd, dir);
2767
+ if (isAbsolute(entry) || name === "" || !isWithin(cwd, dir)) throw new Error(`package.json crust.include entry ${JSON.stringify(entry)} must be a directory inside the project root ${cwd}.`);
2768
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) throw new Error(`package.json crust.include entry ${JSON.stringify(entry)} is not a directory: ${dir}`);
2769
+ assertResolvesInsideProject(cwd, entry, dir);
2770
+ if (isWithin(stageDir, dir) || isWithin(dir, stageDir)) throw new Error(`package.json crust.include entry ${JSON.stringify(entry)} overlaps the build output directory ${stageDir}, which crust build replaces.`);
2771
+ if (name.split(sep)[0] === "bin") throw new Error(`package.json crust.include entry ${JSON.stringify(entry)} conflicts with the generated npm bin directory.\n Include a directory with a different top-level name.`);
2772
+ const posixName = name.replaceAll(sep, "/");
2773
+ const overlap = names.find((staged) => staged === posixName || staged.startsWith(`${posixName}/`) || posixName.startsWith(`${staged}/`));
2774
+ if (overlap !== void 0) throw new Error(`package.json crust.include entry ${JSON.stringify(entry)} overlaps "${overlap}", which is already staged (duplicate include or Extension artifact directory).`);
2775
+ names.push(posixName);
2776
+ includeDirs.push(posixName);
2777
+ }
2778
+ return includeDirs;
2779
+ }
2780
+ /**
2781
+ * Walks `dir` the way the dereferencing copy will (through symlinked
2782
+ * directories) and rejects any path whose real location leaves the project.
2783
+ */
2784
+ function assertResolvesInsideProject(cwd, entry, dir) {
2785
+ const realCwd = realpathSync(cwd);
2786
+ const seen = /* @__PURE__ */ new Set();
2787
+ const walk = (path) => {
2788
+ const real = realpathSync(path);
2789
+ if (!isWithin(realCwd, real)) throw new Error(`package.json crust.include entry ${JSON.stringify(entry)} resolves outside the project root: ${relative(cwd, path)} -> ${real}`);
2790
+ if (seen.has(real) || !statSync(path).isDirectory()) return;
2791
+ seen.add(real);
2792
+ for (const child of readdirSync(path)) walk(join(path, child));
2793
+ };
2794
+ walk(dir);
2795
+ }
2796
+ /**
2797
+ * Copies one entry's Extension build hook output into the shared artifact
2798
+ * directory. Identically spelled directories merge; case-only directory aliases
2799
+ * and a file or file/directory mismatch at a path
2800
+ * another entry already produced is an error, so no entry's hooks can replace
2801
+ * another's output. `owners` tracks case-folded POSIX-relative paths across
2802
+ * entries, including directories so file/ancestor conflicts are portable.
2803
+ */
2804
+ function mergeEntryArtifacts(entryOutDir, artifactDir, command, owners) {
2805
+ const merge = (relativeDir) => {
2806
+ for (const dirent of readdirSync(join(entryOutDir, relativeDir), { withFileTypes: true })) {
2807
+ const relativePath = relativeDir ? `${relativeDir}/${dirent.name}` : dirent.name;
2808
+ if (!dirent.isDirectory() && !dirent.isFile()) throw new Error(`Build artifact "${relativePath}" from bin ${JSON.stringify(command)} must use regular files and directories, not symlinks or other file types.`);
2809
+ const source = join(entryOutDir, relativePath);
2810
+ const destination = join(artifactDir, relativePath);
2811
+ const key = relativePath.toLowerCase();
2812
+ const owner = owners.get(key);
2813
+ const existing = lstatSync(destination, { throwIfNoEntry: false });
2814
+ if (owner && !(dirent.isDirectory() && owner.directory && owner.path === relativePath) || existing && !(dirent.isDirectory() && existing.isDirectory())) throw new Error(`Build artifact "${relativePath}" is written by both bin ${JSON.stringify(owner?.command ?? "an earlier bin")} and ${JSON.stringify(command)}.\n Extension build hooks of different commands must write distinct paths under ${artifactDir}.`);
2815
+ if (!owner) owners.set(key, {
2816
+ command,
2817
+ directory: dirent.isDirectory(),
2818
+ path: relativePath
2819
+ });
2820
+ if (dirent.isDirectory()) {
2821
+ mkdirSync(destination, { recursive: true });
2822
+ merge(relativePath);
2823
+ } else {
2824
+ mkdirSync(dirname(destination), { recursive: true });
2825
+ copyFileSync(source, destination);
2826
+ }
2827
+ }
2828
+ };
2829
+ const root = lstatSync(entryOutDir, { throwIfNoEntry: false });
2830
+ if (root === void 0) return;
2831
+ if (!root.isDirectory()) throw new Error(`Build artifact directory for bin ${JSON.stringify(command)} must be a directory, not a symlink or other file type: ${entryOutDir}`);
2832
+ merge("");
2833
+ }
2834
+ function collectArtifacts(artifactOutDir) {
2835
+ if (!artifactOutDir || !existsSync(artifactOutDir)) return {
2836
+ names: [],
2837
+ manPages: []
2838
+ };
2839
+ const names = readdirSync(artifactOutDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
2840
+ if (names.includes("bin")) throw new Error(`Artifact directory "bin" in ${artifactOutDir} conflicts with the generated npm bin directory.\n Emit build artifacts under a different top-level name.`);
2841
+ return {
2842
+ names,
2843
+ manPages: names.includes("man") ? readdirSync(join(artifactOutDir, "man"), { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => entry.name).sort() : []
2844
+ };
2845
+ }
2846
+ //#endregion
2847
+ //#region src/commands/build.ts
2848
+ /** Also mirrored by `schema/package.json`; build.test.ts guards against drift. */
2849
+ const CRUST_CONFIG_KEYS = [
2850
+ "runtime",
2851
+ "targets",
2852
+ "bunPlugins",
2853
+ "include"
2854
+ ];
2855
+ function isBuildRuntime(value) {
2856
+ return typeof value === "string" && BUILD_RUNTIMES.some((runtime) => runtime === value);
2857
+ }
2858
+ function isString(value) {
2859
+ return typeof value === "string";
2860
+ }
2861
+ function isStringArray(value) {
2862
+ return Array.isArray(value) && value.every(isString);
2863
+ }
2864
+ function readCrustConfig(pkg) {
2865
+ if (pkg === void 0 || !isJsonObject(pkg) || pkg.crust === void 0) return {};
2866
+ const crust = pkg.crust;
2867
+ const allowed = `Allowed keys: ${CRUST_CONFIG_KEYS.join(", ")}`;
2868
+ if (!isJsonObject(crust)) throw new Error(`package.json crust must be an object. ${allowed}`);
2869
+ const unknown = Object.keys(crust).find((key) => !CRUST_CONFIG_KEYS.some((allowedKey) => allowedKey === key));
2870
+ if (unknown !== void 0) throw new Error(`Unknown package.json crust key ${JSON.stringify(unknown)}. ${allowed}`);
2871
+ const config = {};
2872
+ if (crust.runtime !== void 0) {
2873
+ if (!isBuildRuntime(crust.runtime)) throw new Error(`Invalid package.json crust.runtime ${JSON.stringify(crust.runtime)}. Valid runtimes: ${BUILD_RUNTIMES.join(", ")}`);
2874
+ config.runtime = crust.runtime;
2875
+ }
2876
+ if (crust.targets !== void 0) {
2877
+ if (!isStringArray(crust.targets) || crust.targets.length === 0) throw new Error("package.json crust.targets must be a non-empty array of canonical compiler targets, e.g. [\"bun-linux-x64\", \"bun-darwin-arm64\"].");
2878
+ config.targets = crust.targets;
2879
+ }
2880
+ if (crust.bunPlugins !== void 0) {
2881
+ if (!isStringArray(crust.bunPlugins)) throw new Error("package.json crust.bunPlugins must be an array of Bun plugin module specifiers, e.g. [\"@opentui/solid/bun-plugin\", \"./build/plugin.ts\"].");
2882
+ config.bunPlugins = crust.bunPlugins;
2883
+ }
2884
+ if (crust.include !== void 0) {
2885
+ if (!isStringArray(crust.include)) throw new Error("package.json crust.include must be an array of directory names relative to the project root, e.g. [\"templates\"].");
2886
+ config.include = crust.include;
2887
+ }
2888
+ return config;
2889
+ }
2890
+ function hasDependency(pkg, name) {
2891
+ if (!isJsonObject(pkg)) return false;
2892
+ return [pkg.dependencies, pkg.devDependencies].some((deps) => deps !== void 0 && isJsonObject(deps) && name in deps);
2893
+ }
2894
+ const DENO_CONFIG_FILES = ["deno.json", "deno.jsonc"];
2895
+ /**
2896
+ * package.json `crust.runtime` > inference > Bun. Inference uses only
2897
+ * unambiguous signals: a Deno config file, or `@types/node` without
2898
+ * `@types/bun`. Lockfiles say which package manager installed dependencies,
2899
+ * not which runtime runs the CLI, so they are not consulted.
2900
+ */
2901
+ function resolveBuildRuntime(pkg, config, cwd) {
2902
+ if (config.runtime !== void 0) return {
2903
+ runtime: config.runtime,
2904
+ source: "from package.json"
2905
+ };
2906
+ const denoConfig = DENO_CONFIG_FILES.find((file) => existsSync(join(cwd, file)));
2907
+ if (denoConfig) return {
2908
+ runtime: "deno",
2909
+ source: `inferred from ${denoConfig}`
2910
+ };
2911
+ if (pkg !== void 0 && hasDependency(pkg, "@types/node") && !hasDependency(pkg, "@types/bun")) return {
2912
+ runtime: "node",
2913
+ source: "inferred from @types/node"
2914
+ };
2915
+ return {
2916
+ runtime: "bun",
2917
+ source: "default"
2918
+ };
2919
+ }
2920
+ function printBuildReport(command, report, stdout) {
2921
+ if (report.extensions.length === 0) return;
2922
+ stdout(`Preparing Command Snapshot for ${command}...`);
2923
+ const fileCount = (files) => `${files.length} ${files.length === 1 ? "file" : "files"}`;
2924
+ const idWidth = Math.max(...report.extensions.map(({ id }) => id.length));
2925
+ const countWidth = Math.max(...report.extensions.map(({ files }) => fileCount(files).length));
2926
+ for (const { id, files } of report.extensions) {
2927
+ const paths = [...files.slice(0, 3), ...files.length > 3 ? [`+${files.length - 3} more`] : []].join(", ");
2928
+ stdout(` ${id.padEnd(idWidth)} ${fileCount(files).padEnd(countWidth)}${paths ? ` ${paths}` : ""}`);
2929
+ }
2930
+ }
2931
+ function resolveEnvFilePaths(cwd, envFiles) {
2932
+ if (!envFiles || envFiles.length === 0) return [];
2933
+ return envFiles.map((envFile) => {
2934
+ const envPath = resolve(cwd, envFile);
2935
+ if (!existsSync(envPath)) throw new Error(`Env file not found: ${envPath}\n Specify a valid env file with --env-file <path>`);
2936
+ return envPath;
2937
+ });
2938
+ }
2939
+ /** Entry built when package.json has no `bin`. */
2940
+ const DEFAULT_ENTRY = "src/cli.ts";
2941
+ /**
2942
+ * Command names become `bin/<command>.js`, `<command>-<target>` binary
2943
+ * filenames, and launcher text, so they are restricted to a filename-safe
2944
+ * subset of what npm accepts: no separators, dots-only names, or leading `.`/`-`.
2945
+ */
2946
+ const COMMAND_NAME_PATTERN = /^[A-Za-z0-9_~][A-Za-z0-9._~-]*$/;
2947
+ const BIN_EXAMPLE = `{ "my-cli": ${JSON.stringify(DEFAULT_ENTRY)} }`;
2948
+ /**
2949
+ * A `bin` value as an absolute path. The path must be relative and lexically
2950
+ * inside the project (a symlink may point elsewhere, as for `crust.include`),
2951
+ * and must name an existing file, not a directory.
2952
+ */
2953
+ function resolveEntryPath(cwd, command, source) {
2954
+ const entryPath = resolve(cwd, source);
2955
+ if (isAbsolute(source) || entryPath === cwd || !isWithin(cwd, entryPath)) throw new Error(`package.json bin ${JSON.stringify(command)} entry ${JSON.stringify(source)} must be a file inside the project root ${cwd}.`);
2956
+ if (!existsSync(entryPath)) throw new Error(`Entry file not found: ${entryPath}\n Point package.json bin ${JSON.stringify(command)} at your CLI source entry (default ${DEFAULT_ENTRY}).`);
2957
+ if (!statSync(entryPath).isFile()) throw new Error(`package.json bin ${JSON.stringify(command)} entry ${JSON.stringify(source)} is not a file: ${entryPath}`);
2958
+ return entryPath;
2959
+ }
2960
+ /**
2961
+ * package.json `bin` as build entries, in declaration order. Object values are
2962
+ * source files built for the command named by their key; a string `bin` is the
2963
+ * source of a command named after the unscoped package name, and no `bin` builds
2964
+ * `src/cli.ts` under that name.
2965
+ *
2966
+ * Two commands cannot share one entry file: entries are compared by real path,
2967
+ * so `./src/cli.ts`, `src/../src/cli.ts`, and a symlink to `src/cli.ts` all
2968
+ * collide. Command names are compared case-insensitively, because `Tool` and
2969
+ * `tool` would be the same `bin/` file on a case-insensitive filesystem.
2970
+ */
2971
+ function resolveBinEntries(cwd, pkg) {
2972
+ const packageJson = pkg !== void 0 && isJsonObject(pkg) ? pkg : {};
2973
+ const bin = packageJson.bin;
2974
+ let declared;
2975
+ if (bin === void 0 || isString(bin)) {
2976
+ const name = packageJson.name;
2977
+ if (name === void 0 || !isString(name) || name === "") throw new Error(`package.json is missing a name field.\n Without an object bin, the unscoped package name is the command name, e.g. "bin": ${BIN_EXAMPLE}.`);
2978
+ declared = [[name.replace(/^@[^/]+\//, ""), bin ?? "src/cli.ts"]];
2979
+ } else if (isJsonObject(bin) && Object.keys(bin).length > 0) declared = Object.entries(bin);
2980
+ else throw new Error(`package.json bin must be a source entry path or a non-empty object mapping command names to source entries, e.g. ${BIN_EXAMPLE}.`);
2981
+ const commandByLowerName = /* @__PURE__ */ new Map();
2982
+ const commandByRealPath = /* @__PURE__ */ new Map();
2983
+ return declared.map(([command, source]) => {
2984
+ if (!COMMAND_NAME_PATTERN.test(command)) throw new Error(`package.json bin key ${JSON.stringify(command)} is not a valid command name.\n Use letters, digits, ".", "_", "~", and "-", not starting with "." or "-".`);
2985
+ const sameName = commandByLowerName.get(command.toLowerCase());
2986
+ if (sameName !== void 0) throw new Error(`package.json bin keys ${JSON.stringify(sameName)} and ${JSON.stringify(command)} differ only by case.\n Command names become bin/ file names, which collide on case-insensitive filesystems; rename one of them.`);
2987
+ commandByLowerName.set(command.toLowerCase(), command);
2988
+ if (!isString(source)) throw new Error(`package.json bin ${JSON.stringify(command)} must be a project-relative source entry path, e.g. ${JSON.stringify(DEFAULT_ENTRY)}.`);
2989
+ const entryPath = resolveEntryPath(cwd, command, source);
2990
+ const realPath = realpathSync(entryPath);
2991
+ const other = commandByRealPath.get(realPath);
2992
+ if (other !== void 0) throw new Error(`package.json bin ${JSON.stringify(other)} and ${JSON.stringify(command)} both build ${realPath}.\n Each command needs its own entry file; aliases of one entry are not supported.`);
2993
+ commandByRealPath.set(realPath, command);
2994
+ return {
2995
+ command,
2996
+ entryPath
2997
+ };
2998
+ });
2999
+ }
3000
+ function planBuild(options, cwd) {
3001
+ const userPackageJson = readUserPackageJson(cwd);
3002
+ const config = readCrustConfig(userPackageJson);
3003
+ const { runtime, source: runtimeSource } = resolveBuildRuntime(userPackageJson, config, cwd);
3004
+ const entries = resolveBinEntries(cwd, userPackageJson);
3005
+ validatePackageIdentity(userPackageJson, "package.json");
3006
+ const envFiles = resolveEnvFilePaths(cwd, options.envFiles);
3007
+ const bunPlugins = config.bunPlugins ?? [];
3008
+ if (runtime === "node" && options.targets?.length) throw new Error("--target cannot be used with the node runtime.\n Node builds produce one portable JavaScript artifact.");
3009
+ if (runtime === "node" && config.targets !== void 0) throw new Error("package.json crust.targets is not supported with the node runtime.\n Node builds produce one portable JavaScript artifact; remove crust.targets or set crust.runtime to bun or deno.");
3010
+ if (runtime === "deno" && options.minify) throw new Error("--minify is not supported with the deno runtime.\n deno compile has no minification step; drop the flag.");
3011
+ if (runtime === "deno" && envFiles.length > 0) throw new Error("--env-file is not supported with the deno runtime.\n deno compile embeds every variable from the file into the binary — secrets included —\n with no PUBLIC_* filter. Load configuration at runtime instead (e.g. deno run --env-file).");
3012
+ if (runtime === "deno" && bunPlugins.length > 0) throw new Error("package.json crust.bunPlugins is not supported with the deno runtime.\n deno compile has no Bun bundler; remove crust.bunPlugins or set crust.runtime to bun.");
3013
+ const stageDir = resolve(cwd, CRUST_DIR);
3014
+ const common = {
3015
+ cwd,
3016
+ userPackageJson,
3017
+ runtimeSource,
3018
+ entries,
3019
+ envFiles,
3020
+ bunPlugins,
3021
+ include: config.include ?? [],
3022
+ outDir: join(stageDir, "artifacts"),
3023
+ stageDir,
3024
+ validate: options.validate ?? true,
3025
+ minify: runtime === "deno" ? false : options.minify ?? true
3026
+ };
3027
+ if (runtime === "node") return {
3028
+ ...common,
3029
+ runtime
3030
+ };
3031
+ const targetInputs = options.targets?.length ? options.targets : config.targets;
3032
+ if (runtime === "bun") {
3033
+ const targets = resolveTargets(BUN_TARGETS, targetInputs);
3034
+ assertTargetsBuildableWithoutBun(targets);
3035
+ return {
3036
+ ...common,
3037
+ runtime,
3038
+ targets
3039
+ };
3040
+ }
3041
+ return {
3042
+ ...common,
3043
+ runtime,
3044
+ targets: resolveTargets(DENO_TARGETS, targetInputs)
3045
+ };
3046
+ }
3047
+ /** Bun and Deno stage platform packages behind a Node launcher; Node stages a root-only bundle. */
3048
+ async function runStagedBuild(plan, io, reports) {
3049
+ if (plan.runtime === "bun") return runDistributeBuild(plan, {
3050
+ table: BUN_TARGETS,
3051
+ targets: plan.targets,
3052
+ execute: (entry, outfile, target) => execBuild(entry, outfile, plan.minify, target, plan.envFiles, plan.cwd, plan.bunPlugins)
3053
+ }, io, reports);
3054
+ if (plan.runtime === "deno") return runDistributeBuild(plan, {
3055
+ table: DENO_TARGETS,
3056
+ targets: plan.targets,
3057
+ execute: (entry, outfile, target) => execDenoBuild(entry, outfile, target, plan.cwd)
3058
+ }, io, reports);
3059
+ return runDistributeBuild(plan, { execute: (entry, outfile) => execNodeBuild(entry, outfile, plan.minify, plan.envFiles, plan.cwd, plan.bunPlugins) }, io, reports);
3060
+ }
3061
+ /**
3062
+ * Prepares every entry's Command Snapshot and checks that its root command is
3063
+ * named after the bin key, then merges the Extension build hook output into
3064
+ * `plan.outDir`. Each entry's hooks write into their own temporary directory;
3065
+ * the merge fails on any path two entries both write. Returns each command's
3066
+ * Build Report.
3067
+ */
3068
+ async function prepareEntries(plan, io) {
3069
+ const owners = /* @__PURE__ */ new Map();
3070
+ const reports = {};
3071
+ for (const { command, entryPath } of plan.entries) {
3072
+ const entryOutDir = await mkdtemp(join(tmpdir(), "crust-artifacts-"));
3073
+ try {
3074
+ const { snapshot, build: report } = await buildEntrypoint(entryPath, entryOutDir, plan.envFiles, io, plan.cwd);
3075
+ if (snapshot.meta.name !== command) throw new Error(`package.json bin ${JSON.stringify(command)} builds ${entryPath}, whose root command is named ${JSON.stringify(snapshot.meta.name)}.\n The installed command, help, man pages, and skills use the root command name, so new Crust(name) must match the bin key; rename one of them.`);
3076
+ printBuildReport(command, report, io.stdout);
3077
+ mergeEntryArtifacts(entryOutDir, plan.outDir, command, owners);
3078
+ reports[command] = report;
3079
+ } finally {
3080
+ rmSync(entryOutDir, {
3081
+ recursive: true,
3082
+ force: true
3083
+ });
3084
+ }
3085
+ }
3086
+ return reports;
3087
+ }
3088
+ const activeBuilds = /* @__PURE__ */ new Set();
3089
+ /**
3090
+ * Stages the publishable npm tree in `<cwd>/.crust`: a root package with one
3091
+ * `bin/<command>.js` per package.json `bin` entry, each a Node launcher (Bun,
3092
+ * Deno) or the bundle itself (Node), plus one platform package per target for
3093
+ * Bun and Deno holding one binary per command. Command names and source
3094
+ * entries come from `bin`; the runtime, Bun plugins, and extra directories
3095
+ * from package.json `crust`. Compilation and Command Snapshots run in bun
3096
+ * subprocesses (bun on PATH, or the running Bun executable), so this works
3097
+ * under Node as well when Bun is installed.
3098
+ *
3099
+ * Throws on any failure. Planning failures (bad options or package.json) leave
3100
+ * the previous `.crust/` stage untouched; failures after planning leave a
3101
+ * wiped stage without a completion `manifest.json`. Overlapping calls for the
3102
+ * same real project directory in this process are rejected before staging.
3103
+ */
3104
+ async function build(options = {}) {
3105
+ const cwd = resolve(options.cwd ?? process.cwd());
3106
+ const project = realpathSync(cwd);
3107
+ if (activeBuilds.has(project)) throw new Error(`crust build is already building ${project}`);
3108
+ activeBuilds.add(project);
3109
+ try {
3110
+ const onLog = options.onLog ?? (() => {});
3111
+ const io = {
3112
+ stdout: (line) => onLog(line, "stdout"),
3113
+ stderr: (line) => onLog(line, "stderr")
3114
+ };
3115
+ const plan = planBuild(options, cwd);
3116
+ io.stdout(`${dim("Runtime:")} ${plan.runtime} ${dim(`(${plan.runtimeSource})`)}`);
3117
+ rmSync(plan.stageDir, {
3118
+ recursive: true,
3119
+ force: true
3120
+ });
3121
+ const reports = plan.validate ? await prepareEntries(plan, io) : void 0;
3122
+ const artifacts = await runStagedBuild(plan, io, reports);
3123
+ return {
3124
+ stageDir: plan.stageDir,
3125
+ artifacts,
3126
+ ...reports ? { reports } : {}
3127
+ };
3128
+ } finally {
3129
+ activeBuilds.delete(project);
3130
+ }
3131
+ }
3132
+ defineCommand("build", { description: "Build your CLI for Bun, Deno, or Node" }, (command) => command.flags({
3133
+ name: "target",
3134
+ type: "string",
3135
+ multiple: true,
3136
+ description: `Canonical compiler target(s), or "${HOST_TARGET}" for this machine; repeatable. Omit to stage package.json crust.targets, or every Bun/Deno target`,
3137
+ short: "t"
3138
+ }, {
3139
+ name: "env-file",
3140
+ type: "string",
3141
+ multiple: true,
3142
+ description: "Explicit env file(s) used for build-time constants; repeatable"
3143
+ }, {
3144
+ name: "validate",
3145
+ type: "boolean",
3146
+ description: "Materialize command definitions before compiling; --no-validate also skips Extension build hooks",
3147
+ default: true
3148
+ }, {
3149
+ name: "minify",
3150
+ type: "boolean",
3151
+ description: "Minify the output (default for bun and node; unsupported for deno)"
3152
+ }).action(async ({ flags, stdout, stderr }) => {
3153
+ await build({
3154
+ cwd: process.cwd(),
3155
+ targets: flags.target,
3156
+ envFiles: flags["env-file"],
3157
+ minify: flags.minify,
3158
+ validate: flags.validate,
3159
+ onLog: (line, stream) => (stream === "stderr" ? stderr : stdout)(line)
3160
+ });
3161
+ }));
3162
+ //#endregion
3163
+ export { build };