@koda-sl/baker-cli 0.217.0 → 0.224.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
@@ -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,162 @@ 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
+ var FONT_STYLES = ["normal", "italic"];
21276
+ var ITAL_AXIS = { normal: 0, italic: 1 };
21277
+ function fontsourceId(family) {
21278
+ return family.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
21279
+ }
21280
+ function fontsourceFaces(meta, weights, subsets, styles = ["normal"]) {
21281
+ const faces = [];
21282
+ for (const subset of subsets.filter((s) => meta.subsets.includes(s))) {
21283
+ for (const weight of weights.filter((w) => meta.weights.includes(w))) {
21284
+ for (const style of styles.filter((style2) => meta.styles.includes(style2))) {
21285
+ const url = meta.variants[String(weight)]?.[style]?.[subset]?.url?.woff2;
21286
+ if (!url) continue;
21287
+ faces.push({
21288
+ subset,
21289
+ family: meta.family,
21290
+ style,
21291
+ weight,
21292
+ url,
21293
+ unicodeRange: meta.unicodeRange?.[subset] ?? "",
21294
+ provider: "fontsource"
21295
+ });
21296
+ }
21297
+ }
21298
+ }
21299
+ return faces;
21300
+ }
21301
+ function undisambiguatedSubsets(faces) {
21302
+ if (new Set(faces.map((face) => face.subset)).size < 2) return [];
21303
+ return [...new Set(faces.filter((face) => !face.unicodeRange).map((face) => face.subset))];
21304
+ }
21305
+ function suggestFamilies(wanted, catalogue, limit = 5) {
21306
+ const key = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
21307
+ const target = key(wanted);
21308
+ if (!target) return [];
21309
+ const words3 = target.split(" ");
21310
+ const scored = catalogue.map((family) => {
21311
+ const candidate = key(family);
21312
+ if (candidate === target) return { family, score: 0 };
21313
+ if (candidate.startsWith(target)) return { family, score: 1 };
21314
+ if (candidate.includes(target)) return { family, score: 2 };
21315
+ const shared = words3.filter((w) => w.length > 2 && candidate.includes(w)).length;
21316
+ return { family, score: shared > 0 ? 3 + (words3.length - shared) : Number.POSITIVE_INFINITY };
21317
+ }).filter((c) => Number.isFinite(c.score)).sort((a, b) => a.score - b.score || a.family.localeCompare(b.family));
21318
+ return scored.slice(0, limit).map((c) => c.family);
21319
+ }
21320
+ function asFontsourceMeta(body) {
21321
+ if (typeof body !== "object" || body === null) return null;
21322
+ const meta = body;
21323
+ 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;
21324
+ return ok ? meta : null;
21325
+ }
21326
+ async function probeGoogleFaces(family, lookup) {
21327
+ const got = await lookup(googleFontCssUrl(family, STANDARD_WEIGHTS, FONT_STYLES));
21328
+ if (got.status === "unreachable") return null;
21329
+ return got.status === "served" ? parseGoogleFontFaces(got.css) : [];
21330
+ }
21331
+ async function resolveFromGoogle(family, weights, subsets, styles, deps) {
21332
+ const google = await deps.lookupGoogleCss(googleFontCssUrl(family, weights, FONT_STYLES));
21333
+ if (google.status === "unreachable") return { ok: false, reason: "unreachable", provider: "google" };
21334
+ if (google.status === "absent") {
21335
+ const served2 = await probeGoogleFaces(family, deps.lookupGoogleCss);
21336
+ if (served2 === null) return { ok: false, reason: "unreachable", provider: "google" };
21337
+ const available = [...new Set(served2.map((face) => face.weight))].sort((a, b) => a - b);
21338
+ return available.length > 0 ? { ok: false, reason: "weight", provider: "google", available } : null;
21339
+ }
21340
+ const served = parseGoogleFontFaces(google.css);
21341
+ if (served.length === 0) return null;
21342
+ const availableStyles = [...new Set(served.map((face) => face.style))].sort();
21343
+ const inSubset = served.filter((face) => subsets.includes(face.subset));
21344
+ const faces = inSubset.filter((face) => styles.includes(face.style)).map((face) => ({ ...face, provider: "google" }));
21345
+ if (faces.length > 0) return { ok: true, provider: "google", faces, availableStyles };
21346
+ return { ok: false, reason: "subset", provider: "google", available: [...new Set(served.map((f) => f.subset))] };
21347
+ }
21348
+ async function resolveBrandFaces(family, weights, subsets, deps, styles = ["normal"]) {
21349
+ const google = await resolveFromGoogle(family, weights, subsets, styles, deps);
21350
+ if (google) return google;
21351
+ let body;
21352
+ try {
21353
+ body = await deps.fetchJson(`${FONTSOURCE_API}/fonts/${fontsourceId(family)}`);
21354
+ } catch {
21355
+ return { ok: false, reason: "unreachable", provider: "fontsource" };
21356
+ }
21357
+ const meta = asFontsourceMeta(body);
21358
+ if (meta) {
21359
+ const faces = fontsourceFaces(meta, weights, subsets, styles);
21360
+ const ambiguous = undisambiguatedSubsets(faces);
21361
+ if (ambiguous.length > 0) {
21362
+ return { ok: false, reason: "ambiguous-subsets", provider: "fontsource", available: meta.subsets };
21363
+ }
21364
+ if (faces.length > 0) return { ok: true, provider: "fontsource", faces, availableStyles: [...meta.styles].sort() };
21365
+ if (!subsets.some((subset) => meta.subsets.includes(subset))) {
21366
+ return { ok: false, reason: "subset", provider: "fontsource", available: meta.subsets };
21367
+ }
21368
+ return { ok: false, reason: "weight", provider: "fontsource", available: meta.weights };
21369
+ }
21370
+ return { ok: false, reason: "unknown-family" };
21371
+ }
21372
+ function rebaseFontFaceSrc(block, fromDir, toDir = "src/styles") {
21373
+ return block.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi, (whole, _quote, href) => {
21374
+ if (!isRepoFontHref(href)) return whole;
21375
+ const target = posix.normalize(posix.join(fromDir, href));
21376
+ return `url('${posix.relative(toDir, target)}')`;
21377
+ });
21378
+ }
21379
+ function declaredFontFaces(source) {
21380
+ const out = [];
21381
+ for (const rule of stripCssComments(source).matchAll(/@font-face\s*\{[^}]*\}/gi)) {
21382
+ const family = rule[0].match(/font-family\s*:\s*([^;]+)/i)?.[1];
21383
+ if (!family || rule.index === void 0) continue;
21384
+ out.push({ family: normalizeFamily(family), block: source.slice(rule.index, rule.index + rule[0].length) });
21385
+ }
21386
+ return out;
21387
+ }
21388
+ function isRepoFontHref(href) {
21389
+ return !/^(?:data:|https?:|\/)/i.test(href);
21390
+ }
21391
+ function missingFontFiles(urls, available) {
21392
+ return urls.filter(isRepoFontHref).map((url) => posix.basename(url.split("?")[0] ?? "")).filter((file) => !available.has(file));
21393
+ }
21394
+ function planFontAdoption(sources, families) {
21395
+ const wanted = new Map(families.map((family) => [normalizeFamily(family), family]));
21396
+ const byFamily = /* @__PURE__ */ new Map();
21397
+ for (const { path: path39, source } of sources) {
21398
+ const dir = posix.dirname(path39);
21399
+ for (const face of declaredFontFaces(source)) {
21400
+ if (!wanted.has(face.family)) continue;
21401
+ const perFile = byFamily.get(face.family) ?? /* @__PURE__ */ new Map();
21402
+ perFile.set(path39, [...perFile.get(path39) ?? [], rebaseFontFaceSrc(face.block, dir)]);
21403
+ byFamily.set(face.family, perFile);
21404
+ }
21405
+ }
21406
+ const plans = [];
21407
+ const conflicts = [];
21408
+ for (const [family, perFile] of byFamily) {
21409
+ const display = wanted.get(family) ?? family;
21410
+ const files = [...perFile.keys()];
21411
+ const shapes = new Set([...perFile.values()].map((blocks2) => blocks2.join("\n")));
21412
+ if (shapes.size > 1) {
21413
+ conflicts.push({ family: display, files });
21414
+ continue;
21415
+ }
21416
+ const blocks = perFile.get(files[0] ?? "") ?? [];
21417
+ const urls = blocks.flatMap(
21418
+ (block) => [...block.matchAll(/url\(\s*['"]?([^'")]+)['"]?\s*\)/gi)].map((m) => m[1] ?? "")
21419
+ );
21420
+ plans.push({ family: display, blocks, files, urls });
21421
+ }
21422
+ return { plans, conflicts };
21423
+ }
21424
+ async function fontsourceCatalogue(deps) {
21425
+ const body = await deps.fetchJson(`${FONTSOURCE_API}/fonts`).catch(() => null);
21426
+ if (!Array.isArray(body)) return [];
21427
+ return body.map((entry) => typeof entry === "object" && entry !== null ? entry.family : null).filter((family) => typeof family === "string");
21428
+ }
21058
21429
  function parseWeightAxis(spec) {
21059
21430
  const axis = spec.match(/wght@([^&]+)/i)?.[1] ?? "";
21060
21431
  const weights = /* @__PURE__ */ new Set();
@@ -21087,18 +21458,28 @@ function resolveFetchWeights(flag, declared) {
21087
21458
  }
21088
21459
  return declared?.length ? declared : [400];
21089
21460
  }
