@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.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
- return path4.join(runtimePackageRoot(), "node_modules");
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(runtimeNodeModules(), path4.join(dir, "node_modules"), "junction");
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 existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
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 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) {
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 ${res.status} \u2014 family not available there? For a licensed or private face use \`tendril fonts add\` with a local file` }))
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 = existsSync4(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
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 (!existsSync4(src)) throw new Error(`font file not found: ${src}`);
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 = existsSync4(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
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 = existsSync4(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
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 (!existsSync4(mPath)) return [];
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 = existsSync4(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
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 existsSync5, readFileSync as readFileSync4 } from "node:fs";
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 (!existsSync5(manifestPath2)) return "";
3243
+ if (!existsSync6(manifestPath2)) return "";
3202
3244
  const manifest = JSON.parse(readFileSync4(manifestPath2, "utf8"));
3203
3245
  const resolveFile = (f) => {
3204
- if (path10.isAbsolute(f) && existsSync5(f)) return 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 existsSync6, writeFileSync as writeFileSync4 } from "node:fs";
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 (!existsSync6(mPath)) return out;
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 existsSync7, readFileSync as readFileSync6 } from "node:fs";
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 (!existsSync7(entryTsx)) return { error: `${task.entry} missing` };
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 (!existsSync7(f)) return false;
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) => existsSync7(f)).map((f) => readFileSync6(f, "utf8")).join("\n");
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 existsSync8 } from "node:fs";
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 = existsSync8(cssPath) ? readFileSync7(cssPath, "utf8") : "";
3935
- const tokensCss = existsSync8(tokensPath) ? readFileSync7(tokensPath, "utf8") : void 0;
3936
- if (existsSync8(entryPath)) {
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 existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
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 (!existsSync9(file)) return void 0;
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 (!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");
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 rings on :focus-visible, never bare :focus (keyboard shows the ring; mouse click must not leave one).
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 existsSync10, readFileSync as readFileSync9 } from "node:fs";
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) => existsSync10(f)).map((f) => readFileSync9(f, "utf8")).join("\n");
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 existsSync11, readFileSync as readFileSync10 } from "node:fs";
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 (!existsSync11(entryTsx)) return { error: `${task.entry} missing` };
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) => existsSync11(path17.join(setDir, m, "get_metadata_interior.json")));
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) => existsSync11(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
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) => existsSync11(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
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 existsSync12 } from "node:fs";
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 (!existsSync12(entryTsx)) return { error: `${task.entry} missing` };
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/env.ts
4866
- import { existsSync as existsSync13, readFileSync as readFileSync11 } from "node:fs";
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 = path19.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
4880
- if (!existsSync13(envPath)) return void 0;
4881
- return parseEnv(readFileSync11(envPath, "utf8")).get(name);
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 existsSync16, readFileSync as readFileSync14 } from "node:fs";
6145
- import path23 from "node:path";
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(readFileSync14(file, "utf8"));
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(path23.resolve(file), frame));
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(readFileSync14(path23.resolve(file), "utf8"));
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 runRecordNext(opts) {
6300
- const instruction = nextInstruction(opts.setDir);
6301
- const status = sessionStatus(opts.setDir);
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 && !existsSync16(path23.join(opts.setDir, "get_variable_defs.json"))) {
6304
- const manifest = loadManifest(opts.setDir);
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
- 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;
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
- emitData(opts, instruction === null ? { complete: true, progress } : { ...instruction, note: `${instruction.note}. ${ENVELOPE_HELP}`, progress }, () => {
6313
- if (instruction === null) {
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
- 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>
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
- 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());
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 runRecordIngest(opts) {
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
- payload = JSON.parse(readFileSync14(path23.resolve(opts.file), "utf8"));
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(path23.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
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
- emitData(opts, { rep: opts.rep, tool: opts.tool, overwrote }, () => {
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, readFileSync14(path23.resolve(opts.file)));
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(readFileSync14(path23.resolve(opts.rolesFile), "utf8")), humanOverride: true } : { main: derived.main, parts: derived.parts, external: derived.external, humanOverride: false };
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(path23.join(opts.setDir, "recording-set.json"), `${JSON.stringify(updated, null, 1)}
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 existsSync17, mkdirSync as mkdirSync5, readFileSync as readFileSync15, renameSync, writeFileSync as writeFileSync9 } from "node:fs";
6685
- import path24 from "node:path";
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 (!existsSync17(path24.join(outDir, "run-log.json")) && !existsSync17(path24.join(outDir, "loop-state.json"))) return void 0;
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 (existsSync17(`${outDir}-prev-${n}`)) n += 1;
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 = path24.join(opts.outDir, "loop-state.json");
6728
- const resuming = opts.resume === true && existsSync17(statePath);
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 = path24.join(opts.outDir, ".candidate");
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(readFileSync15(statePath, "utf8")));
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(path24.join(scratch, name), content);
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(path24.join(opts.outDir, "responses"), { recursive: true });
6821
- writeFileSync9(path24.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
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(path24.join(opts.outDir, name), content);
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
- path24.join(opts.outDir, "run-log.json"),
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 existsSync18, readFileSync as readFileSync16 } from "node:fs";
6918
- import path25 from "node:path";
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(readFileSync16(file, "utf8")).content[0]?.text ?? "";
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 = path25.join(setDir, "get_variable_defs.json");
7150
- if (existsSync18(setDefs)) fromDefs(envelopeText(setDefs));
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 = 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));
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 = path25.join(setDir, rep.slug, "get_metadata.json");
7175
- if (!existsSync18(metaFile)) {
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 = 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);
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 existsSync19, readFileSync as readFileSync17, readdirSync as readdirSync4 } from "node:fs";
7303
- import path26 from "node:path";
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
- return JSON.parse(readFileSync17(path26.join(set, rep, `${tool}.json`), "utf8")).content[0]?.text ?? "";
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 (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 ?? "{}";
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 = 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 ?? "{}";
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 = path26.join(SET, cfg.rep, "get_design_context.json");
7384
- return existsSync19(f) ? JSON.parse(readFileSync17(f, "utf8")).content[0]?.text ?? "" : "";
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 = readdirSync4(path26.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
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
- ${readFileSync17(path26.join(SET, cfg.rep, f), "utf8")}
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 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";
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 = path27.join(setDir, "recording-set.json");
7544
- if (existsSync20(manifestFile)) {
7724
+ const manifestFile = path28.join(setDir, "recording-set.json");
7725
+ if (existsSync22(manifestFile)) {
7545
7726
  try {
7546
- const lattice = JSON.parse(readFileSync18(manifestFile, "utf8")).latticeNames;
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
- 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));
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(readFileSync18(f, "utf8")).content[0]?.text ?? "";
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 (existsSync20(path27.join(setDir, name))) relPaths.push(name);
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 (existsSync20(path27.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
7751
+ if (existsSync22(path28.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
7571
7752
  }
7572
- if (existsSync20(path27.join(setDir, cfg.rep))) {
7573
- for (const asset of readdirSync5(path27.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
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(readFileSync18(path27.join(setDir, p))),
7761
+ (p) => new Uint8Array(readFileSync19(path28.join(setDir, p))),
7581
7762
  (chunks) => {
7582
- const h = createHash2("sha256");
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) => path27.join(opts.bundleDir, f)).filter((f) => existsSync20(f));
7599
- const families = cssFontFamilies(cssFiles.map((f) => readFileSync18(f, "utf8")).join("\n"));
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 = path27.relative(base, opts.task.set);
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 = path27.join(opts.bundleDir, "component.json");
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 = path27.join(opts.bundleDir, "styles.css");
7657
- if (existsSync20(stylesPath)) {
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 = readFileSync18(stylesPath, "utf8");
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 existsSync21, readFileSync as readFileSync19 } from "node:fs";
7709
- import path28 from "node:path";
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.exit(ExitCode.FontsUnproven);
7902
+ process.exitCode = ExitCode.FontsUnproven;
7721
7903
  }
7722
7904
  }
7723
7905
  async function runFontsResolveSet(opts) {
7724
- const setDir = path28.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
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.exit(ExitCode.FontsUnproven);
7951
+ process.exitCode = ExitCode.FontsUnproven;
7770
7952
  }
7771
7953
  }
7772
7954
  function runFontsStatus(opts) {
7773
- const manifestPath2 = path28.join(opts.cacheDir, "manifest.json");
7774
- if (!existsSync21(manifestPath2)) {
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(readFileSync19(manifestPath2, "utf8"));
7782
- const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path28.resolve(opts.lock), opts.cacheDir) : null;
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 existsSync23, readFileSync as readFileSync21 } from "node:fs";
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 (!existsSync23(opts.bundleDir)) {
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 (existsSync23(manifestPath2)) {
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 (existsSync23(fromRepo)) return fromRepo;
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 (!existsSync23(path30.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path30.resolve(t.set) === path30.resolve(setDir))) {
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 && !existsSync23(path30.join(setDir, "recording-set.json"))) {
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 (!existsSync23(p)) continue;
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) => !existsSync23(path30.join(task.set, c.rep, "get_screenshot.json")) || !existsSync23(path30.join(task.set, c.rep, "get_metadata.json"))
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) => existsSync23(f)).map((f) => readFileSync21(f, "utf8")).join("\n");
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 existsSync24, mkdirSync as mkdirSync6, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "node:fs";
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 = existsSync24(path31.join(asPath, "recording-set.json"));
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 (existsSync24(manifestPath2)) {
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 (existsSync24(manifestPath2)) {
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. When a user is present, 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.",
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 (!existsSync24(candidateDir)) {
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 existsSync25 } from "node:fs";
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 = existsSync25(path32.join(recordedAsPath, "recording-set.json"));
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) => !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"))
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 existsSync14, readFileSync as readFileSync12 } from "node:fs";
8737
- import path20 from "node:path";
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 = path20.join(fontCacheDir(), "manifest.json");
8967
+ const fontManifest = path21.join(fontCacheDir(), "manifest.json");
8815
8968
  checks.push(
8816
- existsSync14(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync12(fontManifest, "utf8")).length} faces)` } : {
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 path21 from "node:path";
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 = path21.resolve(process.cwd(), ".env");
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 = path21.resolve(process.cwd(), ".gitignore");
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 readFileSync13, readdirSync as readdirSync3, existsSync as existsSync15 } from "node:fs";
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 path22 from "node:path";
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 = path22.resolve(input.outDir, semantics.componentName);
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 = path22.join(dir, name);
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 = path22.resolve(input.outDir, artifact.path);
9339
- mkdirSync4(path22.dirname(filePath), { recursive: true });
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 = readFileSync13(contextFile, "utf8");
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 (existsSync15(flags.out)) {
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 (!existsSync15(cjPath)) continue;
9521
- const cj = JSON.parse(readFileSync13(cjPath, "utf8"));
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 && existsSync15(tsxPath) && existsSync15(cssPath)) {
9684
+ if (flags.refine && existsSync17(tsxPath) && existsSync17(cssPath)) {
9530
9685
  initialCode = {
9531
- tsx: readFileSync13(tsxPath, "utf8"),
9532
- css: readFileSync13(cssPath, "utf8")
9686
+ tsx: readFileSync14(tsxPath, "utf8"),
9687
+ css: readFileSync14(cssPath, "utf8")
9533
9688
  };
9534
9689
  const semPath = `${flags.out}/${entry}/semantics.json`;
9535
- if (existsSync15(semPath)) {
9536
- initialSemantics = JSON.parse(readFileSync13(semPath, "utf8"));
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("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();
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").requiredOption("--name <name>", "asset-<id>.<ext>").requiredOption("--file <file>", "downloaded asset file").action(async (_o, cmd) => {
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);