shipseal 0.0.1 → 0.0.2

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.
package/dist/cli.js CHANGED
@@ -1,13 +1,4939 @@
1
1
  #!/usr/bin/env node
2
- //#region src/cli.ts
3
- process.stdout.write([
4
- "shipseal is pre-alpha and not usable yet.",
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { basename, dirname, extname, isAbsolute, join, relative } from "node:path";
4
+ import { cac } from "cac";
5
+ import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
6
+ import { z } from "zod";
7
+ import { createHash } from "node:crypto";
8
+ import { execFile } from "node:child_process";
9
+ import { promisify } from "node:util";
10
+ import { fileURLToPath } from "node:url";
11
+ import { Renderer } from "takumi-js/node";
12
+ import { container, text } from "takumi-js/helpers";
13
+ import { fromJsx } from "takumi-js/helpers/jsx";
14
+ import { createInterface } from "node:readline/promises";
15
+ import { stdin, stdout } from "node:process";
16
+ import { inflateSync } from "node:zlib";
17
+
18
+ //#region src/core/errors.ts
19
+ var ShipsealError = class extends Error {
20
+ code;
21
+ fix;
22
+ constructor(code, message, fix, options) {
23
+ super(message, options);
24
+ this.name = "ShipsealError";
25
+ this.code = code;
26
+ this.fix = fix;
27
+ }
28
+ };
29
+ function formatError(error) {
30
+ return `${error.message}\nFix: ${error.fix}`;
31
+ }
32
+
33
+ //#endregion
34
+ //#region src/brand/schema.ts
35
+ const hexColorSchema = z.string().regex(/^#([\da-f]{6})$/i, "Expected a 6-digit hex color like #0b0b0c");
36
+ const fontFaceSchema = z.object({
37
+ family: z.string().min(1),
38
+ weight: z.number().int().min(100).max(900),
39
+ file: z.string().min(1).optional()
40
+ });
41
+ const brandSchema = z.object({
42
+ $schema: z.string().optional(),
43
+ version: z.literal(1),
44
+ name: z.string().min(1),
45
+ tagline: z.string().optional(),
46
+ url: z.string().optional(),
47
+ logo: z.object({
48
+ light: z.string().min(1),
49
+ dark: z.string().min(1).optional()
50
+ }).optional(),
51
+ colors: z.object({
52
+ background: hexColorSchema,
53
+ foreground: hexColorSchema,
54
+ muted: hexColorSchema.optional(),
55
+ primary: hexColorSchema.optional(),
56
+ accent: hexColorSchema.optional()
57
+ }),
58
+ fonts: z.object({
59
+ heading: fontFaceSchema,
60
+ body: fontFaceSchema,
61
+ mono: fontFaceSchema
62
+ }),
63
+ radius: z.number().nonnegative(),
64
+ theme: z.enum(["dark", "light"]),
65
+ style: z.literal("minimal"),
66
+ tokens: z.string().nullable()
67
+ });
68
+ const DEFAULT_BRAND_COLORS = {
69
+ background: "#0b0b0c",
70
+ foreground: "#fafafa",
71
+ muted: "#a1a1aa",
72
+ primary: "#ff4d4d",
73
+ accent: "#fbbf24"
74
+ };
75
+ const DEFAULT_BRAND_FONTS = {
76
+ heading: {
77
+ family: "Geist",
78
+ weight: 700
79
+ },
80
+ body: {
81
+ family: "Geist",
82
+ weight: 400
83
+ },
84
+ mono: {
85
+ family: "Geist Mono",
86
+ weight: 400
87
+ }
88
+ };
89
+
90
+ //#endregion
91
+ //#region src/brand/load.ts
92
+ async function loadBrand(cwd) {
93
+ const path = join(cwd, ".shipseal", "brand.json");
94
+ try {
95
+ const raw = JSON.parse(await readFile(path, "utf8"));
96
+ return brandSchema.parse(raw);
97
+ } catch (error) {
98
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") throw new ShipsealError("brand.missing", "No .shipseal/brand.json found.", "Run shipseal init to detect brand colors and write brand.json.");
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ //#endregion
104
+ //#region src/formats.ts
105
+ const FORMAT_IDS = [
106
+ "og",
107
+ "github-social",
108
+ "x",
109
+ "linkedin",
110
+ "square",
111
+ "portrait",
112
+ "producthunt",
113
+ "readme-banner"
114
+ ];
115
+ const LANDSCAPE_SAFE_ZONE = 64;
116
+ const FORMATS = {
117
+ og: {
118
+ id: "og",
119
+ width: 1200,
120
+ height: 630,
121
+ safeZone: LANDSCAPE_SAFE_ZONE
122
+ },
123
+ "github-social": {
124
+ id: "github-social",
125
+ width: 1280,
126
+ height: 640,
127
+ safeZone: LANDSCAPE_SAFE_ZONE
128
+ },
129
+ x: {
130
+ id: "x",
131
+ width: 1200,
132
+ height: 675,
133
+ safeZone: LANDSCAPE_SAFE_ZONE
134
+ },
135
+ linkedin: {
136
+ id: "linkedin",
137
+ width: 1200,
138
+ height: 627,
139
+ safeZone: LANDSCAPE_SAFE_ZONE
140
+ },
141
+ square: {
142
+ id: "square",
143
+ width: 1080,
144
+ height: 1080,
145
+ safeZone: LANDSCAPE_SAFE_ZONE
146
+ },
147
+ portrait: {
148
+ id: "portrait",
149
+ width: 1080,
150
+ height: 1350,
151
+ safeZone: LANDSCAPE_SAFE_ZONE
152
+ },
153
+ producthunt: {
154
+ id: "producthunt",
155
+ width: 1270,
156
+ height: 760,
157
+ safeZone: LANDSCAPE_SAFE_ZONE
158
+ },
159
+ "readme-banner": {
160
+ id: "readme-banner",
161
+ width: 1280,
162
+ height: 400,
163
+ safeZone: LANDSCAPE_SAFE_ZONE
164
+ }
165
+ };
166
+
167
+ //#endregion
168
+ //#region src/config/schema.ts
169
+ const configSchema = z.object({
170
+ $schema: z.string().optional(),
171
+ version: z.literal(1),
172
+ outputDir: z.string().optional(),
173
+ formats: z.array(z.enum(FORMAT_IDS)).optional(),
174
+ release: z.object({
175
+ templates: z.array(z.string()).optional(),
176
+ maxHighlights: z.number().int().positive().optional(),
177
+ changelogPath: z.string().optional(),
178
+ snippet: z.string().nullable().optional()
179
+ }).optional(),
180
+ milestones: z.object({
181
+ stars: z.array(z.number().int().positive()).optional(),
182
+ downloads: z.array(z.number().int().positive()).optional(),
183
+ contributors: z.array(z.number().int().positive()).optional()
184
+ }).optional(),
185
+ bench: z.object({ file: z.string().optional() }).optional(),
186
+ copy: z.object({
187
+ llm: z.boolean().optional(),
188
+ provider: z.string().nullable().optional(),
189
+ model: z.string().nullable().optional(),
190
+ maxRetries: z.number().int().nonnegative().optional()
191
+ }).optional(),
192
+ output: z.object({ imageFormat: z.enum([
193
+ "png",
194
+ "webp",
195
+ "jpeg"
196
+ ]).optional() }).optional(),
197
+ attribution: z.boolean().optional()
198
+ });
199
+ const DEFAULT_CONFIG = {
200
+ version: 1,
201
+ outputDir: ".shipseal/output",
202
+ formats: [
203
+ "og",
204
+ "github-social",
205
+ "x",
206
+ "linkedin"
207
+ ],
208
+ release: {
209
+ templates: [
210
+ "release-hero",
211
+ "release-highlights",
212
+ "code-card"
213
+ ],
214
+ maxHighlights: 4,
215
+ changelogPath: "CHANGELOG.md",
216
+ snippet: null
217
+ },
218
+ milestones: {
219
+ stars: [
220
+ 100,
221
+ 250,
222
+ 500,
223
+ 1e3,
224
+ 2500,
225
+ 5e3,
226
+ 1e4
227
+ ],
228
+ downloads: [
229
+ 1e3,
230
+ 1e4,
231
+ 1e5,
232
+ 1e6
233
+ ],
234
+ contributors: [
235
+ 10,
236
+ 25,
237
+ 50,
238
+ 100
239
+ ]
240
+ },
241
+ bench: { file: ".shipseal/bench.json" },
242
+ copy: {
243
+ llm: false,
244
+ provider: null,
245
+ model: null,
246
+ maxRetries: 2
247
+ },
248
+ output: { imageFormat: "png" },
249
+ attribution: true
250
+ };
251
+
252
+ //#endregion
253
+ //#region src/config/load.ts
254
+ async function loadConfig(cwd) {
255
+ const path = join(cwd, ".shipseal", "config.json");
256
+ try {
257
+ const raw = JSON.parse(await readFile(path, "utf8"));
258
+ const parsed = configSchema.parse(raw);
259
+ return mergeConfig(DEFAULT_CONFIG, parsed);
260
+ } catch (error) {
261
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return DEFAULT_CONFIG;
262
+ throw error;
263
+ }
264
+ }
265
+ function mergeConfig(base, overlay) {
266
+ return {
267
+ ...base,
268
+ ...overlay,
269
+ release: {
270
+ ...base.release,
271
+ ...overlay.release
272
+ },
273
+ milestones: {
274
+ ...base.milestones,
275
+ ...overlay.milestones
276
+ },
277
+ bench: {
278
+ ...base.bench,
279
+ ...overlay.bench
280
+ },
281
+ copy: {
282
+ ...base.copy,
283
+ ...overlay.copy
284
+ },
285
+ output: {
286
+ ...base.output,
287
+ ...overlay.output
288
+ }
289
+ };
290
+ }
291
+
292
+ //#endregion
293
+ //#region src/copy/slots.ts
294
+ const COPY_LIMITS = {
295
+ headline: 48,
296
+ subheadline: 90,
297
+ highlight: 56,
298
+ cta: 32,
299
+ milestoneLine: 48
300
+ };
301
+
302
+ //#endregion
303
+ //#region src/copy/deterministic.ts
304
+ function deterministicCopy(facts, maxHighlights = 4) {
305
+ const name = facts.project.name.value;
306
+ const version = facts.release?.version.value;
307
+ const features = (facts.release?.features ?? []).map((item) => item.value);
308
+ const fixes = (facts.release?.fixes ?? []).map((item) => item.value);
309
+ const breaking = (facts.release?.breaking ?? []).map((item) => item.value);
310
+ const headlineSource = features[0];
311
+ const headline = headlineSource !== void 0 ? cleanLine(headlineSource) : version === void 0 ? name : `${name} ${version}`;
312
+ const tagline = facts.project.tagline?.value;
313
+ const subheadline = tagline !== void 0 && tagline.length > 0 ? cleanLine(tagline) : cleanLine(features[1] ?? fixes[0] ?? `What's new in ${version ?? name}`);
314
+ const highlights = [];
315
+ for (const line of features) {
316
+ if (highlights.length >= maxHighlights) break;
317
+ highlights.push(cleanLine(line));
318
+ }
319
+ for (const line of fixes) {
320
+ if (highlights.length >= maxHighlights) break;
321
+ highlights.push(cleanLine(line));
322
+ }
323
+ for (const line of breaking) {
324
+ if (highlights.length >= maxHighlights) break;
325
+ highlights.push(`Breaking: ${cleanLine(line)}`);
326
+ }
327
+ return {
328
+ headline,
329
+ subheadline,
330
+ highlights,
331
+ cta: ctaFor(facts)
332
+ };
333
+ }
334
+ function formatCount$1(value) {
335
+ return value.toLocaleString("en-US");
336
+ }
337
+ function milestoneCopy(facts, metric, threshold) {
338
+ const formatted = formatCount$1(threshold);
339
+ const line = metric === "stars" ? `Thank you for ${formatted} stars` : metric === "downloads" ? `${formatted} weekly downloads` : `Thank you, ${formatted} contributors`;
340
+ const tagline = facts.project.tagline?.value;
341
+ return {
342
+ headline: facts.project.name.value,
343
+ subheadline: tagline !== void 0 && tagline.length > 0 ? cleanLine(tagline) : "",
344
+ highlights: [],
345
+ cta: ctaFor(facts),
346
+ milestoneLine: line
347
+ };
348
+ }
349
+ function benchCopy(facts) {
350
+ const title = facts.bench?.title.value ?? facts.project.name.value;
351
+ const note = facts.bench?.note?.value;
352
+ return {
353
+ headline: title,
354
+ subheadline: note !== void 0 && note.length > 0 ? note : facts.project.tagline?.value ?? "",
355
+ highlights: [],
356
+ cta: ctaFor(facts)
357
+ };
358
+ }
359
+ function cleanLine(text) {
360
+ const stripped = text.replaceAll("—", ":").replaceAll("–", "-").replaceAll("!", ".").trim();
361
+ const noTrail = stripped.endsWith(".") ? stripped.slice(0, -1) : stripped;
362
+ const first = noTrail.at(0);
363
+ if (first === void 0) return noTrail;
364
+ return first.toUpperCase() + noTrail.slice(1);
365
+ }
366
+ function ctaFor(facts) {
367
+ const npm = facts.project.npmPackage?.value;
368
+ if (npm !== void 0 && npm.length > 0) return `npm i ${npm}`;
369
+ const url = facts.project.url?.value ?? facts.project.repo?.value;
370
+ if (url === void 0) return facts.project.name.value;
371
+ return url.replace(/^https?:\/\//, "");
372
+ }
373
+
374
+ //#endregion
375
+ //#region src/bench/percent.ts
376
+ function percentChange(before, after, better) {
377
+ if (before === 0) {
378
+ if (after === 0) return {
379
+ signed: 0,
380
+ absPercent: 0,
381
+ regression: false
382
+ };
383
+ const improved = better === "higher" ? after > 0 : after < 0;
384
+ return {
385
+ signed: improved ? 100 : -100,
386
+ absPercent: 100,
387
+ regression: !improved
388
+ };
389
+ }
390
+ const raw = better === "lower" ? (before - after) / before : (after - before) / before;
391
+ const signed = Math.round(raw * 100);
392
+ return {
393
+ signed,
394
+ absPercent: Math.abs(signed),
395
+ regression: signed < 0
396
+ };
397
+ }
398
+ function changePhrase(unit, better, change) {
399
+ if (change.absPercent === 0) return "unchanged";
400
+ return `${String(change.absPercent)}% ${deltaWord(unit, better, change.regression)}`;
401
+ }
402
+ function deltaWord(unit, better, regression) {
403
+ if (better === "higher") return regression ? "lower" : "higher";
404
+ const normalized = unit.toLowerCase();
405
+ if (normalized === "ms" || normalized === "s" || normalized === "sec" || normalized === "seconds") return regression ? "slower" : "faster";
406
+ if (normalized === "kb" || normalized === "mb" || normalized === "b" || normalized === "bytes" || normalized === "gzip") return regression ? "larger" : "smaller";
407
+ return regression ? "worse" : "better";
408
+ }
409
+
410
+ //#endregion
411
+ //#region src/fit/fit-text.ts
412
+ async function fitText(renderer, text, spec, font) {
413
+ const sizes = fontSizes(spec);
414
+ for (const fontSize of sizes) {
415
+ const measured = await renderer.measureText(text, measureOpts$1(spec, font, fontSize));
416
+ if (measured.lines <= spec.maxLines) return {
417
+ fontSize,
418
+ text,
419
+ lines: measured.lines,
420
+ truncated: false
421
+ };
422
+ }
423
+ const fontSize = spec.minFontSize;
424
+ const truncated = await truncateToFit(renderer, text, spec, font, fontSize);
425
+ return {
426
+ fontSize,
427
+ text: truncated,
428
+ lines: (await renderer.measureText(truncated, measureOpts$1(spec, font, fontSize))).lines,
429
+ truncated: truncated !== text
430
+ };
431
+ }
432
+ function fontSizes(spec) {
433
+ const sizes = [];
434
+ for (let size = spec.maxFontSize; size >= spec.minFontSize; size -= spec.step) sizes.push(size);
435
+ if (sizes[sizes.length - 1] !== spec.minFontSize) sizes.push(spec.minFontSize);
436
+ return sizes;
437
+ }
438
+ function measureOpts$1(spec, font, fontSize) {
439
+ const opts = {
440
+ fontFamily: font.family,
441
+ fontSize,
442
+ maxWidth: spec.box.width,
443
+ lineHeight: font.lineHeight
444
+ };
445
+ if (font.weight !== void 0) opts.fontWeight = font.weight;
446
+ if (font.whiteSpace !== void 0) opts.whiteSpace = font.whiteSpace;
447
+ return opts;
448
+ }
449
+ async function truncateToFit(renderer, text, spec, font, fontSize) {
450
+ const words = text.trim().length === 0 ? [] : text.trim().split(/\s+/);
451
+ if (words.length === 0) return text;
452
+ const fits = async (candidate) => {
453
+ return (await renderer.measureText(candidate, measureOpts$1(spec, font, fontSize))).lines <= spec.maxLines;
454
+ };
455
+ for (let count = words.length - 1; count >= 1; count -= 1) {
456
+ const candidate = `${words.slice(0, count).join(" ")}...`;
457
+ if (await fits(candidate)) return candidate;
458
+ }
459
+ const first = words[0];
460
+ if (first === void 0) return "...";
461
+ for (let length = first.length; length >= 1; length -= 1) {
462
+ const candidate = `${first.slice(0, length)}...`;
463
+ if (await fits(candidate)) return candidate;
464
+ }
465
+ return "...";
466
+ }
467
+
468
+ //#endregion
469
+ //#region src/templates/theme.ts
470
+ function themeColors(brand, theme) {
471
+ const primary = brand.colors.primary ?? DEFAULT_BRAND_COLORS.primary;
472
+ const accent = brand.colors.accent ?? DEFAULT_BRAND_COLORS.accent;
473
+ const muted = brand.colors.muted ?? DEFAULT_BRAND_COLORS.muted;
474
+ if (theme === "dark") {
475
+ const background = brand.theme === "dark" ? brand.colors.background : brand.colors.foreground;
476
+ const foreground = brand.theme === "dark" ? brand.colors.foreground : brand.colors.background;
477
+ return {
478
+ background,
479
+ foreground,
480
+ muted: brand.theme === "dark" ? muted : DEFAULT_BRAND_COLORS.muted,
481
+ primary,
482
+ accent,
483
+ card: mix(background, foreground, .08)
484
+ };
485
+ }
486
+ const background = brand.theme === "light" ? brand.colors.background : brand.colors.foreground;
487
+ const foreground = brand.theme === "light" ? brand.colors.foreground : brand.colors.background;
488
+ return {
489
+ background,
490
+ foreground,
491
+ muted: brand.theme === "light" ? muted : "#52525b",
492
+ primary,
493
+ accent,
494
+ card: mix(background, foreground, .06)
495
+ };
496
+ }
497
+ function mix(a, b, amount) {
498
+ const ar = Number.parseInt(a.slice(1, 3), 16);
499
+ const ag = Number.parseInt(a.slice(3, 5), 16);
500
+ const ab = Number.parseInt(a.slice(5, 7), 16);
501
+ const br = Number.parseInt(b.slice(1, 3), 16);
502
+ const bg = Number.parseInt(b.slice(3, 5), 16);
503
+ const bb = Number.parseInt(b.slice(5, 7), 16);
504
+ return `#${toHex(ar + (br - ar) * amount)}${toHex(ag + (bg - ag) * amount)}${toHex(ab + (bb - ab) * amount)}`;
505
+ }
506
+ function toHex(value) {
507
+ return Math.round(value).toString(16).padStart(2, "0");
508
+ }
509
+
510
+ //#endregion
511
+ //#region src/takumi-jsx/jsx-runtime.ts
512
+ function jsx(type, props, key) {
513
+ return {
514
+ type,
515
+ props,
516
+ key: key ?? null
517
+ };
518
+ }
519
+ const jsxs = jsx;
520
+
521
+ //#endregion
522
+ //#region src/templates/code-card.tsx
523
+ const propsSchema$4 = z.object({
524
+ headline: z.string(),
525
+ headingFamily: z.string(),
526
+ monoFamily: z.string(),
527
+ headingWeight: z.number(),
528
+ monoWeight: z.number(),
529
+ radius: z.number(),
530
+ lines: z.array(z.array(z.object({
531
+ text: z.string(),
532
+ color: z.string()
533
+ })))
534
+ });
535
+ const codeCard = {
536
+ id: "code-card",
537
+ events: ["release"],
538
+ formats: ["x", "linkedin"],
539
+ propsSchema: propsSchema$4,
540
+ slots(format) {
541
+ return { headline: {
542
+ maxLines: 1,
543
+ maxFontSize: 40,
544
+ minFontSize: 28,
545
+ step: 2,
546
+ box: { width: format.width - 2 * format.safeZone }
547
+ } };
548
+ },
549
+ slotText(raw) {
550
+ return { headline: propsSchema$4.parse(raw).headline };
551
+ },
552
+ slotFont(raw, slot) {
553
+ const props = propsSchema$4.parse(raw);
554
+ if (slot === "headline") return {
555
+ family: props.headingFamily,
556
+ weight: props.headingWeight,
557
+ lineHeight: 1.1
558
+ };
559
+ return {
560
+ family: props.monoFamily,
561
+ weight: props.monoWeight,
562
+ lineHeight: 1.35,
563
+ whiteSpace: "pre"
564
+ };
565
+ },
566
+ buildProps(facts, copy, brand) {
567
+ const missing = [];
568
+ const lines = copy.codeLines ?? [];
569
+ if (lines.length === 0) missing.push({
570
+ fact: "release.codeSnippet",
571
+ effect: "code card uses empty snippet"
572
+ });
573
+ return {
574
+ props: {
575
+ headline: copy.headline,
576
+ headingFamily: brand.fonts.heading.family,
577
+ monoFamily: brand.fonts.mono.family,
578
+ headingWeight: brand.fonts.heading.weight,
579
+ monoWeight: brand.fonts.mono.weight,
580
+ radius: brand.radius,
581
+ lines
582
+ },
583
+ missing
584
+ };
585
+ },
586
+ render(raw, ctx) {
587
+ const props = propsSchema$4.parse(raw);
588
+ const colors = themeColors(ctx.brand, ctx.theme);
589
+ const pad = ctx.format.safeZone;
590
+ const headline = ctx.fitted.headline;
591
+ return /* @__PURE__ */ jsxs("div", {
592
+ style: {
593
+ display: "flex",
594
+ flexDirection: "column",
595
+ width: "100%",
596
+ height: "100%",
597
+ backgroundColor: colors.background,
598
+ padding: pad,
599
+ gap: 24
600
+ },
601
+ children: [
602
+ /* @__PURE__ */ jsx("div", {
603
+ style: {
604
+ fontFamily: props.headingFamily,
605
+ fontWeight: props.headingWeight,
606
+ fontSize: headline?.fontSize ?? 36,
607
+ color: colors.foreground
608
+ },
609
+ children: headline?.text ?? props.headline
610
+ }),
611
+ /* @__PURE__ */ jsx("div", {
612
+ style: {
613
+ display: "flex",
614
+ flexDirection: "column",
615
+ flexGrow: 1,
616
+ backgroundColor: colors.card,
617
+ borderRadius: props.radius,
618
+ padding: 28,
619
+ gap: 0
620
+ },
621
+ children: props.lines.map((line, lineIndex) => /* @__PURE__ */ jsx("div", {
622
+ style: {
623
+ display: "flex",
624
+ flexDirection: "row",
625
+ flexWrap: "nowrap"
626
+ },
627
+ children: line.map((token, tokenIndex) => /* @__PURE__ */ jsx("div", {
628
+ style: {
629
+ fontFamily: props.monoFamily,
630
+ fontWeight: props.monoWeight,
631
+ fontSize: 22,
632
+ color: token.color,
633
+ whiteSpace: "pre",
634
+ lineHeight: 1.35
635
+ },
636
+ children: token.text
637
+ }, `${String(lineIndex)}-${String(tokenIndex)}`))
638
+ }, String(lineIndex)))
639
+ }),
640
+ ctx.attribution ? /* @__PURE__ */ jsx("div", {
641
+ style: {
642
+ fontFamily: props.headingFamily,
643
+ fontWeight: 400,
644
+ fontSize: 18,
645
+ color: colors.muted
646
+ },
647
+ children: "made with shipseal.dev"
648
+ }) : void 0
649
+ ]
650
+ });
651
+ }
652
+ };
653
+ function emptyCodeLines() {
654
+ return [[{
655
+ text: " ",
656
+ color: "#e6edf3"
657
+ }]];
658
+ }
659
+
660
+ //#endregion
661
+ //#region src/templates/highlight.ts
662
+ const MAX_LINES = 14;
663
+ async function highlightCode(code, lang, theme) {
664
+ const clipped = code.split("\n").slice(0, MAX_LINES).join("\n");
665
+ const fallbackColor = theme === "dark" ? "#e6edf3" : "#1f2328";
666
+ try {
667
+ const { codeToTokens } = await import("shiki");
668
+ return (await codeToTokens(clipped, {
669
+ lang: mapLang(lang),
670
+ theme: theme === "dark" ? "github-dark" : "github-light"
671
+ })).tokens.map((line) => {
672
+ if (line.length === 0) return [{
673
+ text: " ",
674
+ color: fallbackColor
675
+ }];
676
+ return line.map((token) => ({
677
+ text: token.content,
678
+ color: token.color ?? fallbackColor
679
+ }));
680
+ });
681
+ } catch {
682
+ return clipped.split("\n").map((line) => [{
683
+ text: line.length === 0 ? " " : line,
684
+ color: fallbackColor
685
+ }]);
686
+ }
687
+ }
688
+ function mapLang(lang) {
689
+ const key = lang.toLowerCase();
690
+ if (key === "javascript" || key === "js") return "js";
691
+ if (key === "tsx") return "tsx";
692
+ if (key === "jsx") return "jsx";
693
+ if (key === "json") return "json";
694
+ if (key === "bash" || key === "shell" || key === "sh") return "bash";
695
+ return "ts";
696
+ }
697
+
698
+ //#endregion
699
+ //#region src/templates/bench.tsx
700
+ const rowSchema = z.object({
701
+ label: z.string(),
702
+ beforeText: z.string(),
703
+ afterText: z.string(),
704
+ changeText: z.string(),
705
+ regression: z.boolean()
706
+ });
707
+ const propsSchema$3 = z.object({
708
+ title: z.string(),
709
+ note: z.string(),
710
+ rows: z.array(rowSchema),
711
+ headingFamily: z.string(),
712
+ bodyFamily: z.string(),
713
+ headingWeight: z.number(),
714
+ bodyWeight: z.number(),
715
+ radius: z.number()
716
+ });
717
+ const bench = {
718
+ id: "bench",
719
+ events: ["bench"],
720
+ formats: ["x", "linkedin"],
721
+ propsSchema: propsSchema$3,
722
+ slots(format) {
723
+ const width = format.width - 2 * format.safeZone;
724
+ const slots = { title: {
725
+ maxLines: 2,
726
+ maxFontSize: 48,
727
+ minFontSize: 28,
728
+ step: 2,
729
+ box: { width }
730
+ } };
731
+ for (let index = 0; index < 3; index += 1) slots[`change-${String(index)}`] = {
732
+ maxLines: 1,
733
+ maxFontSize: 36,
734
+ minFontSize: 22,
735
+ step: 2,
736
+ box: { width: Math.min(360, width) }
737
+ };
738
+ return slots;
739
+ },
740
+ slotText(raw) {
741
+ const props = propsSchema$3.parse(raw);
742
+ const text = { title: props.title };
743
+ props.rows.forEach((row, index) => {
744
+ text[`change-${String(index)}`] = row.changeText;
745
+ });
746
+ return text;
747
+ },
748
+ slotFont(raw, slot) {
749
+ const props = propsSchema$3.parse(raw);
750
+ if (slot === "title") return {
751
+ family: props.headingFamily,
752
+ weight: props.headingWeight,
753
+ lineHeight: 1.15
754
+ };
755
+ return {
756
+ family: props.headingFamily,
757
+ weight: props.headingWeight,
758
+ lineHeight: 1.1
759
+ };
760
+ },
761
+ buildProps(facts, copy, brand) {
762
+ const missing = [];
763
+ if (facts.bench === void 0) missing.push({
764
+ fact: "bench",
765
+ effect: "metrics hidden"
766
+ });
767
+ const rows = facts.bench?.metrics.map((metric) => {
768
+ const change = percentChange(metric.before.value, metric.after.value, metric.better.value);
769
+ return {
770
+ label: metric.label.value,
771
+ beforeText: `${formatMetric(metric.before.value)} ${metric.unit.value}`,
772
+ afterText: `${formatMetric(metric.after.value)} ${metric.unit.value}`,
773
+ changeText: changePhrase(metric.unit.value, metric.better.value, change),
774
+ regression: change.regression
775
+ };
776
+ }) ?? [];
777
+ return {
778
+ props: {
779
+ title: copy.headline,
780
+ note: copy.subheadline,
781
+ rows,
782
+ headingFamily: brand.fonts.heading.family,
783
+ bodyFamily: brand.fonts.body.family,
784
+ headingWeight: brand.fonts.heading.weight,
785
+ bodyWeight: brand.fonts.body.weight,
786
+ radius: brand.radius
787
+ },
788
+ missing
789
+ };
790
+ },
791
+ render(raw, ctx) {
792
+ const props = propsSchema$3.parse(raw);
793
+ const colors = themeColors(ctx.brand, ctx.theme);
794
+ const pad = ctx.format.safeZone;
795
+ const title = ctx.fitted.title;
796
+ return /* @__PURE__ */ jsxs("div", {
797
+ style: {
798
+ display: "flex",
799
+ flexDirection: "column",
800
+ justifyContent: "space-between",
801
+ width: "100%",
802
+ height: "100%",
803
+ backgroundColor: colors.background,
804
+ padding: pad
805
+ },
806
+ children: [
807
+ /* @__PURE__ */ jsx("div", {
808
+ style: {
809
+ fontFamily: props.headingFamily,
810
+ fontWeight: props.headingWeight,
811
+ fontSize: title?.fontSize ?? 40,
812
+ color: colors.foreground,
813
+ lineHeight: 1.15
814
+ },
815
+ children: title?.text ?? props.title
816
+ }),
817
+ /* @__PURE__ */ jsx("div", {
818
+ style: {
819
+ display: "flex",
820
+ flexDirection: "column",
821
+ gap: 20
822
+ },
823
+ children: props.rows.map((row, index) => {
824
+ const change = ctx.fitted[`change-${String(index)}`];
825
+ return /* @__PURE__ */ jsxs("div", {
826
+ style: {
827
+ display: "flex",
828
+ flexDirection: "row",
829
+ justifyContent: "space-between",
830
+ alignItems: "center",
831
+ backgroundColor: colors.card,
832
+ borderRadius: props.radius,
833
+ paddingTop: 16,
834
+ paddingBottom: 16,
835
+ paddingLeft: 24,
836
+ paddingRight: 24
837
+ },
838
+ children: [/* @__PURE__ */ jsxs("div", {
839
+ style: {
840
+ display: "flex",
841
+ flexDirection: "column",
842
+ gap: 6
843
+ },
844
+ children: [/* @__PURE__ */ jsx("div", {
845
+ style: {
846
+ fontFamily: props.bodyFamily,
847
+ fontWeight: props.bodyWeight,
848
+ fontSize: 22,
849
+ color: colors.muted
850
+ },
851
+ children: row.label
852
+ }), /* @__PURE__ */ jsx("div", {
853
+ style: {
854
+ fontFamily: props.bodyFamily,
855
+ fontWeight: props.bodyWeight,
856
+ fontSize: 24,
857
+ color: colors.foreground
858
+ },
859
+ children: `${row.beforeText} to ${row.afterText}`
860
+ })]
861
+ }), /* @__PURE__ */ jsxs("div", {
862
+ style: {
863
+ display: "flex",
864
+ flexDirection: "column",
865
+ alignItems: "flex-end",
866
+ gap: 4
867
+ },
868
+ children: [/* @__PURE__ */ jsx("div", {
869
+ style: {
870
+ fontFamily: props.headingFamily,
871
+ fontWeight: props.headingWeight,
872
+ fontSize: change?.fontSize ?? 32,
873
+ color: row.regression ? colors.primary : colors.accent
874
+ },
875
+ children: change?.text ?? row.changeText
876
+ }), row.regression ? /* @__PURE__ */ jsx("div", {
877
+ style: {
878
+ fontFamily: props.bodyFamily,
879
+ fontWeight: props.bodyWeight,
880
+ fontSize: 18,
881
+ color: colors.primary
882
+ },
883
+ children: "regression"
884
+ }) : void 0]
885
+ })]
886
+ }, row.label);
887
+ })
888
+ }),
889
+ /* @__PURE__ */ jsxs("div", {
890
+ style: {
891
+ display: "flex",
892
+ flexDirection: "row",
893
+ justifyContent: "space-between",
894
+ alignItems: "flex-end"
895
+ },
896
+ children: [/* @__PURE__ */ jsx("div", {
897
+ style: {
898
+ fontFamily: props.bodyFamily,
899
+ fontWeight: props.bodyWeight,
900
+ fontSize: 20,
901
+ color: colors.muted
902
+ },
903
+ children: props.note
904
+ }), ctx.attribution ? /* @__PURE__ */ jsx("div", {
905
+ style: {
906
+ fontFamily: props.bodyFamily,
907
+ fontWeight: props.bodyWeight,
908
+ fontSize: 18,
909
+ color: colors.muted
910
+ },
911
+ children: "made with shipseal.dev"
912
+ }) : void 0]
913
+ })
914
+ ]
915
+ });
916
+ }
917
+ };
918
+ function formatMetric(value) {
919
+ return String(value);
920
+ }
921
+
922
+ //#endregion
923
+ //#region src/templates/milestone.tsx
924
+ const propsSchema$2 = z.object({
925
+ name: z.string(),
926
+ numberText: z.string(),
927
+ metricLabel: z.string(),
928
+ thankYou: z.string(),
929
+ headingFamily: z.string(),
930
+ bodyFamily: z.string(),
931
+ headingWeight: z.number(),
932
+ bodyWeight: z.number(),
933
+ radius: z.number(),
934
+ showLogo: z.boolean()
935
+ });
936
+ const milestone = {
937
+ id: "milestone",
938
+ events: ["milestone"],
939
+ formats: [
940
+ "og",
941
+ "x",
942
+ "linkedin"
943
+ ],
944
+ propsSchema: propsSchema$2,
945
+ slots(format) {
946
+ const width = format.width - 2 * format.safeZone;
947
+ return {
948
+ number: {
949
+ maxLines: 1,
950
+ maxFontSize: 160,
951
+ minFontSize: 72,
952
+ step: 8,
953
+ box: { width }
954
+ },
955
+ thankYou: {
956
+ maxLines: 2,
957
+ maxFontSize: 36,
958
+ minFontSize: 24,
959
+ step: 2,
960
+ box: { width }
961
+ }
962
+ };
963
+ },
964
+ slotText(raw) {
965
+ const props = propsSchema$2.parse(raw);
966
+ return {
967
+ number: props.numberText,
968
+ thankYou: props.thankYou
969
+ };
970
+ },
971
+ slotFont(raw, slot) {
972
+ const props = propsSchema$2.parse(raw);
973
+ if (slot === "number") return {
974
+ family: props.headingFamily,
975
+ weight: props.headingWeight,
976
+ lineHeight: 1
977
+ };
978
+ return {
979
+ family: props.bodyFamily,
980
+ weight: props.bodyWeight,
981
+ lineHeight: 1.25
982
+ };
983
+ },
984
+ buildProps(facts, copy, brand) {
985
+ const missing = [];
986
+ const threshold = facts.milestone?.threshold.value;
987
+ if (threshold === void 0) missing.push({
988
+ fact: "milestone.threshold",
989
+ effect: "number hidden"
990
+ });
991
+ const metric = facts.milestone?.metric.value;
992
+ const metricLabel = metric === "downloads" ? "weekly downloads" : metric === "contributors" ? "contributors" : "stars";
993
+ const showLogo = brand.logo !== void 0;
994
+ if (!showLogo) missing.push({
995
+ fact: "brand.logo",
996
+ effect: "logo hidden"
997
+ });
998
+ const thankYou = copy.milestoneLine ?? (threshold === void 0 ? "" : `Thank you for ${formatCount$1(threshold)} ${metricLabel}`);
999
+ return {
1000
+ props: {
1001
+ name: brand.name,
1002
+ numberText: threshold === void 0 ? "" : formatCount$1(threshold),
1003
+ metricLabel,
1004
+ thankYou,
1005
+ headingFamily: brand.fonts.heading.family,
1006
+ bodyFamily: brand.fonts.body.family,
1007
+ headingWeight: brand.fonts.heading.weight,
1008
+ bodyWeight: brand.fonts.body.weight,
1009
+ radius: brand.radius,
1010
+ showLogo
1011
+ },
1012
+ missing
1013
+ };
1014
+ },
1015
+ render(raw, ctx) {
1016
+ const props = propsSchema$2.parse(raw);
1017
+ const colors = themeColors(ctx.brand, ctx.theme);
1018
+ const pad = ctx.format.safeZone;
1019
+ const number = ctx.fitted.number;
1020
+ const thanks = ctx.fitted.thankYou;
1021
+ const compact = ctx.format.height <= 640;
1022
+ return /* @__PURE__ */ jsxs("div", {
1023
+ style: {
1024
+ display: "flex",
1025
+ flexDirection: "column",
1026
+ justifyContent: "space-between",
1027
+ width: "100%",
1028
+ height: "100%",
1029
+ backgroundColor: colors.background,
1030
+ padding: pad
1031
+ },
1032
+ children: [
1033
+ /* @__PURE__ */ jsxs("div", {
1034
+ style: {
1035
+ display: "flex",
1036
+ flexDirection: "row",
1037
+ alignItems: "center",
1038
+ gap: 20
1039
+ },
1040
+ children: [props.showLogo && ctx.logoSrc !== void 0 ? /* @__PURE__ */ jsx("img", {
1041
+ src: ctx.logoSrc,
1042
+ width: compact ? 56 : 72,
1043
+ height: compact ? 56 : 72
1044
+ }) : void 0, /* @__PURE__ */ jsx("div", {
1045
+ style: {
1046
+ fontFamily: props.headingFamily,
1047
+ fontWeight: props.headingWeight,
1048
+ fontSize: 28,
1049
+ color: colors.foreground
1050
+ },
1051
+ children: props.name
1052
+ })]
1053
+ }),
1054
+ /* @__PURE__ */ jsxs("div", {
1055
+ style: {
1056
+ display: "flex",
1057
+ flexDirection: "column",
1058
+ gap: 12
1059
+ },
1060
+ children: [/* @__PURE__ */ jsx("div", {
1061
+ style: {
1062
+ fontFamily: props.headingFamily,
1063
+ fontWeight: props.headingWeight,
1064
+ fontSize: number?.fontSize ?? 140,
1065
+ color: colors.foreground,
1066
+ lineHeight: 1
1067
+ },
1068
+ children: number?.text ?? props.numberText
1069
+ }), /* @__PURE__ */ jsx("div", {
1070
+ style: {
1071
+ fontFamily: props.bodyFamily,
1072
+ fontWeight: props.headingWeight,
1073
+ fontSize: 32,
1074
+ color: colors.primary
1075
+ },
1076
+ children: props.metricLabel
1077
+ })]
1078
+ }),
1079
+ /* @__PURE__ */ jsxs("div", {
1080
+ style: {
1081
+ display: "flex",
1082
+ flexDirection: "row",
1083
+ justifyContent: "space-between",
1084
+ alignItems: "flex-end"
1085
+ },
1086
+ children: [/* @__PURE__ */ jsx("div", {
1087
+ style: {
1088
+ fontFamily: props.bodyFamily,
1089
+ fontWeight: props.bodyWeight,
1090
+ fontSize: thanks?.fontSize ?? 28,
1091
+ color: colors.muted,
1092
+ lineHeight: 1.25
1093
+ },
1094
+ children: thanks?.text ?? props.thankYou
1095
+ }), ctx.attribution ? /* @__PURE__ */ jsx("div", {
1096
+ style: {
1097
+ fontFamily: props.bodyFamily,
1098
+ fontWeight: props.bodyWeight,
1099
+ fontSize: 18,
1100
+ color: colors.muted
1101
+ },
1102
+ children: "made with shipseal.dev"
1103
+ }) : void 0]
1104
+ })
1105
+ ]
1106
+ });
1107
+ }
1108
+ };
1109
+
1110
+ //#endregion
1111
+ //#region src/templates/release-hero.tsx
1112
+ const propsSchema$1 = z.object({
1113
+ name: z.string(),
1114
+ version: z.string(),
1115
+ headline: z.string(),
1116
+ subheadline: z.string(),
1117
+ cta: z.string(),
1118
+ headingFamily: z.string(),
1119
+ bodyFamily: z.string(),
1120
+ headingWeight: z.number(),
1121
+ bodyWeight: z.number(),
1122
+ radius: z.number(),
1123
+ showCta: z.boolean(),
1124
+ showLogo: z.boolean()
1125
+ });
1126
+ const releaseHero = {
1127
+ id: "release-hero",
1128
+ events: ["release"],
1129
+ formats: [
1130
+ "og",
1131
+ "github-social",
1132
+ "x",
1133
+ "linkedin"
1134
+ ],
1135
+ propsSchema: propsSchema$1,
1136
+ slots(format) {
1137
+ const width = format.width - 2 * format.safeZone;
1138
+ return {
1139
+ headline: {
1140
+ maxLines: 2,
1141
+ maxFontSize: 72,
1142
+ minFontSize: 48,
1143
+ step: 4,
1144
+ box: { width }
1145
+ },
1146
+ subheadline: {
1147
+ maxLines: 2,
1148
+ maxFontSize: 32,
1149
+ minFontSize: 24,
1150
+ step: 2,
1151
+ box: { width }
1152
+ },
1153
+ cta: {
1154
+ maxLines: 1,
1155
+ maxFontSize: 28,
1156
+ minFontSize: 22,
1157
+ step: 2,
1158
+ box: { width: Math.min(480, width) }
1159
+ }
1160
+ };
1161
+ },
1162
+ slotText(raw) {
1163
+ const props = propsSchema$1.parse(raw);
1164
+ return {
1165
+ headline: props.headline,
1166
+ subheadline: props.subheadline,
1167
+ cta: props.cta
1168
+ };
1169
+ },
1170
+ slotFont(raw, slot) {
1171
+ const props = propsSchema$1.parse(raw);
1172
+ if (slot === "headline") return {
1173
+ family: props.headingFamily,
1174
+ weight: props.headingWeight,
1175
+ lineHeight: 1.1
1176
+ };
1177
+ return {
1178
+ family: props.bodyFamily,
1179
+ weight: props.bodyWeight,
1180
+ lineHeight: 1.25
1181
+ };
1182
+ },
1183
+ buildProps(facts, copy, brand) {
1184
+ const missing = [];
1185
+ if (facts.project.npmPackage === void 0 && facts.project.url === void 0) missing.push({
1186
+ fact: "project.npmPackage",
1187
+ effect: "cta uses project name"
1188
+ });
1189
+ const showLogo = brand.logo !== void 0;
1190
+ if (!showLogo) missing.push({
1191
+ fact: "brand.logo",
1192
+ effect: "logo hidden"
1193
+ });
1194
+ const version = facts.release?.version.value ?? "";
1195
+ return {
1196
+ props: {
1197
+ name: brand.name,
1198
+ version,
1199
+ headline: copy.headline,
1200
+ subheadline: copy.subheadline,
1201
+ cta: copy.cta,
1202
+ headingFamily: brand.fonts.heading.family,
1203
+ bodyFamily: brand.fonts.body.family,
1204
+ headingWeight: brand.fonts.heading.weight,
1205
+ bodyWeight: brand.fonts.body.weight,
1206
+ radius: brand.radius,
1207
+ showCta: copy.cta.length > 0,
1208
+ showLogo
1209
+ },
1210
+ missing
1211
+ };
1212
+ },
1213
+ render(raw, ctx) {
1214
+ const props = propsSchema$1.parse(raw);
1215
+ const colors = themeColors(ctx.brand, ctx.theme);
1216
+ const pad = ctx.format.safeZone;
1217
+ const headline = ctx.fitted.headline;
1218
+ const sub = ctx.fitted.subheadline;
1219
+ const cta = ctx.fitted.cta;
1220
+ const compact = ctx.format.height <= 640;
1221
+ return /* @__PURE__ */ jsxs("div", {
1222
+ style: {
1223
+ display: "flex",
1224
+ flexDirection: "column",
1225
+ justifyContent: "space-between",
1226
+ width: "100%",
1227
+ height: "100%",
1228
+ backgroundColor: colors.background,
1229
+ padding: pad
1230
+ },
1231
+ children: [
1232
+ /* @__PURE__ */ jsxs("div", {
1233
+ style: {
1234
+ display: "flex",
1235
+ flexDirection: "row",
1236
+ alignItems: "center",
1237
+ gap: 20
1238
+ },
1239
+ children: [props.showLogo && ctx.logoSrc !== void 0 ? /* @__PURE__ */ jsx("img", {
1240
+ src: ctx.logoSrc,
1241
+ width: compact ? 56 : 72,
1242
+ height: compact ? 56 : 72
1243
+ }) : void 0, /* @__PURE__ */ jsxs("div", {
1244
+ style: {
1245
+ display: "flex",
1246
+ flexDirection: "column",
1247
+ gap: 8
1248
+ },
1249
+ children: [/* @__PURE__ */ jsx("div", {
1250
+ style: {
1251
+ fontFamily: props.headingFamily,
1252
+ fontWeight: props.headingWeight,
1253
+ fontSize: 28,
1254
+ color: colors.foreground
1255
+ },
1256
+ children: props.name
1257
+ }), props.version.length > 0 ? /* @__PURE__ */ jsx("div", {
1258
+ style: {
1259
+ display: "flex",
1260
+ alignItems: "center",
1261
+ backgroundColor: colors.card,
1262
+ borderRadius: props.radius,
1263
+ paddingTop: 6,
1264
+ paddingBottom: 6,
1265
+ paddingLeft: 14,
1266
+ paddingRight: 14
1267
+ },
1268
+ children: /* @__PURE__ */ jsx("div", {
1269
+ style: {
1270
+ fontFamily: props.bodyFamily,
1271
+ fontWeight: props.bodyWeight,
1272
+ fontSize: 22,
1273
+ color: colors.primary
1274
+ },
1275
+ children: `v${props.version}`
1276
+ })
1277
+ }) : void 0]
1278
+ })]
1279
+ }),
1280
+ /* @__PURE__ */ jsxs("div", {
1281
+ style: {
1282
+ display: "flex",
1283
+ flexDirection: "column",
1284
+ gap: 16
1285
+ },
1286
+ children: [/* @__PURE__ */ jsx("div", {
1287
+ style: {
1288
+ fontFamily: props.headingFamily,
1289
+ fontWeight: props.headingWeight,
1290
+ fontSize: headline?.fontSize ?? 64,
1291
+ color: colors.foreground,
1292
+ lineHeight: 1.1
1293
+ },
1294
+ children: headline?.text ?? props.headline
1295
+ }), /* @__PURE__ */ jsx("div", {
1296
+ style: {
1297
+ fontFamily: props.bodyFamily,
1298
+ fontWeight: props.bodyWeight,
1299
+ fontSize: sub?.fontSize ?? 28,
1300
+ color: colors.muted,
1301
+ lineHeight: 1.25
1302
+ },
1303
+ children: sub?.text ?? props.subheadline
1304
+ })]
1305
+ }),
1306
+ /* @__PURE__ */ jsxs("div", {
1307
+ style: {
1308
+ display: "flex",
1309
+ flexDirection: "row",
1310
+ justifyContent: "space-between",
1311
+ alignItems: "flex-end"
1312
+ },
1313
+ children: [props.showCta ? /* @__PURE__ */ jsx("div", {
1314
+ style: {
1315
+ display: "flex",
1316
+ backgroundColor: colors.primary,
1317
+ borderRadius: props.radius,
1318
+ paddingTop: 14,
1319
+ paddingBottom: 14,
1320
+ paddingLeft: 24,
1321
+ paddingRight: 24
1322
+ },
1323
+ children: /* @__PURE__ */ jsx("div", {
1324
+ style: {
1325
+ fontFamily: props.bodyFamily,
1326
+ fontWeight: props.headingWeight,
1327
+ fontSize: cta?.fontSize ?? 24,
1328
+ color: colors.background
1329
+ },
1330
+ children: cta?.text ?? props.cta
1331
+ })
1332
+ }) : /* @__PURE__ */ jsx("div", {}), ctx.attribution ? /* @__PURE__ */ jsx("div", {
1333
+ style: {
1334
+ fontFamily: props.bodyFamily,
1335
+ fontWeight: props.bodyWeight,
1336
+ fontSize: 18,
1337
+ color: colors.muted
1338
+ },
1339
+ children: "made with shipseal.dev"
1340
+ }) : void 0]
1341
+ })
1342
+ ]
1343
+ });
1344
+ }
1345
+ };
1346
+
1347
+ //#endregion
1348
+ //#region src/templates/release-highlights.tsx
1349
+ const propsSchema = z.object({
1350
+ name: z.string(),
1351
+ version: z.string(),
1352
+ highlights: z.array(z.string()),
1353
+ headingFamily: z.string(),
1354
+ bodyFamily: z.string(),
1355
+ headingWeight: z.number(),
1356
+ bodyWeight: z.number(),
1357
+ radius: z.number()
1358
+ });
1359
+ const releaseHighlights = {
1360
+ id: "release-highlights",
1361
+ events: ["release"],
1362
+ formats: ["x", "linkedin"],
1363
+ propsSchema,
1364
+ slots(format) {
1365
+ const width = format.width - 2 * format.safeZone - 48;
1366
+ const slots = {};
1367
+ for (let index = 0; index < 4; index += 1) slots[`highlight-${String(index)}`] = {
1368
+ maxLines: 2,
1369
+ maxFontSize: 32,
1370
+ minFontSize: 24,
1371
+ step: 2,
1372
+ box: { width }
1373
+ };
1374
+ return slots;
1375
+ },
1376
+ slotText(raw) {
1377
+ const props = propsSchema.parse(raw);
1378
+ const text = {};
1379
+ props.highlights.forEach((line, index) => {
1380
+ text[`highlight-${String(index)}`] = line;
1381
+ });
1382
+ return text;
1383
+ },
1384
+ slotFont(raw) {
1385
+ const props = propsSchema.parse(raw);
1386
+ return {
1387
+ family: props.bodyFamily,
1388
+ weight: props.bodyWeight,
1389
+ lineHeight: 1.25
1390
+ };
1391
+ },
1392
+ buildProps(facts, copy, brand) {
1393
+ const missing = [];
1394
+ if (copy.highlights.length === 0) missing.push({
1395
+ fact: "release.features",
1396
+ effect: "highlights list empty"
1397
+ });
1398
+ return {
1399
+ props: {
1400
+ name: brand.name,
1401
+ version: facts.release?.version.value ?? "",
1402
+ highlights: copy.highlights,
1403
+ headingFamily: brand.fonts.heading.family,
1404
+ bodyFamily: brand.fonts.body.family,
1405
+ headingWeight: brand.fonts.heading.weight,
1406
+ bodyWeight: brand.fonts.body.weight,
1407
+ radius: brand.radius
1408
+ },
1409
+ missing
1410
+ };
1411
+ },
1412
+ render(raw, ctx) {
1413
+ const props = propsSchema.parse(raw);
1414
+ const colors = themeColors(ctx.brand, ctx.theme);
1415
+ const pad = ctx.format.safeZone;
1416
+ const title = props.version.length > 0 ? `What's new in v${props.version}` : "What's new";
1417
+ return /* @__PURE__ */ jsxs("div", {
1418
+ style: {
1419
+ display: "flex",
1420
+ flexDirection: "column",
1421
+ width: "100%",
1422
+ height: "100%",
1423
+ backgroundColor: colors.background,
1424
+ padding: pad,
1425
+ gap: 28
1426
+ },
1427
+ children: [
1428
+ /* @__PURE__ */ jsxs("div", {
1429
+ style: {
1430
+ display: "flex",
1431
+ flexDirection: "column",
1432
+ gap: 8
1433
+ },
1434
+ children: [/* @__PURE__ */ jsx("div", {
1435
+ style: {
1436
+ fontFamily: props.bodyFamily,
1437
+ fontWeight: props.bodyWeight,
1438
+ fontSize: 22,
1439
+ color: colors.primary
1440
+ },
1441
+ children: props.name
1442
+ }), /* @__PURE__ */ jsx("div", {
1443
+ style: {
1444
+ fontFamily: props.headingFamily,
1445
+ fontWeight: props.headingWeight,
1446
+ fontSize: 48,
1447
+ color: colors.foreground
1448
+ },
1449
+ children: title
1450
+ })]
1451
+ }),
1452
+ /* @__PURE__ */ jsx("div", {
1453
+ style: {
1454
+ display: "flex",
1455
+ flexDirection: "column",
1456
+ gap: 16
1457
+ },
1458
+ children: props.highlights.map((line, index) => {
1459
+ const fitted = ctx.fitted[`highlight-${String(index)}`];
1460
+ const breaking = line.startsWith("Breaking:");
1461
+ return /* @__PURE__ */ jsxs("div", {
1462
+ style: {
1463
+ display: "flex",
1464
+ flexDirection: "row",
1465
+ alignItems: "flex-start",
1466
+ gap: 16,
1467
+ backgroundColor: colors.card,
1468
+ borderRadius: props.radius,
1469
+ padding: 18
1470
+ },
1471
+ children: [/* @__PURE__ */ jsx("div", { style: {
1472
+ width: 12,
1473
+ height: 12,
1474
+ borderRadius: 6,
1475
+ backgroundColor: breaking ? colors.accent : colors.primary,
1476
+ marginTop: 10
1477
+ } }), /* @__PURE__ */ jsx("div", {
1478
+ style: {
1479
+ fontFamily: props.bodyFamily,
1480
+ fontWeight: props.bodyWeight,
1481
+ fontSize: fitted?.fontSize ?? 28,
1482
+ color: colors.foreground,
1483
+ lineHeight: 1.25
1484
+ },
1485
+ children: fitted?.text ?? line
1486
+ })]
1487
+ }, String(index));
1488
+ })
1489
+ }),
1490
+ ctx.attribution ? /* @__PURE__ */ jsx("div", {
1491
+ style: {
1492
+ fontFamily: props.bodyFamily,
1493
+ fontWeight: props.bodyWeight,
1494
+ fontSize: 18,
1495
+ color: colors.muted,
1496
+ marginTop: "auto"
1497
+ },
1498
+ children: "made with shipseal.dev"
1499
+ }) : void 0
1500
+ ]
1501
+ });
1502
+ }
1503
+ };
1504
+
1505
+ //#endregion
1506
+ //#region src/templates/registry.ts
1507
+ const templates = [
1508
+ releaseHero,
1509
+ releaseHighlights,
1510
+ codeCard,
1511
+ milestone,
1512
+ bench
1513
+ ];
1514
+ function getTemplate(id) {
1515
+ const found = templates.find((template) => template.id === id);
1516
+ if (found === void 0) throw new ShipsealError("template.unknown", `Unknown template "${id}".`, `Use one of: ${templates.map((template) => template.id).join(", ")}.`);
1517
+ return found;
1518
+ }
1519
+
1520
+ //#endregion
1521
+ //#region src/core/generate.ts
1522
+ async function generate(input) {
1523
+ const templateIds = templateIdsFor(input.event, input.config);
1524
+ const formatIds = input.config.formats ?? [
1525
+ "og",
1526
+ "github-social",
1527
+ "x",
1528
+ "linkedin"
1529
+ ];
1530
+ const imageFormat = input.config.output?.imageFormat ?? "png";
1531
+ const attribution = input.config.attribution !== false;
1532
+ const files = [];
1533
+ const warnings = [];
1534
+ const missing = [];
1535
+ const recordedMissing = /* @__PURE__ */ new Set();
1536
+ const omitThemeSuffix = input.themes.length === 1;
1537
+ for (const templateId of templateIds) {
1538
+ const template = getTemplate(templateId);
1539
+ if (!template.events.includes(input.event.kind)) continue;
1540
+ for (const formatId of formatIds) {
1541
+ if (!template.formats.includes(formatId)) continue;
1542
+ const format = FORMATS[formatId];
1543
+ for (const theme of input.themes) {
1544
+ const copy = await copyForTemplate(template.id, input.copy, input.facts, theme);
1545
+ const built = template.buildProps(input.facts, copy, input.brand);
1546
+ const props = template.propsSchema.parse(built.props);
1547
+ if (!recordedMissing.has(template.id)) {
1548
+ recordedMissing.add(template.id);
1549
+ for (const item of built.missing) missing.push({
1550
+ template: template.id,
1551
+ fact: item.fact,
1552
+ effect: item.effect
1553
+ });
1554
+ }
1555
+ const { fitted, truncated } = await fitSlots(input.renderer, template, props, format);
1556
+ for (const slot of truncated) warnings.push({
1557
+ type: "fit-warning",
1558
+ template: template.id,
1559
+ format: formatId,
1560
+ slot,
1561
+ action: "truncated"
1562
+ });
1563
+ const logo = logoForTheme(input.logos, theme);
1564
+ const ctx = {
1565
+ format,
1566
+ theme,
1567
+ brand: input.brand,
1568
+ fitted,
1569
+ attribution
1570
+ };
1571
+ if (logo !== void 0) ctx.logoSrc = "shipseal-logo";
1572
+ const jsx = template.render(props, ctx);
1573
+ const node = await input.renderer.fromJsx(jsx);
1574
+ const renderOpts = {
1575
+ width: format.width,
1576
+ height: format.height,
1577
+ format: imageFormat
1578
+ };
1579
+ if (logo !== void 0) renderOpts.images = [{
1580
+ src: "shipseal-logo",
1581
+ data: logo
1582
+ }];
1583
+ const bytes = await input.renderer.render(node, renderOpts);
1584
+ const fileName = outputName(template.id, formatId, theme, omitThemeSuffix, imageFormat);
1585
+ files.push({
1586
+ fileName,
1587
+ template: template.id,
1588
+ format: formatId,
1589
+ theme,
1590
+ width: format.width,
1591
+ height: format.height,
1592
+ bytes,
1593
+ sha256: createHash("sha256").update(bytes).digest("hex")
1594
+ });
1595
+ }
1596
+ }
1597
+ }
1598
+ return {
1599
+ files,
1600
+ warnings,
1601
+ missing,
1602
+ facts: input.facts,
1603
+ copy: input.copy,
1604
+ copyMode: input.copyMode,
1605
+ generatedAt: input.generatedAt,
1606
+ computed: computedFromFacts(input.facts)
1607
+ };
1608
+ }
1609
+ function templateIdsFor(event, config) {
1610
+ if (event.kind === "milestone") return ["milestone"];
1611
+ if (event.kind === "bench") return ["bench"];
1612
+ return config.release?.templates ?? [
1613
+ "release-hero",
1614
+ "release-highlights",
1615
+ "code-card"
1616
+ ];
1617
+ }
1618
+ function computedFromFacts(facts) {
1619
+ const out = {};
1620
+ if (facts.bench === void 0) return out;
1621
+ facts.bench.metrics.forEach((metric, index) => {
1622
+ const change = percentChange(metric.before.value, metric.after.value, metric.better.value);
1623
+ const prefix = `bench.metrics[${String(index)}]`;
1624
+ out[`${prefix}.percent`] = {
1625
+ value: change.signed,
1626
+ computedFrom: [
1627
+ `${prefix}.before`,
1628
+ `${prefix}.after`,
1629
+ `${prefix}.better`
1630
+ ]
1631
+ };
1632
+ });
1633
+ return out;
1634
+ }
1635
+ async function fitSlots(renderer, template, props, format) {
1636
+ const specs = template.slots(format);
1637
+ const texts = template.slotText(props);
1638
+ const fitted = {};
1639
+ const truncated = [];
1640
+ for (const [slot, spec] of Object.entries(specs)) {
1641
+ const text = texts[slot];
1642
+ if (text === void 0) continue;
1643
+ const font = template.slotFont(props, slot);
1644
+ const fitFont = {
1645
+ family: font.family,
1646
+ lineHeight: font.lineHeight,
1647
+ weight: font.weight
1648
+ };
1649
+ if (font.whiteSpace !== void 0) fitFont.whiteSpace = font.whiteSpace;
1650
+ const result = await fitText(renderer, text, spec, fitFont);
1651
+ fitted[slot] = result;
1652
+ if (result.truncated) truncated.push(slot);
1653
+ }
1654
+ return {
1655
+ fitted,
1656
+ truncated
1657
+ };
1658
+ }
1659
+ function outputName(template, format, theme, omitTheme, imageFormat) {
1660
+ return `${template}-${format}${omitTheme ? "" : `-${theme}`}.${imageFormat}`;
1661
+ }
1662
+ function logoForTheme(logos, theme) {
1663
+ if (logos === void 0) return;
1664
+ if (theme === "dark") return logos.dark ?? logos.light;
1665
+ return logos.light ?? logos.dark;
1666
+ }
1667
+ async function copyForTemplate(templateId, copy, facts, theme) {
1668
+ if (templateId !== "code-card") return copy;
1669
+ const snippet = facts.release?.codeSnippet?.value;
1670
+ if (snippet === void 0) return {
1671
+ ...copy,
1672
+ codeLines: emptyCodeLines()
1673
+ };
1674
+ return {
1675
+ ...copy,
1676
+ codeLines: await highlightCode(snippet.code, snippet.lang, theme)
1677
+ };
1678
+ }
1679
+
1680
+ //#endregion
1681
+ //#region src/facts/merge.ts
1682
+ function mergeFacts(parts) {
1683
+ const project = {};
1684
+ const release = {};
1685
+ const metrics = {};
1686
+ const milestone = {};
1687
+ let bench;
1688
+ for (const part of parts) {
1689
+ fillObject(project, part.project);
1690
+ mergeRelease(release, part.release);
1691
+ fillObject(metrics, part.metrics);
1692
+ fillObject(milestone, part.milestone);
1693
+ if (part.bench !== void 0) bench = part.bench;
1694
+ }
1695
+ if (project.name === void 0) throw new ShipsealError("facts.missing-name", "Could not determine the project name.", "Add a name field to package.json, or an H1 in README.md.");
1696
+ const facts = { project: { name: project.name } };
1697
+ if (project.tagline !== void 0) facts.project.tagline = project.tagline;
1698
+ if (project.url !== void 0) facts.project.url = project.url;
1699
+ if (project.repo !== void 0) facts.project.repo = project.repo;
1700
+ if (project.npmPackage !== void 0) facts.project.npmPackage = project.npmPackage;
1701
+ if (project.license !== void 0) facts.project.license = project.license;
1702
+ if (isCompleteRelease(release)) {
1703
+ facts.release = {
1704
+ version: release.version,
1705
+ tag: release.tag,
1706
+ date: release.date,
1707
+ features: release.features ?? [],
1708
+ fixes: release.fixes ?? [],
1709
+ breaking: release.breaking ?? []
1710
+ };
1711
+ if (release.previousVersion !== void 0) facts.release.previousVersion = release.previousVersion;
1712
+ if (release.commitCount !== void 0) facts.release.commitCount = release.commitCount;
1713
+ if (release.contributors !== void 0) facts.release.contributors = release.contributors;
1714
+ if (release.codeSnippet !== void 0) facts.release.codeSnippet = release.codeSnippet;
1715
+ }
1716
+ if (Object.keys(metrics).length > 0) facts.metrics = metrics;
1717
+ if (bench !== void 0) facts.bench = bench;
1718
+ if (milestone.metric !== void 0 && milestone.threshold !== void 0) facts.milestone = {
1719
+ metric: milestone.metric,
1720
+ threshold: milestone.threshold
1721
+ };
1722
+ return facts;
1723
+ }
1724
+ function mergeRelease(target, overlay) {
1725
+ if (overlay === void 0) return;
1726
+ fillObject(target, overlay, [
1727
+ "features",
1728
+ "fixes",
1729
+ "breaking"
1730
+ ]);
1731
+ replaceList(target, overlay, "features");
1732
+ replaceList(target, overlay, "fixes");
1733
+ replaceList(target, overlay, "breaking");
1734
+ }
1735
+ function replaceList(target, overlay, key) {
1736
+ const next = overlay[key];
1737
+ if (next !== void 0) target[key] = next;
1738
+ }
1739
+ function fillObject(target, overlay, skip = []) {
1740
+ if (overlay === void 0) return;
1741
+ const skipped = new Set(skip);
1742
+ for (const key of Object.keys(overlay)) {
1743
+ if (skipped.has(key)) continue;
1744
+ const value = overlay[key];
1745
+ if (value !== void 0 && target[key] === void 0) target[key] = value;
1746
+ }
1747
+ }
1748
+ function isCompleteRelease(release) {
1749
+ return release.version !== void 0 && release.tag !== void 0 && release.date !== void 0;
1750
+ }
1751
+
1752
+ //#endregion
1753
+ //#region src/facts/fact.ts
1754
+ function fact(value, provenance) {
1755
+ const fetchedAt = provenance.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString();
1756
+ return {
1757
+ value,
1758
+ provenance: {
1759
+ source: provenance.source,
1760
+ ref: provenance.ref,
1761
+ fetchedAt
1762
+ }
1763
+ };
1764
+ }
1765
+
1766
+ //#endregion
1767
+ //#region src/sources/bench-file.ts
1768
+ const metricSchema = z.object({
1769
+ label: z.string().min(1),
1770
+ before: z.number().finite(),
1771
+ after: z.number().finite(),
1772
+ unit: z.string().min(1),
1773
+ better: z.enum(["lower", "higher"])
1774
+ });
1775
+ const benchFileSchema = z.object({
1776
+ title: z.string().min(1),
1777
+ metrics: z.array(metricSchema).min(1).max(3),
1778
+ note: z.string().optional()
1779
+ });
1780
+ async function collectBenchFile(cwd, filePath = ".shipseal/bench.json") {
1781
+ const resolved = isAbsolute(filePath) ? filePath : join(cwd, filePath);
1782
+ let raw;
1783
+ try {
1784
+ raw = await readFile(resolved, "utf8");
1785
+ } catch (error) {
1786
+ throw new ShipsealError("bench.missing-file", `Benchmark file not found: ${filePath}.`, "Write .shipseal/bench.json (title, metrics with before/after/unit/better), or pass --file <path>.", { cause: error });
1787
+ }
1788
+ let json;
1789
+ try {
1790
+ json = JSON.parse(raw);
1791
+ } catch (error) {
1792
+ throw new ShipsealError("bench.invalid-json", `Benchmark file is not valid JSON: ${filePath}.`, "Fix the JSON syntax, then retry.", { cause: error });
1793
+ }
1794
+ const parsed = benchFileSchema.safeParse(json);
1795
+ if (!parsed.success) throw new ShipsealError("bench.invalid-shape", `Benchmark file is missing required fields: ${filePath}.`, "Each metric needs label, before, after, unit, and better (\"lower\" or \"higher\"). At most 3 metrics.");
1796
+ const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
1797
+ const ref = filePath;
1798
+ const bench = {
1799
+ title: fact(parsed.data.title, {
1800
+ source: "bench-file",
1801
+ ref: `${ref}#title`,
1802
+ fetchedAt
1803
+ }),
1804
+ metrics: parsed.data.metrics.map((metric, index) => ({
1805
+ label: fact(metric.label, {
1806
+ source: "bench-file",
1807
+ ref: `${ref}#metrics[${String(index)}].label`,
1808
+ fetchedAt
1809
+ }),
1810
+ before: fact(metric.before, {
1811
+ source: "bench-file",
1812
+ ref: `${ref}#metrics[${String(index)}].before`,
1813
+ fetchedAt
1814
+ }),
1815
+ after: fact(metric.after, {
1816
+ source: "bench-file",
1817
+ ref: `${ref}#metrics[${String(index)}].after`,
1818
+ fetchedAt
1819
+ }),
1820
+ unit: fact(metric.unit, {
1821
+ source: "bench-file",
1822
+ ref: `${ref}#metrics[${String(index)}].unit`,
1823
+ fetchedAt
1824
+ }),
1825
+ better: fact(metric.better, {
1826
+ source: "bench-file",
1827
+ ref: `${ref}#metrics[${String(index)}].better`,
1828
+ fetchedAt
1829
+ })
1830
+ }))
1831
+ };
1832
+ if (parsed.data.note !== void 0) bench.note = fact(parsed.data.note, {
1833
+ source: "bench-file",
1834
+ ref: `${ref}#note`,
1835
+ fetchedAt
1836
+ });
1837
+ return { bench };
1838
+ }
1839
+
1840
+ //#endregion
1841
+ //#region src/sources/changelog.ts
1842
+ async function collectChangelog(cwd, version, changelogPath = "CHANGELOG.md") {
1843
+ if (version === void 0) return {};
1844
+ let markdown;
1845
+ try {
1846
+ markdown = await readFile(join(cwd, changelogPath), "utf8");
1847
+ } catch {
1848
+ return {};
1849
+ }
1850
+ const section = findVersionSection(markdown, version);
1851
+ if (section === void 0) return {};
1852
+ const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
1853
+ const features = [];
1854
+ const fixes = [];
1855
+ const breaking = [];
1856
+ for (const group of parseGroups(section.body)) {
1857
+ const bucket = bucketForHeading(group.heading);
1858
+ if (bucket === void 0) continue;
1859
+ for (const item of group.items) {
1860
+ const cleaned = cleanChangelogItem(item);
1861
+ if (cleaned.length === 0) continue;
1862
+ const entry = fact(cleaned, {
1863
+ source: "changelog",
1864
+ ref: `${changelogPath} ${section.heading} ${group.heading}: ${item}`,
1865
+ fetchedAt
1866
+ });
1867
+ if (bucket === "features") features.push(entry);
1868
+ else if (bucket === "fixes") fixes.push(entry);
1869
+ else breaking.push(entry);
1870
+ }
1871
+ }
1872
+ const out = { release: {
1873
+ version: fact(version.replace(/^v/, ""), {
1874
+ source: "changelog",
1875
+ ref: `${changelogPath} ${section.heading}`,
1876
+ fetchedAt
1877
+ }),
1878
+ tag: fact(version.startsWith("v") ? version : `v${version}`, {
1879
+ source: "changelog",
1880
+ ref: `${changelogPath} ${section.heading}`,
1881
+ fetchedAt
1882
+ }),
1883
+ features,
1884
+ fixes,
1885
+ breaking
1886
+ } };
1887
+ if (section.date !== void 0) out.release = {
1888
+ ...out.release,
1889
+ date: fact(section.date, {
1890
+ source: "changelog",
1891
+ ref: `${changelogPath} ${section.heading}`,
1892
+ fetchedAt
1893
+ })
1894
+ };
1895
+ return out;
1896
+ }
1897
+ function findVersionSection(markdown, version) {
1898
+ const needle = version.replace(/^v/, "");
1899
+ const matches = [...markdown.matchAll(/^##\s+(.+)$/gm)];
1900
+ for (let index = 0; index < matches.length; index += 1) {
1901
+ const match = matches[index];
1902
+ if (match === void 0) continue;
1903
+ const heading = match[1];
1904
+ if (heading === void 0 || !headingIncludesVersion(heading, needle)) continue;
1905
+ const start = (match.index ?? 0) + match[0].length;
1906
+ const end = matches[index + 1]?.index ?? markdown.length;
1907
+ const date = extractDate(heading);
1908
+ const body = markdown.slice(start, end);
1909
+ if (date === void 0) return {
1910
+ heading,
1911
+ body
1912
+ };
1913
+ return {
1914
+ heading,
1915
+ body,
1916
+ date
1917
+ };
1918
+ }
1919
+ }
1920
+ function cleanChangelogItem(item) {
1921
+ return item.replace(/^\s*[-*]\s*/, "").replace(/\[`[a-f0-9]{7,40}`]\([^)]+\)/gi, "").replace(/^[a-f0-9]{7,40}:\s*/i, "").replace(/\(#\d+\)/g, "").replace(/\[#\d+]\([^)]+\)/g, "").replace(/Thanks\s+\[@[\w-]+]\([^)]+\)!?\s*-?\s*/gi, "").replace(/\[@[\w-]+]\([^)]+\)/g, "").replace(/\(@[\w-]+\)/g, "").replace(/\s+by\s+@[\w-]+/gi, "").replace(/@[\w-]+/g, "").replace(/^Thanks\s*!?\s*-?\s*/i, "").replace(/^\s*[-*]\s*/, "").replace(/\s+/g, " ").trim().replace(/\.$/, "");
1922
+ }
1923
+ function headingIncludesVersion(heading, version) {
1924
+ const unwrapped = heading.replace(/[[\]]/g, " ");
1925
+ return new RegExp(`(?:^|\\s)v?${escapeRegExp(version)}(?:\\s|$)`).test(unwrapped);
1926
+ }
1927
+ function extractDate(heading) {
1928
+ return /(\d{4}-\d{2}-\d{2})/.exec(heading)?.[1];
1929
+ }
1930
+ function parseGroups(body) {
1931
+ const groups = [];
1932
+ const matches = [...body.matchAll(/^###\s+(.+)$/gm)];
1933
+ if (matches.length === 0) return [{
1934
+ heading: "Added",
1935
+ items: listItems(body)
1936
+ }];
1937
+ for (let index = 0; index < matches.length; index += 1) {
1938
+ const match = matches[index];
1939
+ if (match === void 0) continue;
1940
+ const heading = match[1];
1941
+ if (heading === void 0) continue;
1942
+ const start = (match.index ?? 0) + match[0].length;
1943
+ const end = matches[index + 1]?.index ?? body.length;
1944
+ groups.push({
1945
+ heading,
1946
+ items: listItems(body.slice(start, end))
1947
+ });
1948
+ }
1949
+ return groups;
1950
+ }
1951
+ function listItems(block) {
1952
+ const items = [];
1953
+ let current;
1954
+ for (const raw of block.split("\n")) {
1955
+ const trimmed = raw.trim();
1956
+ if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
1957
+ if (current !== void 0) items.push(current);
1958
+ current = trimmed;
1959
+ continue;
1960
+ }
1961
+ if (current !== void 0 && trimmed.length > 0 && !trimmed.startsWith("#")) current = `${current} ${trimmed}`;
1962
+ }
1963
+ if (current !== void 0) items.push(current);
1964
+ return items;
1965
+ }
1966
+ function bucketForHeading(heading) {
1967
+ const key = heading.trim().toLowerCase();
1968
+ if (key === "added" || key === "changed" || key === "minor changes" || key === "features") return "features";
1969
+ if (key === "fixed" || key === "patch changes" || key === "security" || key === "fixes") return "fixes";
1970
+ if (key === "removed" || key === "major changes" || key === "breaking" || key === "breaking changes") return "breaking";
1971
+ }
1972
+ function escapeRegExp(value) {
1973
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1974
+ }
1975
+
1976
+ //#endregion
1977
+ //#region src/sources/git.ts
1978
+ const execFileAsync$3 = promisify(execFile);
1979
+ async function collectGit(cwd, event) {
1980
+ if (await git(cwd, ["rev-parse", "--is-inside-work-tree"]) !== "true") return {};
1981
+ const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
1982
+ const tag = event.tag ?? await gitCurrentTag(cwd);
1983
+ if (tag === void 0) return {};
1984
+ if (await git(cwd, [
1985
+ "rev-parse",
1986
+ "--verify",
1987
+ `${tag}^{commit}`
1988
+ ]) === void 0) return {};
1989
+ const version = versionFromTag(tag);
1990
+ const tags = await gitLines(cwd, ["tag", "--sort=-v:refname"]);
1991
+ const tagIndex = tags.indexOf(tag);
1992
+ const previousTag = event.previousTag ?? (tagIndex >= 0 ? tags[tagIndex + 1] : tags.find((candidate) => candidate !== tag));
1993
+ const range = previousTag === void 0 ? tag : `${previousTag}..${tag}`;
1994
+ const commits = (await git(cwd, [
1995
+ "log",
1996
+ range,
1997
+ "--pretty=format:%s %an %ae %h %cI"
1998
+ ]) ?? "").split("\n").filter((line) => line.length > 0);
1999
+ const features = [];
2000
+ const fixes = [];
2001
+ const breaking = [];
2002
+ const contributors = /* @__PURE__ */ new Map();
2003
+ for (const line of commits) {
2004
+ const [subject, author, email, hash] = line.split(" ");
2005
+ if (subject === void 0 || hash === void 0) continue;
2006
+ const parsed = parseConventional(subject);
2007
+ const display = parsed.display;
2008
+ const ref = `git log ${range} ${hash}`;
2009
+ const item = fact(display, {
2010
+ source: "git",
2011
+ ref,
2012
+ fetchedAt
2013
+ });
2014
+ if (parsed.breaking) breaking.push(item);
2015
+ else if (parsed.type === "feat") features.push(item);
2016
+ else if (parsed.type === "fix" || parsed.type === "perf") fixes.push(item);
2017
+ if (email !== void 0 && author !== void 0 && !isBot(author, email)) contributors.set(email, author);
2018
+ }
2019
+ const date = (await git(cwd, [
2020
+ "log",
2021
+ "-1",
2022
+ "--format=%cI",
2023
+ tag
2024
+ ]) ?? fetchedAt).slice(0, 10);
2025
+ const names = [...contributors.values()];
2026
+ const release = {
2027
+ version: fact(version, {
2028
+ source: "git",
2029
+ ref: `git tag ${tag}`,
2030
+ fetchedAt
2031
+ }),
2032
+ tag: fact(tag, {
2033
+ source: "git",
2034
+ ref: `git tag ${tag}`,
2035
+ fetchedAt
2036
+ }),
2037
+ date: fact(date, {
2038
+ source: "git",
2039
+ ref: `git log -1 --format=%cI ${tag}`,
2040
+ fetchedAt
2041
+ }),
2042
+ features,
2043
+ fixes,
2044
+ breaking,
2045
+ commitCount: fact(commits.length, {
2046
+ source: "git",
2047
+ ref: `git rev-list --count ${range}`,
2048
+ fetchedAt
2049
+ })
2050
+ };
2051
+ if (previousTag !== void 0) release.previousVersion = fact(versionFromTag(previousTag), {
2052
+ source: "git",
2053
+ ref: `git tag ${previousTag}`,
2054
+ fetchedAt
2055
+ });
2056
+ if (names.length > 0) release.contributors = fact(names, {
2057
+ source: "git",
2058
+ ref: `git log ${range} unique authors`,
2059
+ fetchedAt
2060
+ });
2061
+ return { release };
2062
+ }
2063
+ function parseConventional(subject) {
2064
+ const match = /^(?<type>\w+)(?<scope>\([^)]+\))?(?<bang>!)?:\s*(?<rest>.+)$/.exec(subject);
2065
+ if (match === null || match.groups === void 0) return {
2066
+ type: void 0,
2067
+ display: displaySubject(subject),
2068
+ breaking: false
2069
+ };
2070
+ const type = match.groups.type;
2071
+ const rest = match.groups.rest ?? subject;
2072
+ const breaking = match.groups.bang === "!" || /BREAKING CHANGE:/.test(subject);
2073
+ return {
2074
+ type,
2075
+ display: displaySubject(rest),
2076
+ breaking
2077
+ };
2078
+ }
2079
+ function versionFromTag(tag) {
2080
+ const at = tag.lastIndexOf("@");
2081
+ if (at >= 0 && at < tag.length - 1) return tag.slice(at + 1).replace(/^v/, "");
2082
+ return tag.replace(/^v/, "");
2083
+ }
2084
+ async function gitCurrentTag(cwd) {
2085
+ return git(cwd, [
2086
+ "describe",
2087
+ "--tags",
2088
+ "--abbrev=0"
2089
+ ]);
2090
+ }
2091
+ async function git(cwd, args) {
2092
+ try {
2093
+ const { stdout } = await execFileAsync$3("git", args, { cwd });
2094
+ const trimmed = stdout.trim();
2095
+ return trimmed.length === 0 ? void 0 : trimmed;
2096
+ } catch {
2097
+ return;
2098
+ }
2099
+ }
2100
+ async function gitLines(cwd, args) {
2101
+ const out = await git(cwd, args);
2102
+ return out === void 0 ? [] : out.split("\n").filter((line) => line.length > 0);
2103
+ }
2104
+ function isBot(author, email) {
2105
+ const haystack = `${author} ${email}`.toLowerCase();
2106
+ return author.endsWith("[bot]") || haystack.includes("dependabot") || haystack.includes("renovate") || haystack.includes("github-actions");
2107
+ }
2108
+ function displaySubject(text) {
2109
+ const cleaned = cleanChangelogItem(text);
2110
+ const first = cleaned.at(0);
2111
+ if (first === void 0) return cleaned;
2112
+ return first.toUpperCase() + cleaned.slice(1);
2113
+ }
2114
+
2115
+ //#endregion
2116
+ //#region src/sources/github.ts
2117
+ const API_VERSION = "2022-11-28";
2118
+ const API_ROOT$1 = "https://api.github.com";
2119
+ const repoSchema = z.object({
2120
+ stargazers_count: z.number().int().nonnegative(),
2121
+ description: z.string().nullable().optional(),
2122
+ homepage: z.string().nullable().optional(),
2123
+ license: z.object({ spdx_id: z.string().nullable() }).nullable().optional()
2124
+ });
2125
+ const releaseSchema = z.object({ body: z.string().nullable().optional() });
2126
+ const contributorsSchema = z.array(z.unknown());
2127
+ async function collectGithub(options) {
2128
+ const slug = parseRepo(options.repo);
2129
+ if (slug === void 0) return {};
2130
+ const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
2131
+ const fetchImpl = options.fetchImpl ?? fetch;
2132
+ const cache = options.cache ?? /* @__PURE__ */ new Map();
2133
+ const repoResponse = await githubGet(`${API_ROOT$1}/repos/${slug.owner}/${slug.repo}`, options.token, fetchImpl, cache);
2134
+ if (repoResponse === void 0) return {};
2135
+ const repo = repoSchema.parse(repoResponse.json);
2136
+ const out = {
2137
+ metrics: { stars: fact(repo.stargazers_count, {
2138
+ source: "github-api",
2139
+ ref: `GET /repos/${slug.owner}/${slug.repo} stargazers_count`,
2140
+ fetchedAt
2141
+ }) },
2142
+ project: {}
2143
+ };
2144
+ if (repo.description !== void 0 && repo.description !== null && repo.description.length > 0) out.project = {
2145
+ ...out.project,
2146
+ tagline: fact(repo.description, {
2147
+ source: "github-api",
2148
+ ref: `GET /repos/${slug.owner}/${slug.repo} description`,
2149
+ fetchedAt
2150
+ })
2151
+ };
2152
+ if (repo.homepage !== void 0 && repo.homepage !== null && repo.homepage.length > 0) out.project = {
2153
+ ...out.project,
2154
+ url: fact(repo.homepage, {
2155
+ source: "github-api",
2156
+ ref: `GET /repos/${slug.owner}/${slug.repo} homepage`,
2157
+ fetchedAt
2158
+ })
2159
+ };
2160
+ const license = repo.license?.spdx_id;
2161
+ if (license !== void 0 && license !== null && license !== "NOASSERTION") out.project = {
2162
+ ...out.project,
2163
+ license: fact(license, {
2164
+ source: "github-api",
2165
+ ref: `GET /repos/${slug.owner}/${slug.repo} license.spdx_id`,
2166
+ fetchedAt
2167
+ })
2168
+ };
2169
+ if (options.tag !== void 0) {
2170
+ const releaseResponse = await githubGet(`${API_ROOT$1}/repos/${slug.owner}/${slug.repo}/releases/tags/${encodeURIComponent(options.tag)}`, options.token, fetchImpl, cache, { notFoundOk: true });
2171
+ if (releaseResponse !== void 0) {
2172
+ const features = featuresFromReleaseBody(releaseSchema.parse(releaseResponse.json).body ?? "", options.tag, fetchedAt);
2173
+ if (features.length > 0) out.release = { features };
2174
+ }
2175
+ }
2176
+ const contributors = await githubGet(`${API_ROOT$1}/repos/${slug.owner}/${slug.repo}/contributors?per_page=1&anon=true`, options.token, fetchImpl, cache, { notFoundOk: true });
2177
+ if (contributors !== void 0) {
2178
+ contributorsSchema.parse(contributors.json);
2179
+ const listed = Array.isArray(contributors.json) ? contributors.json.length : 0;
2180
+ const fromLink = lastPageFromLink(contributors.link);
2181
+ out.metrics = {
2182
+ ...out.metrics,
2183
+ contributorCount: fact(fromLink ?? listed, {
2184
+ source: "github-api",
2185
+ ref: `GET /repos/${slug.owner}/${slug.repo}/contributors`,
2186
+ fetchedAt
2187
+ })
2188
+ };
2189
+ }
2190
+ return out;
2191
+ }
2192
+ function parseRepo(value) {
2193
+ const trimmed = value.trim().replace(/\.git$/, "");
2194
+ const match = /^(?:https?:\/\/github\.com\/)?([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(trimmed);
2195
+ if (match === null || match[1] === void 0 || match[2] === void 0) return;
2196
+ return {
2197
+ owner: match[1],
2198
+ repo: match[2]
2199
+ };
2200
+ }
2201
+ function featuresFromReleaseBody(body, tag, fetchedAt) {
2202
+ return body.split("\n").map((line) => line.replace(/^\s*[-*]\s*/, "").trim()).filter((line) => line.length > 0 && !line.startsWith("#")).slice(0, 8).map((line) => fact(line.replace(/\.$/, ""), {
2203
+ source: "github-api",
2204
+ ref: `GET /repos/.../releases/tags/${tag} body`,
2205
+ fetchedAt
2206
+ }));
2207
+ }
2208
+ async function githubGet(url, token, fetchImpl, cache, opts = {}) {
2209
+ const cached = cache.get(url);
2210
+ if (cached !== void 0) return cached;
2211
+ const headers = {
2212
+ Accept: "application/vnd.github+json",
2213
+ "X-GitHub-Api-Version": API_VERSION,
2214
+ "User-Agent": "shipseal"
2215
+ };
2216
+ if (token !== void 0 && token.length > 0) headers.Authorization = `Bearer ${token}`;
2217
+ let response;
2218
+ try {
2219
+ response = await fetchImpl(url, { headers });
2220
+ } catch (error) {
2221
+ throw new ShipsealError("github.network", "Could not reach the GitHub API.", "Check network access, then retry. Unauthenticated calls are optional; facts still come from git and package.json.", { cause: error });
2222
+ }
2223
+ if (response.status === 404 && opts.notFoundOk === true) return;
2224
+ if (response.status === 403 || response.status === 429) throw new ShipsealError("github.rate-limit", "GitHub API rate limit exceeded.", "Set GITHUB_TOKEN in the environment to raise the limit, then retry.");
2225
+ if (!response.ok) throw new ShipsealError("github.http", `GitHub API returned HTTP ${String(response.status)} for ${url}.`, "Confirm the repository exists and GITHUB_TOKEN can read it.");
2226
+ const entry = {
2227
+ json: await response.json(),
2228
+ link: response.headers.get("link")
2229
+ };
2230
+ cache.set(url, entry);
2231
+ return entry;
2232
+ }
2233
+ function lastPageFromLink(link) {
2234
+ if (link === null) return;
2235
+ const match = /[?&]page=(\d+)>;\s*rel="last"/.exec(link);
2236
+ if (match === null || match[1] === void 0) return;
2237
+ return Number.parseInt(match[1], 10);
2238
+ }
2239
+
2240
+ //#endregion
2241
+ //#region src/sources/npm.ts
2242
+ const API_ROOT = "https://api.npmjs.org/downloads/point/last-week";
2243
+ const pointSchema = z.object({
2244
+ downloads: z.number().int().nonnegative(),
2245
+ start: z.string().optional(),
2246
+ end: z.string().optional(),
2247
+ package: z.string().optional()
2248
+ });
2249
+ async function collectNpm(options) {
2250
+ const name = options.npmPackage.trim();
2251
+ if (name.length === 0) return {};
2252
+ const url = `${API_ROOT}/${encodeURIComponent(name)}`;
2253
+ const cache = options.cache ?? /* @__PURE__ */ new Map();
2254
+ const json = cache.get(url) ?? await npmGet(url, options.fetchImpl ?? fetch, cache);
2255
+ if (json === void 0) return {};
2256
+ const point = pointSchema.parse(json);
2257
+ return { metrics: { weeklyDownloads: fact(point.downloads, {
2258
+ source: "npm-api",
2259
+ ref: `GET /downloads/point/last-week/${name} downloads`,
2260
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2261
+ }) } };
2262
+ }
2263
+ async function npmGet(url, fetchImpl, cache) {
2264
+ let response;
2265
+ try {
2266
+ response = await fetchImpl(url, { headers: {
2267
+ Accept: "application/json",
2268
+ "User-Agent": "shipseal"
2269
+ } });
2270
+ } catch (error) {
2271
+ throw new ShipsealError("npm.network", "Could not reach the npm downloads API.", "Check network access, then retry. Weekly downloads are optional; other facts still render.", { cause: error });
2272
+ }
2273
+ if (response.status === 404) return;
2274
+ if (response.status === 403 || response.status === 429) throw new ShipsealError("npm.rate-limit", "npm downloads API rate limit exceeded.", "Wait and retry. Milestone cards can still use stars and contributors without downloads.");
2275
+ if (!response.ok) throw new ShipsealError("npm.http", `npm downloads API returned HTTP ${String(response.status)} for ${url}.`, "Confirm the package name in package.json is published on npm.");
2276
+ const json = await response.json();
2277
+ cache.set(url, json);
2278
+ return json;
2279
+ }
2280
+
2281
+ //#endregion
2282
+ //#region src/sources/package-json.ts
2283
+ const pkgSchema = z.object({
2284
+ name: z.string().optional(),
2285
+ description: z.string().optional(),
2286
+ version: z.string().optional(),
2287
+ homepage: z.string().optional(),
2288
+ license: z.string().optional(),
2289
+ repository: z.union([z.string(), z.object({ url: z.string().optional() })]).optional()
2290
+ });
2291
+ async function collectPackageJson(cwd, packagePath = "package.json") {
2292
+ const path = join(cwd, packagePath);
2293
+ let raw;
2294
+ try {
2295
+ raw = await readFile(path, "utf8");
2296
+ } catch {
2297
+ return {};
2298
+ }
2299
+ let parsed;
2300
+ try {
2301
+ parsed = JSON.parse(raw);
2302
+ } catch {
2303
+ return {};
2304
+ }
2305
+ const pkg = pkgSchema.safeParse(parsed);
2306
+ if (!pkg.success) return {};
2307
+ const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
2308
+ const project = { name: fact(stripScope$1(pkg.data.name ?? basenameFromPath(cwd)), {
2309
+ source: "package-json",
2310
+ ref: `${packagePath}#name`,
2311
+ fetchedAt
2312
+ }) };
2313
+ if (pkg.data.description !== void 0 && pkg.data.description.length > 0) project.tagline = fact(pkg.data.description, {
2314
+ source: "package-json",
2315
+ ref: `${packagePath}#description`,
2316
+ fetchedAt
2317
+ });
2318
+ const url = pkg.data.homepage ?? repositoryUrl$1(pkg.data.repository);
2319
+ if (url !== void 0) project.url = fact(url, {
2320
+ source: "package-json",
2321
+ ref: pkg.data.homepage !== void 0 ? `${packagePath}#homepage` : `${packagePath}#repository`,
2322
+ fetchedAt
2323
+ });
2324
+ const repo = repoSlug(pkg.data.repository);
2325
+ if (repo !== void 0) project.repo = fact(repo, {
2326
+ source: "package-json",
2327
+ ref: `${packagePath}#repository`,
2328
+ fetchedAt
2329
+ });
2330
+ if (pkg.data.name !== void 0) project.npmPackage = fact(pkg.data.name, {
2331
+ source: "package-json",
2332
+ ref: `${packagePath}#name`,
2333
+ fetchedAt
2334
+ });
2335
+ if (pkg.data.license !== void 0) project.license = fact(pkg.data.license, {
2336
+ source: "package-json",
2337
+ ref: `${packagePath}#license`,
2338
+ fetchedAt
2339
+ });
2340
+ const out = { project };
2341
+ if (pkg.data.version !== void 0) out.release = { version: fact(pkg.data.version.replace(/^v/, ""), {
2342
+ source: "package-json",
2343
+ ref: `${packagePath}#version`,
2344
+ fetchedAt
2345
+ }) };
2346
+ return out;
2347
+ }
2348
+ function stripScope$1(name) {
2349
+ const parts = name.split("/");
2350
+ return parts[parts.length - 1] ?? name;
2351
+ }
2352
+ function basenameFromPath(cwd) {
2353
+ const parts = cwd.split(/[/\\]/).filter((part) => part.length > 0);
2354
+ return parts[parts.length - 1] ?? "project";
2355
+ }
2356
+ function repositoryUrl$1(repository) {
2357
+ if (typeof repository === "string") return normalizeGitUrl$1(repository);
2358
+ if (repository?.url !== void 0) return normalizeGitUrl$1(repository.url);
2359
+ }
2360
+ function repoSlug(repository) {
2361
+ const url = repositoryUrl$1(repository);
2362
+ if (url === void 0) return;
2363
+ return /github\.com[/:]([^/]+\/[^/]+)$/.exec(url.replace(/\.git$/, ""))?.[1];
2364
+ }
2365
+ const SHORTHAND = /^(?:github:)?([\w.-]+\/[\w.-]+)$/;
2366
+ function normalizeGitUrl$1(url) {
2367
+ const shorthand = SHORTHAND.exec(url);
2368
+ if (shorthand?.[1] !== void 0) return `https://github.com/${shorthand[1].replace(/\.git$/, "")}`;
2369
+ const ssh = /^git@([^:]+):(.+)$/.exec(url);
2370
+ if (ssh !== null && ssh[1] !== void 0 && ssh[2] !== void 0) return `https://${ssh[1]}/${ssh[2].replace(/\.git$/, "")}`;
2371
+ return url.replace(/^git\+/, "").replace(/\.git$/, "");
2372
+ }
2373
+
2374
+ //#endregion
2375
+ //#region src/sources/readme.ts
2376
+ async function collectReadme(cwd) {
2377
+ let markdown;
2378
+ try {
2379
+ markdown = await readFile(join(cwd, "README.md"), "utf8");
2380
+ } catch {
2381
+ return {};
2382
+ }
2383
+ const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
2384
+ const out = {};
2385
+ const h1 = extractH1(markdown);
2386
+ const tagline = extractTagline(markdown);
2387
+ const snippet = extractFirstCodeFence(markdown);
2388
+ const project = {};
2389
+ if (h1 !== void 0) project.name = fact(h1, {
2390
+ source: "readme",
2391
+ ref: "README.md H1",
2392
+ fetchedAt
2393
+ });
2394
+ if (tagline !== void 0) project.tagline = fact(tagline, {
2395
+ source: "readme",
2396
+ ref: "README.md first paragraph",
2397
+ fetchedAt
2398
+ });
2399
+ if (project.name !== void 0 || project.tagline !== void 0) out.project = project;
2400
+ if (snippet !== void 0) out.release = { codeSnippet: fact(snippet, {
2401
+ source: "readme",
2402
+ ref: "README.md first fenced code block",
2403
+ fetchedAt
2404
+ }) };
2405
+ return out;
2406
+ }
2407
+ async function collectConfiguredSnippet(cwd, spec) {
2408
+ const parsed = parseSnippetSpec(spec);
2409
+ if (parsed === void 0) return {};
2410
+ let contents;
2411
+ try {
2412
+ contents = await readFile(join(cwd, parsed.path), "utf8");
2413
+ } catch {
2414
+ return {};
2415
+ }
2416
+ const slice = contents.split(/\r?\n/).slice(parsed.start - 1, parsed.end).join("\n");
2417
+ const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
2418
+ return { release: { codeSnippet: fact({
2419
+ code: slice,
2420
+ lang: langFromPath(parsed.path)
2421
+ }, {
2422
+ source: "user-config",
2423
+ ref: spec,
2424
+ fetchedAt
2425
+ }) } };
2426
+ }
2427
+ /**
2428
+ * Strip fenced code and HTML before reading prose out of a README.
2429
+ *
2430
+ * Skipping blocks that merely *start* with a fence is not enough. A `#` comment inside a YAML
2431
+ * example reads as a markdown H1, and a blank line inside a fence makes its body look like a
2432
+ * paragraph. Shipseal's own README hit both: the detected name became
2433
+ * ".github/workflows/shipseal.yml" and the tagline became a chunk of workflow YAML.
2434
+ */
2435
+ function stripNonProse(markdown) {
2436
+ return markdown.replace(/```[\s\S]*?```/g, "").replace(/~~~[\s\S]*?~~~/g, "").replace(/<[^>]+>/g, "");
2437
+ }
2438
+ function extractH1(markdown) {
2439
+ const heading = /^#\s+(.+)$/m.exec(stripNonProse(markdown))?.[1];
2440
+ if (heading === void 0) return;
2441
+ return heading.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim();
2442
+ }
2443
+ function extractTagline(markdown) {
2444
+ const prose = stripNonProse(markdown);
2445
+ const h1 = /^#\s+.+$/m.exec(prose);
2446
+ const start = h1 === null ? 0 : (h1.index ?? 0) + h1[0].length;
2447
+ for (const block of prose.slice(start).split(/\n\s*\n/)) {
2448
+ const cleaned = stripBadges(block).trim();
2449
+ if (cleaned.length >= 20 && !cleaned.startsWith("#") && !cleaned.startsWith("```")) return cleaned;
2450
+ }
2451
+ }
2452
+ function extractFirstCodeFence(markdown) {
2453
+ const match = /```([a-zA-Z0-9+-]+)\n([\s\S]*?)```/.exec(markdown);
2454
+ if (match === null || match[1] === void 0 || match[2] === void 0) return;
2455
+ return {
2456
+ lang: match[1],
2457
+ code: match[2].replace(/\n$/, "")
2458
+ };
2459
+ }
2460
+ function parseSnippetSpec(spec) {
2461
+ const match = /^(?<path>.+)#L(?<start>\d+)(?:-L(?<end>\d+))?$/.exec(spec);
2462
+ if (match === null || match.groups === void 0) return;
2463
+ const path = match.groups.path;
2464
+ const start = Number.parseInt(match.groups.start ?? "0", 10);
2465
+ const end = Number.parseInt(match.groups.end ?? match.groups.start ?? "0", 10);
2466
+ if (path === void 0 || start < 1 || end < start) return;
2467
+ return {
2468
+ path,
2469
+ start,
2470
+ end
2471
+ };
2472
+ }
2473
+ function langFromPath(path) {
2474
+ const ext = extname(path).replace(".", "");
2475
+ if (ext === "ts") return "ts";
2476
+ if (ext === "tsx") return "tsx";
2477
+ if (ext === "js" || ext === "mjs" || ext === "cjs") return "js";
2478
+ if (ext === "jsx") return "jsx";
2479
+ return ext.length > 0 ? ext : "text";
2480
+ }
2481
+ function stripBadges(text) {
2482
+ return text.replace(/\[!\[[^\]]*]\([^)]+\)]\([^)]+\)/g, "").replace(/!\[[^\]]*]\([^)]+\)/g, "").replace(/<img[^>]*>/gi, "").replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
2483
+ }
2484
+
2485
+ //#endregion
2486
+ //#region src/sources/collect.ts
2487
+ async function collectFacts(options) {
2488
+ const pkg = await collectPackageJson(options.cwd, options.packagePath ?? "package.json");
2489
+ const tag = options.event.kind === "release" ? options.event.tag : void 0;
2490
+ const previousTag = options.event.kind === "release" ? options.event.previousTag : void 0;
2491
+ const gitEvent = {};
2492
+ if (tag !== void 0) gitEvent.tag = tag;
2493
+ if (previousTag !== void 0) gitEvent.previousTag = previousTag;
2494
+ const git = await collectGit(options.cwd, gitEvent);
2495
+ const version = git.release?.version?.value ?? pkg.release?.version?.value ?? (tag === void 0 ? void 0 : versionFromTag(tag));
2496
+ const changelog = await collectBestChangelog(options.cwd, version, options.changelogPath ?? "CHANGELOG.md", options.packagePath);
2497
+ const readme = await collectReadme(options.cwd);
2498
+ const parts = [
2499
+ pkg,
2500
+ git,
2501
+ readme,
2502
+ changelog,
2503
+ options.snippet !== void 0 && options.snippet !== null && options.snippet.length > 0 ? await collectConfiguredSnippet(options.cwd, options.snippet) : {}
2504
+ ];
2505
+ if (options.event.kind === "bench") parts.push(await collectBenchFile(options.cwd, options.benchFile ?? ".shipseal/bench.json"));
2506
+ if (options.skipNetwork !== true) {
2507
+ const repo = pkg.project?.repo?.value ?? gitRepoFromParts(pkg, readme);
2508
+ if (repo !== void 0) {
2509
+ const githubOpts = { repo };
2510
+ const githubTag = git.release?.tag?.value ?? tag;
2511
+ if (githubTag !== void 0) githubOpts.tag = githubTag;
2512
+ if (options.githubToken !== void 0) githubOpts.token = options.githubToken;
2513
+ if (options.fetchImpl !== void 0) githubOpts.fetchImpl = options.fetchImpl;
2514
+ const github = await collectGithub(githubOpts);
2515
+ const githubWithoutFeatures = {};
2516
+ if (github.project !== void 0) githubWithoutFeatures.project = github.project;
2517
+ if (github.metrics !== void 0) githubWithoutFeatures.metrics = github.metrics;
2518
+ parts.push(githubWithoutFeatures);
2519
+ const mergedPreview = mergeFacts(parts);
2520
+ if (options.event.kind === "release" && (mergedPreview.release?.features.length ?? 0) === 0 && github.release?.features !== void 0 && github.release.features.length > 0) parts.push({ release: { features: github.release.features } });
2521
+ }
2522
+ const npmPackage = pkg.project?.npmPackage?.value;
2523
+ if (npmPackage !== void 0) {
2524
+ const npmOpts = { npmPackage };
2525
+ if (options.fetchImpl !== void 0) npmOpts.fetchImpl = options.fetchImpl;
2526
+ parts.push(await collectNpm(npmOpts));
2527
+ }
2528
+ }
2529
+ const facts = mergeFacts(parts);
2530
+ if (options.event.kind === "release" && facts.release === void 0) throw new ShipsealError("facts.missing-release", "Could not collect release facts.", "Create a git tag, pass --tag, or add a matching version section to CHANGELOG.md.");
2531
+ return facts;
2532
+ }
2533
+ async function collectBestChangelog(cwd, version, changelogPath, packagePath) {
2534
+ const paths = [changelogPath];
2535
+ if (packagePath !== void 0) {
2536
+ const sibling = join(dirname(packagePath), "CHANGELOG.md");
2537
+ if (!paths.includes(sibling)) paths.push(sibling);
2538
+ }
2539
+ return (await Promise.all(paths.map((path) => collectChangelog(cwd, version, path)))).find((facts) => changelogHasNotes(facts)) ?? {};
2540
+ }
2541
+ function changelogHasNotes(facts) {
2542
+ const release = facts.release;
2543
+ if (release === void 0) return false;
2544
+ return (release.features?.length ?? 0) > 0 || (release.fixes?.length ?? 0) > 0 || (release.breaking?.length ?? 0) > 0;
2545
+ }
2546
+ function gitRepoFromParts(pkg, readme) {
2547
+ return pkg.project?.repo?.value ?? pkg.project?.url?.value ?? readme.project?.url?.value;
2548
+ }
2549
+
2550
+ //#endregion
2551
+ //#region src/render/takumi.ts
2552
+ const GEIST_MONO_FAMILY = "Geist Mono";
2553
+ const FONT_REL = join("assets", "fonts", "GeistMono[wght].ttf");
2554
+ async function createTakumiRenderer() {
2555
+ const renderer = new Renderer();
2556
+ const fontPath = join(resolvePackageRoot(), FONT_REL);
2557
+ const data = await readFile(fontPath);
2558
+ await renderer.registerFont({
2559
+ name: GEIST_MONO_FAMILY,
2560
+ data,
2561
+ generic: "monospace"
2562
+ });
2563
+ return new TakumiRenderer(renderer);
2564
+ }
2565
+ function resolvePackageRoot(from = import.meta.url) {
2566
+ let dir = dirname(fileURLToPath(from));
2567
+ for (;;) {
2568
+ if (existsSync(join(dir, "package.json")) && existsSync(join(dir, FONT_REL))) return dir;
2569
+ const parent = dirname(dir);
2570
+ if (parent === dir) throw new ShipsealError("render.font-missing", `Could not find vendored font ${FONT_REL}.`, "Reinstall shipseal so assets/fonts/GeistMono[wght].ttf is present next to package.json.");
2571
+ dir = parent;
2572
+ }
2573
+ }
2574
+ function countLines(node) {
2575
+ const ys = /* @__PURE__ */ new Set();
2576
+ const walk = (current) => {
2577
+ for (const run of current.runs) ys.add(Math.round(run.y));
2578
+ for (const child of current.children) walk(child);
2579
+ };
2580
+ walk(node);
2581
+ return ys.size;
2582
+ }
2583
+ function testCardNode() {
2584
+ return container({
2585
+ style: {
2586
+ width: "100%",
2587
+ height: "100%",
2588
+ display: "flex",
2589
+ flexDirection: "column",
2590
+ justifyContent: "center",
2591
+ padding: 64,
2592
+ backgroundColor: "#0b0b0c"
2593
+ },
2594
+ children: [text("Shipseal", {
2595
+ fontFamily: "Geist",
2596
+ fontSize: 72,
2597
+ fontWeight: 700,
2598
+ color: "#fafafa"
2599
+ }), text("Every release, sealed and ready to share", {
2600
+ fontFamily: GEIST_MONO_FAMILY,
2601
+ fontSize: 28,
2602
+ fontWeight: 400,
2603
+ color: "#a1a1aa"
2604
+ })]
2605
+ });
2606
+ }
2607
+ var TakumiRenderer = class {
2608
+ renderer;
2609
+ constructor(renderer) {
2610
+ this.renderer = renderer;
2611
+ }
2612
+ async fromJsx(element) {
2613
+ const result = await fromJsx(asReactElementLike(element));
2614
+ return pack(result.node, result.css);
2615
+ }
2616
+ async render(node, opts) {
2617
+ const packed = unpack(node);
2618
+ const extras = { css: opts.css ?? packed.css };
2619
+ if (opts.images !== void 0) extras.images = opts.images;
2620
+ const buffer = await renderWithFormat(this.renderer, packed.node, opts, extras);
2621
+ return Uint8Array.from(buffer);
2622
+ }
2623
+ async measure(node, opts) {
2624
+ const packed = unpack(node);
2625
+ const measured = await this.renderer.measure(packed.node, measureOpts(opts));
2626
+ return {
2627
+ lines: countLines(measured),
2628
+ width: measured.width,
2629
+ height: measured.height
2630
+ };
2631
+ }
2632
+ async measureText(value, opts) {
2633
+ const measured = await this.renderer.measure(asNode(textMeasureNode(value, opts)));
2634
+ return {
2635
+ lines: countLines(measured),
2636
+ width: measured.width,
2637
+ height: measured.height
2638
+ };
2639
+ }
2640
+ };
2641
+ function renderWithFormat(renderer, node, opts, extras) {
2642
+ const width = opts.width;
2643
+ const height = opts.height;
2644
+ const css = extras.css;
2645
+ const images = extras.images;
2646
+ if (opts.format === "webp") {
2647
+ if (images !== void 0) return renderer.render(node, {
2648
+ width,
2649
+ height,
2650
+ format: "webp",
2651
+ css,
2652
+ images
2653
+ });
2654
+ return renderer.render(node, {
2655
+ width,
2656
+ height,
2657
+ format: "webp",
2658
+ css
2659
+ });
2660
+ }
2661
+ if (opts.format === "jpeg") {
2662
+ if (images !== void 0) return renderer.render(node, {
2663
+ width,
2664
+ height,
2665
+ format: "jpeg",
2666
+ css,
2667
+ images
2668
+ });
2669
+ return renderer.render(node, {
2670
+ width,
2671
+ height,
2672
+ format: "jpeg",
2673
+ css
2674
+ });
2675
+ }
2676
+ if (images !== void 0) return renderer.render(node, {
2677
+ width,
2678
+ height,
2679
+ format: "png",
2680
+ css,
2681
+ images
2682
+ });
2683
+ return renderer.render(node, {
2684
+ width,
2685
+ height,
2686
+ format: "png",
2687
+ css
2688
+ });
2689
+ }
2690
+ function measureOpts(opts) {
2691
+ const out = {};
2692
+ if (opts?.width !== void 0) out.width = opts.width;
2693
+ if (opts?.height !== void 0) out.height = opts.height;
2694
+ return out;
2695
+ }
2696
+ function textMeasureNode(value, opts) {
2697
+ const style = {
2698
+ fontFamily: opts.fontFamily,
2699
+ fontSize: opts.fontSize,
2700
+ lineHeight: opts.lineHeight,
2701
+ whiteSpace: opts.whiteSpace ?? "normal",
2702
+ maxWidth: opts.maxWidth
2703
+ };
2704
+ if (opts.fontWeight !== void 0) style.fontWeight = opts.fontWeight;
2705
+ return text(value, style);
2706
+ }
2707
+ function asNode(node) {
2708
+ return unpack(node).node;
2709
+ }
2710
+ function pack(node, css = []) {
2711
+ return {
2712
+ __shipseal: true,
2713
+ node,
2714
+ css
2715
+ };
2716
+ }
2717
+ function unpack(node) {
2718
+ if (isPacked(node)) return node;
2719
+ if (isTakumiNode(node)) return {
2720
+ __shipseal: true,
2721
+ node,
2722
+ css: []
2723
+ };
2724
+ throw new ShipsealError("render.invalid-node", "render/measure received a value that is not a Takumi node tree.", "Pass a node built by the renderer adapter (fromJsx, measureText, or testCardNode).");
2725
+ }
2726
+ function isPacked(node) {
2727
+ return typeof node === "object" && node !== null && "__shipseal" in node;
2728
+ }
2729
+ function isTakumiNode(node) {
2730
+ return typeof node === "object" && node !== null && "type" in node;
2731
+ }
2732
+ function asReactElementLike(element) {
2733
+ if (typeof element !== "object" || element === null || !("type" in element)) throw new ShipsealError("render.invalid-jsx", "fromJsx received a value that is not a JSX element.", "Return JSX from a template render function.");
2734
+ const record = element;
2735
+ return {
2736
+ type: "type" in record ? record.type : void 0,
2737
+ props: "props" in record ? record.props : null,
2738
+ key: "key" in record && typeof record.key === "string" ? record.key : null
2739
+ };
2740
+ }
2741
+
2742
+ //#endregion
2743
+ //#region src/copy/number-guard.ts
2744
+ function allowedNumbers(facts) {
2745
+ const allowed = /* @__PURE__ */ new Set();
2746
+ const visit = (value) => {
2747
+ if (typeof value === "number" && Number.isFinite(value)) {
2748
+ addNumber(allowed, value);
2749
+ return;
2750
+ }
2751
+ if (typeof value === "string") {
2752
+ for (const match of value.match(/\d+(?:\.\d+)*/g) ?? []) allowed.add(match);
2753
+ return;
2754
+ }
2755
+ if (Array.isArray(value)) {
2756
+ for (const item of value) visit(item);
2757
+ return;
2758
+ }
2759
+ if (typeof value === "object" && value !== null) {
2760
+ if ("value" in value && "provenance" in value) {
2761
+ visit(value.value);
2762
+ return;
2763
+ }
2764
+ for (const nested of Object.values(value)) visit(nested);
2765
+ }
2766
+ };
2767
+ visit(facts);
2768
+ return allowed;
2769
+ }
2770
+ function unsourcedDigits(text, allowed) {
2771
+ const found = [];
2772
+ const withoutGrouped = text.replace(/\d{1,3}(?:,\d{3})+/g, (match) => {
2773
+ const compact = match.replaceAll(",", "");
2774
+ if (!allowed.has(match) && !allowed.has(compact)) found.push(match);
2775
+ return " ";
2776
+ });
2777
+ for (const match of withoutGrouped.match(/\d+(?:\.\d+)*/g) ?? []) if (!allowed.has(match)) found.push(match);
2778
+ return found;
2779
+ }
2780
+ function guardCopy(copy, facts) {
2781
+ const allowed = allowedNumbers(facts);
2782
+ const slots = [
2783
+ ["headline", copy.headline],
2784
+ ["subheadline", copy.subheadline],
2785
+ ["cta", copy.cta]
2786
+ ];
2787
+ if (copy.milestoneLine !== void 0) slots.push(["milestoneLine", copy.milestoneLine]);
2788
+ copy.highlights.forEach((line, index) => {
2789
+ slots.push([`highlights[${String(index)}]`, line]);
2790
+ });
2791
+ for (const [slot, text] of slots) {
2792
+ const digits = unsourcedDigits(text, allowed);
2793
+ if (digits.length > 0) return {
2794
+ ok: false,
2795
+ slot,
2796
+ digits
2797
+ };
2798
+ }
2799
+ return { ok: true };
2800
+ }
2801
+ function addNumber(allowed, value) {
2802
+ allowed.add(String(value));
2803
+ if (Number.isInteger(value)) allowed.add(value.toLocaleString("en-US"));
2804
+ }
2805
+
2806
+ //#endregion
2807
+ //#region src/copy/llm.ts
2808
+ const llmCopySchema = z.object({
2809
+ headline: z.string(),
2810
+ subheadline: z.string(),
2811
+ highlights: z.array(z.string()).max(4),
2812
+ cta: z.string()
2813
+ });
2814
+ async function llmCopy(facts, options, fallback = deterministicCopy(facts)) {
2815
+ const payload = factsForLlm(facts);
2816
+ let lastError;
2817
+ const retries = Math.max(0, options.maxRetries);
2818
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
2819
+ let parsed;
2820
+ try {
2821
+ const raw = await requestCopy(payload, options);
2822
+ parsed = llmCopySchema.parse(JSON.parse(raw));
2823
+ } catch (error) {
2824
+ lastError = error instanceof Error ? error.message : "LLM request failed";
2825
+ continue;
2826
+ }
2827
+ const expanded = expandPlaceholders(parsed, facts);
2828
+ const styled = {
2829
+ headline: stripStyle(expanded.headline).slice(0, COPY_LIMITS.headline),
2830
+ subheadline: stripStyle(expanded.subheadline).slice(0, COPY_LIMITS.subheadline),
2831
+ highlights: expanded.highlights.map((line) => stripStyle(line).slice(0, COPY_LIMITS.highlight)),
2832
+ cta: stripStyle(expanded.cta).slice(0, COPY_LIMITS.cta)
2833
+ };
2834
+ const guarded = guardCopy(styled, facts);
2835
+ if (!guarded.ok) {
2836
+ lastError = `Number guard rejected ${guarded.slot}: ${guarded.digits.join(", ")}`;
2837
+ continue;
2838
+ }
2839
+ return {
2840
+ copy: styled,
2841
+ mode: "llm"
2842
+ };
2843
+ }
2844
+ return {
2845
+ copy: fallback,
2846
+ mode: "deterministic",
2847
+ warning: lastError ?? "LLM copy failed; using deterministic copy"
2848
+ };
2849
+ }
2850
+ function factsForLlm(facts) {
2851
+ return {
2852
+ project: {
2853
+ name: facts.project.name.value,
2854
+ tagline: facts.project.tagline?.value,
2855
+ npmPackage: facts.project.npmPackage?.value,
2856
+ url: facts.project.url?.value
2857
+ },
2858
+ release: facts.release === void 0 ? void 0 : {
2859
+ version: "{version}",
2860
+ features: facts.release.features.map((item) => item.value),
2861
+ fixes: facts.release.fixes.map((item) => item.value),
2862
+ breaking: facts.release.breaking.map((item) => item.value)
2863
+ },
2864
+ metrics: {
2865
+ stars: facts.metrics?.stars === void 0 ? void 0 : "{stars}",
2866
+ weeklyDownloads: facts.metrics?.weeklyDownloads === void 0 ? void 0 : "{weeklyDownloads}",
2867
+ contributorCount: facts.metrics?.contributorCount === void 0 ? void 0 : "{contributorCount}"
2868
+ }
2869
+ };
2870
+ }
2871
+ function expandPlaceholders(copy, facts) {
2872
+ const map = {
2873
+ "{version}": facts.release?.version.value ?? "",
2874
+ "{stars}": facts.metrics?.stars === void 0 ? "" : formatCount(facts.metrics.stars.value),
2875
+ "{weeklyDownloads}": facts.metrics?.weeklyDownloads === void 0 ? "" : formatCount(facts.metrics.weeklyDownloads.value),
2876
+ "{contributorCount}": facts.metrics?.contributorCount === void 0 ? "" : formatCount(facts.metrics.contributorCount.value)
2877
+ };
2878
+ const expand = (text) => {
2879
+ let out = text;
2880
+ for (const [token, value] of Object.entries(map)) out = out.replaceAll(token, value);
2881
+ return out;
2882
+ };
2883
+ return {
2884
+ headline: expand(copy.headline),
2885
+ subheadline: expand(copy.subheadline),
2886
+ highlights: copy.highlights.map(expand),
2887
+ cta: expand(copy.cta)
2888
+ };
2889
+ }
2890
+ function stripStyle(text) {
2891
+ return text.replaceAll("—", ":").replaceAll("–", "-").replaceAll("!", ".");
2892
+ }
2893
+ function formatCount(value) {
2894
+ return value.toLocaleString("en-US");
2895
+ }
2896
+ async function requestCopy(facts, options) {
2897
+ const base = (options.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "");
2898
+ const response = await (options.fetchImpl ?? fetch)(`${base}/chat/completions`, {
2899
+ method: "POST",
2900
+ headers: {
2901
+ Authorization: `Bearer ${options.apiKey}`,
2902
+ "Content-Type": "application/json"
2903
+ },
2904
+ body: JSON.stringify({
2905
+ model: options.model,
2906
+ response_format: { type: "json_object" },
2907
+ messages: [{
2908
+ role: "system",
2909
+ content: "Write short release-card copy as JSON with keys headline, subheadline, highlights, cta. No em dashes, no exclamation marks. Use placeholders like {version} for numbers. Never invent numbers."
2910
+ }, {
2911
+ role: "user",
2912
+ content: JSON.stringify(facts)
2913
+ }]
2914
+ })
2915
+ });
2916
+ if (!response.ok) throw new ShipsealError("copy.llm-http", `LLM provider returned HTTP ${String(response.status)}.`, "Check SHIPSEAL_LLM_API_KEY, copy.model, and SHIPSEAL_LLM_BASE_URL.");
2917
+ const json = await response.json();
2918
+ const first = z.object({ choices: z.array(z.object({ message: z.object({ content: z.string() }) })).min(1) }).parse(json).choices[0];
2919
+ if (first === void 0) throw new ShipsealError("copy.llm-empty", "LLM returned no choices.", "Retry, or run with --no-copy.");
2920
+ return first.message.content;
2921
+ }
2922
+
2923
+ //#endregion
2924
+ //#region src/outputs/manifest.ts
2925
+ function buildManifest(input) {
2926
+ return {
2927
+ shipseal: input.shipsealVersion,
2928
+ event: input.event,
2929
+ generatedAt: input.result.generatedAt,
2930
+ brand: {
2931
+ name: input.brand.name,
2932
+ theme: input.brand.theme,
2933
+ source: ".shipseal/brand.json"
2934
+ },
2935
+ copy: { mode: input.result.copyMode },
2936
+ files: input.result.files.map((file) => ({
2937
+ path: file.fileName,
2938
+ template: file.template,
2939
+ format: file.format,
2940
+ width: file.width,
2941
+ height: file.height,
2942
+ bytes: file.bytes.byteLength,
2943
+ sha256: file.sha256
2944
+ })),
2945
+ facts: flattenFacts(input.result.facts),
2946
+ computed: input.result.computed,
2947
+ warnings: input.result.warnings,
2948
+ missing: input.result.missing
2949
+ };
2950
+ }
2951
+ function flattenFacts(facts) {
2952
+ const out = {};
2953
+ const walk = (prefix, value) => {
2954
+ if (isFact(value)) {
2955
+ out[prefix] = {
2956
+ value: value.value,
2957
+ source: value.provenance.source,
2958
+ ref: value.provenance.ref,
2959
+ fetchedAt: value.provenance.fetchedAt
2960
+ };
2961
+ return;
2962
+ }
2963
+ if (Array.isArray(value)) {
2964
+ value.forEach((item, index) => {
2965
+ walk(`${prefix}[${String(index)}]`, item);
2966
+ });
2967
+ return;
2968
+ }
2969
+ if (typeof value === "object" && value !== null) for (const [key, nested] of Object.entries(value)) walk(prefix.length === 0 ? key : `${prefix}.${key}`, nested);
2970
+ };
2971
+ walk("project", facts.project);
2972
+ if (facts.release !== void 0) walk("release", facts.release);
2973
+ if (facts.metrics !== void 0) walk("metrics", facts.metrics);
2974
+ if (facts.bench !== void 0) walk("bench", facts.bench);
2975
+ if (facts.milestone !== void 0) walk("milestone", facts.milestone);
2976
+ return out;
2977
+ }
2978
+ function isFact(value) {
2979
+ if (typeof value !== "object" || value === null) return false;
2980
+ if (!("value" in value) || !("provenance" in value)) return false;
2981
+ const provenance = value.provenance;
2982
+ return typeof provenance === "object" && provenance !== null && "source" in provenance && "ref" in provenance && "fetchedAt" in provenance;
2983
+ }
2984
+
2985
+ //#endregion
2986
+ //#region src/outputs/files.ts
2987
+ async function writePack(input) {
2988
+ const dir = join(input.outDir, input.eventId);
2989
+ await mkdir(dir, { recursive: true });
2990
+ await Promise.all(input.result.files.map((file) => writeFile(join(dir, file.fileName), file.bytes)));
2991
+ const manifest = buildManifest({
2992
+ result: input.result,
2993
+ event: input.event,
2994
+ brand: input.brand,
2995
+ shipsealVersion: input.shipsealVersion
2996
+ });
2997
+ await writeFile(join(dir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
2998
+ return {
2999
+ dir,
3000
+ manifest
3001
+ };
3002
+ }
3003
+ function resolveOutputDir(cwd, configured) {
3004
+ return isAbsolute(configured) ? configured : join(cwd, configured);
3005
+ }
3006
+ function eventId(event, generatedAt = "") {
3007
+ if (event.kind === "release") return event.tag;
3008
+ if (event.kind === "milestone") return `milestone-${event.metric}-${String(event.threshold)}`;
3009
+ const day = generatedAt.slice(0, 10);
3010
+ return /^\d{4}-\d{2}-\d{2}$/.test(day) ? `bench-${day}` : "bench";
3011
+ }
3012
+
3013
+ //#endregion
3014
+ //#region src/outputs/github-release.ts
3015
+ const execFileAsync$2 = promisify(execFile);
3016
+ async function uploadReleaseAssets(options) {
3017
+ if (options.files.length === 0) return;
3018
+ const exec = options.exec ?? execFileAsync$2;
3019
+ const args = [
3020
+ "release",
3021
+ "upload",
3022
+ options.tag,
3023
+ ...options.files,
3024
+ "--clobber"
3025
+ ];
3026
+ if (options.repo !== void 0) args.push("--repo", options.repo);
3027
+ const env = { ...process.env };
3028
+ if (options.token !== void 0) {
3029
+ env.GH_TOKEN = options.token;
3030
+ env.GITHUB_TOKEN = options.token;
3031
+ }
3032
+ try {
3033
+ await exec("gh", args, { env });
3034
+ } catch (error) {
3035
+ throw new ShipsealError("github.upload", `Could not upload assets to GitHub release ${options.tag}.`, "Confirm gh is installed, GITHUB_TOKEN can write contents, and the release exists.", { cause: error });
3036
+ }
3037
+ }
3038
+ async function appendJobSummary(path, manifest, dir) {
3039
+ await appendFile(path, `${jobSummaryMarkdown(manifest, dir)}\n`, "utf8");
3040
+ }
3041
+ function jobSummaryMarkdown(manifest, dir) {
3042
+ const lines = [
3043
+ "## Shipseal",
3044
+ "",
3045
+ `Generated **${String(manifest.files.length)}** files in \`${dir}\`.`,
3046
+ ""
3047
+ ];
3048
+ const repo = process.env.GITHUB_REPOSITORY;
3049
+ const tag = manifest.event.kind === "release" ? manifest.event.tag : void 0;
3050
+ if (repo !== void 0 && tag !== void 0) for (const file of manifest.files) {
3051
+ const url = `https://github.com/${repo}/releases/download/${tag}/${file.path}`;
3052
+ lines.push(`![${file.path}](${url})`, "");
3053
+ }
3054
+ else {
3055
+ lines.push("| File | Template | Format |", "|---|---|---|");
3056
+ for (const file of manifest.files) lines.push(`| ${file.path} | ${file.template} | ${file.format} |`);
3057
+ lines.push("");
3058
+ }
3059
+ lines.push("### Facts", "");
3060
+ for (const [key, entry] of Object.entries(manifest.facts)) lines.push(`- \`${key}\`: ${JSON.stringify(entry.value)} (${entry.source})`);
3061
+ if (Object.keys(manifest.computed).length > 0) {
3062
+ lines.push("", "### Computed", "");
3063
+ for (const [key, entry] of Object.entries(manifest.computed)) lines.push(`- \`${key}\`: ${JSON.stringify(entry.value)}`);
3064
+ }
3065
+ if (manifest.warnings.length > 0) {
3066
+ lines.push("", "### Warnings", "");
3067
+ for (const warning of manifest.warnings) lines.push(`- ${warning.template} ${warning.format} ${warning.slot}: ${warning.action}`);
3068
+ }
3069
+ lines.push("");
3070
+ return lines.join("\n");
3071
+ }
3072
+
3073
+ //#endregion
3074
+ //#region src/commands/shared.ts
3075
+ function overlayFormats(config, flags) {
3076
+ const overlay = { ...config };
3077
+ if (flags.formats !== void 0) overlay.formats = parseFormats(flags.formats);
3078
+ if (flags.out !== void 0) overlay.outputDir = flags.out;
3079
+ return mergeConfig(DEFAULT_CONFIG, overlay);
3080
+ }
3081
+ function parseFormats(list) {
3082
+ const formats = [];
3083
+ for (const part of list.split(",")) {
3084
+ const id = part.trim();
3085
+ if (id.length === 0) continue;
3086
+ const match = FORMAT_IDS.find((format) => format === id);
3087
+ if (match === void 0) throw new ShipsealError("release.bad-format", `Unknown format "${id}".`, `Use a comma-separated list from: ${FORMAT_IDS.join(", ")}.`);
3088
+ formats.push(match);
3089
+ }
3090
+ return formats;
3091
+ }
3092
+ function resolveThemes(flag, brandTheme) {
3093
+ if (flag === "both") return ["dark", "light"];
3094
+ if (flag === "dark" || flag === "light") return [flag];
3095
+ return [brandTheme];
3096
+ }
3097
+ async function resolveCopy(facts, config, noCopy, fallback) {
3098
+ if (noCopy || config.copy?.llm !== true) return {
3099
+ copy: fallback,
3100
+ copyMode: "deterministic"
3101
+ };
3102
+ const apiKey = process.env.SHIPSEAL_LLM_API_KEY;
3103
+ if (apiKey === void 0 || apiKey.length === 0) throw new ShipsealError("copy.missing-key", "copy.llm is enabled but SHIPSEAL_LLM_API_KEY is not set.", "Set SHIPSEAL_LLM_API_KEY, or pass --no-copy for deterministic copy.");
3104
+ const llmOpts = {
3105
+ apiKey,
3106
+ model: config.copy.model ?? "gpt-4o-mini",
3107
+ maxRetries: config.copy.maxRetries ?? 2
3108
+ };
3109
+ if (process.env.SHIPSEAL_LLM_BASE_URL !== void 0) llmOpts.baseUrl = process.env.SHIPSEAL_LLM_BASE_URL;
3110
+ const result = await llmCopy(facts, llmOpts, fallback);
3111
+ const out = {
3112
+ copy: result.copy,
3113
+ copyMode: result.mode
3114
+ };
3115
+ if (result.warning !== void 0) out.warning = result.warning;
3116
+ return out;
3117
+ }
3118
+ async function loadLogos(cwd, brand) {
3119
+ if (brand.logo === void 0) return;
3120
+ const logos = { light: await readLogo(cwd, brand.logo.light) };
3121
+ if (brand.logo.dark !== void 0) logos.dark = await readLogo(cwd, brand.logo.dark);
3122
+ return logos;
3123
+ }
3124
+ async function readLogo(cwd, path) {
3125
+ const resolved = isAbsolute(path) ? path : join(cwd, path);
3126
+ return new Uint8Array(await readFile(resolved));
3127
+ }
3128
+ function readShipsealVersion() {
3129
+ const raw = JSON.parse(readFileSync(join(resolvePackageRoot(), "package.json"), "utf8"));
3130
+ if (typeof raw === "object" && raw !== null && "version" in raw && typeof raw.version === "string") return raw.version;
3131
+ return "0.0.0";
3132
+ }
3133
+ function collectEnv(flags) {
3134
+ const out = { skipNetwork: flags.dryRun === true && process.env.GITHUB_TOKEN === void 0 };
3135
+ if (process.env.GITHUB_TOKEN !== void 0) out.githubToken = process.env.GITHUB_TOKEN;
3136
+ if (flags.package !== void 0) out.packagePath = flags.package;
3137
+ if (flags.fetchImpl !== void 0) out.fetchImpl = flags.fetchImpl;
3138
+ return out;
3139
+ }
3140
+ async function finishPack(input) {
3141
+ const generatedAt = input.result.generatedAt;
3142
+ const written = await writePack({
3143
+ outDir: resolveOutputDir(input.flags.cwd, input.flags.out ?? input.config.outputDir ?? ".shipseal/output"),
3144
+ eventId: eventId(input.event, generatedAt),
3145
+ result: input.result,
3146
+ event: input.event,
3147
+ brand: input.brand,
3148
+ shipsealVersion: readShipsealVersion()
3149
+ });
3150
+ if (input.flags.upload === true && input.event.kind === "release") {
3151
+ const files = input.result.files.map((file) => join(written.dir, file.fileName));
3152
+ const uploadOpts = {
3153
+ tag: input.event.tag,
3154
+ files
3155
+ };
3156
+ if (process.env.GITHUB_REPOSITORY !== void 0) uploadOpts.repo = process.env.GITHUB_REPOSITORY;
3157
+ if (process.env.GITHUB_TOKEN !== void 0) uploadOpts.token = process.env.GITHUB_TOKEN;
3158
+ await uploadReleaseAssets(uploadOpts);
3159
+ }
3160
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
3161
+ if (summaryPath !== void 0 && summaryPath.length > 0) await appendJobSummary(summaryPath, written.manifest, written.dir);
3162
+ const exitCode = input.flags.strict === true && input.result.warnings.length > 0 ? 2 : 0;
3163
+ const done = {
3164
+ dryRun: false,
3165
+ facts: input.facts,
3166
+ copy: input.copy,
3167
+ copyMode: input.copyMode,
3168
+ dir: written.dir,
3169
+ manifest: written.manifest,
3170
+ warnings: input.result.warnings,
3171
+ exitCode
3172
+ };
3173
+ if (input.copyWarning !== void 0) done.copyWarning = input.copyWarning;
3174
+ return done;
3175
+ }
3176
+
3177
+ //#endregion
3178
+ //#region src/commands/bench.ts
3179
+ async function runBench(flags) {
3180
+ const cwd = flags.cwd;
3181
+ const config = overlayFormats(await loadConfig(cwd), flags);
3182
+ const brand = await loadBrand(cwd);
3183
+ const file = flags.file ?? config.bench?.file ?? ".shipseal/bench.json";
3184
+ const event = {
3185
+ kind: "bench",
3186
+ file
3187
+ };
3188
+ const env = collectEnv(flags);
3189
+ const collectOpts = {
3190
+ cwd,
3191
+ event,
3192
+ skipNetwork: true,
3193
+ benchFile: file
3194
+ };
3195
+ if (env.packagePath !== void 0) collectOpts.packagePath = env.packagePath;
3196
+ const facts = await collectFacts(collectOpts);
3197
+ const resolved = await resolveCopy(facts, config, flags.copy === false, benchCopy(facts));
3198
+ if (flags.dryRun === true) {
3199
+ const dry = {
3200
+ dryRun: true,
3201
+ facts,
3202
+ copy: resolved.copy,
3203
+ copyMode: resolved.copyMode,
3204
+ warnings: [],
3205
+ exitCode: 0
3206
+ };
3207
+ if (resolved.warning !== void 0) dry.copyWarning = resolved.warning;
3208
+ return dry;
3209
+ }
3210
+ const renderer = await createTakumiRenderer();
3211
+ const generateInput = {
3212
+ event,
3213
+ facts,
3214
+ brand,
3215
+ config,
3216
+ copy: resolved.copy,
3217
+ copyMode: resolved.copyMode,
3218
+ renderer,
3219
+ themes: resolveThemes(flags.themes, brand.theme),
3220
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
3221
+ };
3222
+ const logos = await loadLogos(cwd, brand);
3223
+ if (logos !== void 0) generateInput.logos = logos;
3224
+ const packInput = {
3225
+ flags,
3226
+ config,
3227
+ event,
3228
+ result: await generate(generateInput),
3229
+ brand,
3230
+ copy: resolved.copy,
3231
+ copyMode: resolved.copyMode,
3232
+ facts
3233
+ };
3234
+ if (resolved.warning !== void 0) packInput.copyWarning = resolved.warning;
3235
+ return finishPack(packInput);
3236
+ }
3237
+
3238
+ //#endregion
3239
+ //#region src/commands/doctor.ts
3240
+ const execFileAsync$1 = promisify(execFile);
3241
+ const MIN_NODE_MAJOR = 22;
3242
+ async function runDoctor(cwd) {
3243
+ const checks = [];
3244
+ checks.push(checkNode());
3245
+ checks.push(await checkGit(cwd));
3246
+ checks.push(await checkShallow(cwd));
3247
+ checks.push(await checkBrand(cwd));
3248
+ checks.push(await checkFont());
3249
+ checks.push(await checkLogo(cwd));
3250
+ checks.push(checkGithubToken());
3251
+ checks.push(await checkRender());
3252
+ return {
3253
+ ok: checks.every((check) => check.status !== "fail"),
3254
+ checks
3255
+ };
3256
+ }
3257
+ function checkNode() {
3258
+ const version = process.versions.node;
3259
+ if (Number.parseInt(version.split(".")[0] ?? "0", 10) >= MIN_NODE_MAJOR) return {
3260
+ name: "node",
3261
+ status: "pass",
3262
+ message: `Node.js ${version}`
3263
+ };
3264
+ return {
3265
+ name: "node",
3266
+ status: "fail",
3267
+ message: `Node.js ${version} is below the minimum of ${MIN_NODE_MAJOR}.`,
3268
+ fix: `Install Node.js ${MIN_NODE_MAJOR} or newer.`
3269
+ };
3270
+ }
3271
+ async function checkGit(cwd) {
3272
+ try {
3273
+ const { stdout } = await execFileAsync$1("git", ["--version"], { cwd });
3274
+ return {
3275
+ name: "git",
3276
+ status: "pass",
3277
+ message: stdout.trim()
3278
+ };
3279
+ } catch {
3280
+ return {
3281
+ name: "git",
3282
+ status: "fail",
3283
+ message: "git is not available on PATH.",
3284
+ fix: "Install git and ensure it is on PATH."
3285
+ };
3286
+ }
3287
+ }
3288
+ async function checkShallow(cwd) {
3289
+ try {
3290
+ const { stdout } = await execFileAsync$1("git", ["rev-parse", "--is-shallow-repository"], { cwd });
3291
+ if (stdout.trim() === "true") return {
3292
+ name: "clone-depth",
3293
+ status: "fail",
3294
+ message: "This git clone is shallow, so release facts will be incomplete.",
3295
+ fix: "Re-clone with a full history, or in GitHub Actions set fetch-depth: 0."
3296
+ };
3297
+ return {
3298
+ name: "clone-depth",
3299
+ status: "pass",
3300
+ message: "Full git history is available."
3301
+ };
3302
+ } catch {
3303
+ return {
3304
+ name: "clone-depth",
3305
+ status: "info",
3306
+ message: "Could not determine whether the clone is shallow."
3307
+ };
3308
+ }
3309
+ }
3310
+ async function checkBrand(cwd) {
3311
+ const path = join(cwd, ".shipseal", "brand.json");
3312
+ if (!existsSync(path)) return {
3313
+ name: "brand.json",
3314
+ status: "fail",
3315
+ message: ".shipseal/brand.json is missing.",
3316
+ fix: "Run shipseal init to detect a brand kit."
3317
+ };
3318
+ try {
3319
+ const raw = JSON.parse(await readFile(path, "utf8"));
3320
+ const parsed = brandSchema.safeParse(raw);
3321
+ if (!parsed.success) return {
3322
+ name: "brand.json",
3323
+ status: "fail",
3324
+ message: "brand.json did not match the v1 schema.",
3325
+ fix: parsed.error.issues.map((issue) => issue.message).join("; ")
3326
+ };
3327
+ return {
3328
+ name: "brand.json",
3329
+ status: "pass",
3330
+ message: `Brand ${parsed.data.name} is valid.`
3331
+ };
3332
+ } catch (error) {
3333
+ if (error instanceof SyntaxError) return {
3334
+ name: "brand.json",
3335
+ status: "fail",
3336
+ message: "brand.json is not valid JSON.",
3337
+ fix: "Fix the JSON syntax, or re-run shipseal init --force."
3338
+ };
3339
+ throw error;
3340
+ }
3341
+ }
3342
+ async function checkFont() {
3343
+ try {
3344
+ const root = resolvePackageRoot();
3345
+ const fontPath = join(root, "assets", "fonts", "GeistMono[wght].ttf");
3346
+ if (!existsSync(fontPath)) return {
3347
+ name: "fonts",
3348
+ status: "fail",
3349
+ message: "Vendored Geist Mono file is missing.",
3350
+ fix: "Reinstall shipseal so assets/fonts/GeistMono[wght].ttf is present."
3351
+ };
3352
+ return {
3353
+ name: "fonts",
3354
+ status: "pass",
3355
+ message: `Geist Mono found at ${fontPath}`
3356
+ };
3357
+ } catch {
3358
+ return {
3359
+ name: "fonts",
3360
+ status: "fail",
3361
+ message: "Could not resolve the vendored Geist Mono font.",
3362
+ fix: "Reinstall shipseal so assets/fonts/GeistMono[wght].ttf is present."
3363
+ };
3364
+ }
3365
+ }
3366
+ async function checkLogo(cwd) {
3367
+ const path = join(cwd, ".shipseal", "brand.json");
3368
+ if (!existsSync(path)) return {
3369
+ name: "logo",
3370
+ status: "info",
3371
+ message: "Skipped; brand.json is missing."
3372
+ };
3373
+ const raw = JSON.parse(await readFile(path, "utf8"));
3374
+ const parsed = brandSchema.safeParse(raw);
3375
+ if (!parsed.success || parsed.data.logo === void 0) return {
3376
+ name: "logo",
3377
+ status: "info",
3378
+ message: "No logo path in brand.json."
3379
+ };
3380
+ const light = join(cwd, parsed.data.logo.light);
3381
+ if (!existsSync(light)) return {
3382
+ name: "logo",
3383
+ status: "fail",
3384
+ message: `Logo file ${parsed.data.logo.light} is not readable.`,
3385
+ fix: "Point brand.json logo.light at an existing SVG or PNG, or re-run init."
3386
+ };
3387
+ return {
3388
+ name: "logo",
3389
+ status: "pass",
3390
+ message: `Logo readable at ${parsed.data.logo.light}`
3391
+ };
3392
+ }
3393
+ function checkGithubToken() {
3394
+ if (process.env.GITHUB_TOKEN !== void 0 && process.env.GITHUB_TOKEN.length > 0) return {
3395
+ name: "GITHUB_TOKEN",
3396
+ status: "info",
3397
+ message: "GITHUB_TOKEN is set."
3398
+ };
3399
+ return {
3400
+ name: "GITHUB_TOKEN",
3401
+ status: "info",
3402
+ message: "GITHUB_TOKEN is not set. Unauthenticated GitHub API calls have a low rate limit."
3403
+ };
3404
+ }
3405
+ async function checkRender() {
3406
+ try {
3407
+ const png = await (await createTakumiRenderer()).render(testCardNode(), {
3408
+ width: FORMATS.og.width,
3409
+ height: FORMATS.og.height,
3410
+ format: "png"
3411
+ });
3412
+ if (png.length < 8 || png[0] !== 137 || png[1] !== 80) return {
3413
+ name: "takumi",
3414
+ status: "fail",
3415
+ message: "Takumi rendered bytes that are not a PNG.",
3416
+ fix: "File an issue with the doctor output and your OS/arch."
3417
+ };
3418
+ return {
3419
+ name: "takumi",
3420
+ status: "pass",
3421
+ message: `Rendered a ${FORMATS.og.width}x${FORMATS.og.height} test card (${png.length} bytes).`
3422
+ };
3423
+ } catch (error) {
3424
+ return {
3425
+ name: "takumi",
3426
+ status: "fail",
3427
+ message: error instanceof Error ? error.message : "Takumi failed to render a test card.",
3428
+ fix: "Check that the native Takumi binary installed for this platform."
3429
+ };
3430
+ }
3431
+ }
3432
+
3433
+ //#endregion
3434
+ //#region src/brand/color.ts
3435
+ const HEX = /^#([\da-f]{3}|[\da-f]{6}|[\da-f]{8})$/i;
3436
+ function parseCssColor(value) {
3437
+ const trimmed = value.trim();
3438
+ const hex = normalizeHex(trimmed);
3439
+ if (hex !== void 0) return hex;
3440
+ const oklch = parseOklch(trimmed);
3441
+ if (oklch !== void 0) return oklchToHex(oklch.l, oklch.c, oklch.h);
3442
+ const rgb = parseRgb(trimmed);
3443
+ if (rgb !== void 0) return rgbToHex(rgb[0], rgb[1], rgb[2]);
3444
+ const hsl = parseHsl(trimmed);
3445
+ if (hsl !== void 0) return rgbToHex(...hslToRgb(hsl[0], hsl[1], hsl[2]));
3446
+ }
3447
+ function normalizeHex(value) {
3448
+ const match = HEX.exec(value.trim());
3449
+ if (match === null) return;
3450
+ const raw = match[1];
3451
+ if (raw === void 0) return;
3452
+ if (raw.length === 3) return `#${raw[0]}${raw[0]}${raw[1]}${raw[1]}${raw[2]}${raw[2]}`.toLowerCase();
3453
+ return `#${raw.slice(0, 6).toLowerCase()}`;
3454
+ }
3455
+ function oklchToHex(l, c, h) {
3456
+ const hr = h * Math.PI / 180;
3457
+ const a = c * Math.cos(hr);
3458
+ const b = c * Math.sin(hr);
3459
+ const coneL = l + .3963377774 * a + .2158037573 * b;
3460
+ const coneM = l - .1055613458 * a - .0638541728 * b;
3461
+ const coneS = l - .0894841775 * a - 1.291485548 * b;
3462
+ const l3 = coneL * coneL * coneL;
3463
+ const m3 = coneM * coneM * coneM;
3464
+ const s3 = coneS * coneS * coneS;
3465
+ const r = 4.0767416621 * l3 - 3.3077115913 * m3 + .2309699292 * s3;
3466
+ const g = -1.2684380046 * l3 + 2.6097574011 * m3 - .3413193965 * s3;
3467
+ const bLin = -.0041960863 * l3 - .7034186147 * m3 + 1.707614701 * s3;
3468
+ return rgbToHex(srgbEncode(r) * 255, srgbEncode(g) * 255, srgbEncode(bLin) * 255);
3469
+ }
3470
+ function contrastRatio(hexA, hexB) {
3471
+ const a = relativeLuminance(hexA);
3472
+ const b = relativeLuminance(hexB);
3473
+ const [hi, lo] = a > b ? [a, b] : [b, a];
3474
+ return (hi + .05) / (lo + .05);
3475
+ }
3476
+ const LARGE_TEXT_CONTRAST = 3;
3477
+ function ensureForegroundContrast(background, foreground) {
3478
+ if (contrastRatio(background, foreground) >= 3) return {
3479
+ foreground,
3480
+ adjusted: false
3481
+ };
3482
+ const light = "#fafafa";
3483
+ const dark = "#0b0b0c";
3484
+ return {
3485
+ foreground: contrastRatio(background, light) >= contrastRatio(background, dark) ? light : dark,
3486
+ adjusted: true
3487
+ };
3488
+ }
3489
+ function readableOnBackground(background, candidate) {
3490
+ return contrastRatio(background, candidate) >= 3;
3491
+ }
3492
+ function srgbEncode(channel) {
3493
+ const abs = Math.abs(channel);
3494
+ const encoded = abs <= .0031308 ? 12.92 * abs : 1.055 * abs ** (1 / 2.4) - .055;
3495
+ return Math.sign(channel) * encoded;
3496
+ }
3497
+ function rgbToHex(r, g, b) {
3498
+ return `#${toByte(r)}${toByte(g)}${toByte(b)}`;
3499
+ }
3500
+ function toByte(n) {
3501
+ return Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
3502
+ }
3503
+ function relativeLuminance(hex) {
3504
+ const rgb = hexToRgb(hex);
3505
+ if (rgb === void 0) return 0;
3506
+ const lin = rgb.map((channel) => {
3507
+ const s = channel / 255;
3508
+ return s <= .04045 ? s / 12.92 : ((s + .055) / 1.055) ** 2.4;
3509
+ });
3510
+ return .2126 * (lin[0] ?? 0) + .7152 * (lin[1] ?? 0) + .0722 * (lin[2] ?? 0);
3511
+ }
3512
+ function isNeutralHex(hex) {
3513
+ const rgb = hexToRgb(hex);
3514
+ if (rgb === void 0) return true;
3515
+ return Math.max(rgb[0], rgb[1], rgb[2]) - Math.min(rgb[0], rgb[1], rgb[2]) < 16;
3516
+ }
3517
+ function hexToRgb(hex) {
3518
+ const normalized = normalizeHex(hex);
3519
+ if (normalized === void 0) return;
3520
+ return [
3521
+ Number.parseInt(normalized.slice(1, 3), 16),
3522
+ Number.parseInt(normalized.slice(3, 5), 16),
3523
+ Number.parseInt(normalized.slice(5, 7), 16)
3524
+ ];
3525
+ }
3526
+ function parseOklch(value) {
3527
+ const match = /^oklch\(\s*([^/)]+?)(?:\s*\/\s*[^)]+)?\s*\)$/i.exec(value);
3528
+ if (match === null) return;
3529
+ const body = match[1];
3530
+ if (body === void 0) return;
3531
+ const parts = splitCssArgs(body);
3532
+ if (parts.length < 3) return;
3533
+ const l = parseLightness(parts[0] ?? "");
3534
+ const c = parseNumber(parts[1] ?? "");
3535
+ const h = parseHue(parts[2] ?? "");
3536
+ if (l === void 0 || c === void 0 || h === void 0) return;
3537
+ return {
3538
+ l,
3539
+ c,
3540
+ h
3541
+ };
3542
+ }
3543
+ function parseRgb(value) {
3544
+ const match = /^rgba?\(\s*([^/)]+?)(?:\s*\/\s*[^)]+)?\s*\)$/i.exec(value);
3545
+ if (match === null) return;
3546
+ const parts = splitCssArgs(match[1] ?? "");
3547
+ if (parts.length < 3) return;
3548
+ const r = parseRgbChannel(parts[0] ?? "");
3549
+ const g = parseRgbChannel(parts[1] ?? "");
3550
+ const b = parseRgbChannel(parts[2] ?? "");
3551
+ if (r === void 0 || g === void 0 || b === void 0) return;
3552
+ return [
3553
+ r,
3554
+ g,
3555
+ b
3556
+ ];
3557
+ }
3558
+ function parseHsl(value) {
3559
+ const match = /^hsla?\(\s*([^/)]+?)(?:\s*\/\s*[^)]+)?\s*\)$/i.exec(value);
3560
+ if (match === null) return;
3561
+ const parts = splitCssArgs(match[1] ?? "");
3562
+ if (parts.length < 3) return;
3563
+ const h = parseHue(parts[0] ?? "");
3564
+ const s = parsePercent(parts[1] ?? "");
3565
+ const l = parsePercent(parts[2] ?? "");
3566
+ if (h === void 0 || s === void 0 || l === void 0) return;
3567
+ return [
3568
+ h,
3569
+ s,
3570
+ l
3571
+ ];
3572
+ }
3573
+ function hslToRgb(h, s, l) {
3574
+ const sat = s / 100;
3575
+ const light = l / 100;
3576
+ const c = (1 - Math.abs(2 * light - 1)) * sat;
3577
+ const hp = (h % 360 + 360) % 360 / 60;
3578
+ const x = c * (1 - Math.abs(hp % 2 - 1));
3579
+ let r = 0;
3580
+ let g = 0;
3581
+ let b = 0;
3582
+ if (hp < 1) {
3583
+ r = c;
3584
+ g = x;
3585
+ } else if (hp < 2) {
3586
+ r = x;
3587
+ g = c;
3588
+ } else if (hp < 3) {
3589
+ g = c;
3590
+ b = x;
3591
+ } else if (hp < 4) {
3592
+ g = x;
3593
+ b = c;
3594
+ } else if (hp < 5) {
3595
+ r = x;
3596
+ b = c;
3597
+ } else {
3598
+ r = c;
3599
+ b = x;
3600
+ }
3601
+ const m = light - c / 2;
3602
+ return [
3603
+ (r + m) * 255,
3604
+ (g + m) * 255,
3605
+ (b + m) * 255
3606
+ ];
3607
+ }
3608
+ function splitCssArgs(body) {
3609
+ return body.split(/[\s,]+/).map((part) => part.trim()).filter((part) => part.length > 0);
3610
+ }
3611
+ function parseLightness(token) {
3612
+ if (token.endsWith("%")) {
3613
+ const n = Number.parseFloat(token.slice(0, -1));
3614
+ return Number.isFinite(n) ? n / 100 : void 0;
3615
+ }
3616
+ return parseNumber(token);
3617
+ }
3618
+ function parseHue(token) {
3619
+ if (token === "none") return 0;
3620
+ if (token.endsWith("deg")) return parseNumber(token.slice(0, -3));
3621
+ if (token.endsWith("rad")) {
3622
+ const n = parseNumber(token.slice(0, -3));
3623
+ return n === void 0 ? void 0 : n * 180 / Math.PI;
3624
+ }
3625
+ if (token.endsWith("turn")) {
3626
+ const n = parseNumber(token.slice(0, -4));
3627
+ return n === void 0 ? void 0 : n * 360;
3628
+ }
3629
+ if (token.endsWith("grad")) {
3630
+ const n = parseNumber(token.slice(0, -4));
3631
+ return n === void 0 ? void 0 : n * .9;
3632
+ }
3633
+ return parseNumber(token);
3634
+ }
3635
+ function parsePercent(token) {
3636
+ if (!token.endsWith("%")) return parseNumber(token);
3637
+ return parseNumber(token.slice(0, -1));
3638
+ }
3639
+ function parseRgbChannel(token) {
3640
+ if (token.endsWith("%")) {
3641
+ const n = parseNumber(token.slice(0, -1));
3642
+ return n === void 0 ? void 0 : n / 100 * 255;
3643
+ }
3644
+ return parseNumber(token);
3645
+ }
3646
+ function parseNumber(token) {
3647
+ if (token === "none") return 0;
3648
+ const n = Number.parseFloat(token);
3649
+ return Number.isFinite(n) ? n : void 0;
3650
+ }
3651
+
3652
+ //#endregion
3653
+ //#region src/brand/css-vars.ts
3654
+ const PROP = /--(primary|brand|accent|background|foreground|muted-foreground|muted)\s*:\s*([^;]+);/g;
3655
+ function extractCssRootColors(css) {
3656
+ const fromRoot = extractBlockColors(css, /:root\s*\{/g);
3657
+ const fromDark = extractBlockColors(css, /\.dark\s*\{/g);
3658
+ return mapFoundColors({
3659
+ ...fromRoot,
3660
+ ...fromDark
3661
+ });
3662
+ }
3663
+ function extractBlockColors(css, blockRe) {
3664
+ const found = {};
3665
+ blockRe.lastIndex = 0;
3666
+ let match;
3667
+ while ((match = blockRe.exec(css)) !== null) {
3668
+ const open = css.indexOf("{", match.index);
3669
+ if (open === -1) break;
3670
+ const close = matchingBrace$1(css, open);
3671
+ if (close === -1) break;
3672
+ const body = css.slice(open + 1, close);
3673
+ PROP.lastIndex = 0;
3674
+ let prop;
3675
+ while ((prop = PROP.exec(body)) !== null) {
3676
+ const name = prop[1];
3677
+ const raw = prop[2];
3678
+ if (name === void 0 || raw === void 0) continue;
3679
+ const hex = parseCssColor(raw.trim());
3680
+ if (hex !== void 0) found[name] = hex;
3681
+ }
3682
+ }
3683
+ return found;
3684
+ }
3685
+ function mapFoundColors(found) {
3686
+ const out = {};
3687
+ const background = found.background;
3688
+ if (background !== void 0) out.background = background;
3689
+ const foreground = found.foreground;
3690
+ if (foreground !== void 0) out.foreground = foreground;
3691
+ const muted = found["muted-foreground"] ?? found.muted;
3692
+ if (muted !== void 0) out.muted = muted;
3693
+ const primary = found.primary ?? found.brand;
3694
+ if (primary !== void 0) out.primary = primary;
3695
+ if (found.accent !== void 0) out.accent = found.accent;
3696
+ return out;
3697
+ }
3698
+ function matchingBrace$1(source, openIndex) {
3699
+ let depth = 0;
3700
+ for (let i = openIndex; i < source.length; i += 1) {
3701
+ const ch = source[i];
3702
+ if (ch === "{") depth += 1;
3703
+ else if (ch === "}") {
3704
+ depth -= 1;
3705
+ if (depth === 0) return i;
3706
+ }
3707
+ }
3708
+ return -1;
3709
+ }
3710
+
3711
+ //#endregion
3712
+ //#region src/brand/dtcg.ts
3713
+ function extractDtcgColors(tokens) {
3714
+ const found = {};
3715
+ walk(tokens, [], found);
3716
+ const out = {};
3717
+ const background = found.background ?? found.bg;
3718
+ if (background !== void 0) out.background = background;
3719
+ const foreground = found.foreground ?? found.fg;
3720
+ if (foreground !== void 0) out.foreground = foreground;
3721
+ if (found.muted !== void 0) out.muted = found.muted;
3722
+ const primary = found.primary ?? found.brand;
3723
+ if (primary !== void 0) out.primary = primary;
3724
+ const accent = found.accent ?? found.secondary;
3725
+ if (accent !== void 0) out.accent = accent;
3726
+ return out;
3727
+ }
3728
+ function walk(node, path, found) {
3729
+ if (!isRecord(node)) return;
3730
+ if (node.$type === "color") {
3731
+ const hex = colorValue(node.$value);
3732
+ const leaf = path[path.length - 1];
3733
+ if (hex !== void 0 && leaf !== void 0 && found[leaf] === void 0) found[leaf] = hex;
3734
+ return;
3735
+ }
3736
+ for (const [key, value] of Object.entries(node)) {
3737
+ if (key.startsWith("$")) continue;
3738
+ walk(value, [...path, key], found);
3739
+ }
3740
+ }
3741
+ function colorValue(value) {
3742
+ if (typeof value === "string") return parseCssColor(value);
3743
+ if (isRecord(value) && typeof value.hex === "string") return parseCssColor(value.hex);
3744
+ }
3745
+ function isRecord(value) {
3746
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3747
+ }
3748
+
3749
+ //#endregion
3750
+ //#region src/brand/logo.ts
3751
+ const DIRECTORIES = [
5
3752
  "",
6
- "This release exists to reserve the package name. No commands are implemented.",
7
- `Follow progress at https://shipseal.dev`,
8
- ""
9
- ].join("\n"));
3753
+ "assets",
3754
+ "assets/brand",
3755
+ "public",
3756
+ "public/brand",
3757
+ ".github",
3758
+ "docs",
3759
+ "static",
3760
+ "branding"
3761
+ ];
3762
+ const ICON_NAMES = ["icon.svg", "icon.png"];
3763
+ const NAMES = [
3764
+ "logo.svg",
3765
+ "logo.png",
3766
+ "logo-light.svg",
3767
+ "logo-light.png"
3768
+ ];
3769
+ const FALLBACKS = [...ICON_NAMES, "favicon.svg"];
3770
+ function findLogo(cwd) {
3771
+ for (const dir of DIRECTORIES) for (const name of NAMES) {
3772
+ const relative = dir === "" ? name : join(dir, name);
3773
+ if (existsSync(join(cwd, relative))) return relative;
3774
+ }
3775
+ for (const dir of DIRECTORIES) for (const name of FALLBACKS) {
3776
+ const relative = dir === "" ? name : join(dir, name);
3777
+ if (existsSync(join(cwd, relative))) return relative;
3778
+ }
3779
+ }
3780
+ function findLogoPair(cwd) {
3781
+ const light = findLogo(cwd);
3782
+ if (light === void 0) return;
3783
+ const darkNames = ["logo-dark.svg", "logo-dark.png"];
3784
+ for (const dir of DIRECTORIES) for (const name of darkNames) {
3785
+ const relative = dir === "" ? name : join(dir, name);
3786
+ if (existsSync(join(cwd, relative))) return {
3787
+ light,
3788
+ dark: relative
3789
+ };
3790
+ }
3791
+ return { light };
3792
+ }
3793
+
3794
+ //#endregion
3795
+ //#region src/brand/png.ts
3796
+ const SIGNATURE = Buffer.from([
3797
+ 137,
3798
+ 80,
3799
+ 78,
3800
+ 71,
3801
+ 13,
3802
+ 10,
3803
+ 26,
3804
+ 10
3805
+ ]);
3806
+ /** Channels per pixel for each PNG colour type. Index is the colour type. */
3807
+ const CHANNELS = {
3808
+ 0: 1,
3809
+ 2: 3,
3810
+ 3: 1,
3811
+ 4: 2,
3812
+ 6: 4
3813
+ };
3814
+ function decodePng(buffer) {
3815
+ if (buffer.length < 8 || !buffer.subarray(0, 8).equals(SIGNATURE)) return;
3816
+ let width = 0;
3817
+ let height = 0;
3818
+ let bitDepth = 0;
3819
+ let colorType = 0;
3820
+ let interlace = 0;
3821
+ let palette;
3822
+ let paletteAlpha;
3823
+ const idat = [];
3824
+ let offset = 8;
3825
+ while (offset + 8 <= buffer.length) {
3826
+ const length = buffer.readUInt32BE(offset);
3827
+ const type = buffer.toString("ascii", offset + 4, offset + 8);
3828
+ const start = offset + 8;
3829
+ const end = start + length;
3830
+ if (end > buffer.length) return;
3831
+ if (type === "IHDR") {
3832
+ width = buffer.readUInt32BE(start);
3833
+ height = buffer.readUInt32BE(start + 4);
3834
+ bitDepth = buffer[start + 8] ?? 0;
3835
+ colorType = buffer[start + 9] ?? 0;
3836
+ interlace = buffer[start + 12] ?? 0;
3837
+ } else if (type === "PLTE") palette = buffer.subarray(start, end);
3838
+ else if (type === "tRNS") paletteAlpha = buffer.subarray(start, end);
3839
+ else if (type === "IDAT") idat.push(buffer.subarray(start, end));
3840
+ else if (type === "IEND") break;
3841
+ offset = end + 4;
3842
+ }
3843
+ const channels = CHANNELS[colorType];
3844
+ if (width === 0 || height === 0 || channels === void 0 || interlace !== 0 || bitDepth !== 8 && bitDepth !== 16 || idat.length === 0 || colorType === 3 && palette === void 0) return;
3845
+ if (width * height > 64e6) return;
3846
+ let raw;
3847
+ try {
3848
+ raw = inflateSync(Buffer.concat(idat));
3849
+ } catch {
3850
+ return;
3851
+ }
3852
+ const bytesPerSample = bitDepth / 8;
3853
+ const bpp = channels * bytesPerSample;
3854
+ const stride = width * bpp;
3855
+ if (raw.length < height * (stride + 1)) return;
3856
+ const unfiltered = unfilter(raw, height, stride, bpp);
3857
+ if (unfiltered === void 0) return;
3858
+ return {
3859
+ width,
3860
+ height,
3861
+ pixels: toRgba(unfiltered, width, height, colorType, bytesPerSample, palette, paletteAlpha)
3862
+ };
3863
+ }
3864
+ /** Reverse the per-scanline filters defined in the PNG spec. */
3865
+ function unfilter(raw, height, stride, bpp) {
3866
+ const out = Buffer.alloc(height * stride);
3867
+ let pos = 0;
3868
+ for (let y = 0; y < height; y++) {
3869
+ const filter = raw[pos];
3870
+ pos += 1;
3871
+ if (filter === void 0 || filter > 4) return;
3872
+ const rowStart = y * stride;
3873
+ const prevStart = rowStart - stride;
3874
+ for (let x = 0; x < stride; x++) {
3875
+ const value = raw[pos + x] ?? 0;
3876
+ const a = x >= bpp ? out[rowStart + x - bpp] ?? 0 : 0;
3877
+ const b = y > 0 ? out[prevStart + x] ?? 0 : 0;
3878
+ const c = x >= bpp && y > 0 ? out[prevStart + x - bpp] ?? 0 : 0;
3879
+ let restored;
3880
+ switch (filter) {
3881
+ case 1:
3882
+ restored = value + a;
3883
+ break;
3884
+ case 2:
3885
+ restored = value + b;
3886
+ break;
3887
+ case 3:
3888
+ restored = value + Math.floor((a + b) / 2);
3889
+ break;
3890
+ case 4:
3891
+ restored = value + paeth(a, b, c);
3892
+ break;
3893
+ default: restored = value;
3894
+ }
3895
+ out[rowStart + x] = restored & 255;
3896
+ }
3897
+ pos += stride;
3898
+ }
3899
+ return out;
3900
+ }
3901
+ function paeth(a, b, c) {
3902
+ const p = a + b - c;
3903
+ const pa = Math.abs(p - a);
3904
+ const pb = Math.abs(p - b);
3905
+ const pc = Math.abs(p - c);
3906
+ if (pa <= pb && pa <= pc) return a;
3907
+ return pb <= pc ? b : c;
3908
+ }
3909
+ function toRgba(data, width, height, colorType, bytesPerSample, palette, paletteAlpha) {
3910
+ const pixels = new Uint8Array(width * height * 4);
3911
+ const bpp = (CHANNELS[colorType] ?? 4) * bytesPerSample;
3912
+ for (let i = 0; i < width * height; i++) {
3913
+ const at = (channel) => data[i * bpp + channel * bytesPerSample] ?? 0;
3914
+ const out = i * 4;
3915
+ switch (colorType) {
3916
+ case 0: {
3917
+ const g = at(0);
3918
+ pixels[out] = g;
3919
+ pixels[out + 1] = g;
3920
+ pixels[out + 2] = g;
3921
+ pixels[out + 3] = 255;
3922
+ break;
3923
+ }
3924
+ case 2:
3925
+ pixels[out] = at(0);
3926
+ pixels[out + 1] = at(1);
3927
+ pixels[out + 2] = at(2);
3928
+ pixels[out + 3] = 255;
3929
+ break;
3930
+ case 3: {
3931
+ const index = at(0);
3932
+ pixels[out] = palette?.[index * 3] ?? 0;
3933
+ pixels[out + 1] = palette?.[index * 3 + 1] ?? 0;
3934
+ pixels[out + 2] = palette?.[index * 3 + 2] ?? 0;
3935
+ pixels[out + 3] = paletteAlpha?.[index] ?? 255;
3936
+ break;
3937
+ }
3938
+ case 4: {
3939
+ const g = at(0);
3940
+ pixels[out] = g;
3941
+ pixels[out + 1] = g;
3942
+ pixels[out + 2] = g;
3943
+ pixels[out + 3] = at(1);
3944
+ break;
3945
+ }
3946
+ default:
3947
+ pixels[out] = at(0);
3948
+ pixels[out + 1] = at(1);
3949
+ pixels[out + 2] = at(2);
3950
+ pixels[out + 3] = at(3);
3951
+ }
3952
+ }
3953
+ return pixels;
3954
+ }
3955
+ /**
3956
+ * Most common non-neutral colour in a logo, as a hex string.
3957
+ *
3958
+ * Nearly transparent pixels are ignored so a transparent background does not win, and greys
3959
+ * are ignored because a logo's black wordmark is not its brand colour. Colours are bucketed
3960
+ * into 16 levels per channel so anti-aliased edges group with the solid fill they belong to,
3961
+ * then the winning bucket reports the average of the real pixels inside it rather than the
3962
+ * bucket centre, which would drift the hue.
3963
+ */
3964
+ function dominantNonNeutralColor(png) {
3965
+ const buckets = /* @__PURE__ */ new Map();
3966
+ for (let i = 0; i < png.pixels.length; i += 4) {
3967
+ if ((png.pixels[i + 3] ?? 0) < 128) continue;
3968
+ const r = png.pixels[i] ?? 0;
3969
+ const g = png.pixels[i + 1] ?? 0;
3970
+ const b = png.pixels[i + 2] ?? 0;
3971
+ if (Math.max(r, g, b) - Math.min(r, g, b) < 16) continue;
3972
+ const key = r >> 4 << 8 | g >> 4 << 4 | b >> 4;
3973
+ const bucket = buckets.get(key);
3974
+ if (bucket === void 0) buckets.set(key, {
3975
+ count: 1,
3976
+ r,
3977
+ g,
3978
+ b
3979
+ });
3980
+ else {
3981
+ bucket.count += 1;
3982
+ bucket.r += r;
3983
+ bucket.g += g;
3984
+ bucket.b += b;
3985
+ }
3986
+ }
3987
+ let best;
3988
+ for (const bucket of buckets.values()) if (best === void 0 || bucket.count > best.count) best = bucket;
3989
+ if (best === void 0) return;
3990
+ const channel = (total) => Math.round(total / best.count).toString(16).padStart(2, "0");
3991
+ return `#${channel(best.r)}${channel(best.g)}${channel(best.b)}`;
3992
+ }
3993
+
3994
+ //#endregion
3995
+ //#region src/brand/tailwind.ts
3996
+ const COLOR_PROP = /--color-([a-zA-Z0-9-]+)\s*:\s*([^;]+);/g;
3997
+ const CUSTOM_PROP = /--([a-zA-Z0-9-]+)\s*:\s*([^;]+);/g;
3998
+ const SKIP_PREFIXES = /* @__PURE__ */ new Set([
3999
+ "chart",
4000
+ "sidebar",
4001
+ "ring",
4002
+ "border",
4003
+ "input",
4004
+ "destructive"
4005
+ ]);
4006
+ function extractTailwindV4Colors(css) {
4007
+ const blocks = themeBlocks(css);
4008
+ const custom = collectCustomProperties(css);
4009
+ const fromTheme = {};
4010
+ for (const block of blocks) {
4011
+ COLOR_PROP.lastIndex = 0;
4012
+ let match;
4013
+ while ((match = COLOR_PROP.exec(block)) !== null) {
4014
+ const name = match[1];
4015
+ const raw = match[2];
4016
+ if (name === void 0 || raw === void 0 || shouldSkip(name)) continue;
4017
+ fromTheme[name] = raw.trim();
4018
+ }
4019
+ }
4020
+ const resolved = {};
4021
+ for (const [name, raw] of Object.entries(fromTheme)) {
4022
+ const hex = resolveToHex(raw, custom, 0);
4023
+ if (hex !== void 0) resolved[name] = hex;
4024
+ }
4025
+ return mapBrandColors(resolved);
4026
+ }
4027
+ function extractTailwindV3Colors(source) {
4028
+ const found = {};
4029
+ const re = /\b(primary|brand|accent|background|foreground|muted|secondary)\b\s*:\s*["'`]([^"'`]+)["'`]/g;
4030
+ let match;
4031
+ while ((match = re.exec(source)) !== null) {
4032
+ const name = match[1];
4033
+ const raw = match[2];
4034
+ if (name === void 0 || raw === void 0) continue;
4035
+ const hex = parseCssColor(raw);
4036
+ if (hex !== void 0) found[name] = hex;
4037
+ }
4038
+ return mapBrandColors(found);
4039
+ }
4040
+ function themeBlocks(css) {
4041
+ const blocks = [];
4042
+ const re = /@theme(?:\s+inline)?\s*\{/g;
4043
+ let match;
4044
+ while ((match = re.exec(css)) !== null) {
4045
+ const open = css.indexOf("{", match.index);
4046
+ if (open === -1) break;
4047
+ const close = matchingBrace(css, open);
4048
+ if (close === -1) break;
4049
+ blocks.push(css.slice(open + 1, close));
4050
+ }
4051
+ return blocks;
4052
+ }
4053
+ function matchingBrace(source, openIndex) {
4054
+ let depth = 0;
4055
+ for (let i = openIndex; i < source.length; i += 1) {
4056
+ const ch = source[i];
4057
+ if (ch === "{") depth += 1;
4058
+ else if (ch === "}") {
4059
+ depth -= 1;
4060
+ if (depth === 0) return i;
4061
+ }
4062
+ }
4063
+ return -1;
4064
+ }
4065
+ function collectCustomProperties(css) {
4066
+ const props = /* @__PURE__ */ new Map();
4067
+ CUSTOM_PROP.lastIndex = 0;
4068
+ let match;
4069
+ while ((match = CUSTOM_PROP.exec(css)) !== null) {
4070
+ const name = match[1];
4071
+ const value = match[2];
4072
+ if (name !== void 0 && value !== void 0) props.set(`--${name}`, value.trim());
4073
+ }
4074
+ return props;
4075
+ }
4076
+ function resolveToHex(raw, custom, depth) {
4077
+ if (depth > 3) return;
4078
+ const direct = parseCssColor(raw);
4079
+ if (direct !== void 0) return direct;
4080
+ const varMatch = /^var\(\s*(--[a-zA-Z0-9-]+)\s*(?:,[^)]+)?\)$/.exec(raw);
4081
+ if (varMatch === null) return;
4082
+ const ref = varMatch[1];
4083
+ if (ref === void 0) return;
4084
+ const next = custom.get(ref);
4085
+ if (next === void 0) return;
4086
+ return resolveToHex(next, custom, depth + 1);
4087
+ }
4088
+ function shouldSkip(name) {
4089
+ const root = name.split("-")[0];
4090
+ return root !== void 0 && SKIP_PREFIXES.has(root);
4091
+ }
4092
+ function mapBrandColors(found) {
4093
+ const out = {};
4094
+ const background = found.background ?? found.bg;
4095
+ if (background !== void 0) out.background = background;
4096
+ const foreground = found.foreground ?? found.fg;
4097
+ if (foreground !== void 0) out.foreground = foreground;
4098
+ const muted = found["muted-foreground"] ?? found.muted;
4099
+ if (muted !== void 0) out.muted = muted;
4100
+ const primary = found.primary ?? found.brand;
4101
+ if (primary !== void 0) out.primary = primary;
4102
+ const accent = found.accent ?? found.secondary;
4103
+ if (accent !== void 0) out.accent = accent;
4104
+ return out;
4105
+ }
4106
+
4107
+ //#endregion
4108
+ //#region src/brand/detect.ts
4109
+ const execFileAsync = promisify(execFile);
4110
+ const SKIP_DIRS = /* @__PURE__ */ new Set([
4111
+ "node_modules",
4112
+ ".git",
4113
+ "dist",
4114
+ "coverage",
4115
+ ".shipseal",
4116
+ ".next",
4117
+ "build"
4118
+ ]);
4119
+ const packageSchema = z.object({
4120
+ private: z.boolean().optional(),
4121
+ name: z.string().optional(),
4122
+ description: z.string().optional(),
4123
+ homepage: z.string().optional(),
4124
+ repository: z.union([z.string(), z.object({ url: z.string().optional() })]).optional()
4125
+ });
4126
+ async function detectBrand(cwd) {
4127
+ const sources = [];
4128
+ const notes = [];
4129
+ const pkg = await readPackage(cwd);
4130
+ const readme = await readMaybe(join(cwd, "README.md"));
4131
+ const name = detectName(pkg, readme, cwd, sources);
4132
+ const tagline = detectTagline(pkg, readme, sources);
4133
+ const url = await detectUrl(pkg, cwd, sources);
4134
+ const logo = findLogoPair(cwd);
4135
+ if (logo !== void 0) sources.push({
4136
+ field: "logo",
4137
+ source: logo.light
4138
+ });
4139
+ const colors = await detectColors(cwd, logo?.light, sources, notes);
4140
+ const contrast = ensureForegroundContrast(colors.background, colors.foreground);
4141
+ if (contrast.adjusted) {
4142
+ colors.foreground = contrast.foreground;
4143
+ notes.push(`Foreground failed WCAG AA large-text contrast (3:1) against background, so it was set to ${contrast.foreground}.`);
4144
+ upsertSource(sources, "colors.foreground", "contrast adjustment");
4145
+ }
4146
+ applyReadableMuted(colors, sources, notes);
4147
+ applyReadableAccent(colors, sources, notes);
4148
+ const brand = {
4149
+ version: 1,
4150
+ name,
4151
+ colors,
4152
+ fonts: {
4153
+ heading: { ...DEFAULT_BRAND_FONTS.heading },
4154
+ body: { ...DEFAULT_BRAND_FONTS.body },
4155
+ mono: { ...DEFAULT_BRAND_FONTS.mono }
4156
+ },
4157
+ radius: 16,
4158
+ theme: "dark",
4159
+ style: "minimal",
4160
+ tokens: null
4161
+ };
4162
+ if (tagline !== void 0) brand.tagline = tagline;
4163
+ if (url !== void 0) brand.url = url;
4164
+ if (logo !== void 0) brand.logo = logo.dark === void 0 ? { light: logo.light } : {
4165
+ light: logo.light,
4166
+ dark: logo.dark
4167
+ };
4168
+ sources.push({
4169
+ field: "fonts",
4170
+ source: "built-in Geist / vendored Geist Mono"
4171
+ });
4172
+ return {
4173
+ brand,
4174
+ sources,
4175
+ notes
4176
+ };
4177
+ }
4178
+ function detectName(pkg, readme, cwd, sources) {
4179
+ if (pkg?.name !== void 0 && pkg.private !== true) {
4180
+ const fromPkg = stripScope(pkg.name);
4181
+ const h1 = readme === void 0 ? void 0 : extractH1(readme);
4182
+ if (h1 !== void 0 && h1.toLowerCase() === fromPkg.toLowerCase() && h1 !== fromPkg) {
4183
+ sources.push({
4184
+ field: "name",
4185
+ source: "README.md H1"
4186
+ });
4187
+ return h1;
4188
+ }
4189
+ sources.push({
4190
+ field: "name",
4191
+ source: "package.json#name"
4192
+ });
4193
+ return fromPkg;
4194
+ }
4195
+ const h1 = readme === void 0 ? void 0 : extractH1(readme);
4196
+ if (h1 !== void 0) {
4197
+ sources.push({
4198
+ field: "name",
4199
+ source: "README.md H1"
4200
+ });
4201
+ return h1;
4202
+ }
4203
+ sources.push({
4204
+ field: "name",
4205
+ source: "directory name"
4206
+ });
4207
+ const fromDir = basename(cwd);
4208
+ return fromDir.length > 0 ? fromDir : "project";
4209
+ }
4210
+ function detectTagline(pkg, readme, sources) {
4211
+ if (pkg?.description !== void 0 && pkg.description.trim().length > 0) {
4212
+ sources.push({
4213
+ field: "tagline",
4214
+ source: "package.json#description"
4215
+ });
4216
+ return pkg.description.trim();
4217
+ }
4218
+ if (readme !== void 0) {
4219
+ const tagline = extractTagline(readme);
4220
+ if (tagline !== void 0) {
4221
+ sources.push({
4222
+ field: "tagline",
4223
+ source: "README.md first paragraph"
4224
+ });
4225
+ return tagline;
4226
+ }
4227
+ }
4228
+ }
4229
+ async function detectUrl(pkg, cwd, sources) {
4230
+ if (pkg?.homepage !== void 0 && pkg.homepage.length > 0) {
4231
+ sources.push({
4232
+ field: "url",
4233
+ source: "package.json#homepage"
4234
+ });
4235
+ return pkg.homepage;
4236
+ }
4237
+ const repo = repositoryUrl(pkg?.repository);
4238
+ if (repo !== void 0) {
4239
+ sources.push({
4240
+ field: "url",
4241
+ source: "package.json#repository"
4242
+ });
4243
+ return repo;
4244
+ }
4245
+ const remote = await gitRemote(cwd);
4246
+ if (remote !== void 0) {
4247
+ sources.push({
4248
+ field: "url",
4249
+ source: "git remote origin"
4250
+ });
4251
+ return remote;
4252
+ }
4253
+ }
4254
+ async function detectColors(cwd, logoPath, sources, notes) {
4255
+ const merged = {};
4256
+ const files = await listFiles(cwd, 4);
4257
+ const cssFiles = files.filter((file) => file.endsWith(".css"));
4258
+ const cssContents = await Promise.all(cssFiles.map(async (file) => ({
4259
+ file,
4260
+ css: await readMaybe(file)
4261
+ })));
4262
+ for (const { file, css } of cssContents) {
4263
+ if (css === void 0) continue;
4264
+ const fromTheme = extractTailwindV4Colors(css);
4265
+ const fromRoot = extractCssRootColors(css);
4266
+ const rel = relative(cwd, file);
4267
+ applyColors(merged, fromTheme, sources, `Tailwind v4 @theme in ${rel}`);
4268
+ applyColors(merged, fromRoot, sources, `:root variables in ${rel}`);
4269
+ }
4270
+ const configFiles = files.filter((file) => basename(file).startsWith("tailwind.config."));
4271
+ const configContents = await Promise.all(configFiles.map(async (file) => ({
4272
+ file,
4273
+ source: await readMaybe(file)
4274
+ })));
4275
+ for (const { file, source } of configContents) {
4276
+ if (source === void 0) continue;
4277
+ applyColors(merged, extractTailwindV3Colors(source), sources, `Tailwind config ${relative(cwd, file)}`);
4278
+ }
4279
+ const tokenFile = files.find((file) => file.endsWith(".tokens.json"));
4280
+ if (tokenFile !== void 0) {
4281
+ const raw = await readMaybe(tokenFile);
4282
+ if (raw !== void 0) try {
4283
+ const parsed = JSON.parse(raw);
4284
+ applyColors(merged, extractDtcgColors(parsed), sources, relative(cwd, tokenFile));
4285
+ } catch (error) {
4286
+ if (error instanceof SyntaxError) notes.push(`Could not parse design tokens file ${relative(cwd, tokenFile)}.`);
4287
+ else throw error;
4288
+ }
4289
+ }
4290
+ if (merged.primary === void 0 && logoPath !== void 0) {
4291
+ const fromPng = logoPath.endsWith(".png") ? await dominantLogoColor(join(cwd, logoPath)) : void 0;
4292
+ if (fromPng !== void 0) {
4293
+ merged.primary = fromPng;
4294
+ sources.push({
4295
+ field: "colors.primary",
4296
+ source: `dominant colour in ${logoPath}`
4297
+ });
4298
+ }
4299
+ const svg = logoPath.endsWith(".svg") ? await readMaybe(join(cwd, logoPath)) : void 0;
4300
+ if (svg !== void 0) {
4301
+ const fromLogo = firstNonNeutralSvgColor(svg);
4302
+ if (fromLogo !== void 0) {
4303
+ merged.primary = fromLogo;
4304
+ sources.push({
4305
+ field: "colors.primary",
4306
+ source: `SVG fill in ${logoPath}`
4307
+ });
4308
+ }
4309
+ }
4310
+ }
4311
+ const fellBack = [
4312
+ "background",
4313
+ "foreground",
4314
+ "muted",
4315
+ "primary",
4316
+ "accent"
4317
+ ].filter((field) => merged[field] === void 0);
4318
+ if (fellBack.length > 0) notes.push(`Using built-in defaults for ${fellBack.map((f) => `colors.${f}`).join(", ")}. Nothing in this project set them. Edit .shipseal/brand.json to use your own.`);
4319
+ return {
4320
+ background: merged.background ?? DEFAULT_BRAND_COLORS.background,
4321
+ foreground: merged.foreground ?? DEFAULT_BRAND_COLORS.foreground,
4322
+ muted: merged.muted ?? DEFAULT_BRAND_COLORS.muted,
4323
+ primary: merged.primary ?? DEFAULT_BRAND_COLORS.primary,
4324
+ accent: merged.accent ?? DEFAULT_BRAND_COLORS.accent
4325
+ };
4326
+ }
4327
+ function applyReadableMuted(colors, sources, notes) {
4328
+ if (colors.muted === void 0 || readableOnBackground(colors.background, colors.muted)) return;
4329
+ colors.muted = DEFAULT_BRAND_COLORS.muted;
4330
+ notes.push(`Muted text color failed WCAG AA large-text contrast (3:1) against background, so it was set to ${DEFAULT_BRAND_COLORS.muted}.`);
4331
+ upsertSource(sources, "colors.muted", "contrast adjustment");
4332
+ }
4333
+ function applyReadableAccent(colors, sources, notes) {
4334
+ if (colors.accent === void 0 || readableOnBackground(colors.background, colors.accent)) return;
4335
+ colors.accent = colors.primary ?? DEFAULT_BRAND_COLORS.primary;
4336
+ notes.push("Accent failed contrast against background, so it uses the primary color.");
4337
+ upsertSource(sources, "colors.accent", "contrast adjustment");
4338
+ }
4339
+ function applyColors(target, incoming, sources, source) {
4340
+ for (const key of [
4341
+ "background",
4342
+ "foreground",
4343
+ "muted",
4344
+ "primary",
4345
+ "accent"
4346
+ ]) {
4347
+ const value = incoming[key];
4348
+ if (value !== void 0 && target[key] === void 0) {
4349
+ target[key] = value;
4350
+ sources.push({
4351
+ field: `colors.${key}`,
4352
+ source
4353
+ });
4354
+ }
4355
+ }
4356
+ }
4357
+ function upsertSource(sources, field, source) {
4358
+ const existing = sources.find((entry) => entry.field === field);
4359
+ if (existing !== void 0) {
4360
+ existing.source = source;
4361
+ return;
4362
+ }
4363
+ sources.push({
4364
+ field,
4365
+ source
4366
+ });
4367
+ }
4368
+ async function readPackage(cwd) {
4369
+ const raw = await readMaybe(join(cwd, "package.json"));
4370
+ if (raw === void 0) return;
4371
+ try {
4372
+ const parsed = JSON.parse(raw);
4373
+ const result = packageSchema.safeParse(parsed);
4374
+ return result.success ? result.data : void 0;
4375
+ } catch (error) {
4376
+ if (error instanceof SyntaxError) return;
4377
+ throw error;
4378
+ }
4379
+ }
4380
+ async function gitRemote(cwd) {
4381
+ try {
4382
+ const { stdout } = await execFileAsync("git", [
4383
+ "remote",
4384
+ "get-url",
4385
+ "origin"
4386
+ ], { cwd });
4387
+ return normalizeGitUrl(stdout.trim());
4388
+ } catch (error) {
4389
+ if (error instanceof Error) return;
4390
+ throw error;
4391
+ }
4392
+ }
4393
+ async function listFiles(root, maxDepth) {
4394
+ const out = [];
4395
+ const walk = async (dir, depth) => {
4396
+ if (depth < 0) return;
4397
+ let entries;
4398
+ try {
4399
+ entries = await readdir(dir, { withFileTypes: true });
4400
+ } catch {
4401
+ return;
4402
+ }
4403
+ const nested = [];
4404
+ for (const entry of entries) {
4405
+ if (entry.name.startsWith(".") && entry.name !== ".github") continue;
4406
+ const path = join(dir, entry.name);
4407
+ if (entry.isDirectory()) {
4408
+ if (SKIP_DIRS.has(entry.name)) continue;
4409
+ nested.push(walk(path, depth - 1));
4410
+ } else out.push(path);
4411
+ }
4412
+ await Promise.all(nested);
4413
+ };
4414
+ await walk(root, maxDepth);
4415
+ return out;
4416
+ }
4417
+ async function readMaybe(path) {
4418
+ try {
4419
+ return await readFile(path, "utf8");
4420
+ } catch {
4421
+ return;
4422
+ }
4423
+ }
4424
+ function stripScope(name) {
4425
+ const parts = name.split("/");
4426
+ return parts[parts.length - 1] ?? name;
4427
+ }
4428
+ function repositoryUrl(repository) {
4429
+ if (typeof repository === "string") return normalizeGitUrl(repository);
4430
+ if (repository?.url !== void 0) return normalizeGitUrl(repository.url);
4431
+ }
4432
+ function normalizeGitUrl(url) {
4433
+ const ssh = /^git@([^:]+):(.+)$/.exec(url);
4434
+ if (ssh !== null && ssh[1] !== void 0 && ssh[2] !== void 0) return `https://${ssh[1]}/${ssh[2].replace(/\.git$/, "")}`;
4435
+ return url.replace(/^git\+/, "").replace(/\.git$/, "");
4436
+ }
4437
+ /**
4438
+ * Dominant non-neutral colour of a PNG logo, or undefined when the file cannot be read or
4439
+ * carries no colour. Detection reports the colour as not found rather than falling back to a
4440
+ * built-in default that would be presented to the user as "your brand".
4441
+ */
4442
+ async function dominantLogoColor(path) {
4443
+ let buffer;
4444
+ try {
4445
+ buffer = await readFile(path);
4446
+ } catch {
4447
+ return;
4448
+ }
4449
+ const png = decodePng(buffer);
4450
+ if (png === void 0) return;
4451
+ return dominantNonNeutralColor(png);
4452
+ }
4453
+ function firstNonNeutralSvgColor(svg) {
4454
+ const re = /(?:fill|stroke)="([^"]+)"/g;
4455
+ let match;
4456
+ while ((match = re.exec(svg)) !== null) {
4457
+ const raw = match[1];
4458
+ if (raw === void 0 || raw === "none" || raw === "currentColor") continue;
4459
+ const hex = parseCssColor(raw);
4460
+ if (hex !== void 0 && !isNeutralHex(hex)) return hex;
4461
+ }
4462
+ }
4463
+
4464
+ //#endregion
4465
+ //#region src/commands/init.ts
4466
+ async function runInit(options) {
4467
+ const detection = await detectBrand(options.cwd);
4468
+ const brandDir = join(options.cwd, ".shipseal");
4469
+ const brandPath = join(brandDir, "brand.json");
4470
+ const configPath = join(brandDir, "config.json");
4471
+ if (!options.force && (existsSync(brandPath) || existsSync(configPath))) throw new ShipsealError("init.exists", ".shipseal/brand.json or config.json already exists.", "Re-run with --force to overwrite, or edit the files by hand.");
4472
+ if (!options.yes) {
4473
+ if (!await confirmInit(detection)) throw new ShipsealError("init.cancelled", "Init cancelled; no files were written.", "Re-run shipseal init and confirm, or pass --yes to accept detections.");
4474
+ }
4475
+ const brand = brandSchema.parse(detection.brand);
4476
+ const config = configSchema.parse(DEFAULT_CONFIG);
4477
+ await mkdir(brandDir, { recursive: true });
4478
+ await writeFile(brandPath, `${JSON.stringify(brand, null, 2)}\n`, "utf8");
4479
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
4480
+ await ensureGitignore(options.cwd);
4481
+ const sampleDir = join(brandDir, "output", "sample");
4482
+ await mkdir(sampleDir, { recursive: true });
4483
+ const samplePath = join(sampleDir, "release-hero-og.png");
4484
+ const png = await (await createTakumiRenderer()).render(testCardNode(), {
4485
+ width: FORMATS.og.width,
4486
+ height: FORMATS.og.height,
4487
+ format: "png"
4488
+ });
4489
+ await writeFile(samplePath, png);
4490
+ return {
4491
+ brandPath,
4492
+ configPath,
4493
+ samplePath,
4494
+ detection
4495
+ };
4496
+ }
4497
+ async function confirmInit(detection) {
4498
+ if (!stdin.isTTY) throw new ShipsealError("init.unattended", "stdin is not a TTY, so init cannot ask for confirmation.", "Re-run with --yes to accept the detected brand and write files.");
4499
+ stdout.write("Detected brand:\n");
4500
+ for (const field of detection.sources) stdout.write(` ${field.field}: from ${field.source}\n`);
4501
+ for (const note of detection.notes) stdout.write(` note: ${note}\n`);
4502
+ const rl = createInterface({
4503
+ input: stdin,
4504
+ output: stdout
4505
+ });
4506
+ try {
4507
+ const answer = await rl.question("Write .shipseal/brand.json and config.json? [y/N] ");
4508
+ return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
4509
+ } finally {
4510
+ rl.close();
4511
+ }
4512
+ }
4513
+ async function ensureGitignore(cwd) {
4514
+ const path = join(cwd, ".gitignore");
4515
+ const entry = ".shipseal/output/";
4516
+ if (!existsSync(path)) {
4517
+ await writeFile(path, `${entry}\n`, "utf8");
4518
+ return;
4519
+ }
4520
+ const current = await readFile(path, "utf8");
4521
+ if (current.split(/\r?\n/).some((line) => line.trim() === entry || line.trim() === ".shipseal/output")) return;
4522
+ const prefix = current.endsWith("\n") || current.length === 0 ? "" : "\n";
4523
+ await writeFile(path, `${current}${prefix}${entry}\n`, "utf8");
4524
+ }
4525
+
4526
+ //#endregion
4527
+ //#region src/commands/milestone.ts
4528
+ async function runMilestone(flags) {
4529
+ const cwd = flags.cwd;
4530
+ const config = overlayFormats(await loadConfig(cwd), flags);
4531
+ const brand = await loadBrand(cwd);
4532
+ const placeholder = {
4533
+ kind: "milestone",
4534
+ metric: "stars",
4535
+ threshold: 0
4536
+ };
4537
+ const env = collectEnv(flags);
4538
+ const collectOpts = {
4539
+ cwd,
4540
+ event: placeholder,
4541
+ skipNetwork: env.skipNetwork
4542
+ };
4543
+ if (env.packagePath !== void 0) collectOpts.packagePath = env.packagePath;
4544
+ if (env.githubToken !== void 0) collectOpts.githubToken = env.githubToken;
4545
+ if (env.fetchImpl !== void 0) collectOpts.fetchImpl = env.fetchImpl;
4546
+ const collected = await collectFacts(collectOpts);
4547
+ const selected = selectMilestone(collected, config, parseMetric(flags.metric), parseThreshold(flags.threshold));
4548
+ if (selected === void 0) return {
4549
+ dryRun: flags.dryRun === true,
4550
+ skipped: true,
4551
+ message: "No milestone threshold crossed. Nothing to generate.",
4552
+ facts: collected,
4553
+ warnings: [],
4554
+ exitCode: 0
4555
+ };
4556
+ const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
4557
+ const metricKey = selected.metric === "downloads" ? "downloads" : selected.metric;
4558
+ const facts = {
4559
+ ...collected,
4560
+ milestone: {
4561
+ metric: fact(selected.metric, {
4562
+ source: "user-config",
4563
+ ref: "shipseal milestone --metric",
4564
+ fetchedAt
4565
+ }),
4566
+ threshold: fact(selected.threshold, {
4567
+ source: "user-config",
4568
+ ref: `config.json milestones.${metricKey}`,
4569
+ fetchedAt
4570
+ })
4571
+ }
4572
+ };
4573
+ const event = {
4574
+ kind: "milestone",
4575
+ metric: selected.metric,
4576
+ threshold: selected.threshold
4577
+ };
4578
+ const fallback = milestoneCopy(facts, selected.metric, selected.threshold);
4579
+ const resolved = await resolveCopy(facts, config, flags.copy === false, fallback);
4580
+ if (flags.dryRun === true) {
4581
+ const dry = {
4582
+ dryRun: true,
4583
+ skipped: false,
4584
+ facts,
4585
+ copy: resolved.copy,
4586
+ copyMode: resolved.copyMode,
4587
+ warnings: [],
4588
+ exitCode: 0
4589
+ };
4590
+ if (resolved.warning !== void 0) dry.copyWarning = resolved.warning;
4591
+ return dry;
4592
+ }
4593
+ const renderer = await createTakumiRenderer();
4594
+ const generateInput = {
4595
+ event,
4596
+ facts,
4597
+ brand,
4598
+ config,
4599
+ copy: resolved.copy,
4600
+ copyMode: resolved.copyMode,
4601
+ renderer,
4602
+ themes: resolveThemes(flags.themes, brand.theme),
4603
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
4604
+ };
4605
+ const logos = await loadLogos(cwd, brand);
4606
+ if (logos !== void 0) generateInput.logos = logos;
4607
+ const packInput = {
4608
+ flags,
4609
+ config,
4610
+ event,
4611
+ result: await generate(generateInput),
4612
+ brand,
4613
+ copy: resolved.copy,
4614
+ copyMode: resolved.copyMode,
4615
+ facts
4616
+ };
4617
+ if (resolved.warning !== void 0) packInput.copyWarning = resolved.warning;
4618
+ return {
4619
+ ...await finishPack(packInput),
4620
+ skipped: false
4621
+ };
4622
+ }
4623
+ function highestCrossed(thresholds, current) {
4624
+ return thresholds.filter((item) => current >= item).toSorted((a, b) => b - a)[0];
4625
+ }
4626
+ function selectMilestone(facts, config, metric, threshold) {
4627
+ const metrics = metric === void 0 ? [
4628
+ "stars",
4629
+ "downloads",
4630
+ "contributors"
4631
+ ] : [metric];
4632
+ const candidates = [];
4633
+ for (const item of metrics) {
4634
+ const current = currentValue(facts, item);
4635
+ if (current === void 0) {
4636
+ if (metric !== void 0) throw missingMetric(item);
4637
+ continue;
4638
+ }
4639
+ const list = thresholdsFor(config, item);
4640
+ if (threshold !== void 0) {
4641
+ if (metric !== void 0 || list.includes(threshold)) return {
4642
+ metric: item,
4643
+ threshold
4644
+ };
4645
+ continue;
4646
+ }
4647
+ const crossed = highestCrossed(list, current);
4648
+ if (crossed !== void 0) candidates.push({
4649
+ metric: item,
4650
+ threshold: crossed
4651
+ });
4652
+ }
4653
+ return candidates.toSorted((a, b) => b.threshold - a.threshold || metrics.indexOf(a.metric) - metrics.indexOf(b.metric))[0];
4654
+ }
4655
+ function currentValue(facts, metric) {
4656
+ if (metric === "stars") return facts.metrics?.stars?.value;
4657
+ if (metric === "downloads") return facts.metrics?.weeklyDownloads?.value;
4658
+ return facts.metrics?.contributorCount?.value;
4659
+ }
4660
+ function thresholdsFor(config, metric) {
4661
+ if (metric === "stars") return config.milestones?.stars ?? [];
4662
+ if (metric === "downloads") return config.milestones?.downloads ?? [];
4663
+ return config.milestones?.contributors ?? [];
4664
+ }
4665
+ function parseMetric(value) {
4666
+ if (value === void 0) return;
4667
+ if (value === "stars" || value === "downloads" || value === "contributors") return value;
4668
+ throw new ShipsealError("milestone.bad-metric", `Unknown metric "${value}".`, "Use --metric stars, --metric downloads, or --metric contributors.");
4669
+ }
4670
+ function parseThreshold(value) {
4671
+ if (value === void 0) return;
4672
+ const parsed = Number.parseInt(value, 10);
4673
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new ShipsealError("milestone.bad-threshold", `Invalid threshold "${value}".`, "Pass a positive integer, for example --threshold 1000.");
4674
+ return parsed;
4675
+ }
4676
+ function missingMetric(metric) {
4677
+ if (metric === "stars") return new ShipsealError("milestone.missing-stars", "Could not read GitHub stars for this repo.", "Set GITHUB_TOKEN, or confirm package.json repository points at a public GitHub repo.");
4678
+ if (metric === "downloads") return new ShipsealError("milestone.missing-downloads", "Could not read npm weekly downloads for this package.", "Confirm package.json name is published on npm, then retry without --dry-run.");
4679
+ return new ShipsealError("milestone.missing-contributors", "Could not read the GitHub contributor count for this repo.", "Set GITHUB_TOKEN, or confirm package.json repository points at a public GitHub repo.");
4680
+ }
4681
+
4682
+ //#endregion
4683
+ //#region src/commands/release.ts
4684
+ async function runRelease(flags) {
4685
+ const cwd = flags.cwd;
4686
+ const config = overlayReleaseConfig(await loadConfig(cwd), flags);
4687
+ const brand = await loadBrand(cwd);
4688
+ const tag = flags.tag ?? await gitCurrentTag(cwd);
4689
+ if (tag === void 0) throw new ShipsealError("release.no-tag", "No git tag found for this release.", "Create a git tag, or pass --tag vX.Y.Z.");
4690
+ const event = flags.from === void 0 ? {
4691
+ kind: "release",
4692
+ tag
4693
+ } : {
4694
+ kind: "release",
4695
+ tag,
4696
+ previousTag: flags.from
4697
+ };
4698
+ const env = collectEnv(flags);
4699
+ const collectOpts = {
4700
+ cwd,
4701
+ event,
4702
+ skipNetwork: env.skipNetwork
4703
+ };
4704
+ if (env.packagePath !== void 0) collectOpts.packagePath = env.packagePath;
4705
+ if (config.release?.changelogPath !== void 0) collectOpts.changelogPath = config.release.changelogPath;
4706
+ if (config.release?.snippet !== void 0) collectOpts.snippet = config.release.snippet;
4707
+ if (env.githubToken !== void 0) collectOpts.githubToken = env.githubToken;
4708
+ if (env.fetchImpl !== void 0) collectOpts.fetchImpl = env.fetchImpl;
4709
+ const facts = await collectFacts(collectOpts);
4710
+ const noCopy = flags.copy === false;
4711
+ const resolved = await resolveCopy(facts, config, noCopy, deterministicCopy(facts, config.release?.maxHighlights));
4712
+ if (flags.dryRun === true) {
4713
+ const dry = {
4714
+ dryRun: true,
4715
+ facts,
4716
+ copy: resolved.copy,
4717
+ copyMode: resolved.copyMode,
4718
+ warnings: [],
4719
+ exitCode: 0
4720
+ };
4721
+ if (resolved.warning !== void 0) dry.copyWarning = resolved.warning;
4722
+ return dry;
4723
+ }
4724
+ const renderer = await createTakumiRenderer();
4725
+ const generateInput = {
4726
+ event,
4727
+ facts,
4728
+ brand,
4729
+ config,
4730
+ copy: resolved.copy,
4731
+ copyMode: resolved.copyMode,
4732
+ renderer,
4733
+ themes: resolveThemes(flags.themes, brand.theme),
4734
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
4735
+ };
4736
+ const logos = await loadLogos(cwd, brand);
4737
+ if (logos !== void 0) generateInput.logos = logos;
4738
+ const packInput = {
4739
+ flags,
4740
+ config,
4741
+ event,
4742
+ result: await generate(generateInput),
4743
+ brand,
4744
+ copy: resolved.copy,
4745
+ copyMode: resolved.copyMode,
4746
+ facts
4747
+ };
4748
+ if (resolved.warning !== void 0) packInput.copyWarning = resolved.warning;
4749
+ return finishPack(packInput);
4750
+ }
4751
+ function overlayReleaseConfig(config, flags) {
4752
+ const overlay = overlayFormats(config, flags);
4753
+ if (flags.templates === void 0) return overlay;
4754
+ return mergeConfig(DEFAULT_CONFIG, {
4755
+ ...overlay,
4756
+ release: {
4757
+ ...overlay.release,
4758
+ templates: flags.templates.split(",").map((item) => item.trim()).filter((item) => item.length > 0)
4759
+ }
4760
+ });
4761
+ }
4762
+
4763
+ //#endregion
4764
+ //#region src/cli.ts
4765
+ async function runCli(argv = process.argv) {
4766
+ process.exitCode = 0;
4767
+ const cli = cac("shipseal");
4768
+ cli.option("--cwd <path>", "Working directory", { default: process.cwd() });
4769
+ cli.option("--json", "Machine-readable JSON output");
4770
+ cli.option("--quiet", "Suppress non-error output");
4771
+ cli.option("--verbose", "Verbose output");
4772
+ cli.command("init", "Detect brand and write .shipseal/brand.json + config.json").option("--yes", "Accept detections without prompting").option("--force", "Overwrite existing brand.json and config.json").action(async (flags) => {
4773
+ const result = await runInit({
4774
+ cwd: stringFlag(flags.cwd, process.cwd()),
4775
+ yes: flags.yes === true,
4776
+ force: flags.force === true
4777
+ });
4778
+ if (flags.json === true) {
4779
+ process.stdout.write(`${JSON.stringify(result.detection.brand, null, 2)}\n`);
4780
+ return;
4781
+ }
4782
+ if (flags.quiet !== true) {
4783
+ process.stdout.write(`Wrote ${result.brandPath}\n`);
4784
+ process.stdout.write(`Wrote ${result.configPath}\n`);
4785
+ process.stdout.write(`Sample card: ${result.samplePath}\n`);
4786
+ for (const field of result.detection.sources) process.stdout.write(` ${field.field}: ${field.source}\n`);
4787
+ for (const note of result.detection.notes) process.stdout.write(` note: ${note}\n`);
4788
+ }
4789
+ });
4790
+ cli.command("doctor", "Check Node, git, brand, fonts, and Takumi").action(async (flags) => {
4791
+ const result = await runDoctor(stringFlag(flags.cwd, process.cwd()));
4792
+ if (flags.json === true) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
4793
+ else if (flags.quiet !== true) for (const check of result.checks) {
4794
+ const mark = check.status === "pass" ? "ok" : check.status === "fail" ? "FAIL" : "info";
4795
+ process.stdout.write(`[${mark}] ${check.name}: ${check.message}\n`);
4796
+ if (check.fix !== void 0 && check.status === "fail") process.stdout.write(` Fix: ${check.fix}\n`);
4797
+ }
4798
+ if (!result.ok) process.exitCode = 1;
4799
+ });
4800
+ cli.command("release", "Generate the release pack").option("--tag <tag>", "Release tag").option("--from <tag>", "Previous tag").option("--formats <list>", "Comma-separated formats").option("--templates <list>", "Comma-separated templates").option("--themes <dark|light|both>", "Themes to render").option("--copy", "Allow optional LLM copy (default: on when config.copy.llm is true)").option("--out <dir>", "Output directory").option("--strict", "Fit warnings exit with code 2").option("--dry-run", "Collect facts and print them; render nothing").option("--package <path>", "package.json path for monorepos").option("--upload", "Upload PNG files to the GitHub release").action(async (flags) => {
4801
+ const releaseFlags = {
4802
+ cwd: stringFlag(flags.cwd, process.cwd()),
4803
+ strict: flags.strict === true,
4804
+ dryRun: flags.dryRun === true
4805
+ };
4806
+ applySharedFlags(releaseFlags, flags);
4807
+ const tag = optionalString(flags.tag);
4808
+ if (tag !== void 0) releaseFlags.tag = tag;
4809
+ const from = optionalString(flags.from);
4810
+ if (from !== void 0) releaseFlags.from = from;
4811
+ const templates = optionalString(flags.templates);
4812
+ if (templates !== void 0) releaseFlags.templates = templates;
4813
+ const result = await runRelease(releaseFlags);
4814
+ process.exitCode = result.exitCode;
4815
+ writeCommandOutput(flags, result);
4816
+ });
4817
+ cli.command("milestone", "Generate a milestone card for the highest crossed threshold").option("--metric <stars|downloads|contributors>", "Metric to check").option("--threshold <n>", "Force a specific threshold").option("--formats <list>", "Comma-separated formats").option("--themes <dark|light|both>", "Themes to render").option("--copy", "Allow optional LLM copy (default: on when config.copy.llm is true)").option("--out <dir>", "Output directory").option("--strict", "Fit warnings exit with code 2").option("--dry-run", "Collect facts and print them; render nothing").option("--package <path>", "package.json path for monorepos").action(async (flags) => {
4818
+ const milestoneFlags = {
4819
+ cwd: stringFlag(flags.cwd, process.cwd()),
4820
+ strict: flags.strict === true,
4821
+ dryRun: flags.dryRun === true
4822
+ };
4823
+ applySharedFlags(milestoneFlags, flags);
4824
+ const metric = optionalString(flags.metric);
4825
+ if (metric !== void 0) milestoneFlags.metric = metric;
4826
+ const threshold = optionalString(flags.threshold);
4827
+ if (threshold !== void 0) milestoneFlags.threshold = threshold;
4828
+ const result = await runMilestone(milestoneFlags);
4829
+ process.exitCode = result.exitCode;
4830
+ if (flags.json === true) {
4831
+ writeJson(resultToJson(result));
4832
+ return;
4833
+ }
4834
+ if (flags.quiet === true) return;
4835
+ if (result.skipped) {
4836
+ process.stdout.write(`${result.message ?? "No milestone threshold crossed."}\n`);
4837
+ return;
4838
+ }
4839
+ writeHumanResult(result);
4840
+ });
4841
+ cli.command("bench", "Generate benchmark cards from bench JSON").option("--file <path>", "Path to bench JSON").option("--formats <list>", "Comma-separated formats").option("--themes <dark|light|both>", "Themes to render").option("--copy", "Allow optional LLM copy (default: on when config.copy.llm is true)").option("--out <dir>", "Output directory").option("--strict", "Fit warnings exit with code 2").option("--dry-run", "Collect facts and print them; render nothing").option("--package <path>", "package.json path for monorepos").action(async (flags) => {
4842
+ const benchFlags = {
4843
+ cwd: stringFlag(flags.cwd, process.cwd()),
4844
+ strict: flags.strict === true,
4845
+ dryRun: flags.dryRun === true
4846
+ };
4847
+ applySharedFlags(benchFlags, flags);
4848
+ const file = optionalString(flags.file);
4849
+ if (file !== void 0) benchFlags.file = file;
4850
+ const result = await runBench(benchFlags);
4851
+ process.exitCode = result.exitCode;
4852
+ writeCommandOutput(flags, result);
4853
+ });
4854
+ cli.help();
4855
+ cli.version(readVersion());
4856
+ try {
4857
+ cli.parse(argv, { run: false });
4858
+ await cli.runMatchedCommand();
4859
+ return typeof process.exitCode === "number" ? process.exitCode : 0;
4860
+ } catch (error) {
4861
+ if (error instanceof ShipsealError) {
4862
+ process.stderr.write(`${formatError(error)}\n`);
4863
+ return 1;
4864
+ }
4865
+ if (error instanceof Error) {
4866
+ process.stderr.write(`${error.message}\n`);
4867
+ return 1;
4868
+ }
4869
+ throw error;
4870
+ }
4871
+ }
4872
+ function stringFlag(value, fallback) {
4873
+ return typeof value === "string" && value.length > 0 ? value : fallback;
4874
+ }
4875
+ function optionalString(value) {
4876
+ return typeof value === "string" && value.length > 0 ? value : void 0;
4877
+ }
4878
+ function applySharedFlags(target, flags) {
4879
+ const formats = optionalString(flags.formats);
4880
+ if (formats !== void 0) target.formats = formats;
4881
+ const themes = optionalString(flags.themes);
4882
+ if (themes !== void 0) target.themes = themes;
4883
+ if (flags.copy === false) target.copy = false;
4884
+ const out = optionalString(flags.out);
4885
+ if (out !== void 0) target.out = out;
4886
+ const pkg = optionalString(flags.package);
4887
+ if (pkg !== void 0) target.package = pkg;
4888
+ if (flags.upload === true) target.upload = true;
4889
+ }
4890
+ function writeCommandOutput(flags, result) {
4891
+ if (flags.json === true) {
4892
+ writeJson(resultToJson(result));
4893
+ return;
4894
+ }
4895
+ if (flags.quiet === true) return;
4896
+ writeHumanResult(result);
4897
+ }
4898
+ function resultToJson(result) {
4899
+ const payload = {};
4900
+ if (result.skipped === true) payload.skipped = true;
4901
+ if (result.message !== void 0) payload.message = result.message;
4902
+ if (result.dir !== void 0) payload.dir = result.dir;
4903
+ if (result.manifest !== void 0) payload.manifest = result.manifest;
4904
+ else payload.facts = result.facts;
4905
+ return payload;
4906
+ }
4907
+ function writeHumanResult(result) {
4908
+ if (result.skipped === true) {
4909
+ process.stdout.write(`${result.message ?? "Nothing to generate."}\n`);
4910
+ return;
4911
+ }
4912
+ if (result.dryRun) {
4913
+ process.stdout.write(`${JSON.stringify(result.facts, null, 2)}\n`);
4914
+ return;
4915
+ }
4916
+ if (result.dir !== void 0) process.stdout.write(`Wrote ${result.dir}\n`);
4917
+ if (result.copyWarning !== void 0) process.stdout.write(`Copy: ${result.copyWarning}\n`);
4918
+ for (const warning of result.warnings) process.stdout.write(`Warning: ${warning.template} ${warning.format} ${warning.slot} ${warning.action}\n`);
4919
+ }
4920
+ function writeJson(value) {
4921
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
4922
+ }
4923
+ function readVersion() {
4924
+ const raw = JSON.parse(readFileSync(join(resolvePackageRoot(), "package.json"), "utf8"));
4925
+ if (typeof raw === "object" && raw !== null && "version" in raw && typeof raw.version === "string") return raw.version;
4926
+ return "0.0.0";
4927
+ }
4928
+ if (shouldRun(process.argv[1])) runCli().then((code) => {
4929
+ process.exit(code);
4930
+ });
4931
+ function shouldRun(invoked) {
4932
+ if (invoked === void 0) return false;
4933
+ const normalized = invoked.replaceAll("\\", "/");
4934
+ return normalized.endsWith("/cli.js") || normalized.endsWith("/cli.ts") || normalized.endsWith("/shipseal");
4935
+ }
10
4936
 
11
4937
  //#endregion
12
- export { };
4938
+ export { runCli };
13
4939
  //# sourceMappingURL=cli.js.map