@koda-sl/baker-cli 0.217.0 → 0.223.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  ulid,
59
59
  validateCanvasDeep,
60
60
  ytDlpBlockSignal
61
- } from "./chunk-EKLAHWSF.js";
61
+ } from "./chunk-CMPAHYLB.js";
62
62
  import {
63
63
  csvOrJson,
64
64
  daysAgoIso,
@@ -21006,10 +21006,225 @@ Full guide: __tooling__/docs/tools/baker/avatars.md`
21006
21006
  import { defineCommand as defineCommand94 } from "citty";
21007
21007
 
21008
21008
  // src/commands/brand/fonts.ts
21009
- import { mkdir, readFile, writeFile } from "fs/promises";
21010
- import path from "path";
21009
+ import { mkdir, readdir, readFile as readFile2, writeFile } from "fs/promises";
21010
+ import path2 from "path";
21011
21011
  import { defineCommand as defineCommand93 } from "citty";
21012
21012
 
21013
+ // src/engine/brand/fonts.ts
21014
+ import { posix } from "path";
21015
+
21016
+ // src/engine/landing/lib/brand-tokens.ts
21017
+ import { readFile } from "fs/promises";
21018
+ import path from "path";
21019
+
21020
+ // src/engine/landing/lib/color.ts
21021
+ var NEUTRAL_COLOR_KEYWORDS = /* @__PURE__ */ new Set([
21022
+ "transparent",
21023
+ "currentcolor",
21024
+ "black",
21025
+ "white",
21026
+ "gray",
21027
+ "grey",
21028
+ "silver",
21029
+ "dimgray",
21030
+ "dimgrey",
21031
+ "darkgray",
21032
+ "darkgrey",
21033
+ "lightgray",
21034
+ "lightgrey",
21035
+ "gainsboro",
21036
+ "whitesmoke"
21037
+ ]);
21038
+ function hexChannels(color) {
21039
+ const long = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})(?:[0-9a-f]{2})?$/i);
21040
+ if (long)
21041
+ return [
21042
+ Number.parseInt(long[1] ?? "0", 16),
21043
+ Number.parseInt(long[2] ?? "0", 16),
21044
+ Number.parseInt(long[3] ?? "0", 16)
21045
+ ];
21046
+ const short = color.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])(?:[0-9a-f])?$/i);
21047
+ if (short) {
21048
+ const dup = (s) => Number.parseInt(`${s ?? "0"}${s ?? "0"}`, 16);
21049
+ return [dup(short[1]), dup(short[2]), dup(short[3])];
21050
+ }
21051
+ return null;
21052
+ }
21053
+ function parseColor(raw) {
21054
+ const c = String(raw || "").trim().toLowerCase();
21055
+ if (!c || c === "transparent") return null;
21056
+ const rgb = c.match(/rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:[\s,/]+([\d.]+%?))?\s*\)/i);
21057
+ if (rgb) {
21058
+ const alpha = rgb[4];
21059
+ const a = alpha === void 0 ? 1 : alpha.endsWith("%") ? Number.parseFloat(alpha) / 100 : Number.parseFloat(alpha);
21060
+ return { r: Number(rgb[1]), g: Number(rgb[2]), b: Number(rgb[3]), a };
21061
+ }
21062
+ const oklch = parseOklch(c);
21063
+ if (oklch) return oklch;
21064
+ const hex = hexChannels(c);
21065
+ if (hex) return { r: hex[0], g: hex[1], b: hex[2], a: 1 };
21066
+ return null;
21067
+ }
21068
+ function parseOklch(c) {
21069
+ const m = c.match(/oklch\(\s*([\d.]+%?)\s+([\d.]+%?)\s+([\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+%?))?\s*\)/i);
21070
+ if (!m) return null;
21071
+ const pct = (s, scale) => s.endsWith("%") ? Number.parseFloat(s) / 100 * scale : Number.parseFloat(s);
21072
+ const alpha = m[4];
21073
+ const a = alpha === void 0 ? 1 : pct(alpha, 1);
21074
+ return { ...oklchToRgb(pct(m[1] ?? "0", 1), pct(m[2] ?? "0", 0.4), Number.parseFloat(m[3] ?? "0")), a };
21075
+ }
21076
+ function oklchToRgb(L, C, H) {
21077
+ const hRad = H * Math.PI / 180;
21078
+ const a = C * Math.cos(hRad);
21079
+ const b = C * Math.sin(hRad);
21080
+ const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
21081
+ const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
21082
+ const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
21083
+ const lin = [
21084
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
21085
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
21086
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
21087
+ ].map((v) => {
21088
+ const g = v <= 31308e-7 ? 12.92 * v : 1.055 * Math.sign(v) * Math.abs(v) ** (1 / 2.4) - 0.055;
21089
+ return Math.round(Math.max(0, Math.min(1, g)) * 255);
21090
+ });
21091
+ return { r: lin[0] ?? 0, g: lin[1] ?? 0, b: lin[2] ?? 0 };
21092
+ }
21093
+ function isNeutralAuthoredColor(rawColor) {
21094
+ const c = String(rawColor || "").trim().toLowerCase();
21095
+ if (!c) return false;
21096
+ if (NEUTRAL_COLOR_KEYWORDS.has(c)) return true;
21097
+ if (/^rgba?\(/i.test(c)) {
21098
+ const channels2 = c.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);
21099
+ if (channels2) {
21100
+ const values = [1, 2, 3].map((i) => Number(channels2[i]));
21101
+ return Math.max(...values) - Math.min(...values) < 30;
21102
+ }
21103
+ return false;
21104
+ }
21105
+ const oklch = c.match(/oklch\(\s*[\d.]+%?\s+([\d.-]+)/i);
21106
+ if (oklch) return Number.parseFloat(oklch[1] ?? "0") < 0.02;
21107
+ const lch = c.match(/lch\(\s*[\d.]+%?\s+([\d.-]+)/i);
21108
+ if (lch) return Number.parseFloat(lch[1] ?? "0") < 3;
21109
+ const hsl = c.match(/hsla?\(\s*[\d.-]+\s*,?\s*([\d.]+)%/i);
21110
+ if (hsl) return Number.parseFloat(hsl[1] ?? "0") < 10;
21111
+ const channels = hexChannels(c);
21112
+ if (channels) return Math.max(...channels) - Math.min(...channels) < 30;
21113
+ return false;
21114
+ }
21115
+ function hasChroma(c, threshold = 30) {
21116
+ if (!c) return false;
21117
+ return Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b) >= threshold;
21118
+ }
21119
+ function getHue(c) {
21120
+ if (!c) return 0;
21121
+ const r = c.r / 255;
21122
+ const g = c.g / 255;
21123
+ const b = c.b / 255;
21124
+ const max = Math.max(r, g, b);
21125
+ const min = Math.min(r, g, b);
21126
+ if (max === min) return 0;
21127
+ const d = max - min;
21128
+ let h;
21129
+ if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
21130
+ else if (max === g) h = ((b - r) / d + 2) / 6;
21131
+ else h = ((r - g) / d + 4) / 6;
21132
+ return Math.round(h * 360);
21133
+ }
21134
+ function isAiPurpleHue(hue) {
21135
+ return hue >= 225 && hue <= 310;
21136
+ }
21137
+ function isCreamColor(c) {
21138
+ if (Math.min(c.r, c.g, c.b) < 209) return false;
21139
+ if (!(c.r >= c.g && c.g >= c.b)) return false;
21140
+ const warmth = c.r - c.b;
21141
+ return warmth >= 6 && warmth <= 48;
21142
+ }
21143
+
21144
+ // src/engine/landing/lib/brand-tokens.ts
21145
+ var EMPTY = { fonts: /* @__PURE__ */ new Set(), colors: [], hasTokens: false };
21146
+ function isNotAFamily(value) {
21147
+ return /^[1-9]00$/.test(value) || /^\d+(?:\.\d+)?(?:px|rem|em|%|pt|vw|vh|ch|ex)$/i.test(value) || /^(?:normal|bold|bolder|lighter|italic|oblique|none|auto|inherit|initial|unset)$/i.test(value);
21148
+ }
21149
+ function parseBrandTokens(globalCss, brandMd) {
21150
+ const fonts = /* @__PURE__ */ new Set();
21151
+ const colors = [];
21152
+ parseGlobalCss(globalCss, fonts, colors);
21153
+ parseBrandMd(brandMd, fonts, colors);
21154
+ return { fonts, colors, hasTokens: fonts.size > 0 || colors.length > 0 };
21155
+ }
21156
+ function parseGlobalCss(rawCss, fonts, colors) {
21157
+ const globalCss = rawCss.replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, " "));
21158
+ for (const m of globalCss.matchAll(/(?<![\w-])--font-([\w-]*)\s*:\s*([^;]+);/gi)) {
21159
+ const slug = m[1] ?? "";
21160
+ if (slug.includes("--") || /^weight(?:$|[-\s])/i.test(slug)) continue;
21161
+ const first = (m[2] ?? "").split(",")[0]?.trim().replace(/^['"]|['"]$/g, "").toLowerCase();
21162
+ if (!first || isNotAFamily(first)) continue;
21163
+ fonts.add(first);
21164
+ }
21165
+ for (const m of globalCss.matchAll(
21166
+ /--color-[\w-]*\s*:\s*(#[0-9a-f]{3,8}\b|rgba?\([^)]+\)|oklch\([^)]+\)|hsla?\([^)]+\))/gi
21167
+ )) {
21168
+ const c = parseColor(m[1] ?? "");
21169
+ if (c) colors.push(c);
21170
+ }
21171
+ }
21172
+ function parseBrandMd(brandMd, fonts, colors) {
21173
+ const isProhibition = (line) => /\b(?:never|don'?t|do not|avoid|not|instead of|ban(?:ned)?)\b/i.test(line);
21174
+ for (const line of brandMd.split("\n")) {
21175
+ if (isProhibition(line)) continue;
21176
+ for (const m of line.matchAll(/#[0-9a-f]{6}\b/gi)) {
21177
+ const c = parseColor(m[0]);
21178
+ if (c) colors.push(c);
21179
+ }
21180
+ if (!/\b(?:font|typeface|family)\b/i.test(line)) continue;
21181
+ const named = line.match(/["'`]([A-Za-z][\w ]{1,30})["'`]/g);
21182
+ if (named) for (const n of named) fonts.add(n.slice(1, -1).trim().toLowerCase());
21183
+ }
21184
+ }
21185
+ function brandTokensFromCanonical(tokens) {
21186
+ const fonts = new Set(tokens.fonts.map((f) => f.family.toLowerCase()));
21187
+ const colors = [];
21188
+ for (const color of tokens.colors) {
21189
+ const parsed = parseColor(color.value);
21190
+ if (parsed) colors.push(parsed);
21191
+ }
21192
+ return { fonts, colors, hasTokens: fonts.size > 0 || colors.length > 0 };
21193
+ }
21194
+ async function loadBrandTokens(projectRoot) {
21195
+ const tokensRaw = await safeRead(path.join(projectRoot, "src", "brand", "tokens.json"));
21196
+ if (tokensRaw) {
21197
+ try {
21198
+ const canonical2 = brandTokensSchema.safeParse(JSON.parse(tokensRaw));
21199
+ if (canonical2.success) return brandTokensFromCanonical(canonical2.data);
21200
+ } catch {
21201
+ }
21202
+ }
21203
+ const globalCss = await safeRead(path.join(projectRoot, "src", "styles", "global.css"));
21204
+ const brandMd = await safeRead(path.join(projectRoot, "src", "brand", "BRAND.md"));
21205
+ if (!globalCss && !brandMd) return EMPTY;
21206
+ return parseBrandTokens(globalCss, brandMd);
21207
+ }
21208
+ async function safeRead(file) {
21209
+ try {
21210
+ return await readFile(file, "utf8");
21211
+ } catch {
21212
+ return "";
21213
+ }
21214
+ }
21215
+ function brandCommitsFont(brand, snippet) {
21216
+ if (brand.fonts.size === 0) return false;
21217
+ const lower = snippet.toLowerCase();
21218
+ for (const f of brand.fonts) if (f.length >= 3 && lower.includes(f)) return true;
21219
+ return false;
21220
+ }
21221
+ function brandCommitsAiHue(brand) {
21222
+ return brand.colors.some((c) => hasChroma(c, 40) && isAiPurpleHue(getHue(c)));
21223
+ }
21224
+ function brandCommitsCream(brand) {
21225
+ return brand.colors.some((c) => isCreamColor(c));
21226
+ }
21227
+
21013
21228
  // src/engine/brand/fonts.ts
21014
21229
  var GOOGLE_CSS2 = "https://fonts.googleapis.com/css2";
21015
21230
  function parseGoogleFontFaces(css) {
@@ -21055,6 +21270,157 @@ function renderFontFaceCss(faces) {
21055
21270
  ].join("\n")
21056
21271
  ).join("\n");
21057
21272
  }
21273
+ var FONT_PROVIDERS = ["google", "fontsource"];
21274
+ var FONTSOURCE_API = "https://api.fontsource.org/v1";
21275
+ function fontsourceId(family) {
21276
+ return family.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
21277
+ }
21278
+ function fontsourceFaces(meta, weights, subsets) {
21279
+ const faces = [];
21280
+ for (const subset of subsets.filter((s) => meta.subsets.includes(s))) {
21281
+ for (const weight of weights.filter((w) => meta.weights.includes(w))) {
21282
+ const url = meta.variants[String(weight)]?.normal?.[subset]?.url?.woff2;
21283
+ if (!url) continue;
21284
+ faces.push({
21285
+ subset,
21286
+ family: meta.family,
21287
+ style: "normal",
21288
+ weight,
21289
+ url,
21290
+ unicodeRange: meta.unicodeRange?.[subset] ?? "",
21291
+ provider: "fontsource"
21292
+ });
21293
+ }
21294
+ }
21295
+ return faces;
21296
+ }
21297
+ function undisambiguatedSubsets(faces) {
21298
+ if (new Set(faces.map((face) => face.subset)).size < 2) return [];
21299
+ return [...new Set(faces.filter((face) => !face.unicodeRange).map((face) => face.subset))];
21300
+ }
21301
+ function suggestFamilies(wanted, catalogue, limit = 5) {
21302
+ const key = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
21303
+ const target = key(wanted);
21304
+ if (!target) return [];
21305
+ const words3 = target.split(" ");
21306
+ const scored = catalogue.map((family) => {
21307
+ const candidate = key(family);
21308
+ if (candidate === target) return { family, score: 0 };
21309
+ if (candidate.startsWith(target)) return { family, score: 1 };
21310
+ if (candidate.includes(target)) return { family, score: 2 };
21311
+ const shared = words3.filter((w) => w.length > 2 && candidate.includes(w)).length;
21312
+ return { family, score: shared > 0 ? 3 + (words3.length - shared) : Number.POSITIVE_INFINITY };
21313
+ }).filter((c) => Number.isFinite(c.score)).sort((a, b) => a.score - b.score || a.family.localeCompare(b.family));
21314
+ return scored.slice(0, limit).map((c) => c.family);
21315
+ }
21316
+ function asFontsourceMeta(body) {
21317
+ if (typeof body !== "object" || body === null) return null;
21318
+ const meta = body;
21319
+ const ok = typeof meta.family === "string" && Array.isArray(meta.weights) && Array.isArray(meta.styles) && Array.isArray(meta.subsets) && typeof meta.variants === "object" && meta.variants !== null;
21320
+ return ok ? meta : null;
21321
+ }
21322
+ async function probeGoogleWeights(family, lookup) {
21323
+ const probes = await Promise.all(
21324
+ STANDARD_WEIGHTS.map(async (weight) => ({ weight, got: await lookup(googleFontCssUrl(family, [weight])) }))
21325
+ );
21326
+ if (probes.some((probe) => probe.got.status === "unreachable")) return null;
21327
+ return probes.filter((probe) => probe.got.status === "served").map((probe) => probe.weight);
21328
+ }
21329
+ async function resolveFromGoogle(family, weights, subsets, deps) {
21330
+ const google = await deps.lookupGoogleCss(googleFontCssUrl(family, weights));
21331
+ if (google.status === "unreachable") return { ok: false, reason: "unreachable", provider: "google" };
21332
+ if (google.status === "absent") {
21333
+ const served2 = await probeGoogleWeights(family, deps.lookupGoogleCss);
21334
+ if (served2 === null) return { ok: false, reason: "unreachable", provider: "google" };
21335
+ return served2.length > 0 ? { ok: false, reason: "weight", provider: "google", available: served2 } : null;
21336
+ }
21337
+ const served = parseGoogleFontFaces(google.css);
21338
+ if (served.length === 0) return null;
21339
+ const faces = served.filter((face) => subsets.includes(face.subset)).map((face) => ({ ...face, provider: "google" }));
21340
+ if (faces.length > 0) return { ok: true, provider: "google", faces };
21341
+ return { ok: false, reason: "subset", provider: "google", available: [...new Set(served.map((f) => f.subset))] };
21342
+ }
21343
+ async function resolveBrandFaces(family, weights, subsets, deps) {
21344
+ const google = await resolveFromGoogle(family, weights, subsets, deps);
21345
+ if (google) return google;
21346
+ let body;
21347
+ try {
21348
+ body = await deps.fetchJson(`${FONTSOURCE_API}/fonts/${fontsourceId(family)}`);
21349
+ } catch {
21350
+ return { ok: false, reason: "unreachable", provider: "fontsource" };
21351
+ }
21352
+ const meta = asFontsourceMeta(body);
21353
+ if (meta) {
21354
+ const faces = fontsourceFaces(meta, weights, subsets);
21355
+ const ambiguous = undisambiguatedSubsets(faces);
21356
+ if (ambiguous.length > 0) {
21357
+ return { ok: false, reason: "ambiguous-subsets", provider: "fontsource", available: meta.subsets };
21358
+ }
21359
+ if (faces.length > 0) return { ok: true, provider: "fontsource", faces };
21360
+ if (!subsets.some((subset) => meta.subsets.includes(subset))) {
21361
+ return { ok: false, reason: "subset", provider: "fontsource", available: meta.subsets };
21362
+ }
21363
+ return { ok: false, reason: "weight", provider: "fontsource", available: meta.weights };
21364
+ }
21365
+ return { ok: false, reason: "unknown-family" };
21366
+ }
21367
+ function rebaseFontFaceSrc(block, fromDir, toDir = "src/styles") {
21368
+ return block.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi, (whole, _quote, href) => {
21369
+ if (!isRepoFontHref(href)) return whole;
21370
+ const target = posix.normalize(posix.join(fromDir, href));
21371
+ return `url('${posix.relative(toDir, target)}')`;
21372
+ });
21373
+ }
21374
+ function declaredFontFaces(source) {
21375
+ const out = [];
21376
+ for (const rule of stripCssComments(source).matchAll(/@font-face\s*\{[^}]*\}/gi)) {
21377
+ const family = rule[0].match(/font-family\s*:\s*([^;]+)/i)?.[1];
21378
+ if (!family || rule.index === void 0) continue;
21379
+ out.push({ family: normalizeFamily(family), block: source.slice(rule.index, rule.index + rule[0].length) });
21380
+ }
21381
+ return out;
21382
+ }
21383
+ function isRepoFontHref(href) {
21384
+ return !/^(?:data:|https?:|\/)/i.test(href);
21385
+ }
21386
+ function missingFontFiles(urls, available) {
21387
+ return urls.filter(isRepoFontHref).map((url) => posix.basename(url.split("?")[0] ?? "")).filter((file) => !available.has(file));
21388
+ }
21389
+ function planFontAdoption(sources, families) {
21390
+ const wanted = new Map(families.map((family) => [normalizeFamily(family), family]));
21391
+ const byFamily = /* @__PURE__ */ new Map();
21392
+ for (const { path: path39, source } of sources) {
21393
+ const dir = posix.dirname(path39);
21394
+ for (const face of declaredFontFaces(source)) {
21395
+ if (!wanted.has(face.family)) continue;
21396
+ const perFile = byFamily.get(face.family) ?? /* @__PURE__ */ new Map();
21397
+ perFile.set(path39, [...perFile.get(path39) ?? [], rebaseFontFaceSrc(face.block, dir)]);
21398
+ byFamily.set(face.family, perFile);
21399
+ }
21400
+ }
21401
+ const plans = [];
21402
+ const conflicts = [];
21403
+ for (const [family, perFile] of byFamily) {
21404
+ const display = wanted.get(family) ?? family;
21405
+ const files = [...perFile.keys()];
21406
+ const shapes = new Set([...perFile.values()].map((blocks2) => blocks2.join("\n")));
21407
+ if (shapes.size > 1) {
21408
+ conflicts.push({ family: display, files });
21409
+ continue;
21410
+ }
21411
+ const blocks = perFile.get(files[0] ?? "") ?? [];
21412
+ const urls = blocks.flatMap(
21413
+ (block) => [...block.matchAll(/url\(\s*['"]?([^'")]+)['"]?\s*\)/gi)].map((m) => m[1] ?? "")
21414
+ );
21415
+ plans.push({ family: display, blocks, files, urls });
21416
+ }
21417
+ return { plans, conflicts };
21418
+ }
21419
+ async function fontsourceCatalogue(deps) {
21420
+ const body = await deps.fetchJson(`${FONTSOURCE_API}/fonts`).catch(() => null);
21421
+ if (!Array.isArray(body)) return [];
21422
+ return body.map((entry) => typeof entry === "object" && entry !== null ? entry.family : null).filter((family) => typeof family === "string");
21423
+ }
21058
21424
  function parseWeightAxis(spec) {
21059
21425
  const axis = spec.match(/wght@([^&]+)/i)?.[1] ?? "";
21060
21426
  const weights = /* @__PURE__ */ new Set();
@@ -21128,25 +21494,132 @@ async function checkFontRequests(requests, fetchCss) {
21128
21494
  }
21129
21495
  return results;
21130
21496
  }
