@tendrilapp/cli 0.1.4 → 0.1.6
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 +39 -11
- package/dist/tendril.js +394 -239
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -1008,6 +1008,21 @@ function ingestEnvelope(setDir, slug, tool, payload) {
|
|
|
1008
1008
|
`);
|
|
1009
1009
|
return { overwrote };
|
|
1010
1010
|
}
|
|
1011
|
+
function envelopeTextContent(env) {
|
|
1012
|
+
const parts = env?.content ?? [];
|
|
1013
|
+
return parts.map((c) => c.text ?? "").filter((t) => t !== "").join("\n");
|
|
1014
|
+
}
|
|
1015
|
+
function envelopeFirstTextPart(env) {
|
|
1016
|
+
const parts = env?.content ?? [];
|
|
1017
|
+
return parts.find((c) => typeof c.text === "string" && c.text !== "")?.text ?? "";
|
|
1018
|
+
}
|
|
1019
|
+
function assetUrlsFromEnvelopeText(text) {
|
|
1020
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
1021
|
+
for (const m of text.matchAll(/const\s+\w+\s*=\s*"(https?:\/\/[^"]+\/assets?\/([a-z0-9-]+)\.(svg|png))"/gi)) {
|
|
1022
|
+
byUrl.set(m[1], `asset-${m[2]}.${m[3].toLowerCase()}`);
|
|
1023
|
+
}
|
|
1024
|
+
return [...byUrl].map(([url, name]) => ({ url, name }));
|
|
1025
|
+
}
|
|
1011
1026
|
function ingestAsset(setDir, slug, name, content) {
|
|
1012
1027
|
const manifest = loadManifest(setDir);
|
|
1013
1028
|
if (!manifest.reps.some((r) => r.slug === slug)) throw new Error(`unknown rep "${slug}" \u2014 not in the planned manifest`);
|
|
@@ -1789,7 +1804,7 @@ var init_browser = __esm({
|
|
|
1789
1804
|
});
|
|
1790
1805
|
|
|
1791
1806
|
// packages/verify/src/runtime.ts
|
|
1792
|
-
import { mkdtempSync, symlinkSync } from "node:fs";
|
|
1807
|
+
import { existsSync as existsSync4, mkdtempSync, symlinkSync } from "node:fs";
|
|
1793
1808
|
import os from "node:os";
|
|
1794
1809
|
import path4 from "node:path";
|
|
1795
1810
|
import { fileURLToPath } from "node:url";
|
|
@@ -1799,13 +1814,25 @@ function runtimePackageRoot() {
|
|
|
1799
1814
|
return path4.resolve(path4.dirname(fileURLToPath(import.meta.url)), "..");
|
|
1800
1815
|
}
|
|
1801
1816
|
function runtimeNodeModules() {
|
|
1802
|
-
|
|
1817
|
+
const root = runtimePackageRoot();
|
|
1818
|
+
for (let dir = root; ; dir = path4.dirname(dir)) {
|
|
1819
|
+
const candidate = path4.basename(dir) === "node_modules" ? dir : path4.join(dir, "node_modules");
|
|
1820
|
+
if (existsSync4(path4.join(candidate, "react"))) return candidate;
|
|
1821
|
+
if (path4.dirname(dir) === dir) break;
|
|
1822
|
+
}
|
|
1823
|
+
throw new Error(
|
|
1824
|
+
`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.`
|
|
1825
|
+
);
|
|
1803
1826
|
}
|
|
1804
1827
|
function newScratchDir(prefix) {
|
|
1805
1828
|
const dir = mkdtempSync(path4.join(os.tmpdir(), `tendril-${prefix}-`));
|
|
1829
|
+
const nodeModules = runtimeNodeModules();
|
|
1806
1830
|
try {
|
|
1807
|
-
symlinkSync(
|
|
1808
|
-
} catch {
|
|
1831
|
+
symlinkSync(nodeModules, path4.join(dir, "node_modules"), "junction");
|
|
1832
|
+
} catch (err) {
|
|
1833
|
+
throw new Error(
|
|
1834
|
+
`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.`
|
|
1835
|
+
);
|
|
1809
1836
|
}
|
|
1810
1837
|
return dir;
|
|
1811
1838
|
}
|
|
@@ -1968,7 +1995,11 @@ var init_token_lint = __esm({
|
|
|
1968
1995
|
"unset",
|
|
1969
1996
|
"none",
|
|
1970
1997
|
"auto",
|
|
1971
|
-
"0"
|
|
1998
|
+
"0",
|
|
1999
|
+
// Positional/structural keywords, not themable values: a fully
|
|
2000
|
+
// tokenised `box-shadow: var(--x) inset` was flagged for the literal
|
|
2001
|
+
// `inset` (first Windows component run).
|
|
2002
|
+
"inset"
|
|
1972
2003
|
];
|
|
1973
2004
|
STYLELINT_CONFIG = {
|
|
1974
2005
|
// The MODULE, not the name string: stylelint resolves plugin strings
|
|
@@ -3074,7 +3105,7 @@ var init_paths = __esm({
|
|
|
3074
3105
|
|
|
3075
3106
|
// packages/verify/src/font-resolve.ts
|
|
3076
3107
|
import { createHash } from "node:crypto";
|
|
3077
|
-
import { existsSync as
|
|
3108
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3078
3109
|
import os2 from "node:os";
|
|
3079
3110
|
import path9 from "node:path";
|
|
3080
3111
|
function fontCacheDir() {
|
|
@@ -3082,21 +3113,32 @@ function fontCacheDir() {
|
|
|
3082
3113
|
if (env !== void 0 && env !== "") return path9.resolve(env);
|
|
3083
3114
|
return path9.join(os2.homedir(), ".tendril", "fonts");
|
|
3084
3115
|
}
|
|
3116
|
+
function googleQueryCandidates(family) {
|
|
3117
|
+
const stripped = family.replace(/\s+(Variable|VF)$/i, "");
|
|
3118
|
+
return stripped === family ? [family] : [family, stripped];
|
|
3119
|
+
}
|
|
3085
3120
|
async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3086
3121
|
mkdirSync2(cacheDir, { recursive: true });
|
|
3087
3122
|
const resolved = [];
|
|
3088
3123
|
const failures = [];
|
|
3089
|
-
let css;
|
|
3124
|
+
let css = null;
|
|
3125
|
+
let lastStatus = 0;
|
|
3090
3126
|
try {
|
|
3091
|
-
const
|
|
3092
|
-
|
|
3093
|
-
|
|
3127
|
+
for (const queryName of googleQueryCandidates(family)) {
|
|
3128
|
+
const cssUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(queryName).replace(/%20/g, "+")}:wght@${weights.join(";")}&display=swap`;
|
|
3129
|
+
const res = await fetch(cssUrl, { headers: { "User-Agent": UA } });
|
|
3130
|
+
if (res.ok) {
|
|
3131
|
+
css = await res.text();
|
|
3132
|
+
break;
|
|
3133
|
+
}
|
|
3134
|
+
lastStatus = res.status;
|
|
3135
|
+
}
|
|
3136
|
+
if (css === null) {
|
|
3094
3137
|
return {
|
|
3095
3138
|
resolved,
|
|
3096
|
-
failures: weights.map((weight) => ({ family, weight, reason: `Google Fonts returned HTTP ${
|
|
3139
|
+
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
3140
|
};
|
|
3098
3141
|
}
|
|
3099
|
-
css = await res.text();
|
|
3100
3142
|
} catch (err) {
|
|
3101
3143
|
return { resolved, failures: weights.map((weight) => ({ family, weight, reason: `offline or fetch failed: ${err instanceof Error ? err.message : String(err)}` })) };
|
|
3102
3144
|
}
|
|
@@ -3124,7 +3166,7 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3124
3166
|
}
|
|
3125
3167
|
}
|
|
3126
3168
|
const mPath = path9.join(cacheDir, "manifest.json");
|
|
3127
|
-
const prior =
|
|
3169
|
+
const prior = existsSync5(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3128
3170
|
const portable2 = resolved.map((m) => ({ ...m, file: path9.basename(m.file) }));
|
|
3129
3171
|
const merged = [...prior.filter((p) => !resolved.some((m) => m.family === p.family && m.weight === p.weight)), ...portable2];
|
|
3130
3172
|
if (resolved.length > 0) writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
@@ -3133,7 +3175,7 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3133
3175
|
}
|
|
3134
3176
|
function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3135
3177
|
const src = path9.resolve(filePath);
|
|
3136
|
-
if (!
|
|
3178
|
+
if (!existsSync5(src)) throw new Error(`font file not found: ${src}`);
|
|
3137
3179
|
const ext = path9.extname(src).toLowerCase();
|
|
3138
3180
|
if (![".woff2", ".woff", ".ttf", ".otf"].includes(ext)) {
|
|
3139
3181
|
throw new Error(`unsupported font file "${ext}" \u2014 use .woff2 (preferred), .woff, .ttf or .otf`);
|
|
@@ -3146,7 +3188,7 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3146
3188
|
writeFileSync3(file, bytes);
|
|
3147
3189
|
const face = { family, weight, source: `local:${path9.basename(src)}`, sha256, file };
|
|
3148
3190
|
const mPath = path9.join(cacheDir, "manifest.json");
|
|
3149
|
-
const prior =
|
|
3191
|
+
const prior = existsSync5(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3150
3192
|
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path9.basename(file) }];
|
|
3151
3193
|
writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
3152
3194
|
`);
|
|
@@ -3155,7 +3197,7 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3155
3197
|
function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3156
3198
|
const lock = JSON.parse(readFileSync3(lockPath, "utf8"));
|
|
3157
3199
|
const mPath = path9.join(cacheDir, "manifest.json");
|
|
3158
|
-
const manifest =
|
|
3200
|
+
const manifest = existsSync5(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3159
3201
|
return lock.map((l) => {
|
|
3160
3202
|
const m = manifest.find((x) => x.family === l.family && x.weight === l.weight);
|
|
3161
3203
|
if (m === void 0) return { family: l.family, weight: l.weight, status: "missing" };
|
|
@@ -3164,7 +3206,7 @@ function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3164
3206
|
}
|
|
3165
3207
|
function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3166
3208
|
const mPath = path9.join(cacheDir, "manifest.json");
|
|
3167
|
-
if (!
|
|
3209
|
+
if (!existsSync5(mPath)) return [];
|
|
3168
3210
|
let entries;
|
|
3169
3211
|
try {
|
|
3170
3212
|
entries = JSON.parse(readFileSync3(mPath, "utf8"));
|
|
@@ -3182,7 +3224,7 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3182
3224
|
}
|
|
3183
3225
|
function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3184
3226
|
const mPath = path9.join(cacheDir, "manifest.json");
|
|
3185
|
-
const manifest =
|
|
3227
|
+
const manifest = existsSync5(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3186
3228
|
return manifest.filter((f) => families.includes(f.family));
|
|
3187
3229
|
}
|
|
3188
3230
|
var DEFAULT_FONT_CACHE, UA;
|
|
@@ -3195,13 +3237,13 @@ var init_font_resolve = __esm({
|
|
|
3195
3237
|
});
|
|
3196
3238
|
|
|
3197
3239
|
// packages/verify/src/font-faces.ts
|
|
3198
|
-
import { existsSync as
|
|
3240
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
3199
3241
|
import path10 from "node:path";
|
|
3200
3242
|
function fontFaceCss(manifestPath2 = path10.join(fontCacheDir(), "manifest.json")) {
|
|
3201
|
-
if (!
|
|
3243
|
+
if (!existsSync6(manifestPath2)) return "";
|
|
3202
3244
|
const manifest = JSON.parse(readFileSync4(manifestPath2, "utf8"));
|
|
3203
3245
|
const resolveFile = (f) => {
|
|
3204
|
-
if (path10.isAbsolute(f) &&
|
|
3246
|
+
if (path10.isAbsolute(f) && existsSync6(f)) return f;
|
|
3205
3247
|
return path10.resolve(path10.dirname(manifestPath2), path10.basename(f));
|
|
3206
3248
|
};
|
|
3207
3249
|
const byFile = /* @__PURE__ */ new Map();
|
|
@@ -3245,7 +3287,7 @@ var init_mount_limits = __esm({
|
|
|
3245
3287
|
});
|
|
3246
3288
|
|
|
3247
3289
|
// packages/verify/src/admission.ts
|
|
3248
|
-
import { readFileSync as readFileSync5, readdirSync as readdirSync2, existsSync as
|
|
3290
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync2, existsSync as existsSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3249
3291
|
import path11 from "node:path";
|
|
3250
3292
|
import { build as build2 } from "esbuild";
|
|
3251
3293
|
import postcss from "postcss";
|
|
@@ -3254,7 +3296,7 @@ import { chromium as chromium2 } from "playwright-core";
|
|
|
3254
3296
|
function fontWeightsByFamily() {
|
|
3255
3297
|
const mPath = path11.join(fontCacheDir(), "manifest.json");
|
|
3256
3298
|
const out = /* @__PURE__ */ new Map();
|
|
3257
|
-
if (!
|
|
3299
|
+
if (!existsSync7(mPath)) return out;
|
|
3258
3300
|
for (const f of JSON.parse(readFileSync5(mPath, "utf8")))
|
|
3259
3301
|
out.set(f.family, [...out.get(f.family) ?? [], f.weight]);
|
|
3260
3302
|
return out;
|
|
@@ -3477,7 +3519,7 @@ __export(behavior_exports, {
|
|
|
3477
3519
|
compileMount: () => compileMount,
|
|
3478
3520
|
recordingIsDark: () => recordingIsDark
|
|
3479
3521
|
});
|
|
3480
|
-
import { existsSync as
|
|
3522
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
|
|
3481
3523
|
import path13 from "node:path";
|
|
3482
3524
|
import { build as build3 } from "esbuild";
|
|
3483
3525
|
import { chromium as chromium3 } from "playwright-core";
|
|
@@ -3488,7 +3530,7 @@ function getFontFaces() {
|
|
|
3488
3530
|
}
|
|
3489
3531
|
async function compileMount(task, bundleDir) {
|
|
3490
3532
|
const entryTsx = path13.join(bundleDir, task.entry);
|
|
3491
|
-
if (!
|
|
3533
|
+
if (!existsSync8(entryTsx)) return { error: `${task.entry} missing` };
|
|
3492
3534
|
const mountSrc = `
|
|
3493
3535
|
import { createElement } from "react";
|
|
3494
3536
|
import { createRoot } from "react-dom/client";
|
|
@@ -3701,7 +3743,7 @@ function recordingIsDark(task) {
|
|
|
3701
3743
|
const rep = task.configs[0]?.rep;
|
|
3702
3744
|
if (rep === void 0) return false;
|
|
3703
3745
|
const f = path13.join(task.set, rep, "get_screenshot.json");
|
|
3704
|
-
if (!
|
|
3746
|
+
if (!existsSync8(f)) return false;
|
|
3705
3747
|
try {
|
|
3706
3748
|
const env = JSON.parse(readFileSync6(f, "utf8")).content.find((c) => c.type === "image");
|
|
3707
3749
|
if (env?.data === void 0) return false;
|
|
@@ -3775,7 +3817,7 @@ async function checkBehaviors(task, bundleDir, opts = {}) {
|
|
|
3775
3817
|
const deadlineMs = timeoutMs + 1e4;
|
|
3776
3818
|
const js = await compileMount(task, bundleDir);
|
|
3777
3819
|
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) =>
|
|
3820
|
+
const css = ["tokens.css", "styles.css"].map((f) => path13.join(bundleDir, f)).filter((f) => existsSync8(f)).map((f) => readFileSync6(f, "utf8")).join("\n");
|
|
3779
3821
|
const server = await chromium3.launchServer({ executablePath: resolveChrome(), headless: true, args: [MEMORY_CAP_ARG] });
|
|
3780
3822
|
const browser = await chromium3.connect(server.wsEndpoint());
|
|
3781
3823
|
const results = [];
|
|
@@ -3918,7 +3960,7 @@ var init_behavior = __esm({
|
|
|
3918
3960
|
});
|
|
3919
3961
|
|
|
3920
3962
|
// packages/verify/src/bundle-quality.ts
|
|
3921
|
-
import { readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as
|
|
3963
|
+
import { readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync9 } from "node:fs";
|
|
3922
3964
|
import path14 from "node:path";
|
|
3923
3965
|
function definedVars(tokensCss) {
|
|
3924
3966
|
if (tokensCss === void 0) return void 0;
|
|
@@ -3931,9 +3973,9 @@ async function checkBundleQuality(bundleDir, entry) {
|
|
|
3931
3973
|
const entryPath = path14.join(bundleDir, entry);
|
|
3932
3974
|
const cssPath = path14.join(bundleDir, "styles.css");
|
|
3933
3975
|
const tokensPath = path14.join(bundleDir, "tokens.css");
|
|
3934
|
-
const css =
|
|
3935
|
-
const tokensCss =
|
|
3936
|
-
if (
|
|
3976
|
+
const css = existsSync9(cssPath) ? readFileSync7(cssPath, "utf8") : "";
|
|
3977
|
+
const tokensCss = existsSync9(tokensPath) ? readFileSync7(tokensPath, "utf8") : void 0;
|
|
3978
|
+
if (existsSync9(entryPath)) {
|
|
3937
3979
|
const workDir = newScratchDir("quality");
|
|
3938
3980
|
try {
|
|
3939
3981
|
const tsxPath = path14.join(workDir, entry);
|
|
@@ -4000,7 +4042,7 @@ var init_effect_geometry = __esm({
|
|
|
4000
4042
|
});
|
|
4001
4043
|
|
|
4002
4044
|
// packages/verify/src/bundle-score.ts
|
|
4003
|
-
import { existsSync as
|
|
4045
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4004
4046
|
import path15 from "node:path";
|
|
4005
4047
|
import { build as build4 } from "esbuild";
|
|
4006
4048
|
import { chromium as chromium4 } from "playwright-core";
|
|
@@ -4064,7 +4106,7 @@ function repRef(set, rep) {
|
|
|
4064
4106
|
}
|
|
4065
4107
|
function repEffectExtents(set, rep) {
|
|
4066
4108
|
const file = path15.join(set, rep, "get_design_context.json");
|
|
4067
|
-
if (!
|
|
4109
|
+
if (!existsSync10(file)) return void 0;
|
|
4068
4110
|
try {
|
|
4069
4111
|
const text = JSON.parse(readFileSync8(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4070
4112
|
const extents = shadowExtents(text);
|
|
@@ -4078,8 +4120,8 @@ async function scoreBundleForTask(task, bundleDir, bar = BAR, opts = {}) {
|
|
|
4078
4120
|
const CONFIGS2 = task.configs;
|
|
4079
4121
|
const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
|
|
4080
4122
|
const entryTsx = path15.join(bundleDir, task.entry);
|
|
4081
|
-
if (!
|
|
4082
|
-
const css = ["tokens.css", "styles.css"].map((f) => path15.join(bundleDir, f)).filter((f) =>
|
|
4123
|
+
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` }));
|
|
4124
|
+
const css = ["tokens.css", "styles.css"].map((f) => path15.join(bundleDir, f)).filter((f) => existsSync10(f)).map((f) => readFileSync8(f, "utf8")).join("\n");
|
|
4083
4125
|
const mountSrc = `
|
|
4084
4126
|
import { createElement } from "react";
|
|
4085
4127
|
import { createRoot } from "react-dom/client";
|
|
@@ -4275,13 +4317,13 @@ var init_prelude = __esm({
|
|
|
4275
4317
|
- Component root: -webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale (recorded rasterization); font-synthesis: none (a missing font weight must fall back visibly, never fake-bold); color-scheme MATCHING YOUR RECORDING (light for a light capture, dark for a dark one) and direction: ltr \u2014 pin them, so the render cannot follow the viewer OS preference and drift from the capture it is graded against; isolation: isolate (own stacking context; overlay z-indexes never fight the host).
|
|
4276
4318
|
- Interactive controls (buttons, options): touch-action: manipulation and user-select: none. Text inputs stay selectable (never user-select: none on them).
|
|
4277
4319
|
- Scrollable popovers/menus: overscroll-behavior: contain.
|
|
4278
|
-
- Focus
|
|
4320
|
+
- Focus indicators bind to :focus-visible, never bare :focus. If the recording contains a focus pose, style the indicator from that recorded truth. If NO focus pose is recorded, do NOT invent ring colors/widths/offsets \u2014 an invented ring is unrecorded pixels; keep the browser's default focus indicator (leave outline in place on :focus-visible) and state the gap in your report. Text inputs match :focus-visible even on mouse click BY SPEC \u2014 that is platform-correct accessibility, never a bug to suppress with JS modality tracking.
|
|
4279
4321
|
- Every animation wrapped in @media (prefers-reduced-motion: no-preference) or disabled under reduce.`;
|
|
4280
4322
|
}
|
|
4281
4323
|
});
|
|
4282
4324
|
|
|
4283
4325
|
// packages/verify/src/parity.ts
|
|
4284
|
-
import { existsSync as
|
|
4326
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
|
|
4285
4327
|
import path16 from "node:path";
|
|
4286
4328
|
import { chromium as chromium6 } from "playwright-core";
|
|
4287
4329
|
function getFontFaces3() {
|
|
@@ -4311,7 +4353,7 @@ async function checkHoverParity(task, bundleDir, opts = {}) {
|
|
|
4311
4353
|
const deadlineMs = timeoutMs + 1e4;
|
|
4312
4354
|
const js = await compileMount(task, bundleDir);
|
|
4313
4355
|
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) =>
|
|
4356
|
+
const css = ["tokens.css", "styles.css"].map((f) => path16.join(bundleDir, f)).filter((f) => existsSync11(f)).map((f) => readFileSync9(f, "utf8")).join("\n");
|
|
4315
4357
|
const server = await chromium6.launchServer({ executablePath: resolveChrome(), headless: true, args: [MEMORY_CAP_ARG] });
|
|
4316
4358
|
const browser = await chromium6.connect(server.wsEndpoint());
|
|
4317
4359
|
const results = [];
|
|
@@ -4391,7 +4433,7 @@ var init_parity = __esm({
|
|
|
4391
4433
|
|
|
4392
4434
|
// packages/verify/src/composition.ts
|
|
4393
4435
|
import { createRequire } from "node:module";
|
|
4394
|
-
import { existsSync as
|
|
4436
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
|
|
4395
4437
|
import path17 from "node:path";
|
|
4396
4438
|
import { build as build6 } from "esbuild";
|
|
4397
4439
|
import { chromium as chromium7 } from "playwright-core";
|
|
@@ -4401,7 +4443,7 @@ function getFontFaces4() {
|
|
|
4401
4443
|
}
|
|
4402
4444
|
async function compileInstrumentedMount(task, bundleDir) {
|
|
4403
4445
|
const entryTsx = path17.join(bundleDir, task.entry);
|
|
4404
|
-
if (!
|
|
4446
|
+
if (!existsSync12(entryTsx)) return { error: `${task.entry} missing` };
|
|
4405
4447
|
const requireFromVerify = createRequire(path17.join(VERIFY_PKG_DIR, "package.json"));
|
|
4406
4448
|
let realJsxPath;
|
|
4407
4449
|
try {
|
|
@@ -4465,7 +4507,7 @@ function expectedParts(task, roles, mainSlug) {
|
|
|
4465
4507
|
}
|
|
4466
4508
|
function interiorRegions(setDir, roles) {
|
|
4467
4509
|
const mains = roles.main;
|
|
4468
|
-
const withInterior = mains.filter((m) =>
|
|
4510
|
+
const withInterior = mains.filter((m) => existsSync12(path17.join(setDir, m, "get_metadata_interior.json")));
|
|
4469
4511
|
if (withInterior.length === 0) {
|
|
4470
4512
|
return { unavailable: "no interior geometry recorded for any main (pre-obligation set) \u2014 re-record with `tendril record` to enable crop checks" };
|
|
4471
4513
|
}
|
|
@@ -4482,7 +4524,7 @@ async function checkCropComposition(task, bundleDir, regions, opts = {}) {
|
|
|
4482
4524
|
if (typeof js !== "string") {
|
|
4483
4525
|
return regions.map((r) => ({ id: cropId(r), pass: false, similarity: 0, inkRecall: 0, detail: js.error }));
|
|
4484
4526
|
}
|
|
4485
|
-
const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) =>
|
|
4527
|
+
const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) => existsSync12(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
|
|
4486
4528
|
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: [MEMORY_CAP_ARG] });
|
|
4487
4529
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
4488
4530
|
const PAD = 4;
|
|
@@ -4587,7 +4629,7 @@ async function checkStructuralComposition(task, bundleDir, roles, opts = {}) {
|
|
|
4587
4629
|
const deadlineMs = timeoutMs + 1e4;
|
|
4588
4630
|
const js = await compileInstrumentedMount(task, bundleDir);
|
|
4589
4631
|
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) =>
|
|
4632
|
+
const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) => existsSync12(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
|
|
4591
4633
|
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: [MEMORY_CAP_ARG] });
|
|
4592
4634
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
4593
4635
|
try {
|
|
@@ -4677,13 +4719,13 @@ export const jsxs = (t, p, k) => real.jsxs(t, inject(t, p), k);
|
|
|
4677
4719
|
});
|
|
4678
4720
|
|
|
4679
4721
|
// packages/verify/src/occlusion.ts
|
|
4680
|
-
import { existsSync as
|
|
4722
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
4681
4723
|
import path18 from "node:path";
|
|
4682
4724
|
import { build as build7 } from "esbuild";
|
|
4683
4725
|
import { chromium as chromium8 } from "playwright-core";
|
|
4684
4726
|
async function compileTwoUp(task, bundleDir) {
|
|
4685
4727
|
const entryTsx = path18.join(bundleDir, task.entry);
|
|
4686
|
-
if (!
|
|
4728
|
+
if (!existsSync13(entryTsx)) return { error: `${task.entry} missing` };
|
|
4687
4729
|
const src = `
|
|
4688
4730
|
import { createElement } from "react";
|
|
4689
4731
|
import { createRoot } from "react-dom/client";
|
|
@@ -4862,9 +4904,48 @@ var init_src4 = __esm({
|
|
|
4862
4904
|
}
|
|
4863
4905
|
});
|
|
4864
4906
|
|
|
4865
|
-
// packages/cli/src/
|
|
4866
|
-
import { existsSync as
|
|
4907
|
+
// packages/cli/src/environment.ts
|
|
4908
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
|
|
4867
4909
|
import path19 from "node:path";
|
|
4910
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
4911
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
4912
|
+
function cliVersion() {
|
|
4913
|
+
try {
|
|
4914
|
+
return JSON.parse(readFileSync11(path19.join(path19.dirname(fileURLToPath4(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
|
|
4915
|
+
} catch {
|
|
4916
|
+
return "dev";
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
function environmentStamp(taskFamilies) {
|
|
4920
|
+
const manifestPath2 = path19.join(fontCacheDir(), "manifest.json");
|
|
4921
|
+
let fontsHash = null;
|
|
4922
|
+
if (existsSync14(manifestPath2)) {
|
|
4923
|
+
try {
|
|
4924
|
+
const entries = JSON.parse(readFileSync11(manifestPath2, "utf8"));
|
|
4925
|
+
const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
|
|
4926
|
+
const faces = scoped.map((f) => `${f.family}:${f.weight}:${f.sha256}`).sort();
|
|
4927
|
+
fontsHash = faces.length === 0 ? null : createHash2("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
|
|
4928
|
+
} catch {
|
|
4929
|
+
fontsHash = null;
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
let chrome = "unavailable";
|
|
4933
|
+
try {
|
|
4934
|
+
chrome = resolveChrome();
|
|
4935
|
+
} catch {
|
|
4936
|
+
}
|
|
4937
|
+
return { chrome, chromeVersion: chrome === "unavailable" ? null : chromeVersion(), fontsManifestSha256: fontsHash };
|
|
4938
|
+
}
|
|
4939
|
+
var init_environment = __esm({
|
|
4940
|
+
"packages/cli/src/environment.ts"() {
|
|
4941
|
+
"use strict";
|
|
4942
|
+
init_src4();
|
|
4943
|
+
}
|
|
4944
|
+
});
|
|
4945
|
+
|
|
4946
|
+
// packages/cli/src/env.ts
|
|
4947
|
+
import { existsSync as existsSync15, readFileSync as readFileSync12 } from "node:fs";
|
|
4948
|
+
import path20 from "node:path";
|
|
4868
4949
|
function parseEnv(content) {
|
|
4869
4950
|
const entries = /* @__PURE__ */ new Map();
|
|
4870
4951
|
for (const line of content.split("\n")) {
|
|
@@ -4876,9 +4957,9 @@ function parseEnv(content) {
|
|
|
4876
4957
|
function resolveCredential(name) {
|
|
4877
4958
|
const fromProcess = process.env[name];
|
|
4878
4959
|
if (fromProcess) return fromProcess;
|
|
4879
|
-
const envPath =
|
|
4880
|
-
if (!
|
|
4881
|
-
return parseEnv(
|
|
4960
|
+
const envPath = path20.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
|
|
4961
|
+
if (!existsSync15(envPath)) return void 0;
|
|
4962
|
+
return parseEnv(readFileSync12(envPath, "utf8")).get(name);
|
|
4882
4963
|
}
|
|
4883
4964
|
var init_env = __esm({
|
|
4884
4965
|
"packages/cli/src/env.ts"() {
|
|
@@ -6133,6 +6214,8 @@ var record_exports = {};
|
|
|
6133
6214
|
__export(record_exports, {
|
|
6134
6215
|
instanceLeads: () => instanceLeads,
|
|
6135
6216
|
isFigmaAssetUrl: () => isFigmaAssetUrl,
|
|
6217
|
+
isLocalAssetUrl: () => isLocalAssetUrl,
|
|
6218
|
+
nextPayload: () => nextPayload,
|
|
6136
6219
|
runRecordAsset: () => runRecordAsset,
|
|
6137
6220
|
runRecordFetch: () => runRecordFetch,
|
|
6138
6221
|
runRecordFinish: () => runRecordFinish,
|
|
@@ -6141,11 +6224,11 @@ __export(record_exports, {
|
|
|
6141
6224
|
runRecordPlan: () => runRecordPlan,
|
|
6142
6225
|
runRecordStatus: () => runRecordStatus
|
|
6143
6226
|
});
|
|
6144
|
-
import { existsSync as
|
|
6145
|
-
import
|
|
6227
|
+
import { existsSync as existsSync18, readFileSync as readFileSync15, readdirSync as readdirSync4 } from "node:fs";
|
|
6228
|
+
import path24 from "node:path";
|
|
6146
6229
|
import { writeFileSync as writeFileSync8 } from "node:fs";
|
|
6147
6230
|
function symbolsFromMetadataEnvelope(file, sourceFrame) {
|
|
6148
|
-
const env = JSON.parse(
|
|
6231
|
+
const env = JSON.parse(readFileSync15(file, "utf8"));
|
|
6149
6232
|
const text = env.content.map((c) => c.text ?? "").join("\n");
|
|
6150
6233
|
const symbols = [];
|
|
6151
6234
|
const walk2 = (node, ancestor) => {
|
|
@@ -6189,7 +6272,7 @@ function runRecordPlan(opts) {
|
|
|
6189
6272
|
for (const spec of opts.metadataFiles) {
|
|
6190
6273
|
const [file, frame] = spec.split("@");
|
|
6191
6274
|
try {
|
|
6192
|
-
symbols.push(...symbolsFromMetadataEnvelope(
|
|
6275
|
+
symbols.push(...symbolsFromMetadataEnvelope(path24.resolve(file), frame));
|
|
6193
6276
|
} catch (err) {
|
|
6194
6277
|
fail(opts, ExitCode.InputValidation, {
|
|
6195
6278
|
error: `could not read metadata envelope ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -6221,7 +6304,7 @@ function runRecordPlan(opts) {
|
|
|
6221
6304
|
const leads = opts.metadataFiles.flatMap((spec) => {
|
|
6222
6305
|
const [file] = spec.split("@");
|
|
6223
6306
|
try {
|
|
6224
|
-
const env = JSON.parse(
|
|
6307
|
+
const env = JSON.parse(readFileSync15(path24.resolve(file), "utf8"));
|
|
6225
6308
|
return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
|
|
6226
6309
|
} catch {
|
|
6227
6310
|
return [];
|
|
@@ -6296,27 +6379,28 @@ function runRecordPlan(opts) {
|
|
|
6296
6379
|
}
|
|
6297
6380
|
);
|
|
6298
6381
|
}
|
|
6299
|
-
function
|
|
6300
|
-
const instruction = nextInstruction(
|
|
6301
|
-
const status = sessionStatus(
|
|
6382
|
+
function nextPayload(setDir) {
|
|
6383
|
+
const instruction = nextInstruction(setDir);
|
|
6384
|
+
const status = sessionStatus(setDir);
|
|
6302
6385
|
const progress = { recordedReps: status.reps.filter((x) => x.missing.length === 0).length, totalReps: status.reps.length };
|
|
6303
|
-
if (instruction === null && !
|
|
6304
|
-
const manifest = loadManifest(
|
|
6386
|
+
if (instruction === null && !existsSync18(path24.join(setDir, "get_variable_defs.json"))) {
|
|
6387
|
+
const manifest = loadManifest(setDir);
|
|
6305
6388
|
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;
|
|
6389
|
+
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
6390
|
}
|
|
6312
|
-
|
|
6313
|
-
|
|
6391
|
+
return instruction === null ? { complete: true, progress } : { ...instruction, note: `${instruction.note}. ${ENVELOPE_HELP}`, progress };
|
|
6392
|
+
}
|
|
6393
|
+
function runRecordNext(opts) {
|
|
6394
|
+
const payload = nextPayload(opts.setDir);
|
|
6395
|
+
emitData(opts, payload, () => {
|
|
6396
|
+
if (payload["complete"] === true) {
|
|
6314
6397
|
process.stdout.write("set complete \u2014 every planned rep has its required envelopes\n");
|
|
6315
6398
|
return;
|
|
6316
6399
|
}
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
\u2192
|
|
6400
|
+
const progress = payload["progress"];
|
|
6401
|
+
process.stdout.write(`[${progress.recordedReps}/${progress.totalReps} reps] ${payload["tool"]} for ${payload["slug"]} (node ${payload["nodeId"]})
|
|
6402
|
+
\u2192 ${payload["note"]}
|
|
6403
|
+
\u2192 then: tendril record ingest --set <dir> --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>
|
|
6320
6404
|
`);
|
|
6321
6405
|
});
|
|
6322
6406
|
}
|
|
@@ -6331,6 +6415,29 @@ function isFigmaAssetUrl(raw) {
|
|
|
6331
6415
|
if (u.username !== "" || u.password !== "") return false;
|
|
6332
6416
|
return u.hostname === "figma.com" || u.hostname.endsWith(".figma.com");
|
|
6333
6417
|
}
|
|
6418
|
+
function isLocalAssetUrl(raw) {
|
|
6419
|
+
let u;
|
|
6420
|
+
try {
|
|
6421
|
+
u = new URL(raw);
|
|
6422
|
+
} catch {
|
|
6423
|
+
return false;
|
|
6424
|
+
}
|
|
6425
|
+
if (u.username !== "" || u.password !== "") return false;
|
|
6426
|
+
return (u.protocol === "http:" || u.protocol === "https:") && (u.hostname === "localhost" || u.hostname === "127.0.0.1");
|
|
6427
|
+
}
|
|
6428
|
+
async function fetchAssetBytes(startUrl, allowed) {
|
|
6429
|
+
let url = startUrl;
|
|
6430
|
+
let res = await fetch(url, { redirect: "manual" });
|
|
6431
|
+
for (let hop = 0; res.status >= 300 && res.status < 400 && hop < 5; hop++) {
|
|
6432
|
+
const next = res.headers.get("location");
|
|
6433
|
+
if (next === null) break;
|
|
6434
|
+
url = new URL(next, url).toString();
|
|
6435
|
+
if (!allowed(url)) throw new Error(`redirect left the allowed hosts: ${url}`);
|
|
6436
|
+
res = await fetch(url, { redirect: "manual" });
|
|
6437
|
+
}
|
|
6438
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
6439
|
+
return Buffer.from(await res.arrayBuffer());
|
|
6440
|
+
}
|
|
6334
6441
|
async function runRecordFetch(opts) {
|
|
6335
6442
|
if (!isFigmaAssetUrl(opts.url)) {
|
|
6336
6443
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -6341,17 +6448,7 @@ async function runRecordFetch(opts) {
|
|
|
6341
6448
|
}
|
|
6342
6449
|
let bytes;
|
|
6343
6450
|
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());
|
|
6451
|
+
bytes = await fetchAssetBytes(opts.url, isFigmaAssetUrl);
|
|
6355
6452
|
} catch (err) {
|
|
6356
6453
|
fail(opts, ExitCode.InputValidation, {
|
|
6357
6454
|
error: `asset fetch failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -6369,7 +6466,7 @@ async function runRecordFetch(opts) {
|
|
|
6369
6466
|
const payload = { content: [{ type: "image", data: bytes.toString("base64"), mimeType: "image/png" }] };
|
|
6370
6467
|
try {
|
|
6371
6468
|
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" }, () => {
|
|
6469
|
+
emitData(opts, { rep: opts.rep, tool: opts.tool, bytes: bytes.length, overwrote, source: "cli-fetch", next: nextPayload(opts.setDir) }, () => {
|
|
6373
6470
|
process.stdout.write(`fetched + ingested ${opts.rep}/${opts.tool} (${bytes.length} bytes, no model in the byte path)
|
|
6374
6471
|
`);
|
|
6375
6472
|
});
|
|
@@ -6381,15 +6478,53 @@ async function runRecordFetch(opts) {
|
|
|
6381
6478
|
});
|
|
6382
6479
|
}
|
|
6383
6480
|
}
|
|
6384
|
-
function
|
|
6481
|
+
async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
6482
|
+
const fetched = [];
|
|
6483
|
+
const skipped = [];
|
|
6484
|
+
const failed = [];
|
|
6485
|
+
for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
|
|
6486
|
+
if (existsSync18(path24.join(setDir, rep, name))) {
|
|
6487
|
+
skipped.push(name);
|
|
6488
|
+
continue;
|
|
6489
|
+
}
|
|
6490
|
+
if (!isAutoFetchAssetUrl(url)) {
|
|
6491
|
+
failed.push({ name, url, reason: "host not on the auto-fetch allowlist (Figma hosts and the desktop app's localhost only)" });
|
|
6492
|
+
continue;
|
|
6493
|
+
}
|
|
6494
|
+
try {
|
|
6495
|
+
const bytes = await fetchAssetBytes(url, isAutoFetchAssetUrl);
|
|
6496
|
+
if (name.endsWith(".svg") && !bytes.toString("utf8", 0, 512).includes("<svg")) throw new Error("response is not an SVG (expired URL usually returns HTML)");
|
|
6497
|
+
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)");
|
|
6498
|
+
ingestAsset(setDir, rep, name, bytes);
|
|
6499
|
+
fetched.push(name);
|
|
6500
|
+
} catch (err) {
|
|
6501
|
+
failed.push({ name, url, reason: err instanceof Error ? err.message : String(err) });
|
|
6502
|
+
}
|
|
6503
|
+
}
|
|
6504
|
+
return { fetched, skipped, failed };
|
|
6505
|
+
}
|
|
6506
|
+
async function runRecordIngest(opts) {
|
|
6385
6507
|
let payload;
|
|
6386
6508
|
try {
|
|
6387
|
-
|
|
6509
|
+
if (opts.rawParts === true) {
|
|
6510
|
+
const parts = JSON.parse(readFileSync15(path24.resolve(opts.file), "utf8"));
|
|
6511
|
+
if (!Array.isArray(parts) || parts.length === 0 || parts.some((p) => typeof p !== "string")) throw new Error("--raw-parts file must be a non-empty JSON array of strings");
|
|
6512
|
+
payload = { content: parts.map((text) => ({ type: "text", text })) };
|
|
6513
|
+
} else {
|
|
6514
|
+
payload = opts.raw === true ? { content: [{ type: "text", text: readFileSync15(path24.resolve(opts.file), "utf8") }] } : JSON.parse(readFileSync15(path24.resolve(opts.file), "utf8"));
|
|
6515
|
+
}
|
|
6388
6516
|
} catch (err) {
|
|
6389
6517
|
fail(opts, ExitCode.InputValidation, {
|
|
6390
6518
|
error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
|
|
6391
6519
|
code: "envelope-unreadable",
|
|
6392
|
-
remediation: "Pass the tool response saved verbatim as JSON."
|
|
6520
|
+
remediation: "Pass the tool response saved verbatim: as JSON, or as raw text with --raw."
|
|
6521
|
+
});
|
|
6522
|
+
}
|
|
6523
|
+
if ((opts.raw === true || opts.rawParts === true) && opts.tool === "get_screenshot") {
|
|
6524
|
+
fail(opts, ExitCode.InputValidation, {
|
|
6525
|
+
error: "--raw is for text tool responses; screenshots are binary",
|
|
6526
|
+
code: "envelope-invalid",
|
|
6527
|
+
remediation: "Use `tendril record fetch` with the image_url instead."
|
|
6393
6528
|
});
|
|
6394
6529
|
}
|
|
6395
6530
|
if (opts.rep === "__set__" && opts.tool === "get_variable_defs") {
|
|
@@ -6401,18 +6536,25 @@ function runRecordIngest(opts) {
|
|
|
6401
6536
|
remediation: "Save the get_variable_defs response verbatim as a text envelope."
|
|
6402
6537
|
});
|
|
6403
6538
|
}
|
|
6404
|
-
writeFileSync8(
|
|
6539
|
+
writeFileSync8(path24.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
|
|
6405
6540
|
`);
|
|
6406
|
-
emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true }, () => {
|
|
6541
|
+
emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
|
|
6407
6542
|
process.stdout.write("set-level get_variable_defs ingested\n");
|
|
6408
6543
|
});
|
|
6409
6544
|
return;
|
|
6410
6545
|
}
|
|
6411
6546
|
try {
|
|
6412
6547
|
const { overwrote } = ingestEnvelope(opts.setDir, opts.rep, opts.tool, payload);
|
|
6413
|
-
|
|
6548
|
+
const assets = opts.tool === "get_design_context" ? await autoFetchAssets(opts.setDir, opts.rep, payload.content.map((c) => c.text ?? "").join("\n")) : void 0;
|
|
6549
|
+
emitData(opts, { rep: opts.rep, tool: opts.tool, overwrote, ...assets !== void 0 ? { assets } : {}, next: nextPayload(opts.setDir) }, () => {
|
|
6414
6550
|
process.stdout.write(`ingested ${opts.rep}/${opts.tool}${overwrote ? " (overwrote prior recording)" : ""}
|
|
6415
6551
|
`);
|
|
6552
|
+
if (assets !== void 0) {
|
|
6553
|
+
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
6554
|
+
`);
|
|
6555
|
+
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it and run tendril record asset
|
|
6556
|
+
`);
|
|
6557
|
+
}
|
|
6416
6558
|
});
|
|
6417
6559
|
} catch (err) {
|
|
6418
6560
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -6423,8 +6565,44 @@ function runRecordIngest(opts) {
|
|
|
6423
6565
|
}
|
|
6424
6566
|
}
|
|
6425
6567
|
function runRecordAsset(opts) {
|
|
6568
|
+
if (opts.dir !== void 0) {
|
|
6569
|
+
const dir = path24.resolve(opts.dir);
|
|
6570
|
+
const names = readdirSync4(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
|
|
6571
|
+
if (names.length === 0) {
|
|
6572
|
+
fail(opts, ExitCode.InputValidation, {
|
|
6573
|
+
error: `no asset-*.<ext> files found in ${dir}`,
|
|
6574
|
+
code: "asset-rejected",
|
|
6575
|
+
remediation: "Name downloaded assets asset-<id>.<ext> (the id from the asset URL), or pass --name/--file for a single asset."
|
|
6576
|
+
});
|
|
6577
|
+
}
|
|
6578
|
+
const ingested = [];
|
|
6579
|
+
try {
|
|
6580
|
+
for (const name of names) {
|
|
6581
|
+
ingestAsset(opts.setDir, opts.rep, name, readFileSync15(path24.join(dir, name)));
|
|
6582
|
+
ingested.push(name);
|
|
6583
|
+
}
|
|
6584
|
+
} catch (err) {
|
|
6585
|
+
fail(opts, ExitCode.InputValidation, {
|
|
6586
|
+
error: `after ${ingested.length} ingested: ${err instanceof Error ? err.message : String(err)}`,
|
|
6587
|
+
code: "asset-rejected",
|
|
6588
|
+
remediation: "Assets must be named asset-<id>.<ext>, size-capped, and SVGs must carry no active content."
|
|
6589
|
+
});
|
|
6590
|
+
}
|
|
6591
|
+
emitData(opts, { rep: opts.rep, assets: ingested }, () => {
|
|
6592
|
+
for (const name of ingested) process.stdout.write(`ingested ${opts.rep}/${name}
|
|
6593
|
+
`);
|
|
6594
|
+
});
|
|
6595
|
+
return;
|
|
6596
|
+
}
|
|
6597
|
+
if (opts.name === void 0 || opts.file === void 0) {
|
|
6598
|
+
fail(opts, ExitCode.InputValidation, {
|
|
6599
|
+
error: "pass --name and --file for a single asset, or --dir for a batch",
|
|
6600
|
+
code: "asset-rejected",
|
|
6601
|
+
remediation: "tendril record asset --set <dir> --rep <slug> --dir <downloads-dir>"
|
|
6602
|
+
});
|
|
6603
|
+
}
|
|
6426
6604
|
try {
|
|
6427
|
-
ingestAsset(opts.setDir, opts.rep, opts.name,
|
|
6605
|
+
ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync15(path24.resolve(opts.file)));
|
|
6428
6606
|
emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
|
|
6429
6607
|
process.stdout.write(`ingested ${opts.rep}/${opts.name}
|
|
6430
6608
|
`);
|
|
@@ -6452,7 +6630,7 @@ ${status.reps.filter((r) => r.missing.length > 0).length} rep(s) pending
|
|
|
6452
6630
|
function runRecordFinish(opts) {
|
|
6453
6631
|
const manifest = loadManifest(opts.setDir);
|
|
6454
6632
|
const derived = deriveRoles(opts.setDir, manifest);
|
|
6455
|
-
const roles = opts.rolesFile !== void 0 ? { ...JSON.parse(
|
|
6633
|
+
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
6634
|
emitData(opts, { derived, confirmed: opts.confirmRoles }, () => {
|
|
6457
6635
|
process.stdout.write(`derived mains: ${derived.main.join(", ") || "(none)"}
|
|
6458
6636
|
`);
|
|
@@ -6479,11 +6657,11 @@ function runRecordFinish(opts) {
|
|
|
6479
6657
|
});
|
|
6480
6658
|
}
|
|
6481
6659
|
const updated = { ...manifest, roles };
|
|
6482
|
-
writeFileSync8(
|
|
6660
|
+
writeFileSync8(path24.join(opts.setDir, "recording-set.json"), `${JSON.stringify(updated, null, 1)}
|
|
6483
6661
|
`);
|
|
6484
6662
|
if (!opts.json) process.stdout.write("roles written to recording-set.json\n");
|
|
6485
6663
|
}
|
|
6486
|
-
var ENVELOPE_HELP;
|
|
6664
|
+
var ENVELOPE_HELP, isAutoFetchAssetUrl;
|
|
6487
6665
|
var init_record = __esm({
|
|
6488
6666
|
"packages/cli/src/commands/record.ts"() {
|
|
6489
6667
|
"use strict";
|
|
@@ -6491,6 +6669,7 @@ var init_record = __esm({
|
|
|
6491
6669
|
init_src();
|
|
6492
6670
|
init_output();
|
|
6493
6671
|
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.';
|
|
6672
|
+
isAutoFetchAssetUrl = (url) => isFigmaAssetUrl(url) || isLocalAssetUrl(url);
|
|
6494
6673
|
}
|
|
6495
6674
|
});
|
|
6496
6675
|
|
|
@@ -6681,8 +6860,8 @@ var init_engine_curated = __esm({
|
|
|
6681
6860
|
});
|
|
6682
6861
|
|
|
6683
6862
|
// packages/generate/src/loop.ts
|
|
6684
|
-
import { existsSync as
|
|
6685
|
-
import
|
|
6863
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync5, readFileSync as readFileSync16, renameSync, writeFileSync as writeFileSync9 } from "node:fs";
|
|
6864
|
+
import path25 from "node:path";
|
|
6686
6865
|
import { z as z11 } from "zod";
|
|
6687
6866
|
function objective(scores, behaviors) {
|
|
6688
6867
|
const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
|
|
@@ -6713,9 +6892,9 @@ ${preludeLines.join("\n")}` : ""}
|
|
|
6713
6892
|
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
6893
|
}
|
|
6715
6894
|
function archivePriorRun(outDir) {
|
|
6716
|
-
if (!
|
|
6895
|
+
if (!existsSync19(path25.join(outDir, "run-log.json")) && !existsSync19(path25.join(outDir, "loop-state.json"))) return void 0;
|
|
6717
6896
|
let n = 1;
|
|
6718
|
-
while (
|
|
6897
|
+
while (existsSync19(`${outDir}-prev-${n}`)) n += 1;
|
|
6719
6898
|
renameSync(outDir, `${outDir}-prev-${n}`);
|
|
6720
6899
|
return `${outDir}-prev-${n}`;
|
|
6721
6900
|
}
|
|
@@ -6724,14 +6903,14 @@ async function runEngineLoop(opts) {
|
|
|
6724
6903
|
const plateau = opts.plateau ?? 2;
|
|
6725
6904
|
const progress = opts.onProgress ?? (() => {
|
|
6726
6905
|
});
|
|
6727
|
-
const statePath =
|
|
6728
|
-
const resuming = opts.resume === true &&
|
|
6906
|
+
const statePath = path25.join(opts.outDir, "loop-state.json");
|
|
6907
|
+
const resuming = opts.resume === true && existsSync19(statePath);
|
|
6729
6908
|
if (!resuming) {
|
|
6730
6909
|
const archived = archivePriorRun(opts.outDir);
|
|
6731
6910
|
if (archived !== void 0) progress(`previous run archived to ${archived}`);
|
|
6732
6911
|
}
|
|
6733
6912
|
mkdirSync5(opts.outDir, { recursive: true });
|
|
6734
|
-
const scratch =
|
|
6913
|
+
const scratch = path25.join(opts.outDir, ".candidate");
|
|
6735
6914
|
let attempts = [];
|
|
6736
6915
|
let log = [];
|
|
6737
6916
|
let best;
|
|
@@ -6739,7 +6918,7 @@ async function runEngineLoop(opts) {
|
|
|
6739
6918
|
let nonAccepted = 0;
|
|
6740
6919
|
let stopReason = "max-iterations";
|
|
6741
6920
|
if (resuming) {
|
|
6742
|
-
const restored = LoopStateSchema.parse(JSON.parse(
|
|
6921
|
+
const restored = LoopStateSchema.parse(JSON.parse(readFileSync16(statePath, "utf8")));
|
|
6743
6922
|
attempts = restored.attempts;
|
|
6744
6923
|
log = restored.iterations;
|
|
6745
6924
|
spentUsd = restored.spentUsd;
|
|
@@ -6759,7 +6938,7 @@ async function runEngineLoop(opts) {
|
|
|
6759
6938
|
};
|
|
6760
6939
|
const writeCandidate = (files) => {
|
|
6761
6940
|
mkdirSync5(scratch, { recursive: true });
|
|
6762
|
-
for (const [name, content] of Object.entries(files)) writeFileSync9(
|
|
6941
|
+
for (const [name, content] of Object.entries(files)) writeFileSync9(path25.join(scratch, name), content);
|
|
6763
6942
|
};
|
|
6764
6943
|
const scoreCandidate = async (candidate, iter, usd, modelMs) => {
|
|
6765
6944
|
writeCandidate(candidate.files);
|
|
@@ -6817,8 +6996,8 @@ async function runEngineLoop(opts) {
|
|
|
6817
6996
|
const usd = candidate.usage?.usd ?? 0;
|
|
6818
6997
|
spentUsd += usd;
|
|
6819
6998
|
if (candidate.raw !== void 0) {
|
|
6820
|
-
mkdirSync5(
|
|
6821
|
-
writeFileSync9(
|
|
6999
|
+
mkdirSync5(path25.join(opts.outDir, "responses"), { recursive: true });
|
|
7000
|
+
writeFileSync9(path25.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
|
|
6822
7001
|
}
|
|
6823
7002
|
if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
|
|
6824
7003
|
const finish = candidate.usage?.finishReason ?? "?";
|
|
@@ -6844,10 +7023,10 @@ async function runEngineLoop(opts) {
|
|
|
6844
7023
|
}
|
|
6845
7024
|
}
|
|
6846
7025
|
}
|
|
6847
|
-
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync9(
|
|
7026
|
+
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync9(path25.join(opts.outDir, name), content);
|
|
6848
7027
|
const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
|
|
6849
7028
|
writeFileSync9(
|
|
6850
|
-
|
|
7029
|
+
path25.join(opts.outDir, "run-log.json"),
|
|
6851
7030
|
`${JSON.stringify(
|
|
6852
7031
|
{
|
|
6853
7032
|
...opts.meta,
|
|
@@ -6914,8 +7093,8 @@ var init_loop2 = __esm({
|
|
|
6914
7093
|
});
|
|
6915
7094
|
|
|
6916
7095
|
// packages/generate/src/brief.ts
|
|
6917
|
-
import { existsSync as
|
|
6918
|
-
import
|
|
7096
|
+
import { existsSync as existsSync20, readFileSync as readFileSync17 } from "node:fs";
|
|
7097
|
+
import path26 from "node:path";
|
|
6919
7098
|
function singleAxes2(name) {
|
|
6920
7099
|
const parsed = parseVariantAxes(name);
|
|
6921
7100
|
if (parsed === void 0) return void 0;
|
|
@@ -7111,7 +7290,7 @@ function authorBehaviors(api) {
|
|
|
7111
7290
|
return { behaviors, prelude: { controls: interactive ? ["> *"] : [], textInputs: [] }, disclosures };
|
|
7112
7291
|
}
|
|
7113
7292
|
function envelopeText(file) {
|
|
7114
|
-
return JSON.parse(
|
|
7293
|
+
return envelopeFirstTextPart(JSON.parse(readFileSync17(file, "utf8")));
|
|
7115
7294
|
}
|
|
7116
7295
|
function recordedFontNeeds(setDir) {
|
|
7117
7296
|
const byFamily = /* @__PURE__ */ new Map();
|
|
@@ -7146,13 +7325,13 @@ function recordedFontNeeds(setDir) {
|
|
|
7146
7325
|
}
|
|
7147
7326
|
};
|
|
7148
7327
|
const manifest = loadManifest(setDir);
|
|
7149
|
-
const setDefs =
|
|
7150
|
-
if (
|
|
7328
|
+
const setDefs = path26.join(setDir, "get_variable_defs.json");
|
|
7329
|
+
if (existsSync20(setDefs)) fromDefs(envelopeText(setDefs));
|
|
7151
7330
|
for (const rep of manifest.reps) {
|
|
7152
|
-
const ctx =
|
|
7153
|
-
if (
|
|
7154
|
-
const defs =
|
|
7155
|
-
if (
|
|
7331
|
+
const ctx = path26.join(setDir, rep.slug, "get_design_context.json");
|
|
7332
|
+
if (existsSync20(ctx)) fromEmission(envelopeText(ctx));
|
|
7333
|
+
const defs = path26.join(setDir, rep.slug, "get_variable_defs.json");
|
|
7334
|
+
if (existsSync20(defs)) fromDefs(envelopeText(defs));
|
|
7156
7335
|
}
|
|
7157
7336
|
return [...byFamily.entries()].map(([family, paired]) => {
|
|
7158
7337
|
const weights = /* @__PURE__ */ new Set([...paired, ...unpaired]);
|
|
@@ -7171,8 +7350,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
7171
7350
|
const poses = [];
|
|
7172
7351
|
const missing = [];
|
|
7173
7352
|
for (const rep of manifest.reps) {
|
|
7174
|
-
const metaFile =
|
|
7175
|
-
if (!
|
|
7353
|
+
const metaFile = path26.join(setDir, rep.slug, "get_metadata.json");
|
|
7354
|
+
if (!existsSync20(metaFile)) {
|
|
7176
7355
|
missing.push(rep.slug);
|
|
7177
7356
|
continue;
|
|
7178
7357
|
}
|
|
@@ -7186,8 +7365,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
7186
7365
|
if (missing.length > 0) {
|
|
7187
7366
|
throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
|
|
7188
7367
|
}
|
|
7189
|
-
const setMeta =
|
|
7190
|
-
const latticeNames = manifest.latticeNames ?? (
|
|
7368
|
+
const setMeta = path26.join(setDir, "get_metadata.json");
|
|
7369
|
+
const latticeNames = manifest.latticeNames ?? (existsSync20(setMeta) ? [...envelopeText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
|
|
7191
7370
|
const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
|
|
7192
7371
|
const recordedFonts = recordedFontFamilies(setDir);
|
|
7193
7372
|
const api = authorComponentApi({
|
|
@@ -7299,10 +7478,11 @@ var init_brief = __esm({
|
|
|
7299
7478
|
});
|
|
7300
7479
|
|
|
7301
7480
|
// packages/generate/src/segments.ts
|
|
7302
|
-
import { existsSync as
|
|
7303
|
-
import
|
|
7481
|
+
import { existsSync as existsSync21, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "node:fs";
|
|
7482
|
+
import path27 from "node:path";
|
|
7304
7483
|
function repText(set, rep, tool) {
|
|
7305
|
-
|
|
7484
|
+
const env = JSON.parse(readFileSync18(path27.join(set, rep, `${tool}.json`), "utf8"));
|
|
7485
|
+
return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
|
|
7306
7486
|
}
|
|
7307
7487
|
function stripFigmaInstructions(emission) {
|
|
7308
7488
|
const STYLE_FACTS = "These styles are contained in the design:";
|
|
@@ -7362,17 +7542,17 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
|
|
|
7362
7542
|
function buildSegments(task, mode = "fenced") {
|
|
7363
7543
|
const SET = task.set;
|
|
7364
7544
|
let rawDefs = {};
|
|
7365
|
-
if (
|
|
7366
|
-
const text = JSON.parse(
|
|
7545
|
+
if (existsSync21(path27.join(SET, "get_variable_defs.json"))) {
|
|
7546
|
+
const text = envelopeFirstTextPart(JSON.parse(readFileSync18(path27.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
|
|
7367
7547
|
try {
|
|
7368
7548
|
rawDefs = JSON.parse(text);
|
|
7369
7549
|
} catch {
|
|
7370
7550
|
}
|
|
7371
7551
|
} else {
|
|
7372
7552
|
for (const cfg of task.configs) {
|
|
7373
|
-
const f =
|
|
7374
|
-
if (!
|
|
7375
|
-
const text = JSON.parse(
|
|
7553
|
+
const f = path27.join(SET, cfg.rep, "get_variable_defs.json");
|
|
7554
|
+
if (!existsSync21(f)) continue;
|
|
7555
|
+
const text = envelopeFirstTextPart(JSON.parse(readFileSync18(f, "utf8"))) || "{}";
|
|
7376
7556
|
try {
|
|
7377
7557
|
for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
|
|
7378
7558
|
} catch {
|
|
@@ -7380,8 +7560,8 @@ function buildSegments(task, mode = "fenced") {
|
|
|
7380
7560
|
}
|
|
7381
7561
|
}
|
|
7382
7562
|
const emissionTexts = task.configs.map((cfg) => {
|
|
7383
|
-
const f =
|
|
7384
|
-
return
|
|
7563
|
+
const f = path27.join(SET, cfg.rep, "get_design_context.json");
|
|
7564
|
+
return existsSync21(f) ? envelopeFirstTextPart(JSON.parse(readFileSync18(f, "utf8"))) : "";
|
|
7385
7565
|
});
|
|
7386
7566
|
const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
|
|
7387
7567
|
const defs = JSON.stringify(map, null, 1);
|
|
@@ -7396,9 +7576,9 @@ ${defs}
|
|
|
7396
7576
|
for (const cfg of task.configs) {
|
|
7397
7577
|
const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
|
|
7398
7578
|
const emission = stripFigmaInstructions(repText(SET, cfg.rep, "get_design_context"));
|
|
7399
|
-
const assets =
|
|
7579
|
+
const assets = readdirSync5(path27.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
|
|
7400
7580
|
\`\`\`svg
|
|
7401
|
-
${
|
|
7581
|
+
${readFileSync18(path27.join(SET, cfg.rep, f), "utf8")}
|
|
7402
7582
|
\`\`\``).join("\n");
|
|
7403
7583
|
parts.push(`
|
|
7404
7584
|
## Config ${cfg.rep} \u2192 ${cfg.component} ${JSON.stringify(cfg.props)}
|
|
@@ -7431,6 +7611,7 @@ var cssIdent;
|
|
|
7431
7611
|
var init_segments = __esm({
|
|
7432
7612
|
"packages/generate/src/segments.ts"() {
|
|
7433
7613
|
"use strict";
|
|
7614
|
+
init_src();
|
|
7434
7615
|
cssIdent = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
7435
7616
|
}
|
|
7436
7617
|
});
|
|
@@ -7489,9 +7670,9 @@ var init_adapter = __esm({
|
|
|
7489
7670
|
});
|
|
7490
7671
|
|
|
7491
7672
|
// packages/generate/src/bundle-emit.ts
|
|
7492
|
-
import { createHash as
|
|
7493
|
-
import { existsSync as
|
|
7494
|
-
import
|
|
7673
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
7674
|
+
import { existsSync as existsSync22, readFileSync as readFileSync19, readdirSync as readdirSync6, writeFileSync as writeFileSync10 } from "node:fs";
|
|
7675
|
+
import path28 from "node:path";
|
|
7495
7676
|
function pinFromConfigs(configs) {
|
|
7496
7677
|
const domains = /* @__PURE__ */ new Map();
|
|
7497
7678
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -7540,22 +7721,22 @@ function cssFontFamilies(css) {
|
|
|
7540
7721
|
return [...out];
|
|
7541
7722
|
}
|
|
7542
7723
|
function countLatticeSymbols(setDir) {
|
|
7543
|
-
const manifestFile =
|
|
7544
|
-
if (
|
|
7724
|
+
const manifestFile = path28.join(setDir, "recording-set.json");
|
|
7725
|
+
if (existsSync22(manifestFile)) {
|
|
7545
7726
|
try {
|
|
7546
|
-
const lattice = JSON.parse(
|
|
7727
|
+
const lattice = JSON.parse(readFileSync19(manifestFile, "utf8")).latticeNames;
|
|
7547
7728
|
if (lattice !== void 0 && lattice.length > 0) return lattice.length;
|
|
7548
7729
|
} catch {
|
|
7549
7730
|
}
|
|
7550
7731
|
}
|
|
7551
7732
|
const files = [
|
|
7552
|
-
|
|
7553
|
-
...
|
|
7554
|
-
].filter((f) =>
|
|
7733
|
+
path28.join(setDir, "get_metadata.json"),
|
|
7734
|
+
...existsSync22(setDir) ? readdirSync6(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path28.join(setDir, f)) : []
|
|
7735
|
+
].filter((f) => existsSync22(f));
|
|
7555
7736
|
if (files.length === 0) return null;
|
|
7556
7737
|
let count = 0;
|
|
7557
7738
|
for (const f of files) {
|
|
7558
|
-
const text = JSON.parse(
|
|
7739
|
+
const text = envelopeTextContent(JSON.parse(readFileSync19(f, "utf8")));
|
|
7559
7740
|
count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
|
|
7560
7741
|
}
|
|
7561
7742
|
return count > 0 ? count : null;
|
|
@@ -7563,23 +7744,23 @@ function countLatticeSymbols(setDir) {
|
|
|
7563
7744
|
function recordingSetHash(setDir, configs) {
|
|
7564
7745
|
const relPaths = [];
|
|
7565
7746
|
for (const name of ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json"]) {
|
|
7566
|
-
if (
|
|
7747
|
+
if (existsSync22(path28.join(setDir, name))) relPaths.push(name);
|
|
7567
7748
|
}
|
|
7568
7749
|
for (const cfg of configs) {
|
|
7569
7750
|
for (const f of ["get_design_context.json", "get_metadata.json", "get_screenshot.json", "get_variable_defs.json"]) {
|
|
7570
|
-
if (
|
|
7751
|
+
if (existsSync22(path28.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
|
|
7571
7752
|
}
|
|
7572
|
-
if (
|
|
7573
|
-
for (const asset of
|
|
7753
|
+
if (existsSync22(path28.join(setDir, cfg.rep))) {
|
|
7754
|
+
for (const asset of readdirSync6(path28.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
|
|
7574
7755
|
relPaths.push(`${cfg.rep}/${asset}`);
|
|
7575
7756
|
}
|
|
7576
7757
|
}
|
|
7577
7758
|
}
|
|
7578
7759
|
return hashRecordingSet(
|
|
7579
7760
|
relPaths,
|
|
7580
|
-
(p) => new Uint8Array(
|
|
7761
|
+
(p) => new Uint8Array(readFileSync19(path28.join(setDir, p))),
|
|
7581
7762
|
(chunks) => {
|
|
7582
|
-
const h =
|
|
7763
|
+
const h = createHash3("sha256");
|
|
7583
7764
|
for (const c of chunks) h.update(c);
|
|
7584
7765
|
return h.digest("hex");
|
|
7585
7766
|
}
|
|
@@ -7595,8 +7776,8 @@ function emitBundleV1(opts) {
|
|
|
7595
7776
|
const lattice = countLatticeSymbols(opts.task.set);
|
|
7596
7777
|
const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:"));
|
|
7597
7778
|
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) =>
|
|
7779
|
+
const cssFiles = ["styles.css", "tokens.css"].map((f) => path28.join(opts.bundleDir, f)).filter((f) => existsSync22(f));
|
|
7780
|
+
const families = cssFontFamilies(cssFiles.map((f) => readFileSync19(f, "utf8")).join("\n"));
|
|
7600
7781
|
const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
|
|
7601
7782
|
family: f.family,
|
|
7602
7783
|
weight: f.weight,
|
|
@@ -7625,7 +7806,7 @@ function emitBundleV1(opts) {
|
|
|
7625
7806
|
// resolvable via verify's --set override).
|
|
7626
7807
|
path: (() => {
|
|
7627
7808
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
7628
|
-
const rel =
|
|
7809
|
+
const rel = path28.relative(base, opts.task.set);
|
|
7629
7810
|
return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
|
|
7630
7811
|
})(),
|
|
7631
7812
|
component: opts.componentName,
|
|
@@ -7649,14 +7830,14 @@ function emitBundleV1(opts) {
|
|
|
7649
7830
|
})
|
|
7650
7831
|
};
|
|
7651
7832
|
const written = [];
|
|
7652
|
-
const manifestPath2 =
|
|
7833
|
+
const manifestPath2 = path28.join(opts.bundleDir, "component.json");
|
|
7653
7834
|
writeFileSync10(manifestPath2, `${JSON.stringify(manifest, null, 2)}
|
|
7654
7835
|
`);
|
|
7655
7836
|
written.push(manifestPath2);
|
|
7656
|
-
const stylesPath =
|
|
7657
|
-
if (
|
|
7837
|
+
const stylesPath = path28.join(opts.bundleDir, "styles.css");
|
|
7838
|
+
if (existsSync22(stylesPath)) {
|
|
7658
7839
|
const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
|
|
7659
|
-
const current =
|
|
7840
|
+
const current = readFileSync19(stylesPath, "utf8");
|
|
7660
7841
|
const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
|
|
7661
7842
|
writeFileSync10(stylesPath, `${comment}
|
|
7662
7843
|
${stripped}`);
|
|
@@ -7671,6 +7852,7 @@ var init_bundle_emit = __esm({
|
|
|
7671
7852
|
"packages/generate/src/bundle-emit.ts"() {
|
|
7672
7853
|
"use strict";
|
|
7673
7854
|
init_src6();
|
|
7855
|
+
init_src();
|
|
7674
7856
|
init_src4();
|
|
7675
7857
|
BARS = {
|
|
7676
7858
|
pass: { sim: 0.95, ink: 0.95 },
|
|
@@ -7705,8 +7887,8 @@ __export(fonts_exports, {
|
|
|
7705
7887
|
runFontsResolveSet: () => runFontsResolveSet,
|
|
7706
7888
|
runFontsStatus: () => runFontsStatus
|
|
7707
7889
|
});
|
|
7708
|
-
import { existsSync as
|
|
7709
|
-
import
|
|
7890
|
+
import { existsSync as existsSync23, readFileSync as readFileSync20 } from "node:fs";
|
|
7891
|
+
import path29 from "node:path";
|
|
7710
7892
|
async function runFontsResolve(opts) {
|
|
7711
7893
|
const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
|
|
7712
7894
|
emitData(opts, result, () => {
|
|
@@ -7717,11 +7899,11 @@ async function runFontsResolve(opts) {
|
|
|
7717
7899
|
});
|
|
7718
7900
|
if (result.failures.length > 0) {
|
|
7719
7901
|
warn(opts, `${result.failures.length} face(s) unresolved \u2014 verification will refuse to score under substitution`);
|
|
7720
|
-
process.
|
|
7902
|
+
process.exitCode = ExitCode.FontsUnproven;
|
|
7721
7903
|
}
|
|
7722
7904
|
}
|
|
7723
7905
|
async function runFontsResolveSet(opts) {
|
|
7724
|
-
const setDir =
|
|
7906
|
+
const setDir = path29.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
|
|
7725
7907
|
let needs = [];
|
|
7726
7908
|
try {
|
|
7727
7909
|
needs = recordedFontNeeds(setDir);
|
|
@@ -7766,20 +7948,20 @@ async function runFontsResolveSet(opts) {
|
|
|
7766
7948
|
});
|
|
7767
7949
|
if (failures.length > 0) {
|
|
7768
7950
|
warn(opts, `${failures.length} face(s) unresolved \u2014 verification will refuse to score under substitution`);
|
|
7769
|
-
process.
|
|
7951
|
+
process.exitCode = ExitCode.FontsUnproven;
|
|
7770
7952
|
}
|
|
7771
7953
|
}
|
|
7772
7954
|
function runFontsStatus(opts) {
|
|
7773
|
-
const manifestPath2 =
|
|
7774
|
-
if (!
|
|
7955
|
+
const manifestPath2 = path29.join(opts.cacheDir, "manifest.json");
|
|
7956
|
+
if (!existsSync23(manifestPath2)) {
|
|
7775
7957
|
fail(opts, ExitCode.FontsUnproven, {
|
|
7776
7958
|
error: `no font cache at ${opts.cacheDir}`,
|
|
7777
7959
|
code: "fonts-unresolved",
|
|
7778
7960
|
remediation: 'Run `tendril fonts resolve --set <recording-dir>` (or `tendril fonts resolve "<Family>" --weights 400 500 600`) first.'
|
|
7779
7961
|
});
|
|
7780
7962
|
}
|
|
7781
|
-
const faces = JSON.parse(
|
|
7782
|
-
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(
|
|
7963
|
+
const faces = JSON.parse(readFileSync20(manifestPath2, "utf8"));
|
|
7964
|
+
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path29.resolve(opts.lock), opts.cacheDir) : null;
|
|
7783
7965
|
emitData(opts, { faces, lockVerdicts }, () => {
|
|
7784
7966
|
for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
|
|
7785
7967
|
`);
|
|
@@ -7828,37 +8010,6 @@ var init_fonts = __esm({
|
|
|
7828
8010
|
}
|
|
7829
8011
|
});
|
|
7830
8012
|
|
|
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
8013
|
// packages/cli/src/font-guidance.ts
|
|
7863
8014
|
function fontsUnprovenRemediation(setDir) {
|
|
7864
8015
|
if (setDir !== void 0) {
|
|
@@ -7895,7 +8046,7 @@ __export(verify_exports, {
|
|
|
7895
8046
|
interactionCoverage: () => interactionCoverage,
|
|
7896
8047
|
runVerify: () => runVerify
|
|
7897
8048
|
});
|
|
7898
|
-
import { existsSync as
|
|
8049
|
+
import { existsSync as existsSync24, readFileSync as readFileSync21 } from "node:fs";
|
|
7899
8050
|
import path30 from "node:path";
|
|
7900
8051
|
function interactionCoverage(behaviors) {
|
|
7901
8052
|
const interaction = behaviors.filter((b) => !b.id.startsWith("prelude:"));
|
|
@@ -7934,7 +8085,7 @@ async function runVerify(opts) {
|
|
|
7934
8085
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
7935
8086
|
const setOverride = opts.set !== void 0 ? path30.resolve(callerCwd, opts.set) : void 0;
|
|
7936
8087
|
opts = { ...opts, bundleDir: path30.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
|
|
7937
|
-
if (!
|
|
8088
|
+
if (!existsSync24(opts.bundleDir)) {
|
|
7938
8089
|
fail(opts, ExitCode.InputValidation, {
|
|
7939
8090
|
error: `bundle directory not found: ${opts.bundleDir}`,
|
|
7940
8091
|
code: "bundle-missing",
|
|
@@ -7943,7 +8094,7 @@ async function runVerify(opts) {
|
|
|
7943
8094
|
}
|
|
7944
8095
|
const manifestPath2 = path30.join(opts.bundleDir, "component.json");
|
|
7945
8096
|
let manifest;
|
|
7946
|
-
if (
|
|
8097
|
+
if (existsSync24(manifestPath2)) {
|
|
7947
8098
|
const { manifest: parsed, issues } = readBundleManifest(readFileSync21(manifestPath2, "utf8"));
|
|
7948
8099
|
if (issues.length > 0) {
|
|
7949
8100
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -7973,11 +8124,11 @@ async function runVerify(opts) {
|
|
|
7973
8124
|
const resolveSetDir = (p) => {
|
|
7974
8125
|
if (path30.isAbsolute(p)) return p;
|
|
7975
8126
|
const fromRepo = path30.resolve(REPO_ROOT, p);
|
|
7976
|
-
if (
|
|
8127
|
+
if (existsSync24(fromRepo)) return fromRepo;
|
|
7977
8128
|
return path30.resolve(callerCwd, p);
|
|
7978
8129
|
};
|
|
7979
8130
|
const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
|
|
7980
|
-
if (!
|
|
8131
|
+
if (!existsSync24(path30.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path30.resolve(t.set) === path30.resolve(setDir))) {
|
|
7981
8132
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
7982
8133
|
error: `recording set not found or unmanifested: ${setDir}`,
|
|
7983
8134
|
code: "recording-set-missing",
|
|
@@ -7985,7 +8136,7 @@ async function runVerify(opts) {
|
|
|
7985
8136
|
});
|
|
7986
8137
|
}
|
|
7987
8138
|
const registry = Object.values(TASKS).find((t) => path30.resolve(t.set) === path30.resolve(setDir));
|
|
7988
|
-
if (registry !== void 0 && !
|
|
8139
|
+
if (registry !== void 0 && !existsSync24(path30.join(setDir, "recording-set.json"))) {
|
|
7989
8140
|
const recordedSlugs = registry.configs.map((c) => c.rep);
|
|
7990
8141
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
7991
8142
|
unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
@@ -8010,7 +8161,7 @@ async function runVerify(opts) {
|
|
|
8010
8161
|
}
|
|
8011
8162
|
for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
|
|
8012
8163
|
const p = path30.join(opts.bundleDir, name);
|
|
8013
|
-
if (!
|
|
8164
|
+
if (!existsSync24(p)) continue;
|
|
8014
8165
|
const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync21(p)));
|
|
8015
8166
|
if (issues.length > 0) {
|
|
8016
8167
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -8035,7 +8186,7 @@ async function runVerify(opts) {
|
|
|
8035
8186
|
});
|
|
8036
8187
|
}
|
|
8037
8188
|
const missing = task.configs.filter(
|
|
8038
|
-
(c) => !
|
|
8189
|
+
(c) => !existsSync24(path30.join(task.set, c.rep, "get_screenshot.json")) || !existsSync24(path30.join(task.set, c.rep, "get_metadata.json"))
|
|
8039
8190
|
);
|
|
8040
8191
|
if (missing.length > 0) {
|
|
8041
8192
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -8048,7 +8199,7 @@ async function runVerify(opts) {
|
|
|
8048
8199
|
const evidenceDir = path30.join(opts.bundleDir, "verify-evidence");
|
|
8049
8200
|
const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
8050
8201
|
const quality = await checkBundleQuality(opts.bundleDir, task.entry);
|
|
8051
|
-
const bundleCss = ["tokens.css", "styles.css"].map((f) => path30.join(opts.bundleDir, f)).filter((f) =>
|
|
8202
|
+
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
8203
|
const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
|
|
8053
8204
|
const parity = await checkHoverParity(task, opts.bundleDir);
|
|
8054
8205
|
const behaviors = [...await checkBehaviors(task, opts.bundleDir), ...parity];
|
|
@@ -8236,11 +8387,11 @@ __export(engine_exports, {
|
|
|
8236
8387
|
runEngineBrief: () => runEngineBrief,
|
|
8237
8388
|
runEngineScore: () => runEngineScore
|
|
8238
8389
|
});
|
|
8239
|
-
import { existsSync as
|
|
8390
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync6, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "node:fs";
|
|
8240
8391
|
import path31 from "node:path";
|
|
8241
8392
|
function resolveEngineTask(opts, callerCwd) {
|
|
8242
8393
|
const asPath = path31.resolve(callerCwd, opts.taskOrSet);
|
|
8243
|
-
const isSet =
|
|
8394
|
+
const isSet = existsSync25(path31.join(asPath, "recording-set.json"));
|
|
8244
8395
|
const registry = TASKS[opts.taskOrSet];
|
|
8245
8396
|
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet };
|
|
8246
8397
|
if (isSet) {
|
|
@@ -8270,7 +8421,7 @@ function runEngineBrief(opts) {
|
|
|
8270
8421
|
const segments = buildSegments(task, "files");
|
|
8271
8422
|
let notRecorded;
|
|
8272
8423
|
const manifestPath2 = path31.join(task.set, "recording-set.json");
|
|
8273
|
-
if (
|
|
8424
|
+
if (existsSync25(manifestPath2)) {
|
|
8274
8425
|
notRecorded = JSON.parse(readFileSync22(manifestPath2, "utf8")).notRecorded;
|
|
8275
8426
|
}
|
|
8276
8427
|
const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
|
|
@@ -8278,7 +8429,7 @@ function runEngineBrief(opts) {
|
|
|
8278
8429
|
=== UNVERIFIED SURFACE (recorded-set disclosure \u2014 these poses were never recorded; nothing verifies them) ===
|
|
8279
8430
|
${notRecorded}` : "";
|
|
8280
8431
|
let fontProvisioning;
|
|
8281
|
-
if (
|
|
8432
|
+
if (existsSync25(manifestPath2)) {
|
|
8282
8433
|
const provided = resolvedFontFamilies().map((f) => f.family);
|
|
8283
8434
|
const unprovided = recordedFontFamilies(task.set).filter((r) => !provided.some((p) => p.toLowerCase() === r.toLowerCase()));
|
|
8284
8435
|
if (unprovided.length > 0) {
|
|
@@ -8315,7 +8466,7 @@ ${segments}`;
|
|
|
8315
8466
|
...notRecorded !== void 0 && notRecorded !== "" ? { notRecorded } : {},
|
|
8316
8467
|
...fontProvisioning !== void 0 ? { fontProvisioning } : {},
|
|
8317
8468
|
modelSelection: {
|
|
8318
|
-
instruction: "The model declaration is MECHANICAL: `engine score` refuses to run without --model.
|
|
8469
|
+
instruction: "The model declaration is MECHANICAL: `engine score` refuses to run without --model. ONLY ask when the answer can take effect \u2014 i.e. you can delegate implementation to an agent running the chosen model; if you cannot delegate in this session, skip the question, build as yourself, and declare your own model honestly (a question whose answer changes nothing wastes the user's trust \u2014 measured, second Windows run). When you do ask: BEFORE reading the payload, using the template below verbatim where your host renders option dialogs. The audience is non-technical: plain cost/quality language, no model knowledge assumed. In a non-interactive session pick the balanced tier, state the reason in your report, and declare it \u2014 a silent default is not possible.",
|
|
8319
8470
|
questionTemplate: {
|
|
8320
8471
|
prompt: "Which model should build this component? Every choice is scored by the same independent measurement \u2014 a cheaper model may need more attempts, but it can never ship a lower-quality certified result.",
|
|
8321
8472
|
options: [
|
|
@@ -8347,7 +8498,7 @@ async function runEngineScore(opts) {
|
|
|
8347
8498
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
8348
8499
|
const candidateDir = path31.resolve(callerCwd, opts.candidateDir);
|
|
8349
8500
|
const { task, name, apiPin } = resolveEngineTask(opts, callerCwd);
|
|
8350
|
-
if (!
|
|
8501
|
+
if (!existsSync25(candidateDir)) {
|
|
8351
8502
|
fail(opts, ExitCode.InputValidation, {
|
|
8352
8503
|
error: `candidate directory not found: ${candidateDir}`,
|
|
8353
8504
|
code: "candidate-missing",
|
|
@@ -8469,7 +8620,7 @@ __export(generate_recorded_exports, {
|
|
|
8469
8620
|
runGenerateRecorded: () => runGenerateRecorded
|
|
8470
8621
|
});
|
|
8471
8622
|
import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
|
|
8472
|
-
import { existsSync as
|
|
8623
|
+
import { existsSync as existsSync26 } from "node:fs";
|
|
8473
8624
|
import path32 from "node:path";
|
|
8474
8625
|
async function runGenerateRecorded(opts) {
|
|
8475
8626
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
@@ -8479,7 +8630,7 @@ async function runGenerateRecorded(opts) {
|
|
|
8479
8630
|
let taskName;
|
|
8480
8631
|
let authoredApi;
|
|
8481
8632
|
let composition;
|
|
8482
|
-
const isSet =
|
|
8633
|
+
const isSet = existsSync26(path32.join(recordedAsPath, "recording-set.json"));
|
|
8483
8634
|
const registry = TASKS[opts.recorded];
|
|
8484
8635
|
if (registry !== void 0 && !isSet) {
|
|
8485
8636
|
task = registry;
|
|
@@ -8515,7 +8666,7 @@ async function runGenerateRecorded(opts) {
|
|
|
8515
8666
|
});
|
|
8516
8667
|
}
|
|
8517
8668
|
const missing = task.configs.filter(
|
|
8518
|
-
(c) => !
|
|
8669
|
+
(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
8670
|
);
|
|
8520
8671
|
if (missing.length > 0) {
|
|
8521
8672
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -8728,13 +8879,14 @@ import { CommanderError } from "commander";
|
|
|
8728
8879
|
|
|
8729
8880
|
// packages/cli/src/program.ts
|
|
8730
8881
|
init_src3();
|
|
8882
|
+
init_environment();
|
|
8731
8883
|
import { Command } from "commander";
|
|
8732
8884
|
|
|
8733
8885
|
// packages/cli/src/commands/doctor.ts
|
|
8734
8886
|
init_src4();
|
|
8735
8887
|
init_src();
|
|
8736
|
-
import { existsSync as
|
|
8737
|
-
import
|
|
8888
|
+
import { existsSync as existsSync16, readFileSync as readFileSync13 } from "node:fs";
|
|
8889
|
+
import path21 from "node:path";
|
|
8738
8890
|
|
|
8739
8891
|
// packages/cli/src/describe.ts
|
|
8740
8892
|
var COMMON_EXIT_CODES = {
|
|
@@ -8751,6 +8903,7 @@ function printDescription(description) {
|
|
|
8751
8903
|
|
|
8752
8904
|
// packages/cli/src/commands/doctor.ts
|
|
8753
8905
|
init_env();
|
|
8906
|
+
init_environment();
|
|
8754
8907
|
init_output();
|
|
8755
8908
|
var DEFAULT_MCP_URL = "http://127.0.0.1:3845/mcp";
|
|
8756
8909
|
var DOCTOR_DESCRIPTION = {
|
|
@@ -8811,9 +8964,9 @@ async function runDoctorChecks(options) {
|
|
|
8811
8964
|
remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
|
|
8812
8965
|
});
|
|
8813
8966
|
}
|
|
8814
|
-
const fontManifest =
|
|
8967
|
+
const fontManifest = path21.join(fontCacheDir(), "manifest.json");
|
|
8815
8968
|
checks.push(
|
|
8816
|
-
|
|
8969
|
+
existsSync16(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync13(fontManifest, "utf8")).length} faces)` } : {
|
|
8817
8970
|
name: "font-cache",
|
|
8818
8971
|
ok: true,
|
|
8819
8972
|
detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
|
|
@@ -8834,7 +8987,9 @@ async function runDoctor(flags) {
|
|
|
8834
8987
|
return;
|
|
8835
8988
|
}
|
|
8836
8989
|
const report = await runDoctorChecks({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} });
|
|
8837
|
-
emitData(flags, report, () => {
|
|
8990
|
+
emitData(flags, { version: cliVersion(), ...report }, () => {
|
|
8991
|
+
process.stdout.write(`tendril ${cliVersion()}
|
|
8992
|
+
`);
|
|
8838
8993
|
for (const check of report.checks) {
|
|
8839
8994
|
process.stdout.write(`${check.ok ? "\u2713" : "\u2717"} ${check.name}: ${check.detail}
|
|
8840
8995
|
`);
|
|
@@ -8850,7 +9005,7 @@ async function runDoctor(flags) {
|
|
|
8850
9005
|
init_src3();
|
|
8851
9006
|
import { intro, isCancel, outro, password } from "@clack/prompts";
|
|
8852
9007
|
import fs from "node:fs";
|
|
8853
|
-
import
|
|
9008
|
+
import path22 from "node:path";
|
|
8854
9009
|
init_env();
|
|
8855
9010
|
init_output();
|
|
8856
9011
|
var INIT_DESCRIPTION = {
|
|
@@ -8889,7 +9044,7 @@ async function runInit(flags) {
|
|
|
8889
9044
|
printDescription(INIT_DESCRIPTION);
|
|
8890
9045
|
return;
|
|
8891
9046
|
}
|
|
8892
|
-
const envPath =
|
|
9047
|
+
const envPath = path22.resolve(process.cwd(), ".env");
|
|
8893
9048
|
const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
|
|
8894
9049
|
let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
|
|
8895
9050
|
let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
|
|
@@ -8910,7 +9065,7 @@ async function runInit(flags) {
|
|
|
8910
9065
|
next.set(ENV_KEYS.figma, figmaToken);
|
|
8911
9066
|
next.set(ENV_KEYS.openrouter, openrouterKey);
|
|
8912
9067
|
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 =
|
|
9068
|
+
const gitignorePath = path22.resolve(process.cwd(), ".gitignore");
|
|
8914
9069
|
const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
8915
9070
|
const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
|
|
8916
9071
|
if (flags.dryRun) {
|
|
@@ -8961,7 +9116,7 @@ init_src();
|
|
|
8961
9116
|
init_src5();
|
|
8962
9117
|
init_src2();
|
|
8963
9118
|
import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
|
|
8964
|
-
import { readFileSync as
|
|
9119
|
+
import { readFileSync as readFileSync14, readdirSync as readdirSync3, existsSync as existsSync17 } from "node:fs";
|
|
8965
9120
|
init_env();
|
|
8966
9121
|
init_output();
|
|
8967
9122
|
|
|
@@ -8970,7 +9125,7 @@ init_src2();
|
|
|
8970
9125
|
init_src4();
|
|
8971
9126
|
init_src6();
|
|
8972
9127
|
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync7 } from "node:fs";
|
|
8973
|
-
import
|
|
9128
|
+
import path23 from "node:path";
|
|
8974
9129
|
|
|
8975
9130
|
// packages/cli/src/assets-module.ts
|
|
8976
9131
|
init_src();
|
|
@@ -9306,7 +9461,7 @@ async function runGenerationPipeline(input) {
|
|
|
9306
9461
|
});
|
|
9307
9462
|
const written = [];
|
|
9308
9463
|
if (!input.dryRun) {
|
|
9309
|
-
const dir =
|
|
9464
|
+
const dir = path23.resolve(input.outDir, semantics.componentName);
|
|
9310
9465
|
mkdirSync4(dir, { recursive: true });
|
|
9311
9466
|
const files = {
|
|
9312
9467
|
// Bundle-local tokens: THE emission the component's CSS resolves
|
|
@@ -9330,13 +9485,13 @@ async function runGenerationPipeline(input) {
|
|
|
9330
9485
|
`
|
|
9331
9486
|
};
|
|
9332
9487
|
for (const [name, content] of Object.entries(files)) {
|
|
9333
|
-
const filePath =
|
|
9488
|
+
const filePath = path23.join(dir, name);
|
|
9334
9489
|
writeFileSync7(filePath, content);
|
|
9335
9490
|
written.push(filePath);
|
|
9336
9491
|
}
|
|
9337
9492
|
for (const artifact of emitTokenArtifacts(input.mapping)) {
|
|
9338
|
-
const filePath =
|
|
9339
|
-
mkdirSync4(
|
|
9493
|
+
const filePath = path23.resolve(input.outDir, artifact.path);
|
|
9494
|
+
mkdirSync4(path23.dirname(filePath), { recursive: true });
|
|
9340
9495
|
writeFileSync7(filePath, artifact.content);
|
|
9341
9496
|
written.push(filePath);
|
|
9342
9497
|
}
|
|
@@ -9395,7 +9550,7 @@ var GENERATE_DESCRIPTION = {
|
|
|
9395
9550
|
function resolveProvidedSource(flags, contextFile) {
|
|
9396
9551
|
let raw;
|
|
9397
9552
|
try {
|
|
9398
|
-
raw =
|
|
9553
|
+
raw = readFileSync14(contextFile, "utf8");
|
|
9399
9554
|
} catch {
|
|
9400
9555
|
fail(flags, ExitCode.InputValidation, {
|
|
9401
9556
|
error: `Cannot read context file "${contextFile}".`,
|
|
@@ -9514,11 +9669,11 @@ token mapping (${mapping.flat.length} variables):
|
|
|
9514
9669
|
let initialCode;
|
|
9515
9670
|
let initialSemantics;
|
|
9516
9671
|
try {
|
|
9517
|
-
if (
|
|
9672
|
+
if (existsSync17(flags.out)) {
|
|
9518
9673
|
for (const entry of readdirSync3(flags.out)) {
|
|
9519
9674
|
const cjPath = `${flags.out}/${entry}/component.json`;
|
|
9520
|
-
if (!
|
|
9521
|
-
const cj = JSON.parse(
|
|
9675
|
+
if (!existsSync17(cjPath)) continue;
|
|
9676
|
+
const cj = JSON.parse(readFileSync14(cjPath, "utf8"));
|
|
9522
9677
|
if (cj.name !== void 0 && Array.isArray(cj.props)) {
|
|
9523
9678
|
previousApi = JSON.stringify({
|
|
9524
9679
|
componentName: cj.name,
|
|
@@ -9526,14 +9681,14 @@ token mapping (${mapping.flat.length} variables):
|
|
|
9526
9681
|
});
|
|
9527
9682
|
const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
|
|
9528
9683
|
const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
|
|
9529
|
-
if (flags.refine &&
|
|
9684
|
+
if (flags.refine && existsSync17(tsxPath) && existsSync17(cssPath)) {
|
|
9530
9685
|
initialCode = {
|
|
9531
|
-
tsx:
|
|
9532
|
-
css:
|
|
9686
|
+
tsx: readFileSync14(tsxPath, "utf8"),
|
|
9687
|
+
css: readFileSync14(cssPath, "utf8")
|
|
9533
9688
|
};
|
|
9534
9689
|
const semPath = `${flags.out}/${entry}/semantics.json`;
|
|
9535
|
-
if (
|
|
9536
|
-
initialSemantics = JSON.parse(
|
|
9690
|
+
if (existsSync17(semPath)) {
|
|
9691
|
+
initialSemantics = JSON.parse(readFileSync14(semPath, "utf8"));
|
|
9537
9692
|
}
|
|
9538
9693
|
}
|
|
9539
9694
|
break;
|
|
@@ -9687,7 +9842,7 @@ function buildProgram() {
|
|
|
9687
9842
|
const program = new Command();
|
|
9688
9843
|
program.name("tendril").description(
|
|
9689
9844
|
"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(
|
|
9845
|
+
).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
9846
|
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
9847
|
const flags = globalFlags(cmd);
|
|
9693
9848
|
const local = cmd.opts();
|
|
@@ -9721,11 +9876,11 @@ function buildProgram() {
|
|
|
9721
9876
|
const { runRecordNext: runRecordNext2 } = await Promise.resolve().then(() => (init_record(), record_exports));
|
|
9722
9877
|
runRecordNext2({ ...flags, setDir: cmd.opts()["set"] });
|
|
9723
9878
|
});
|
|
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) => {
|
|
9879
|
+
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").option("--raw-parts", "the file holds a JSON array of response block texts (multi-block transport); each becomes a content part verbatim").action(async (_o, cmd) => {
|
|
9725
9880
|
const flags = globalFlags(cmd.parent.parent);
|
|
9726
9881
|
const local = cmd.opts();
|
|
9727
9882
|
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"] });
|
|
9883
|
+
await runRecordIngest2({ ...flags, setDir: local["set"], rep: local["rep"], tool: local["tool"], file: local["file"], raw: local["raw"], rawParts: local["rawParts"] });
|
|
9729
9884
|
});
|
|
9730
9885
|
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
9886
|
const flags = globalFlags(cmd.parent.parent);
|
|
@@ -9733,11 +9888,11 @@ function buildProgram() {
|
|
|
9733
9888
|
const { runRecordFetch: runRecordFetch2 } = await Promise.resolve().then(() => (init_record(), record_exports));
|
|
9734
9889
|
await runRecordFetch2({ ...flags, setDir: local["set"], rep: local["rep"], tool: local["tool"], url: local["url"] });
|
|
9735
9890
|
});
|
|
9736
|
-
record.command("asset").requiredOption("--set <dir>", "recording set directory").requiredOption("--rep <slug>", "planned rep slug").
|
|
9891
|
+
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
9892
|
const flags = globalFlags(cmd.parent.parent);
|
|
9738
9893
|
const local = cmd.opts();
|
|
9739
9894
|
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"] });
|
|
9895
|
+
runRecordAsset2({ ...flags, setDir: local["set"], rep: local["rep"], name: local["name"], file: local["file"], dir: local["dir"] });
|
|
9741
9896
|
});
|
|
9742
9897
|
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
9898
|
const flags = globalFlags(cmd.parent.parent);
|