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