21497
+ var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
21498
+ "sans-serif",
21499
+ "serif",
21500
+ "monospace",
21501
+ "cursive",
21502
+ "fantasy",
21503
+ "system-ui",
21504
+ "ui-sans-serif",
21505
+ "ui-serif",
21506
+ "ui-monospace",
21507
+ "ui-rounded",
21508
+ "arial",
21509
+ "helvetica",
21510
+ "georgia",
21511
+ "times",
21512
+ "courier",
21513
+ "inherit",
21514
+ "initial",
21515
+ "unset"
21516
+ ]);
21517
+ function stripCssComments(css) {
21518
+ return css.replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, " "));
21519
+ }
21520
+ function normalizeFamily(raw) {
21521
+ return raw.trim().replace(/^['"]|['"]$/g, "").trim().toLowerCase();
21522
+ }
21523
+ function fontFaceFamilies(css) {
21524
+ const out = /* @__PURE__ */ new Set();
21525
+ for (const rule of stripCssComments(css).matchAll(/@font-face\s*\{([^}]*)\}/gi)) {
21526
+ const family = (rule[1] ?? "").match(/font-family\s*:\s*([^;]+)/i)?.[1];
21527
+ if (family) out.add(normalizeFamily(family));
21528
+ }
21529
+ return out;
21530
+ }
21531
+ function declaredWeights(body) {
21532
+ const spec = (body.match(/font-weight\s*:\s*([^;}]+)/i)?.[1] ?? "").match(/\d{3}/g) ?? [];
21533
+ if (spec.length !== 2) return spec.map(Number);
21534
+ const range = [];
21535
+ for (let w = Number(spec[0]); w <= Number(spec[1]); w += 100) range.push(w);
21536
+ return range;
21537
+ }
21538
+ function selfHostedWeights(css, family) {
21539
+ const target = normalizeFamily(family);
21540
+ const weights = /* @__PURE__ */ new Set();
21541
+ for (const rule of stripCssComments(css).matchAll(/@font-face\s*\{([^}]*)\}/gi)) {
21542
+ const body = rule[1] ?? "";
21543
+ const declared = body.match(/font-family\s*:\s*([^;]+)/i)?.[1];
21544
+ if (!declared || normalizeFamily(declared) !== target) continue;
21545
+ for (const weight of declaredWeights(body)) weights.add(weight);
21546
+ }
21547
+ return [...weights].sort((a, b) => a - b);
21548
+ }
21549
+ function fontsourceFamilies(css) {
21550
+ const out = /* @__PURE__ */ new Set();
21551
+ for (const imported of stripCssComments(css).matchAll(/@fontsource(-variable)?\/([\w-]+)/gi)) {
21552
+ const slug = imported[2];
21553
+ if (!slug) continue;
21554
+ out.add(`${slug.replaceAll("-", " ")}${imported[1] ? " variable" : ""}`.toLowerCase());
21555
+ }
21556
+ return out;
21557
+ }
21558
+ function asAuthored(css, normalized) {
21559
+ const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
21560
+ const text2 = stripCssComments(css);
21561
+ return text2.match(new RegExp(`["']${escaped}["']`, "i"))?.[0]?.slice(1, -1) ?? text2.match(new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i"))?.[0] ?? normalized;
21562
+ }
21563
+ function unresolvedBrandFamilies(css) {
21564
+ const declared = [...parseBrandTokens(css, "").fonts].filter((family) => !GENERIC_FAMILIES.has(family));
21565
+ const loaded = fontFaceFamilies(css);
21566
+ for (const request of googleFontRequests(stripCssComments(css))) loaded.add(normalizeFamily(request.family));
21567
+ return declared.filter((family) => !loaded.has(family)).map((family) => asAuthored(css, family));
21568
+ }
21569
+ function stripFontFaceBlocks(source, family) {
21570
+ const target = normalizeFamily(family);
21571
+ const ranges = [];
21572
+ for (const rule of stripCssComments(source).matchAll(/@font-face\s*\{[^}]*\}/gi)) {
21573
+ const declared = rule[0].match(/font-family\s*:\s*([^;]+)/i)?.[1];
21574
+ if (declared && normalizeFamily(declared) === target && rule.index !== void 0) {
21575
+ ranges.push([rule.index, rule.index + rule[0].length]);
21576
+ }
21577
+ }
21578
+ let out = source;
21579
+ for (const [start, end] of ranges.reverse()) out = `${out.slice(0, start)}${out.slice(end)}`;
21580
+ return out.replace(/\n{3,}/g, "\n\n");
21581
+ }
21582
+ function upsertFontFaceCss(css, family, block) {
21583
+ const out = stripFontFaceBlocks(css, family);
21584
+ const themeAt = stripCssComments(out).search(/@theme\b/);
21585
+ if (themeAt < 0) return `${out.trimEnd()}
21586
+
21587
+ ${block}
21588
+ `;
21589
+ const head = out.slice(0, themeAt).trimEnd();
21590
+ const tail = out.slice(themeAt);
21591
+ return head ? `${head}
21592
+
21593
+ ${block}
21594
+
21595
+ ${tail}` : `${block}
21596
+
21597
+ ${tail}`;
21598
+ }
21131
21599
 
21132
21600
  // src/commands/brand/fonts.ts
21133
- var GLOBAL_CSS = path.join("src", "styles", "global.css");
21134
- var FONTS_DIR = path.join("src", "brand", "fonts");
21601
+ var GLOBAL_CSS = path2.join("src", "styles", "global.css");
21602
+ var FONTS_DIR = path2.join("src", "brand", "fonts");
21135
21603
  var DEFAULT_SUBSETS = "latin";
21136
21604
  var BROWSER_UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36";
21137
21605
  registerSchema({
21138
21606
  command: "brand.fonts.check",
21139
- description: "Start here when a brand's fonts are 'confirmed' but nobody verified it. Asks Google Fonts what it ACTUALLY serves for every family/weight src/styles/global.css requests, and reports any the brand promises but Google will not serve.",
21607
+ description: "Start here when a brand's fonts are 'confirmed' but nobody verified it. Fails if any --font-* family in src/styles/global.css has nothing loading it (no @font-face, no Google import) \u2014 the silent fallback that also fails the client's verify \u2014 then asks Google Fonts what it ACTUALLY serves for every family/weight the stylesheet requests.",
21608
+ args: {}
21609
+ });
21610
+ registerSchema({
21611
+ command: "brand.fonts.adopt",
21612
+ description: "Move brand faces a page already self-hosts into src/styles/global.css, so every page loads them rather than only the page that declared them. Use when `check` reports a family as not loaded but its files are already in src/brand/fonts/. Never downloads: re-fetching would hand that page a different build of the same face.",
21140
21613
  args: {}
21141
21614
  });
