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