@tendrilapp/cli 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tendril-mcp.js +35 -11
- package/dist/tendril.js +375 -237
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -1008,6 +1008,13 @@ function ingestEnvelope(setDir, slug, tool, payload) {
|
|
|
1008
1008
|
`);
|
|
1009
1009
|
return { overwrote };
|
|
1010
1010
|
}
|
|
1011
|
+
function assetUrlsFromEnvelopeText(text) {
|
|
1012
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
1013
|
+
for (const m of text.matchAll(/const\s+\w+\s*=\s*"(https?:\/\/[^"]+\/assets?\/([a-z0-9-]+)\.(svg|png))"/gi)) {
|
|
1014
|
+
byUrl.set(m[1], `asset-${m[2]}.${m[3].toLowerCase()}`);
|
|
1015
|
+
}
|
|
1016
|
+
return [...byUrl].map(([url, name]) => ({ url, name }));
|
|
1017
|
+
}
|
|
1011
1018
|
function ingestAsset(setDir, slug, name, content) {
|
|
1012
1019
|
const manifest = loadManifest(setDir);
|
|
1013
1020
|
if (!manifest.reps.some((r) => r.slug === slug)) throw new Error(`unknown rep "${slug}" \u2014 not in the planned manifest`);
|
|
@@ -1789,7 +1796,7 @@ var init_browser = __esm({
|
|
|
1789
1796
|
});
|
|
1790
1797
|
|
|
1791
1798
|
// packages/verify/src/runtime.ts
|
|
1792
|
-
import { mkdtempSync, symlinkSync } from "node:fs";
|
|
1799
|
+
import { existsSync as existsSync4, mkdtempSync, symlinkSync } from "node:fs";
|
|
1793
1800
|
import os from "node:os";
|
|
1794
1801
|
import path4 from "node:path";
|
|
1795
1802
|
import { fileURLToPath } from "node:url";
|
|
@@ -1799,13 +1806,25 @@ function runtimePackageRoot() {
|
|
|
1799
1806
|
return path4.resolve(path4.dirname(fileURLToPath(import.meta.url)), "..");
|
|
1800
1807
|
}
|
|
1801
1808
|
function runtimeNodeModules() {
|
|
1802
|
-
|
|
1809
|
+
const root = runtimePackageRoot();
|
|
1810
|
+
for (let dir = root; ; dir = path4.dirname(dir)) {
|
|
1811
|
+
const candidate = path4.basename(dir) === "node_modules" ? dir : path4.join(dir, "node_modules");
|
|
1812
|
+
if (existsSync4(path4.join(candidate, "react"))) return candidate;
|
|
1813
|
+
if (path4.dirname(dir) === dir) break;
|
|
1814
|
+
}
|
|
1815
|
+
throw new Error(
|
|
1816
|
+
`Tendril's installed dependencies are missing: no node_modules containing react found at or above ${root}. This is an INSTALLATION problem, not a component error \u2014 reinstall with \`npm install -g @tendrilapp/cli\` (or \`tendrilapp\`) and retry.`
|
|
1817
|
+
);
|
|
1803
1818
|
}
|
|
1804
1819
|
function newScratchDir(prefix) {
|
|
1805
1820
|
const dir = mkdtempSync(path4.join(os.tmpdir(), `tendril-${prefix}-`));
|
|
1821
|
+
const nodeModules = runtimeNodeModules();
|
|
1806
1822
|
try {
|
|
1807
|
-
symlinkSync(
|
|
1808
|
-
} catch {
|
|
1823
|
+
symlinkSync(nodeModules, path4.join(dir, "node_modules"), "junction");
|
|
1824
|
+
} catch (err) {
|
|
1825
|
+
throw new Error(
|
|
1826
|
+
`Tendril could not link its dependencies into the scratch dir (${nodeModules} -> ${dir}): ${err instanceof Error ? err.message : String(err)}. This is an environment problem, not a component error.`
|
|
1827
|
+
);
|
|
1809
1828
|
}
|
|
1810
1829
|
return dir;
|
|
1811
1830
|
}
|
|
@@ -1968,7 +1987,11 @@ var init_token_lint = __esm({
|
|
|
1968
1987
|
"unset",
|
|
1969
1988
|
"none",
|
|
1970
1989
|
"auto",
|
|
1971
|
-
"0"
|
|
1990
|
+
"0",
|
|
1991
|
+
// Positional/structural keywords, not themable values: a fully
|
|
1992
|
+
// tokenised `box-shadow: var(--x) inset` was flagged for the literal
|
|
1993
|
+
// `inset` (first Windows component run).
|
|
1994
|
+
"inset"
|
|
1972
1995
|
];
|
|
1973
1996
|
STYLELINT_CONFIG = {
|
|
1974
1997
|
// The MODULE, not the name string: stylelint resolves plugin strings
|
|
@@ -3074,7 +3097,7 @@ var init_paths = __esm({
|
|
|
3074
3097
|
|
|
3075
3098
|
// packages/verify/src/font-resolve.ts
|
|
3076
3099
|
import { createHash } from "node:crypto";
|
|
3077
|
-
import { existsSync as
|
|
3100
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3078
3101
|
import os2 from "node:os";
|
|
3079
3102
|
import path9 from "node:path";
|
|
3080
3103
|
function fontCacheDir() {
|
|
@@ -3082,21 +3105,32 @@ function fontCacheDir() {
|
|
|
3082
3105
|
if (env !== void 0 && env !== "") return path9.resolve(env);
|
|
3083
3106
|
return path9.join(os2.homedir(), ".tendril", "fonts");
|
|
3084
3107
|
}
|
|
3108
|
+
function googleQueryCandidates(family) {
|
|
3109
|
+
const stripped = family.replace(/\s+(Variable|VF)$/i, "");
|
|
3110
|
+
return stripped === family ? [family] : [family, stripped];
|
|
3111
|
+
}
|
|
3085
3112
|
async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3086
3113
|
mkdirSync2(cacheDir, { recursive: true });
|
|
3087
3114
|
const resolved = [];
|
|
3088
3115
|
const failures = [];
|
|
3089
|
-
let css;
|
|
3116
|
+
let css = null;
|
|
3117
|
+
let lastStatus = 0;
|
|
3090
3118
|
try {
|
|
3091
|
-
const
|
|
3092
|
-
|
|
3093
|
-
|
|
3119
|
+
for (const queryName of googleQueryCandidates(family)) {
|
|
3120
|
+
const cssUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(queryName).replace(/%20/g, "+")}:wght@${weights.join(";")}&display=swap`;
|
|
3121
|
+
const res = await fetch(cssUrl, { headers: { "User-Agent": UA } });
|
|
3122
|
+
if (res.ok) {
|
|
3123
|
+
css = await res.text();
|
|
3124
|
+
break;
|
|
3125
|
+
}
|
|
3126
|
+
lastStatus = res.status;
|
|
3127
|
+
}
|
|
3128
|
+
if (css === null) {
|
|
3094
3129
|
return {
|
|
3095
3130
|
resolved,
|
|
3096
|
-
failures: weights.map((weight) => ({ family, weight, reason: `Google Fonts returned HTTP ${
|
|
3131
|
+
failures: weights.map((weight) => ({ family, weight, reason: `Google Fonts returned HTTP ${lastStatus} \u2014 family not available there? For a licensed or private face use \`tendril fonts add\` with a local file` }))
|
|
3097
3132
|
};
|
|
3098
3133
|
}
|
|
3099
|
-
css = await res.text();
|
|
3100
3134
|
} catch (err) {
|
|
3101
3135
|
return { resolved, failures: weights.map((weight) => ({ family, weight, reason: `offline or fetch failed: ${err instanceof Error ? err.message : String(err)}` })) };
|
|
3102
3136
|
}
|
|
@@ -3124,7 +3158,7 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3124
3158
|
}
|
|
3125
3159
|
}
|
|
3126
3160
|
const mPath = path9.join(cacheDir, "manifest.json");
|
|
3127
|
-
const prior =
|
|
3161
|
+
const prior = existsSync5(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3128
3162
|
const portable2 = resolved.map((m) => ({ ...m, file: path9.basename(m.file) }));
|
|
3129
3163
|
const merged = [...prior.filter((p) => !resolved.some((m) => m.family === p.family && m.weight === p.weight)), ...portable2];
|
|
3130
3164
|
if (resolved.length > 0) writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
@@ -3133,7 +3167,7 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3133
3167
|
}
|
|
3134
3168
|
function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3135
3169
|
const src = path9.resolve(filePath);
|
|
3136
|
-
if (!
|
|
3170
|
+
if (!existsSync5(src)) throw new Error(`font file not found: ${src}`);
|
|
3137
3171
|
const ext = path9.extname(src).toLowerCase();
|
|
3138
3172
|
if (![".woff2", ".woff", ".ttf", ".otf"].includes(ext)) {
|
|
3139
3173
|
throw new Error(`unsupported font file "${ext}" \u2014 use .woff2 (preferred), .woff, .ttf or .otf`);
|
|
@@ -3146,7 +3180,7 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3146
3180
|
writeFileSync3(file, bytes);
|
|
3147
3181
|
const face = { family, weight, source: `local:${path9.basename(src)}`, sha256, file };
|
|
3148
3182
|
const mPath = path9.join(cacheDir, "manifest.json");
|
|
3149
|
-
const prior =
|
|
3183
|
+
const prior = existsSync5(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3150
3184
|
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path9.basename(file) }];
|
|
3151
3185
|
writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
3152
3186
|
`);
|
|
@@ -3155,7 +3189,7 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3155
3189
|
function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3156
3190
|
const lock = JSON.parse(readFileSync3(lockPath, "utf8"));
|
|
3157
3191
|
const mPath = path9.join(cacheDir, "manifest.json");
|
|
3158
|
-
const manifest =
|
|
3192
|
+
const manifest = existsSync5(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3159
3193
|
return lock.map((l) => {
|
|
3160
3194
|
const m = manifest.find((x) => x.family === l.family && x.weight === l.weight);
|
|
3161
3195
|
if (m === void 0) return { family: l.family, weight: l.weight, status: "missing" };
|
|
@@ -3164,7 +3198,7 @@ function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3164
3198
|
}
|
|
3165
3199
|
function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3166
3200
|
const mPath = path9.join(cacheDir, "manifest.json");
|
|
3167
|
-
if (!
|
|
3201
|
+
if (!existsSync5(mPath)) return [];
|
|
3168
3202
|
let entries;
|
|
3169
3203
|
try {
|
|
3170
3204
|
entries = JSON.parse(readFileSync3(mPath, "utf8"));
|
|
@@ -3182,7 +3216,7 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3182
3216
|
}
|
|
3183
3217
|
function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3184
3218
|
const mPath = path9.join(cacheDir, "manifest.json");
|
|
3185
|
-
const manifest =
|
|
3219
|
+
const manifest = existsSync5(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3186
3220
|
return manifest.filter((f) => families.includes(f.family));
|
|
3187
3221
|
}
|
|
3188
3222
|
var DEFAULT_FONT_CACHE, UA;
|
|
@@ -3195,13 +3229,13 @@ var init_font_resolve = __esm({
|
|
|
3195
3229
|
});
|
|
3196
3230
|
|
|
3197
3231
|
// packages/verify/src/font-faces.ts
|
|
3198
|
-
import { existsSync as
|
|
3232
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
3199
3233
|
import path10 from "node:path";
|
|
3200
3234
|
function fontFaceCss(manifestPath2 = path10.join(fontCacheDir(), "manifest.json")) {
|
|
3201
|
-
if (!
|
|
3235
|
+
if (!existsSync6(manifestPath2)) return "";
|
|
3202
3236
|
const manifest = JSON.parse(readFileSync4(manifestPath2, "utf8"));
|
|
3203
3237
|
const resolveFile = (f) => {
|
|
3204
|
-
if (path10.isAbsolute(f) &&
|
|
3238
|
+
if (path10.isAbsolute(f) && existsSync6(f)) return f;
|
|
3205
3239
|
return path10.resolve(path10.dirname(manifestPath2), path10.basename(f));
|
|
3206
3240
|
};
|
|
3207
3241
|
const byFile = /* @__PURE__ */ new Map();
|
|
@@ -3245,7 +3279,7 @@ var init_mount_limits = __esm({
|
|
|
3245
3279
|
});
|
|
3246
3280
|
|
|
3247
3281
|
// packages/verify/src/admission.ts
|
|
3248
|
-
import { readFileSync as readFileSync5, readdirSync as readdirSync2, existsSync as
|
|
3282
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync2, existsSync as existsSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3249
3283
|
import path11 from "node:path";
|
|
3250
3284
|
import { build as build2 } from "esbuild";
|
|
3251
3285
|
import postcss from "postcss";
|
|
@@ -3254,7 +3288,7 @@ import { chromium as chromium2 } from "playwright-core";
|
|
|
3254
3288
|
function fontWeightsByFamily() {
|
|
3255
3289
|
const mPath = path11.join(fontCacheDir(), "manifest.json");
|
|
3256
3290
|
const out = /* @__PURE__ */ new Map();
|
|
3257
|
-
if (!
|
|
3291
|
+
if (!existsSync7(mPath)) return out;
|
|
3258
3292
|
for (const f of JSON.parse(readFileSync5(mPath, "utf8")))
|
|
3259
3293
|
out.set(f.family, [...out.get(f.family) ?? [], f.weight]);
|
|
3260
3294
|
return out;
|
|
@@ -3477,7 +3511,7 @@ __export(behavior_exports, {
|
|
|
3477
3511
|
compileMount: () => compileMount,
|
|
3478
3512
|
recordingIsDark: () => recordingIsDark
|
|
3479
3513
|
});
|
|
3480
|
-
import { existsSync as
|
|
3514
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
|
|
3481
3515
|
import path13 from "node:path";
|
|
3482
3516
|
import { build as build3 } from "esbuild";
|
|
3483
3517
|
import { chromium as chromium3 } from "playwright-core";
|
|
@@ -3488,7 +3522,7 @@ function getFontFaces() {
|
|
|
3488
3522
|
}
|
|
3489
3523
|
async function compileMount(task, bundleDir) {
|
|
3490
3524
|
const entryTsx = path13.join(bundleDir, task.entry);
|
|
3491
|
-
if (!
|
|
3525
|
+
if (!existsSync8(entryTsx)) return { error: `${task.entry} missing` };
|
|
3492
3526
|
const mountSrc = `
|
|
3493
3527
|
import { createElement } from "react";
|
|
3494
3528
|
import { createRoot } from "react-dom/client";
|
|
@@ -3701,7 +3735,7 @@ function recordingIsDark(task) {
|
|
|
3701
3735
|
const rep = task.configs[0]?.rep;
|
|
3702
3736
|
if (rep === void 0) return false;
|
|
3703
3737
|
const f = path13.join(task.set, rep, "get_screenshot.json");
|
|
3704
|
-
if (!
|
|
3738
|
+
if (!existsSync8(f)) return false;
|
|
3705
3739
|
try {
|
|
3706
3740
|
const env = JSON.parse(readFileSync6(f, "utf8")).content.find((c) => c.type === "image");
|
|
3707
3741
|
if (env?.data === void 0) return false;
|
|
@@ -3775,7 +3809,7 @@ async function checkBehaviors(task, bundleDir, opts = {}) {
|
|
|
3775
3809
|
const deadlineMs = timeoutMs + 1e4;
|
|
3776
3810
|
const js = await compileMount(task, bundleDir);
|
|
3777
3811
|
if (typeof js !== "string") return task.behaviors.map((b) => ({ id: b.id, pass: false, detail: js.error }));
|
|
3778
|
-
const css = ["tokens.css", "styles.css"].map((f) => path13.join(bundleDir, f)).filter((f) =>
|
|
3812
|
+
const css = ["tokens.css", "styles.css"].map((f) => path13.join(bundleDir, f)).filter((f) => existsSync8(f)).map((f) => readFileSync6(f, "utf8")).join("\n");
|
|
3779
3813
|
const server = await chromium3.launchServer({ executablePath: resolveChrome(), headless: true, args: [MEMORY_CAP_ARG] });
|
|
3780
3814
|
const browser = await chromium3.connect(server.wsEndpoint());
|
|
3781
3815
|
const results = [];
|
|
@@ -3918,7 +3952,7 @@ var init_behavior = __esm({
|
|
|
3918
3952
|
});
|
|
3919
3953
|
|
|
3920
3954
|
// packages/verify/src/bundle-quality.ts
|
|
3921
|
-
import { readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as
|
|
3955
|
+
import { readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync9 } from "node:fs";
|
|
3922
3956
|
import path14 from "node:path";
|
|
3923
3957
|
function definedVars(tokensCss) {
|
|
3924
3958
|
if (tokensCss === void 0) return void 0;
|
|
@@ -3931,9 +3965,9 @@ async function checkBundleQuality(bundleDir, entry) {
|
|
|
3931
3965
|
const entryPath = path14.join(bundleDir, entry);
|
|
3932
3966
|
const cssPath = path14.join(bundleDir, "styles.css");
|
|
3933
3967
|
const tokensPath = path14.join(bundleDir, "tokens.css");
|
|
3934
|
-
const css =
|
|
3935
|
-
const tokensCss =
|
|
3936
|
-
if (
|
|
3968
|
+
const css = existsSync9(cssPath) ? readFileSync7(cssPath, "utf8") : "";
|
|
3969
|
+
const tokensCss = existsSync9(tokensPath) ? readFileSync7(tokensPath, "utf8") : void 0;
|
|
3970
|
+
if (existsSync9(entryPath)) {
|
|
3937
3971
|
const workDir = newScratchDir("quality");
|
|
3938
3972
|
try {
|
|
3939
3973
|
const tsxPath = path14.join(workDir, entry);
|
|
@@ -4000,7 +4034,7 @@ var init_effect_geometry = __esm({
|
|
|
4000
4034
|
});
|
|
4001
4035
|
|
|
4002
4036
|
// packages/verify/src/bundle-score.ts
|
|
4003
|
-
import { existsSync as
|
|
4037
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4004
4038
|
import path15 from "node:path";
|
|
4005
4039
|
import { build as build4 } from "esbuild";
|
|
4006
4040
|
import { chromium as chromium4 } from "playwright-core";
|
|
@@ -4064,7 +4098,7 @@ function repRef(set, rep) {
|
|
|
4064
4098
|
}
|
|
4065
4099
|
function repEffectExtents(set, rep) {
|
|
4066
4100
|
const file = path15.join(set, rep, "get_design_context.json");
|
|
4067
|
-
if (!
|
|
4101
|
+
if (!existsSync10(file)) return void 0;
|
|
4068
4102
|
try {
|
|
4069
4103
|
const text = JSON.parse(readFileSync8(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4070
4104
|
const extents = shadowExtents(text);
|
|
@@ -4078,8 +4112,8 @@ async function scoreBundleForTask(task, bundleDir, bar = BAR, opts = {}) {
|
|
|
4078
4112
|
const CONFIGS2 = task.configs;
|
|
4079
4113
|
const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
|
|
4080
4114
|
const entryTsx = path15.join(bundleDir, task.entry);
|
|
4081
|
-
if (!
|
|
4082
|
-
const css = ["tokens.css", "styles.css"].map((f) => path15.join(bundleDir, f)).filter((f) =>
|
|
4115
|
+
if (!existsSync10(entryTsx)) return CONFIGS2.map((c) => ({ rep: c.rep, similarity: 0, inkRecall: 0, exact: { similarity: 0, inkRecall: 0 }, pass: false, error: `${task.entry} missing` }));
|
|
4116
|
+
const css = ["tokens.css", "styles.css"].map((f) => path15.join(bundleDir, f)).filter((f) => existsSync10(f)).map((f) => readFileSync8(f, "utf8")).join("\n");
|
|
4083
4117
|
const mountSrc = `
|
|
4084
4118
|
import { createElement } from "react";
|
|
4085
4119
|
import { createRoot } from "react-dom/client";
|
|
@@ -4281,7 +4315,7 @@ var init_prelude = __esm({
|
|
|
4281
4315
|
});
|
|
4282
4316
|
|
|
4283
4317
|
// packages/verify/src/parity.ts
|
|
4284
|
-
import { existsSync as
|
|
4318
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
|
|
4285
4319
|
import path16 from "node:path";
|
|
4286
4320
|
import { chromium as chromium6 } from "playwright-core";
|
|
4287
4321
|
function getFontFaces3() {
|
|
@@ -4311,7 +4345,7 @@ async function checkHoverParity(task, bundleDir, opts = {}) {
|
|
|
4311
4345
|
const deadlineMs = timeoutMs + 1e4;
|
|
4312
4346
|
const js = await compileMount(task, bundleDir);
|
|
4313
4347
|
if (typeof js !== "string") return configs.map((c) => ({ id: `parity:${c.rep}`, pass: false, detail: js.error }));
|
|
4314
|
-
const css = ["tokens.css", "styles.css"].map((f) => path16.join(bundleDir, f)).filter((f) =>
|
|
4348
|
+
const css = ["tokens.css", "styles.css"].map((f) => path16.join(bundleDir, f)).filter((f) => existsSync11(f)).map((f) => readFileSync9(f, "utf8")).join("\n");
|
|
4315
4349
|
const server = await chromium6.launchServer({ executablePath: resolveChrome(), headless: true, args: [MEMORY_CAP_ARG] });
|
|
4316
4350
|
const browser = await chromium6.connect(server.wsEndpoint());
|
|
4317
4351
|
const results = [];
|
|
@@ -4391,7 +4425,7 @@ var init_parity = __esm({
|
|
|
4391
4425
|
|
|
4392
4426
|
// packages/verify/src/composition.ts
|
|
4393
4427
|
import { createRequire } from "node:module";
|
|
4394
|
-
import { existsSync as
|
|
4428
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
|
|
4395
4429
|
import path17 from "node:path";
|
|
4396
4430
|
import { build as build6 } from "esbuild";
|
|
4397
4431
|
import { chromium as chromium7 } from "playwright-core";
|
|
@@ -4401,7 +4435,7 @@ function getFontFaces4() {
|
|
|
4401
4435
|
}
|
|
4402
4436
|
async function compileInstrumentedMount(task, bundleDir) {
|
|
4403
4437
|
const entryTsx = path17.join(bundleDir, task.entry);
|
|
4404
|
-
if (!
|
|
4438
|
+
if (!existsSync12(entryTsx)) return { error: `${task.entry} missing` };
|
|
4405
4439
|
const requireFromVerify = createRequire(path17.join(VERIFY_PKG_DIR, "package.json"));
|
|
4406
4440
|
let realJsxPath;
|
|
4407
4441
|
try {
|
|
@@ -4465,7 +4499,7 @@ function expectedParts(task, roles, mainSlug) {
|
|
|
4465
4499
|
}
|
|
4466
4500
|
function interiorRegions(setDir, roles) {
|
|
4467
4501
|
const mains = roles.main;
|
|
4468
|
-
const withInterior = mains.filter((m) =>
|
|
4502
|
+
const withInterior = mains.filter((m) => existsSync12(path17.join(setDir, m, "get_metadata_interior.json")));
|
|
4469
4503
|
if (withInterior.length === 0) {
|
|
4470
4504
|
return { unavailable: "no interior geometry recorded for any main (pre-obligation set) \u2014 re-record with `tendril record` to enable crop checks" };
|
|
4471
4505
|
}
|
|
@@ -4482,7 +4516,7 @@ async function checkCropComposition(task, bundleDir, regions, opts = {}) {
|
|
|
4482
4516
|
if (typeof js !== "string") {
|
|
4483
4517
|
return regions.map((r) => ({ id: cropId(r), pass: false, similarity: 0, inkRecall: 0, detail: js.error }));
|
|
4484
4518
|
}
|
|
4485
|
-
const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) =>
|
|
4519
|
+
const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) => existsSync12(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
|
|
4486
4520
|
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: [MEMORY_CAP_ARG] });
|
|
4487
4521
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
4488
4522
|
const PAD = 4;
|
|
@@ -4587,7 +4621,7 @@ async function checkStructuralComposition(task, bundleDir, roles, opts = {}) {
|
|
|
4587
4621
|
const deadlineMs = timeoutMs + 1e4;
|
|
4588
4622
|
const js = await compileInstrumentedMount(task, bundleDir);
|
|
4589
4623
|
if (typeof js !== "string") return [...results, ...mains.map((m) => ({ id: `composition:${m}`, pass: false, detail: js.error }))];
|
|
4590
|
-
const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) =>
|
|
4624
|
+
const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) => existsSync12(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
|
|
4591
4625
|
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: [MEMORY_CAP_ARG] });
|
|
4592
4626
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
4593
4627
|
try {
|
|
@@ -4677,13 +4711,13 @@ export const jsxs = (t, p, k) => real.jsxs(t, inject(t, p), k);
|
|
|
4677
4711
|
});
|
|
4678
4712
|
|
|
4679
4713
|
// packages/verify/src/occlusion.ts
|
|
4680
|
-
import { existsSync as
|
|
4714
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
4681
4715
|
import path18 from "node:path";
|
|
4682
4716
|
import { build as build7 } from "esbuild";
|
|
4683
4717
|
import { chromium as chromium8 } from "playwright-core";
|
|
4684
4718
|
async function compileTwoUp(task, bundleDir) {
|
|
4685
4719
|
const entryTsx = path18.join(bundleDir, task.entry);
|
|
4686
|
-
if (!
|
|
4720
|
+
if (!existsSync13(entryTsx)) return { error: `${task.entry} missing` };
|
|
4687
4721
|
const src = `
|
|
4688
4722
|
import { createElement } from "react";
|
|
4689
4723
|
import { createRoot } from "react-dom/client";
|
|
@@ -4862,9 +4896,48 @@ var init_src4 = __esm({
|
|
|
4862
4896
|
}
|
|
4863
4897
|
});
|
|
4864
4898
|
|
|
4865
|
-
// packages/cli/src/
|
|
4866
|
-
import { existsSync as
|
|
4899
|
+
// packages/cli/src/environment.ts
|
|
4900
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
|
|
4867
4901
|
import path19 from "node:path";
|
|
4902
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
4903
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
4904
|
+
function cliVersion() {
|
|
4905
|
+
try {
|
|
4906
|
+
return JSON.parse(readFileSync11(path19.join(path19.dirname(fileURLToPath4(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
|
|
4907
|
+
} catch {
|
|
4908
|
+
return "dev";
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
4911
|
+
function environmentStamp(taskFamilies) {
|
|
4912
|
+
const manifestPath2 = path19.join(fontCacheDir(), "manifest.json");
|
|
4913
|
+
let fontsHash = null;
|
|
4914
|
+
if (existsSync14(manifestPath2)) {
|
|
4915
|
+
try {
|
|
4916
|
+
const entries = JSON.parse(readFileSync11(manifestPath2, "utf8"));
|
|
4917
|
+
const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
|
|
4918
|
+
const faces = scoped.map((f) => `${f.family}:${f.weight}:${f.sha256}`).sort();
|
|
4919
|
+
fontsHash = faces.length === 0 ? null : createHash2("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
|
|
4920
|
+
} catch {
|
|
4921
|
+
fontsHash = null;
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
let chrome = "unavailable";
|
|
4925
|
+
try {
|
|
4926
|
+
chrome = resolveChrome();
|
|
4927
|
+
} catch {
|
|
4928
|
+
}
|
|
4929
|
+
return { chrome, chromeVersion: chrome === "unavailable" ? null : chromeVersion(), fontsManifestSha256: fontsHash };
|
|
4930
|
+
}
|
|
4931
|
+
var init_environment = __esm({
|
|
4932
|
+
"packages/cli/src/environment.ts"() {
|
|
4933
|
+
"use strict";
|
|
4934
|
+
init_src4();
|
|
4935
|
+
}
|
|
4936
|
+
});
|
|
4937
|
+
|
|
4938
|
+
// packages/cli/src/env.ts
|
|
4939
|
+
import { existsSync as existsSync15, readFileSync as readFileSync12 } from "node:fs";
|
|
4940
|
+
import path20 from "node:path";
|
|
4868
4941
|
function parseEnv(content) {
|
|
4869
4942
|
const entries = /* @__PURE__ */ new Map();
|
|
4870
4943
|
for (const line of content.split("\n")) {
|
|
@@ -4876,9 +4949,9 @@ function parseEnv(content) {
|
|
|
4876
4949
|
function resolveCredential(name) {
|
|
4877
4950
|
const fromProcess = process.env[name];
|
|
4878
4951
|
if (fromProcess) return fromProcess;
|
|
4879
|
-
const envPath =
|
|
4880
|
-
if (!
|
|
4881
|
-
return parseEnv(
|
|
4952
|
+
const envPath = path20.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
|
|
4953
|
+
if (!existsSync15(envPath)) return void 0;
|
|
4954
|
+
return parseEnv(readFileSync12(envPath, "utf8")).get(name);
|
|
4882
4955
|
}
|
|
4883
4956
|
var init_env = __esm({
|
|
4884
4957
|
"packages/cli/src/env.ts"() {
|
|
@@ -6133,6 +6206,8 @@ var record_exports = {};
|
|
|
6133
6206
|
__export(record_exports, {
|
|
6134
6207
|
instanceLeads: () => instanceLeads,
|
|
6135
6208
|
isFigmaAssetUrl: () => isFigmaAssetUrl,
|
|
6209
|
+
isLocalAssetUrl: () => isLocalAssetUrl,
|
|
6210
|
+
nextPayload: () => nextPayload,
|
|
6136
6211
|
runRecordAsset: () => runRecordAsset,
|
|
6137
6212
|
runRecordFetch: () => runRecordFetch,
|
|
6138
6213
|
runRecordFinish: () => runRecordFinish,
|
|
@@ -6141,11 +6216,11 @@ __export(record_exports, {
|
|
|
6141
6216
|
runRecordPlan: () => runRecordPlan,
|
|
6142
6217
|
runRecordStatus: () => runRecordStatus
|
|
6143
6218
|
});
|
|
6144
|
-
import { existsSync as
|
|
6145
|
-
import
|
|
6219
|
+
import { existsSync as existsSync18, readFileSync as readFileSync15, readdirSync as readdirSync4 } from "node:fs";
|
|
6220
|
+
import path24 from "node:path";
|
|
6146
6221
|
import { writeFileSync as writeFileSync8 } from "node:fs";
|
|
6147
6222
|
function symbolsFromMetadataEnvelope(file, sourceFrame) {
|
|
6148
|
-
const env = JSON.parse(
|
|
6223
|
+
const env = JSON.parse(readFileSync15(file, "utf8"));
|
|
6149
6224
|
const text = env.content.map((c) => c.text ?? "").join("\n");
|
|
6150
6225
|
const symbols = [];
|
|
6151
6226
|
const walk2 = (node, ancestor) => {
|
|
@@ -6189,7 +6264,7 @@ function runRecordPlan(opts) {
|
|
|
6189
6264
|
for (const spec of opts.metadataFiles) {
|
|
6190
6265
|
const [file, frame] = spec.split("@");
|
|
6191
6266
|
try {
|
|
6192
|
-
symbols.push(...symbolsFromMetadataEnvelope(
|
|
6267
|
+
symbols.push(...symbolsFromMetadataEnvelope(path24.resolve(file), frame));
|
|
6193
6268
|
} catch (err) {
|
|
6194
6269
|
fail(opts, ExitCode.InputValidation, {
|
|
6195
6270
|
error: `could not read metadata envelope ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -6221,7 +6296,7 @@ function runRecordPlan(opts) {
|
|
|
6221
6296
|
const leads = opts.metadataFiles.flatMap((spec) => {
|
|
6222
6297
|
const [file] = spec.split("@");
|
|
6223
6298
|
try {
|
|
6224
|
-
const env = JSON.parse(
|
|
6299
|
+
const env = JSON.parse(readFileSync15(path24.resolve(file), "utf8"));
|
|
6225
6300
|
return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
|
|
6226
6301
|
} catch {
|
|
6227
6302
|
return [];
|
|
@@ -6296,27 +6371,28 @@ function runRecordPlan(opts) {
|
|
|
6296
6371
|
}
|
|
6297
6372
|
);
|
|
6298
6373
|
}
|
|
6299
|
-
function
|
|
6300
|
-
const instruction = nextInstruction(
|
|
6301
|
-
const status = sessionStatus(
|
|
6374
|
+
function nextPayload(setDir) {
|
|
6375
|
+
const instruction = nextInstruction(setDir);
|
|
6376
|
+
const status = sessionStatus(setDir);
|
|
6302
6377
|
const progress = { recordedReps: status.reps.filter((x) => x.missing.length === 0).length, totalReps: status.reps.length };
|
|
6303
|
-
if (instruction === null && !
|
|
6304
|
-
const manifest = loadManifest(
|
|
6378
|
+
if (instruction === null && !existsSync18(path24.join(setDir, "get_variable_defs.json"))) {
|
|
6379
|
+
const manifest = loadManifest(setDir);
|
|
6305
6380
|
const frameNode = Object.keys(manifest.sourceFrames ?? {})[0] ?? manifest.reps[0]?.nodeId ?? "";
|
|
6306
|
-
|
|
6307
|
-
process.stdout.write(`get_variable_defs (SET-LEVEL) on node ${frameNode} \u2192 ingest with --rep __set__
|
|
6308
|
-
`);
|
|
6309
|
-
});
|
|
6310
|
-
return;
|
|
6381
|
+
return { slug: "__set__", nodeId: frameNode, tool: "get_variable_defs", note: `SET-LEVEL: call get_variable_defs on the component frame and ingest with --rep __set__. ${ENVELOPE_HELP}`, progress };
|
|
6311
6382
|
}
|
|
6312
|
-
|
|
6313
|
-
|
|
6383
|
+
return instruction === null ? { complete: true, progress } : { ...instruction, note: `${instruction.note}. ${ENVELOPE_HELP}`, progress };
|
|
6384
|
+
}
|
|
6385
|
+
function runRecordNext(opts) {
|
|
6386
|
+
const payload = nextPayload(opts.setDir);
|
|
6387
|
+
emitData(opts, payload, () => {
|
|
6388
|
+
if (payload["complete"] === true) {
|
|
6314
6389
|
process.stdout.write("set complete \u2014 every planned rep has its required envelopes\n");
|
|
6315
6390
|
return;
|
|
6316
6391
|
}
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
\u2192
|
|
6392
|
+
const progress = payload["progress"];
|
|
6393
|
+
process.stdout.write(`[${progress.recordedReps}/${progress.totalReps} reps] ${payload["tool"]} for ${payload["slug"]} (node ${payload["nodeId"]})
|
|
6394
|
+
\u2192 ${payload["note"]}
|
|
6395
|
+
\u2192 then: tendril record ingest --set <dir> --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>
|
|
6320
6396
|
`);
|
|
6321
6397
|
});
|
|
6322
6398
|
}
|
|
@@ -6331,6 +6407,29 @@ function isFigmaAssetUrl(raw) {
|
|
|
6331
6407
|
if (u.username !== "" || u.password !== "") return false;
|
|
6332
6408
|
return u.hostname === "figma.com" || u.hostname.endsWith(".figma.com");
|
|
6333
6409
|
}
|
|
6410
|
+
function isLocalAssetUrl(raw) {
|
|
6411
|
+
let u;
|
|
6412
|
+
try {
|
|
6413
|
+
u = new URL(raw);
|
|
6414
|
+
} catch {
|
|
6415
|
+
return false;
|
|
6416
|
+
}
|
|
6417
|
+
if (u.username !== "" || u.password !== "") return false;
|
|
6418
|
+
return (u.protocol === "http:" || u.protocol === "https:") && (u.hostname === "localhost" || u.hostname === "127.0.0.1");
|
|
6419
|
+
}
|
|
6420
|
+
async function fetchAssetBytes(startUrl, allowed) {
|
|
6421
|
+
let url = startUrl;
|
|
6422
|
+
let res = await fetch(url, { redirect: "manual" });
|
|
6423
|
+
for (let hop = 0; res.status >= 300 && res.status < 400 && hop < 5; hop++) {
|
|
6424
|
+
const next = res.headers.get("location");
|
|
6425
|
+
if (next === null) break;
|
|
6426
|
+
url = new URL(next, url).toString();
|
|
6427
|
+
if (!allowed(url)) throw new Error(`redirect left the allowed hosts: ${url}`);
|
|
6428
|
+
res = await fetch(url, { redirect: "manual" });
|
|
6429
|
+
}
|
|
6430
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
6431
|
+
return Buffer.from(await res.arrayBuffer());
|
|
6432
|
+
}
|
|
6334
6433
|
async function runRecordFetch(opts) {
|
|
6335
6434
|
if (!isFigmaAssetUrl(opts.url)) {
|
|
6336
6435
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -6341,17 +6440,7 @@ async function runRecordFetch(opts) {
|
|
|
6341
6440
|
}
|
|
6342
6441
|
let bytes;
|
|
6343
6442
|
try {
|
|
6344
|
-
|
|
6345
|
-
let res = await fetch(url, { redirect: "manual" });
|
|
6346
|
-
for (let hop = 0; res.status >= 300 && res.status < 400 && hop < 5; hop++) {
|
|
6347
|
-
const next = res.headers.get("location");
|
|
6348
|
-
if (next === null) break;
|
|
6349
|
-
url = new URL(next, url).toString();
|
|
6350
|
-
if (!isFigmaAssetUrl(url)) throw new Error(`redirect left Figma: ${url}`);
|
|
6351
|
-
res = await fetch(url, { redirect: "manual" });
|
|
6352
|
-
}
|
|
6353
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
6354
|
-
bytes = Buffer.from(await res.arrayBuffer());
|
|
6443
|
+
bytes = await fetchAssetBytes(opts.url, isFigmaAssetUrl);
|
|
6355
6444
|
} catch (err) {
|
|
6356
6445
|
fail(opts, ExitCode.InputValidation, {
|
|
6357
6446
|
error: `asset fetch failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -6369,7 +6458,7 @@ async function runRecordFetch(opts) {
|
|
|
6369
6458
|
const payload = { content: [{ type: "image", data: bytes.toString("base64"), mimeType: "image/png" }] };
|
|
6370
6459
|
try {
|
|
6371
6460
|
const { overwrote } = ingestEnvelope(opts.setDir, opts.rep, opts.tool, payload);
|
|
6372
|
-
emitData(opts, { rep: opts.rep, tool: opts.tool, bytes: bytes.length, overwrote, source: "cli-fetch" }, () => {
|
|
6461
|
+
emitData(opts, { rep: opts.rep, tool: opts.tool, bytes: bytes.length, overwrote, source: "cli-fetch", next: nextPayload(opts.setDir) }, () => {
|
|
6373
6462
|
process.stdout.write(`fetched + ingested ${opts.rep}/${opts.tool} (${bytes.length} bytes, no model in the byte path)
|
|
6374
6463
|
`);
|
|
6375
6464
|
});
|
|
@@ -6381,15 +6470,47 @@ async function runRecordFetch(opts) {
|
|
|
6381
6470
|
});
|
|
6382
6471
|
}
|
|
6383
6472
|
}
|
|
6384
|
-
function
|
|
6473
|
+
async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
6474
|
+
const fetched = [];
|
|
6475
|
+
const skipped = [];
|
|
6476
|
+
const failed = [];
|
|
6477
|
+
for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
|
|
6478
|
+
if (existsSync18(path24.join(setDir, rep, name))) {
|
|
6479
|
+
skipped.push(name);
|
|
6480
|
+
continue;
|
|
6481
|
+
}
|
|
6482
|
+
if (!isAutoFetchAssetUrl(url)) {
|
|
6483
|
+
failed.push({ name, url, reason: "host not on the auto-fetch allowlist (Figma hosts and the desktop app's localhost only)" });
|
|
6484
|
+
continue;
|
|
6485
|
+
}
|
|
6486
|
+
try {
|
|
6487
|
+
const bytes = await fetchAssetBytes(url, isAutoFetchAssetUrl);
|
|
6488
|
+
if (name.endsWith(".svg") && !bytes.toString("utf8", 0, 512).includes("<svg")) throw new Error("response is not an SVG (expired URL usually returns HTML)");
|
|
6489
|
+
if (name.endsWith(".png") && !(bytes.length > 8 && bytes[0] === 137 && bytes[1] === 80)) throw new Error("response is not a PNG (expired URL usually returns HTML)");
|
|
6490
|
+
ingestAsset(setDir, rep, name, bytes);
|
|
6491
|
+
fetched.push(name);
|
|
6492
|
+
} catch (err) {
|
|
6493
|
+
failed.push({ name, url, reason: err instanceof Error ? err.message : String(err) });
|
|
6494
|
+
}
|
|
6495
|
+
}
|
|
6496
|
+
return { fetched, skipped, failed };
|
|
6497
|
+
}
|
|
6498
|
+
async function runRecordIngest(opts) {
|
|
6385
6499
|
let payload;
|
|
6386
6500
|
try {
|
|
6387
|
-
payload = JSON.parse(
|
|
6501
|
+
payload = opts.raw === true ? { content: [{ type: "text", text: readFileSync15(path24.resolve(opts.file), "utf8") }] } : JSON.parse(readFileSync15(path24.resolve(opts.file), "utf8"));
|
|
6388
6502
|
} catch (err) {
|
|
6389
6503
|
fail(opts, ExitCode.InputValidation, {
|
|
6390
6504
|
error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
|
|
6391
6505
|
code: "envelope-unreadable",
|
|
6392
|
-
remediation: "Pass the tool response saved verbatim as JSON."
|
|
6506
|
+
remediation: "Pass the tool response saved verbatim: as JSON, or as raw text with --raw."
|
|
6507
|
+
});
|
|
6508
|
+
}
|
|
6509
|
+
if (opts.raw === true && opts.tool === "get_screenshot") {
|
|
6510
|
+
fail(opts, ExitCode.InputValidation, {
|
|
6511
|
+
error: "--raw is for text tool responses; screenshots are binary",
|
|
6512
|
+
code: "envelope-invalid",
|
|
6513
|
+
remediation: "Use `tendril record fetch` with the image_url instead."
|
|
6393
6514
|
});
|
|
6394
6515
|
}
|
|
6395
6516
|
if (opts.rep === "__set__" && opts.tool === "get_variable_defs") {
|
|
@@ -6401,18 +6522,25 @@ function runRecordIngest(opts) {
|
|
|
6401
6522
|
remediation: "Save the get_variable_defs response verbatim as a text envelope."
|
|
6402
6523
|
});
|
|
6403
6524
|
}
|
|
6404
|
-
writeFileSync8(
|
|
6525
|
+
writeFileSync8(path24.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
|
|
6405
6526
|
`);
|
|
6406
|
-
emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true }, () => {
|
|
6527
|
+
emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
|
|
6407
6528
|
process.stdout.write("set-level get_variable_defs ingested\n");
|
|
6408
6529
|
});
|
|
6409
6530
|
return;
|
|
6410
6531
|
}
|
|
6411
6532
|
try {
|
|
6412
6533
|
const { overwrote } = ingestEnvelope(opts.setDir, opts.rep, opts.tool, payload);
|
|
6413
|
-
|
|
6534
|
+
const assets = opts.tool === "get_design_context" ? await autoFetchAssets(opts.setDir, opts.rep, payload.content.map((c) => c.text ?? "").join("\n")) : void 0;
|
|
6535
|
+
emitData(opts, { rep: opts.rep, tool: opts.tool, overwrote, ...assets !== void 0 ? { assets } : {}, next: nextPayload(opts.setDir) }, () => {
|
|
6414
6536
|
process.stdout.write(`ingested ${opts.rep}/${opts.tool}${overwrote ? " (overwrote prior recording)" : ""}
|
|
6415
6537
|
`);
|
|
6538
|
+
if (assets !== void 0) {
|
|
6539
|
+
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
6540
|
+
`);
|
|
6541
|
+
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it and run tendril record asset
|
|
6542
|
+
`);
|
|
6543
|
+
}
|
|
6416
6544
|
});
|
|
6417
6545
|
} catch (err) {
|
|
6418
6546
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -6423,8 +6551,44 @@ function runRecordIngest(opts) {
|
|
|
6423
6551
|
}
|
|
6424
6552
|
}
|
|
6425
6553
|
function runRecordAsset(opts) {
|
|
6554
|
+
if (opts.dir !== void 0) {
|
|
6555
|
+
const dir = path24.resolve(opts.dir);
|
|
6556
|
+
const names = readdirSync4(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
|
|
6557
|
+
if (names.length === 0) {
|
|
6558
|
+
fail(opts, ExitCode.InputValidation, {
|
|
6559
|
+
error: `no asset-*.<ext> files found in ${dir}`,
|
|
6560
|
+
code: "asset-rejected",
|
|
6561
|
+
remediation: "Name downloaded assets asset-<id>.<ext> (the id from the asset URL), or pass --name/--file for a single asset."
|
|
6562
|
+
});
|
|
6563
|
+
}
|
|
6564
|
+
const ingested = [];
|
|
6565
|
+
try {
|
|
6566
|
+
for (const name of names) {
|
|
6567
|
+
ingestAsset(opts.setDir, opts.rep, name, readFileSync15(path24.join(dir, name)));
|
|
6568
|
+
ingested.push(name);
|
|
6569
|
+
}
|
|
6570
|
+
} catch (err) {
|
|
6571
|
+
fail(opts, ExitCode.InputValidation, {
|
|
6572
|
+
error: `after ${ingested.length} ingested: ${err instanceof Error ? err.message : String(err)}`,
|
|
6573
|
+
code: "asset-rejected",
|
|
6574
|
+
remediation: "Assets must be named asset-<id>.<ext>, size-capped, and SVGs must carry no active content."
|
|
6575
|
+
});
|
|
6576
|
+
}
|
|
6577
|
+
emitData(opts, { rep: opts.rep, assets: ingested }, () => {
|
|
6578
|
+
for (const name of ingested) process.stdout.write(`ingested ${opts.rep}/${name}
|
|
6579
|
+
`);
|
|
6580
|
+
});
|
|
6581
|
+
return;
|
|
6582
|
+
}
|
|
6583
|
+
if (opts.name === void 0 || opts.file === void 0) {
|
|
6584
|
+
fail(opts, ExitCode.InputValidation, {
|
|
6585
|
+
error: "pass --name and --file for a single asset, or --dir for a batch",
|
|
6586
|
+
code: "asset-rejected",
|
|
6587
|
+
remediation: "tendril record asset --set <dir> --rep <slug> --dir <downloads-dir>"
|
|
6588
|
+
});
|
|
6589
|
+
}
|
|
6426
6590
|
try {
|
|
6427
|
-
ingestAsset(opts.setDir, opts.rep, opts.name,
|
|
6591
|
+
ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync15(path24.resolve(opts.file)));
|
|
6428
6592
|
emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
|
|
6429
6593
|
process.stdout.write(`ingested ${opts.rep}/${opts.name}
|
|
6430
6594
|
`);
|
|
@@ -6452,7 +6616,7 @@ ${status.reps.filter((r) => r.missing.length > 0).length} rep(s) pending
|
|
|
6452
6616
|
function runRecordFinish(opts) {
|
|
6453
6617
|
const manifest = loadManifest(opts.setDir);
|
|
6454
6618
|
const derived = deriveRoles(opts.setDir, manifest);
|
|
6455
|
-
const roles = opts.rolesFile !== void 0 ? { ...JSON.parse(
|
|
6619
|
+
const roles = opts.rolesFile !== void 0 ? { ...JSON.parse(readFileSync15(path24.resolve(opts.rolesFile), "utf8")), humanOverride: true } : { main: derived.main, parts: derived.parts, external: derived.external, humanOverride: false };
|
|
6456
6620
|
emitData(opts, { derived, confirmed: opts.confirmRoles }, () => {
|
|
6457
6621
|
process.stdout.write(`derived mains: ${derived.main.join(", ") || "(none)"}
|
|
6458
6622
|
`);
|
|
@@ -6479,11 +6643,11 @@ function runRecordFinish(opts) {
|
|
|
6479
6643
|
});
|
|
6480
6644
|
}
|
|
6481
6645
|
const updated = { ...manifest, roles };
|
|
6482
|
-
writeFileSync8(
|
|
6646
|
+
writeFileSync8(path24.join(opts.setDir, "recording-set.json"), `${JSON.stringify(updated, null, 1)}
|
|
6483
6647
|
`);
|
|
6484
6648
|
if (!opts.json) process.stdout.write("roles written to recording-set.json\n");
|
|
6485
6649
|
}
|
|
6486
|
-
var ENVELOPE_HELP;
|
|
6650
|
+
var ENVELOPE_HELP, isAutoFetchAssetUrl;
|
|
6487
6651
|
var init_record = __esm({
|
|
6488
6652
|
"packages/cli/src/commands/record.ts"() {
|
|
6489
6653
|
"use strict";
|
|
@@ -6491,6 +6655,7 @@ var init_record = __esm({
|
|
|
6491
6655
|
init_src();
|
|
6492
6656
|
init_output();
|
|
6493
6657
|
ENVELOPE_HELP = 'Envelope format: text tools save {"content":[{"type":"text","text":"<VERBATIM response text incl. any Currently-selected-nodes block>"}]}; get_screenshot: do NOT download the image yourself \u2014 pass its image_url to `tendril record fetch` (MCP: tendril_record_fetch), which pulls the bytes to disk directly. That keeps the pixel ground truth out of your context and costs one approval instead of a shell command per asset. Assets are asset-<first-8-hex-of-figma-uuid>.<ext>; their URLs appear as const declarations inside the design-context text and also expire.';
|
|
6658
|
+
isAutoFetchAssetUrl = (url) => isFigmaAssetUrl(url) || isLocalAssetUrl(url);
|
|
6494
6659
|
}
|
|
6495
6660
|
});
|
|
6496
6661
|
|
|
@@ -6681,8 +6846,8 @@ var init_engine_curated = __esm({
|
|
|
6681
6846
|
});
|
|
6682
6847
|
|
|
6683
6848
|
// packages/generate/src/loop.ts
|
|
6684
|
-
import { existsSync as
|
|
6685
|
-
import
|
|
6849
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync5, readFileSync as readFileSync16, renameSync, writeFileSync as writeFileSync9 } from "node:fs";
|
|
6850
|
+
import path25 from "node:path";
|
|
6686
6851
|
import { z as z11 } from "zod";
|
|
6687
6852
|
function objective(scores, behaviors) {
|
|
6688
6853
|
const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
|
|
@@ -6713,9 +6878,9 @@ ${preludeLines.join("\n")}` : ""}
|
|
|
6713
6878
|
Fix the FAIL configs (region coordinates are in the recorded screenshot's frame; ink<1 means recorded foreground pixels your render does not cover). Do not regress PASS configs. ${mode === "files" ? "Update the files in your candidate directory and re-run the score." : "Reply with the complete corrected files in the same FILE format."}`;
|
|
6714
6879
|
}
|
|
6715
6880
|
function archivePriorRun(outDir) {
|
|
6716
|
-
if (!
|
|
6881
|
+
if (!existsSync19(path25.join(outDir, "run-log.json")) && !existsSync19(path25.join(outDir, "loop-state.json"))) return void 0;
|
|
6717
6882
|
let n = 1;
|
|
6718
|
-
while (
|
|
6883
|
+
while (existsSync19(`${outDir}-prev-${n}`)) n += 1;
|
|
6719
6884
|
renameSync(outDir, `${outDir}-prev-${n}`);
|
|
6720
6885
|
return `${outDir}-prev-${n}`;
|
|
6721
6886
|
}
|
|
@@ -6724,14 +6889,14 @@ async function runEngineLoop(opts) {
|
|
|
6724
6889
|
const plateau = opts.plateau ?? 2;
|
|
6725
6890
|
const progress = opts.onProgress ?? (() => {
|
|
6726
6891
|
});
|
|
6727
|
-
const statePath =
|
|
6728
|
-
const resuming = opts.resume === true &&
|
|
6892
|
+
const statePath = path25.join(opts.outDir, "loop-state.json");
|
|
6893
|
+
const resuming = opts.resume === true && existsSync19(statePath);
|
|
6729
6894
|
if (!resuming) {
|
|
6730
6895
|
const archived = archivePriorRun(opts.outDir);
|
|
6731
6896
|
if (archived !== void 0) progress(`previous run archived to ${archived}`);
|
|
6732
6897
|
}
|
|
6733
6898
|
mkdirSync5(opts.outDir, { recursive: true });
|
|
6734
|
-
const scratch =
|
|
6899
|
+
const scratch = path25.join(opts.outDir, ".candidate");
|
|
6735
6900
|
let attempts = [];
|
|
6736
6901
|
let log = [];
|
|
6737
6902
|
let best;
|
|
@@ -6739,7 +6904,7 @@ async function runEngineLoop(opts) {
|
|
|
6739
6904
|
let nonAccepted = 0;
|
|
6740
6905
|
let stopReason = "max-iterations";
|
|
6741
6906
|
if (resuming) {
|
|
6742
|
-
const restored = LoopStateSchema.parse(JSON.parse(
|
|
6907
|
+
const restored = LoopStateSchema.parse(JSON.parse(readFileSync16(statePath, "utf8")));
|
|
6743
6908
|
attempts = restored.attempts;
|
|
6744
6909
|
log = restored.iterations;
|
|
6745
6910
|
spentUsd = restored.spentUsd;
|
|
@@ -6759,7 +6924,7 @@ async function runEngineLoop(opts) {
|
|
|
6759
6924
|
};
|
|
6760
6925
|
const writeCandidate = (files) => {
|
|
6761
6926
|
mkdirSync5(scratch, { recursive: true });
|
|
6762
|
-
for (const [name, content] of Object.entries(files)) writeFileSync9(
|
|
6927
|
+
for (const [name, content] of Object.entries(files)) writeFileSync9(path25.join(scratch, name), content);
|
|
6763
6928
|
};
|
|
6764
6929
|
const scoreCandidate = async (candidate, iter, usd, modelMs) => {
|
|
6765
6930
|
writeCandidate(candidate.files);
|
|
@@ -6817,8 +6982,8 @@ async function runEngineLoop(opts) {
|
|
|
6817
6982
|
const usd = candidate.usage?.usd ?? 0;
|
|
6818
6983
|
spentUsd += usd;
|
|
6819
6984
|
if (candidate.raw !== void 0) {
|
|
6820
|
-
mkdirSync5(
|
|
6821
|
-
writeFileSync9(
|
|
6985
|
+
mkdirSync5(path25.join(opts.outDir, "responses"), { recursive: true });
|
|
6986
|
+
writeFileSync9(path25.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
|
|
6822
6987
|
}
|
|
6823
6988
|
if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
|
|
6824
6989
|
const finish = candidate.usage?.finishReason ?? "?";
|
|
@@ -6844,10 +7009,10 @@ async function runEngineLoop(opts) {
|
|
|
6844
7009
|
}
|
|
6845
7010
|
}
|
|
6846
7011
|
}
|
|
6847
|
-
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync9(
|
|
7012
|
+
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync9(path25.join(opts.outDir, name), content);
|
|
6848
7013
|
const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
|
|
6849
7014
|
writeFileSync9(
|
|
6850
|
-
|
|
7015
|
+
path25.join(opts.outDir, "run-log.json"),
|
|
6851
7016
|
`${JSON.stringify(
|
|
6852
7017
|
{
|
|
6853
7018
|
...opts.meta,
|
|
@@ -6914,8 +7079,8 @@ var init_loop2 = __esm({
|
|
|
6914
7079
|
});
|
|
6915
7080
|
|
|
6916
7081
|
// packages/generate/src/brief.ts
|
|
6917
|
-
import { existsSync as
|
|
6918
|
-
import
|
|
7082
|
+
import { existsSync as existsSync20, readFileSync as readFileSync17 } from "node:fs";
|
|
7083
|
+
import path26 from "node:path";
|
|
6919
7084
|
function singleAxes2(name) {
|
|
6920
7085
|
const parsed = parseVariantAxes(name);
|
|
6921
7086
|
if (parsed === void 0) return void 0;
|
|
@@ -7111,7 +7276,7 @@ function authorBehaviors(api) {
|
|
|
7111
7276
|
return { behaviors, prelude: { controls: interactive ? ["> *"] : [], textInputs: [] }, disclosures };
|
|
7112
7277
|
}
|
|
7113
7278
|
function envelopeText(file) {
|
|
7114
|
-
return JSON.parse(
|
|
7279
|
+
return JSON.parse(readFileSync17(file, "utf8")).content[0]?.text ?? "";
|
|
7115
7280
|
}
|
|
7116
7281
|
function recordedFontNeeds(setDir) {
|
|
7117
7282
|
const byFamily = /* @__PURE__ */ new Map();
|
|
@@ -7146,13 +7311,13 @@ function recordedFontNeeds(setDir) {
|
|
|
7146
7311
|
}
|
|
7147
7312
|
};
|
|
7148
7313
|
const manifest = loadManifest(setDir);
|
|
7149
|
-
const setDefs =
|
|
7150
|
-
if (
|
|
7314
|
+
const setDefs = path26.join(setDir, "get_variable_defs.json");
|
|
7315
|
+
if (existsSync20(setDefs)) fromDefs(envelopeText(setDefs));
|
|
7151
7316
|
for (const rep of manifest.reps) {
|
|
7152
|
-
const ctx =
|
|
7153
|
-
if (
|
|
7154
|
-
const defs =
|
|
7155
|
-
if (
|
|
7317
|
+
const ctx = path26.join(setDir, rep.slug, "get_design_context.json");
|
|
7318
|
+
if (existsSync20(ctx)) fromEmission(envelopeText(ctx));
|
|
7319
|
+
const defs = path26.join(setDir, rep.slug, "get_variable_defs.json");
|
|
7320
|
+
if (existsSync20(defs)) fromDefs(envelopeText(defs));
|
|
7156
7321
|
}
|
|
7157
7322
|
return [...byFamily.entries()].map(([family, paired]) => {
|
|
7158
7323
|
const weights = /* @__PURE__ */ new Set([...paired, ...unpaired]);
|
|
@@ -7171,8 +7336,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
7171
7336
|
const poses = [];
|
|
7172
7337
|
const missing = [];
|
|
7173
7338
|
for (const rep of manifest.reps) {
|
|
7174
|
-
const metaFile =
|
|
7175
|
-
if (!
|
|
7339
|
+
const metaFile = path26.join(setDir, rep.slug, "get_metadata.json");
|
|
7340
|
+
if (!existsSync20(metaFile)) {
|
|
7176
7341
|
missing.push(rep.slug);
|
|
7177
7342
|
continue;
|
|
7178
7343
|
}
|
|
@@ -7186,8 +7351,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
7186
7351
|
if (missing.length > 0) {
|
|
7187
7352
|
throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
|
|
7188
7353
|
}
|
|
7189
|
-
const setMeta =
|
|
7190
|
-
const latticeNames = manifest.latticeNames ?? (
|
|
7354
|
+
const setMeta = path26.join(setDir, "get_metadata.json");
|
|
7355
|
+
const latticeNames = manifest.latticeNames ?? (existsSync20(setMeta) ? [...envelopeText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
|
|
7191
7356
|
const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
|
|
7192
7357
|
const recordedFonts = recordedFontFamilies(setDir);
|
|
7193
7358
|
const api = authorComponentApi({
|
|
@@ -7299,10 +7464,10 @@ var init_brief = __esm({
|
|
|
7299
7464
|
});
|
|
7300
7465
|
|
|
7301
7466
|
// packages/generate/src/segments.ts
|
|
7302
|
-
import { existsSync as
|
|
7303
|
-
import
|
|
7467
|
+
import { existsSync as existsSync21, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "node:fs";
|
|
7468
|
+
import path27 from "node:path";
|
|
7304
7469
|
function repText(set, rep, tool) {
|
|
7305
|
-
return JSON.parse(
|
|
7470
|
+
return JSON.parse(readFileSync18(path27.join(set, rep, `${tool}.json`), "utf8")).content[0]?.text ?? "";
|
|
7306
7471
|
}
|
|
7307
7472
|
function stripFigmaInstructions(emission) {
|
|
7308
7473
|
const STYLE_FACTS = "These styles are contained in the design:";
|
|
@@ -7362,17 +7527,17 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
|
|
|
7362
7527
|
function buildSegments(task, mode = "fenced") {
|
|
7363
7528
|
const SET = task.set;
|
|
7364
7529
|
let rawDefs = {};
|
|
7365
|
-
if (
|
|
7366
|
-
const text = JSON.parse(
|
|
7530
|
+
if (existsSync21(path27.join(SET, "get_variable_defs.json"))) {
|
|
7531
|
+
const text = JSON.parse(readFileSync18(path27.join(SET, "get_variable_defs.json"), "utf8")).content[0]?.text ?? "{}";
|
|
7367
7532
|
try {
|
|
7368
7533
|
rawDefs = JSON.parse(text);
|
|
7369
7534
|
} catch {
|
|
7370
7535
|
}
|
|
7371
7536
|
} else {
|
|
7372
7537
|
for (const cfg of task.configs) {
|
|
7373
|
-
const f =
|
|
7374
|
-
if (!
|
|
7375
|
-
const text = JSON.parse(
|
|
7538
|
+
const f = path27.join(SET, cfg.rep, "get_variable_defs.json");
|
|
7539
|
+
if (!existsSync21(f)) continue;
|
|
7540
|
+
const text = JSON.parse(readFileSync18(f, "utf8")).content[0]?.text ?? "{}";
|
|
7376
7541
|
try {
|
|
7377
7542
|
for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
|
|
7378
7543
|
} catch {
|
|
@@ -7380,8 +7545,8 @@ function buildSegments(task, mode = "fenced") {
|
|
|
7380
7545
|
}
|
|
7381
7546
|
}
|
|
7382
7547
|
const emissionTexts = task.configs.map((cfg) => {
|
|
7383
|
-
const f =
|
|
7384
|
-
return
|
|
7548
|
+
const f = path27.join(SET, cfg.rep, "get_design_context.json");
|
|
7549
|
+
return existsSync21(f) ? JSON.parse(readFileSync18(f, "utf8")).content[0]?.text ?? "" : "";
|
|
7385
7550
|
});
|
|
7386
7551
|
const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
|
|
7387
7552
|
const defs = JSON.stringify(map, null, 1);
|
|
@@ -7396,9 +7561,9 @@ ${defs}
|
|
|
7396
7561
|
for (const cfg of task.configs) {
|
|
7397
7562
|
const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
|
|
7398
7563
|
const emission = stripFigmaInstructions(repText(SET, cfg.rep, "get_design_context"));
|
|
7399
|
-
const assets =
|
|
7564
|
+
const assets = readdirSync5(path27.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
|
|
7400
7565
|
\`\`\`svg
|
|
7401
|
-
${
|
|
7566
|
+
${readFileSync18(path27.join(SET, cfg.rep, f), "utf8")}
|
|
7402
7567
|
\`\`\``).join("\n");
|
|
7403
7568
|
parts.push(`
|
|
7404
7569
|
## Config ${cfg.rep} \u2192 ${cfg.component} ${JSON.stringify(cfg.props)}
|
|
@@ -7489,9 +7654,9 @@ var init_adapter = __esm({
|
|
|
7489
7654
|
});
|
|
7490
7655
|
|
|
7491
7656
|
// packages/generate/src/bundle-emit.ts
|
|
7492
|
-
import { createHash as
|
|
7493
|
-
import { existsSync as
|
|
7494
|
-
import
|
|
7657
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
7658
|
+
import { existsSync as existsSync22, readFileSync as readFileSync19, readdirSync as readdirSync6, writeFileSync as writeFileSync10 } from "node:fs";
|
|
7659
|
+
import path28 from "node:path";
|
|
7495
7660
|
function pinFromConfigs(configs) {
|
|
7496
7661
|
const domains = /* @__PURE__ */ new Map();
|
|
7497
7662
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -7540,22 +7705,22 @@ function cssFontFamilies(css) {
|
|
|
7540
7705
|
return [...out];
|
|
7541
7706
|
}
|
|
7542
7707
|
function countLatticeSymbols(setDir) {
|
|
7543
|
-
const manifestFile =
|
|
7544
|
-
if (
|
|
7708
|
+
const manifestFile = path28.join(setDir, "recording-set.json");
|
|
7709
|
+
if (existsSync22(manifestFile)) {
|
|
7545
7710
|
try {
|
|
7546
|
-
const lattice = JSON.parse(
|
|
7711
|
+
const lattice = JSON.parse(readFileSync19(manifestFile, "utf8")).latticeNames;
|
|
7547
7712
|
if (lattice !== void 0 && lattice.length > 0) return lattice.length;
|
|
7548
7713
|
} catch {
|
|
7549
7714
|
}
|
|
7550
7715
|
}
|
|
7551
7716
|
const files = [
|
|
7552
|
-
|
|
7553
|
-
...
|
|
7554
|
-
].filter((f) =>
|
|
7717
|
+
path28.join(setDir, "get_metadata.json"),
|
|
7718
|
+
...existsSync22(setDir) ? readdirSync6(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path28.join(setDir, f)) : []
|
|
7719
|
+
].filter((f) => existsSync22(f));
|
|
7555
7720
|
if (files.length === 0) return null;
|
|
7556
7721
|
let count = 0;
|
|
7557
7722
|
for (const f of files) {
|
|
7558
|
-
const text = JSON.parse(
|
|
7723
|
+
const text = JSON.parse(readFileSync19(f, "utf8")).content[0]?.text ?? "";
|
|
7559
7724
|
count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
|
|
7560
7725
|
}
|
|
7561
7726
|
return count > 0 ? count : null;
|
|
@@ -7563,23 +7728,23 @@ function countLatticeSymbols(setDir) {
|
|
|
7563
7728
|
function recordingSetHash(setDir, configs) {
|
|
7564
7729
|
const relPaths = [];
|
|
7565
7730
|
for (const name of ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json"]) {
|
|
7566
|
-
if (
|
|
7731
|
+
if (existsSync22(path28.join(setDir, name))) relPaths.push(name);
|
|
7567
7732
|
}
|
|
7568
7733
|
for (const cfg of configs) {
|
|
7569
7734
|
for (const f of ["get_design_context.json", "get_metadata.json", "get_screenshot.json", "get_variable_defs.json"]) {
|
|
7570
|
-
if (
|
|
7735
|
+
if (existsSync22(path28.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
|
|
7571
7736
|
}
|
|
7572
|
-
if (
|
|
7573
|
-
for (const asset of
|
|
7737
|
+
if (existsSync22(path28.join(setDir, cfg.rep))) {
|
|
7738
|
+
for (const asset of readdirSync6(path28.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
|
|
7574
7739
|
relPaths.push(`${cfg.rep}/${asset}`);
|
|
7575
7740
|
}
|
|
7576
7741
|
}
|
|
7577
7742
|
}
|
|
7578
7743
|
return hashRecordingSet(
|
|
7579
7744
|
relPaths,
|
|
7580
|
-
(p) => new Uint8Array(
|
|
7745
|
+
(p) => new Uint8Array(readFileSync19(path28.join(setDir, p))),
|
|
7581
7746
|
(chunks) => {
|
|
7582
|
-
const h =
|
|
7747
|
+
const h = createHash3("sha256");
|
|
7583
7748
|
for (const c of chunks) h.update(c);
|
|
7584
7749
|
return h.digest("hex");
|
|
7585
7750
|
}
|
|
@@ -7595,8 +7760,8 @@ function emitBundleV1(opts) {
|
|
|
7595
7760
|
const lattice = countLatticeSymbols(opts.task.set);
|
|
7596
7761
|
const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:"));
|
|
7597
7762
|
const prelude = opts.behaviors.filter((b) => b.id.startsWith("prelude:"));
|
|
7598
|
-
const cssFiles = ["styles.css", "tokens.css"].map((f) =>
|
|
7599
|
-
const families = cssFontFamilies(cssFiles.map((f) =>
|
|
7763
|
+
const cssFiles = ["styles.css", "tokens.css"].map((f) => path28.join(opts.bundleDir, f)).filter((f) => existsSync22(f));
|
|
7764
|
+
const families = cssFontFamilies(cssFiles.map((f) => readFileSync19(f, "utf8")).join("\n"));
|
|
7600
7765
|
const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
|
|
7601
7766
|
family: f.family,
|
|
7602
7767
|
weight: f.weight,
|
|
@@ -7625,7 +7790,7 @@ function emitBundleV1(opts) {
|
|
|
7625
7790
|
// resolvable via verify's --set override).
|
|
7626
7791
|
path: (() => {
|
|
7627
7792
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
7628
|
-
const rel =
|
|
7793
|
+
const rel = path28.relative(base, opts.task.set);
|
|
7629
7794
|
return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
|
|
7630
7795
|
})(),
|
|
7631
7796
|
component: opts.componentName,
|
|
@@ -7649,14 +7814,14 @@ function emitBundleV1(opts) {
|
|
|
7649
7814
|
})
|
|
7650
7815
|
};
|
|
7651
7816
|
const written = [];
|
|
7652
|
-
const manifestPath2 =
|
|
7817
|
+
const manifestPath2 = path28.join(opts.bundleDir, "component.json");
|
|
7653
7818
|
writeFileSync10(manifestPath2, `${JSON.stringify(manifest, null, 2)}
|
|
7654
7819
|
`);
|
|
7655
7820
|
written.push(manifestPath2);
|
|
7656
|
-
const stylesPath =
|
|
7657
|
-
if (
|
|
7821
|
+
const stylesPath = path28.join(opts.bundleDir, "styles.css");
|
|
7822
|
+
if (existsSync22(stylesPath)) {
|
|
7658
7823
|
const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
|
|
7659
|
-
const current =
|
|
7824
|
+
const current = readFileSync19(stylesPath, "utf8");
|
|
7660
7825
|
const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
|
|
7661
7826
|
writeFileSync10(stylesPath, `${comment}
|
|
7662
7827
|
${stripped}`);
|
|
@@ -7705,8 +7870,8 @@ __export(fonts_exports, {
|
|
|
7705
7870
|
runFontsResolveSet: () => runFontsResolveSet,
|
|
7706
7871
|
runFontsStatus: () => runFontsStatus
|
|
7707
7872
|
});
|
|
7708
|
-
import { existsSync as
|
|
7709
|
-
import
|
|
7873
|
+
import { existsSync as existsSync23, readFileSync as readFileSync20 } from "node:fs";
|
|
7874
|
+
import path29 from "node:path";
|
|
7710
7875
|
async function runFontsResolve(opts) {
|
|
7711
7876
|
const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
|
|
7712
7877
|
emitData(opts, result, () => {
|
|
@@ -7717,11 +7882,11 @@ async function runFontsResolve(opts) {
|
|
|
7717
7882
|
});
|
|
7718
7883
|
if (result.failures.length > 0) {
|
|
7719
7884
|
warn(opts, `${result.failures.length} face(s) unresolved \u2014 verification will refuse to score under substitution`);
|
|
7720
|
-
process.
|
|
7885
|
+
process.exitCode = ExitCode.FontsUnproven;
|
|
7721
7886
|
}
|
|
7722
7887
|
}
|
|
7723
7888
|
async function runFontsResolveSet(opts) {
|
|
7724
|
-
const setDir =
|
|
7889
|
+
const setDir = path29.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
|
|
7725
7890
|
let needs = [];
|
|
7726
7891
|
try {
|
|
7727
7892
|
needs = recordedFontNeeds(setDir);
|
|
@@ -7766,20 +7931,20 @@ async function runFontsResolveSet(opts) {
|
|
|
7766
7931
|
});
|
|
7767
7932
|
if (failures.length > 0) {
|
|
7768
7933
|
warn(opts, `${failures.length} face(s) unresolved \u2014 verification will refuse to score under substitution`);
|
|
7769
|
-
process.
|
|
7934
|
+
process.exitCode = ExitCode.FontsUnproven;
|
|
7770
7935
|
}
|
|
7771
7936
|
}
|
|
7772
7937
|
function runFontsStatus(opts) {
|
|
7773
|
-
const manifestPath2 =
|
|
7774
|
-
if (!
|
|
7938
|
+
const manifestPath2 = path29.join(opts.cacheDir, "manifest.json");
|
|
7939
|
+
if (!existsSync23(manifestPath2)) {
|
|
7775
7940
|
fail(opts, ExitCode.FontsUnproven, {
|
|
7776
7941
|
error: `no font cache at ${opts.cacheDir}`,
|
|
7777
7942
|
code: "fonts-unresolved",
|
|
7778
7943
|
remediation: 'Run `tendril fonts resolve --set <recording-dir>` (or `tendril fonts resolve "<Family>" --weights 400 500 600`) first.'
|
|
7779
7944
|
});
|
|
7780
7945
|
}
|
|
7781
|
-
const faces = JSON.parse(
|
|
7782
|
-
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(
|
|
7946
|
+
const faces = JSON.parse(readFileSync20(manifestPath2, "utf8"));
|
|
7947
|
+
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path29.resolve(opts.lock), opts.cacheDir) : null;
|
|
7783
7948
|
emitData(opts, { faces, lockVerdicts }, () => {
|
|
7784
7949
|
for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
|
|
7785
7950
|
`);
|
|
@@ -7828,37 +7993,6 @@ var init_fonts = __esm({
|
|
|
7828
7993
|
}
|
|
7829
7994
|
});
|
|
7830
7995
|
|
|
7831
|
-
// packages/cli/src/environment.ts
|
|
7832
|
-
import { existsSync as existsSync22, readFileSync as readFileSync20 } from "node:fs";
|
|
7833
|
-
import path29 from "node:path";
|
|
7834
|
-
import { createHash as createHash3 } from "node:crypto";
|
|
7835
|
-
function environmentStamp(taskFamilies) {
|
|
7836
|
-
const manifestPath2 = path29.join(fontCacheDir(), "manifest.json");
|
|
7837
|
-
let fontsHash = null;
|
|
7838
|
-
if (existsSync22(manifestPath2)) {
|
|
7839
|
-
try {
|
|
7840
|
-
const entries = JSON.parse(readFileSync20(manifestPath2, "utf8"));
|
|
7841
|
-
const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
|
|
7842
|
-
const faces = scoped.map((f) => `${f.family}:${f.weight}:${f.sha256}`).sort();
|
|
7843
|
-
fontsHash = faces.length === 0 ? null : createHash3("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
|
|
7844
|
-
} catch {
|
|
7845
|
-
fontsHash = null;
|
|
7846
|
-
}
|
|
7847
|
-
}
|
|
7848
|
-
let chrome = "unavailable";
|
|
7849
|
-
try {
|
|
7850
|
-
chrome = resolveChrome();
|
|
7851
|
-
} catch {
|
|
7852
|
-
}
|
|
7853
|
-
return { chrome, chromeVersion: chrome === "unavailable" ? null : chromeVersion(), fontsManifestSha256: fontsHash };
|
|
7854
|
-
}
|
|
7855
|
-
var init_environment = __esm({
|
|
7856
|
-
"packages/cli/src/environment.ts"() {
|
|
7857
|
-
"use strict";
|
|
7858
|
-
init_src4();
|
|
7859
|
-
}
|
|
7860
|
-
});
|
|
7861
|
-
|
|
7862
7996
|
// packages/cli/src/font-guidance.ts
|
|
7863
7997
|
function fontsUnprovenRemediation(setDir) {
|
|
7864
7998
|
if (setDir !== void 0) {
|
|
@@ -7895,7 +8029,7 @@ __export(verify_exports, {
|
|
|
7895
8029
|
interactionCoverage: () => interactionCoverage,
|
|
7896
8030
|
runVerify: () => runVerify
|
|
7897
8031
|
});
|
|
7898
|
-
import { existsSync as
|
|
8032
|
+
import { existsSync as existsSync24, readFileSync as readFileSync21 } from "node:fs";
|
|
7899
8033
|
import path30 from "node:path";
|
|
7900
8034
|
function interactionCoverage(behaviors) {
|
|
7901
8035
|
const interaction = behaviors.filter((b) => !b.id.startsWith("prelude:"));
|
|
@@ -7934,7 +8068,7 @@ async function runVerify(opts) {
|
|
|
7934
8068
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
7935
8069
|
const setOverride = opts.set !== void 0 ? path30.resolve(callerCwd, opts.set) : void 0;
|
|
7936
8070
|
opts = { ...opts, bundleDir: path30.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
|
|
7937
|
-
if (!
|
|
8071
|
+
if (!existsSync24(opts.bundleDir)) {
|
|
7938
8072
|
fail(opts, ExitCode.InputValidation, {
|
|
7939
8073
|
error: `bundle directory not found: ${opts.bundleDir}`,
|
|
7940
8074
|
code: "bundle-missing",
|
|
@@ -7943,7 +8077,7 @@ async function runVerify(opts) {
|
|
|
7943
8077
|
}
|
|
7944
8078
|
const manifestPath2 = path30.join(opts.bundleDir, "component.json");
|
|
7945
8079
|
let manifest;
|
|
7946
|
-
if (
|
|
8080
|
+
if (existsSync24(manifestPath2)) {
|
|
7947
8081
|
const { manifest: parsed, issues } = readBundleManifest(readFileSync21(manifestPath2, "utf8"));
|
|
7948
8082
|
if (issues.length > 0) {
|
|
7949
8083
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -7973,11 +8107,11 @@ async function runVerify(opts) {
|
|
|
7973
8107
|
const resolveSetDir = (p) => {
|
|
7974
8108
|
if (path30.isAbsolute(p)) return p;
|
|
7975
8109
|
const fromRepo = path30.resolve(REPO_ROOT, p);
|
|
7976
|
-
if (
|
|
8110
|
+
if (existsSync24(fromRepo)) return fromRepo;
|
|
7977
8111
|
return path30.resolve(callerCwd, p);
|
|
7978
8112
|
};
|
|
7979
8113
|
const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
|
|
7980
|
-
if (!
|
|
8114
|
+
if (!existsSync24(path30.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path30.resolve(t.set) === path30.resolve(setDir))) {
|
|
7981
8115
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
7982
8116
|
error: `recording set not found or unmanifested: ${setDir}`,
|
|
7983
8117
|
code: "recording-set-missing",
|
|
@@ -7985,7 +8119,7 @@ async function runVerify(opts) {
|
|
|
7985
8119
|
});
|
|
7986
8120
|
}
|
|
7987
8121
|
const registry = Object.values(TASKS).find((t) => path30.resolve(t.set) === path30.resolve(setDir));
|
|
7988
|
-
if (registry !== void 0 && !
|
|
8122
|
+
if (registry !== void 0 && !existsSync24(path30.join(setDir, "recording-set.json"))) {
|
|
7989
8123
|
const recordedSlugs = registry.configs.map((c) => c.rep);
|
|
7990
8124
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
7991
8125
|
unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
@@ -8010,7 +8144,7 @@ async function runVerify(opts) {
|
|
|
8010
8144
|
}
|
|
8011
8145
|
for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
|
|
8012
8146
|
const p = path30.join(opts.bundleDir, name);
|
|
8013
|
-
if (!
|
|
8147
|
+
if (!existsSync24(p)) continue;
|
|
8014
8148
|
const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync21(p)));
|
|
8015
8149
|
if (issues.length > 0) {
|
|
8016
8150
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -8035,7 +8169,7 @@ async function runVerify(opts) {
|
|
|
8035
8169
|
});
|
|
8036
8170
|
}
|
|
8037
8171
|
const missing = task.configs.filter(
|
|
8038
|
-
(c) => !
|
|
8172
|
+
(c) => !existsSync24(path30.join(task.set, c.rep, "get_screenshot.json")) || !existsSync24(path30.join(task.set, c.rep, "get_metadata.json"))
|
|
8039
8173
|
);
|
|
8040
8174
|
if (missing.length > 0) {
|
|
8041
8175
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -8048,7 +8182,7 @@ async function runVerify(opts) {
|
|
|
8048
8182
|
const evidenceDir = path30.join(opts.bundleDir, "verify-evidence");
|
|
8049
8183
|
const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
8050
8184
|
const quality = await checkBundleQuality(opts.bundleDir, task.entry);
|
|
8051
|
-
const bundleCss = ["tokens.css", "styles.css"].map((f) => path30.join(opts.bundleDir, f)).filter((f) =>
|
|
8185
|
+
const bundleCss = ["tokens.css", "styles.css"].map((f) => path30.join(opts.bundleDir, f)).filter((f) => existsSync24(f)).map((f) => readFileSync21(f, "utf8")).join("\n");
|
|
8052
8186
|
const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
|
|
8053
8187
|
const parity = await checkHoverParity(task, opts.bundleDir);
|
|
8054
8188
|
const behaviors = [...await checkBehaviors(task, opts.bundleDir), ...parity];
|
|
@@ -8236,11 +8370,11 @@ __export(engine_exports, {
|
|
|
8236
8370
|
runEngineBrief: () => runEngineBrief,
|
|
8237
8371
|
runEngineScore: () => runEngineScore
|
|
8238
8372
|
});
|
|
8239
|
-
import { existsSync as
|
|
8373
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync6, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "node:fs";
|
|
8240
8374
|
import path31 from "node:path";
|
|
8241
8375
|
function resolveEngineTask(opts, callerCwd) {
|
|
8242
8376
|
const asPath = path31.resolve(callerCwd, opts.taskOrSet);
|
|
8243
|
-
const isSet =
|
|
8377
|
+
const isSet = existsSync25(path31.join(asPath, "recording-set.json"));
|
|
8244
8378
|
const registry = TASKS[opts.taskOrSet];
|
|
8245
8379
|
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet };
|
|
8246
8380
|
if (isSet) {
|
|
@@ -8270,7 +8404,7 @@ function runEngineBrief(opts) {
|
|
|
8270
8404
|
const segments = buildSegments(task, "files");
|
|
8271
8405
|
let notRecorded;
|
|
8272
8406
|
const manifestPath2 = path31.join(task.set, "recording-set.json");
|
|
8273
|
-
if (
|
|
8407
|
+
if (existsSync25(manifestPath2)) {
|
|
8274
8408
|
notRecorded = JSON.parse(readFileSync22(manifestPath2, "utf8")).notRecorded;
|
|
8275
8409
|
}
|
|
8276
8410
|
const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
|
|
@@ -8278,7 +8412,7 @@ function runEngineBrief(opts) {
|
|
|
8278
8412
|
=== UNVERIFIED SURFACE (recorded-set disclosure \u2014 these poses were never recorded; nothing verifies them) ===
|
|
8279
8413
|
${notRecorded}` : "";
|
|
8280
8414
|
let fontProvisioning;
|
|
8281
|
-
if (
|
|
8415
|
+
if (existsSync25(manifestPath2)) {
|
|
8282
8416
|
const provided = resolvedFontFamilies().map((f) => f.family);
|
|
8283
8417
|
const unprovided = recordedFontFamilies(task.set).filter((r) => !provided.some((p) => p.toLowerCase() === r.toLowerCase()));
|
|
8284
8418
|
if (unprovided.length > 0) {
|
|
@@ -8347,7 +8481,7 @@ async function runEngineScore(opts) {
|
|
|
8347
8481
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
8348
8482
|
const candidateDir = path31.resolve(callerCwd, opts.candidateDir);
|
|
8349
8483
|
const { task, name, apiPin } = resolveEngineTask(opts, callerCwd);
|
|
8350
|
-
if (!
|
|
8484
|
+
if (!existsSync25(candidateDir)) {
|
|
8351
8485
|
fail(opts, ExitCode.InputValidation, {
|
|
8352
8486
|
error: `candidate directory not found: ${candidateDir}`,
|
|
8353
8487
|
code: "candidate-missing",
|
|
@@ -8469,7 +8603,7 @@ __export(generate_recorded_exports, {
|
|
|
8469
8603
|
runGenerateRecorded: () => runGenerateRecorded
|
|
8470
8604
|
});
|
|
8471
8605
|
import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
|
|
8472
|
-
import { existsSync as
|
|
8606
|
+
import { existsSync as existsSync26 } from "node:fs";
|
|
8473
8607
|
import path32 from "node:path";
|
|
8474
8608
|
async function runGenerateRecorded(opts) {
|
|
8475
8609
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
@@ -8479,7 +8613,7 @@ async function runGenerateRecorded(opts) {
|
|
|
8479
8613
|
let taskName;
|
|
8480
8614
|
let authoredApi;
|
|
8481
8615
|
let composition;
|
|
8482
|
-
const isSet =
|
|
8616
|
+
const isSet = existsSync26(path32.join(recordedAsPath, "recording-set.json"));
|
|
8483
8617
|
const registry = TASKS[opts.recorded];
|
|
8484
8618
|
if (registry !== void 0 && !isSet) {
|
|
8485
8619
|
task = registry;
|
|
@@ -8515,7 +8649,7 @@ async function runGenerateRecorded(opts) {
|
|
|
8515
8649
|
});
|
|
8516
8650
|
}
|
|
8517
8651
|
const missing = task.configs.filter(
|
|
8518
|
-
(c) => !
|
|
8652
|
+
(c) => !existsSync26(path32.join(task.set, c.rep, "get_screenshot.json")) || !existsSync26(path32.join(task.set, c.rep, "get_metadata.json")) || !existsSync26(path32.join(task.set, c.rep, "get_design_context.json"))
|
|
8519
8653
|
);
|
|
8520
8654
|
if (missing.length > 0) {
|
|
8521
8655
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -8728,13 +8862,14 @@ import { CommanderError } from "commander";
|
|
|
8728
8862
|
|
|
8729
8863
|
// packages/cli/src/program.ts
|
|
8730
8864
|
init_src3();
|
|
8865
|
+
init_environment();
|
|
8731
8866
|
import { Command } from "commander";
|
|
8732
8867
|
|
|
8733
8868
|
// packages/cli/src/commands/doctor.ts
|
|
8734
8869
|
init_src4();
|
|
8735
8870
|
init_src();
|
|
8736
|
-
import { existsSync as
|
|
8737
|
-
import
|
|
8871
|
+
import { existsSync as existsSync16, readFileSync as readFileSync13 } from "node:fs";
|
|
8872
|
+
import path21 from "node:path";
|
|
8738
8873
|
|
|
8739
8874
|
// packages/cli/src/describe.ts
|
|
8740
8875
|
var COMMON_EXIT_CODES = {
|
|
@@ -8751,6 +8886,7 @@ function printDescription(description) {
|
|
|
8751
8886
|
|
|
8752
8887
|
// packages/cli/src/commands/doctor.ts
|
|
8753
8888
|
init_env();
|
|
8889
|
+
init_environment();
|
|
8754
8890
|
init_output();
|
|
8755
8891
|
var DEFAULT_MCP_URL = "http://127.0.0.1:3845/mcp";
|
|
8756
8892
|
var DOCTOR_DESCRIPTION = {
|
|
@@ -8811,9 +8947,9 @@ async function runDoctorChecks(options) {
|
|
|
8811
8947
|
remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
|
|
8812
8948
|
});
|
|
8813
8949
|
}
|
|
8814
|
-
const fontManifest =
|
|
8950
|
+
const fontManifest = path21.join(fontCacheDir(), "manifest.json");
|
|
8815
8951
|
checks.push(
|
|
8816
|
-
|
|
8952
|
+
existsSync16(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync13(fontManifest, "utf8")).length} faces)` } : {
|
|
8817
8953
|
name: "font-cache",
|
|
8818
8954
|
ok: true,
|
|
8819
8955
|
detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
|
|
@@ -8834,7 +8970,9 @@ async function runDoctor(flags) {
|
|
|
8834
8970
|
return;
|
|
8835
8971
|
}
|
|
8836
8972
|
const report = await runDoctorChecks({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} });
|
|
8837
|
-
emitData(flags, report, () => {
|
|
8973
|
+
emitData(flags, { version: cliVersion(), ...report }, () => {
|
|
8974
|
+
process.stdout.write(`tendril ${cliVersion()}
|
|
8975
|
+
`);
|
|
8838
8976
|
for (const check of report.checks) {
|
|
8839
8977
|
process.stdout.write(`${check.ok ? "\u2713" : "\u2717"} ${check.name}: ${check.detail}
|
|
8840
8978
|
`);
|
|
@@ -8850,7 +8988,7 @@ async function runDoctor(flags) {
|
|
|
8850
8988
|
init_src3();
|
|
8851
8989
|
import { intro, isCancel, outro, password } from "@clack/prompts";
|
|
8852
8990
|
import fs from "node:fs";
|
|
8853
|
-
import
|
|
8991
|
+
import path22 from "node:path";
|
|
8854
8992
|
init_env();
|
|
8855
8993
|
init_output();
|
|
8856
8994
|
var INIT_DESCRIPTION = {
|
|
@@ -8889,7 +9027,7 @@ async function runInit(flags) {
|
|
|
8889
9027
|
printDescription(INIT_DESCRIPTION);
|
|
8890
9028
|
return;
|
|
8891
9029
|
}
|
|
8892
|
-
const envPath =
|
|
9030
|
+
const envPath = path22.resolve(process.cwd(), ".env");
|
|
8893
9031
|
const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
|
|
8894
9032
|
let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
|
|
8895
9033
|
let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
|
|
@@ -8910,7 +9048,7 @@ async function runInit(flags) {
|
|
|
8910
9048
|
next.set(ENV_KEYS.figma, figmaToken);
|
|
8911
9049
|
next.set(ENV_KEYS.openrouter, openrouterKey);
|
|
8912
9050
|
const changed = existing.get(ENV_KEYS.figma) !== next.get(ENV_KEYS.figma) || existing.get(ENV_KEYS.openrouter) !== next.get(ENV_KEYS.openrouter);
|
|
8913
|
-
const gitignorePath =
|
|
9051
|
+
const gitignorePath = path22.resolve(process.cwd(), ".gitignore");
|
|
8914
9052
|
const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
8915
9053
|
const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
|
|
8916
9054
|
if (flags.dryRun) {
|
|
@@ -8961,7 +9099,7 @@ init_src();
|
|
|
8961
9099
|
init_src5();
|
|
8962
9100
|
init_src2();
|
|
8963
9101
|
import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
|
|
8964
|
-
import { readFileSync as
|
|
9102
|
+
import { readFileSync as readFileSync14, readdirSync as readdirSync3, existsSync as existsSync17 } from "node:fs";
|
|
8965
9103
|
init_env();
|
|
8966
9104
|
init_output();
|
|
8967
9105
|
|
|
@@ -8970,7 +9108,7 @@ init_src2();
|
|
|
8970
9108
|
init_src4();
|
|
8971
9109
|
init_src6();
|
|
8972
9110
|
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync7 } from "node:fs";
|
|
8973
|
-
import
|
|
9111
|
+
import path23 from "node:path";
|
|
8974
9112
|
|
|
8975
9113
|
// packages/cli/src/assets-module.ts
|
|
8976
9114
|
init_src();
|
|
@@ -9306,7 +9444,7 @@ async function runGenerationPipeline(input) {
|
|
|
9306
9444
|
});
|
|
9307
9445
|
const written = [];
|
|
9308
9446
|
if (!input.dryRun) {
|
|
9309
|
-
const dir =
|
|
9447
|
+
const dir = path23.resolve(input.outDir, semantics.componentName);
|
|
9310
9448
|
mkdirSync4(dir, { recursive: true });
|
|
9311
9449
|
const files = {
|
|
9312
9450
|
// Bundle-local tokens: THE emission the component's CSS resolves
|
|
@@ -9330,13 +9468,13 @@ async function runGenerationPipeline(input) {
|
|
|
9330
9468
|
`
|
|
9331
9469
|
};
|
|
9332
9470
|
for (const [name, content] of Object.entries(files)) {
|
|
9333
|
-
const filePath =
|
|
9471
|
+
const filePath = path23.join(dir, name);
|
|
9334
9472
|
writeFileSync7(filePath, content);
|
|
9335
9473
|
written.push(filePath);
|
|
9336
9474
|
}
|
|
9337
9475
|
for (const artifact of emitTokenArtifacts(input.mapping)) {
|
|
9338
|
-
const filePath =
|
|
9339
|
-
mkdirSync4(
|
|
9476
|
+
const filePath = path23.resolve(input.outDir, artifact.path);
|
|
9477
|
+
mkdirSync4(path23.dirname(filePath), { recursive: true });
|
|
9340
9478
|
writeFileSync7(filePath, artifact.content);
|
|
9341
9479
|
written.push(filePath);
|
|
9342
9480
|
}
|
|
@@ -9395,7 +9533,7 @@ var GENERATE_DESCRIPTION = {
|
|
|
9395
9533
|
function resolveProvidedSource(flags, contextFile) {
|
|
9396
9534
|
let raw;
|
|
9397
9535
|
try {
|
|
9398
|
-
raw =
|
|
9536
|
+
raw = readFileSync14(contextFile, "utf8");
|
|
9399
9537
|
} catch {
|
|
9400
9538
|
fail(flags, ExitCode.InputValidation, {
|
|
9401
9539
|
error: `Cannot read context file "${contextFile}".`,
|
|
@@ -9514,11 +9652,11 @@ token mapping (${mapping.flat.length} variables):
|
|
|
9514
9652
|
let initialCode;
|
|
9515
9653
|
let initialSemantics;
|
|
9516
9654
|
try {
|
|
9517
|
-
if (
|
|
9655
|
+
if (existsSync17(flags.out)) {
|
|
9518
9656
|
for (const entry of readdirSync3(flags.out)) {
|
|
9519
9657
|
const cjPath = `${flags.out}/${entry}/component.json`;
|
|
9520
|
-
if (!
|
|
9521
|
-
const cj = JSON.parse(
|
|
9658
|
+
if (!existsSync17(cjPath)) continue;
|
|
9659
|
+
const cj = JSON.parse(readFileSync14(cjPath, "utf8"));
|
|
9522
9660
|
if (cj.name !== void 0 && Array.isArray(cj.props)) {
|
|
9523
9661
|
previousApi = JSON.stringify({
|
|
9524
9662
|
componentName: cj.name,
|
|
@@ -9526,14 +9664,14 @@ token mapping (${mapping.flat.length} variables):
|
|
|
9526
9664
|
});
|
|
9527
9665
|
const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
|
|
9528
9666
|
const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
|
|
9529
|
-
if (flags.refine &&
|
|
9667
|
+
if (flags.refine && existsSync17(tsxPath) && existsSync17(cssPath)) {
|
|
9530
9668
|
initialCode = {
|
|
9531
|
-
tsx:
|
|
9532
|
-
css:
|
|
9669
|
+
tsx: readFileSync14(tsxPath, "utf8"),
|
|
9670
|
+
css: readFileSync14(cssPath, "utf8")
|
|
9533
9671
|
};
|
|
9534
9672
|
const semPath = `${flags.out}/${entry}/semantics.json`;
|
|
9535
|
-
if (
|
|
9536
|
-
initialSemantics = JSON.parse(
|
|
9673
|
+
if (existsSync17(semPath)) {
|
|
9674
|
+
initialSemantics = JSON.parse(readFileSync14(semPath, "utf8"));
|
|
9537
9675
|
}
|
|
9538
9676
|
}
|
|
9539
9677
|
break;
|
|
@@ -9687,7 +9825,7 @@ function buildProgram() {
|
|
|
9687
9825
|
const program = new Command();
|
|
9688
9826
|
program.name("tendril").description(
|
|
9689
9827
|
"Figma design systems \u2192 agent-safe, verified front-end code.\nEvery command supports --json (data on stdout, JSON errors on stderr)\nand --describe (machine-readable schema)."
|
|
9690
|
-
).version(
|
|
9828
|
+
).version(cliVersion()).option("--json", "machine-readable output", false).option("--yes", "assume yes; never prompt", false).option("--dry-run", "describe mutations without performing them", false).option("--describe", "print the command's schema as JSON and exit", false).option("--verbosity <level>", "concise | detailed", "concise").exitOverride();
|
|
9691
9829
|
program.command("init").description("Configure Figma + OpenRouter credentials in .env (idempotent).").option("--figma-token <token>", "Figma personal access token").option("--openrouter-key <key>", "OpenRouter API key").action(async (_opts, cmd) => {
|
|
9692
9830
|
const flags = globalFlags(cmd);
|
|
9693
9831
|
const local = cmd.opts();
|
|
@@ -9721,11 +9859,11 @@ function buildProgram() {
|
|
|
9721
9859
|
const { runRecordNext: runRecordNext2 } = await Promise.resolve().then(() => (init_record(), record_exports));
|
|
9722
9860
|
runRecordNext2({ ...flags, setDir: cmd.opts()["set"] });
|
|
9723
9861
|
});
|
|
9724
|
-
record.command("ingest").requiredOption("--set <dir>", "recording set directory").requiredOption("--rep <slug>", "planned rep slug").requiredOption("--tool <tool>", "get_design_context | get_metadata | get_screenshot | get_variable_defs").requiredOption("--file <file>", "verbatim envelope JSON").action(async (_o, cmd) => {
|
|
9862
|
+
record.command("ingest").requiredOption("--set <dir>", "recording set directory").requiredOption("--rep <slug>", "planned rep slug").requiredOption("--tool <tool>", "get_design_context | get_metadata | get_screenshot | get_variable_defs").requiredOption("--file <file>", "verbatim envelope JSON (or raw response text with --raw)").option("--raw", "the file holds the tool response TEXT verbatim; the CLI builds the envelope").action(async (_o, cmd) => {
|
|
9725
9863
|
const flags = globalFlags(cmd.parent.parent);
|
|
9726
9864
|
const local = cmd.opts();
|
|
9727
9865
|
const { runRecordIngest: runRecordIngest2 } = await Promise.resolve().then(() => (init_record(), record_exports));
|
|
9728
|
-
runRecordIngest2({ ...flags, setDir: local["set"], rep: local["rep"], tool: local["tool"], file: local["file"] });
|
|
9866
|
+
await runRecordIngest2({ ...flags, setDir: local["set"], rep: local["rep"], tool: local["tool"], file: local["file"], raw: local["raw"] });
|
|
9729
9867
|
});
|
|
9730
9868
|
record.command("fetch").description("Download a Figma asset URL straight to disk and ingest it \u2014 no shell, no model in the byte path.").requiredOption("--set <dir>", "recording set directory").requiredOption("--rep <slug>", "planned rep slug").requiredOption("--tool <tool>", "get_screenshot").requiredOption("--url <url>", "image_url from the Figma tool response, verbatim").action(async (_o, cmd) => {
|
|
9731
9869
|
const flags = globalFlags(cmd.parent.parent);
|
|
@@ -9733,11 +9871,11 @@ function buildProgram() {
|
|
|
9733
9871
|
const { runRecordFetch: runRecordFetch2 } = await Promise.resolve().then(() => (init_record(), record_exports));
|
|
9734
9872
|
await runRecordFetch2({ ...flags, setDir: local["set"], rep: local["rep"], tool: local["tool"], url: local["url"] });
|
|
9735
9873
|
});
|
|
9736
|
-
record.command("asset").requiredOption("--set <dir>", "recording set directory").requiredOption("--rep <slug>", "planned rep slug").
|
|
9874
|
+
record.command("asset").requiredOption("--set <dir>", "recording set directory").requiredOption("--rep <slug>", "planned rep slug").option("--name <name>", "asset-<id>.<ext> (single-asset mode)").option("--file <file>", "downloaded asset file (single-asset mode)").option("--dir <dir>", "batch mode: ingest every asset-*.<ext> in this directory").action(async (_o, cmd) => {
|
|
9737
9875
|
const flags = globalFlags(cmd.parent.parent);
|
|
9738
9876
|
const local = cmd.opts();
|
|
9739
9877
|
const { runRecordAsset: runRecordAsset2 } = await Promise.resolve().then(() => (init_record(), record_exports));
|
|
9740
|
-
runRecordAsset2({ ...flags, setDir: local["set"], rep: local["rep"], name: local["name"], file: local["file"] });
|
|
9878
|
+
runRecordAsset2({ ...flags, setDir: local["set"], rep: local["rep"], name: local["name"], file: local["file"], dir: local["dir"] });
|
|
9741
9879
|
});
|
|
9742
9880
|
record.command("finish").requiredOption("--set <dir>", "recording set directory").option("--confirm-roles", "HUMAN-ONLY: accept the derived role proposal", false).option("--roles-file <file>", "override roles with a reviewed JSON graph").action(async (_o, cmd) => {
|
|
9743
9881
|
const flags = globalFlags(cmd.parent.parent);
|