21142
21615
  registerSchema({
21143
21616
  command: "brand.fonts.fetch",
21144
- description: "Download a Google font into src/brand/fonts/ so the brand stops depending on a third party at page load, and print the @font-face block to paste above @theme in src/styles/global.css.",
21617
+ description: "Download a font into src/brand/fonts/ so the brand stops depending on a third party at page load, and write its @font-face into src/styles/global.css above @theme. Resolves against Google Fonts first, then Fontsource \u2014 which carries ~2000 open-licence families Google does not, so a name Google 400s is often still fetchable. Idempotent: re-running replaces that family's faces rather than stacking duplicates.",
21145
21618
  args: {
21146
21619
  family: { type: "string", description: 'Font family, e.g. "DM Sans"', required: true },
21147
21620
  weights: {
21148
21621
  type: "string",
21149
- description: "Comma-separated weights (default: what global.css requests, else 400)",
21622
+ description: "Comma-separated weights (default: the weights global.css already requests or self-hosts, else 400)",
21150
21623
  required: false
21151
21624
  },
21152
21625
  subsets: {
@@ -21161,39 +21634,169 @@ function fail(code, message, fix) {
21161
21634
  process.exit(2);
21162
21635
  }
21163
21636
  var REQUEST_TIMEOUT_MS = 15e3;
21164
- async function fetchGoogleCss(url) {
21637
+ async function fetchJson(url) {
21638
+ const res = await fetch(url, {
21639
+ headers: { "User-Agent": BROWSER_UA },
21640
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
21641
+ });
21642
+ if (res.status === 404) return null;
21643
+ if (!res.ok) throw new Error(`Fontsource answered ${res.status}`);
21644
+ return await res.json();
21645
+ }
21646
+ async function lookupGoogleCss(url) {
21165
21647
  try {
21166
21648
  const res = await fetch(url, {
21167
21649
  headers: { "User-Agent": BROWSER_UA },
21168
21650
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
21169
21651
  });
21170
- if (!res.ok) return null;
21171
- return await res.text();
21652
+ if (res.ok) return { status: "served", css: await res.text() };
21653
+ return { status: res.status === 400 ? "absent" : "unreachable" };
21172
21654
  } catch {
21173
- return null;
21655
+ return { status: "unreachable" };
21656
+ }
21657
+ }
21658
+ var fetchGoogleCss = async (url) => {
21659
+ const got = await lookupGoogleCss(url);
21660
+ return got.status === "served" ? got.css : null;
21661
+ };
21662
+ var WIRING_HINT = {
21663
+ written: (family) => `Wrote the @font-face for "${family}" into ${GLOBAL_CSS}, above @theme. Remove "${family}" from the Google @import if one is still there, so the page stops fetching it at load.`,
21664
+ // Told apart from "no stylesheet" on purpose: they used to share one hint, so
21665
+ // an idempotent re-run instructed the agent to paste a block that was already
21666
+ // in the file, and doing as it was told duplicated the face.
21667
+ "already-wired": (family) => `${GLOBAL_CSS} already loads exactly these faces for "${family}" \u2014 nothing to change.`,
21668
+ "no-stylesheet": () => `No ${GLOBAL_CSS} to write to \u2014 paste the \`fontFace\` block above the @theme block once the stylesheet exists, or the files just downloaded will never load.`
21669
+ };
21670
+ var PROVIDER_DEPS = { lookupGoogleCss, fetchJson };
21671
+ var PROVIDER_LABEL = { google: "Google Fonts", fontsource: "Fontsource" };
21672
+ function fetchHints({
21673
+ family,
21674
+ downloadedWeights,
21675
+ incomplete,
21676
+ wiring,
21677
+ provider
21678
+ }) {
21679
+ return [
21680
+ WIRING_HINT[wiring](family),
21681
+ `Record the exact weights (${downloadedWeights.join(", ")}) in src/brand/BRAND.md \u2014 a weight listed there but not downloaded renders as a synthesized fallback.`,
21682
+ ...incomplete.length > 0 ? [
21683
+ // The remedy is provider-specific: only a Google family has an @import
21684
+ // to fall back on, and telling an agent to keep one for a Fontsource
21685
+ // family sends it to write an import Google will 400.
21686
+ provider === "google" ? `PARTIAL: ${incomplete.join(", ")} did not download and is NOT in the \`fontFace\` block. Keep "${family}" in the Google @import for those weights, or re-run \`baker brand fonts fetch "${family}" --weights ${incomplete.join(",")}\`.` : `PARTIAL: ${incomplete.join(", ")} did not download and is NOT in the \`fontFace\` block. ${PROVIDER_LABEL[provider]} does not serve them, so re-run \`baker brand fonts fetch "${family}" --weights ${downloadedWeights.join(",")}\` and record only those weights in src/brand/BRAND.md.`
21687
+ ] : [],
21688
+ `Licensing: ${PROVIDER_LABEL[provider]} carries open-licensed families, which are safe to self-host. A client's own commercial face is not \u2014 confirm before re-hosting one.`
21689
+ ];
21690
+ }
21691
+ async function wireFontFaceIntoStylesheet(css, family, fontFace) {
21692
+ if (!css) return "no-stylesheet";
21693
+ const wired = upsertFontFaceCss(css, family, fontFace);
21694
+ if (wired === css) return "already-wired";
21695
+ await writeFile(path2.resolve(process.cwd(), GLOBAL_CSS), wired);
21696
+ return "written";
21697
+ }
21698
+ function failResolution(family, weights, outcome) {
21699
+ if (outcome.reason === "unreachable") {
21700
+ return fail(
21701
+ "CATALOGUE_UNREACHABLE",
21702
+ `Could not reach ${PROVIDER_LABEL[outcome.provider]} to ask about "${family}".`,
21703
+ {
21704
+ action: "Run the same command again.",
21705
+ explanation: `${PROVIDER_LABEL[outcome.provider]} did not answer, which is not the same as not having the family \u2014 so nothing was concluded from it. Guessing either way would self-host the wrong build, or report a real family as missing.`,
21706
+ provider: outcome.provider
21707
+ }
21708
+ );
21709
+ }
21710
+ if (outcome.reason === "ambiguous-subsets") {
21711
+ return fail(
21712
+ "SUBSET_AMBIGUOUS",
21713
+ `${PROVIDER_LABEL[outcome.provider]} serves "${family}" in several subsets but reports no unicode-range to tell them apart.`,
21714
+ {
21715
+ action: `Retry with a single --subsets value, e.g. --subsets ${outcome.available[0] ?? "latin"}.`,
21716
+ explanation: "Two @font-face rules with the same family, style and weight and no unicode-range both claim every character, so the last one silently wins and the other file is downloaded and never read.",
21717
+ available: outcome.available
21718
+ }
21719
+ );
21720
+ }
21721
+ if (outcome.reason === "subset") {
21722
+ return fail("SUBSET_NOT_SERVED", `${PROVIDER_LABEL[outcome.provider]} has "${family}", but not in that subset.`, {
21723
+ action: `Retry with --subsets set to one it serves: ${outcome.available.join(",")}.`,
21724
+ explanation: "A family is split into unicode subsets and not every family ships every one. The weights you asked for are fine \u2014 only the subset is wrong.",
21725
+ available: outcome.available
21726
+ });
21727
+ }
21728
+ if (outcome.reason === "weight") {
21729
+ return fail(
21730
+ "FONT_NOT_SERVED",
21731
+ `${PROVIDER_LABEL[outcome.provider]} has "${family}", but not at weight ${weights.join("/")}.`,
21732
+ {
21733
+ action: `Retry with --weights set to one of these: ${outcome.available.join(",")}.`,
21734
+ explanation: "The family exists; the weight you asked for is not one it ships.",
21735
+ served: outcome.available
21736
+ }
21737
+ );
21738
+ }
21739
+ return failUnknownFamily(family);
21740
+ }
21741
+ async function failUnknownFamily(family) {
21742
+ const didYouMean = suggestFamilies(family, await fontsourceCatalogue(PROVIDER_DEPS));
21743
+ return fail("FONT_NOT_SERVED", `No font catalogue carries a family called "${family}".`, {
21744
+ action: didYouMean.length ? `Did you mean one of these? ${didYouMean.join(", ")}. Retry with the exact name, and update the --font-* token in ${GLOBAL_CSS} to match whichever you pick.` : `Check the exact spelling, then retry. If it is a commercial typeface, add the file to ${FONTS_DIR} and write the @font-face by hand \u2014 it cannot be downloaded.`,
21745
+ explanation: `Tried ${FONT_PROVIDERS.join(" then ")}. Family names are case- and space-sensitive in every catalogue.`,
21746
+ tried: [...FONT_PROVIDERS],
21747
+ ...didYouMean.length ? { didYouMean } : {}
21748
+ });
21749
+ }
21750
+ async function listFontFiles() {
21751
+ try {
21752
+ const entries = await readdir(path2.resolve(process.cwd(), FONTS_DIR), { withFileTypes: true });
21753
+ return entries.filter((e) => e.isFile()).map((e) => e.name);
21754
+ } catch {
21755
+ return [];
21174
21756
  }
21175
21757
  }
21176
21758
  async function readGlobalCss() {
21177
21759
  try {
21178
- return await readFile(path.resolve(process.cwd(), GLOBAL_CSS), "utf8");
21760
+ return await readFile2(path2.resolve(process.cwd(), GLOBAL_CSS), "utf8");
21179
21761
  } catch {
21180
21762
  return "";
21181
21763
  }
21182
21764
  }
21765
+ async function unresolvedAction(css, unresolved) {
21766
+ const { plans } = planFontAdoption(await astroSources(), unresolved);
21767
+ if (plans.length > 0) {
21768
+ const names = plans.map((p) => `"${p.family}"`).join(", ");
21769
+ return `Run \`baker brand fonts adopt\` \u2014 ${names} is already self-hosted by ${plans[0]?.files.join(", ")}, so it only needs moving into ${GLOBAL_CSS} to load on every page. Do NOT fetch it: that downloads a different build of the same face.`;
21770
+ }
21771
+ const viaFontsource = [...fontsourceFamilies(css)];
21772
+ const stranded = unresolved.filter((family) => viaFontsource.includes(family.toLowerCase()));
21773
+ if (stranded.length > 0) {
21774
+ return `Run \`baker brand fonts fetch "${stranded[0]}"\` to self-host it. Its only provider is an @fontsource import, and that package does not survive a workspace sync \u2014 the import is left pointing at nothing, which fails the build before the font check even runs.`;
21775
+ }
21776
+ return `Run \`baker brand fonts fetch "${unresolved[0]}"\` to self-host it, which writes the @font-face for you. For a face no catalogue carries, add an @font-face pointing at a file in ${FONTS_DIR}.`;
21777
+ }
21183
21778
  var fontsCheckCommand = defineCommand93({
21184
21779
  meta: {
21185
21780
  name: "check",
21186
- description: "Verify the brand's fonts really exist. For every family/weight src/styles/global.css requests from Google Fonts, ask Google what it actually serves and report anything missing \u2014 a weight nobody serves renders as a synthesized fallback and looks off-brand everywhere."
21781
+ description: "Verify the brand's fonts really exist. First: every --font-* family in src/styles/global.css must be provided by an @font-face or a Google import, or it renders as a fallback on every page. Then, for families requested from Google, ask Google what it actually serves \u2014 a weight nobody serves is silently synthesized."
21187
21782
  },
21188
21783
  async run() {
21189
21784
  const css = await readGlobalCss();
21190
21785
  const requests = googleFontRequests(css);
21786
+ const unresolved = unresolvedBrandFamilies(css);
21787
+ if (unresolved.length > 0) {
21788
+ fail("FONT_NOT_LOADED", `Nothing loads ${unresolved.map((f) => `"${f}"`).join(", ")} in ${GLOBAL_CSS}.`, {
21789
+ action: await unresolvedAction(css, unresolved),
21790
+ explanation: "The family is declared as a brand font but no @font-face and no Google import provides it, so every page silently renders a fallback. The client's verify fails on this too.",
21791
+ unresolved
21792
+ });
21793
+ }
21191
21794
  if (requests.length === 0) {
21192
21795
  writeJson({
21193
21796
  ok: true,
21194
21797
  data: { families: [], selfHosted: true },
21195
21798
  hints: [
21196
- `No Google Fonts requested in ${GLOBAL_CSS}. If the brand self-hosts, the offline validator already checks those files; nothing to verify against Google.`
21799
+ `No Google Fonts requested in ${GLOBAL_CSS}, and every brand face is provided by an @font-face \u2014 nothing to verify against Google.`
21197
21800
  ]
21198
21801
  });
21199
21802
  return;
@@ -21227,11 +21830,14 @@ var fontsCheckCommand = defineCommand93({
21227
21830
  var fontsFetchCommand = defineCommand93({
21228
21831
  meta: {
21229
21832
  name: "fetch",
21230
- description: "Download a Google font into src/brand/fonts/ and print the @font-face block to paste above @theme in src/styles/global.css. Use when a brand font should be self-hosted rather than requested from Google on every page load."
21833
+ description: "Download a font from Google Fonts or Fontsource into src/brand/fonts/ and write its @font-face into src/styles/global.css above @theme, so the family actually loads. Use when a brand font should be self-hosted rather than requested from a third party on every page load."
21231
21834
  },
21232
21835
  args: {
21233
21836
  family: { type: "positional", required: true, description: 'Font family, e.g. "DM Sans"' },
21234
- weights: { type: "string", description: "Comma-separated weights (default: what global.css requests, else 400)" },
21837
+ weights: {
21838
+ type: "string",
21839
+ description: "Comma-separated weights (default: the weights global.css already requests or self-hosts, else 400)"
21840
+ },
21235
21841
  subsets: { type: "string", description: `Comma-separated unicode subsets (default: ${DEFAULT_SUBSETS})` }
21236
21842
  },
21237
21843
  async run({ args }) {
@@ -21239,38 +21845,18 @@ var fontsFetchCommand = defineCommand93({
21239
21845
  if (!family) fail("INVALID_FAMILY", 'Pass a font family, e.g. `baker brand fonts fetch "DM Sans"`.');
21240
21846
  const css = await readGlobalCss();
21241
21847
  const declared = googleFontRequests(css).find((r) => r.family.toLowerCase() === family.toLowerCase());
21242
- const weights = resolveFetchWeights(args.weights, declared?.weights);
21848
+ const provided = declared?.weights.length ? declared.weights : selfHostedWeights(css, family);
21849
+ const weights = resolveFetchWeights(args.weights, provided);
21243
21850
  if (weights.length === 0) {
21244
21851
  fail("INVALID_WEIGHTS", `"${args.weights}" has no usable weight \u2014 pass whole hundreds, e.g. --weights 400,700.`);
21245
21852
  }
21246
21853
  const wanted = new Set(
21247
21854
  String(args.subsets ?? DEFAULT_SUBSETS).split(",").map((s) => s.trim()).filter(Boolean)
21248
21855
  );
21249
- const servedCss = await fetchGoogleCss(googleFontCssUrl(family, weights));
21250
- if (servedCss === null) {
21251
- const [diagnosis] = await checkFontRequests([{ family, weights }], fetchGoogleCss);
21252
- const served = diagnosis?.served ?? [];
21253
- fail(
21254
- "FONT_NOT_SERVED",
21255
- served.length === 0 ? `Google Fonts has no family called "${family}".` : `"${family}" exists on Google Fonts but not at weight ${weights.join("/")}.`,
21256
- served.length === 0 ? {
21257
- action: "Check the exact family name at fonts.google.com, then retry.",
21258
- explanation: "Family names are case- and space-sensitive in the Google Fonts API."
21259
- } : {
21260
- action: `Retry with --weights set to one of these: ${served.join(",")}.`,
21261
- explanation: "Google rejects the whole request when one requested weight does not exist.",
21262
- served
21263
- }
21264
- );
21265
- }
21266
- const faces = parseGoogleFontFaces(servedCss).filter((f) => wanted.has(f.subset));
21267
- if (faces.length === 0) {
21268
- fail("SUBSET_NOT_SERVED", `"${family}" has no ${[...wanted].join("/")} subset.`, {
21269
- action: `Retry with --subsets set to one Google actually serves for this family.`,
21270
- explanation: "Google splits each family into unicode subsets; not every family ships every subset."
21271
- });
21272
- }
21273
- const fontsDir = path.resolve(process.cwd(), FONTS_DIR);
21856
+ const resolved = await resolveBrandFaces(family, weights, [...wanted], PROVIDER_DEPS);
21857
+ if (!resolved.ok) return failResolution(family, weights, resolved);
21858
+ const faces = resolved.faces;
21859
+ const fontsDir = path2.resolve(process.cwd(), FONTS_DIR);
21274
21860
  await mkdir(fontsDir, { recursive: true });
21275
21861
  const downloaded = [];
21276
21862
  for (const face of faces) {
@@ -21279,7 +21865,7 @@ var fontsFetchCommand = defineCommand93({
21279
21865
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
21280
21866
  });
21281
21867
  if (!res.ok) continue;
21282
- await writeFile(path.join(fontsDir, fontFileName(face)), Buffer.from(await res.arrayBuffer()));
21868
+ await writeFile(path2.join(fontsDir, fontFileName(face)), Buffer.from(await res.arrayBuffer()));
21283
21869
  downloaded.push(face);
21284
21870
  }
21285
21871
  const written = downloaded.map(fontFileName);
@@ -21292,16 +21878,115 @@ var fontsFetchCommand = defineCommand93({
21292
21878
  const fontFace = renderFontFaceCss(downloaded);
21293
21879
  const downloadedWeights = [...new Set(downloaded.map((f) => f.weight))].sort((a, b) => a - b);
21294
21880
  const incomplete = weights.filter((w) => !downloadedWeights.includes(w));
21881
+ const wiring = await wireFontFaceIntoStylesheet(css, family, fontFace);
21295
21882
  writeJson({
21296
21883
  ok: true,
21297
- data: { family, weights: downloadedWeights, files: written, fontFace },
21884
+ data: {
21885
+ family,
21886
+ provider: resolved.provider,
21887
+ weights: downloadedWeights,
21888
+ files: written,
21889
+ fontFace,
21890
+ stylesheet: wiring
21891
+ },
21892
+ hints: fetchHints({ family, downloadedWeights, incomplete, wiring, provider: resolved.provider })
21893
+ });
21894
+ }
21895
+ });
21896
+ async function astroSources() {
21897
+ const out = [];
21898
+ let entries;
21899
+ try {
21900
+ entries = await readdir("src", { recursive: true, withFileTypes: true });
21901
+ } catch {
21902
+ return out;
21903
+ }
21904
+ for (const entry of entries) {
21905
+ if (!entry.isFile() || !entry.name.endsWith(".astro")) continue;
21906
+ const file = path2.posix.join(entry.parentPath ?? "src", entry.name);
21907
+ try {
21908
+ out.push({ path: file, source: await readFile2(file, "utf8") });
21909
+ } catch {
21910
+ }
21911
+ }
21912
+ return out;
21913
+ }
21914
+ var fontsAdoptCommand = defineCommand93({
21915
+ meta: {
21916
+ name: "adopt",
21917
+ description: "Move brand faces a page already self-hosts into src/styles/global.css, so every page loads them instead of just that one. Use when `check` reports a family as not loaded but the font files are already in src/brand/fonts/ \u2014 downloading it again would hand that page a different build of the same face."
21918
+ },
21919
+ async run() {
21920
+ const css = await readGlobalCss();
21921
+ if (!css) fail("NO_STYLESHEET", `There is no ${GLOBAL_CSS} to adopt faces into.`);
21922
+ const unresolved = unresolvedBrandFamilies(css);
21923
+ if (unresolved.length === 0) {
21924
+ writeJson({
21925
+ ok: true,
21926
+ data: { adopted: [], conflicts: [] },
21927
+ hints: [`Every --font-* family in ${GLOBAL_CSS} is already loaded by it \u2014 nothing to adopt.`]
21928
+ });
21929
+ return;
21930
+ }
21931
+ const { plans, conflicts } = planFontAdoption(await astroSources(), unresolved);
21932
+ const available = new Set(await listFontFiles());
21933
+ const adopted = [];
21934
+ const skipped = [];
21935
+ let next = css;
21936
+ for (const plan of plans) {
21937
+ const missing = missingFontFiles(plan.urls, available);
21938
+ if (missing.length > 0) {
21939
+ skipped.push({ family: plan.family, reason: `${missing.join(", ")} is not in ${FONTS_DIR}` });
21940
+ continue;
21941
+ }
21942
+ next = upsertFontFaceCss(next, plan.family, plan.blocks.join("\n"));
21943
+ adopted.push({
21944
+ family: plan.family,
21945
+ files: plan.files,
21946
+ weights: plan.blocks.map((b) => b.match(/font-weight\s*:\s*([^;]+)/i)?.[1]?.trim() ?? "400")
21947
+ });
21948
+ }
21949
+ if (adopted.length === 0) {
21950
+ if (conflicts.length > 0) {
21951
+ fail(
21952
+ "ADOPTION_CONFLICT",
21953
+ `${conflicts.map((c) => `"${c.family}" is declared differently in ${c.files.join(" and ")}`).join("; ")}.`,
21954
+ {
21955
+ action: "Make the declarations match \u2014 same weights, same files \u2014 then re-run. Pick the one whose page renders correctly.",
21956
+ explanation: "One global scope holds one mapping per family, so adopting would silently repaint whichever page lost.",
21957
+ conflicts
21958
+ }
21959
+ );
21960
+ }
21961
+ if (skipped.length > 0) {
21962
+ fail("FONT_FILE_MISSING", `${skipped.map((s) => `${s.family} \u2014 ${s.reason}`).join("; ")}.`, {
21963
+ action: `Add the missing file to ${FONTS_DIR}, or fix the src: url() in the page that declares it, then re-run.`,
21964
+ explanation: "The page declares the face but points at a file that is not in the repo, so the rule renders as a fallback. Moving it into the stylesheet would spread that fallback to every page.",
21965
+ skipped
21966
+ });
21967
+ }
21968
+ fail("NOTHING_TO_ADOPT", `No page declares a face for ${unresolved.map((f) => `"${f}"`).join(", ")}.`, {
21969
+ action: `Self-host it instead: \`baker brand fonts fetch "${unresolved[0]}"\`.`,
21970
+ explanation: "Adopting only moves faces the repo already ships; it never downloads one."
21971
+ });
21972
+ }
21973
+ await writeFile(path2.resolve(process.cwd(), GLOBAL_CSS), next);
21974
+ for (const plan of plans.filter((p) => adopted.some((a) => a.family === p.family))) {
21975
+ for (const file of plan.files) {
21976
+ const source = await readFile2(file, "utf8");
21977
+ await writeFile(file, stripFontFaceBlocks(source, plan.family));
21978
+ }
21979
+ }
21980
+ writeJson({
21981
+ ok: true,
21982
+ data: { adopted, conflicts, skipped },
21298
21983
  hints: [
21299
- `Paste the \`fontFace\` block ABOVE the @theme block in ${GLOBAL_CSS}, then remove "${family}" from the Google @import so the page stops fetching it at load.`,
21300
- `Record the exact weights (${downloadedWeights.join(", ")}) in src/brand/BRAND.md \u2014 a weight listed there but not downloaded renders as a synthesized fallback.`,
21301
- ...incomplete.length > 0 ? [
21302
- `PARTIAL: ${incomplete.join(", ")} did not download and is NOT in the \`fontFace\` block. Keep "${family}" in the Google @import for those weights, or re-run \`baker brand fonts fetch "${family}" --weights ${incomplete.join(",")}\`.`
21984
+ `Moved ${adopted.map((a) => `"${a.family}" (${a.weights.join("/")})`).join(", ")} into ${GLOBAL_CSS} and removed the page copies, so every page loads them \u2014 not just the one that declared them.`,
21985
+ ...conflicts.length > 0 ? [
21986
+ `CONFLICT: ${conflicts.map((c) => `"${c.family}" is declared differently in ${c.files.join(" and ")}`).join("; ")}. One global scope can hold only one mapping, so pick which files the family should use and make the declarations match before re-running.`
21303
21987
  ] : [],
21304
- "Licensing: Google Fonts are open-licensed and safe to self-host. A client's own commercial face is not \u2014 confirm before re-hosting one."
21988
+ ...skipped.length > 0 ? [`SKIPPED: ${skipped.map((s) => `${s.family} \u2014 ${s.reason}`).join("; ")}.`] : [],
21989
+ `Run \`baker brand fonts check\` to confirm nothing is left unresolved.`
21305
21990
  ]
21306
21991
  });
21307
21992
  }
@@ -21311,15 +21996,17 @@ var fontsCommand = defineCommand93({
21311
21996
  name: "fonts",
21312
21997
  description: `Verify and self-host the brand's typefaces.
21313
21998
 
21314
- Start here: \`baker brand fonts check\` \u2014 confirms the fonts the brand claims are really served.
21999
+ Start here: \`baker brand fonts check\` \u2014 confirms the fonts the brand claims really load.
21315
22000
 
21316
22001
  Subcommands:
21317
- baker brand fonts check \u2014 ask Google what it actually serves for every family/weight global.css requests
21318
- baker brand fonts fetch <family> \u2014 download a family into src/brand/fonts/ and print its @font-face block`
22002
+ baker brand fonts check \u2014 fail if any --font-* family has nothing loading it, then ask Google what it actually serves for the ones global.css requests
22003
+ baker brand fonts fetch <family> \u2014 download a family into src/brand/fonts/ and wire its @font-face into src/styles/global.css
22004
+ baker brand fonts adopt \u2014 move faces a page already self-hosts into global.css, so every page loads them. Try this BEFORE fetch when the font files are already in src/brand/fonts/`
21319
22005
  },
21320
22006
  subCommands: {
21321
22007
  check: fontsCheckCommand,
21322
- fetch: fontsFetchCommand
22008
+ fetch: fontsFetchCommand,
22009
+ adopt: fontsAdoptCommand
21323
22010
  }
21324
22011
  });
21325
22012
 
@@ -21357,8 +22044,8 @@ var catalogCommand = defineCommand95({
21357
22044
  });
21358
22045
 
21359
22046
  // src/commands/canvas/critique.ts
21360
- import { readFile as readFile2 } from "fs/promises";
21361
- import path2 from "path";
22047
+ import { readFile as readFile3 } from "fs/promises";
22048
+ import path3 from "path";
21362
22049
  import { defineCommand as defineCommand96 } from "citty";
21363
22050
 
21364
22051
  // src/engine/scaffold/lib/critique.ts
@@ -21486,8 +22173,8 @@ var critiqueCommand = defineCommand96({
21486
22173
  },
21487
22174
  args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
21488
22175
  async run({ args }) {
21489
- const filePath = path2.resolve(String(args.file));
21490
- const raw = await readFile2(filePath, "utf8");
22176
+ const filePath = path3.resolve(String(args.file));
22177
+ const raw = await readFile3(filePath, "utf8");
21491
22178
  let parsed;
21492
22179
  try {
21493
22180
  parsed = JSON.parse(raw);
@@ -21518,8 +22205,8 @@ var critiqueCommand = defineCommand96({
21518
22205
 
21519
22206
  // src/commands/canvas/inspect.ts
21520
22207
  import { execFile } from "child_process";
21521
- import { readdir, readFile as readFile3, stat } from "fs/promises";
21522
- import path3 from "path";
22208
+ import { readdir as readdir2, readFile as readFile4, stat } from "fs/promises";
22209
+ import path4 from "path";
21523
22210
  import { promisify } from "util";
21524
22211
  import { defineCommand as defineCommand97 } from "citty";
21525
22212
  var execFileAsync = promisify(execFile);
@@ -21537,7 +22224,7 @@ var inspectCommand = defineCommand97({
21537
22224
  }
21538
22225
  },
21539
22226
  async run({ args }) {
21540
- const outputsDir = path3.resolve(String(args["outputs-dir"] ?? "canvas"));
22227
+ const outputsDir = path4.resolve(String(args["outputs-dir"] ?? "canvas"));
21541
22228
  const runArg = String(args.run);
21542
22229
  const runDir = await resolveRunDir(runArg, outputsDir);
21543
22230
  const manifest = await loadManifest(runDir);
@@ -21549,7 +22236,7 @@ var inspectCommand = defineCommand97({
21549
22236
  }
21550
22237
  const summary = {
21551
22238
  ok: true,
21552
- run_id: manifest.run_id ?? path3.basename(runDir),
22239
+ run_id: manifest.run_id ?? path4.basename(runDir),
21553
22240
  run_dir: runDir,
21554
22241
  stats: manifest.stats ?? null,
21555
22242
  output: manifest.output ?? null,
@@ -21562,20 +22249,20 @@ var inspectCommand = defineCommand97({
21562
22249
  }
21563
22250
  });
21564
22251
  async function resolveRunDir(run, outputsDir) {
21565
- if (path3.isAbsolute(run)) {
22252
+ if (path4.isAbsolute(run)) {
21566
22253
  const s2 = await stat(run).catch(() => null);
21567
22254
  if (s2?.isDirectory()) return run;
21568
22255
  throw new Error(`inspect: ${run} is not a directory`);
21569
22256
  }
21570
- const candidate = path3.join(outputsDir, run);
22257
+ const candidate = path4.join(outputsDir, run);
21571
22258
  const s = await stat(candidate).catch(() => null);
21572
22259
  if (s?.isDirectory()) return candidate;
21573
22260
  throw new Error(`inspect: no run directory at ${candidate}`);
21574
22261
  }
21575
22262
  async function loadManifest(runDir) {
21576
- const manifestPath = path3.join(runDir, "manifest.json");
22263
+ const manifestPath = path4.join(runDir, "manifest.json");
21577
22264
  try {
21578
- const raw = await readFile3(manifestPath, "utf-8");
22265
+ const raw = await readFile4(manifestPath, "utf-8");
21579
22266
  return JSON.parse(raw);
21580
22267
  } catch {
21581
22268
  return {};
@@ -21583,9 +22270,9 @@ async function loadManifest(runDir) {
21583
22270
  }
21584
22271
  async function listRunFiles(runDir) {
21585
22272
  const out = [];
21586
- const names = await readdir(runDir);
22273
+ const names = await readdir2(runDir);
21587
22274
  for (const name of names) {
21588
- const abs = path3.join(runDir, name);
22275
+ const abs = path4.join(runDir, name);
21589
22276
  const s = await stat(abs).catch(() => null);
21590
22277
  if (!s?.isFile()) continue;
21591
22278
  out.push({ name, path: abs, size: s.size });
@@ -21630,58 +22317,58 @@ async function probeDuration(filePath) {
21630
22317
  }
21631
22318
 
21632
22319
  // src/commands/canvas/rerun.ts
21633
- import path15 from "path";
22320
+ import path16 from "path";
21634
22321
  import { defineCommand as defineCommand99 } from "citty";
21635
22322
 
21636
22323
  // src/commands/canvas/run.ts
21637
- import { readFile as readFile10 } from "fs/promises";
21638
- import path14 from "path";
22324
+ import { readFile as readFile11 } from "fs/promises";
22325
+ import path15 from "path";
21639
22326
  import { defineCommand as defineCommand98 } from "citty";
21640
22327
 
21641
22328
  // src/commands/canvas/normalize-paths.ts
21642
22329
  import { existsSync as existsSync3, realpathSync } from "fs";
21643
22330
  import { writeFile as writeFile2 } from "fs/promises";
21644
- import path4 from "path";
22331
+ import path5 from "path";
21645
22332
  function findWorkspaceRoot(startDir, exists = existsSync3, maxDepth = 12) {
21646
- let dir = path4.resolve(startDir);
22333
+ let dir = path5.resolve(startDir);
21647
22334
  for (let i = 0; i < maxDepth; i++) {
21648
- if (exists(path4.join(dir, "package.json"))) return dir;
21649
- const parent = path4.dirname(dir);
22335
+ if (exists(path5.join(dir, "package.json"))) return dir;
22336
+ const parent = path5.dirname(dir);
21650
22337
  if (parent === dir) break;
21651
22338
  dir = parent;
21652
22339
  }
21653
22340
  return null;
21654
22341
  }
21655
22342
  function canonicalize(target) {
21656
- const abs = path4.resolve(target);
22343
+ const abs = path5.resolve(target);
21657
22344
  let dir = abs;
21658
22345
  for (; ; ) {
21659
22346
  try {
21660
22347
  const real = realpathSync(dir);
21661
- return dir === abs ? real : path4.join(real, path4.relative(dir, abs));
22348
+ return dir === abs ? real : path5.join(real, path5.relative(dir, abs));
21662
22349
  } catch {
21663
- const parent = path4.dirname(dir);
22350
+ const parent = path5.dirname(dir);
21664
22351
  if (parent === dir) return abs;
21665
22352
  dir = parent;
21666
22353
  }
21667
22354
  }
21668
22355
  }
21669
22356
  function isInside(root, target) {
21670
- const rel = path4.relative(root, target);
21671
- return rel !== "" && !rel.startsWith("..") && !path4.isAbsolute(rel);
22357
+ const rel = path5.relative(root, target);
22358
+ return rel !== "" && !rel.startsWith("..") && !path5.isAbsolute(rel);
21672
22359
  }
21673
22360
  function toCanvasRelative(canvasDir, target) {
21674
- return path4.relative(canvasDir, target).split(path4.sep).join("/");
22361
+ return path5.relative(canvasDir, target).split(path5.sep).join("/");
21675
22362
  }
21676
22363
  var RUNTIME_WORKSPACE_ROOT = "/home/user/repo";
21677
22364
  function rewriteTarget(value, workspaceRoot) {
21678
22365
  if (typeof value !== "string" || value.length === 0) return null;
21679
- if (value.includes("[TODO") || looksLikeHttpUrl(value) || !path4.isAbsolute(value)) return null;
22366
+ if (value.includes("[TODO") || looksLikeHttpUrl(value) || !path5.isAbsolute(value)) return null;
21680
22367
  const target = canonicalize(value);
21681
22368
  if (isInside(workspaceRoot, target)) return target;
21682
- const fromRuntime = path4.relative(RUNTIME_WORKSPACE_ROOT, path4.normalize(value));
21683
- if (fromRuntime !== "" && !fromRuntime.startsWith("..") && !path4.isAbsolute(fromRuntime)) {
21684
- return path4.join(workspaceRoot, fromRuntime);
22369
+ const fromRuntime = path5.relative(RUNTIME_WORKSPACE_ROOT, path5.normalize(value));
22370
+ if (fromRuntime !== "" && !fromRuntime.startsWith("..") && !path5.isAbsolute(fromRuntime)) {
22371
+ return path5.join(workspaceRoot, fromRuntime);
21685
22372
  }
21686
22373
  return null;
21687
22374
  }
@@ -21709,7 +22396,7 @@ function normalizeAbsoluteCanvasPaths(canvas, canvasDir, workspaceRoot) {
21709
22396
  return rewrites.length === 0 ? { canvas, rewrites } : { canvas: { ...canvas, nodes }, rewrites };
21710
22397
  }
21711
22398
  async function healAbsoluteCanvasPaths(filePath, canvas) {
21712
- const canvasDir = path4.dirname(filePath);
22399
+ const canvasDir = path5.dirname(filePath);
21713
22400
  const workspaceRoot = findWorkspaceRoot(canvasDir);
21714
22401
  if (!workspaceRoot) return { canvas, rewrites: [], text: null };
21715
22402
  const normalized = normalizeAbsoluteCanvasPaths(canvas, canvasDir, workspaceRoot);
@@ -21740,7 +22427,7 @@ function unsuppliedPlaceholderAssets(canvas) {
21740
22427
  }
21741
22428
 
21742
22429
  // src/commands/canvas/resolve-paths.ts
21743
- import path5 from "path";
22430
+ import path6 from "path";
21744
22431
  function resolveRelativeCanvasPaths(canvas, baseDir) {
21745
22432
  if (!canvas || typeof canvas !== "object") return canvas;
21746
22433
  const c = canvas;
@@ -21753,24 +22440,24 @@ function resolveNode(node, baseDir) {
21753
22440
  const params = n.params;
21754
22441
  if (!params || typeof params !== "object") return node;
21755
22442
  if (n.type === "ingest" && params.source === "path" && isResolvableRelative(params.path)) {
21756
- return { ...node, params: { ...params, path: path5.resolve(baseDir, params.path) } };
22443
+ return { ...node, params: { ...params, path: path6.resolve(baseDir, params.path) } };
21757
22444
  }
21758
22445
  if (n.type === "hyperframe_render" && isResolvableRelative(params.composition)) {
21759
- return { ...node, params: { ...params, composition: path5.resolve(baseDir, params.composition) } };
22446
+ return { ...node, params: { ...params, composition: path6.resolve(baseDir, params.composition) } };
21760
22447
  }
21761
22448
  return node;
21762
22449
  }
21763
22450
  function isResolvableRelative(value) {
21764
- return typeof value === "string" && value.length > 0 && !value.includes("[TODO") && !looksLikeHttpUrl(value) && !path5.isAbsolute(value);
22451
+ return typeof value === "string" && value.length > 0 && !value.includes("[TODO") && !looksLikeHttpUrl(value) && !path6.isAbsolute(value);
21765
22452
  }
21766
22453
 
21767
22454
  // src/commands/canvas/source-version.ts
21768
- import { readFile as readFile5 } from "fs/promises";
21769
- import path7 from "path";
22455
+ import { readFile as readFile6 } from "fs/promises";
22456
+ import path8 from "path";
21770
22457
 
21771
22458
  // src/commands/canvas/scene-files.ts
21772
- import { mkdir as mkdir2, readFile as readFile4, readdir as readdir2, rm, writeFile as writeFile3 } from "fs/promises";
21773
- import path6 from "path";
22459
+ import { mkdir as mkdir2, readFile as readFile5, readdir as readdir3, rm, writeFile as writeFile3 } from "fs/promises";
22460
+ import path7 from "path";
21774
22461
  var SCENES_DIR = "scenes";
21775
22462
  var GLOBAL_PROMPT_FILE = "prompt.json";
21776
22463
  var REBUILD_FILE = "prompt.rebuild.json";
@@ -21787,42 +22474,42 @@ function splitBlueprint(blueprint) {
21787
22474
  }
21788
22475
  async function writeSceneFiles(outDir, blueprint) {
21789
22476
  const { global, scenes } = splitBlueprint(blueprint);
21790
- await writeFile3(path6.join(outDir, GLOBAL_PROMPT_FILE), `${JSON.stringify(global, null, 2)}
22477
+ await writeFile3(path7.join(outDir, GLOBAL_PROMPT_FILE), `${JSON.stringify(global, null, 2)}
21791
22478
  `, "utf8");
21792
- const scenesDir = path6.join(outDir, SCENES_DIR);
22479
+ const scenesDir = path7.join(outDir, SCENES_DIR);
21793
22480
  await mkdir2(scenesDir, { recursive: true });
21794
22481
  const written = /* @__PURE__ */ new Set();
21795
22482
  for (let i = 0; i < scenes.length; i++) {
21796
22483
  const name = sceneFileName(i, scenes.length);
21797
22484
  written.add(name);
21798
- await writeFile3(path6.join(scenesDir, name), `${JSON.stringify(scenes[i], null, 2)}
22485
+ await writeFile3(path7.join(scenesDir, name), `${JSON.stringify(scenes[i], null, 2)}
21799
22486
  `, "utf8");
21800
22487
  }
21801
22488
  for (const name of await listSceneFileNames(scenesDir)) {
21802
- if (!written.has(name)) await rm(path6.join(scenesDir, name), { force: true });
22489
+ if (!written.has(name)) await rm(path7.join(scenesDir, name), { force: true });
21803
22490
  }
21804
22491
  }
21805
22492
  async function listSceneFileNames(scenesDir) {
21806
22493
  let entries;
21807
22494
  try {
21808
- entries = await readdir2(scenesDir);
22495
+ entries = await readdir3(scenesDir);
21809
22496
  } catch {
21810
22497
  return [];
21811
22498
  }
21812
22499
  return entries.filter((n) => /^s\d+\.json$/.test(n)).sort(bySceneIndex);
21813
22500
  }
21814
22501
  async function listSceneFiles(creativeDir) {
21815
- const scenesDir = path6.join(creativeDir, SCENES_DIR);
21816
- return (await listSceneFileNames(scenesDir)).map((n) => path6.join(scenesDir, n));
22502
+ const scenesDir = path7.join(creativeDir, SCENES_DIR);
22503
+ return (await listSceneFileNames(scenesDir)).map((n) => path7.join(scenesDir, n));
21817
22504
  }
21818
22505
  async function reassembleBlueprint(creativeDir) {
21819
22506
  const files = await listSceneFiles(creativeDir);
21820
22507
  if (files.length === 0) return null;
21821
- const globalRaw = await readFile4(path6.join(creativeDir, GLOBAL_PROMPT_FILE), "utf8");
22508
+ const globalRaw = await readFile5(path7.join(creativeDir, GLOBAL_PROMPT_FILE), "utf8");
21822
22509
  const global = JSON.parse(globalRaw);
21823
22510
  const scenes = [];
21824
22511
  for (const file of files) {
21825
- scenes.push(JSON.parse(await readFile4(file, "utf8")));
22512
+ scenes.push(JSON.parse(await readFile5(file, "utf8")));
21826
22513
  }
21827
22514
  return { ...global, scenes };
21828
22515
  }
@@ -21836,15 +22523,15 @@ function bySceneIndex(a, b) {
21836
22523
  async function computeSourceSha(canvasPath) {
21837
22524
  let canvasBytes;
21838
22525
  try {
21839
- canvasBytes = await readFile5(canvasPath);
22526
+ canvasBytes = await readFile6(canvasPath);
21840
22527
  } catch {
21841
22528
  return void 0;
21842
22529
  }
21843
- const canvasDir = path7.dirname(canvasPath);
21844
- const promptPath = path7.join(canvasDir, "prompt.json");
22530
+ const canvasDir = path8.dirname(canvasPath);
22531
+ const promptPath = path8.join(canvasDir, "prompt.json");
21845
22532
  let promptBytes;
21846
22533
  try {
21847
- promptBytes = await readFile5(promptPath);
22534
+ promptBytes = await readFile6(promptPath);
21848
22535
  } catch {
21849
22536
  promptBytes = Buffer.alloc(0);
21850
22537
  }
@@ -21857,7 +22544,7 @@ async function computeSourceSha(canvasPath) {
21857
22544
  for (const sceneFile of await listSceneFiles(canvasDir)) {
21858
22545
  let sceneBytes;
21859
22546
  try {
21860
- sceneBytes = await readFile5(sceneFile);
22547
+ sceneBytes = await readFile6(sceneFile);
21861
22548
  } catch {
21862
22549
  sceneBytes = Buffer.alloc(0);
21863
22550
  }
@@ -21867,8 +22554,8 @@ async function computeSourceSha(canvasPath) {
21867
22554
  }
21868
22555
 
21869
22556
  // src/commands/canvas/scene-projection.ts
21870
- import { readFile as readFile6 } from "fs/promises";
21871
- import path8 from "path";
22557
+ import { readFile as readFile7 } from "fs/promises";
22558
+ import path9 from "path";
21872
22559
 
21873
22560
  // src/engine/scaffold/video.ts
21874
22561
  import { toCardinal as nwAr } from "n2words/ar-SA";
@@ -25154,12 +25841,12 @@ function nonPromptParamsDiverge(live = {}, rebuilt = {}) {
25154
25841
  return false;
25155
25842
  }
25156
25843
  async function syncSceneNodeParams(canvas, canvasPath, log) {
25157
- const creativeDir = path8.dirname(canvasPath);
25844
+ const creativeDir = path9.dirname(canvasPath);
25158
25845
  const blueprint = await reassembleBlueprint(creativeDir);
25159
25846
  if (!blueprint) return "not_applicable";
25160
25847
  let rebuildRaw;
25161
25848
  try {
25162
- rebuildRaw = await readFile6(path8.join(creativeDir, REBUILD_FILE), "utf8");
25849
+ rebuildRaw = await readFile7(path9.join(creativeDir, REBUILD_FILE), "utf8");
25163
25850
  } catch {
25164
25851
  return "not_applicable";
25165
25852
  }
@@ -25200,7 +25887,7 @@ async function syncSceneNodeParams(canvas, canvasPath, log) {
25200
25887
  }
25201
25888
 
25202
25889
  // src/commands/canvas/style-projection.ts
25203
- import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
25890
+ import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
25204
25891
  function findBlueprintProjection(canvas) {
25205
25892
  if (!canvas || typeof canvas !== "object") return null;
25206
25893
  const nodes = canvas.nodes;
@@ -25228,8 +25915,8 @@ function renderStyleProjectionFromValue(blueprint) {
25228
25915
  async function syncStyleProjection(canvas, log) {
25229
25916
  const pair = findBlueprintProjection(canvas);
25230
25917
  if (!pair) return "not_applicable";
25231
- const rendered = renderStyleProjection(await readFile7(pair.promptPath, "utf8"));
25232
- const current = await readFile7(pair.stylePath, "utf8").catch(() => null);
25918
+ const rendered = renderStyleProjection(await readFile8(pair.promptPath, "utf8"));
25919
+ const current = await readFile8(pair.stylePath, "utf8").catch(() => null);
25233
25920
  if (current === rendered) return "up_to_date";
25234
25921
  await writeFile4(pair.stylePath, rendered, "utf8");
25235
25922
  log(
@@ -25291,13 +25978,13 @@ ${body}` : header || body || compactJson(record);
25291
25978
  }
25292
25979
 
25293
25980
  // src/commands/canvas/run-record.ts
25294
- import path9 from "path";
25981
+ import path10 from "path";
25295
25982
  var MAX_RUN_NODES = 200;
25296
25983
  var MAX_OUTPUTS_PER_NODE = 10;
25297
25984
  var MAX_FINAL_OUTPUTS = 10;
25298
25985
  var MAX_CREATIVE_SLUG_LENGTH = 100;
25299
25986
  function creativeSlugFromCanvasPath(filePath) {
25300
- const normalized = filePath.split(path9.sep).join("/");
25987
+ const normalized = filePath.split(path10.sep).join("/");
25301
25988
  const match = normalized.match(/(?:^|\/)src\/creatives\/([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\//);
25302
25989
  const slug = match?.[1] ?? null;
25303
25990
  return slug && slug.length <= MAX_CREATIVE_SLUG_LENGTH ? slug : null;
@@ -25585,24 +26272,24 @@ var RunRecordPoster = class {
25585
26272
 
25586
26273
  // src/commands/canvas/run-retention.ts
25587
26274
  import { rm as rm2 } from "fs/promises";
25588
- import path10 from "path";
26275
+ import path11 from "path";
25589
26276
  function runDirsToPrune(entries, keep, currentRunId) {
25590
26277
  const runs = entries.filter((e) => /^r_[0-9A-Za-z]+$/.test(e) && e !== currentRunId).sort();
25591
26278
  if (keep <= 0) return runs;
25592
26279
  return runs.slice(0, Math.max(0, runs.length - keep));
25593
26280
  }
25594
26281
  async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
25595
- const { readdir: readdir9 } = await import("fs/promises");
26282
+ const { readdir: readdir10 } = await import("fs/promises");
25596
26283
  let entries;
25597
26284
  try {
25598
- entries = await readdir9(outputsDir);
26285
+ entries = await readdir10(outputsDir);
25599
26286
  } catch {
25600
26287
  return;
25601
26288
  }
25602
26289
  const toPrune = runDirsToPrune(entries, keep, currentRunId);
25603
26290
  if (toPrune.length === 0) return;
25604
26291
  for (const dir of toPrune) {
25605
- await rm2(path10.join(outputsDir, dir), { recursive: true, force: true }).catch(
26292
+ await rm2(path11.join(outputsDir, dir), { recursive: true, force: true }).catch(
25606
26293
  (e) => log(`[prune ] could not remove ${dir}: ${e.message}`)
25607
26294
  );
25608
26295
  }
@@ -25610,13 +26297,13 @@ async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
25610
26297
  }
25611
26298
 
25612
26299
  // src/commands/canvas/dirty-marker.ts
25613
- import { mkdir as mkdir3, readdir as readdir3, rm as rm3, writeFile as writeFile5 } from "fs/promises";
25614
- import path11 from "path";
26300
+ import { mkdir as mkdir3, readdir as readdir4, rm as rm3, writeFile as writeFile5 } from "fs/promises";
26301
+ import path12 from "path";
25615
26302
  function creativeDirtyDir(base) {
25616
- return base ?? path11.resolve("canvas", ".dirty");
26303
+ return base ?? path12.resolve("canvas", ".dirty");
25617
26304
  }
25618
26305
  function dirtyMarkerFile(slug, base) {
25619
- return path11.join(creativeDirtyDir(base), `${slug}.json`);
26306
+ return path12.join(creativeDirtyDir(base), `${slug}.json`);
25620
26307
  }
25621
26308
  async function clearCreativeDirty(slug, base) {
25622
26309
  try {
@@ -25626,18 +26313,18 @@ async function clearCreativeDirty(slug, base) {
25626
26313
  }
25627
26314
 
25628
26315
  // src/commands/canvas/run-resume.ts
25629
- import { mkdir as mkdir4, readFile as readFile8, rm as rm4, writeFile as writeFile6 } from "fs/promises";
25630
- import path12 from "path";
26316
+ import { mkdir as mkdir4, readFile as readFile9, rm as rm4, writeFile as writeFile6 } from "fs/promises";
26317
+ import path13 from "path";
25631
26318
  function markerKey(canvasPath) {
25632
26319
  const slug = creativeSlugFromCanvasPath(canvasPath);
25633
- const identity = slug ?? path12.relative(process.cwd(), path12.resolve(canvasPath));
26320
+ const identity = slug ?? path13.relative(process.cwd(), path13.resolve(canvasPath));
25634
26321
  return sha256Hex(Buffer.from(identity)).slice(0, 32);
25635
26322
  }
25636
26323
  function legacyMarkerKey(canvasPath) {
25637
- return sha256Hex(Buffer.from(path12.resolve(canvasPath))).slice(0, 32);
26324
+ return sha256Hex(Buffer.from(path13.resolve(canvasPath))).slice(0, 32);
25638
26325
  }
25639
26326
  function markerFile(outputsDir, key) {
25640
- return path12.join(outputsDir, ".inflight", `${key}.json`);
26327
+ return path13.join(outputsDir, ".inflight", `${key}.json`);
25641
26328
  }
25642
26329
  var REMOTE_ADOPT_STALE_MS = 12e4;
25643
26330
  function classifyRemoteRun(run, now) {
@@ -25673,7 +26360,7 @@ async function resolveRunId(opts) {
25673
26360
  async function readMarkerRunId(outputsDir, canvasPath) {
25674
26361
  for (const key of [markerKey(canvasPath), legacyMarkerKey(canvasPath)]) {
25675
26362
  try {
25676
- const raw = await readFile8(markerFile(outputsDir, key), "utf8");
26363
+ const raw = await readFile9(markerFile(outputsDir, key), "utf8");
25677
26364
  const parsed = JSON.parse(raw);
25678
26365
  if (typeof parsed.runId === "string" && parsed.runId.length > 0) return parsed.runId;
25679
26366
  } catch {
@@ -25684,8 +26371,8 @@ async function readMarkerRunId(outputsDir, canvasPath) {
25684
26371
  async function markRunInFlight(outputsDir, canvasPath, runId) {
25685
26372
  try {
25686
26373
  const file = markerFile(outputsDir, markerKey(canvasPath));
25687
- await mkdir4(path12.dirname(file), { recursive: true });
25688
- await writeFile6(file, JSON.stringify({ runId, canvasPath: path12.resolve(canvasPath), startedAt: Date.now() }));
26374
+ await mkdir4(path13.dirname(file), { recursive: true });
26375
+ await writeFile6(file, JSON.stringify({ runId, canvasPath: path13.resolve(canvasPath), startedAt: Date.now() }));
25689
26376
  } catch {
25690
26377
  }
25691
26378
  }
@@ -25699,8 +26386,8 @@ async function clearRunMarker(outputsDir, canvasPath) {
25699
26386
  }
25700
26387
 
25701
26388
  // src/commands/canvas/run-snapshot.ts
25702
- import { mkdir as mkdir5, readdir as readdir4, readFile as readFile9, stat as stat2, writeFile as writeFile7 } from "fs/promises";
25703
- import path13 from "path";
26389
+ import { mkdir as mkdir5, readdir as readdir5, readFile as readFile10, stat as stat2, writeFile as writeFile7 } from "fs/promises";
26390
+ import path14 from "path";
25704
26391
  var SNAPSHOT_SCHEMA = "baker-canvas-snapshot/1";
25705
26392
  var MAX_SNAPSHOT_FILE_BYTES = 32 * 1024 * 1024;
25706
26393
  var EXT_TO_MIME = {
@@ -25727,15 +26414,15 @@ var EXT_TO_MIME = {
25727
26414
  woff2: "font/woff2"
25728
26415
  };
25729
26416
  function mimeForFile(filePath) {
25730
- const ext = path13.extname(filePath).slice(1).toLowerCase();
26417
+ const ext = path14.extname(filePath).slice(1).toLowerCase();
25731
26418
  return EXT_TO_MIME[ext] ?? "application/octet-stream";
25732
26419
  }
25733
26420
  function toPosix(p) {
25734
- return p.split(path13.sep).join("/");
26421
+ return p.split(path14.sep).join("/");
25735
26422
  }
25736
26423
  function isInside2(dir, target) {
25737
- const rel = path13.relative(dir, target);
25738
- return !rel.startsWith("..") && !path13.isAbsolute(rel);
26424
+ const rel = path14.relative(dir, target);
26425
+ return !rel.startsWith("..") && !path14.isAbsolute(rel);
25739
26426
  }
25740
26427
  function localPathRefsFromCanvas(parsed) {
25741
26428
  const nodes = parsed?.nodes;
@@ -25760,24 +26447,24 @@ function isSnapshotablePath(value) {
25760
26447
  async function sourceRefsToSnapshot(canvasDir, parsed) {
25761
26448
  const refs = new Set(localPathRefsFromCanvas(parsed));
25762
26449
  for (const sceneFile of await listSceneFiles(canvasDir)) {
25763
- refs.add(toPosix(path13.relative(canvasDir, sceneFile)));
26450
+ refs.add(toPosix(path14.relative(canvasDir, sceneFile)));
25764
26451
  }
25765
26452
  refs.add(REBUILD_FILE);
25766
26453
  return [...refs];
25767
26454
  }
25768
26455
  async function uploadRunSnapshot(client, opts) {
25769
26456
  try {
25770
- const canvasDir = path13.dirname(opts.canvasPath);
26457
+ const canvasDir = path14.dirname(opts.canvasPath);
25771
26458
  const put = (bytes, mime) => putContentAddressed(client, bytes, mime, opts.signal);
25772
26459
  const canvasBytes = Buffer.from(opts.raw);
25773
26460
  const canvasUpload = await put(canvasBytes, "application/json");
25774
26461
  const files = [];
25775
26462
  const skipped = [];
25776
26463
  for (const refPath of await sourceRefsToSnapshot(canvasDir, opts.parsed)) {
25777
- const abs = path13.isAbsolute(refPath) ? refPath : path13.resolve(canvasDir, refPath);
26464
+ const abs = path14.isAbsolute(refPath) ? refPath : path14.resolve(canvasDir, refPath);
25778
26465
  if (!isInside2(canvasDir, abs)) {
25779
26466
  skipped.push({
25780
- path: toPosix(path13.relative(canvasDir, abs)),
26467
+ path: toPosix(path14.relative(canvasDir, abs)),
25781
26468
  reason: "outside the creative folder \u2014 read from the workspace on rerun"
25782
26469
  });
25783
26470
  continue;
@@ -25786,18 +26473,18 @@ async function uploadRunSnapshot(client, opts) {
25786
26473
  try {
25787
26474
  st = await stat2(abs);
25788
26475
  } catch {
25789
- skipped.push({ path: toPosix(path13.relative(canvasDir, abs)), reason: "missing" });
26476
+ skipped.push({ path: toPosix(path14.relative(canvasDir, abs)), reason: "missing" });
25790
26477
  continue;
25791
26478
  }
25792
26479
  const fileList = st.isDirectory() ? await listFilesRecursive(abs) : [abs];
25793
26480
  for (const file of fileList) {
25794
- const rel = toPosix(path13.relative(canvasDir, file));
26481
+ const rel = toPosix(path14.relative(canvasDir, file));
25795
26482
  const size = (await stat2(file)).size;
25796
26483
  if (size > MAX_SNAPSHOT_FILE_BYTES) {
25797
26484
  skipped.push({ path: rel, reason: `too large (${size} bytes)` });
25798
26485
  continue;
25799
26486
  }
25800
- const bytes = await readFile9(file);
26487
+ const bytes = await readFile10(file);
25801
26488
  const upload = await put(bytes, mimeForFile(file));
25802
26489
  files.push({ path: rel, sha256: upload.sha256, url: upload.url });
25803
26490
  }
@@ -25806,7 +26493,7 @@ async function uploadRunSnapshot(client, opts) {
25806
26493
  schema: SNAPSHOT_SCHEMA,
25807
26494
  creativeSlug: opts.creativeSlug,
25808
26495
  canvasSha: canvasUpload.sha256,
25809
- canvas: { path: path13.basename(opts.canvasPath), sha256: canvasUpload.sha256, url: canvasUpload.url },
26496
+ canvas: { path: path14.basename(opts.canvasPath), sha256: canvasUpload.sha256, url: canvasUpload.url },
25810
26497
  files,
25811
26498
  skipped: skipped.length > 0 ? skipped : void 0
25812
26499
  };
@@ -25832,8 +26519,8 @@ async function putContentAddressed(client, bytes, mime, signal) {
25832
26519
  return { sha256, url: publicUrl };
25833
26520
  }
25834
26521
  async function listFilesRecursive(dir) {
25835
- const entries = await readdir4(dir, { recursive: true, withFileTypes: true });
25836
- return entries.filter((d) => d.isFile()).map((d) => path13.join(d.parentPath, d.name));
26522
+ const entries = await readdir5(dir, { recursive: true, withFileTypes: true });
26523
+ return entries.filter((d) => d.isFile()).map((d) => path14.join(d.parentPath, d.name));
25837
26524
  }
25838
26525
  var SnapshotConflictError = class extends Error {
25839
26526
  conflicts;
@@ -25845,19 +26532,19 @@ var SnapshotConflictError = class extends Error {
25845
26532
  };
25846
26533
  async function restoreRunSnapshot(manifest, targetDir, opts = {}) {
25847
26534
  const entries = [manifest.canvas, ...manifest.files];
25848
- const resolvedTarget = path13.resolve(targetDir);
26535
+ const resolvedTarget = path14.resolve(targetDir);
25849
26536
  const planned = [];
25850
26537
  const conflicts = [];
25851
26538
  const upToDate = [];
25852
26539
  for (const entry of entries) {
25853
- if (path13.isAbsolute(entry.path) || entry.path.split("/").includes("..")) {
26540
+ if (path14.isAbsolute(entry.path) || entry.path.split("/").includes("..")) {
25854
26541
  throw new Error(`snapshot entry escapes the creative directory: ${entry.path}`);
25855
26542
  }
25856
- const target = path13.resolve(resolvedTarget, entry.path);
25857
- if (target !== resolvedTarget && !target.startsWith(resolvedTarget + path13.sep)) {
26543
+ const target = path14.resolve(resolvedTarget, entry.path);
26544
+ if (target !== resolvedTarget && !target.startsWith(resolvedTarget + path14.sep)) {
25858
26545
  throw new Error(`snapshot entry escapes the creative directory: ${entry.path}`);
25859
26546
  }
25860
- const existing = await readFile9(target).catch(() => null);
26547
+ const existing = await readFile10(target).catch(() => null);
25861
26548
  if (existing) {
25862
26549
  if (sha256Hex(existing) === entry.sha256) {
25863
26550
  upToDate.push(entry.path);
@@ -25879,11 +26566,11 @@ async function restoreRunSnapshot(manifest, targetDir, opts = {}) {
25879
26566
  if (sha256Hex(bytes) !== entry.sha256) {
25880
26567
  throw new Error(`snapshot download for ${entry.path} does not match its recorded sha256`);
25881
26568
  }
25882
- await mkdir5(path13.dirname(target), { recursive: true });
26569
+ await mkdir5(path14.dirname(target), { recursive: true });
25883
26570
  await writeFile7(target, bytes);
25884
26571
  restored.push(entry.path);
25885
26572
  }
25886
- return { canvasPath: path13.resolve(resolvedTarget, manifest.canvas.path), restored, upToDate };
26573
+ return { canvasPath: path14.resolve(resolvedTarget, manifest.canvas.path), restored, upToDate };
25887
26574
  }
25888
26575
 
25889
26576
  // src/commands/canvas/run.ts
@@ -25963,8 +26650,8 @@ function resolveMaxCredits(...candidates) {
25963
26650
  return void 0;
25964
26651
  }
25965
26652
  async function executeCanvasRun(opts) {
25966
- const filePath = path14.resolve(opts.file);
25967
- const raw = await readFile10(filePath, "utf8");
26653
+ const filePath = path15.resolve(opts.file);
26654
+ const raw = await readFile11(filePath, "utf8");
25968
26655
  let parsed;
25969
26656
  try {
25970
26657
  parsed = JSON.parse(raw);
@@ -25986,7 +26673,7 @@ ${describeRewrites(healed.rewrites)}
25986
26673
  `
25987
26674
  );
25988
26675
  }
25989
- parsed = resolveRelativeCanvasPaths(parsed, path14.dirname(filePath));
26676
+ parsed = resolveRelativeCanvasPaths(parsed, path15.dirname(filePath));
25990
26677
  try {
25991
26678
  await syncStyleProjection(parsed, (line) => process.stderr.write(`${line}
25992
26679
  `));
@@ -26092,7 +26779,7 @@ ${describeRewrites(healed.rewrites)}
26092
26779
  const canvasSha = sha256Hex(Buffer.from(canvasText));
26093
26780
  const creativeSlug = creativeSlugFromCanvasPath(filePath) ?? void 0;
26094
26781
  const client = opts.record === false ? null : buildBackendClient();
26095
- const outputsDir = opts.outputsDir ? path14.resolve(opts.outputsDir) : path14.resolve("canvas");
26782
+ const outputsDir = opts.outputsDir ? path15.resolve(opts.outputsDir) : path15.resolve("canvas");
26096
26783
  const { runId, resumed, source, concurrentRunId } = await resolveRunId({
26097
26784
  explicitRunId: opts.runId,
26098
26785
  fresh: opts.fresh === true,
@@ -26126,7 +26813,7 @@ ${describeRewrites(healed.rewrites)}
26126
26813
  const canvasSnapshotUrl = client && creativeSlug ? await uploadRunSnapshot(client, { canvasPath: filePath, raw: canvasText, creativeSlug, parsed }) ?? void 0 : void 0;
26127
26814
  const recordMeta = {
26128
26815
  creativeSlug,
26129
- canvasPath: path14.relative(process.cwd(), filePath) || void 0,
26816
+ canvasPath: path15.relative(process.cwd(), filePath) || void 0,
26130
26817
  canvasSha,
26131
26818
  // The fingerprint the dashboard compares against the current source to flag
26132
26819
  // "edited since last render". Computed from the on-disk canvas.json +
@@ -26313,7 +27000,7 @@ var rerunCommand = defineCommand99({
26313
27000
  if (latest.canvasSha && manifest.canvasSha !== latest.canvasSha) {
26314
27001
  fail2("snapshot_mismatch", `snapshot manifest for run ${latest.runId} does not match its recorded canvas sha`);
26315
27002
  }
26316
- const targetDir = path15.resolve("src", "creatives", slug);
27003
+ const targetDir = path16.resolve("src", "creatives", slug);
26317
27004
  let restoredCanvasPath;
26318
27005
  try {
26319
27006
  const restore = await restoreRunSnapshot(manifest, targetDir, { force: args["force-remote"] === true });
@@ -26362,8 +27049,8 @@ async function fetchManifest(url) {
26362
27049
  }
26363
27050
 
26364
27051
  // src/commands/canvas/scaffold-static-ad.ts
26365
- import { access, mkdir as mkdir6, readFile as readFile12, writeFile as writeFile8 } from "fs/promises";
26366
- import path19 from "path";
27052
+ import { access, mkdir as mkdir6, readFile as readFile13, writeFile as writeFile8 } from "fs/promises";
27053
+ import path20 from "path";
26367
27054
  import { defineCommand as defineCommand101 } from "citty";
26368
27055
 
26369
27056
  // src/engine/scaffold/staticAd.ts
@@ -26645,7 +27332,7 @@ function staticAdReport(input, elementsInput, opts) {
26645
27332
  }
26646
27333
 
26647
27334
  // src/commands/canvas/creative-definition.ts
26648
- import path16 from "path";
27335
+ import path17 from "path";
26649
27336
  var PLATFORM_VALUES = ["meta", "google", "linkedin", "tiktok", "youtube", "x", "other"];
26650
27337
  var FORMAT_VALUES = ["1:1", "4:5", "9:16", "16:9", "1.91:1"];
26651
27338
  function titleFromSlug(slug) {
@@ -26693,16 +27380,16 @@ function buildCreativeDefinition(input) {
26693
27380
  }
26694
27381
 
26695
27382
  // src/commands/canvas/scaffold-static-ad-paths.ts
26696
- import path17 from "path";
27383
+ import path18 from "path";
26697
27384
  function resolveScaffoldStaticAdPaths(rawFile, out, cwd = process.cwd(), slug) {
26698
27385
  const file = rawFile.trim();
26699
27386
  const imageIsUrl = /^https?:\/\//i.test(file);
26700
- const imageSource = imageIsUrl ? file : path17.resolve(cwd, file);
26701
- const outPath = out ? path17.resolve(cwd, out) : slug ? path17.join(cwd, "src", "creatives", slug, `${slug}.canvas.json`) : imageIsUrl ? path17.join(cwd, "static-ad.canvas.json") : path17.join(path17.dirname(imageSource), "static-ad.canvas.json");
26702
- const blueprintPath = path17.join(path17.dirname(outPath), "prompt.json");
26703
- const creativeDir = slug ? path17.dirname(outPath) : null;
26704
- const definitionPath = creativeDir ? path17.join(creativeDir, "_definition.md") : null;
26705
- const referencesDir = creativeDir ? path17.join(creativeDir, "references") : null;
27387
+ const imageSource = imageIsUrl ? file : path18.resolve(cwd, file);
27388
+ const outPath = out ? path18.resolve(cwd, out) : slug ? path18.join(cwd, "src", "creatives", slug, `${slug}.canvas.json`) : imageIsUrl ? path18.join(cwd, "static-ad.canvas.json") : path18.join(path18.dirname(imageSource), "static-ad.canvas.json");
27389
+ const blueprintPath = path18.join(path18.dirname(outPath), "prompt.json");
27390
+ const creativeDir = slug ? path18.dirname(outPath) : null;
27391
+ const definitionPath = creativeDir ? path18.join(creativeDir, "_definition.md") : null;
27392
+ const referencesDir = creativeDir ? path18.join(creativeDir, "references") : null;
26706
27393
  return { imageIsUrl, imageSource, outPath, blueprintPath, creativeDir, definitionPath, referencesDir };
26707
27394
  }
26708
27395
  var SCAFFOLD_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
@@ -26712,8 +27399,8 @@ function isValidScaffoldSlug(slug) {
26712
27399
  }
26713
27400
 
26714
27401
  // src/commands/canvas/sync-definition.ts
26715
- import { readdir as readdir5, readFile as readFile11, stat as stat3 } from "fs/promises";
26716
- import path18 from "path";
27402
+ import { readdir as readdir6, readFile as readFile12, stat as stat3 } from "fs/promises";
27403
+ import path19 from "path";
26717
27404
  import { defineCommand as defineCommand100 } from "citty";
26718
27405
 
26719
27406
  // src/commands/canvas/definition-graph.ts
@@ -26806,26 +27493,26 @@ async function syncCreativeDefinitionBestEffort(input) {
26806
27493
  }
26807
27494
  }
26808
27495
  async function resolveCanvasPath(inputPath) {
26809
- const resolved = path18.resolve(inputPath);
27496
+ const resolved = path19.resolve(inputPath);
26810
27497
  let dir = resolved;
26811
27498
  try {
26812
27499
  if ((await stat3(resolved)).isFile()) {
26813
27500
  if (resolved.endsWith(".canvas.json")) return resolved;
26814
- dir = path18.dirname(resolved);
27501
+ dir = path19.dirname(resolved);
26815
27502
  }
26816
27503
  } catch {
26817
- dir = resolved.endsWith(".canvas.json") ? path18.dirname(resolved) : resolved;
27504
+ dir = resolved.endsWith(".canvas.json") ? path19.dirname(resolved) : resolved;
26818
27505
  }
26819
27506
  let entries;
26820
27507
  try {
26821
- entries = await readdir5(dir);
27508
+ entries = await readdir6(dir);
26822
27509
  } catch {
26823
27510
  return null;
26824
27511
  }
26825
27512
  const canvases = entries.filter((name) => name.endsWith(".canvas.json"));
26826
27513
  const slug = creativeSlugFromCanvasPath(`${dir}/x/`);
26827
27514
  const chosen = (slug ? canvases.find((name) => name === `${slug}.canvas.json`) : void 0) ?? canvases[0];
26828
- return chosen ? path18.join(dir, chosen) : null;
27515
+ return chosen ? path19.join(dir, chosen) : null;
26829
27516
  }
26830
27517
  var syncDefinitionCommand = defineCommand100({
26831
27518
  meta: {
@@ -26848,7 +27535,7 @@ var syncDefinitionCommand = defineCommand100({
26848
27535
  if (!slug) return;
26849
27536
  let canvas;
26850
27537
  try {
26851
- canvas = JSON.parse(await readFile11(canvasPath, "utf8"));
27538
+ canvas = JSON.parse(await readFile12(canvasPath, "utf8"));
26852
27539
  } catch {
26853
27540
  return;
26854
27541
  }
@@ -26875,7 +27562,7 @@ async function uploadSourceAsReference(source, isUrl, client) {
26875
27562
  throw new Error(`failed to download source image (${e instanceof Error ? e.message : String(e)})`);
26876
27563
  }
26877
27564
  } else {
26878
- bytes = await readFile12(source);
27565
+ bytes = await readFile13(source);
26879
27566
  }
26880
27567
  const safe = await toModelSafeImage(bytes);
26881
27568
  const sha256 = sha256Hex(safe.bytes);
@@ -26930,7 +27617,7 @@ DROP background extras, decorative props, generic scenery, and anything small or
26930
27617
  For each kept element return: { "type": one of logo|product|person|animal|badge, "label": a short UPPER_SNAKE_CASE name (e.g. LOGO, PRODUCT, HERO_DOG, TRUSTPILOT), "description": a concrete reusable description to source/shoot the real asset (include the exact expression for a living subject, and its castable attributes \u2014 breed/species for an animal, apparent age band, apparent origin/ethnicity, and wardrobe/setting for a person \u2014 so it can be recast to fit OUR audience/market), "expression": the facial expression for a living subject or null, "reason": why it is identity-critical, "locator": the blueprint entry this element came from as { "collection": one of "subjects" | "people" | "brands_logos", "index": its 0-based position in that array } (people -> people; logos/badges -> brands_logos; products/animals/objects -> subjects). Output ONLY the JSON object.`;
26931
27618
  async function loadAssetText(ref, label) {
26932
27619
  const r = ref;
26933
- if (typeof r?.path === "string") return readFile12(r.path, "utf8");
27620
+ if (typeof r?.path === "string") return readFile13(r.path, "utf8");
26934
27621
  if (typeof r?.url === "string") {
26935
27622
  const res = await fetch(r.url);
26936
27623
  if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
@@ -27106,7 +27793,7 @@ var scaffoldStaticAdCommand = defineCommand101({
27106
27793
  process.cwd(),
27107
27794
  slug
27108
27795
  );
27109
- await mkdir6(path19.dirname(outPath), { recursive: true });
27796
+ await mkdir6(path20.dirname(outPath), { recursive: true });
27110
27797
  const { describeModel, selectModel, layoutModel, genModel } = resolveModels(args);
27111
27798
  let durableSourceUrl;
27112
27799
  if (referencesDir) {
@@ -27226,7 +27913,7 @@ var scaffoldStaticAdCommand = defineCommand101({
27226
27913
  run_estimated_credits: validation.estimatedCredits
27227
27914
  },
27228
27915
  checklist: {
27229
- edit_prompt: `Edit ${path19.basename(blueprintPath)} \u2014 it is the blueprint generated from your image; rewrite it into the ad you want (palette, copy, claims, subjects). It feeds the generator directly.`,
27916
+ edit_prompt: `Edit ${path20.basename(blueprintPath)} \u2014 it is the blueprint generated from your image; rewrite it into the ad you want (palette, copy, claims, subjects). It feeds the generator directly.`,
27230
27917
  assets_to_supply: report.elements,
27231
27918
  font_slot: report.includes_font ? "Drop a brand font at the [TODO] brandfont path (the describe pass recorded the ad's typefaces under `fonts` in prompt.json \u2014 match those). The font is wired into the render as a TYPE SPECIMEN reference so generated text takes the brand letterforms. Delete the brandfont + type_ref nodes to skip it." : "skipped (--skip-font)",
27232
27919
  actor_sheets: report.actor_sheets.length > 0 ? `Each living hero (${report.actor_sheets.join(", ")}) is fused into a generated multi-view reference sheet (image_reference_sheet) that the render grounds on \u2014 so drop ONE clean photo at that hero's ingest and the sheet builds the consistent turnaround. Pass --skip-actor-sheets to ground on the lone photo instead.` : "none (no person/animal heroes detected, or --skip-actor-sheets)",
@@ -27243,9 +27930,9 @@ var scaffoldStaticAdCommand = defineCommand101({
27243
27930
  });
27244
27931
 
27245
27932
  // src/commands/canvas/scaffold-video.ts
27246
- import { access as access2, cp, mkdir as mkdir7, readFile as readFile15, rm as rm6, writeFile as writeFile9 } from "fs/promises";
27933
+ import { access as access2, cp, mkdir as mkdir7, readFile as readFile16, rm as rm6, writeFile as writeFile9 } from "fs/promises";
27247
27934
  import { tmpdir as tmpdir2 } from "os";
27248
- import path22 from "path";
27935
+ import path23 from "path";
27249
27936
  import { defineCommand as defineCommand102 } from "citty";
27250
27937
 
27251
27938
  // src/engine/scaffold/lib/model-router.ts
@@ -27285,7 +27972,7 @@ function routeVideoModel(input) {
27285
27972
 
27286
27973
  // src/engine/nodes/local/lib/sceneDetect.ts
27287
27974
  import { execFile as execFile2 } from "child_process";
27288
- import { mkdtemp, readdir as readdir6, readFile as readFile13, rm as rm5 } from "fs/promises";
27975
+ import { mkdtemp, readdir as readdir7, readFile as readFile14, rm as rm5 } from "fs/promises";
27289
27976
  import { tmpdir } from "os";
27290
27977
  import { join as join2 } from "path";
27291
27978
  import { promisify as promisify2 } from "util";
@@ -27359,9 +28046,9 @@ async function runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs)
27359
28046
  ],
27360
28047
  { encoding: "utf-8", maxBuffer: 32 * 1024 * 1024, timeout: timeoutMs }
27361
28048
  );
27362
- const csvName = (await readdir6(outDir)).find((f) => f.toLowerCase().endsWith(".csv"));
28049
+ const csvName = (await readdir7(outDir)).find((f) => f.toLowerCase().endsWith(".csv"));
27363
28050
  if (!csvName) return [];
27364
- return parsePySceneDetectCsvCuts(await readFile13(join2(outDir, csvName), "utf-8"));
28051
+ return parsePySceneDetectCsvCuts(await readFile14(join2(outDir, csvName), "utf-8"));
27365
28052
  } finally {
27366
28053
  await rm5(outDir, { recursive: true, force: true });
27367
28054
  }
@@ -27386,23 +28073,23 @@ async function detectSceneCutsPySceneDetect(filePath, opts = {}) {
27386
28073
 
27387
28074
  // src/commands/canvas/composition-path.ts
27388
28075
  import { existsSync as existsSync4 } from "fs";
27389
- import path20 from "path";
28076
+ import path21 from "path";
27390
28077
  function resolveShippedCanvasDir(name, startDir, exists = existsSync4, maxDepth = 8) {
27391
- const rel = path20.join("canvas", name);
28078
+ const rel = path21.join("canvas", name);
27392
28079
  let dir = startDir;
27393
28080
  for (let i = 0; i < maxDepth; i++) {
27394
- const candidate = path20.join(dir, rel);
27395
- if (exists(path20.join(candidate, "meta.json"))) return candidate;
27396
- const parent = path20.dirname(dir);
28081
+ const candidate = path21.join(dir, rel);
28082
+ if (exists(path21.join(candidate, "meta.json"))) return candidate;
28083
+ const parent = path21.dirname(dir);
27397
28084
  if (parent === dir) break;
27398
28085
  dir = parent;
27399
28086
  }
27400
- return path20.resolve(startDir, "../../../", rel);
28087
+ return path21.resolve(startDir, "../../../", rel);
27401
28088
  }
27402
28089
 
27403
28090
  // src/commands/canvas/gitignore.ts
27404
- import { appendFile, readFile as readFile14 } from "fs/promises";
27405
- import path21 from "path";
28091
+ import { appendFile, readFile as readFile15 } from "fs/promises";
28092
+ import path22 from "path";
27406
28093
  function missingGitignoreEntries(existing, entries) {
27407
28094
  const present2 = new Set(
27408
28095
  existing.split("\n").map((l) => l.trim().replace(/\/+$/, "")).filter((l) => l.length > 0 && !l.startsWith("#"))
@@ -27410,10 +28097,10 @@ function missingGitignoreEntries(existing, entries) {
27410
28097
  return entries.filter((e) => !present2.has(e.trim().replace(/\/+$/, "")));
27411
28098
  }
27412
28099
  async function ensureGitignore(dir, entries) {
27413
- const file = path21.join(dir, ".gitignore");
28100
+ const file = path22.join(dir, ".gitignore");
27414
28101
  let existing;
27415
28102
  try {
27416
- existing = await readFile14(file, "utf8");
28103
+ existing = await readFile15(file, "utf8");
27417
28104
  } catch {
27418
28105
  return;
27419
28106
  }
@@ -27452,7 +28139,7 @@ ONE PERSON, MULTIPLE LOOKS: if a single individual plays MULTIPLE personas or wa
27452
28139
  For each kept element return: { "type": one of person|animal|product|logo|badge|location, "label": a short UPPER_SNAKE_CASE name (e.g. HERO, CREATOR_SKEPTIC, INSURANCE_CARD, LOGO), "description": a concrete reusable description to source/shoot the real asset \u2014 for a person/animal give a NEUTRAL castable role (e.g. "hero pet-owner, woman in her 30s" or "a small beagle"), NOT the original individual's literal face/identity: we RECAST with a FRESH person/animal, so never tell the agent to reuse the original. "expression": a living subject's typical expression or null, "cast_id": the global.cast id if it maps to one else null, "same_as": the label of another element this is the SAME individual as (different wardrobe/persona) else null, "scenes": the 0-based indices of ONLY the scenes where the element is ACTUALLY VISIBLE ON SCREEN \u2014 judged from that scene's start_frame_prompt / end_frame_prompt subjects and its action_detail, NOT from who is merely speaking. A narrator heard over b-roll is NOT present in that b-roll scene; a dog-running cutaway does NOT contain the couch creator just because she talks across it. Do NOT pad the list \u2014 an element wrongly listed in a scene makes the reproduction render the wrong subject there (e.g. the creator appearing in a pure-dog b-roll). When in doubt, leave a scene OUT. Output ONLY the JSON object.`;
27453
28140
  async function loadAssetText2(ref, label) {
27454
28141
  const r = ref;
27455
- if (typeof r?.path === "string") return readFile15(r.path, "utf8");
28142
+ if (typeof r?.path === "string") return readFile16(r.path, "utf8");
27456
28143
  if (typeof r?.url === "string") {
27457
28144
  const res = await fetch(r.url);
27458
28145
  if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
@@ -27471,7 +28158,7 @@ async function loadTranscriptBestEffort(ref) {
27471
28158
  async function stageCaptions(outDir, transcript) {
27472
28159
  const text2 = transcript?.trim();
27473
28160
  if (!text2 || text2 === "[]") return {};
27474
- const compositionPath = path22.join(outDir, "tiktok-captions-composition");
28161
+ const compositionPath = path23.join(outDir, "tiktok-captions-composition");
27475
28162
  await cp(SHIPPED_CAPTIONS_DIR, compositionPath, { recursive: true });
27476
28163
  return { compositionPath };
27477
28164
  }
@@ -27489,11 +28176,11 @@ function patchCompositionHtml(html, dims) {
27489
28176
  return html.replace(/(<meta\s+name="viewport"\s+content="width=)\d+(,\s*height=)\d+(")/i, `$1${dims.w}$2${dims.h}$3`).replace(/(width:\s*)\d+(px;\s*height:\s*)\d+(px;)/i, `$1${dims.w}$2${dims.h}$3`).replace(/(data-width=")\d+(")/i, `$1${dims.w}$2`).replace(/(data-height=")\d+(")/i, `$1${dims.h}$2`);
27490
28177
  }
27491
28178
  async function stampCompositionDims(compositionDir, dims) {
27492
- const metaPath = path22.join(compositionDir, "meta.json");
27493
- const rawMeta = await readFile15(metaPath, "utf8");
28179
+ const metaPath = path23.join(compositionDir, "meta.json");
28180
+ const rawMeta = await readFile16(metaPath, "utf8");
27494
28181
  await writeFile9(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
27495
- const htmlPath = path22.join(compositionDir, "index.html");
27496
- const rawHtml = await readFile15(htmlPath, "utf8");
28182
+ const htmlPath = path23.join(compositionDir, "index.html");
28183
+ const rawHtml = await readFile16(htmlPath, "utf8");
27497
28184
  await writeFile9(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
27498
28185
  }
27499
28186
  function parseElements2(raw) {
@@ -27541,7 +28228,7 @@ var VIDEO_EXT_BY_MIME = {
27541
28228
  "video/x-matroska": ".mkv"
27542
28229
  };
27543
28230
  function referenceVideoExt(url, contentType) {
27544
- const fromPath = path22.extname(new URL(url).pathname).toLowerCase();
28231
+ const fromPath = path23.extname(new URL(url).pathname).toLowerCase();
27545
28232
  if (fromPath && fromPath.length <= 5) return fromPath;
27546
28233
  const mime = (contentType ?? "").split(";")[0]?.trim().toLowerCase();
27547
28234
  return mime && VIDEO_EXT_BY_MIME[mime] || ".mp4";
@@ -27567,7 +28254,7 @@ function videoDefinitionDescription(blueprint) {
27567
28254
  return typeof product === "string" && product.trim() ? product.trim() : void 0;
27568
28255
  }
27569
28256
  async function materializeReferenceVideo(fileArg2) {
27570
- if (!/^https?:\/\//i.test(fileArg2)) return path22.resolve(fileArg2);
28257
+ if (!/^https?:\/\//i.test(fileArg2)) return path23.resolve(fileArg2);
27571
28258
  let bytes;
27572
28259
  let contentType;
27573
28260
  try {
@@ -27579,7 +28266,7 @@ async function materializeReferenceVideo(fileArg2) {
27579
28266
  throw new Error(`failed to download reference video: ${e instanceof Error ? e.message : String(e)}`);
27580
28267
  }
27581
28268
  if (bytes.length === 0) throw new Error("reference video download was empty");
27582
- const dest = path22.join(
28269
+ const dest = path23.join(
27583
28270
  tmpdir2(),
27584
28271
  `baker-ref-${sha256Hex(bytes).slice(0, 16)}${referenceVideoExt(fileArg2, contentType)}`
27585
28272
  );
@@ -27799,11 +28486,11 @@ var scaffoldVideoCommand = defineCommand102({
27799
28486
  } catch (e) {
27800
28487
  return fail4("download", e instanceof Error ? e.message : String(e));
27801
28488
  }
27802
- const base = path22.basename(videoPath, path22.extname(videoPath));
27803
- const outPath = args.out ? path22.resolve(String(args.out)) : slug ? path22.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path22.join(path22.dirname(videoPath), `${base}.video.canvas.json`);
27804
- const outDir = path22.dirname(outPath);
27805
- const blueprintPath = path22.join(outDir, "prompt.json");
27806
- const blueprintStylePath = path22.join(outDir, "prompt.style.json");
28489
+ const base = path23.basename(videoPath, path23.extname(videoPath));
28490
+ const outPath = args.out ? path23.resolve(String(args.out)) : slug ? path23.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path23.join(path23.dirname(videoPath), `${base}.video.canvas.json`);
28491
+ const outDir = path23.dirname(outPath);
28492
+ const blueprintPath = path23.join(outDir, "prompt.json");
28493
+ const blueprintStylePath = path23.join(outDir, "prompt.style.json");
27807
28494
  const frames = args.frames === "reuse" ? "reuse" : "generate";
27808
28495
  const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
27809
28496
  if (Number.isFinite(maxScenes)) {
@@ -27848,12 +28535,12 @@ var scaffoldVideoCommand = defineCommand102({
27848
28535
  `
27849
28536
  );
27850
28537
  }
27851
- const compositionDest = path22.join(outDir, "video-overlay-composition");
28538
+ const compositionDest = path23.join(outDir, "video-overlay-composition");
27852
28539
  await cp(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
27853
28540
  await stampCompositionDims(compositionDest, outDims);
27854
- const indexPath = path22.join(compositionDest, "index.html");
28541
+ const indexPath = path23.join(compositionDest, "index.html");
27855
28542
  const overlayHtml = buildOverlayHtml(blueprint, { captionsActive: Boolean(transcript) });
27856
- const indexHtml = await readFile15(indexPath, "utf8");
28543
+ const indexHtml = await readFile16(indexPath, "utf8");
27857
28544
  const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
27858
28545
  if (injected === indexHtml && overlayHtml.trim()) {
27859
28546
  fail4(
@@ -27867,10 +28554,10 @@ var scaffoldVideoCommand = defineCommand102({
27867
28554
  const opts = {
27868
28555
  imageModel,
27869
28556
  videoModel,
27870
- overlayCompositionPath: path22.relative(outDir, compositionDest),
27871
- captionsCompositionPath: captions.compositionPath ? path22.relative(outDir, captions.compositionPath) : void 0,
27872
- blueprintPath: path22.relative(outDir, blueprintPath),
27873
- blueprintStylePath: path22.relative(outDir, blueprintStylePath),
28557
+ overlayCompositionPath: path23.relative(outDir, compositionDest),
28558
+ captionsCompositionPath: captions.compositionPath ? path23.relative(outDir, captions.compositionPath) : void 0,
28559
+ blueprintPath: path23.relative(outDir, blueprintPath),
28560
+ blueprintStylePath: path23.relative(outDir, blueprintStylePath),
27874
28561
  frames,
27875
28562
  ambient: Boolean(args.ambient),
27876
28563
  seamDedup: resolveSeamDedup(args["seam-dedup"]),
@@ -27898,7 +28585,7 @@ var scaffoldVideoCommand = defineCommand102({
27898
28585
  await writeFile9(outPath, `${JSON.stringify(canvas, null, 2)}
27899
28586
  `, "utf8");
27900
28587
  await writeFile9(
27901
- path22.join(outDir, REBUILD_FILE),
28588
+ path23.join(outDir, REBUILD_FILE),
27902
28589
  `${JSON.stringify({ elements, opts }, null, 2)}
27903
28590
  `,
27904
28591
  "utf8"
@@ -27923,7 +28610,7 @@ var scaffoldVideoCommand = defineCommand102({
27923
28610
  await ensureGitignore(process.cwd(), ["canvas/", ".context/"]);
27924
28611
  const sourceRef = videoSourceReference(blueprint, fileArg2);
27925
28612
  if (slug) {
27926
- const definitionPath = path22.join(outDir, "_definition.md");
28613
+ const definitionPath = path23.join(outDir, "_definition.md");
27927
28614
  if (!await fileExists2(definitionPath)) {
27928
28615
  await writeFile9(
27929
28616
  definitionPath,
@@ -27978,7 +28665,7 @@ var scaffoldVideoCommand = defineCommand102({
27978
28665
  graph: canvas.metadata?.video?.graph_stats
27979
28666
  },
27980
28667
  checklist: {
27981
- edit_prompt: `The blueprint is split so you edit ONE small file at a time \u2014 never a giant one. Per-scene content (a scene's dialogue, action, frame prompts, overlays) lives in \`scenes/sNN.json\` \u2014 edit the single scene you want to change. Global cast/palette/brand/copy lives in \`${path22.basename(blueprintPath)}\`. \`baker canvas validate\`/\`run\` re-assemble the blueprint and re-flow every edited scene back into the render (and regenerate ${path22.basename(blueprintStylePath)}, the projection each frame's target_blueprint reads) \u2014 so your scene edits reach the render automatically. Never hand-edit the inlined node prompts in the canvas or the derived ${path22.basename(blueprintStylePath)}; both are regenerated.`,
28668
+ edit_prompt: `The blueprint is split so you edit ONE small file at a time \u2014 never a giant one. Per-scene content (a scene's dialogue, action, frame prompts, overlays) lives in \`scenes/sNN.json\` \u2014 edit the single scene you want to change. Global cast/palette/brand/copy lives in \`${path23.basename(blueprintPath)}\`. \`baker canvas validate\`/\`run\` re-assemble the blueprint and re-flow every edited scene back into the render (and regenerate ${path23.basename(blueprintStylePath)}, the projection each frame's target_blueprint reads) \u2014 so your scene edits reach the render automatically. Never hand-edit the inlined node prompts in the canvas or the derived ${path23.basename(blueprintStylePath)}; both are regenerated.`,
27982
28669
  recurring_elements_to_supply: report.elements,
27983
28670
  voices_to_confirm: report.dialogue.map((d) => ({
27984
28671
  scene: d.scene,
@@ -28015,8 +28702,8 @@ var scaffoldVideoCommand = defineCommand102({
28015
28702
  });
28016
28703
 
28017
28704
  // src/commands/canvas/set-prompt.ts
28018
- import { readFile as readFile16, writeFile as writeFile10 } from "fs/promises";
28019
- import path23 from "path";
28705
+ import { readFile as readFile17, writeFile as writeFile10 } from "fs/promises";
28706
+ import path24 from "path";
28020
28707
  import { defineCommand as defineCommand103 } from "citty";
28021
28708
  function setNodePrompt(canvas, nodeId, text2) {
28022
28709
  const nodes = canvas?.nodes;
@@ -28044,17 +28731,17 @@ var setPromptCommand = defineCommand103({
28044
28731
  "text-file": { type: "string", description: "Read the new prompt from a UTF-8 file (preserves accents/newlines)" }
28045
28732
  },
28046
28733
  async run({ args }) {
28047
- const filePath = path23.resolve(String(args.file));
28734
+ const filePath = path24.resolve(String(args.file));
28048
28735
  let canvas;
28049
28736
  try {
28050
- canvas = JSON.parse(await readFile16(filePath, "utf8"));
28737
+ canvas = JSON.parse(await readFile17(filePath, "utf8"));
28051
28738
  } catch (e) {
28052
28739
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "parse", message: String(e) } }, null, 2)}
28053
28740
  `);
28054
28741
  process.exit(2);
28055
28742
  }
28056
28743
  let text2;
28057
- if (args["text-file"]) text2 = await readFile16(path23.resolve(String(args["text-file"])), "utf8");
28744
+ if (args["text-file"]) text2 = await readFile17(path24.resolve(String(args["text-file"])), "utf8");
28058
28745
  else if (args.text !== void 0) text2 = String(args.text);
28059
28746
  else {
28060
28747
  process.stderr.write(
@@ -28075,7 +28762,7 @@ var setPromptCommand = defineCommand103({
28075
28762
  process.exit(2);
28076
28763
  return;
28077
28764
  }
28078
- const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path23.dirname(filePath)), defaultRegistry());
28765
+ const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path24.dirname(filePath)), defaultRegistry());
28079
28766
  if (!validation.ok) {
28080
28767
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "validation", issues: validation.issues } }, null, 2)}
28081
28768
  `);
@@ -28090,8 +28777,8 @@ var setPromptCommand = defineCommand103({
28090
28777
  });
28091
28778
 
28092
28779
  // src/commands/canvas/validate.ts
28093
- import { readFile as readFile17 } from "fs/promises";
28094
- import path24 from "path";
28780
+ import { readFile as readFile18 } from "fs/promises";
28781
+ import path25 from "path";
28095
28782
  import { defineCommand as defineCommand104 } from "citty";
28096
28783
  var validateCommand = defineCommand104({
28097
28784
  meta: {
@@ -28100,8 +28787,8 @@ var validateCommand = defineCommand104({
28100
28787
  },
28101
28788
  args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
28102
28789
  async run({ args }) {
28103
- const filePath = path24.resolve(String(args.file));
28104
- const raw = await readFile17(filePath, "utf8");
28790
+ const filePath = path25.resolve(String(args.file));
28791
+ const raw = await readFile18(filePath, "utf8");
28105
28792
  let parsed;
28106
28793
  try {
28107
28794
  parsed = JSON.parse(raw);
@@ -28113,7 +28800,7 @@ var validateCommand = defineCommand104({
28113
28800
  }
28114
28801
  const healed = await healAbsoluteCanvasPaths(filePath, parsed);
28115
28802
  parsed = healed.canvas;
28116
- parsed = resolveRelativeCanvasPaths(parsed, path24.dirname(filePath));
28803
+ parsed = resolveRelativeCanvasPaths(parsed, path25.dirname(filePath));
28117
28804
  let styleProjection = "not_applicable";
28118
28805
  try {
28119
28806
  styleProjection = await syncStyleProjection(parsed, (line) => process.stderr.write(`${line}
@@ -28537,7 +29224,7 @@ import { defineCommand as defineCommand109 } from "citty";
28537
29224
  import { defineCommand as defineCommand108 } from "citty";
28538
29225
 
28539
29226
  // src/commands/images/api.ts
28540
- import { readFile as readFile18 } from "fs/promises";
29227
+ import { readFile as readFile19 } from "fs/promises";
28541
29228
  import { basename, extname } from "path";
28542
29229
  var imageProcessingTimeoutMs = 18e4;
28543
29230
  var imageReadyPollIntervalMs = 2e3;
@@ -28551,7 +29238,7 @@ var mimeMap = {
28551
29238
  ".avif": "image/avif"
28552
29239
  };
28553
29240
  var defaultImageApiDeps = {
28554
- readFile: readFile18,
29241
+ readFile: readFile19,
28555
29242
  post: apiPost,
28556
29243
  get: apiGet,
28557
29244
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
@@ -31270,7 +31957,7 @@ function cropSprite(input, region) {
31270
31957
 
31271
31958
  // src/lib/image/io.ts
31272
31959
  import { randomBytes } from "crypto";
31273
- import { glob as fsGlob, readFile as readFile19, rename, stat as stat4, writeFile as writeFile11 } from "fs/promises";
31960
+ import { glob as fsGlob, readFile as readFile20, rename, stat as stat4, writeFile as writeFile11 } from "fs/promises";
31274
31961
  import { dirname as dirname2, extname as extname2, join as join4, resolve as resolve4 } from "path";
31275
31962
  var REMOTE_RE = /^https?:\/\//i;
31276
31963
  var GLOB_RE = /[*?[\]{}]/;
@@ -31303,7 +31990,7 @@ async function readImageBuffer(pathOrUrl) {
31303
31990
  const { buffer } = await fetchExternalBytes(pathOrUrl, { maxBytes: MAX_REMOTE_IMAGE_BYTES });
31304
31991
  return buffer;
31305
31992
  }
31306
- return readFile19(pathOrUrl);
31993
+ return readFile20(pathOrUrl);
31307
31994
  }
31308
31995
  async function isDirectory(path39) {
31309
31996
  try {
@@ -33639,7 +34326,7 @@ function parseResize(args) {
33639
34326
  }
33640
34327
  return void 0;
33641
34328
  }
33642
- function parseColor(raw) {
34329
+ function parseColor2(raw) {
33643
34330
  if (typeof raw !== "string" || raw.length === 0) return void 0;
33644
34331
  try {
33645
34332
  return parseHex(raw);
@@ -33649,7 +34336,7 @@ function parseColor(raw) {
33649
34336
  }
33650
34337
  function buildOptions(args) {
33651
34338
  const resize = parseResize(args);
33652
- const color = parseColor(args.color);
34339
+ const color = parseColor2(args.color);
33653
34340
  const removeBackground2 = typeof args["remove-bg"] === "boolean" ? args["remove-bg"] : color !== void 0;
33654
34341
  const shrinkToContent = typeof args["shrink-to-content"] === "boolean" ? args["shrink-to-content"] : removeBackground2;
33655
34342
  return {
@@ -34666,222 +35353,10 @@ Full guide: __tooling__/docs/tools/baker/images.md`
34666
35353
  import { defineCommand as defineCommand160 } from "citty";
34667
35354
 
34668
35355
  // src/commands/landing/critique.ts
34669
- import { readdir as readdir8, stat as stat6 } from "fs/promises";
35356
+ import { readdir as readdir9, stat as stat6 } from "fs/promises";
34670
35357
  import path29 from "path";
34671
35358
  import { defineCommand as defineCommand150 } from "citty";
34672
35359
 
34673
- // src/engine/landing/lib/brand-tokens.ts
34674
- import { readFile as readFile20 } from "fs/promises";
34675
- import path25 from "path";
34676
-
34677
- // src/engine/landing/lib/color.ts
34678
- var NEUTRAL_COLOR_KEYWORDS = /* @__PURE__ */ new Set([
34679
- "transparent",
34680
- "currentcolor",
34681
- "black",
34682
- "white",
34683
- "gray",
34684
- "grey",
34685
- "silver",
34686
- "dimgray",
34687
- "dimgrey",
34688
- "darkgray",
34689
- "darkgrey",
34690
- "lightgray",
34691
- "lightgrey",
34692
- "gainsboro",
34693
- "whitesmoke"
34694
- ]);
34695
- function hexChannels(color) {
34696
- const long = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})(?:[0-9a-f]{2})?$/i);
34697
- if (long)
34698
- return [
34699
- Number.parseInt(long[1] ?? "0", 16),
34700
- Number.parseInt(long[2] ?? "0", 16),
34701
- Number.parseInt(long[3] ?? "0", 16)
34702
- ];
34703
- const short = color.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])(?:[0-9a-f])?$/i);
34704
- if (short) {
34705
- const dup = (s) => Number.parseInt(`${s ?? "0"}${s ?? "0"}`, 16);
34706
- return [dup(short[1]), dup(short[2]), dup(short[3])];
34707
- }
34708
- return null;
34709
- }
34710
- function parseColor2(raw) {
34711
- const c = String(raw || "").trim().toLowerCase();
34712
- if (!c || c === "transparent") return null;
34713
- const rgb = c.match(/rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:[\s,/]+([\d.]+%?))?\s*\)/i);
34714
- if (rgb) {
34715
- const alpha = rgb[4];
34716
- const a = alpha === void 0 ? 1 : alpha.endsWith("%") ? Number.parseFloat(alpha) / 100 : Number.parseFloat(alpha);
34717
- return { r: Number(rgb[1]), g: Number(rgb[2]), b: Number(rgb[3]), a };
34718
- }
34719
- const oklch = parseOklch(c);
34720
- if (oklch) return oklch;
34721
- const hex = hexChannels(c);
34722
- if (hex) return { r: hex[0], g: hex[1], b: hex[2], a: 1 };
34723
- return null;
34724
- }
34725
- function parseOklch(c) {
34726
- const m = c.match(/oklch\(\s*([\d.]+%?)\s+([\d.]+%?)\s+([\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+%?))?\s*\)/i);
34727
- if (!m) return null;
34728
- const pct = (s, scale) => s.endsWith("%") ? Number.parseFloat(s) / 100 * scale : Number.parseFloat(s);
34729
- const alpha = m[4];
34730
- const a = alpha === void 0 ? 1 : pct(alpha, 1);
34731
- return { ...oklchToRgb(pct(m[1] ?? "0", 1), pct(m[2] ?? "0", 0.4), Number.parseFloat(m[3] ?? "0")), a };
34732
- }
34733
- function oklchToRgb(L, C, H) {
34734
- const hRad = H * Math.PI / 180;
34735
- const a = C * Math.cos(hRad);
34736
- const b = C * Math.sin(hRad);
34737
- const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
34738
- const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
34739
- const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
34740
- const lin = [
34741
- 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
34742
- -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
34743
- -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
34744
- ].map((v) => {
34745
- const g = v <= 31308e-7 ? 12.92 * v : 1.055 * Math.sign(v) * Math.abs(v) ** (1 / 2.4) - 0.055;
34746
- return Math.round(Math.max(0, Math.min(1, g)) * 255);
34747
- });
34748
- return { r: lin[0] ?? 0, g: lin[1] ?? 0, b: lin[2] ?? 0 };
34749
- }
34750
- function isNeutralAuthoredColor(rawColor) {
34751
- const c = String(rawColor || "").trim().toLowerCase();
34752
- if (!c) return false;
34753
- if (NEUTRAL_COLOR_KEYWORDS.has(c)) return true;
34754
- if (/^rgba?\(/i.test(c)) {
34755
- const channels2 = c.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);
34756
- if (channels2) {
34757
- const values = [1, 2, 3].map((i) => Number(channels2[i]));
34758
- return Math.max(...values) - Math.min(...values) < 30;
34759
- }
34760
- return false;
34761
- }
34762
- const oklch = c.match(/oklch\(\s*[\d.]+%?\s+([\d.-]+)/i);
34763
- if (oklch) return Number.parseFloat(oklch[1] ?? "0") < 0.02;
34764
- const lch = c.match(/lch\(\s*[\d.]+%?\s+([\d.-]+)/i);
34765
- if (lch) return Number.parseFloat(lch[1] ?? "0") < 3;
34766
- const hsl = c.match(/hsla?\(\s*[\d.-]+\s*,?\s*([\d.]+)%/i);
34767
- if (hsl) return Number.parseFloat(hsl[1] ?? "0") < 10;
34768
- const channels = hexChannels(c);
34769
- if (channels) return Math.max(...channels) - Math.min(...channels) < 30;
34770
- return false;
34771
- }
34772
- function hasChroma(c, threshold = 30) {
34773
- if (!c) return false;
34774
- return Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b) >= threshold;
34775
- }
34776
- function getHue(c) {
34777
- if (!c) return 0;
34778
- const r = c.r / 255;
34779
- const g = c.g / 255;
34780
- const b = c.b / 255;
34781
- const max = Math.max(r, g, b);
34782
- const min = Math.min(r, g, b);
34783
- if (max === min) return 0;
34784
- const d = max - min;
34785
- let h;
34786
- if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
34787
- else if (max === g) h = ((b - r) / d + 2) / 6;
34788
- else h = ((r - g) / d + 4) / 6;
34789
- return Math.round(h * 360);
34790
- }
34791
- function isAiPurpleHue(hue) {
34792
- return hue >= 225 && hue <= 310;
34793
- }
34794
- function isCreamColor(c) {
34795
- if (Math.min(c.r, c.g, c.b) < 209) return false;
34796
- if (!(c.r >= c.g && c.g >= c.b)) return false;
34797
- const warmth = c.r - c.b;
34798
- return warmth >= 6 && warmth <= 48;
34799
- }
34800
-
34801
- // src/engine/landing/lib/brand-tokens.ts
34802
- var EMPTY = { fonts: /* @__PURE__ */ new Set(), colors: [], hasTokens: false };
34803
- function isWeightOrStyle(value) {
34804
- return /^(?:\d+(?:\.\d+)?[a-z%]*|normal|bold|bolder|lighter|italic|oblique|none|auto)$/i.test(value);
34805
- }
34806
- function parseBrandTokens(globalCss, brandMd) {
34807
- const fonts = /* @__PURE__ */ new Set();
34808
- const colors = [];
34809
- parseGlobalCss(globalCss, fonts, colors);
34810
- parseBrandMd(brandMd, fonts, colors);
34811
- return { fonts, colors, hasTokens: fonts.size > 0 || colors.length > 0 };
34812
- }
34813
- function parseGlobalCss(rawCss, fonts, colors) {
34814
- const globalCss = rawCss.replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, " "));
34815
- for (const m of globalCss.matchAll(/(?<![\w-])--font-([\w-]*)\s*:\s*([^;]+);/gi)) {
34816
- const slug = m[1] ?? "";
34817
- if (slug.includes("--") || /^weight(?:$|[-\s])/i.test(slug)) continue;
34818
- const first = (m[2] ?? "").split(",")[0]?.trim().replace(/^['"]|['"]$/g, "").toLowerCase();
34819
- if (!first || isWeightOrStyle(first)) continue;
34820
- fonts.add(first);
34821
- }
34822
- for (const m of globalCss.matchAll(
34823
- /--color-[\w-]*\s*:\s*(#[0-9a-f]{3,8}\b|rgba?\([^)]+\)|oklch\([^)]+\)|hsla?\([^)]+\))/gi
34824
- )) {
34825
- const c = parseColor2(m[1] ?? "");
34826
- if (c) colors.push(c);
34827
- }
34828
- }
34829
- function parseBrandMd(brandMd, fonts, colors) {
34830
- const isProhibition = (line) => /\b(?:never|don'?t|do not|avoid|not|instead of|ban(?:ned)?)\b/i.test(line);
34831
- for (const line of brandMd.split("\n")) {
34832
- if (isProhibition(line)) continue;
34833
- for (const m of line.matchAll(/#[0-9a-f]{6}\b/gi)) {
34834
- const c = parseColor2(m[0]);
34835
- if (c) colors.push(c);
34836
- }
34837
- if (!/\b(?:font|typeface|family)\b/i.test(line)) continue;
34838
- const named = line.match(/["'`]([A-Za-z][\w ]{1,30})["'`]/g);
34839
- if (named) for (const n of named) fonts.add(n.slice(1, -1).trim().toLowerCase());
34840
- }
34841
- }
34842
- function brandTokensFromCanonical(tokens) {
34843
- const fonts = new Set(tokens.fonts.map((f) => f.family.toLowerCase()));
34844
- const colors = [];
34845
- for (const color of tokens.colors) {
34846
- const parsed = parseColor2(color.value);
34847
- if (parsed) colors.push(parsed);
34848
- }
34849
- return { fonts, colors, hasTokens: fonts.size > 0 || colors.length > 0 };
34850
- }
34851
- async function loadBrandTokens(projectRoot) {
34852
- const tokensRaw = await safeRead(path25.join(projectRoot, "src", "brand", "tokens.json"));
34853
- if (tokensRaw) {
34854
- try {
34855
- const canonical2 = brandTokensSchema.safeParse(JSON.parse(tokensRaw));
34856
- if (canonical2.success) return brandTokensFromCanonical(canonical2.data);
34857
- } catch {
34858
- }
34859
- }
34860
- const globalCss = await safeRead(path25.join(projectRoot, "src", "styles", "global.css"));
34861
- const brandMd = await safeRead(path25.join(projectRoot, "src", "brand", "BRAND.md"));
34862
- if (!globalCss && !brandMd) return EMPTY;
34863
- return parseBrandTokens(globalCss, brandMd);
34864
- }
34865
- async function safeRead(file) {
34866
- try {
34867
- return await readFile20(file, "utf8");
34868
- } catch {
34869
- return "";
34870
- }
34871
- }
34872
- function brandCommitsFont(brand, snippet) {
34873
- if (brand.fonts.size === 0) return false;
34874
- const lower = snippet.toLowerCase();
34875
- for (const f of brand.fonts) if (f.length >= 3 && lower.includes(f)) return true;
34876
- return false;
34877
- }
34878
- function brandCommitsAiHue(brand) {
34879
- return brand.colors.some((c) => hasChroma(c, 40) && isAiPurpleHue(getHue(c)));
34880
- }
34881
- function brandCommitsCream(brand) {
34882
- return brand.colors.some((c) => isCreamColor(c));
34883
- }
34884
-
34885
35360
  // src/engine/landing/lib/constants.ts
34886
35361
  var OVERUSED_FONTS = /* @__PURE__ */ new Set([
34887
35362
  // Older monoculture (still ubiquitous):
@@ -35444,7 +35919,7 @@ var LINE_MATCHERS = [
35444
35919
  id: "cream-palette",
35445
35920
  regex: /background(?:-color)?\s*:\s*(#[0-9a-f]{6}\b|rgba?\([^)]+\))/gi,
35446
35921
  test: (m) => {
35447
- const c = parseColor2(cap2(m, 1));
35922
+ const c = parseColor(cap2(m, 1));
35448
35923
  return c !== null && isCreamColor(c);
35449
35924
  },
35450
35925
  fmt: (m) => `${cap2(m, 1)} (cream ground)`
@@ -35785,7 +36260,7 @@ function shadowIsGlow(value) {
35785
36260
  const y = Number.parseFloat(offsets[1] ?? "0");
35786
36261
  const blur = Number.parseFloat(offsets[2] ?? "0");
35787
36262
  if (!(x === 0 && y === 0 && blur > 4)) return false;
35788
- const c = parseColor2(colorMatch[0]);
36263
+ const c = parseColor(colorMatch[0]);
35789
36264
  return c !== null && hasChroma(c, 30);
35790
36265
  }
35791
36266
  function firstFamily(decl) {
@@ -35979,7 +36454,7 @@ async function writeCritiqueSnapshot(projectRoot, snapshot) {
35979
36454
  }
35980
36455
 
35981
36456
  // src/commands/landing/source-version.ts
35982
- import { readdir as readdir7, readFile as readFile22, stat as stat5 } from "fs/promises";
36457
+ import { readdir as readdir8, readFile as readFile22, stat as stat5 } from "fs/promises";
35983
36458
  import path28 from "path";
35984
36459
  async function landingSourceRelPaths(landingDir) {
35985
36460
  const rel = [];
@@ -36020,7 +36495,7 @@ async function isFile(p) {
36020
36495
  async function walkAstro(dir) {
36021
36496
  let entries;
36022
36497
  try {
36023
- entries = await readdir7(dir, { withFileTypes: true });
36498
+ entries = await readdir8(dir, { withFileTypes: true });
36024
36499
  } catch {
36025
36500
  return [];
36026
36501
  }
@@ -36146,7 +36621,7 @@ async function critiqueOne(projectRoot, slug, brand, references) {
36146
36621
  }
36147
36622
  async function listLandingSlugs(projectRoot) {
36148
36623
  try {
36149
- const entries = await readdir8(path29.join(projectRoot, "src", "pages"), { withFileTypes: true });
36624
+ const entries = await readdir9(path29.join(projectRoot, "src", "pages"), { withFileTypes: true });
36150
36625
  return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
36151
36626
  } catch {
36152
36627
  return [];
@@ -39669,7 +40144,7 @@ var ESCALATION = {
39669
40144
  medium: "`medium` is a quick lookup, not a search. Re-ask the SAME question once with `--depth high` before concluding the web has nothing on it.",
39670
40145
  high: "`high` already thought hard about this. `--depth xhigh` is the only step left and it can run 15 minutes \u2014 take it only if this answer blocks a deliverable; otherwise record the gap and move on."
39671
40146
  };
39672
- function fetchHints({
40147
+ function fetchHints2({
39673
40148
  truncated,
39674
40149
  contentChars,
39675
40150
  shownChars
@@ -40153,7 +40628,7 @@ Examples:
40153
40628
  content
40154
40629
  },
40155
40630
  fields: FIELDS6,
40156
- hints: fetchHints({
40631
+ hints: fetchHints2({
40157
40632
  truncated: page.truncated,
40158
40633
  contentChars: page.contentChars,
40159
40634
  shownChars: content.length