@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.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
- return path4.join(runtimePackageRoot(), "node_modules");
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(runtimeNodeModules(), path4.join(dir, "node_modules"), "junction");
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 existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
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 cssUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family).replace(/%20/g, "+")}:wght@${weights.join(";")}&display=swap`;
3092
- const res = await fetch(cssUrl, { headers: { "User-Agent": UA } });
3093
- if (!res.ok) {
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 ${res.status} \u2014 family not available there? For a licensed or private face use \`tendril fonts add\` with a local file` }))
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 = existsSync4(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
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 (!existsSync4(src)) throw new Error(`font file not found: ${src}`);
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 = existsSync4(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
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 = existsSync4(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
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 (!existsSync4(mPath)) return [];
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 = existsSync4(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
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 existsSync5, readFileSync as readFileSync4 } from "node:fs";
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 (!existsSync5(manifestPath2)) return "";
3235
+ if (!existsSync6(manifestPath2)) return "";
3202
3236
  const manifest = JSON.parse(readFileSync4(manifestPath2, "utf8"));
3203
3237
  const resolveFile = (f) => {
3204
- if (path10.isAbsolute(f) && existsSync5(f)) return 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 existsSync6, writeFileSync as writeFileSync4 } from "node:fs";
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 (!existsSync6(mPath)) return out;
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 existsSync7, readFileSync as readFileSync6 } from "node:fs";
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 (!existsSync7(entryTsx)) return { error: `${task.entry} missing` };
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 (!existsSync7(f)) return false;
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) => existsSync7(f)).map((f) => readFileSync6(f, "utf8")).join("\n");
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 existsSync8 } from "node:fs";
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 = existsSync8(cssPath) ? readFileSync7(cssPath, "utf8") : "";
3935
- const tokensCss = existsSync8(tokensPath) ? readFileSync7(tokensPath, "utf8") : void 0;
3936
- if (existsSync8(entryPath)) {
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 existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
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 (!existsSync9(file)) return void 0;
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 (!existsSync9(entryTsx)) return CONFIGS2.map((c) => ({ rep: c.rep, similarity: 0, inkRecall: 0, exact: { similarity: 0, inkRecall: 0 }, pass: false, error: `${task.entry} missing` }));
4082
- const css = ["tokens.css", "styles.css"].map((f) => path15.join(bundleDir, f)).filter((f) => existsSync9(f)).map((f) => readFileSync8(f, "utf8")).join("\n");
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 existsSync10, readFileSync as readFileSync9 } from "node:fs";
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) => existsSync10(f)).map((f) => readFileSync9(f, "utf8")).join("\n");
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 existsSync11, readFileSync as readFileSync10 } from "node:fs";
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 (!existsSync11(entryTsx)) return { error: `${task.entry} missing` };
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) => existsSync11(path17.join(setDir, m, "get_metadata_interior.json")));
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) => existsSync11(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
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) => existsSync11(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
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 existsSync12 } from "node:fs";
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 (!existsSync12(entryTsx)) return { error: `${task.entry} missing` };
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/env.ts
4866
- import { existsSync as existsSync13, readFileSync as readFileSync11 } from "node:fs";
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 = path19.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
4880
- if (!existsSync13(envPath)) return void 0;
4881
- return parseEnv(readFileSync11(envPath, "utf8")).get(name);
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 existsSync16, readFileSync as readFileSync14 } from "node:fs";
6145
- import path23 from "node:path";
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(readFileSync14(file, "utf8"));
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(path23.resolve(file), frame));
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(readFileSync14(path23.resolve(file), "utf8"));
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 runRecordNext(opts) {
6300
- const instruction = nextInstruction(opts.setDir);
6301
- const status = sessionStatus(opts.setDir);
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 && !existsSync16(path23.join(opts.setDir, "get_variable_defs.json"))) {
6304
- const manifest = loadManifest(opts.setDir);
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
- emitData(opts, { 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 }, () => {
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
- emitData(opts, instruction === null ? { complete: true, progress } : { ...instruction, note: `${instruction.note}. ${ENVELOPE_HELP}`, progress }, () => {
6313
- if (instruction === null) {
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
- process.stdout.write(`[${progress.recordedReps}/${progress.totalReps} reps] ${instruction.tool} for ${instruction.slug} (node ${instruction.nodeId})
6318
- \u2192 ${instruction.note}
6319
- \u2192 then: tendril record ingest --set <dir> --rep ${instruction.slug} --tool ${instruction.tool} --file <envelope.json>
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
- let url = opts.url;
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 runRecordIngest(opts) {
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(readFileSync14(path23.resolve(opts.file), "utf8"));
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(path23.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
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
- emitData(opts, { rep: opts.rep, tool: opts.tool, overwrote }, () => {
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, readFileSync14(path23.resolve(opts.file)));
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(readFileSync14(path23.resolve(opts.rolesFile), "utf8")), humanOverride: true } : { main: derived.main, parts: derived.parts, external: derived.external, humanOverride: false };
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(path23.join(opts.setDir, "recording-set.json"), `${JSON.stringify(updated, null, 1)}
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 existsSync17, mkdirSync as mkdirSync5, readFileSync as readFileSync15, renameSync, writeFileSync as writeFileSync9 } from "node:fs";
6685
- import path24 from "node:path";
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 (!existsSync17(path24.join(outDir, "run-log.json")) && !existsSync17(path24.join(outDir, "loop-state.json"))) return void 0;
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 (existsSync17(`${outDir}-prev-${n}`)) n += 1;
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 = path24.join(opts.outDir, "loop-state.json");
6728
- const resuming = opts.resume === true && existsSync17(statePath);
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 = path24.join(opts.outDir, ".candidate");
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(readFileSync15(statePath, "utf8")));
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(path24.join(scratch, name), content);
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(path24.join(opts.outDir, "responses"), { recursive: true });
6821
- writeFileSync9(path24.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
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(path24.join(opts.outDir, name), content);
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
- path24.join(opts.outDir, "run-log.json"),
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 existsSync18, readFileSync as readFileSync16 } from "node:fs";
6918
- import path25 from "node:path";
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(readFileSync16(file, "utf8")).content[0]?.text ?? "";
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 = path25.join(setDir, "get_variable_defs.json");
7150
- if (existsSync18(setDefs)) fromDefs(envelopeText(setDefs));
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 = path25.join(setDir, rep.slug, "get_design_context.json");
7153
- if (existsSync18(ctx)) fromEmission(envelopeText(ctx));
7154
- const defs = path25.join(setDir, rep.slug, "get_variable_defs.json");
7155
- if (existsSync18(defs)) fromDefs(envelopeText(defs));
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 = path25.join(setDir, rep.slug, "get_metadata.json");
7175
- if (!existsSync18(metaFile)) {
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 = path25.join(setDir, "get_metadata.json");
7190
- const latticeNames = manifest.latticeNames ?? (existsSync18(setMeta) ? [...envelopeText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
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 existsSync19, readFileSync as readFileSync17, readdirSync as readdirSync4 } from "node:fs";
7303
- import path26 from "node:path";
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(readFileSync17(path26.join(set, rep, `${tool}.json`), "utf8")).content[0]?.text ?? "";
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 (existsSync19(path26.join(SET, "get_variable_defs.json"))) {
7366
- const text = JSON.parse(readFileSync17(path26.join(SET, "get_variable_defs.json"), "utf8")).content[0]?.text ?? "{}";
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 = path26.join(SET, cfg.rep, "get_variable_defs.json");
7374
- if (!existsSync19(f)) continue;
7375
- const text = JSON.parse(readFileSync17(f, "utf8")).content[0]?.text ?? "{}";
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 = path26.join(SET, cfg.rep, "get_design_context.json");
7384
- return existsSync19(f) ? JSON.parse(readFileSync17(f, "utf8")).content[0]?.text ?? "" : "";
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 = readdirSync4(path26.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
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
- ${readFileSync17(path26.join(SET, cfg.rep, f), "utf8")}
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 createHash2 } from "node:crypto";
7493
- import { existsSync as existsSync20, readFileSync as readFileSync18, readdirSync as readdirSync5, writeFileSync as writeFileSync10 } from "node:fs";
7494
- import path27 from "node:path";
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 = path27.join(setDir, "recording-set.json");
7544
- if (existsSync20(manifestFile)) {
7708
+ const manifestFile = path28.join(setDir, "recording-set.json");
7709
+ if (existsSync22(manifestFile)) {
7545
7710
  try {
7546
- const lattice = JSON.parse(readFileSync18(manifestFile, "utf8")).latticeNames;
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
- path27.join(setDir, "get_metadata.json"),
7553
- ...existsSync20(setDir) ? readdirSync5(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path27.join(setDir, f)) : []
7554
- ].filter((f) => existsSync20(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(readFileSync18(f, "utf8")).content[0]?.text ?? "";
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 (existsSync20(path27.join(setDir, name))) relPaths.push(name);
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 (existsSync20(path27.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
7735
+ if (existsSync22(path28.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
7571
7736
  }
7572
- if (existsSync20(path27.join(setDir, cfg.rep))) {
7573
- for (const asset of readdirSync5(path27.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
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(readFileSync18(path27.join(setDir, p))),
7745
+ (p) => new Uint8Array(readFileSync19(path28.join(setDir, p))),
7581
7746
  (chunks) => {
7582
- const h = createHash2("sha256");
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) => path27.join(opts.bundleDir, f)).filter((f) => existsSync20(f));
7599
- const families = cssFontFamilies(cssFiles.map((f) => readFileSync18(f, "utf8")).join("\n"));
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 = path27.relative(base, opts.task.set);
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 = path27.join(opts.bundleDir, "component.json");
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 = path27.join(opts.bundleDir, "styles.css");
7657
- if (existsSync20(stylesPath)) {
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 = readFileSync18(stylesPath, "utf8");
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 existsSync21, readFileSync as readFileSync19 } from "node:fs";
7709
- import path28 from "node:path";
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.exit(ExitCode.FontsUnproven);
7885
+ process.exitCode = ExitCode.FontsUnproven;
7721
7886
  }
7722
7887
  }
7723
7888
  async function runFontsResolveSet(opts) {
7724
- const setDir = path28.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
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.exit(ExitCode.FontsUnproven);
7934
+ process.exitCode = ExitCode.FontsUnproven;
7770
7935
  }
7771
7936
  }
7772
7937
  function runFontsStatus(opts) {
7773
- const manifestPath2 = path28.join(opts.cacheDir, "manifest.json");
7774
- if (!existsSync21(manifestPath2)) {
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(readFileSync19(manifestPath2, "utf8"));
7782
- const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path28.resolve(opts.lock), opts.cacheDir) : null;
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 existsSync23, readFileSync as readFileSync21 } from "node:fs";
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 (!existsSync23(opts.bundleDir)) {
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 (existsSync23(manifestPath2)) {
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 (existsSync23(fromRepo)) return fromRepo;
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 (!existsSync23(path30.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path30.resolve(t.set) === path30.resolve(setDir))) {
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 && !existsSync23(path30.join(setDir, "recording-set.json"))) {
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 (!existsSync23(p)) continue;
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) => !existsSync23(path30.join(task.set, c.rep, "get_screenshot.json")) || !existsSync23(path30.join(task.set, c.rep, "get_metadata.json"))
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) => existsSync23(f)).map((f) => readFileSync21(f, "utf8")).join("\n");
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 existsSync24, mkdirSync as mkdirSync6, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "node:fs";
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 = existsSync24(path31.join(asPath, "recording-set.json"));
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 (existsSync24(manifestPath2)) {
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 (existsSync24(manifestPath2)) {
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 (!existsSync24(candidateDir)) {
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 existsSync25 } from "node:fs";
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 = existsSync25(path32.join(recordedAsPath, "recording-set.json"));
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) => !existsSync25(path32.join(task.set, c.rep, "get_screenshot.json")) || !existsSync25(path32.join(task.set, c.rep, "get_metadata.json")) || !existsSync25(path32.join(task.set, c.rep, "get_design_context.json"))
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 existsSync14, readFileSync as readFileSync12 } from "node:fs";
8737
- import path20 from "node:path";
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 = path20.join(fontCacheDir(), "manifest.json");
8950
+ const fontManifest = path21.join(fontCacheDir(), "manifest.json");
8815
8951
  checks.push(
8816
- existsSync14(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync12(fontManifest, "utf8")).length} faces)` } : {
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 path21 from "node:path";
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 = path21.resolve(process.cwd(), ".env");
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 = path21.resolve(process.cwd(), ".gitignore");
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 readFileSync13, readdirSync as readdirSync3, existsSync as existsSync15 } from "node:fs";
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 path22 from "node:path";
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 = path22.resolve(input.outDir, semantics.componentName);
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 = path22.join(dir, name);
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 = path22.resolve(input.outDir, artifact.path);
9339
- mkdirSync4(path22.dirname(filePath), { recursive: true });
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 = readFileSync13(contextFile, "utf8");
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 (existsSync15(flags.out)) {
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 (!existsSync15(cjPath)) continue;
9521
- const cj = JSON.parse(readFileSync13(cjPath, "utf8"));
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 && existsSync15(tsxPath) && existsSync15(cssPath)) {
9667
+ if (flags.refine && existsSync17(tsxPath) && existsSync17(cssPath)) {
9530
9668
  initialCode = {
9531
- tsx: readFileSync13(tsxPath, "utf8"),
9532
- css: readFileSync13(cssPath, "utf8")
9669
+ tsx: readFileSync14(tsxPath, "utf8"),
9670
+ css: readFileSync14(cssPath, "utf8")
9533
9671
  };
9534
9672
  const semPath = `${flags.out}/${entry}/semantics.json`;
9535
- if (existsSync15(semPath)) {
9536
- initialSemantics = JSON.parse(readFileSync13(semPath, "utf8"));
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("0.0.0").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();
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").requiredOption("--name <name>", "asset-<id>.<ext>").requiredOption("--file <file>", "downloaded asset file").action(async (_o, cmd) => {
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);