21090
- function googleFontCssUrl(family, weights) {
21461
+ var DEFAULT_SUBSETS = ["latin"];
21462
+ function resolveFetchSubsets(flag, selfHosted) {
21463
+ const asked = String(flag ?? "").split(",").map((subset) => subset.trim()).filter(Boolean);
21464
+ const named = flag ? [...new Set(asked)] : selfHosted;
21465
+ return named.length > 0 ? named : [...DEFAULT_SUBSETS];
21466
+ }
21467
+ function resolveFetchStyles(italic, selfHosted) {
21468
+ return italic || selfHosted.includes("italic") ? ["normal", "italic"] : ["normal"];
21469
+ }
21470
+ function googleFontCssUrl(family, weights, styles = ["normal"]) {
21091
21471
  const name = family.trim().split(/\s+/).map(encodeURIComponent).join("+");
21092
- const axis = [...new Set(weights)].sort((a, b) => a - b).join(";");
21093
- const spec = axis ? `${name}:wght@${axis}` : name;
21472
+ const sorted = [...new Set(weights)].sort((a, b) => a - b);
21473
+ if (sorted.length === 0) return `${GOOGLE_CSS2}?family=${name}&display=swap`;
21474
+ const wanted = FONT_STYLES.filter((style) => styles.includes(style));
21475
+ const spec = wanted.length > 1 ? `${name}:ital,wght@${wanted.flatMap((style) => sorted.map((w) => `${ITAL_AXIS[style]},${w}`)).join(";")}` : `${name}:wght@${sorted.join(";")}`;
21094
21476
  return `${GOOGLE_CSS2}?family=${spec}&display=swap`;
21095
21477
  }
21096
21478
  var STANDARD_WEIGHTS = [100, 200, 300, 400, 500, 600, 700, 800, 900];
21097
21479
  async function probeServedWeights(family, fetchCss) {
21098
- const probes = await Promise.all(
21099
- STANDARD_WEIGHTS.map(async (weight) => await fetchCss(googleFontCssUrl(family, [weight])) ? weight : null)
21100
- );
21101
- return probes.filter((w) => w !== null);
21480
+ const css = await fetchCss(googleFontCssUrl(family, STANDARD_WEIGHTS));
21481
+ if (css === null) return [];
21482
+ return [...new Set(parseGoogleFontFaces(css).map((face) => face.weight))].sort((a, b) => a - b);
21102
21483
  }
21103
21484
  async function checkFontRequests(requests, fetchCss) {
21104
21485
  const results = [];
@@ -21128,30 +21509,181 @@ async function checkFontRequests(requests, fetchCss) {
21128
21509
  }
21129
21510
  return results;
21130
21511
  }
21512
+ var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
21513
+ "sans-serif",
21514
+ "serif",
21515
+ "monospace",
21516
+ "cursive",
21517
+ "fantasy",
21518
+ "system-ui",
21519
+ "ui-sans-serif",
21520
+ "ui-serif",
21521
+ "ui-monospace",
21522
+ "ui-rounded",
21523
+ "arial",
21524
+ "helvetica",
21525
+ "georgia",
21526
+ "times",
21527
+ "courier",
21528
+ "inherit",
21529
+ "initial",
21530
+ "unset"
21531
+ ]);
21532
+ function stripCssComments(css) {
21533
+ return css.replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, " "));
21534
+ }
21535
+ function normalizeFamily(raw) {
21536
+ return raw.trim().replace(/^['"]|['"]$/g, "").trim().toLowerCase();
21537
+ }
21538
+ function fontFaceFamilies(css) {
21539
+ const out = /* @__PURE__ */ new Set();
21540
+ for (const rule of stripCssComments(css).matchAll(/@font-face\s*\{([^}]*)\}/gi)) {
21541
+ const family = (rule[1] ?? "").match(/font-family\s*:\s*([^;]+)/i)?.[1];
21542
+ if (family) out.add(normalizeFamily(family));
21543
+ }
21544
+ return out;
21545
+ }
21546
+ function declaredWeights(body) {
21547
+ const spec = (body.match(/font-weight\s*:\s*([^;}]+)/i)?.[1] ?? "").match(/\d{3}/g) ?? [];
21548
+ if (spec.length !== 2) return spec.map(Number);
21549
+ const range = [];
21550
+ for (let w = Number(spec[0]); w <= Number(spec[1]); w += 100) range.push(w);
21551
+ return range;
21552
+ }
21553
+ function selfHostedWeights(css, family) {
21554
+ const target = normalizeFamily(family);
21555
+ const weights = /* @__PURE__ */ new Set();
21556
+ for (const rule of stripCssComments(css).matchAll(/@font-face\s*\{([^}]*)\}/gi)) {
21557
+ const body = rule[1] ?? "";
21558
+ const declared = body.match(/font-family\s*:\s*([^;]+)/i)?.[1];
21559
+ if (!declared || normalizeFamily(declared) !== target) continue;
21560
+ for (const weight of declaredWeights(body)) weights.add(weight);
21561
+ }
21562
+ return [...weights].sort((a, b) => a - b);
21563
+ }
21564
+ function fontFileSubset(href) {
21565
+ const file = posix.basename((href.split("?")[0] ?? "").split("#")[0] ?? "");
21566
+ return /^[A-Za-z0-9]+-[1-9]00(?:italic)?-([\w-]+)\.woff2$/.exec(file)?.[1] ?? null;
21567
+ }
21568
+ function selfHostedSubsets(css, family) {
21569
+ const target = normalizeFamily(family);
21570
+ const subsets = /* @__PURE__ */ new Set();
21571
+ for (const rule of stripCssComments(css).matchAll(/@font-face\s*\{([^}]*)\}/gi)) {
21572
+ const body = rule[1] ?? "";
21573
+ const declared = body.match(/font-family\s*:\s*([^;]+)/i)?.[1];
21574
+ if (!declared || normalizeFamily(declared) !== target) continue;
21575
+ for (const url of body.matchAll(/url\(\s*['"]?([^'")]+)['"]?\s*\)/gi)) {
21576
+ const subset = fontFileSubset(url[1] ?? "");
21577
+ if (subset) subsets.add(subset);
21578
+ }
21579
+ }
21580
+ return [...subsets].sort();
21581
+ }
21582
+ function selfHostedStyles(css, family) {
21583
+ const target = normalizeFamily(family);
21584
+ const styles = /* @__PURE__ */ new Set();
21585
+ for (const rule of stripCssComments(css).matchAll(/@font-face\s*\{([^}]*)\}/gi)) {
21586
+ const body = rule[1] ?? "";
21587
+ const declared = body.match(/font-family\s*:\s*([^;]+)/i)?.[1];
21588
+ if (declared && normalizeFamily(declared) === target) styles.add(faceStyle(body));
21589
+ }
21590
+ return [...styles].sort();
21591
+ }
21592
+ function fontsourceFamilies(css) {
21593
+ const out = /* @__PURE__ */ new Set();
21594
+ for (const imported of stripCssComments(css).matchAll(/@fontsource(-variable)?\/([\w-]+)/gi)) {
21595
+ const slug = imported[2];
21596
+ if (!slug) continue;
21597
+ out.add(`${slug.replaceAll("-", " ")}${imported[1] ? " variable" : ""}`.toLowerCase());
21598
+ }
21599
+ return out;
21600
+ }
21601
+ function asAuthored(css, normalized) {
21602
+ const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
21603
+ const text2 = stripCssComments(css);
21604
+ return text2.match(new RegExp(`["']${escaped}["']`, "i"))?.[0]?.slice(1, -1) ?? text2.match(new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i"))?.[0] ?? normalized;
21605
+ }
21606
+ function unresolvedBrandFamilies(css) {
21607
+ const declared = [...parseBrandTokens(css, "").fonts].filter((family) => !GENERIC_FAMILIES.has(family));
21608
+ const loaded = fontFaceFamilies(css);
21609
+ for (const request of googleFontRequests(stripCssComments(css))) loaded.add(normalizeFamily(request.family));
21610
+ return declared.filter((family) => !loaded.has(family)).map((family) => asAuthored(css, family));
21611
+ }
21612
+ function stripFontFaceBlocks(source, family, styles) {
21613
+ const target = normalizeFamily(family);
21614
+ const ranges = [];
21615
+ for (const rule of stripCssComments(source).matchAll(/@font-face\s*\{([^}]*)\}/gi)) {
21616
+ const body = rule[1] ?? "";
21617
+ const declared = body.match(/font-family\s*:\s*([^;]+)/i)?.[1];
21618
+ if (!declared || normalizeFamily(declared) !== target || rule.index === void 0) continue;
21619
+ if (styles && !styles.has(faceStyle(body))) continue;
21620
+ ranges.push([rule.index, rule.index + rule[0].length]);
21621
+ }
21622
+ let out = source;
21623
+ for (const [start, end] of ranges.reverse()) out = `${out.slice(0, start)}${out.slice(end)}`;
21624
+ return out.replace(/\n{3,}/g, "\n\n");
21625
+ }
21626
+ function faceStyle(body) {
21627
+ return body.match(/font-style\s*:\s*([\w-]+)/i)?.[1]?.trim().toLowerCase() ?? "normal";
21628
+ }
21629
+ function declaredStyles(block) {
21630
+ const styles = /* @__PURE__ */ new Set();
21631
+ for (const rule of stripCssComments(block).matchAll(/@font-face\s*\{([^}]*)\}/gi))
21632
+ styles.add(faceStyle(rule[1] ?? ""));
21633
+ return styles;
21634
+ }
21635
+ function upsertFontFaceCss(css, family, block) {
21636
+ const styles = declaredStyles(block);
21637
+ const out = stripFontFaceBlocks(css, family, styles.size > 0 ? styles : void 0);
21638
+ const themeAt = stripCssComments(out).search(/@theme\b/);
21639
+ if (themeAt < 0) return `${out.trimEnd()}
21640
+
21641
+ ${block}
21642
+ `;
21643
+ const head = out.slice(0, themeAt).trimEnd();
21644
+ const tail = out.slice(themeAt);
21645
+ return head ? `${head}
21646
+
21647
+ ${block}
21648
+
21649
+ ${tail}` : `${block}
21650
+
21651
+ ${tail}`;
21652
+ }
21131
21653
 
21132
21654
  // src/commands/brand/fonts.ts
21133
- var GLOBAL_CSS = path.join("src", "styles", "global.css");
21134
- var FONTS_DIR = path.join("src", "brand", "fonts");
21135
- var DEFAULT_SUBSETS = "latin";
21655
+ var GLOBAL_CSS = path2.join("src", "styles", "global.css");
21656
+ var FONTS_DIR = path2.join("src", "brand", "fonts");
21657
+ var DEFAULT_SUBSETS_LABEL = DEFAULT_SUBSETS.join(",");
21136
21658
  var BROWSER_UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36";
21137
21659
  registerSchema({
21138
21660
  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.",
21661
+ 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.",
21662
+ args: {}
21663
+ });
21664
+ registerSchema({
21665
+ command: "brand.fonts.adopt",
21666
+ 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
21667
  args: {}
21141
21668
  });
21142
21669
  registerSchema({
21143
21670
  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.",
21671
+ 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
21672
  args: {
21146
21673
  family: { type: "string", description: 'Font family, e.g. "DM Sans"', required: true },
21147
21674
  weights: {
21148
21675
  type: "string",
21149
- description: "Comma-separated weights (default: what global.css requests, else 400)",
21676
+ description: "Comma-separated weights (default: the weights global.css already requests or self-hosts, else 400)",
21150
21677
  required: false
21151
21678
  },
21152
21679
  subsets: {
21153
21680
  type: "string",
21154
- description: `Comma-separated unicode subsets (default: ${DEFAULT_SUBSETS})`,
21681
+ description: `Comma-separated unicode subsets (default: the subsets global.css already self-hosts, else ${DEFAULT_SUBSETS_LABEL})`,
21682
+ required: false
21683
+ },
21684
+ italic: {
21685
+ type: "boolean",
21686
+ description: "Also self-host the family's italic faces. On automatically when global.css already loads one, so a re-fetch never strands it.",
21155
21687
  required: false
21156
21688
  }
21157
21689
  }
@@ -21161,39 +21693,188 @@ function fail(code, message, fix) {
21161
21693
  process.exit(2);
21162
21694
  }
21163
21695
  var REQUEST_TIMEOUT_MS = 15e3;
21164
- async function fetchGoogleCss(url) {
21696
+ async function fetchJson(url) {
21697
+ const res = await fetch(url, {
21698
+ headers: { "User-Agent": BROWSER_UA },
21699
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
21700
+ });
21701
+ if (res.status === 404) return null;
21702
+ if (!res.ok) throw new Error(`Fontsource answered ${res.status}`);
21703
+ return await res.json();
21704
+ }
21705
+ async function lookupGoogleCss(url) {
21165
21706
  try {
21166
21707
  const res = await fetch(url, {
21167
21708
  headers: { "User-Agent": BROWSER_UA },
21168
21709
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
21169
21710
  });
21170
- if (!res.ok) return null;
21171
- return await res.text();
21711
+ if (res.ok) return { status: "served", css: await res.text() };
21712
+ return { status: res.status === 400 ? "absent" : "unreachable" };
21172
21713
  } catch {
21173
- return null;
21714
+ return { status: "unreachable" };
21715
+ }
21716
+ }
21717
+ var fetchGoogleCss = async (url) => {
21718
+ const got = await lookupGoogleCss(url);
21719
+ return got.status === "served" ? got.css : null;
21720
+ };
21721
+ var WIRING_HINT = {
21722
+ 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.`,
21723
+ // Told apart from "no stylesheet" on purpose: they used to share one hint, so
21724
+ // an idempotent re-run instructed the agent to paste a block that was already
21725
+ // in the file, and doing as it was told duplicated the face.
21726
+ "already-wired": (family) => `${GLOBAL_CSS} already loads exactly these faces for "${family}" \u2014 nothing to change.`,
21727
+ "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.`
21728
+ };
21729
+ var PROVIDER_DEPS = { lookupGoogleCss, fetchJson };
21730
+ var PROVIDER_LABEL = { google: "Google Fonts", fontsource: "Fontsource" };
21731
+ function italicHints(family, wantedStyles, availableStyles, downloadedStyles, provider) {
21732
+ const wanted = wantedStyles.includes("italic");
21733
+ const available = availableStyles.includes("italic");
21734
+ if (wanted && !downloadedStyles.includes("italic")) {
21735
+ return [
21736
+ `NO ITALIC: ${PROVIDER_LABEL[provider]} does not ship an italic for "${family}", so none was written. Drop italic from src/brand/BRAND.md, or the browser will slant the upright face instead.`
21737
+ ];
21738
+ }
21739
+ if (!wanted && available) {
21740
+ return [
21741
+ `${PROVIDER_LABEL[provider]} also ships an italic for "${family}", which was NOT downloaded. If the brand uses italic anywhere, re-run with \`--italic\` \u2014 otherwise the browser slants the upright face, which is not the same shapes.`
21742
+ ];
21743
+ }
21744
+ return [];
21745
+ }
21746
+ function fetchHints({
21747
+ family,
21748
+ downloadedWeights,
21749
+ downloadedStyles,
21750
+ incomplete,
21751
+ wiring,
21752
+ provider,
21753
+ wantedStyles,
21754
+ availableStyles
21755
+ }) {
21756
+ return [
21757
+ WIRING_HINT[wiring](family),
21758
+ ...italicHints(family, wantedStyles, availableStyles, downloadedStyles, provider),
21759
+ `Record the exact weights (${downloadedWeights.join(", ")}) in src/brand/BRAND.md \u2014 a weight listed there but not downloaded renders as a synthesized fallback.`,
21760
+ ...incomplete.length > 0 ? [
21761
+ // The remedy is provider-specific: only a Google family has an @import
21762
+ // to fall back on, and telling an agent to keep one for a Fontsource
21763
+ // family sends it to write an import Google will 400.
21764
+ 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.`
21765
+ ] : [],
21766
+ `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.`
21767
+ ];
21768
+ }
21769
+ async function wireFontFaceIntoStylesheet(css, family, fontFace) {
21770
+ if (!css) return "no-stylesheet";
21771
+ const wired = upsertFontFaceCss(css, family, fontFace);
21772
+ if (wired === css) return "already-wired";
21773
+ await writeFile(path2.resolve(process.cwd(), GLOBAL_CSS), wired);
21774
+ return "written";
21775
+ }
21776
+ function failResolution(family, weights, outcome) {
21777
+ if (outcome.reason === "unreachable") {
21778
+ return fail(
21779
+ "CATALOGUE_UNREACHABLE",
21780
+ `Could not reach ${PROVIDER_LABEL[outcome.provider]} to ask about "${family}".`,
21781
+ {
21782
+ action: "Run the same command again.",
21783
+ 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.`,
21784
+ provider: outcome.provider
21785
+ }
21786
+ );
21787
+ }
21788
+ if (outcome.reason === "ambiguous-subsets") {
21789
+ return fail(
21790
+ "SUBSET_AMBIGUOUS",
21791
+ `${PROVIDER_LABEL[outcome.provider]} serves "${family}" in several subsets but reports no unicode-range to tell them apart.`,
21792
+ {
21793
+ action: `Retry with a single --subsets value, e.g. --subsets ${outcome.available[0] ?? "latin"}.`,
21794
+ 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.",
21795
+ available: outcome.available
21796
+ }
21797
+ );
21798
+ }
21799
+ if (outcome.reason === "subset") {
21800
+ return fail("SUBSET_NOT_SERVED", `${PROVIDER_LABEL[outcome.provider]} has "${family}", but not in that subset.`, {
21801
+ action: `Retry with --subsets set to one it serves: ${outcome.available.join(",")}.`,
21802
+ 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.",
21803
+ available: outcome.available
21804
+ });
21805
+ }
21806
+ if (outcome.reason === "weight") {
21807
+ return fail(
21808
+ "FONT_NOT_SERVED",
21809
+ `${PROVIDER_LABEL[outcome.provider]} has "${family}", but not at weight ${weights.join("/")}.`,
21810
+ {
21811
+ action: `Retry with --weights set to one of these: ${outcome.available.join(",")}.`,
21812
+ explanation: "The family exists; the weight you asked for is not one it ships.",
21813
+ served: outcome.available
21814
+ }
21815
+ );
21816
+ }
21817
+ return failUnknownFamily(family);
21818
+ }
21819
+ async function failUnknownFamily(family) {
21820
+ const didYouMean = suggestFamilies(family, await fontsourceCatalogue(PROVIDER_DEPS));
21821
+ return fail("FONT_NOT_SERVED", `No font catalogue carries a family called "${family}".`, {
21822
+ 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.`,
21823
+ explanation: `Tried ${FONT_PROVIDERS.join(" then ")}. Family names are case- and space-sensitive in every catalogue.`,
21824
+ tried: [...FONT_PROVIDERS],
21825
+ ...didYouMean.length ? { didYouMean } : {}
21826
+ });
21827
+ }
21828
+ async function listFontFiles() {
21829
+ try {
21830
+ const entries = await readdir(path2.resolve(process.cwd(), FONTS_DIR), { withFileTypes: true });
21831
+ return entries.filter((e) => e.isFile()).map((e) => e.name);
21832
+ } catch {
21833
+ return [];
21174
21834
  }
21175
21835
  }
21176
21836
  async function readGlobalCss() {
21177
21837
  try {
21178
- return await readFile(path.resolve(process.cwd(), GLOBAL_CSS), "utf8");
21838
+ return await readFile2(path2.resolve(process.cwd(), GLOBAL_CSS), "utf8");
21179
21839
  } catch {
21180
21840
  return "";
21181
21841
  }
21182
21842
  }
21843
+ async function unresolvedAction(css, unresolved) {
21844
+ const { plans } = planFontAdoption(await astroSources(), unresolved);
21845
+ if (plans.length > 0) {
21846
+ const names = plans.map((p) => `"${p.family}"`).join(", ");
21847
+ 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.`;
21848
+ }
21849
+ const viaFontsource = [...fontsourceFamilies(css)];
21850
+ const stranded = unresolved.filter((family) => viaFontsource.includes(family.toLowerCase()));
21851
+ if (stranded.length > 0) {
21852
+ 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.`;
21853
+ }
21854
+ 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}.`;
21855
+ }
21183
21856
  var fontsCheckCommand = defineCommand93({
21184
21857
  meta: {
21185
21858
  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."
21859
+ 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
21860
  },
21188
21861
  async run() {
21189
21862
  const css = await readGlobalCss();
21190
21863
  const requests = googleFontRequests(css);
21864
+ const unresolved = unresolvedBrandFamilies(css);
21865
+ if (unresolved.length > 0) {
21866
+ fail("FONT_NOT_LOADED", `Nothing loads ${unresolved.map((f) => `"${f}"`).join(", ")} in ${GLOBAL_CSS}.`, {
21867
+ action: await unresolvedAction(css, unresolved),
21868
+ 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.",
21869
+ unresolved
21870
+ });
21871
+ }
21191
21872
  if (requests.length === 0) {
21192
21873
  writeJson({
21193
21874
  ok: true,
21194
21875
  data: { families: [], selfHosted: true },
21195
21876
  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.`
21877
+ `No Google Fonts requested in ${GLOBAL_CSS}, and every brand face is provided by an @font-face \u2014 nothing to verify against Google.`
21197
21878
  ]
21198
21879
  });
21199
21880
  return;
@@ -21227,50 +21908,39 @@ var fontsCheckCommand = defineCommand93({
21227
21908
  var fontsFetchCommand = defineCommand93({
21228
21909
  meta: {
21229
21910
  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."
21911
+ 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
21912
  },
21232
21913
  args: {
21233
21914
  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)" },
21235
- subsets: { type: "string", description: `Comma-separated unicode subsets (default: ${DEFAULT_SUBSETS})` }
21915
+ weights: {
21916
+ type: "string",
21917
+ description: "Comma-separated weights (default: the weights global.css already requests or self-hosts, else 400)"
21918
+ },
21919
+ subsets: {
21920
+ type: "string",
21921
+ description: `Comma-separated unicode subsets (default: the subsets global.css already self-hosts, else ${DEFAULT_SUBSETS_LABEL})`
21922
+ },
21923
+ italic: {
21924
+ type: "boolean",
21925
+ description: "Also self-host the family's italic faces (default: on when global.css already loads one, off otherwise)"
21926
+ }
21236
21927
  },
21237
21928
  async run({ args }) {
21238
21929
  const family = String(args.family).trim();
21239
21930
  if (!family) fail("INVALID_FAMILY", 'Pass a font family, e.g. `baker brand fonts fetch "DM Sans"`.');
21240
21931
  const css = await readGlobalCss();
21241
21932
  const declared = googleFontRequests(css).find((r) => r.family.toLowerCase() === family.toLowerCase());
21242
- const weights = resolveFetchWeights(args.weights, declared?.weights);
21933
+ const provided = declared?.weights.length ? declared.weights : selfHostedWeights(css, family);
21934
+ const weights = resolveFetchWeights(args.weights, provided);
21243
21935
  if (weights.length === 0) {
21244
21936
  fail("INVALID_WEIGHTS", `"${args.weights}" has no usable weight \u2014 pass whole hundreds, e.g. --weights 400,700.`);
21245
21937
  }
21246
- const wanted = new Set(
21247
- String(args.subsets ?? DEFAULT_SUBSETS).split(",").map((s) => s.trim()).filter(Boolean)
21248
- );
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);
21938
+ const wanted = new Set(resolveFetchSubsets(args.subsets, selfHostedSubsets(css, family)));
21939
+ const styles = resolveFetchStyles(args.italic, selfHostedStyles(css, family));
21940
+ const resolved = await resolveBrandFaces(family, weights, [...wanted], PROVIDER_DEPS, styles);
21941
+ if (!resolved.ok) return failResolution(family, weights, resolved);
21942
+ const faces = resolved.faces;
21943
+ const fontsDir = path2.resolve(process.cwd(), FONTS_DIR);
21274
21944
  await mkdir(fontsDir, { recursive: true });
21275
21945
  const downloaded = [];
21276
21946
  for (const face of faces) {
@@ -21279,7 +21949,7 @@ var fontsFetchCommand = defineCommand93({
21279
21949
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
21280
21950
  });
21281
21951
  if (!res.ok) continue;
21282
- await writeFile(path.join(fontsDir, fontFileName(face)), Buffer.from(await res.arrayBuffer()));
21952
+ await writeFile(path2.join(fontsDir, fontFileName(face)), Buffer.from(await res.arrayBuffer()));
21283
21953
  downloaded.push(face);
21284
21954
  }
21285
21955
  const written = downloaded.map(fontFileName);
@@ -21291,17 +21961,127 @@ var fontsFetchCommand = defineCommand93({
21291
21961
  }
21292
21962
  const fontFace = renderFontFaceCss(downloaded);
21293
21963
  const downloadedWeights = [...new Set(downloaded.map((f) => f.weight))].sort((a, b) => a - b);
21964
+ const downloadedStyles = [...new Set(downloaded.map((f) => f.style))].sort();
21294
21965
  const incomplete = weights.filter((w) => !downloadedWeights.includes(w));
21966
+ const wiring = await wireFontFaceIntoStylesheet(css, family, fontFace);
21967
+ writeJson({
21968
+ ok: true,
21969
+ data: {
21970
+ family,
21971
+ provider: resolved.provider,
21972
+ weights: downloadedWeights,
21973
+ styles: downloadedStyles,
21974
+ files: written,
21975
+ fontFace,
21976
+ stylesheet: wiring
21977
+ },
21978
+ hints: fetchHints({
21979
+ family,
21980
+ downloadedWeights,
21981
+ downloadedStyles,
21982
+ incomplete,
21983
+ wiring,
21984
+ provider: resolved.provider,
21985
+ wantedStyles: styles,
21986
+ availableStyles: resolved.availableStyles
21987
+ })
21988
+ });
21989
+ }
21990
+ });
21991
+ async function astroSources() {
21992
+ const out = [];
21993
+ let entries;
21994
+ try {
21995
+ entries = await readdir("src", { recursive: true, withFileTypes: true });
21996
+ } catch {
21997
+ return out;
21998
+ }
21999
+ for (const entry of entries) {
22000
+ if (!entry.isFile() || !entry.name.endsWith(".astro")) continue;
22001
+ const file = path2.posix.join(entry.parentPath ?? "src", entry.name);
22002
+ try {
22003
+ out.push({ path: file, source: await readFile2(file, "utf8") });
22004
+ } catch {
22005
+ }
22006
+ }
22007
+ return out;
22008
+ }
22009
+ var fontsAdoptCommand = defineCommand93({
22010
+ meta: {
22011
+ name: "adopt",
22012
+ 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."
22013
+ },
22014
+ async run() {
22015
+ const css = await readGlobalCss();
22016
+ if (!css) fail("NO_STYLESHEET", `There is no ${GLOBAL_CSS} to adopt faces into.`);
22017
+ const unresolved = unresolvedBrandFamilies(css);
22018
+ if (unresolved.length === 0) {
22019
+ writeJson({
22020
+ ok: true,
22021
+ data: { adopted: [], conflicts: [] },
22022
+ hints: [`Every --font-* family in ${GLOBAL_CSS} is already loaded by it \u2014 nothing to adopt.`]
22023
+ });
22024
+ return;
22025
+ }
22026
+ const { plans, conflicts } = planFontAdoption(await astroSources(), unresolved);
22027
+ const available = new Set(await listFontFiles());
22028
+ const adopted = [];
22029
+ const skipped = [];
22030
+ let next = css;
22031
+ for (const plan of plans) {
22032
+ const missing = missingFontFiles(plan.urls, available);
22033
+ if (missing.length > 0) {
22034
+ skipped.push({ family: plan.family, reason: `${missing.join(", ")} is not in ${FONTS_DIR}` });
22035
+ continue;
22036
+ }
22037
+ next = upsertFontFaceCss(next, plan.family, plan.blocks.join("\n"));
22038
+ adopted.push({
22039
+ family: plan.family,
22040
+ files: plan.files,
22041
+ weights: plan.blocks.map((b) => b.match(/font-weight\s*:\s*([^;]+)/i)?.[1]?.trim() ?? "400")
22042
+ });
22043
+ }
22044
+ if (adopted.length === 0) {
22045
+ if (conflicts.length > 0) {
22046
+ fail(
22047
+ "ADOPTION_CONFLICT",
22048
+ `${conflicts.map((c) => `"${c.family}" is declared differently in ${c.files.join(" and ")}`).join("; ")}.`,
22049
+ {
22050
+ action: "Make the declarations match \u2014 same weights, same files \u2014 then re-run. Pick the one whose page renders correctly.",
22051
+ explanation: "One global scope holds one mapping per family, so adopting would silently repaint whichever page lost.",
22052
+ conflicts
22053
+ }
22054
+ );
22055
+ }
22056
+ if (skipped.length > 0) {
22057
+ fail("FONT_FILE_MISSING", `${skipped.map((s) => `${s.family} \u2014 ${s.reason}`).join("; ")}.`, {
22058
+ action: `Add the missing file to ${FONTS_DIR}, or fix the src: url() in the page that declares it, then re-run.`,
22059
+ 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.",
22060
+ skipped
22061
+ });
22062
+ }
22063
+ fail("NOTHING_TO_ADOPT", `No page declares a face for ${unresolved.map((f) => `"${f}"`).join(", ")}.`, {
22064
+ action: `Self-host it instead: \`baker brand fonts fetch "${unresolved[0]}"\`.`,
22065
+ explanation: "Adopting only moves faces the repo already ships; it never downloads one."
22066
+ });
22067
+ }
22068
+ await writeFile(path2.resolve(process.cwd(), GLOBAL_CSS), next);
22069
+ for (const plan of plans.filter((p) => adopted.some((a) => a.family === p.family))) {
22070
+ for (const file of plan.files) {
22071
+ const source = await readFile2(file, "utf8");
22072
+ await writeFile(file, stripFontFaceBlocks(source, plan.family));
22073
+ }
22074
+ }
21295
22075
  writeJson({
21296
22076
  ok: true,
21297
- data: { family, weights: downloadedWeights, files: written, fontFace },
22077
+ data: { adopted, conflicts, skipped },
21298
22078
  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(",")}\`.`
22079
+ `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.`,
22080
+ ...conflicts.length > 0 ? [
22081
+ `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
22082
  ] : [],
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."
22083
+ ...skipped.length > 0 ? [`SKIPPED: ${skipped.map((s) => `${s.family} \u2014 ${s.reason}`).join("; ")}.`] : [],
22084
+ `Run \`baker brand fonts check\` to confirm nothing is left unresolved.`
21305
22085
  ]
21306
22086
  });
21307
22087
  }
@@ -21311,15 +22091,17 @@ var fontsCommand = defineCommand93({
21311
22091
  name: "fonts",
21312
22092
  description: `Verify and self-host the brand's typefaces.
21313
22093
 
21314
- Start here: \`baker brand fonts check\` \u2014 confirms the fonts the brand claims are really served.
22094
+ Start here: \`baker brand fonts check\` \u2014 confirms the fonts the brand claims really load.
21315
22095
 
21316
22096
  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`
22097
+ 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
22098
+ baker brand fonts fetch <family> \u2014 download a family into src/brand/fonts/ and wire its @font-face into src/styles/global.css
22099
+ 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
22100
  },
21320
22101
  subCommands: {
21321
22102
  check: fontsCheckCommand,
21322
- fetch: fontsFetchCommand
22103
+ fetch: fontsFetchCommand,
22104
+ adopt: fontsAdoptCommand
21323
22105
  }
21324
22106
  });
21325
22107
 
@@ -21357,8 +22139,8 @@ var catalogCommand = defineCommand95({
21357
22139
  });
21358
22140
 
21359
22141
  // src/commands/canvas/critique.ts
21360
- import { readFile as readFile2 } from "fs/promises";
21361
- import path2 from "path";
22142
+ import { readFile as readFile3 } from "fs/promises";
22143
+ import path3 from "path";
21362
22144
  import { defineCommand as defineCommand96 } from "citty";
21363
22145
 
21364
22146
  // src/engine/scaffold/lib/critique.ts
@@ -21486,8 +22268,8 @@ var critiqueCommand = defineCommand96({
21486
22268
  },
21487
22269
  args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
21488
22270
  async run({ args }) {
21489
- const filePath = path2.resolve(String(args.file));
21490
- const raw = await readFile2(filePath, "utf8");
22271
+ const filePath = path3.resolve(String(args.file));
22272
+ const raw = await readFile3(filePath, "utf8");
21491
22273
  let parsed;
21492
22274
  try {
21493
22275
  parsed = JSON.parse(raw);
@@ -21518,8 +22300,8 @@ var critiqueCommand = defineCommand96({
21518
22300
 
21519
22301
  // src/commands/canvas/inspect.ts
21520
22302
  import { execFile } from "child_process";
21521
- import { readdir, readFile as readFile3, stat } from "fs/promises";
21522
- import path3 from "path";
22303
+ import { readdir as readdir2, readFile as readFile4, stat } from "fs/promises";
22304
+ import path4 from "path";
21523
22305
  import { promisify } from "util";
21524
22306
  import { defineCommand as defineCommand97 } from "citty";
21525
22307
  var execFileAsync = promisify(execFile);
@@ -21537,7 +22319,7 @@ var inspectCommand = defineCommand97({
21537
22319
  }
21538
22320
  },
21539
22321
  async run({ args }) {
21540
- const outputsDir = path3.resolve(String(args["outputs-dir"] ?? "canvas"));
22322
+ const outputsDir = path4.resolve(String(args["outputs-dir"] ?? "canvas"));
21541
22323
  const runArg = String(args.run);
21542
22324
  const runDir = await resolveRunDir(runArg, outputsDir);
21543
22325
  const manifest = await loadManifest(runDir);
@@ -21549,7 +22331,7 @@ var inspectCommand = defineCommand97({
21549
22331
  }
21550
22332
  const summary = {
21551
22333
  ok: true,
21552
- run_id: manifest.run_id ?? path3.basename(runDir),
22334
+ run_id: manifest.run_id ?? path4.basename(runDir),
21553
22335
  run_dir: runDir,
21554
22336
  stats: manifest.stats ?? null,
21555
22337
  output: manifest.output ?? null,
@@ -21562,20 +22344,20 @@ var inspectCommand = defineCommand97({
21562
22344
  }
21563
22345
  });
21564
22346
  async function resolveRunDir(run, outputsDir) {
21565
- if (path3.isAbsolute(run)) {
22347
+ if (path4.isAbsolute(run)) {
21566
22348
  const s2 = await stat(run).catch(() => null);
21567
22349
  if (s2?.isDirectory()) return run;
21568
22350
  throw new Error(`inspect: ${run} is not a directory`);
21569
22351
  }
21570
- const candidate = path3.join(outputsDir, run);
22352
+ const candidate = path4.join(outputsDir, run);
21571
22353
  const s = await stat(candidate).catch(() => null);
21572
22354
  if (s?.isDirectory()) return candidate;
21573
22355
  throw new Error(`inspect: no run directory at ${candidate}`);
21574
22356
  }
21575
22357
  async function loadManifest(runDir) {
21576
- const manifestPath = path3.join(runDir, "manifest.json");
22358
+ const manifestPath = path4.join(runDir, "manifest.json");
21577
22359
  try {
21578
- const raw = await readFile3(manifestPath, "utf-8");
22360
+ const raw = await readFile4(manifestPath, "utf-8");
21579
22361
  return JSON.parse(raw);
21580
22362
  } catch {
21581
22363
  return {};
@@ -21583,9 +22365,9 @@ async function loadManifest(runDir) {
21583
22365
  }
21584
22366
  async function listRunFiles(runDir) {
21585
22367
  const out = [];
21586
- const names = await readdir(runDir);
22368
+ const names = await readdir2(runDir);
21587
22369
  for (const name of names) {
21588
- const abs = path3.join(runDir, name);
22370
+ const abs = path4.join(runDir, name);
21589
22371
  const s = await stat(abs).catch(() => null);
21590
22372
  if (!s?.isFile()) continue;
21591
22373
  out.push({ name, path: abs, size: s.size });
@@ -21630,58 +22412,58 @@ async function probeDuration(filePath) {
21630
22412
  }
21631
22413
 
21632
22414
  // src/commands/canvas/rerun.ts
21633
- import path15 from "path";
22415
+ import path16 from "path";
21634
22416
  import { defineCommand as defineCommand99 } from "citty";
21635
22417
 
21636
22418
  // src/commands/canvas/run.ts
21637
- import { readFile as readFile10 } from "fs/promises";
21638
- import path14 from "path";
22419
+ import { readFile as readFile11 } from "fs/promises";
22420
+ import path15 from "path";
21639
22421
  import { defineCommand as defineCommand98 } from "citty";
21640
22422
 
21641
22423
  // src/commands/canvas/normalize-paths.ts
21642
22424
  import { existsSync as existsSync3, realpathSync } from "fs";
21643
22425
  import { writeFile as writeFile2 } from "fs/promises";
21644
- import path4 from "path";
22426
+ import path5 from "path";
21645
22427
  function findWorkspaceRoot(startDir, exists = existsSync3, maxDepth = 12) {
21646
- let dir = path4.resolve(startDir);
22428
+ let dir = path5.resolve(startDir);
21647
22429
  for (let i = 0; i < maxDepth; i++) {
21648
- if (exists(path4.join(dir, "package.json"))) return dir;
21649
- const parent = path4.dirname(dir);
22430
+ if (exists(path5.join(dir, "package.json"))) return dir;
22431
+ const parent = path5.dirname(dir);
21650
22432
  if (parent === dir) break;
21651
22433
  dir = parent;
21652
22434
  }
21653
22435
  return null;
21654
22436
  }
21655
22437
  function canonicalize(target) {
21656
- const abs = path4.resolve(target);
22438
+ const abs = path5.resolve(target);
21657
22439
  let dir = abs;
21658
22440
  for (; ; ) {
21659
22441
  try {
21660
22442
  const real = realpathSync(dir);
21661
- return dir === abs ? real : path4.join(real, path4.relative(dir, abs));
22443
+ return dir === abs ? real : path5.join(real, path5.relative(dir, abs));
21662
22444
  } catch {
21663
- const parent = path4.dirname(dir);
22445
+ const parent = path5.dirname(dir);
21664
22446
  if (parent === dir) return abs;
21665
22447
  dir = parent;
21666
22448
  }
21667
22449
  }
21668
22450
  }
21669
22451
  function isInside(root, target) {
21670
- const rel = path4.relative(root, target);
21671
- return rel !== "" && !rel.startsWith("..") && !path4.isAbsolute(rel);
22452
+ const rel = path5.relative(root, target);
22453
+ return rel !== "" && !rel.startsWith("..") && !path5.isAbsolute(rel);
21672
22454
  }
21673
22455
  function toCanvasRelative(canvasDir, target) {
21674
- return path4.relative(canvasDir, target).split(path4.sep).join("/");
22456
+ return path5.relative(canvasDir, target).split(path5.sep).join("/");
21675
22457
  }
21676
22458
  var RUNTIME_WORKSPACE_ROOT = "/home/user/repo";
21677
22459
  function rewriteTarget(value, workspaceRoot) {
21678
22460
  if (typeof value !== "string" || value.length === 0) return null;
21679
- if (value.includes("[TODO") || looksLikeHttpUrl(value) || !path4.isAbsolute(value)) return null;
22461
+ if (value.includes("[TODO") || looksLikeHttpUrl(value) || !path5.isAbsolute(value)) return null;
21680
22462
  const target = canonicalize(value);
21681
22463
  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);
22464
+ const fromRuntime = path5.relative(RUNTIME_WORKSPACE_ROOT, path5.normalize(value));
22465
+ if (fromRuntime !== "" && !fromRuntime.startsWith("..") && !path5.isAbsolute(fromRuntime)) {
22466
+ return path5.join(workspaceRoot, fromRuntime);
21685
22467
  }
21686
22468
  return null;
21687
22469
  }
@@ -21709,7 +22491,7 @@ function normalizeAbsoluteCanvasPaths(canvas, canvasDir, workspaceRoot) {
21709
22491
  return rewrites.length === 0 ? { canvas, rewrites } : { canvas: { ...canvas, nodes }, rewrites };
21710
22492
  }
21711
22493
  async function healAbsoluteCanvasPaths(filePath, canvas) {
21712
- const canvasDir = path4.dirname(filePath);
22494
+ const canvasDir = path5.dirname(filePath);
21713
22495
  const workspaceRoot = findWorkspaceRoot(canvasDir);
21714
22496
  if (!workspaceRoot) return { canvas, rewrites: [], text: null };
21715
22497
  const normalized = normalizeAbsoluteCanvasPaths(canvas, canvasDir, workspaceRoot);
@@ -21740,7 +22522,7 @@ function unsuppliedPlaceholderAssets(canvas) {
21740
22522
  }
21741
22523
 
21742
22524
  // src/commands/canvas/resolve-paths.ts
21743
- import path5 from "path";
22525
+ import path6 from "path";
21744
22526
  function resolveRelativeCanvasPaths(canvas, baseDir) {
21745
22527
  if (!canvas || typeof canvas !== "object") return canvas;
21746
22528
  const c = canvas;
@@ -21753,24 +22535,24 @@ function resolveNode(node, baseDir) {
21753
22535
  const params = n.params;
21754
22536
  if (!params || typeof params !== "object") return node;
21755
22537
  if (n.type === "ingest" && params.source === "path" && isResolvableRelative(params.path)) {
21756
- return { ...node, params: { ...params, path: path5.resolve(baseDir, params.path) } };
22538
+ return { ...node, params: { ...params, path: path6.resolve(baseDir, params.path) } };
21757
22539
  }
21758
22540
  if (n.type === "hyperframe_render" && isResolvableRelative(params.composition)) {
21759
- return { ...node, params: { ...params, composition: path5.resolve(baseDir, params.composition) } };
22541
+ return { ...node, params: { ...params, composition: path6.resolve(baseDir, params.composition) } };
21760
22542
  }
21761
22543
  return node;
21762
22544
  }
21763
22545
  function isResolvableRelative(value) {
21764
- return typeof value === "string" && value.length > 0 && !value.includes("[TODO") && !looksLikeHttpUrl(value) && !path5.isAbsolute(value);
22546
+ return typeof value === "string" && value.length > 0 && !value.includes("[TODO") && !looksLikeHttpUrl(value) && !path6.isAbsolute(value);
21765
22547
  }
21766
22548
 
21767
22549
  // src/commands/canvas/source-version.ts
21768
- import { readFile as readFile5 } from "fs/promises";
21769
- import path7 from "path";
22550
+ import { readFile as readFile6 } from "fs/promises";
22551
+ import path8 from "path";
21770
22552
 
21771
22553
  // 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";
22554
+ import { mkdir as mkdir2, readFile as readFile5, readdir as readdir3, rm, writeFile as writeFile3 } from "fs/promises";
22555
+ import path7 from "path";
21774
22556
  var SCENES_DIR = "scenes";
21775
22557
  var GLOBAL_PROMPT_FILE = "prompt.json";
21776
22558
  var REBUILD_FILE = "prompt.rebuild.json";
@@ -21787,42 +22569,42 @@ function splitBlueprint(blueprint) {
21787
22569
  }
21788
22570
  async function writeSceneFiles(outDir, blueprint) {
21789
22571
  const { global, scenes } = splitBlueprint(blueprint);
21790
- await writeFile3(path6.join(outDir, GLOBAL_PROMPT_FILE), `${JSON.stringify(global, null, 2)}
22572
+ await writeFile3(path7.join(outDir, GLOBAL_PROMPT_FILE), `${JSON.stringify(global, null, 2)}
21791
22573
  `, "utf8");
21792
- const scenesDir = path6.join(outDir, SCENES_DIR);
22574
+ const scenesDir = path7.join(outDir, SCENES_DIR);
21793
22575
  await mkdir2(scenesDir, { recursive: true });
21794
22576
  const written = /* @__PURE__ */ new Set();
21795
22577
  for (let i = 0; i < scenes.length; i++) {
21796
22578
  const name = sceneFileName(i, scenes.length);
21797
22579
  written.add(name);
21798
- await writeFile3(path6.join(scenesDir, name), `${JSON.stringify(scenes[i], null, 2)}
22580
+ await writeFile3(path7.join(scenesDir, name), `${JSON.stringify(scenes[i], null, 2)}
21799
22581
  `, "utf8");
21800
22582
  }
21801
22583
  for (const name of await listSceneFileNames(scenesDir)) {
21802
- if (!written.has(name)) await rm(path6.join(scenesDir, name), { force: true });
22584
+ if (!written.has(name)) await rm(path7.join(scenesDir, name), { force: true });
21803
22585
  }
21804
22586
  }
21805
22587
  async function listSceneFileNames(scenesDir) {
21806
22588
  let entries;
21807
22589
  try {
21808
- entries = await readdir2(scenesDir);
22590
+ entries = await readdir3(scenesDir);
21809
22591
  } catch {
21810
22592
  return [];
21811
22593
  }
21812
22594
  return entries.filter((n) => /^s\d+\.json$/.test(n)).sort(bySceneIndex);
21813
22595
  }
21814
22596
  async function listSceneFiles(creativeDir) {
21815
- const scenesDir = path6.join(creativeDir, SCENES_DIR);
21816
- return (await listSceneFileNames(scenesDir)).map((n) => path6.join(scenesDir, n));
22597
+ const scenesDir = path7.join(creativeDir, SCENES_DIR);
22598
+ return (await listSceneFileNames(scenesDir)).map((n) => path7.join(scenesDir, n));
21817
22599
  }
21818
22600
  async function reassembleBlueprint(creativeDir) {
21819
22601
  const files = await listSceneFiles(creativeDir);
21820
22602
  if (files.length === 0) return null;
21821
- const globalRaw = await readFile4(path6.join(creativeDir, GLOBAL_PROMPT_FILE), "utf8");
22603
+ const globalRaw = await readFile5(path7.join(creativeDir, GLOBAL_PROMPT_FILE), "utf8");
21822
22604
  const global = JSON.parse(globalRaw);
21823
22605
  const scenes = [];
21824
22606
  for (const file of files) {
21825
- scenes.push(JSON.parse(await readFile4(file, "utf8")));
22607
+ scenes.push(JSON.parse(await readFile5(file, "utf8")));
21826
22608
  }
21827
22609
  return { ...global, scenes };
21828
22610
  }
@@ -21836,15 +22618,15 @@ function bySceneIndex(a, b) {
21836
22618
  async function computeSourceSha(canvasPath) {
21837
22619
  let canvasBytes;
21838
22620
  try {
21839
- canvasBytes = await readFile5(canvasPath);
22621
+ canvasBytes = await readFile6(canvasPath);
21840
22622
  } catch {
21841
22623
  return void 0;
21842
22624
  }
21843
- const canvasDir = path7.dirname(canvasPath);
21844
- const promptPath = path7.join(canvasDir, "prompt.json");
22625
+ const canvasDir = path8.dirname(canvasPath);
22626
+ const promptPath = path8.join(canvasDir, "prompt.json");
21845
22627
  let promptBytes;
21846
22628
  try {
21847
- promptBytes = await readFile5(promptPath);
22629
+ promptBytes = await readFile6(promptPath);
21848
22630
  } catch {
21849
22631
  promptBytes = Buffer.alloc(0);
21850
22632
  }
@@ -21857,7 +22639,7 @@ async function computeSourceSha(canvasPath) {
21857
22639
  for (const sceneFile of await listSceneFiles(canvasDir)) {
21858
22640
  let sceneBytes;
21859
22641
  try {
21860
- sceneBytes = await readFile5(sceneFile);
22642
+ sceneBytes = await readFile6(sceneFile);
21861
22643
  } catch {
21862
22644
  sceneBytes = Buffer.alloc(0);
21863
22645
  }
@@ -21867,8 +22649,8 @@ async function computeSourceSha(canvasPath) {
21867
22649
  }
21868
22650
 
21869
22651
  // src/commands/canvas/scene-projection.ts
21870
- import { readFile as readFile6 } from "fs/promises";
21871
- import path8 from "path";
22652
+ import { readFile as readFile7 } from "fs/promises";
22653
+ import path9 from "path";
21872
22654
 
21873
22655
  // src/engine/scaffold/video.ts
21874
22656
  import { toCardinal as nwAr } from "n2words/ar-SA";
@@ -25154,12 +25936,12 @@ function nonPromptParamsDiverge(live = {}, rebuilt = {}) {
25154
25936
  return false;
25155
25937
  }
25156
25938
  async function syncSceneNodeParams(canvas, canvasPath, log) {
25157
- const creativeDir = path8.dirname(canvasPath);
25939
+ const creativeDir = path9.dirname(canvasPath);
25158
25940
  const blueprint = await reassembleBlueprint(creativeDir);
25159
25941
  if (!blueprint) return "not_applicable";
25160
25942
  let rebuildRaw;
25161
25943
  try {
25162
- rebuildRaw = await readFile6(path8.join(creativeDir, REBUILD_FILE), "utf8");
25944
+ rebuildRaw = await readFile7(path9.join(creativeDir, REBUILD_FILE), "utf8");
25163
25945
  } catch {
25164
25946
  return "not_applicable";
25165
25947
  }
@@ -25200,7 +25982,7 @@ async function syncSceneNodeParams(canvas, canvasPath, log) {
25200
25982
  }
25201
25983
 
25202
25984
  // src/commands/canvas/style-projection.ts
25203
- import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
25985
+ import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
25204
25986
  function findBlueprintProjection(canvas) {
25205
25987
  if (!canvas || typeof canvas !== "object") return null;
25206
25988
  const nodes = canvas.nodes;
@@ -25228,8 +26010,8 @@ function renderStyleProjectionFromValue(blueprint) {
25228
26010
  async function syncStyleProjection(canvas, log) {
25229
26011
  const pair = findBlueprintProjection(canvas);
25230
26012
  if (!pair) return "not_applicable";
25231
- const rendered = renderStyleProjection(await readFile7(pair.promptPath, "utf8"));
25232
- const current = await readFile7(pair.stylePath, "utf8").catch(() => null);
26013
+ const rendered = renderStyleProjection(await readFile8(pair.promptPath, "utf8"));
26014
+ const current = await readFile8(pair.stylePath, "utf8").catch(() => null);
25233
26015
  if (current === rendered) return "up_to_date";
25234
26016
  await writeFile4(pair.stylePath, rendered, "utf8");
25235
26017
  log(
@@ -25291,13 +26073,13 @@ ${body}` : header || body || compactJson(record);
25291
26073
  }
25292
26074
 
25293
26075
  // src/commands/canvas/run-record.ts
25294
- import path9 from "path";
26076
+ import path10 from "path";
25295
26077
  var MAX_RUN_NODES = 200;
25296
26078
  var MAX_OUTPUTS_PER_NODE = 10;
25297
26079
  var MAX_FINAL_OUTPUTS = 10;
25298
26080
  var MAX_CREATIVE_SLUG_LENGTH = 100;
25299
26081
  function creativeSlugFromCanvasPath(filePath) {
25300
- const normalized = filePath.split(path9.sep).join("/");
26082
+ const normalized = filePath.split(path10.sep).join("/");
25301
26083
  const match = normalized.match(/(?:^|\/)src\/creatives\/([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\//);
25302
26084
  const slug = match?.[1] ?? null;
25303
26085
  return slug && slug.length <= MAX_CREATIVE_SLUG_LENGTH ? slug : null;
@@ -25585,24 +26367,24 @@ var RunRecordPoster = class {
25585
26367
 
25586
26368
  // src/commands/canvas/run-retention.ts
25587
26369
  import { rm as rm2 } from "fs/promises";
25588
- import path10 from "path";
26370
+ import path11 from "path";
25589
26371
  function runDirsToPrune(entries, keep, currentRunId) {
25590
26372
  const runs = entries.filter((e) => /^r_[0-9A-Za-z]+$/.test(e) && e !== currentRunId).sort();
25591
26373
  if (keep <= 0) return runs;
25592
26374
  return runs.slice(0, Math.max(0, runs.length - keep));
25593
26375
  }
25594
26376
  async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
25595
- const { readdir: readdir9 } = await import("fs/promises");
26377
+ const { readdir: readdir10 } = await import("fs/promises");
25596
26378
  let entries;
25597
26379
  try {
25598
- entries = await readdir9(outputsDir);
26380
+ entries = await readdir10(outputsDir);
25599
26381
  } catch {
25600
26382
  return;
25601
26383
  }
25602
26384
  const toPrune = runDirsToPrune(entries, keep, currentRunId);
25603
26385
  if (toPrune.length === 0) return;
25604
26386
  for (const dir of toPrune) {
25605
- await rm2(path10.join(outputsDir, dir), { recursive: true, force: true }).catch(
26387
+ await rm2(path11.join(outputsDir, dir), { recursive: true, force: true }).catch(
25606
26388
  (e) => log(`[prune ] could not remove ${dir}: ${e.message}`)
25607
26389
  );
25608
26390
  }
@@ -25610,13 +26392,13 @@ async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
25610
26392
  }
25611
26393
 
25612
26394
  // 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";
26395
+ import { mkdir as mkdir3, readdir as readdir4, rm as rm3, writeFile as writeFile5 } from "fs/promises";
26396
+ import path12 from "path";
25615
26397
  function creativeDirtyDir(base) {
25616
- return base ?? path11.resolve("canvas", ".dirty");
26398
+ return base ?? path12.resolve("canvas", ".dirty");
25617
26399
  }
25618
26400
  function dirtyMarkerFile(slug, base) {
25619
- return path11.join(creativeDirtyDir(base), `${slug}.json`);
26401
+ return path12.join(creativeDirtyDir(base), `${slug}.json`);
25620
26402
  }
25621
26403
  async function clearCreativeDirty(slug, base) {
25622
26404
  try {
@@ -25626,18 +26408,18 @@ async function clearCreativeDirty(slug, base) {
25626
26408
  }
25627
26409
 
25628
26410
  // 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";
26411
+ import { mkdir as mkdir4, readFile as readFile9, rm as rm4, writeFile as writeFile6 } from "fs/promises";
26412
+ import path13 from "path";
25631
26413
  function markerKey(canvasPath) {
25632
26414
  const slug = creativeSlugFromCanvasPath(canvasPath);
25633
- const identity = slug ?? path12.relative(process.cwd(), path12.resolve(canvasPath));
26415
+ const identity = slug ?? path13.relative(process.cwd(), path13.resolve(canvasPath));
25634
26416
  return sha256Hex(Buffer.from(identity)).slice(0, 32);
25635
26417
  }
25636
26418
  function legacyMarkerKey(canvasPath) {
25637
- return sha256Hex(Buffer.from(path12.resolve(canvasPath))).slice(0, 32);
26419
+ return sha256Hex(Buffer.from(path13.resolve(canvasPath))).slice(0, 32);
25638
26420
  }
25639
26421
  function markerFile(outputsDir, key) {
25640
- return path12.join(outputsDir, ".inflight", `${key}.json`);
26422
+ return path13.join(outputsDir, ".inflight", `${key}.json`);
25641
26423
  }
25642
26424
  var REMOTE_ADOPT_STALE_MS = 12e4;
25643
26425
  function classifyRemoteRun(run, now) {
@@ -25673,7 +26455,7 @@ async function resolveRunId(opts) {
25673
26455
  async function readMarkerRunId(outputsDir, canvasPath) {
25674
26456
  for (const key of [markerKey(canvasPath), legacyMarkerKey(canvasPath)]) {
25675
26457
  try {
25676
- const raw = await readFile8(markerFile(outputsDir, key), "utf8");
26458
+ const raw = await readFile9(markerFile(outputsDir, key), "utf8");
25677
26459
  const parsed = JSON.parse(raw);
25678
26460
  if (typeof parsed.runId === "string" && parsed.runId.length > 0) return parsed.runId;
25679
26461
  } catch {
@@ -25684,8 +26466,8 @@ async function readMarkerRunId(outputsDir, canvasPath) {
25684
26466
  async function markRunInFlight(outputsDir, canvasPath, runId) {
25685
26467
  try {
25686
26468
  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() }));
26469
+ await mkdir4(path13.dirname(file), { recursive: true });
26470
+ await writeFile6(file, JSON.stringify({ runId, canvasPath: path13.resolve(canvasPath), startedAt: Date.now() }));
25689
26471
  } catch {
25690
26472
  }
25691
26473
  }
@@ -25699,8 +26481,8 @@ async function clearRunMarker(outputsDir, canvasPath) {
25699
26481
  }
25700
26482
 
25701
26483
  // 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";
26484
+ import { mkdir as mkdir5, readdir as readdir5, readFile as readFile10, stat as stat2, writeFile as writeFile7 } from "fs/promises";
26485
+ import path14 from "path";
25704
26486
  var SNAPSHOT_SCHEMA = "baker-canvas-snapshot/1";
25705
26487
  var MAX_SNAPSHOT_FILE_BYTES = 32 * 1024 * 1024;
25706
26488
  var EXT_TO_MIME = {
@@ -25727,15 +26509,15 @@ var EXT_TO_MIME = {
25727
26509
  woff2: "font/woff2"
25728
26510
  };
25729
26511
  function mimeForFile(filePath) {
25730
- const ext = path13.extname(filePath).slice(1).toLowerCase();
26512
+ const ext = path14.extname(filePath).slice(1).toLowerCase();
25731
26513
  return EXT_TO_MIME[ext] ?? "application/octet-stream";
25732
26514
  }
25733
26515
  function toPosix(p) {
25734
- return p.split(path13.sep).join("/");
26516
+ return p.split(path14.sep).join("/");
25735
26517
  }
25736
26518
  function isInside2(dir, target) {
25737
- const rel = path13.relative(dir, target);
25738
- return !rel.startsWith("..") && !path13.isAbsolute(rel);
26519
+ const rel = path14.relative(dir, target);
26520
+ return !rel.startsWith("..") && !path14.isAbsolute(rel);
25739
26521
  }
25740
26522
  function localPathRefsFromCanvas(parsed) {
25741
26523
  const nodes = parsed?.nodes;
@@ -25760,24 +26542,24 @@ function isSnapshotablePath(value) {
25760
26542
  async function sourceRefsToSnapshot(canvasDir, parsed) {
25761
26543
  const refs = new Set(localPathRefsFromCanvas(parsed));
25762
26544
  for (const sceneFile of await listSceneFiles(canvasDir)) {
25763
- refs.add(toPosix(path13.relative(canvasDir, sceneFile)));
26545
+ refs.add(toPosix(path14.relative(canvasDir, sceneFile)));
25764
26546
  }
25765
26547
  refs.add(REBUILD_FILE);
25766
26548
  return [...refs];
25767
26549
  }
25768
26550
  async function uploadRunSnapshot(client, opts) {
25769
26551
  try {
25770
- const canvasDir = path13.dirname(opts.canvasPath);
26552
+ const canvasDir = path14.dirname(opts.canvasPath);
25771
26553
  const put = (bytes, mime) => putContentAddressed(client, bytes, mime, opts.signal);
25772
26554
  const canvasBytes = Buffer.from(opts.raw);
25773
26555
  const canvasUpload = await put(canvasBytes, "application/json");
25774
26556
  const files = [];
25775
26557
  const skipped = [];
25776
26558
  for (const refPath of await sourceRefsToSnapshot(canvasDir, opts.parsed)) {
25777
- const abs = path13.isAbsolute(refPath) ? refPath : path13.resolve(canvasDir, refPath);
26559
+ const abs = path14.isAbsolute(refPath) ? refPath : path14.resolve(canvasDir, refPath);
25778
26560
  if (!isInside2(canvasDir, abs)) {
25779
26561
  skipped.push({
25780
- path: toPosix(path13.relative(canvasDir, abs)),
26562
+ path: toPosix(path14.relative(canvasDir, abs)),
25781
26563
  reason: "outside the creative folder \u2014 read from the workspace on rerun"
25782
26564
  });
25783
26565
  continue;
@@ -25786,18 +26568,18 @@ async function uploadRunSnapshot(client, opts) {
25786
26568
  try {
25787
26569
  st = await stat2(abs);
25788
26570
  } catch {
25789
- skipped.push({ path: toPosix(path13.relative(canvasDir, abs)), reason: "missing" });
26571
+ skipped.push({ path: toPosix(path14.relative(canvasDir, abs)), reason: "missing" });
25790
26572
  continue;
25791
26573
  }
25792
26574
  const fileList = st.isDirectory() ? await listFilesRecursive(abs) : [abs];
25793
26575
  for (const file of fileList) {
25794
- const rel = toPosix(path13.relative(canvasDir, file));
26576
+ const rel = toPosix(path14.relative(canvasDir, file));
25795
26577
  const size = (await stat2(file)).size;
25796
26578
  if (size > MAX_SNAPSHOT_FILE_BYTES) {
25797
26579
  skipped.push({ path: rel, reason: `too large (${size} bytes)` });
25798
26580
  continue;
25799
26581
  }
25800
- const bytes = await readFile9(file);
26582
+ const bytes = await readFile10(file);
25801
26583
  const upload = await put(bytes, mimeForFile(file));
25802
26584
  files.push({ path: rel, sha256: upload.sha256, url: upload.url });
25803
26585
  }
@@ -25806,7 +26588,7 @@ async function uploadRunSnapshot(client, opts) {
25806
26588
  schema: SNAPSHOT_SCHEMA,
25807
26589
  creativeSlug: opts.creativeSlug,
25808
26590
  canvasSha: canvasUpload.sha256,
25809
- canvas: { path: path13.basename(opts.canvasPath), sha256: canvasUpload.sha256, url: canvasUpload.url },
26591
+ canvas: { path: path14.basename(opts.canvasPath), sha256: canvasUpload.sha256, url: canvasUpload.url },
25810
26592
  files,
25811
26593
  skipped: skipped.length > 0 ? skipped : void 0
25812
26594
  };
@@ -25832,8 +26614,8 @@ async function putContentAddressed(client, bytes, mime, signal) {
25832
26614
  return { sha256, url: publicUrl };
25833
26615
  }
25834
26616
  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));
26617
+ const entries = await readdir5(dir, { recursive: true, withFileTypes: true });
26618
+ return entries.filter((d) => d.isFile()).map((d) => path14.join(d.parentPath, d.name));
25837
26619
  }
25838
26620
  var SnapshotConflictError = class extends Error {
25839
26621
  conflicts;
@@ -25845,19 +26627,19 @@ var SnapshotConflictError = class extends Error {
25845
26627
  };
25846
26628
  async function restoreRunSnapshot(manifest, targetDir, opts = {}) {
25847
26629
  const entries = [manifest.canvas, ...manifest.files];
25848
- const resolvedTarget = path13.resolve(targetDir);
26630
+ const resolvedTarget = path14.resolve(targetDir);
25849
26631
  const planned = [];
25850
26632
  const conflicts = [];
25851
26633
  const upToDate = [];
25852
26634
  for (const entry of entries) {
25853
- if (path13.isAbsolute(entry.path) || entry.path.split("/").includes("..")) {
26635
+ if (path14.isAbsolute(entry.path) || entry.path.split("/").includes("..")) {
25854
26636
  throw new Error(`snapshot entry escapes the creative directory: ${entry.path}`);
25855
26637
  }
25856
- const target = path13.resolve(resolvedTarget, entry.path);
25857
- if (target !== resolvedTarget && !target.startsWith(resolvedTarget + path13.sep)) {
26638
+ const target = path14.resolve(resolvedTarget, entry.path);
26639
+ if (target !== resolvedTarget && !target.startsWith(resolvedTarget + path14.sep)) {
25858
26640
  throw new Error(`snapshot entry escapes the creative directory: ${entry.path}`);
25859
26641
  }
25860
- const existing = await readFile9(target).catch(() => null);
26642
+ const existing = await readFile10(target).catch(() => null);
25861
26643
  if (existing) {
25862
26644
  if (sha256Hex(existing) === entry.sha256) {
25863
26645
  upToDate.push(entry.path);
@@ -25879,11 +26661,11 @@ async function restoreRunSnapshot(manifest, targetDir, opts = {}) {
25879
26661
  if (sha256Hex(bytes) !== entry.sha256) {
25880
26662
  throw new Error(`snapshot download for ${entry.path} does not match its recorded sha256`);
25881
26663
  }
25882
- await mkdir5(path13.dirname(target), { recursive: true });
26664
+ await mkdir5(path14.dirname(target), { recursive: true });
25883
26665
  await writeFile7(target, bytes);
25884
26666
  restored.push(entry.path);
25885
26667
  }
25886
- return { canvasPath: path13.resolve(resolvedTarget, manifest.canvas.path), restored, upToDate };
26668
+ return { canvasPath: path14.resolve(resolvedTarget, manifest.canvas.path), restored, upToDate };
25887
26669
  }
25888
26670
 
25889
26671
  // src/commands/canvas/run.ts
@@ -25963,8 +26745,8 @@ function resolveMaxCredits(...candidates) {
25963
26745
  return void 0;
25964
26746
  }
25965
26747
  async function executeCanvasRun(opts) {
25966
- const filePath = path14.resolve(opts.file);
25967
- const raw = await readFile10(filePath, "utf8");
26748
+ const filePath = path15.resolve(opts.file);
26749
+ const raw = await readFile11(filePath, "utf8");
25968
26750
  let parsed;
25969
26751
  try {
25970
26752
  parsed = JSON.parse(raw);
@@ -25986,7 +26768,7 @@ ${describeRewrites(healed.rewrites)}
25986
26768
  `
25987
26769
  );
25988
26770
  }
25989
- parsed = resolveRelativeCanvasPaths(parsed, path14.dirname(filePath));
26771
+ parsed = resolveRelativeCanvasPaths(parsed, path15.dirname(filePath));
25990
26772
  try {
25991
26773
  await syncStyleProjection(parsed, (line) => process.stderr.write(`${line}
25992
26774
  `));
@@ -26092,7 +26874,7 @@ ${describeRewrites(healed.rewrites)}
26092
26874
  const canvasSha = sha256Hex(Buffer.from(canvasText));
26093
26875
  const creativeSlug = creativeSlugFromCanvasPath(filePath) ?? void 0;
26094
26876
  const client = opts.record === false ? null : buildBackendClient();
26095
- const outputsDir = opts.outputsDir ? path14.resolve(opts.outputsDir) : path14.resolve("canvas");
26877
+ const outputsDir = opts.outputsDir ? path15.resolve(opts.outputsDir) : path15.resolve("canvas");
26096
26878
  const { runId, resumed, source, concurrentRunId } = await resolveRunId({
26097
26879
  explicitRunId: opts.runId,
26098
26880
  fresh: opts.fresh === true,
@@ -26126,7 +26908,7 @@ ${describeRewrites(healed.rewrites)}
26126
26908
  const canvasSnapshotUrl = client && creativeSlug ? await uploadRunSnapshot(client, { canvasPath: filePath, raw: canvasText, creativeSlug, parsed }) ?? void 0 : void 0;
26127
26909
  const recordMeta = {
26128
26910
  creativeSlug,
26129
- canvasPath: path14.relative(process.cwd(), filePath) || void 0,
26911
+ canvasPath: path15.relative(process.cwd(), filePath) || void 0,
26130
26912
  canvasSha,
26131
26913
  // The fingerprint the dashboard compares against the current source to flag
26132
26914
  // "edited since last render". Computed from the on-disk canvas.json +
@@ -26313,7 +27095,7 @@ var rerunCommand = defineCommand99({
26313
27095
  if (latest.canvasSha && manifest.canvasSha !== latest.canvasSha) {
26314
27096
  fail2("snapshot_mismatch", `snapshot manifest for run ${latest.runId} does not match its recorded canvas sha`);
26315
27097
  }
26316
- const targetDir = path15.resolve("src", "creatives", slug);
27098
+ const targetDir = path16.resolve("src", "creatives", slug);
26317
27099
  let restoredCanvasPath;
26318
27100
  try {
26319
27101
  const restore = await restoreRunSnapshot(manifest, targetDir, { force: args["force-remote"] === true });
@@ -26362,8 +27144,8 @@ async function fetchManifest(url) {
26362
27144
  }
26363
27145
 
26364
27146
  // 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";
27147
+ import { access, mkdir as mkdir6, readFile as readFile13, writeFile as writeFile8 } from "fs/promises";
27148
+ import path20 from "path";
26367
27149
  import { defineCommand as defineCommand101 } from "citty";
26368
27150
 
26369
27151
  // src/engine/scaffold/staticAd.ts
@@ -26645,7 +27427,7 @@ function staticAdReport(input, elementsInput, opts) {
26645
27427
  }
26646
27428
 
26647
27429
  // src/commands/canvas/creative-definition.ts
26648
- import path16 from "path";
27430
+ import path17 from "path";
26649
27431
  var PLATFORM_VALUES = ["meta", "google", "linkedin", "tiktok", "youtube", "x", "other"];
26650
27432
  var FORMAT_VALUES = ["1:1", "4:5", "9:16", "16:9", "1.91:1"];
26651
27433
  function titleFromSlug(slug) {
@@ -26693,16 +27475,16 @@ function buildCreativeDefinition(input) {
26693
27475
  }
26694
27476
 
26695
27477
  // src/commands/canvas/scaffold-static-ad-paths.ts
26696
- import path17 from "path";
27478
+ import path18 from "path";
26697
27479
  function resolveScaffoldStaticAdPaths(rawFile, out, cwd = process.cwd(), slug) {
26698
27480
  const file = rawFile.trim();
26699
27481
  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;
27482
+ const imageSource = imageIsUrl ? file : path18.resolve(cwd, file);
27483
+ 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");
27484
+ const blueprintPath = path18.join(path18.dirname(outPath), "prompt.json");
27485
+ const creativeDir = slug ? path18.dirname(outPath) : null;
27486
+ const definitionPath = creativeDir ? path18.join(creativeDir, "_definition.md") : null;
27487
+ const referencesDir = creativeDir ? path18.join(creativeDir, "references") : null;
26706
27488
  return { imageIsUrl, imageSource, outPath, blueprintPath, creativeDir, definitionPath, referencesDir };
26707
27489
  }
26708
27490
  var SCAFFOLD_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
@@ -26712,8 +27494,8 @@ function isValidScaffoldSlug(slug) {
26712
27494
  }
26713
27495
 
26714
27496
  // 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";
27497
+ import { readdir as readdir6, readFile as readFile12, stat as stat3 } from "fs/promises";
27498
+ import path19 from "path";
26717
27499
  import { defineCommand as defineCommand100 } from "citty";
26718
27500
 
26719
27501
  // src/commands/canvas/definition-graph.ts
@@ -26806,26 +27588,26 @@ async function syncCreativeDefinitionBestEffort(input) {
26806
27588
  }
26807
27589
  }
26808
27590
  async function resolveCanvasPath(inputPath) {
26809
- const resolved = path18.resolve(inputPath);
27591
+ const resolved = path19.resolve(inputPath);
26810
27592
  let dir = resolved;
26811
27593
  try {
26812
27594
  if ((await stat3(resolved)).isFile()) {
26813
27595
  if (resolved.endsWith(".canvas.json")) return resolved;
26814
- dir = path18.dirname(resolved);
27596
+ dir = path19.dirname(resolved);
26815
27597
  }
26816
27598
  } catch {
26817
- dir = resolved.endsWith(".canvas.json") ? path18.dirname(resolved) : resolved;
27599
+ dir = resolved.endsWith(".canvas.json") ? path19.dirname(resolved) : resolved;
26818
27600
  }
26819
27601
  let entries;
26820
27602
  try {
26821
- entries = await readdir5(dir);
27603
+ entries = await readdir6(dir);
26822
27604
  } catch {
26823
27605
  return null;
26824
27606
  }
26825
27607
  const canvases = entries.filter((name) => name.endsWith(".canvas.json"));
26826
27608
  const slug = creativeSlugFromCanvasPath(`${dir}/x/`);
26827
27609
  const chosen = (slug ? canvases.find((name) => name === `${slug}.canvas.json`) : void 0) ?? canvases[0];
26828
- return chosen ? path18.join(dir, chosen) : null;
27610
+ return chosen ? path19.join(dir, chosen) : null;
26829
27611
  }
26830
27612
  var syncDefinitionCommand = defineCommand100({
26831
27613
  meta: {
@@ -26848,7 +27630,7 @@ var syncDefinitionCommand = defineCommand100({
26848
27630
  if (!slug) return;
26849
27631
  let canvas;
26850
27632
  try {
26851
- canvas = JSON.parse(await readFile11(canvasPath, "utf8"));
27633
+ canvas = JSON.parse(await readFile12(canvasPath, "utf8"));
26852
27634
  } catch {
26853
27635
  return;
26854
27636
  }
@@ -26875,7 +27657,7 @@ async function uploadSourceAsReference(source, isUrl, client) {
26875
27657
  throw new Error(`failed to download source image (${e instanceof Error ? e.message : String(e)})`);
26876
27658
  }
26877
27659
  } else {
26878
- bytes = await readFile12(source);
27660
+ bytes = await readFile13(source);
26879
27661
  }
26880
27662
  const safe = await toModelSafeImage(bytes);
26881
27663
  const sha256 = sha256Hex(safe.bytes);
@@ -26930,7 +27712,7 @@ DROP background extras, decorative props, generic scenery, and anything small or
26930
27712
  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
27713
  async function loadAssetText(ref, label) {
26932
27714
  const r = ref;
26933
- if (typeof r?.path === "string") return readFile12(r.path, "utf8");
27715
+ if (typeof r?.path === "string") return readFile13(r.path, "utf8");
26934
27716
  if (typeof r?.url === "string") {
26935
27717
  const res = await fetch(r.url);
26936
27718
  if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
@@ -27106,7 +27888,7 @@ var scaffoldStaticAdCommand = defineCommand101({
27106
27888
  process.cwd(),
27107
27889
  slug
27108
27890
  );
27109
- await mkdir6(path19.dirname(outPath), { recursive: true });
27891
+ await mkdir6(path20.dirname(outPath), { recursive: true });
27110
27892
  const { describeModel, selectModel, layoutModel, genModel } = resolveModels(args);
27111
27893
  let durableSourceUrl;
27112
27894
  if (referencesDir) {
@@ -27226,7 +28008,7 @@ var scaffoldStaticAdCommand = defineCommand101({
27226
28008
  run_estimated_credits: validation.estimatedCredits
27227
28009
  },
27228
28010
  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.`,
28011
+ 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
28012
  assets_to_supply: report.elements,
27231
28013
  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
28014
  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 +28025,9 @@ var scaffoldStaticAdCommand = defineCommand101({
27243
28025
  });
27244
28026
 
27245
28027
  // 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";
28028
+ import { access as access2, cp, mkdir as mkdir7, readFile as readFile16, rm as rm6, writeFile as writeFile9 } from "fs/promises";
27247
28029
  import { tmpdir as tmpdir2 } from "os";
27248
- import path22 from "path";
28030
+ import path23 from "path";
27249
28031
  import { defineCommand as defineCommand102 } from "citty";
27250
28032
 
27251
28033
  // src/engine/scaffold/lib/model-router.ts
@@ -27285,7 +28067,7 @@ function routeVideoModel(input) {
27285
28067
 
27286
28068
  // src/engine/nodes/local/lib/sceneDetect.ts
27287
28069
  import { execFile as execFile2 } from "child_process";
27288
- import { mkdtemp, readdir as readdir6, readFile as readFile13, rm as rm5 } from "fs/promises";
28070
+ import { mkdtemp, readdir as readdir7, readFile as readFile14, rm as rm5 } from "fs/promises";
27289
28071
  import { tmpdir } from "os";
27290
28072
  import { join as join2 } from "path";
27291
28073
  import { promisify as promisify2 } from "util";
@@ -27359,9 +28141,9 @@ async function runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs)
27359
28141
  ],
27360
28142
  { encoding: "utf-8", maxBuffer: 32 * 1024 * 1024, timeout: timeoutMs }
27361
28143
  );
27362
- const csvName = (await readdir6(outDir)).find((f) => f.toLowerCase().endsWith(".csv"));
28144
+ const csvName = (await readdir7(outDir)).find((f) => f.toLowerCase().endsWith(".csv"));
27363
28145
  if (!csvName) return [];
27364
- return parsePySceneDetectCsvCuts(await readFile13(join2(outDir, csvName), "utf-8"));
28146
+ return parsePySceneDetectCsvCuts(await readFile14(join2(outDir, csvName), "utf-8"));
27365
28147
  } finally {
27366
28148
  await rm5(outDir, { recursive: true, force: true });
27367
28149
  }
@@ -27386,23 +28168,23 @@ async function detectSceneCutsPySceneDetect(filePath, opts = {}) {
27386
28168
 
27387
28169
  // src/commands/canvas/composition-path.ts
27388
28170
  import { existsSync as existsSync4 } from "fs";
27389
- import path20 from "path";
28171
+ import path21 from "path";
27390
28172
  function resolveShippedCanvasDir(name, startDir, exists = existsSync4, maxDepth = 8) {
27391
- const rel = path20.join("canvas", name);
28173
+ const rel = path21.join("canvas", name);
27392
28174
  let dir = startDir;
27393
28175
  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);
28176
+ const candidate = path21.join(dir, rel);
28177
+ if (exists(path21.join(candidate, "meta.json"))) return candidate;
28178
+ const parent = path21.dirname(dir);
27397
28179
  if (parent === dir) break;
27398
28180
  dir = parent;
27399
28181
  }
27400
- return path20.resolve(startDir, "../../../", rel);
28182
+ return path21.resolve(startDir, "../../../", rel);
27401
28183
  }
27402
28184
 
27403
28185
  // src/commands/canvas/gitignore.ts
27404
- import { appendFile, readFile as readFile14 } from "fs/promises";
27405
- import path21 from "path";
28186
+ import { appendFile, readFile as readFile15 } from "fs/promises";
28187
+ import path22 from "path";
27406
28188
  function missingGitignoreEntries(existing, entries) {
27407
28189
  const present2 = new Set(
27408
28190
  existing.split("\n").map((l) => l.trim().replace(/\/+$/, "")).filter((l) => l.length > 0 && !l.startsWith("#"))
@@ -27410,10 +28192,10 @@ function missingGitignoreEntries(existing, entries) {
27410
28192
  return entries.filter((e) => !present2.has(e.trim().replace(/\/+$/, "")));
27411
28193
  }
27412
28194
  async function ensureGitignore(dir, entries) {
27413
- const file = path21.join(dir, ".gitignore");
28195
+ const file = path22.join(dir, ".gitignore");
27414
28196
  let existing;
27415
28197
  try {
27416
- existing = await readFile14(file, "utf8");
28198
+ existing = await readFile15(file, "utf8");
27417
28199
  } catch {
27418
28200
  return;
27419
28201
  }
@@ -27452,7 +28234,7 @@ ONE PERSON, MULTIPLE LOOKS: if a single individual plays MULTIPLE personas or wa
27452
28234
  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
28235
  async function loadAssetText2(ref, label) {
27454
28236
  const r = ref;
27455
- if (typeof r?.path === "string") return readFile15(r.path, "utf8");
28237
+ if (typeof r?.path === "string") return readFile16(r.path, "utf8");
27456
28238
  if (typeof r?.url === "string") {
27457
28239
  const res = await fetch(r.url);
27458
28240
  if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
@@ -27471,7 +28253,7 @@ async function loadTranscriptBestEffort(ref) {
27471
28253
  async function stageCaptions(outDir, transcript) {
27472
28254
  const text2 = transcript?.trim();
27473
28255
  if (!text2 || text2 === "[]") return {};
27474
- const compositionPath = path22.join(outDir, "tiktok-captions-composition");
28256
+ const compositionPath = path23.join(outDir, "tiktok-captions-composition");
27475
28257
  await cp(SHIPPED_CAPTIONS_DIR, compositionPath, { recursive: true });
27476
28258
  return { compositionPath };
27477
28259
  }
@@ -27489,11 +28271,11 @@ function patchCompositionHtml(html, dims) {
27489
28271
  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
28272
  }
27491
28273
  async function stampCompositionDims(compositionDir, dims) {
27492
- const metaPath = path22.join(compositionDir, "meta.json");
27493
- const rawMeta = await readFile15(metaPath, "utf8");
28274
+ const metaPath = path23.join(compositionDir, "meta.json");
28275
+ const rawMeta = await readFile16(metaPath, "utf8");
27494
28276
  await writeFile9(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
27495
- const htmlPath = path22.join(compositionDir, "index.html");
27496
- const rawHtml = await readFile15(htmlPath, "utf8");
28277
+ const htmlPath = path23.join(compositionDir, "index.html");
28278
+ const rawHtml = await readFile16(htmlPath, "utf8");
27497
28279
  await writeFile9(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
27498
28280
  }
27499
28281
  function parseElements2(raw) {
@@ -27541,7 +28323,7 @@ var VIDEO_EXT_BY_MIME = {
27541
28323
  "video/x-matroska": ".mkv"
27542
28324
  };
27543
28325
  function referenceVideoExt(url, contentType) {
27544
- const fromPath = path22.extname(new URL(url).pathname).toLowerCase();
28326
+ const fromPath = path23.extname(new URL(url).pathname).toLowerCase();
27545
28327
  if (fromPath && fromPath.length <= 5) return fromPath;
27546
28328
  const mime = (contentType ?? "").split(";")[0]?.trim().toLowerCase();
27547
28329
  return mime && VIDEO_EXT_BY_MIME[mime] || ".mp4";
@@ -27567,7 +28349,7 @@ function videoDefinitionDescription(blueprint) {
27567
28349
  return typeof product === "string" && product.trim() ? product.trim() : void 0;
27568
28350
  }
27569
28351
  async function materializeReferenceVideo(fileArg2) {
27570
- if (!/^https?:\/\//i.test(fileArg2)) return path22.resolve(fileArg2);
28352
+ if (!/^https?:\/\//i.test(fileArg2)) return path23.resolve(fileArg2);
27571
28353
  let bytes;
27572
28354
  let contentType;
27573
28355
  try {
@@ -27579,7 +28361,7 @@ async function materializeReferenceVideo(fileArg2) {
27579
28361
  throw new Error(`failed to download reference video: ${e instanceof Error ? e.message : String(e)}`);
27580
28362
  }
27581
28363
  if (bytes.length === 0) throw new Error("reference video download was empty");
27582
- const dest = path22.join(
28364
+ const dest = path23.join(
27583
28365
  tmpdir2(),
27584
28366
  `baker-ref-${sha256Hex(bytes).slice(0, 16)}${referenceVideoExt(fileArg2, contentType)}`
27585
28367
  );
@@ -27799,11 +28581,11 @@ var scaffoldVideoCommand = defineCommand102({
27799
28581
  } catch (e) {
27800
28582
  return fail4("download", e instanceof Error ? e.message : String(e));
27801
28583
  }
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");
28584
+ const base = path23.basename(videoPath, path23.extname(videoPath));
28585
+ 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`);
28586
+ const outDir = path23.dirname(outPath);
28587
+ const blueprintPath = path23.join(outDir, "prompt.json");
28588
+ const blueprintStylePath = path23.join(outDir, "prompt.style.json");
27807
28589
  const frames = args.frames === "reuse" ? "reuse" : "generate";
27808
28590
  const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
27809
28591
  if (Number.isFinite(maxScenes)) {
@@ -27848,12 +28630,12 @@ var scaffoldVideoCommand = defineCommand102({
27848
28630
  `
27849
28631
  );
27850
28632
  }
27851
- const compositionDest = path22.join(outDir, "video-overlay-composition");
28633
+ const compositionDest = path23.join(outDir, "video-overlay-composition");
27852
28634
  await cp(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
27853
28635
  await stampCompositionDims(compositionDest, outDims);
27854
- const indexPath = path22.join(compositionDest, "index.html");
28636
+ const indexPath = path23.join(compositionDest, "index.html");
27855
28637
  const overlayHtml = buildOverlayHtml(blueprint, { captionsActive: Boolean(transcript) });
27856
- const indexHtml = await readFile15(indexPath, "utf8");
28638
+ const indexHtml = await readFile16(indexPath, "utf8");
27857
28639
  const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
27858
28640
  if (injected === indexHtml && overlayHtml.trim()) {
27859
28641
  fail4(
@@ -27867,10 +28649,10 @@ var scaffoldVideoCommand = defineCommand102({
27867
28649
  const opts = {
27868
28650
  imageModel,
27869
28651
  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),
28652
+ overlayCompositionPath: path23.relative(outDir, compositionDest),
28653
+ captionsCompositionPath: captions.compositionPath ? path23.relative(outDir, captions.compositionPath) : void 0,
28654
+ blueprintPath: path23.relative(outDir, blueprintPath),
28655
+ blueprintStylePath: path23.relative(outDir, blueprintStylePath),
27874
28656
  frames,
27875
28657
  ambient: Boolean(args.ambient),
27876
28658
  seamDedup: resolveSeamDedup(args["seam-dedup"]),
@@ -27898,7 +28680,7 @@ var scaffoldVideoCommand = defineCommand102({
27898
28680
  await writeFile9(outPath, `${JSON.stringify(canvas, null, 2)}
27899
28681
  `, "utf8");
27900
28682
  await writeFile9(
27901
- path22.join(outDir, REBUILD_FILE),
28683
+ path23.join(outDir, REBUILD_FILE),
27902
28684
  `${JSON.stringify({ elements, opts }, null, 2)}
27903
28685
  `,
27904
28686
  "utf8"
@@ -27923,7 +28705,7 @@ var scaffoldVideoCommand = defineCommand102({
27923
28705
  await ensureGitignore(process.cwd(), ["canvas/", ".context/"]);
27924
28706
  const sourceRef = videoSourceReference(blueprint, fileArg2);
27925
28707
  if (slug) {
27926
- const definitionPath = path22.join(outDir, "_definition.md");
28708
+ const definitionPath = path23.join(outDir, "_definition.md");
27927
28709
  if (!await fileExists2(definitionPath)) {
27928
28710
  await writeFile9(
27929
28711
  definitionPath,
@@ -27978,7 +28760,7 @@ var scaffoldVideoCommand = defineCommand102({
27978
28760
  graph: canvas.metadata?.video?.graph_stats
27979
28761
  },
27980
28762
  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.`,
28763
+ 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
28764
  recurring_elements_to_supply: report.elements,
27983
28765
  voices_to_confirm: report.dialogue.map((d) => ({
27984
28766
  scene: d.scene,
@@ -28015,8 +28797,8 @@ var scaffoldVideoCommand = defineCommand102({
28015
28797
  });
28016
28798
 
28017
28799
  // src/commands/canvas/set-prompt.ts
28018
- import { readFile as readFile16, writeFile as writeFile10 } from "fs/promises";
28019
- import path23 from "path";
28800
+ import { readFile as readFile17, writeFile as writeFile10 } from "fs/promises";
28801
+ import path24 from "path";
28020
28802
  import { defineCommand as defineCommand103 } from "citty";
28021
28803
  function setNodePrompt(canvas, nodeId, text2) {
28022
28804
  const nodes = canvas?.nodes;
@@ -28044,17 +28826,17 @@ var setPromptCommand = defineCommand103({
28044
28826
  "text-file": { type: "string", description: "Read the new prompt from a UTF-8 file (preserves accents/newlines)" }
28045
28827
  },
28046
28828
  async run({ args }) {
28047
- const filePath = path23.resolve(String(args.file));
28829
+ const filePath = path24.resolve(String(args.file));
28048
28830
  let canvas;
28049
28831
  try {
28050
- canvas = JSON.parse(await readFile16(filePath, "utf8"));
28832
+ canvas = JSON.parse(await readFile17(filePath, "utf8"));
28051
28833
  } catch (e) {
28052
28834
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "parse", message: String(e) } }, null, 2)}
28053
28835
  `);
28054
28836
  process.exit(2);
28055
28837
  }
28056
28838
  let text2;
28057
- if (args["text-file"]) text2 = await readFile16(path23.resolve(String(args["text-file"])), "utf8");
28839
+ if (args["text-file"]) text2 = await readFile17(path24.resolve(String(args["text-file"])), "utf8");
28058
28840
  else if (args.text !== void 0) text2 = String(args.text);
28059
28841
  else {
28060
28842
  process.stderr.write(
@@ -28075,7 +28857,7 @@ var setPromptCommand = defineCommand103({
28075
28857
  process.exit(2);
28076
28858
  return;
28077
28859
  }
28078
- const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path23.dirname(filePath)), defaultRegistry());
28860
+ const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path24.dirname(filePath)), defaultRegistry());
28079
28861
  if (!validation.ok) {
28080
28862
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "validation", issues: validation.issues } }, null, 2)}
28081
28863
  `);
@@ -28090,8 +28872,8 @@ var setPromptCommand = defineCommand103({
28090
28872
  });
28091
28873
 
28092
28874
  // src/commands/canvas/validate.ts
28093
- import { readFile as readFile17 } from "fs/promises";
28094
- import path24 from "path";
28875
+ import { readFile as readFile18 } from "fs/promises";
28876
+ import path25 from "path";
28095
28877
  import { defineCommand as defineCommand104 } from "citty";
28096
28878
  var validateCommand = defineCommand104({
28097
28879
  meta: {
@@ -28100,8 +28882,8 @@ var validateCommand = defineCommand104({
28100
28882
  },
28101
28883
  args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
28102
28884
  async run({ args }) {
28103
- const filePath = path24.resolve(String(args.file));
28104
- const raw = await readFile17(filePath, "utf8");
28885
+ const filePath = path25.resolve(String(args.file));
28886
+ const raw = await readFile18(filePath, "utf8");
28105
28887
  let parsed;
28106
28888
  try {
28107
28889
  parsed = JSON.parse(raw);
@@ -28113,7 +28895,7 @@ var validateCommand = defineCommand104({
28113
28895
  }
28114
28896
  const healed = await healAbsoluteCanvasPaths(filePath, parsed);
28115
28897
  parsed = healed.canvas;
28116
- parsed = resolveRelativeCanvasPaths(parsed, path24.dirname(filePath));
28898
+ parsed = resolveRelativeCanvasPaths(parsed, path25.dirname(filePath));
28117
28899
  let styleProjection = "not_applicable";
28118
28900
  try {
28119
28901
  styleProjection = await syncStyleProjection(parsed, (line) => process.stderr.write(`${line}
@@ -28537,7 +29319,7 @@ import { defineCommand as defineCommand109 } from "citty";
28537
29319
  import { defineCommand as defineCommand108 } from "citty";
28538
29320
 
28539
29321
  // src/commands/images/api.ts
28540
- import { readFile as readFile18 } from "fs/promises";
29322
+ import { readFile as readFile19 } from "fs/promises";
28541
29323
  import { basename, extname } from "path";
28542
29324
  var imageProcessingTimeoutMs = 18e4;
28543
29325
  var imageReadyPollIntervalMs = 2e3;
@@ -28551,7 +29333,7 @@ var mimeMap = {
28551
29333
  ".avif": "image/avif"
28552
29334
  };
28553
29335
  var defaultImageApiDeps = {
28554
- readFile: readFile18,
29336
+ readFile: readFile19,
28555
29337
  post: apiPost,
28556
29338
  get: apiGet,
28557
29339
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
@@ -31270,7 +32052,7 @@ function cropSprite(input, region) {
31270
32052
 
31271
32053
  // src/lib/image/io.ts
31272
32054
  import { randomBytes } from "crypto";
31273
- import { glob as fsGlob, readFile as readFile19, rename, stat as stat4, writeFile as writeFile11 } from "fs/promises";
32055
+ import { glob as fsGlob, readFile as readFile20, rename, stat as stat4, writeFile as writeFile11 } from "fs/promises";
31274
32056
  import { dirname as dirname2, extname as extname2, join as join4, resolve as resolve4 } from "path";
31275
32057
  var REMOTE_RE = /^https?:\/\//i;
31276
32058
  var GLOB_RE = /[*?[\]{}]/;
@@ -31303,7 +32085,7 @@ async function readImageBuffer(pathOrUrl) {
31303
32085
  const { buffer } = await fetchExternalBytes(pathOrUrl, { maxBytes: MAX_REMOTE_IMAGE_BYTES });
31304
32086
  return buffer;
31305
32087
  }
31306
- return readFile19(pathOrUrl);
32088
+ return readFile20(pathOrUrl);
31307
32089
  }
31308
32090
  async function isDirectory(path39) {
31309
32091
  try {
@@ -33639,7 +34421,7 @@ function parseResize(args) {
33639
34421
  }
33640
34422
  return void 0;
33641
34423
  }
33642
- function parseColor(raw) {
34424
+ function parseColor2(raw) {
33643
34425
  if (typeof raw !== "string" || raw.length === 0) return void 0;
33644
34426
  try {
33645
34427
  return parseHex(raw);
@@ -33649,7 +34431,7 @@ function parseColor(raw) {
33649
34431
  }
33650
34432
  function buildOptions(args) {
33651
34433
  const resize = parseResize(args);
33652
- const color = parseColor(args.color);
34434
+ const color = parseColor2(args.color);
33653
34435
  const removeBackground2 = typeof args["remove-bg"] === "boolean" ? args["remove-bg"] : color !== void 0;
33654
34436
  const shrinkToContent = typeof args["shrink-to-content"] === "boolean" ? args["shrink-to-content"] : removeBackground2;
33655
34437
  return {
@@ -34666,222 +35448,10 @@ Full guide: __tooling__/docs/tools/baker/images.md`
34666
35448
  import { defineCommand as defineCommand160 } from "citty";
34667
35449
 
34668
35450
  // src/commands/landing/critique.ts
34669
- import { readdir as readdir8, stat as stat6 } from "fs/promises";
35451
+ import { readdir as readdir9, stat as stat6 } from "fs/promises";
34670
35452
  import path29 from "path";
34671
35453
  import { defineCommand as defineCommand150 } from "citty";
34672
35454
 
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
35455
  // src/engine/landing/lib/constants.ts
34886
35456
  var OVERUSED_FONTS = /* @__PURE__ */ new Set([
34887
35457
  // Older monoculture (still ubiquitous):
@@ -35444,7 +36014,7 @@ var LINE_MATCHERS = [
35444
36014
  id: "cream-palette",
35445
36015
  regex: /background(?:-color)?\s*:\s*(#[0-9a-f]{6}\b|rgba?\([^)]+\))/gi,
35446
36016
  test: (m) => {
35447
- const c = parseColor2(cap2(m, 1));
36017
+ const c = parseColor(cap2(m, 1));
35448
36018
  return c !== null && isCreamColor(c);
35449
36019
  },
35450
36020
  fmt: (m) => `${cap2(m, 1)} (cream ground)`
@@ -35785,7 +36355,7 @@ function shadowIsGlow(value) {
35785
36355
  const y = Number.parseFloat(offsets[1] ?? "0");
35786
36356
  const blur = Number.parseFloat(offsets[2] ?? "0");
35787
36357
  if (!(x === 0 && y === 0 && blur > 4)) return false;
35788
- const c = parseColor2(colorMatch[0]);
36358
+ const c = parseColor(colorMatch[0]);
35789
36359
  return c !== null && hasChroma(c, 30);
35790
36360
  }
35791
36361
  function firstFamily(decl) {
@@ -35979,7 +36549,7 @@ async function writeCritiqueSnapshot(projectRoot, snapshot) {
35979
36549
  }
35980
36550
 
35981
36551
  // src/commands/landing/source-version.ts
35982
- import { readdir as readdir7, readFile as readFile22, stat as stat5 } from "fs/promises";
36552
+ import { readdir as readdir8, readFile as readFile22, stat as stat5 } from "fs/promises";
35983
36553
  import path28 from "path";
35984
36554
  async function landingSourceRelPaths(landingDir) {
35985
36555
  const rel = [];
@@ -36020,7 +36590,7 @@ async function isFile(p) {
36020
36590
  async function walkAstro(dir) {
36021
36591
  let entries;
36022
36592
  try {
36023
- entries = await readdir7(dir, { withFileTypes: true });
36593
+ entries = await readdir8(dir, { withFileTypes: true });
36024
36594
  } catch {
36025
36595
  return [];
36026
36596
  }
@@ -36146,7 +36716,7 @@ async function critiqueOne(projectRoot, slug, brand, references) {
36146
36716
  }
36147
36717
  async function listLandingSlugs(projectRoot) {
36148
36718
  try {
36149
- const entries = await readdir8(path29.join(projectRoot, "src", "pages"), { withFileTypes: true });
36719
+ const entries = await readdir9(path29.join(projectRoot, "src", "pages"), { withFileTypes: true });
36150
36720
  return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
36151
36721
  } catch {
36152
36722
  return [];
@@ -39669,7 +40239,7 @@ var ESCALATION = {
39669
40239
  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
40240
  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
40241
  };
39672
- function fetchHints({
40242
+ function fetchHints2({
39673
40243
  truncated,
39674
40244
  contentChars,
39675
40245
  shownChars
@@ -40153,7 +40723,7 @@ Examples:
40153
40723
  content
40154
40724
  },
40155
40725
  fields: FIELDS6,
40156
- hints: fetchHints({
40726
+ hints: fetchHints2({
40157
40727
  truncated: page.truncated,
40158
40728
  contentChars: page.contentChars,
40159
40729
  shownChars: content.length