@tendrilapp/cli 0.1.17 → 0.1.18
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/SKILL.md +8 -2
- package/dist/tendril-mcp.js +34 -16
- package/dist/tendril.js +905 -256
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -1214,8 +1214,8 @@ var init_src = __esm({
|
|
|
1214
1214
|
function variableNameToPath(name) {
|
|
1215
1215
|
return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
|
|
1216
1216
|
}
|
|
1217
|
-
function tokenPathToCssVar(
|
|
1218
|
-
return `--${
|
|
1217
|
+
function tokenPathToCssVar(path36) {
|
|
1218
|
+
return `--${path36.join("-")}`;
|
|
1219
1219
|
}
|
|
1220
1220
|
function toDtcgToken(variable, defaultMode) {
|
|
1221
1221
|
const modes = Object.keys(variable.valuesByMode);
|
|
@@ -1259,11 +1259,11 @@ function toDtcgToken(variable, defaultMode) {
|
|
|
1259
1259
|
}
|
|
1260
1260
|
function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
1261
1261
|
const entries = variables.map((variable) => {
|
|
1262
|
-
const
|
|
1263
|
-
if (
|
|
1262
|
+
const path36 = variableNameToPath(variable.name);
|
|
1263
|
+
if (path36.length === 0) {
|
|
1264
1264
|
throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
|
|
1265
1265
|
}
|
|
1266
|
-
return { variable, path:
|
|
1266
|
+
return { variable, path: path36 };
|
|
1267
1267
|
});
|
|
1268
1268
|
const groupPrefixes = /* @__PURE__ */ new Set();
|
|
1269
1269
|
for (const e of entries) {
|
|
@@ -1284,21 +1284,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
|
1284
1284
|
}
|
|
1285
1285
|
const tokens = {};
|
|
1286
1286
|
const flat = [];
|
|
1287
|
-
for (const { variable, path:
|
|
1287
|
+
for (const { variable, path: path36 } of entries) {
|
|
1288
1288
|
const token = toDtcgToken(variable, defaultMode);
|
|
1289
1289
|
let group = tokens;
|
|
1290
|
-
for (const segment of
|
|
1290
|
+
for (const segment of path36.slice(0, -1)) {
|
|
1291
1291
|
const existing = group[segment];
|
|
1292
1292
|
group = existing ?? (group[segment] = {});
|
|
1293
1293
|
}
|
|
1294
|
-
const leaf =
|
|
1294
|
+
const leaf = path36[path36.length - 1];
|
|
1295
1295
|
if (group[leaf] !== void 0) {
|
|
1296
|
-
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${
|
|
1296
|
+
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path36.join(".")}" (variable ${variable.id})`);
|
|
1297
1297
|
}
|
|
1298
1298
|
group[leaf] = token;
|
|
1299
1299
|
flat.push({
|
|
1300
|
-
path:
|
|
1301
|
-
cssVar: tokenPathToCssVar(
|
|
1300
|
+
path: path36.join("."),
|
|
1301
|
+
cssVar: tokenPathToCssVar(path36),
|
|
1302
1302
|
type: token.$type,
|
|
1303
1303
|
value: token.$value
|
|
1304
1304
|
});
|
|
@@ -1487,9 +1487,9 @@ function boundId(value) {
|
|
|
1487
1487
|
return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
|
|
1488
1488
|
}
|
|
1489
1489
|
function resolveBinding(ctx, id) {
|
|
1490
|
-
const
|
|
1491
|
-
if (
|
|
1492
|
-
return
|
|
1490
|
+
const path36 = ctx.pathById.get(id);
|
|
1491
|
+
if (path36 === void 0) ctx.unresolved.add(id);
|
|
1492
|
+
return path36;
|
|
1493
1493
|
}
|
|
1494
1494
|
function parseVariantProps(name) {
|
|
1495
1495
|
if (!name.includes("=")) return void 0;
|
|
@@ -1524,8 +1524,8 @@ function walk(ctx, raw) {
|
|
|
1524
1524
|
if (!isObject(paint) || paint["visible"] === false) continue;
|
|
1525
1525
|
const id = boundId(paint);
|
|
1526
1526
|
if (id !== void 0) {
|
|
1527
|
-
const
|
|
1528
|
-
if (
|
|
1527
|
+
const path36 = resolveBinding(ctx, id);
|
|
1528
|
+
if (path36 !== void 0) tokens.add(path36);
|
|
1529
1529
|
} else if (typeof paint["color"] === "string") {
|
|
1530
1530
|
ctx.hardcoded.push({ node: name, property, value: paint["color"] });
|
|
1531
1531
|
}
|
|
@@ -1533,8 +1533,8 @@ function walk(ctx, raw) {
|
|
|
1533
1533
|
}
|
|
1534
1534
|
const radiusId = boundId(raw["cornerRadius"]);
|
|
1535
1535
|
if (radiusId !== void 0) {
|
|
1536
|
-
const
|
|
1537
|
-
if (
|
|
1536
|
+
const path36 = resolveBinding(ctx, radiusId);
|
|
1537
|
+
if (path36 !== void 0) tokens.add(path36);
|
|
1538
1538
|
} else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
|
|
1539
1539
|
ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
|
|
1540
1540
|
}
|
|
@@ -1544,10 +1544,10 @@ function walk(ctx, raw) {
|
|
|
1544
1544
|
layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
|
|
1545
1545
|
const gapId = boundId(raw["itemSpacing"]);
|
|
1546
1546
|
if (gapId !== void 0) {
|
|
1547
|
-
const
|
|
1548
|
-
if (
|
|
1549
|
-
layout.gap =
|
|
1550
|
-
tokens.add(
|
|
1547
|
+
const path36 = resolveBinding(ctx, gapId);
|
|
1548
|
+
if (path36 !== void 0) {
|
|
1549
|
+
layout.gap = path36;
|
|
1550
|
+
tokens.add(path36);
|
|
1551
1551
|
}
|
|
1552
1552
|
} else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
|
|
1553
1553
|
ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
|
|
@@ -1556,10 +1556,10 @@ function walk(ctx, raw) {
|
|
|
1556
1556
|
for (const field of PADDING_FIELDS) {
|
|
1557
1557
|
const id = boundId(raw[field]);
|
|
1558
1558
|
if (id !== void 0) {
|
|
1559
|
-
const
|
|
1560
|
-
if (
|
|
1561
|
-
paddingPaths.push(
|
|
1562
|
-
tokens.add(
|
|
1559
|
+
const path36 = resolveBinding(ctx, id);
|
|
1560
|
+
if (path36 !== void 0) {
|
|
1561
|
+
paddingPaths.push(path36);
|
|
1562
|
+
tokens.add(path36);
|
|
1563
1563
|
}
|
|
1564
1564
|
} else if (typeof raw[field] === "number" && raw[field] !== 0) {
|
|
1565
1565
|
ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
|
|
@@ -1968,13 +1968,13 @@ var init_tsc_check = __esm({
|
|
|
1968
1968
|
// packages/verify/src/token-lint.ts
|
|
1969
1969
|
import strictValue from "stylelint-declaration-strict-value";
|
|
1970
1970
|
import stylelint from "stylelint";
|
|
1971
|
-
async function runTokenLint(css, fileLabel = "generated.css", definedVars2) {
|
|
1971
|
+
async function runTokenLint(css, fileLabel = "generated.css", definedVars2, recordedMapEmpty = false) {
|
|
1972
1972
|
const result = await stylelint.lint({
|
|
1973
1973
|
code: css,
|
|
1974
1974
|
codeFilename: fileLabel,
|
|
1975
1975
|
config: STYLELINT_CONFIG
|
|
1976
1976
|
});
|
|
1977
|
-
const mapKnownEmpty = definedVars2 !== void 0 && definedVars2.size === 0;
|
|
1977
|
+
const mapKnownEmpty = recordedMapEmpty || definedVars2 !== void 0 && definedVars2.size === 0;
|
|
1978
1978
|
const violations = result.results.flatMap(
|
|
1979
1979
|
(r) => r.warnings.filter((w) => !(mapKnownEmpty && w.rule === "scale-unlimited/declaration-strict-value")).map((w) => ({
|
|
1980
1980
|
file: fileLabel,
|
|
@@ -3409,7 +3409,29 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3409
3409
|
function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3410
3410
|
const mPath = path9.join(cacheDir, "manifest.json");
|
|
3411
3411
|
const manifest = existsSync5(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3412
|
-
|
|
3412
|
+
const wanted = new Set(families.map((f) => f.toLowerCase()));
|
|
3413
|
+
return manifest.filter((f) => wanted.has(f.family.toLowerCase()));
|
|
3414
|
+
}
|
|
3415
|
+
function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3416
|
+
const mPath = path9.join(cacheDir, "manifest.json");
|
|
3417
|
+
if (!existsSync5(mPath)) return [];
|
|
3418
|
+
let entries;
|
|
3419
|
+
try {
|
|
3420
|
+
entries = JSON.parse(readFileSync3(mPath, "utf8"));
|
|
3421
|
+
} catch {
|
|
3422
|
+
return [];
|
|
3423
|
+
}
|
|
3424
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
3425
|
+
for (const e of entries) {
|
|
3426
|
+
if (typeof e.family !== "string" || typeof e.weight !== "number" || typeof e.file !== "string") continue;
|
|
3427
|
+
const file = path9.isAbsolute(e.file) && existsSync5(e.file) ? e.file : path9.resolve(cacheDir, path9.basename(e.file));
|
|
3428
|
+
if (!existsSync5(file)) continue;
|
|
3429
|
+
if (createHash("sha256").update(readFileSync3(file)).digest("hex") !== e.sha256) continue;
|
|
3430
|
+
const set = byFamily.get(e.family) ?? /* @__PURE__ */ new Set();
|
|
3431
|
+
set.add(e.weight);
|
|
3432
|
+
byFamily.set(e.family, set);
|
|
3433
|
+
}
|
|
3434
|
+
return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
|
|
3413
3435
|
}
|
|
3414
3436
|
var DEFAULT_FONT_CACHE, UA;
|
|
3415
3437
|
var init_font_resolve = __esm({
|
|
@@ -3421,11 +3443,21 @@ var init_font_resolve = __esm({
|
|
|
3421
3443
|
});
|
|
3422
3444
|
|
|
3423
3445
|
// packages/verify/src/font-faces.ts
|
|
3446
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3424
3447
|
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
3425
3448
|
import path10 from "node:path";
|
|
3426
3449
|
function fontFaceCss(manifestPath2 = path10.join(fontCacheDir(), "manifest.json")) {
|
|
3427
3450
|
if (!existsSync6(manifestPath2)) return "";
|
|
3428
|
-
const
|
|
3451
|
+
const claimed = JSON.parse(readFileSync4(manifestPath2, "utf8"));
|
|
3452
|
+
const resolveEntry = (f) => {
|
|
3453
|
+
if (path10.isAbsolute(f) && existsSync6(f)) return f;
|
|
3454
|
+
return path10.resolve(path10.dirname(manifestPath2), path10.basename(f));
|
|
3455
|
+
};
|
|
3456
|
+
const manifest = claimed.filter((f) => {
|
|
3457
|
+
const file = resolveEntry(f.file);
|
|
3458
|
+
if (!existsSync6(file)) return false;
|
|
3459
|
+
return createHash2("sha256").update(readFileSync4(file)).digest("hex") === f.sha256;
|
|
3460
|
+
});
|
|
3429
3461
|
const resolveFile = (f) => {
|
|
3430
3462
|
if (path10.isAbsolute(f) && existsSync6(f)) return f;
|
|
3431
3463
|
return path10.resolve(path10.dirname(manifestPath2), path10.basename(f));
|
|
@@ -4196,7 +4228,28 @@ function definedVars(tokensCss) {
|
|
|
4196
4228
|
for (const m of tokensCss.matchAll(/(--[\w-]+)\s*:/g)) names.add(m[1]);
|
|
4197
4229
|
return names;
|
|
4198
4230
|
}
|
|
4199
|
-
|
|
4231
|
+
function recordedTokenMapEmpty(setDir, reps) {
|
|
4232
|
+
const readMap = (file) => {
|
|
4233
|
+
if (!existsSync9(file)) return void 0;
|
|
4234
|
+
try {
|
|
4235
|
+
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync7(file, "utf8"))) || "{}");
|
|
4236
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
4237
|
+
} catch {
|
|
4238
|
+
return {};
|
|
4239
|
+
}
|
|
4240
|
+
};
|
|
4241
|
+
const setLevel = readMap(path14.join(setDir, "get_variable_defs.json"));
|
|
4242
|
+
if (setLevel !== void 0) return Object.keys(setLevel).length === 0;
|
|
4243
|
+
let recorded = false;
|
|
4244
|
+
for (const rep of reps) {
|
|
4245
|
+
const m = readMap(path14.join(setDir, rep, "get_variable_defs.json"));
|
|
4246
|
+
if (m === void 0) continue;
|
|
4247
|
+
recorded = true;
|
|
4248
|
+
if (Object.keys(m).length > 0) return false;
|
|
4249
|
+
}
|
|
4250
|
+
return recorded;
|
|
4251
|
+
}
|
|
4252
|
+
async function checkBundleQuality(bundleDir, entry, set) {
|
|
4200
4253
|
const findings = [];
|
|
4201
4254
|
const entryPath = path14.join(bundleDir, entry);
|
|
4202
4255
|
const cssPath = path14.join(bundleDir, "styles.css");
|
|
@@ -4216,7 +4269,8 @@ async function checkBundleQuality(bundleDir, entry) {
|
|
|
4216
4269
|
}
|
|
4217
4270
|
}
|
|
4218
4271
|
if (css !== "") {
|
|
4219
|
-
|
|
4272
|
+
const recordedEmpty = set !== void 0 && recordedTokenMapEmpty(set.dir, set.reps);
|
|
4273
|
+
for (const v of (await runTokenLint(css, "styles.css", definedVars(tokensCss), recordedEmpty)).violations) {
|
|
4220
4274
|
findings.push({ kind: "token-lint", file: "styles.css", line: v.line, message: v.message });
|
|
4221
4275
|
}
|
|
4222
4276
|
}
|
|
@@ -4225,6 +4279,7 @@ async function checkBundleQuality(bundleDir, entry) {
|
|
|
4225
4279
|
var init_bundle_quality = __esm({
|
|
4226
4280
|
"packages/verify/src/bundle-quality.ts"() {
|
|
4227
4281
|
"use strict";
|
|
4282
|
+
init_src();
|
|
4228
4283
|
init_runtime();
|
|
4229
4284
|
init_tsc_check();
|
|
4230
4285
|
init_token_lint();
|
|
@@ -5174,7 +5229,7 @@ var init_src4 = __esm({
|
|
|
5174
5229
|
// packages/cli/src/environment.ts
|
|
5175
5230
|
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
|
|
5176
5231
|
import path19 from "node:path";
|
|
5177
|
-
import { createHash as
|
|
5232
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
5178
5233
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
5179
5234
|
function cliVersion() {
|
|
5180
5235
|
try {
|
|
@@ -5191,7 +5246,7 @@ function environmentStamp(taskFamilies) {
|
|
|
5191
5246
|
const entries = JSON.parse(readFileSync11(manifestPath2, "utf8"));
|
|
5192
5247
|
const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
|
|
5193
5248
|
const faces = scoped.map((f) => `${f.family}:${f.weight}:${f.sha256}`).sort();
|
|
5194
|
-
fontsHash = faces.length === 0 ? null :
|
|
5249
|
+
fontsHash = faces.length === 0 ? null : createHash3("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
|
|
5195
5250
|
} catch {
|
|
5196
5251
|
fontsHash = null;
|
|
5197
5252
|
}
|
|
@@ -5210,6 +5265,25 @@ var init_environment = __esm({
|
|
|
5210
5265
|
}
|
|
5211
5266
|
});
|
|
5212
5267
|
|
|
5268
|
+
// packages/cli/src/describe.ts
|
|
5269
|
+
function printDescription(description) {
|
|
5270
|
+
process.stdout.write(`${JSON.stringify(description, null, 2)}
|
|
5271
|
+
`);
|
|
5272
|
+
}
|
|
5273
|
+
var COMMON_EXIT_CODES;
|
|
5274
|
+
var init_describe = __esm({
|
|
5275
|
+
"packages/cli/src/describe.ts"() {
|
|
5276
|
+
"use strict";
|
|
5277
|
+
COMMON_EXIT_CODES = {
|
|
5278
|
+
0: "success",
|
|
5279
|
+
1: "general error",
|
|
5280
|
+
2: "authentication error",
|
|
5281
|
+
3: "input validation error",
|
|
5282
|
+
4: "confirmation required (re-run with --yes or answer the prompt)"
|
|
5283
|
+
};
|
|
5284
|
+
}
|
|
5285
|
+
});
|
|
5286
|
+
|
|
5213
5287
|
// packages/cli/src/env.ts
|
|
5214
5288
|
import { existsSync as existsSync15, readFileSync as readFileSync12 } from "node:fs";
|
|
5215
5289
|
import path20 from "node:path";
|
|
@@ -5386,6 +5460,211 @@ var init_entitlement = __esm({
|
|
|
5386
5460
|
}
|
|
5387
5461
|
});
|
|
5388
5462
|
|
|
5463
|
+
// packages/cli/src/commands/doctor.ts
|
|
5464
|
+
import { spawnSync } from "node:child_process";
|
|
5465
|
+
import { existsSync as existsSync17, readFileSync as readFileSync14, readdirSync as readdirSync3 } from "node:fs";
|
|
5466
|
+
import os4 from "node:os";
|
|
5467
|
+
import path22 from "node:path";
|
|
5468
|
+
async function runDoctorChecks(options) {
|
|
5469
|
+
const checks = [];
|
|
5470
|
+
const mcpUrl = options.mcpUrl ?? DEFAULT_MCP_URL;
|
|
5471
|
+
const client = new McpHttpClient({
|
|
5472
|
+
url: mcpUrl,
|
|
5473
|
+
...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
|
|
5474
|
+
});
|
|
5475
|
+
try {
|
|
5476
|
+
const info = await client.initialize();
|
|
5477
|
+
const tools = await client.listTools();
|
|
5478
|
+
checks.push({
|
|
5479
|
+
name: "figma-desktop-mcp",
|
|
5480
|
+
ok: true,
|
|
5481
|
+
detail: `${info.name} ${info.version} (protocol ${info.protocolVersion}), ${tools.length} tools: ${tools.map((t) => t.name).join(", ")}`
|
|
5482
|
+
});
|
|
5483
|
+
} catch (err) {
|
|
5484
|
+
checks.push({
|
|
5485
|
+
name: "figma-desktop-mcp",
|
|
5486
|
+
ok: false,
|
|
5487
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
5488
|
+
remediation: err instanceof McpUnavailableError ? "Open the Figma desktop app, sign in, then Figma menu \u2192 Preferences \u2192 Enable local MCP server (Dev or Full seat on a paid plan)." : "The server answered but the exchange failed \u2014 check the signed-in account's seat, then retry."
|
|
5489
|
+
});
|
|
5490
|
+
}
|
|
5491
|
+
const openrouterKey = resolveCredential("OPENROUTER_API_KEY");
|
|
5492
|
+
checks.push(
|
|
5493
|
+
openrouterKey ? { name: "openrouter-key", ok: true, detail: "OPENROUTER_API_KEY configured" } : {
|
|
5494
|
+
name: "openrouter-key",
|
|
5495
|
+
ok: false,
|
|
5496
|
+
detail: "OPENROUTER_API_KEY not set \u2014 optional; only the curated API-model engine needs it",
|
|
5497
|
+
remediation: "Run `tendril init` to store your OpenRouter key (BYOK) if you plan to use the curated engine."
|
|
5498
|
+
}
|
|
5499
|
+
);
|
|
5500
|
+
try {
|
|
5501
|
+
const chrome = resolveChrome();
|
|
5502
|
+
checks.push({ name: "browser", ok: true, detail: `${chromeVersion() ?? "version unknown"} at ${chrome}` });
|
|
5503
|
+
} catch (err) {
|
|
5504
|
+
checks.push({
|
|
5505
|
+
name: "browser",
|
|
5506
|
+
ok: false,
|
|
5507
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
5508
|
+
remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
|
|
5509
|
+
});
|
|
5510
|
+
}
|
|
5511
|
+
const fontManifest = path22.join(fontCacheDir(), "manifest.json");
|
|
5512
|
+
checks.push(
|
|
5513
|
+
existsSync17(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync14(fontManifest, "utf8")).length} faces)` } : {
|
|
5514
|
+
name: "font-cache",
|
|
5515
|
+
ok: true,
|
|
5516
|
+
detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
|
|
5517
|
+
remediation: "Nothing to do now: `tendril fonts resolve --set <recording-dir>` fetches exactly what a recording declares, and generate/verify name that command when they need it."
|
|
5518
|
+
}
|
|
5519
|
+
);
|
|
5520
|
+
const pluginRoot = path22.join(os4.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
|
|
5521
|
+
if (existsSync17(pluginRoot)) {
|
|
5522
|
+
try {
|
|
5523
|
+
const versions = readdirSync3(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
|
|
5524
|
+
const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
|
|
5525
|
+
if (newest !== void 0) {
|
|
5526
|
+
const skewed = versionIsNewer(newest, cliVersion());
|
|
5527
|
+
checks.push(
|
|
5528
|
+
skewed ? {
|
|
5529
|
+
name: "plugin-skew",
|
|
5530
|
+
ok: false,
|
|
5531
|
+
detail: `Claude Code plugin cache holds v${newest} while this CLI is ${cliVersion()} \u2014 the plugin's skill/agents are STALE and will silently shadow current doctrine`,
|
|
5532
|
+
remediation: "Update the plugin \u2014 REFRESH THE MARKETPLACE FIRST (its local clone goes stale and a reinstall faithfully reinstalls the old version): terminal Claude Code \u2014 `claude plugin marketplace update tendrilapp` then `claude plugin update tendril`; VS Code extension \u2014 /plugins panel \u2192 Marketplaces tab \u2192 refresh tendrilapp, THEN Plugins tab \u2192 uninstall + reinstall tendril, reopen the chat panel. If the refresh doesn't take, remove the tendrilapp marketplace entirely and re-add TendrilApp/claude-plugin (a fresh clone cannot be stale)."
|
|
5533
|
+
} : { name: "plugin-skew", ok: true, detail: `Claude Code plugin v${newest} matches this CLI` }
|
|
5534
|
+
);
|
|
5535
|
+
}
|
|
5536
|
+
} catch {
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
5539
|
+
const pathBinary = cliVersion() === "0.0.0" ? null : findPathTendril(process.env["PATH"] ?? "", process.platform);
|
|
5540
|
+
if (pathBinary !== null) {
|
|
5541
|
+
const reported = probeVersion(pathBinary);
|
|
5542
|
+
if (reported !== null) {
|
|
5543
|
+
checks.push(
|
|
5544
|
+
reported === cliVersion() ? { name: "path-skew", ok: true, detail: `tendril on PATH (${pathBinary}) is this CLI (${reported})` } : {
|
|
5545
|
+
name: "path-skew",
|
|
5546
|
+
ok: false,
|
|
5547
|
+
detail: `tendril on PATH (${pathBinary}) reports ${reported} while this CLI is ${cliVersion()} \u2014 a stale global install silently shadows the current release in terminals`,
|
|
5548
|
+
remediation: "npm rm -g tendrilapp (or npm install -g @tendrilapp/cli@latest) so the PATH binary matches; plugin/npx sessions already run the current release."
|
|
5549
|
+
}
|
|
5550
|
+
);
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
const ent = checkEntitlement();
|
|
5554
|
+
checks.push(
|
|
5555
|
+
ent.ok ? ent.mode === "pre-launch" ? { name: "entitlement", ok: true, detail: "pre-launch build \u2014 entitlement gates present but unarmed (no public key shipped); record/generate run freely" } : { name: "entitlement", ok: true, detail: `active plan "${ent.claims.plan}" until ${new Date(ent.claims.exp).toISOString().slice(0, 10)}${ent.stale ? " (stale \u2014 renews at next opportunity)" : ""}` } : { name: "entitlement", ok: false, detail: ent.error, remediation: ent.remediation }
|
|
5556
|
+
);
|
|
5557
|
+
const figmaToken = resolveCredential("FIGMA_TOKEN");
|
|
5558
|
+
checks.push({
|
|
5559
|
+
name: "figma-pat",
|
|
5560
|
+
ok: true,
|
|
5561
|
+
detail: figmaToken ? "FIGMA_TOKEN configured (REST fallback available)" : "FIGMA_TOKEN not set \u2014 optional; only needed for the REST fallback transport"
|
|
5562
|
+
});
|
|
5563
|
+
return { ok: checks.filter((c) => c.name !== "figma-pat" && c.name !== "openrouter-key" && c.name !== "path-skew").every((c) => c.ok), checks };
|
|
5564
|
+
}
|
|
5565
|
+
function findPathTendril(pathEnv, platform) {
|
|
5566
|
+
const dirs = pathEnv.split(path22.delimiter).filter((d) => d !== "" && !/node_modules[\\/]\.bin/.test(d) && !/[\\/]_npx[\\/]/.test(d));
|
|
5567
|
+
const names = platform === "win32" ? ["tendril.cmd", "tendril.bat"] : ["tendril"];
|
|
5568
|
+
for (const dir of dirs) {
|
|
5569
|
+
for (const name of names) {
|
|
5570
|
+
const candidate = path22.join(dir, name);
|
|
5571
|
+
if (existsSync17(candidate)) return candidate;
|
|
5572
|
+
}
|
|
5573
|
+
}
|
|
5574
|
+
return null;
|
|
5575
|
+
}
|
|
5576
|
+
function probeVersion(binary) {
|
|
5577
|
+
const windowsShim = /\.(cmd|bat)$/i.test(binary);
|
|
5578
|
+
const res = windowsShim ? spawnSync(`"${binary}" --version`, { shell: true, timeout: 5e3, encoding: "utf8" }) : spawnSync(binary, ["--version"], { timeout: 5e3, encoding: "utf8" });
|
|
5579
|
+
if (res.error !== void 0 || res.status !== 0) return null;
|
|
5580
|
+
const m = /^(\d+\.\d+\.\d+)\s*$/m.exec(res.stdout ?? "");
|
|
5581
|
+
return m === null ? null : m[1];
|
|
5582
|
+
}
|
|
5583
|
+
function versionIsNewer(a, b) {
|
|
5584
|
+
const pa = a.split(".").map(Number);
|
|
5585
|
+
const pb = b.split(".").map(Number);
|
|
5586
|
+
for (let i = 0; i < 3; i++) {
|
|
5587
|
+
if ((pb[i] ?? 0) > (pa[i] ?? 0)) return true;
|
|
5588
|
+
if ((pb[i] ?? 0) < (pa[i] ?? 0)) return false;
|
|
5589
|
+
}
|
|
5590
|
+
return false;
|
|
5591
|
+
}
|
|
5592
|
+
async function latestVersionInfo() {
|
|
5593
|
+
try {
|
|
5594
|
+
const ctl = new AbortController();
|
|
5595
|
+
const timer = setTimeout(() => ctl.abort(), 2500);
|
|
5596
|
+
const res = await fetch("https://registry.npmjs.org/@tendrilapp%2fcli", { signal: ctl.signal });
|
|
5597
|
+
clearTimeout(timer);
|
|
5598
|
+
if (!res.ok) return null;
|
|
5599
|
+
const doc = await res.json();
|
|
5600
|
+
const latest = doc["dist-tags"]?.latest;
|
|
5601
|
+
if (latest === void 0) return null;
|
|
5602
|
+
const stamp = doc.time?.[latest];
|
|
5603
|
+
return { latest, ...stamp !== void 0 ? { publishedAt: stamp.slice(0, 10) } : {} };
|
|
5604
|
+
} catch {
|
|
5605
|
+
return null;
|
|
5606
|
+
}
|
|
5607
|
+
}
|
|
5608
|
+
async function runDoctor(flags) {
|
|
5609
|
+
if (flags.describe) {
|
|
5610
|
+
printDescription(DOCTOR_DESCRIPTION);
|
|
5611
|
+
return;
|
|
5612
|
+
}
|
|
5613
|
+
const [report, latest] = await Promise.all([runDoctorChecks({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} }), latestVersionInfo()]);
|
|
5614
|
+
emitData(flags, { version: cliVersion(), ...latest !== null ? { latest: latest.latest, latestPublishedAt: latest.publishedAt ?? null, updateAvailable: versionIsNewer(cliVersion(), latest.latest) } : {}, ...report }, () => {
|
|
5615
|
+
const version = cliVersion();
|
|
5616
|
+
if (latest === null) {
|
|
5617
|
+
process.stdout.write(`tendril ${version} (latest: unreachable)
|
|
5618
|
+
`);
|
|
5619
|
+
} else if (!versionIsNewer(version, latest.latest)) {
|
|
5620
|
+
process.stdout.write(`tendril ${version} (${latest.latest === version ? "latest" : `ahead of registry ${latest.latest}`}${latest.publishedAt !== void 0 ? `, published ${latest.publishedAt}` : ""})
|
|
5621
|
+
`);
|
|
5622
|
+
} else {
|
|
5623
|
+
process.stdout.write(`tendril ${version} \u2014 UPDATE AVAILABLE: ${latest.latest}${latest.publishedAt !== void 0 ? ` (published ${latest.publishedAt})` : ""}
|
|
5624
|
+
`);
|
|
5625
|
+
process.stdout.write(` \u2192 npm install -g @tendrilapp/cli@latest (plugin MCP users update automatically at next session; a stale npx cache clears with npm cache clean --force)
|
|
5626
|
+
`);
|
|
5627
|
+
}
|
|
5628
|
+
for (const check of report.checks) {
|
|
5629
|
+
process.stdout.write(`${check.ok ? "\u2713" : "\u2717"} ${check.name}: ${check.detail}
|
|
5630
|
+
`);
|
|
5631
|
+
if (check.remediation) process.stdout.write(` \u2192 ${check.remediation}
|
|
5632
|
+
`);
|
|
5633
|
+
}
|
|
5634
|
+
process.stdout.write(report.ok ? "\nready to generate\n" : "\nnot ready \u2014 fix the items above\n");
|
|
5635
|
+
});
|
|
5636
|
+
if (!report.ok) process.exit(1);
|
|
5637
|
+
}
|
|
5638
|
+
var DEFAULT_MCP_URL, DOCTOR_DESCRIPTION;
|
|
5639
|
+
var init_doctor = __esm({
|
|
5640
|
+
"packages/cli/src/commands/doctor.ts"() {
|
|
5641
|
+
"use strict";
|
|
5642
|
+
init_src4();
|
|
5643
|
+
init_src();
|
|
5644
|
+
init_describe();
|
|
5645
|
+
init_env();
|
|
5646
|
+
init_environment();
|
|
5647
|
+
init_output();
|
|
5648
|
+
init_entitlement();
|
|
5649
|
+
DEFAULT_MCP_URL = "http://127.0.0.1:3845/mcp";
|
|
5650
|
+
DOCTOR_DESCRIPTION = {
|
|
5651
|
+
name: "doctor",
|
|
5652
|
+
summary: "Check whether this machine can run tendril generate end to end.",
|
|
5653
|
+
args: [],
|
|
5654
|
+
flags: [
|
|
5655
|
+
{ flag: "--mcp-url <url>", description: "Figma MCP endpoint", default: DEFAULT_MCP_URL },
|
|
5656
|
+
{ flag: "--json", description: "Machine-readable output" }
|
|
5657
|
+
],
|
|
5658
|
+
output: {
|
|
5659
|
+
ok: "boolean \u2014 Figma desktop MCP reachable AND a scoring browser found (fonts resolve lazily per design system; keys are informational)",
|
|
5660
|
+
checks: "[{ name, ok, detail, remediation? }]"
|
|
5661
|
+
},
|
|
5662
|
+
exitCodes: { 0: "healthy", 1: "one or more required checks failed" },
|
|
5663
|
+
examples: ["tendril doctor", "tendril doctor --json"]
|
|
5664
|
+
};
|
|
5665
|
+
}
|
|
5666
|
+
});
|
|
5667
|
+
|
|
5389
5668
|
// packages/llm/src/model-config.ts
|
|
5390
5669
|
import { z as z6 } from "zod";
|
|
5391
5670
|
function resolveModel(config, requestedId) {
|
|
@@ -6687,7 +6966,8 @@ __export(record_exports, {
|
|
|
6687
6966
|
runRecordPlan: () => runRecordPlan,
|
|
6688
6967
|
runRecordStatus: () => runRecordStatus
|
|
6689
6968
|
});
|
|
6690
|
-
import { existsSync as existsSync19, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "node:fs";
|
|
6969
|
+
import { existsSync as existsSync19, mkdtempSync as mkdtempSync2, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "node:fs";
|
|
6970
|
+
import os5 from "node:os";
|
|
6691
6971
|
import path25 from "node:path";
|
|
6692
6972
|
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
6693
6973
|
function symbolsFromMetadataEnvelope(file, sourceFrame) {
|
|
@@ -6734,10 +7014,42 @@ function runRecordPlan(opts) {
|
|
|
6734
7014
|
}
|
|
6735
7015
|
defaults[spec.slice(0, eq)] = spec.slice(eq + 1);
|
|
6736
7016
|
}
|
|
7017
|
+
if (opts.metadataRawFile !== void 0 && opts.metadataRawPartsFile !== void 0) {
|
|
7018
|
+
fail(opts, ExitCode.InputValidation, {
|
|
7019
|
+
error: "pass at most one of --metadata-raw-file and --metadata-raw-parts-file",
|
|
7020
|
+
code: "bad-envelope",
|
|
7021
|
+
remediation: "One get_metadata response: single block \u2192 --metadata-raw-file; multi-block \u2192 --metadata-raw-parts-file (a JSON array of the blocks)."
|
|
7022
|
+
});
|
|
7023
|
+
}
|
|
7024
|
+
const metadataEntries = (opts.metadataFiles ?? []).map((spec) => {
|
|
7025
|
+
const [file, frame] = spec.split("@");
|
|
7026
|
+
return frame === void 0 ? { file } : { file, frame };
|
|
7027
|
+
});
|
|
7028
|
+
const rawFile = opts.metadataRawPartsFile ?? opts.metadataRawFile;
|
|
7029
|
+
if (rawFile !== void 0) {
|
|
7030
|
+
try {
|
|
7031
|
+
const envelope = rawEnvelopeFromFile(rawFile, opts.metadataRawPartsFile !== void 0);
|
|
7032
|
+
const tmp = path25.join(mkdtempSync2(path25.join(os5.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
|
|
7033
|
+
writeFileSync9(tmp, JSON.stringify(envelope));
|
|
7034
|
+
metadataEntries.push({ file: tmp });
|
|
7035
|
+
} catch (err) {
|
|
7036
|
+
fail(opts, ExitCode.InputValidation, {
|
|
7037
|
+
error: `could not read raw metadata ${rawFile}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
7038
|
+
code: "bad-envelope",
|
|
7039
|
+
remediation: "The raw file holds the get_metadata response text exactly as received (or, for --metadata-raw-parts-file, a JSON array of its blocks)."
|
|
7040
|
+
});
|
|
7041
|
+
}
|
|
7042
|
+
}
|
|
7043
|
+
if (metadataEntries.length === 0) {
|
|
7044
|
+
fail(opts, ExitCode.InputValidation, {
|
|
7045
|
+
error: "no metadata provided",
|
|
7046
|
+
code: "bad-envelope",
|
|
7047
|
+
remediation: "Pass --metadata <envelope.json> (repeatable), or the response text via --metadata-raw-file / --metadata-raw-parts-file."
|
|
7048
|
+
});
|
|
7049
|
+
}
|
|
6737
7050
|
let symbols = [];
|
|
6738
7051
|
let metadataTruncated = false;
|
|
6739
|
-
for (const
|
|
6740
|
-
const [file, frame] = spec.split("@");
|
|
7052
|
+
for (const { file, frame } of metadataEntries) {
|
|
6741
7053
|
try {
|
|
6742
7054
|
const parsed = symbolsFromMetadataEnvelope(path25.resolve(file), frame);
|
|
6743
7055
|
symbols.push(...parsed.symbols);
|
|
@@ -6770,8 +7082,7 @@ function runRecordPlan(opts) {
|
|
|
6770
7082
|
});
|
|
6771
7083
|
}
|
|
6772
7084
|
if (symbols.length === 0) {
|
|
6773
|
-
const leads =
|
|
6774
|
-
const [file] = spec.split("@");
|
|
7085
|
+
const leads = metadataEntries.flatMap(({ file }) => {
|
|
6775
7086
|
try {
|
|
6776
7087
|
const env = JSON.parse(readFileSync16(path25.resolve(file), "utf8"));
|
|
6777
7088
|
return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
|
|
@@ -8464,8 +8775,8 @@ var init_adapter = __esm({
|
|
|
8464
8775
|
});
|
|
8465
8776
|
|
|
8466
8777
|
// packages/generate/src/bundle-emit.ts
|
|
8467
|
-
import { createHash as
|
|
8468
|
-
import { existsSync as existsSync23, readFileSync as readFileSync20, readdirSync as readdirSync7, writeFileSync as writeFileSync11 } from "node:fs";
|
|
8778
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
8779
|
+
import { copyFileSync, existsSync as existsSync23, mkdirSync as mkdirSync7, readFileSync as readFileSync20, readdirSync as readdirSync7, rmSync as rmSync3, writeFileSync as writeFileSync11 } from "node:fs";
|
|
8469
8780
|
import path29 from "node:path";
|
|
8470
8781
|
function pinFromConfigs(configs) {
|
|
8471
8782
|
const domains = /* @__PURE__ */ new Map();
|
|
@@ -8514,6 +8825,37 @@ function cssFontFamilies(css) {
|
|
|
8514
8825
|
}
|
|
8515
8826
|
return [...out];
|
|
8516
8827
|
}
|
|
8828
|
+
function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE) {
|
|
8829
|
+
const header = [
|
|
8830
|
+
"/* tendril fonts.css \u2014 the exact faces this bundle was scored with (sha-pinned in",
|
|
8831
|
+
" component.json requiredFonts). Load it alongside styles.css so the component",
|
|
8832
|
+
" renders in the recorded typeface \u2014 without it the browser substitutes, which is",
|
|
8833
|
+
" exactly the drift verification exists to rule out. Not read by scoring. */"
|
|
8834
|
+
];
|
|
8835
|
+
const lines = [];
|
|
8836
|
+
for (const face of faces) {
|
|
8837
|
+
const src = path29.join(cacheDir, path29.basename(face.file));
|
|
8838
|
+
const target = `./fonts/${path29.basename(face.file)}`;
|
|
8839
|
+
const format = FONT_FORMATS[path29.extname(face.file).toLowerCase()] ?? "truetype";
|
|
8840
|
+
const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
|
|
8841
|
+
const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
|
|
8842
|
+
if (face.source.startsWith("local:")) {
|
|
8843
|
+
lines.push(
|
|
8844
|
+
`/* '${family}' ${face.weight} is a user-licensed face (tendril fonts add; sha256 ${face.sha256.slice(0, 16)}\u2026).`,
|
|
8845
|
+
` Licensed bytes are never copied into bundles \u2014 place your copy at ${target} and uncomment: */`,
|
|
8846
|
+
`/* ${decl} */`
|
|
8847
|
+
);
|
|
8848
|
+
} else if (existsSync23(src) && createHash4("sha256").update(readFileSync20(src)).digest("hex") === face.sha256) {
|
|
8849
|
+
mkdirSync7(path29.join(bundleDir, "fonts"), { recursive: true });
|
|
8850
|
+
copyFileSync(src, path29.join(bundleDir, "fonts", path29.basename(face.file)));
|
|
8851
|
+
lines.push(decl);
|
|
8852
|
+
} else {
|
|
8853
|
+
lines.push(`/* '${family}' ${face.weight} (sha256 ${face.sha256.slice(0, 16)}\u2026) could not be shipped: cache bytes missing or hash mismatch \u2014 re-run \`tendril fonts resolve\` and re-emit. */`);
|
|
8854
|
+
}
|
|
8855
|
+
}
|
|
8856
|
+
return lines.length === 0 ? null : `${[...header, ...lines].join("\n")}
|
|
8857
|
+
`;
|
|
8858
|
+
}
|
|
8517
8859
|
function countLatticeSymbols(setDir) {
|
|
8518
8860
|
const manifestFile = path29.join(setDir, "recording-set.json");
|
|
8519
8861
|
if (existsSync23(manifestFile)) {
|
|
@@ -8554,7 +8896,7 @@ function recordingSetHash(setDir, configs) {
|
|
|
8554
8896
|
relPaths,
|
|
8555
8897
|
(p) => new Uint8Array(readFileSync20(path29.join(setDir, p))),
|
|
8556
8898
|
(chunks) => {
|
|
8557
|
-
const h =
|
|
8899
|
+
const h = createHash4("sha256");
|
|
8558
8900
|
for (const c of chunks) h.update(c);
|
|
8559
8901
|
return h.digest("hex");
|
|
8560
8902
|
}
|
|
@@ -8564,7 +8906,11 @@ function statusOf(s) {
|
|
|
8564
8906
|
return tierOf(s, BARS.cert);
|
|
8565
8907
|
}
|
|
8566
8908
|
function emitBundleV1(opts) {
|
|
8567
|
-
const
|
|
8909
|
+
const substituted = (opts.substitutedFamilies ?? []).length > 0;
|
|
8910
|
+
const statuses = opts.scores.map((s) => {
|
|
8911
|
+
const tier = statusOf(s);
|
|
8912
|
+
return { rep: s.rep, similarity: s.similarity, inkRecall: s.inkRecall, ...s.exact !== void 0 ? { exact: s.exact } : {}, status: substituted && tier === "certified" ? "pass" : tier };
|
|
8913
|
+
});
|
|
8568
8914
|
const certified = statuses.filter((s) => s.status === "certified").length;
|
|
8569
8915
|
const pass = statuses.filter((s) => s.status !== "fail").length;
|
|
8570
8916
|
const lattice = countLatticeSymbols(opts.task.set);
|
|
@@ -8637,17 +8983,26 @@ function emitBundleV1(opts) {
|
|
|
8637
8983
|
${stripped}`);
|
|
8638
8984
|
written.push(stylesPath);
|
|
8639
8985
|
}
|
|
8986
|
+
const fontsCssPath = path29.join(opts.bundleDir, "fonts.css");
|
|
8987
|
+
rmSync3(path29.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
|
|
8988
|
+
rmSync3(fontsCssPath, { force: true });
|
|
8989
|
+
const fontsCss = fontsCssFor(requiredFontsManifest(families, opts.fontCacheDir), opts.bundleDir, opts.fontCacheDir);
|
|
8990
|
+
if (fontsCss !== null) {
|
|
8991
|
+
writeFileSync11(fontsCssPath, fontsCss);
|
|
8992
|
+
written.push(fontsCssPath);
|
|
8993
|
+
}
|
|
8640
8994
|
const unrecorded = lattice === null ? null : Math.max(0, lattice - statuses.length);
|
|
8641
8995
|
const statusLine = `bundle: ${pass}/${statuses.length} recorded configs \u2265 pass bar (${certified} certified), ` + (interaction.length === 0 ? "interaction behaviors NONE VERIFIED" : `interaction behaviors ${interaction.filter((b) => b.pass).length}/${interaction.length}`) + (prelude.length === 0 ? "" : `, page hygiene ${prelude.filter((b) => b.pass).length}/${prelude.length}`) + (unrecorded === null ? "" : `; ${unrecorded} lattice configs UNVERIFIED`) + ` \u2014 claims in component.json, recompute with \`tendril verify\``;
|
|
8642
8996
|
return { manifest, statusLine, written };
|
|
8643
8997
|
}
|
|
8644
|
-
var BARS;
|
|
8998
|
+
var FONT_FORMATS, BARS;
|
|
8645
8999
|
var init_bundle_emit = __esm({
|
|
8646
9000
|
"packages/generate/src/bundle-emit.ts"() {
|
|
8647
9001
|
"use strict";
|
|
8648
9002
|
init_src6();
|
|
8649
9003
|
init_src();
|
|
8650
9004
|
init_src4();
|
|
9005
|
+
FONT_FORMATS = { ".woff2": "woff2", ".woff": "woff", ".ttf": "truetype", ".otf": "opentype" };
|
|
8651
9006
|
BARS = {
|
|
8652
9007
|
pass: { sim: 0.95, ink: 0.95 },
|
|
8653
9008
|
cert: { sim: 0.97, ink: 0.95 }
|
|
@@ -8826,6 +9181,12 @@ function taskFontFamilies(setDir) {
|
|
|
8826
9181
|
return null;
|
|
8827
9182
|
}
|
|
8828
9183
|
}
|
|
9184
|
+
function unprovisionedFamilies(setDir, cacheDir) {
|
|
9185
|
+
const declared = taskFontFamilies(setDir);
|
|
9186
|
+
if (declared === null) return [];
|
|
9187
|
+
const provided = new Set(verifiedFontFamilies(cacheDir).map((f) => f.family.toLowerCase()));
|
|
9188
|
+
return declared.filter((f) => !provided.has(f.toLowerCase()));
|
|
9189
|
+
}
|
|
8829
9190
|
var init_font_guidance = __esm({
|
|
8830
9191
|
"packages/cli/src/font-guidance.ts"() {
|
|
8831
9192
|
"use strict";
|
|
@@ -8990,6 +9351,12 @@ async function runVerify(opts) {
|
|
|
8990
9351
|
remediation: fontsUnprovenRemediation(task.set)
|
|
8991
9352
|
});
|
|
8992
9353
|
}
|
|
9354
|
+
const substitutedFamilies = unprovisionedFamilies(task.set);
|
|
9355
|
+
if (substitutedFamilies.length > 0) {
|
|
9356
|
+
warn(opts, `substituted fonts: cache lacks ${substitutedFamilies.map((f) => `"${f}"`).join(", ")} \u2014 scores measure a substitute face and no config can be certified under one (the stamp covers only the provisioned subset); resolve the families to certify`);
|
|
9357
|
+
} else if (opts.bar === "cert" && taskFontFamilies(task.set) === null) {
|
|
9358
|
+
warn(opts, "family coverage could not be established from this recording (no declared families) \u2014 the certification gate covers only cache non-emptiness here");
|
|
9359
|
+
}
|
|
8993
9360
|
const missing = task.configs.filter(
|
|
8994
9361
|
(c) => !existsSync25(path31.join(task.set, c.rep, "get_screenshot.json")) || !existsSync25(path31.join(task.set, c.rep, "get_metadata.json"))
|
|
8995
9362
|
);
|
|
@@ -9003,7 +9370,7 @@ async function runVerify(opts) {
|
|
|
9003
9370
|
const bar = BARS2[opts.bar];
|
|
9004
9371
|
const evidenceDir = path31.join(opts.bundleDir, "verify-evidence");
|
|
9005
9372
|
const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
9006
|
-
const quality = await checkBundleQuality(opts.bundleDir, task.entry);
|
|
9373
|
+
const quality = await checkBundleQuality(opts.bundleDir, task.entry, { dir: task.set, reps: task.configs.map((c) => c.rep) });
|
|
9007
9374
|
const bundleCss = ["tokens.css", "styles.css"].map((f) => path31.join(opts.bundleDir, f)).filter((f) => existsSync25(f)).map((f) => readFileSync22(f, "utf8")).join("\n");
|
|
9008
9375
|
const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
|
|
9009
9376
|
const parity = await checkHoverParity(task, opts.bundleDir);
|
|
@@ -9029,6 +9396,10 @@ async function runVerify(opts) {
|
|
|
9029
9396
|
status = "pass";
|
|
9030
9397
|
certDemote.push(...s.absentInk.map((c) => `absent ink: ${c.name ?? "unnamed region"} \u2014 ${c.px}px of recorded ink with no render ink within reach (missing or invisible feature)`));
|
|
9031
9398
|
}
|
|
9399
|
+
if (status === "certified" && substitutedFamilies.length > 0) {
|
|
9400
|
+
status = "pass";
|
|
9401
|
+
certDemote.push(`substituted fonts: cache lacks ${substitutedFamilies.map((f) => `"${f}"`).join(", ")} \u2014 certification never measures a substitute face`);
|
|
9402
|
+
}
|
|
9032
9403
|
const base = certDemote.length > 0 ? { ...reported, status, demotedBy: certDemote } : { ...reported, status };
|
|
9033
9404
|
const reasons = demotions.get(s.rep);
|
|
9034
9405
|
return reasons === void 0 ? base : { ...base, status: "fail", demotedBy: [...certDemote.length > 0 ? certDemote : [], ...reasons] };
|
|
@@ -9191,9 +9562,27 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
9191
9562
|
}
|
|
9192
9563
|
process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
|
|
9193
9564
|
`);
|
|
9565
|
+
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path31.join(opts.bundleDir, f)).filter((f) => existsSync25(f)).map((f) => readFileSync22(f, "utf8")).join("\n")));
|
|
9566
|
+
if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
|
|
9567
|
+
process.stdout.write(`fonts: scored with Tendril-cache faces \u2014 a consuming app must provision the same families (the bundle ships fonts.css when faces are shippable; sha-pinned list in component.json requiredFonts)
|
|
9568
|
+
`);
|
|
9569
|
+
}
|
|
9194
9570
|
process.stdout.write(`evidence: ${evidenceDir} (render/ref/diff per config)
|
|
9195
9571
|
`);
|
|
9196
9572
|
});
|
|
9573
|
+
if (opts.bar === "cert" && substitutedFamilies.length > 0) {
|
|
9574
|
+
const names = substitutedFamilies.map((f) => `"${f}"`).join(", ");
|
|
9575
|
+
if (opts.json) {
|
|
9576
|
+
process.stderr.write(`${JSON.stringify({ error: `font cache lacks ${names} \u2014 certification never measures a substitute face (scores above are pass-capped)`, code: "fonts-unproven", remediation: fontsUnprovenRemediation(task.set) })}
|
|
9577
|
+
`);
|
|
9578
|
+
} else {
|
|
9579
|
+
process.stderr.write(`error (fonts-unproven): font cache lacks ${names} \u2014 certification never measures a substitute face (scores above are pass-capped)
|
|
9580
|
+
\u2192 ${fontsUnprovenRemediation(task.set)}
|
|
9581
|
+
`);
|
|
9582
|
+
}
|
|
9583
|
+
process.exitCode = ExitCode.FontsUnproven;
|
|
9584
|
+
return;
|
|
9585
|
+
}
|
|
9197
9586
|
if (!ok) {
|
|
9198
9587
|
warn(
|
|
9199
9588
|
opts,
|
|
@@ -9227,7 +9616,7 @@ __export(engine_exports, {
|
|
|
9227
9616
|
runEngineBrief: () => runEngineBrief,
|
|
9228
9617
|
runEngineScore: () => runEngineScore
|
|
9229
9618
|
});
|
|
9230
|
-
import { existsSync as existsSync26, mkdirSync as
|
|
9619
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync8, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
|
|
9231
9620
|
import path32 from "node:path";
|
|
9232
9621
|
function resolveEngineTask(opts, callerCwd) {
|
|
9233
9622
|
const asPath = path32.resolve(callerCwd, opts.taskOrSet);
|
|
@@ -9276,7 +9665,7 @@ DO NOT INVENT PIXELS FOR THESE POSES. Implement them ONLY as the composition of
|
|
|
9276
9665
|
${notRecorded}` : "";
|
|
9277
9666
|
let fontProvisioning;
|
|
9278
9667
|
if (existsSync26(manifestPath2)) {
|
|
9279
|
-
const provided =
|
|
9668
|
+
const provided = verifiedFontFamilies().map((f) => f.family);
|
|
9280
9669
|
const unprovided = recordedFontFamilies(task.set).filter((r) => !provided.some((p) => p.toLowerCase() === r.toLowerCase()));
|
|
9281
9670
|
if (unprovided.length > 0) {
|
|
9282
9671
|
const names = unprovided.map((f) => `'${f}'`).join(", ");
|
|
@@ -9286,10 +9675,10 @@ ${notRecorded}` : "";
|
|
|
9286
9675
|
prompt: `This design's text uses ${names}, which is not in the local font kit. How should it be handled?`,
|
|
9287
9676
|
options: [
|
|
9288
9677
|
`(Recommended) Run \`tendril fonts resolve --set ${task.set}\` \u2014 open-source families are fetched automatically. Any face that fails there is one you license privately: run \`tendril fonts add "<Family>" <weight> <file>\` with the .woff2/.ttf/.otf you hold (the file never leaves this machine). Then re-run this brief \u2014 exact text fidelity.`,
|
|
9289
|
-
|
|
9678
|
+
'Continue with a substitute face: generation proceeds in a provided family, no config can score "certified" under it (cert-bar runs exit fonts-unproven after their report), the substitution is disclosed, and small text differences are expected and not fixable from CSS.'
|
|
9290
9679
|
]
|
|
9291
9680
|
},
|
|
9292
|
-
nonInteractive: "Continue with the substitute and state the substitution in your report."
|
|
9681
|
+
nonInteractive: opts.bar === "cert" ? `Resolve the families first (\`tendril fonts resolve --set ${task.set}\`) \u2014 this loop targets the cert bar, and scoring exits fonts-unproven under a substitute, so continuing without resolving is a dead end.` : "Continue with the substitute and state the substitution in your report."
|
|
9293
9682
|
};
|
|
9294
9683
|
}
|
|
9295
9684
|
}
|
|
@@ -9298,7 +9687,7 @@ ${notRecorded}` : "";
|
|
|
9298
9687
|
=== TASK PAYLOAD (recorded truth, verbatim) ===
|
|
9299
9688
|
${segments}`;
|
|
9300
9689
|
const payloadFile = path32.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
|
|
9301
|
-
|
|
9690
|
+
mkdirSync8(path32.dirname(payloadFile), { recursive: true });
|
|
9302
9691
|
writeFileSync12(payloadFile, payload);
|
|
9303
9692
|
emitData(
|
|
9304
9693
|
opts,
|
|
@@ -9359,6 +9748,10 @@ async function runEngineScore(opts) {
|
|
|
9359
9748
|
remediation: fontsUnprovenRemediation(task.set)
|
|
9360
9749
|
});
|
|
9361
9750
|
}
|
|
9751
|
+
const substitutedFamilies = unprovisionedFamilies(task.set);
|
|
9752
|
+
if (substitutedFamilies.length > 0) {
|
|
9753
|
+
warn(opts, `substituted fonts: cache lacks ${substitutedFamilies.map((f) => `"${f}"`).join(", ")} \u2014 scores measure a substitute face and no config can be certified under one (the stamp covers only the provisioned subset); resolve the families to certify`);
|
|
9754
|
+
}
|
|
9362
9755
|
if (opts.rebind !== true && existsSync26(path32.join(candidateDir, "component.json"))) {
|
|
9363
9756
|
const prior = (() => {
|
|
9364
9757
|
try {
|
|
@@ -9391,7 +9784,7 @@ async function runEngineScore(opts) {
|
|
|
9391
9784
|
const parityCoverage = parity.length > 0 ? `${parity.filter((p) => p.pass).length}/${parity.length} hover-forced configs` : "not applicable (no hover-forced configs in this set)";
|
|
9392
9785
|
const obj = objective(scores, behaviors);
|
|
9393
9786
|
const total = scores.length + behaviors.length;
|
|
9394
|
-
const quality = await checkBundleQuality(candidateDir, task.entry);
|
|
9787
|
+
const quality = await checkBundleQuality(candidateDir, task.entry, { dir: task.set, reps: task.configs.map((c) => c.rep) });
|
|
9395
9788
|
const qualityFeedback = quality.findings.length === 0 && !quality.tokensAbsent ? "" : `
|
|
9396
9789
|
|
|
9397
9790
|
QUALITY (does not affect the bar \u2014 fix alongside the failing configs):
|
|
@@ -9402,7 +9795,7 @@ ${[
|
|
|
9402
9795
|
const allPass = obj[0] === total && total > 0;
|
|
9403
9796
|
const certBar = BARS3["cert"];
|
|
9404
9797
|
const parityDemoted = new Set(parity.filter((p) => !p.pass).map((p) => p.id.replace(/^parity:/, "")));
|
|
9405
|
-
const certifiedReps = scores.filter((sc) => tierOf(sc, certBar) === "certified" && !parityDemoted.has(sc.rep) && (sc.absentInk === void 0 || sc.absentInk.length === 0)).map((sc) => sc.rep);
|
|
9798
|
+
const certifiedReps = substitutedFamilies.length > 0 ? [] : scores.filter((sc) => tierOf(sc, certBar) === "certified" && !parityDemoted.has(sc.rep) && (sc.absentInk === void 0 || sc.absentInk.length === 0)).map((sc) => sc.rep);
|
|
9406
9799
|
const certifiedSet = new Set(certifiedReps);
|
|
9407
9800
|
const absentFindings = scores.flatMap((sc) => (sc.absentInk ?? []).map((c) => `- ${sc.rep}: ${c.name ?? "unnamed region"} \u2014 ${c.px}px of recorded ink with NO render ink within reach at [x ${c.x}-${c.x + c.w}, y ${c.y}-${c.y + c.h}] (missing or invisible feature; magnified crops in the evidence dir; coordinates are reference-space \u2014 subtract the config's seat for box-space). A certified-tier config with such a finding is demoted to pass.`));
|
|
9408
9801
|
const absentBlock = absentFindings.length > 0 ? `
|
|
@@ -9423,7 +9816,8 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
9423
9816
|
model: `${opts.model} (self-reported${opts.host !== void 0 ? ` via ${opts.host}` : ""})`,
|
|
9424
9817
|
scores,
|
|
9425
9818
|
behaviors,
|
|
9426
|
-
environment: environmentStamp(taskFontFamilies(task.set))
|
|
9819
|
+
environment: environmentStamp(taskFontFamilies(task.set)),
|
|
9820
|
+
substitutedFamilies
|
|
9427
9821
|
});
|
|
9428
9822
|
emitData(
|
|
9429
9823
|
opts,
|
|
@@ -9461,6 +9855,19 @@ ${obj[0]}/${total} \xB7 certified ${certifiedReps.length}/${total} \xB7 floor=${
|
|
|
9461
9855
|
}
|
|
9462
9856
|
}
|
|
9463
9857
|
);
|
|
9858
|
+
if (opts.bar === "cert" && substitutedFamilies.length > 0) {
|
|
9859
|
+
const names = substitutedFamilies.map((f) => `"${f}"`).join(", ");
|
|
9860
|
+
if (opts.json) {
|
|
9861
|
+
process.stderr.write(`${JSON.stringify({ error: `font cache lacks ${names} \u2014 certification never measures a substitute face (scores above are pass-capped)`, code: "fonts-unproven", remediation: fontsUnprovenRemediation(task.set) })}
|
|
9862
|
+
`);
|
|
9863
|
+
} else {
|
|
9864
|
+
process.stderr.write(`error (fonts-unproven): font cache lacks ${names} \u2014 certification never measures a substitute face (scores above are pass-capped)
|
|
9865
|
+
\u2192 ${fontsUnprovenRemediation(task.set)}
|
|
9866
|
+
`);
|
|
9867
|
+
}
|
|
9868
|
+
process.exitCode = ExitCode.FontsUnproven;
|
|
9869
|
+
return;
|
|
9870
|
+
}
|
|
9464
9871
|
if (!allPass) process.exitCode = ExitCode.VerificationFailed;
|
|
9465
9872
|
}
|
|
9466
9873
|
var BARS3;
|
|
@@ -9660,9 +10067,402 @@ var init_codeconnect = __esm({
|
|
|
9660
10067
|
}
|
|
9661
10068
|
});
|
|
9662
10069
|
|
|
9663
|
-
// packages/
|
|
9664
|
-
|
|
9665
|
-
|
|
10070
|
+
// packages/mcp/src/server.ts
|
|
10071
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
10072
|
+
import { existsSync as existsSync28, mkdtempSync as mkdtempSync3, readFileSync as readFileSync25, readdirSync as readdirSync8, writeFileSync as writeFileSync14 } from "node:fs";
|
|
10073
|
+
import os6 from "node:os";
|
|
10074
|
+
import path34 from "node:path";
|
|
10075
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
10076
|
+
import { z as z12 } from "zod";
|
|
10077
|
+
function sourceHash() {
|
|
10078
|
+
const dir = path34.dirname(fileURLToPath5(import.meta.url));
|
|
10079
|
+
const h = createHash5("sha256");
|
|
10080
|
+
for (const f of readdirSync8(dir).filter((n) => n.endsWith(".ts")).sort()) {
|
|
10081
|
+
h.update(f);
|
|
10082
|
+
h.update(readFileSync25(path34.join(dir, f)));
|
|
10083
|
+
}
|
|
10084
|
+
return h.digest("hex").slice(0, 16);
|
|
10085
|
+
}
|
|
10086
|
+
var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
|
|
10087
|
+
var init_server = __esm({
|
|
10088
|
+
"packages/mcp/src/server.ts"() {
|
|
10089
|
+
"use strict";
|
|
10090
|
+
REPO_ROOT3 = path34.resolve(path34.dirname(fileURLToPath5(import.meta.url)), "..", "..", "..");
|
|
10091
|
+
CLI_BIN = path34.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
|
|
10092
|
+
BUNDLED_CLI = path34.join(path34.dirname(fileURLToPath5(import.meta.url)), "tendril.js");
|
|
10093
|
+
CLI_SPAWN = existsSync28(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
|
|
10094
|
+
str = (d) => z12.string().describe(d);
|
|
10095
|
+
optStr = (d) => z12.string().optional().describe(d);
|
|
10096
|
+
TOOLS = [
|
|
10097
|
+
{
|
|
10098
|
+
name: "tendril_record_plan",
|
|
10099
|
+
description: "ENTRY POINT for implementing/building a React component from a Figma design or figma.com URL \u2014 start the Tendril pipeline here (after loading the tendril skill, if installed). Plans a recording session: computes the rep queue (anchor + one-factor + conflict crosses) from the verbatim get_metadata response (pass its blocks via metadataParts \u2014 no file to write) and persists the set manifest. Resumes if the set already exists. The output may carry USER QUESTIONS \u2014 defaultsToConfirm (which pose is the component's default) or a multiple-component-sets error (which set to record): render them to a present user and apply the answers via `defaults` / `componentSet`; non-interactive runs follow each question's stated fallback. Recording cost is stated in figmaCallEstimate, never asked about \u2014 proceed with what the user provided.",
|
|
10100
|
+
schema: z12.object({
|
|
10101
|
+
setDir: str("recording set directory to create/resume"),
|
|
10102
|
+
component: str("component/system name"),
|
|
10103
|
+
// Parts array FIRST, same reason as ingest_rep: real responses
|
|
10104
|
+
// are usually multi-block, and the file param made plan the ONE
|
|
10105
|
+
// remaining hand-built-envelope entry point (run 10: the agent
|
|
10106
|
+
// wrote the file twice — once as text, once as JSON envelope).
|
|
10107
|
+
metadataParts: z12.array(z12.string()).optional().describe("the frame-level get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join or save them yourself; this is the NORMAL param"),
|
|
10108
|
+
metadata: optStr("ONLY when get_metadata genuinely returned one single block: its text verbatim (otherwise use metadataParts)"),
|
|
10109
|
+
metadataFiles: z12.array(z12.string()).optional().describe('saved verbatim get_metadata envelope file paths \u2014 JSON shape {"content":[{"type":"text","text":"<frame \u2026>"}]}; optionally <file>@<frameId>. Prefer metadataParts: no file to write'),
|
|
10110
|
+
defaults: z12.array(z12.string()).optional().describe(`axis defaults as "Axis=Value" (from the user's defaultsToConfirm answers; may re-plan a set with nothing recorded yet)`),
|
|
10111
|
+
componentSet: optStr("record only this component set (the user's pick from a multiple-component-sets error)")
|
|
10112
|
+
}),
|
|
10113
|
+
// Texts ride temp files, never argv: Windows caps a command line at
|
|
10114
|
+
// ~32 KB and metadata envelopes can exceed it.
|
|
10115
|
+
argv: (i) => {
|
|
10116
|
+
const argvOut = ["record", "plan", "--set", i["setDir"], "--component", i["component"]];
|
|
10117
|
+
if (i["metadata"] !== void 0 && i["metadataParts"] !== void 0) throw new Error("pass at most one of `metadata` and `metadataParts`");
|
|
10118
|
+
const files = i["metadataFiles"];
|
|
10119
|
+
if (files !== void 0 && files.length > 0) argvOut.push("--metadata", ...files);
|
|
10120
|
+
const single = i["metadata"];
|
|
10121
|
+
const parts = i["metadataParts"];
|
|
10122
|
+
if (single !== void 0 || parts !== void 0) {
|
|
10123
|
+
const tmp = path34.join(mkdtempSync3(path34.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
10124
|
+
if (single !== void 0) {
|
|
10125
|
+
writeFileSync14(tmp, single);
|
|
10126
|
+
argvOut.push("--metadata-raw-file", tmp);
|
|
10127
|
+
} else {
|
|
10128
|
+
if (parts.length === 0) throw new Error("`metadataParts` must be a non-empty array of block texts");
|
|
10129
|
+
writeFileSync14(tmp, JSON.stringify(parts));
|
|
10130
|
+
argvOut.push("--metadata-raw-parts-file", tmp);
|
|
10131
|
+
}
|
|
10132
|
+
}
|
|
10133
|
+
if (argvOut.length === 6) throw new Error("nothing to plan from \u2014 pass metadataParts (normal), metadata (single-block), or metadataFiles");
|
|
10134
|
+
argvOut.push(...i["defaults"] !== void 0 ? ["--default", ...i["defaults"]] : [], ...i["componentSet"] !== void 0 ? ["--component-set", i["componentSet"]] : []);
|
|
10135
|
+
return argvOut;
|
|
10136
|
+
}
|
|
10137
|
+
},
|
|
10138
|
+
{
|
|
10139
|
+
name: "tendril_doctor",
|
|
10140
|
+
annotations: { readOnlyHint: true },
|
|
10141
|
+
description: "Machine readiness + version status in one shot: installed vs latest version (with publish date and update remediation), browser identity, font-cache state, Figma desktop MCP reachability. Use to self-diagnose before recording/scoring, or whenever versions are in question. Exit 1 = something not ready; the report says exactly what and how to fix it.",
|
|
10142
|
+
schema: z12.object({}),
|
|
10143
|
+
argv: () => ["doctor"]
|
|
10144
|
+
},
|
|
10145
|
+
{
|
|
10146
|
+
name: "tendril_record_next",
|
|
10147
|
+
annotations: { readOnlyHint: true },
|
|
10148
|
+
description: "Get the next pending recording instruction (which Figma MCP tool to call for which node, and how to save it). RARELY NEEDED: every ingest/fetch response already carries `next` \u2014 use this only to resume an interrupted session. The full queue is known from plan, so independent reps may be recorded in any order (and in parallel).",
|
|
10149
|
+
schema: z12.object({ setDir: str("recording set directory") }),
|
|
10150
|
+
argv: (i) => ["record", "next", "--set", i["setDir"]]
|
|
10151
|
+
},
|
|
10152
|
+
{
|
|
10153
|
+
name: "tendril_record_fetch",
|
|
10154
|
+
description: "Download a Figma asset URL (from get_screenshot's image_url) straight to disk and ingest it as the rep's envelope \u2014 the fallback when only the screenshot piece needs (re-)recording; for a rep's standard three recordings PREFER tendril_record_ingest_rep. Never download the image yourself: the bytes must not pass through your context.",
|
|
10155
|
+
schema: z12.object({
|
|
10156
|
+
setDir: str("recording set directory"),
|
|
10157
|
+
rep: str("planned rep slug"),
|
|
10158
|
+
tool: z12.enum(["get_screenshot"]).describe("get_screenshot"),
|
|
10159
|
+
url: str("image_url from the Figma response, verbatim")
|
|
10160
|
+
}),
|
|
10161
|
+
argv: (i) => ["record", "fetch", "--set", i["setDir"], "--rep", i["rep"], "--tool", i["tool"], "--url", i["url"]]
|
|
10162
|
+
},
|
|
10163
|
+
{
|
|
10164
|
+
name: "tendril_record_ingest_rep",
|
|
10165
|
+
description: "Ingest a rep's ENTIRE recording in ONE call \u2014 the get_metadata response, the get_design_context response, and the get_screenshot image_url together. Make the three Figma calls first, in protocol order (get_metadata, then get_design_context with excludeScreenshot=true, then get_screenshot), then pass all three here VERBATIM. PREFER THIS over three separate ingest/fetch calls: one approvable operation per rep instead of three. Pieces land independently: on a partial failure the error names exactly which piece(s) to re-record \u2014 the rest are already on disk. The response carries `next` and, for design context, `assets` (auto-fetched server-side; only listed failures need record_asset).",
|
|
10166
|
+
schema: z12.object({
|
|
10167
|
+
setDir: str("recording set directory"),
|
|
10168
|
+
rep: str("planned rep slug"),
|
|
10169
|
+
// Parts arrays FIRST: in the field, EVERY real Figma response is
|
|
10170
|
+
// multi-block (run 6: 49/49 reps — metadata 2 blocks, design
|
|
10171
|
+
// context 5-6), so the arrays are the norm and the single-string
|
|
10172
|
+
// params the rare case, not the reverse.
|
|
10173
|
+
metadataParts: z12.array(z12.string()).optional().describe("get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join blocks yourself. Multi-block responses are common (run 6: 49/49 reps); a single block wrapped in a one-element array is equally fine."),
|
|
10174
|
+
contextParts: z12.array(z12.string()).optional().describe("get_design_context response blocks, every block in order, each verbatim \u2014 the NORMAL param (real responses arrive as 5-6 blocks)"),
|
|
10175
|
+
screenshotUrl: optStr("image_url from the get_screenshot response, verbatim \u2014 the CLI downloads it; the bytes never pass through your context"),
|
|
10176
|
+
metadata: optStr("ONLY when get_metadata genuinely returned one single block: its text verbatim (otherwise use metadataParts)"),
|
|
10177
|
+
context: optStr("ONLY when get_design_context genuinely returned one single block: its text verbatim (otherwise use contextParts)")
|
|
10178
|
+
}),
|
|
10179
|
+
// Texts ride temp files, never argv: Windows caps a command line at
|
|
10180
|
+
// ~32 KB and design-context envelopes routinely exceed it.
|
|
10181
|
+
argv: (i) => {
|
|
10182
|
+
const argvOut = ["record", "ingest-rep", "--set", i["setDir"], "--rep", i["rep"]];
|
|
10183
|
+
const bridge = (label, single, parts) => {
|
|
10184
|
+
if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
|
|
10185
|
+
if (single === void 0 && parts === void 0) return;
|
|
10186
|
+
const tmp = path34.join(mkdtempSync3(path34.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
10187
|
+
if (single !== void 0) {
|
|
10188
|
+
writeFileSync14(tmp, single);
|
|
10189
|
+
argvOut.push(`--${label}-file`, tmp);
|
|
10190
|
+
} else {
|
|
10191
|
+
const blocks = parts;
|
|
10192
|
+
if (blocks.length === 0) throw new Error(`\`${label}Parts\` must be a non-empty array of block texts`);
|
|
10193
|
+
writeFileSync14(tmp, JSON.stringify(blocks));
|
|
10194
|
+
argvOut.push(`--${label}-parts-file`, tmp);
|
|
10195
|
+
}
|
|
10196
|
+
};
|
|
10197
|
+
bridge("metadata", i["metadata"], i["metadataParts"]);
|
|
10198
|
+
bridge("context", i["context"], i["contextParts"]);
|
|
10199
|
+
if (i["screenshotUrl"] !== void 0) argvOut.push("--screenshot-url", i["screenshotUrl"]);
|
|
10200
|
+
if (argvOut.length === 6) throw new Error("nothing to ingest \u2014 pass at least one of metadata/metadataParts, context/contextParts, screenshotUrl");
|
|
10201
|
+
return argvOut;
|
|
10202
|
+
}
|
|
10203
|
+
},
|
|
10204
|
+
{
|
|
10205
|
+
name: "tendril_record_ingest",
|
|
10206
|
+
description: "Single-piece ingest of a VERBATIM Figma tool-response \u2014 the fallback path (re-recording one failed piece, set-level get_variable_defs, get_metadata_interior for mains); for a rep's standard three recordings PREFER tendril_record_ingest_rep, which takes them all in one call. Pass `text` (single block) or `texts` (response split into multiple output blocks \u2014 each block verbatim, in order; NEVER hand-join them): the CLI constructs the envelope from the same bytes. The response includes `next` and, for get_design_context, `assets` (auto-fetched; only listed failures need manual handling).",
|
|
10207
|
+
schema: z12.object({
|
|
10208
|
+
setDir: str("recording set directory"),
|
|
10209
|
+
rep: str("planned rep slug, or __set__ for the set-level get_variable_defs"),
|
|
10210
|
+
// Enumerated, not a free string: this value reaches a file path.
|
|
10211
|
+
// As a bare string it was an arbitrary relative-path overwrite
|
|
10212
|
+
// (--tool "../../outside/victim" replaced a file outside the set,
|
|
10213
|
+
// exit 0). The sink in session.ts now contains the path too — this
|
|
10214
|
+
// is the second layer, and it makes the tool self-documenting.
|
|
10215
|
+
tool: z12.enum(["get_design_context", "get_metadata", "get_screenshot", "get_variable_defs", "get_metadata_interior"]),
|
|
10216
|
+
text: optStr("the tool response text VERBATIM \u2014 exactly as returned, unmodified (single-block responses; text tools only \u2014 screenshots go through record_fetch)"),
|
|
10217
|
+
texts: z12.array(z12.string()).optional().describe("when the response arrived as MULTIPLE output blocks: every block, in order, each verbatim \u2014 never hand-join blocks yourself"),
|
|
10218
|
+
file: optStr("path to a saved envelope JSON (alternative to text/texts)")
|
|
10219
|
+
}),
|
|
10220
|
+
// The text rides a temp file, never argv: Windows caps a command
|
|
10221
|
+
// line at ~32 KB and design-context envelopes routinely exceed it.
|
|
10222
|
+
argv: (i) => {
|
|
10223
|
+
const base = ["record", "ingest", "--set", i["setDir"], "--rep", i["rep"], "--tool", i["tool"]];
|
|
10224
|
+
const text = i["text"];
|
|
10225
|
+
const texts = i["texts"];
|
|
10226
|
+
const file = i["file"];
|
|
10227
|
+
if ([text, texts, file].filter((x) => x !== void 0).length !== 1) throw new Error("pass exactly one of `text` (single-block response), `texts` (multi-block response), or `file` (a saved envelope)");
|
|
10228
|
+
if (file !== void 0) return [...base, "--file", file];
|
|
10229
|
+
const tmp = path34.join(mkdtempSync3(path34.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
10230
|
+
if (text !== void 0) {
|
|
10231
|
+
writeFileSync14(tmp, text);
|
|
10232
|
+
return [...base, "--file", tmp, "--raw"];
|
|
10233
|
+
}
|
|
10234
|
+
writeFileSync14(tmp, JSON.stringify(texts));
|
|
10235
|
+
return [...base, "--file", tmp, "--raw-parts"];
|
|
10236
|
+
}
|
|
10237
|
+
},
|
|
10238
|
+
{
|
|
10239
|
+
name: "tendril_record_asset",
|
|
10240
|
+
description: "FALLBACK ONLY \u2014 ingest auto-fetches design-context assets; use this just for assets listed in an ingest response's `assets.failed`. Batch mode: pass `dir` to ingest every asset-*.<ext> in a directory in ONE call. SVGs with active content are rejected; sizes are capped.",
|
|
10241
|
+
schema: z12.object({
|
|
10242
|
+
setDir: str("recording set directory"),
|
|
10243
|
+
rep: str("planned rep slug"),
|
|
10244
|
+
name: optStr("asset-<id>.<ext> (single-asset mode)"),
|
|
10245
|
+
file: optStr("downloaded asset file path (single-asset mode)"),
|
|
10246
|
+
dir: optStr("batch mode: directory holding downloaded asset-*.<ext> files")
|
|
10247
|
+
}),
|
|
10248
|
+
argv: (i) => [
|
|
10249
|
+
"record",
|
|
10250
|
+
"asset",
|
|
10251
|
+
"--set",
|
|
10252
|
+
i["setDir"],
|
|
10253
|
+
"--rep",
|
|
10254
|
+
i["rep"],
|
|
10255
|
+
...i["dir"] !== void 0 ? ["--dir", i["dir"]] : ["--name", i["name"], "--file", i["file"]]
|
|
10256
|
+
]
|
|
10257
|
+
},
|
|
10258
|
+
{
|
|
10259
|
+
name: "tendril_record_status",
|
|
10260
|
+
annotations: { readOnlyHint: true },
|
|
10261
|
+
description: "Recording-set completeness: per-rep recorded/missing tools.",
|
|
10262
|
+
schema: z12.object({ setDir: str("recording set directory") }),
|
|
10263
|
+
argv: (i) => ["record", "status", "--set", i["setDir"]]
|
|
10264
|
+
},
|
|
10265
|
+
{
|
|
10266
|
+
name: "tendril_engine_brief",
|
|
10267
|
+
description: "AGENT-HARNESS engine, step 1: emits the task payload file (system brief + every recorded config's emission, box, assets, tokens) and the protocol. YOU (the calling agent) implement the bundle; the CLI is the only judge. Read the payload file completely before proposing.",
|
|
10268
|
+
schema: z12.object({
|
|
10269
|
+
taskOrSet: str("a recording-set directory (from tendril_record), or a reference task name \u2014 an unknown name returns the valid list in the error"),
|
|
10270
|
+
// Required by design, not convenience: the model choice must be
|
|
10271
|
+
// settled BEFORE generation starts. A smoke run picked its own
|
|
10272
|
+
// model silently because the ask lived in instruction text, which
|
|
10273
|
+
// evaporates in non-interactive sessions; a required parameter
|
|
10274
|
+
// cannot evaporate. Ask the user when one is present; otherwise
|
|
10275
|
+
// choose, declare, and state the reason in your report.
|
|
10276
|
+
model: str("the model that will WRITE the implementation \u2014 ask the user when interactive; declare your reasoned choice when not"),
|
|
10277
|
+
bar: optStr("pass (default) or cert"),
|
|
10278
|
+
out: optStr("payload file path override")
|
|
10279
|
+
}),
|
|
10280
|
+
argv: (i) => [
|
|
10281
|
+
"engine",
|
|
10282
|
+
"brief",
|
|
10283
|
+
i["taskOrSet"],
|
|
10284
|
+
"--model",
|
|
10285
|
+
i["model"],
|
|
10286
|
+
...i["bar"] !== void 0 ? ["--bar", i["bar"]] : [],
|
|
10287
|
+
...i["out"] !== void 0 ? ["--out", i["out"]] : []
|
|
10288
|
+
]
|
|
10289
|
+
},
|
|
10290
|
+
{
|
|
10291
|
+
name: "tendril_engine_score",
|
|
10292
|
+
description: "AGENT-HARNESS engine, step 2 (the oracle): scores a candidate bundle directory against recorded truth \u2014 per-config pixels, behaviors, hover parity \u2014 and returns feedback plus evidence artifacts. Iterate until allPass or two non-improving rounds. Only THIS tool's output counts as a score; never claim numbers yourself.",
|
|
10293
|
+
schema: z12.object({
|
|
10294
|
+
taskOrSet: str("reference task name or recording-set directory"),
|
|
10295
|
+
candidateDir: str("directory containing the proposed bundle files"),
|
|
10296
|
+
bar: optStr("pass (default) or cert"),
|
|
10297
|
+
host: optStr("your host identity (e.g. claude-code, cursor, codex) \u2014 recorded as self-reported provenance"),
|
|
10298
|
+
model: str("the model that proposed the candidate \u2014 REQUIRED; recorded as self-reported provenance, and the CLI refuses to score without it"),
|
|
10299
|
+
rebind: z12.boolean().optional().describe("explicitly re-bind an already-bound bundle to a DIFFERENT recording set \u2014 scoring refuses this otherwise, because rebinding silently rewrites the bundle's verification identity; only pass after telling the user")
|
|
10300
|
+
}),
|
|
10301
|
+
argv: (i) => [
|
|
10302
|
+
"engine",
|
|
10303
|
+
"score",
|
|
10304
|
+
i["taskOrSet"],
|
|
10305
|
+
i["candidateDir"],
|
|
10306
|
+
...i["bar"] !== void 0 ? ["--bar", i["bar"]] : [],
|
|
10307
|
+
...i["host"] !== void 0 ? ["--host", i["host"]] : [],
|
|
10308
|
+
"--model",
|
|
10309
|
+
i["model"],
|
|
10310
|
+
...i["rebind"] === true ? ["--rebind"] : []
|
|
10311
|
+
]
|
|
10312
|
+
},
|
|
10313
|
+
{
|
|
10314
|
+
name: "tendril_codeconnect",
|
|
10315
|
+
description: "Emit a Figma Code Connect template (.figma.ts) for a certified bundle \u2014 every Figma variant value mapped to its verified prop fragment from recorded truth, stamped with the bundle's trust statement. EXTRA VALUE step after verify passes: offer it to the user. Publishing is the USER'S action (their Figma token, Organization/Enterprise plan) \u2014 via npx @figma/code-connect connect publish, or the Figma MCP's own add_code_connect_map/send_code_connect_mappings tools if available in this session.",
|
|
10316
|
+
schema: z12.object({
|
|
10317
|
+
bundleDir: str("bundle directory (carries component.json)"),
|
|
10318
|
+
figmaUrl: str("figma.com /design/ URL of the COMPONENT SET, with node-id (ask the user to Copy link to selection if you don't have it)"),
|
|
10319
|
+
set: optStr("recording set override (default: the bundle's provenance path)"),
|
|
10320
|
+
out: optStr("output file path (default: <bundle>/<Component>.figma.ts)")
|
|
10321
|
+
}),
|
|
10322
|
+
argv: (i) => [
|
|
10323
|
+
"codeconnect",
|
|
10324
|
+
i["bundleDir"],
|
|
10325
|
+
"--figma-url",
|
|
10326
|
+
i["figmaUrl"],
|
|
10327
|
+
...i["set"] !== void 0 ? ["--set", i["set"]] : [],
|
|
10328
|
+
...i["out"] !== void 0 ? ["--out", i["out"]] : []
|
|
10329
|
+
]
|
|
10330
|
+
},
|
|
10331
|
+
{
|
|
10332
|
+
name: "tendril_verify",
|
|
10333
|
+
description: "Verify/check that a component matches its Figma design \u2014 use when the user asks whether an implementation is faithful to the design, or to re-certify an existing Tendril bundle. Recomputes full verification (per-config status, behaviors, composition, evidence artifacts). Free, account-less, network-less \u2014 the trust anchor. Exit 5 means sub-bar with an honest report.",
|
|
10334
|
+
schema: z12.object({
|
|
10335
|
+
bundleDir: str("bundle directory to verify"),
|
|
10336
|
+
bar: optStr("pass (default) or cert"),
|
|
10337
|
+
set: optStr("recording-set directory override")
|
|
10338
|
+
}),
|
|
10339
|
+
argv: (i) => [
|
|
10340
|
+
"verify",
|
|
10341
|
+
i["bundleDir"],
|
|
10342
|
+
...i["bar"] !== void 0 ? ["--bar", i["bar"]] : [],
|
|
10343
|
+
...i["set"] !== void 0 ? ["--set", i["set"]] : []
|
|
10344
|
+
]
|
|
10345
|
+
},
|
|
10346
|
+
{
|
|
10347
|
+
name: "tendril_generate_curated",
|
|
10348
|
+
description: "CURATED engine (explicit alternative path): generation by an allowlisted API model over the user's OpenRouter-compatible key, with cost consent, hard spend caps, and resume. Use ONLY when the user asks for API-model generation instead of implementing it yourself.",
|
|
10349
|
+
schema: z12.object({
|
|
10350
|
+
input: str("reference task name or recording-set directory"),
|
|
10351
|
+
model: optStr("OpenRouter model id (default: allowlist pointer)"),
|
|
10352
|
+
bar: optStr("pass (default) or cert"),
|
|
10353
|
+
cap: optStr("spend cap in USD (default 1.50)"),
|
|
10354
|
+
yes: z12.boolean().optional().describe("accept the cost consent (the user must have approved the spend)")
|
|
10355
|
+
}),
|
|
10356
|
+
argv: (i) => [
|
|
10357
|
+
"generate",
|
|
10358
|
+
i["input"],
|
|
10359
|
+
...i["model"] !== void 0 ? ["--model", i["model"]] : [],
|
|
10360
|
+
...i["bar"] !== void 0 ? ["--bar", i["bar"]] : [],
|
|
10361
|
+
...i["cap"] !== void 0 ? ["--cap", i["cap"]] : [],
|
|
10362
|
+
...i["yes"] === true ? ["--yes"] : []
|
|
10363
|
+
]
|
|
10364
|
+
}
|
|
10365
|
+
];
|
|
10366
|
+
BOOT_HASH = (() => {
|
|
10367
|
+
try {
|
|
10368
|
+
return sourceHash();
|
|
10369
|
+
} catch {
|
|
10370
|
+
return "unknown";
|
|
10371
|
+
}
|
|
10372
|
+
})();
|
|
10373
|
+
}
|
|
10374
|
+
});
|
|
10375
|
+
|
|
10376
|
+
// packages/cli/src/commands/permissions.ts
|
|
10377
|
+
var permissions_exports = {};
|
|
10378
|
+
__export(permissions_exports, {
|
|
10379
|
+
PERMISSIONS_DESCRIPTION: () => PERMISSIONS_DESCRIPTION,
|
|
10380
|
+
buildPermissions: () => buildPermissions,
|
|
10381
|
+
runPermissions: () => runPermissions
|
|
10382
|
+
});
|
|
10383
|
+
async function buildPermissions(options) {
|
|
10384
|
+
const LEGAL_TOOL_NAME = /^[A-Za-z0-9_-]+$/;
|
|
10385
|
+
const pipeline = new Set(FIGMA_TOOL_FALLBACK);
|
|
10386
|
+
let figmaTools = FIGMA_TOOL_FALLBACK;
|
|
10387
|
+
try {
|
|
10388
|
+
const client = new McpHttpClient({ url: options.mcpUrl ?? DEFAULT_MCP_URL, ...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {} });
|
|
10389
|
+
await client.initialize();
|
|
10390
|
+
const live = (await client.listTools()).map((t) => t.name).filter((n) => LEGAL_TOOL_NAME.test(n) && pipeline.has(n));
|
|
10391
|
+
if (live.length > 0) figmaTools = live;
|
|
10392
|
+
} catch {
|
|
10393
|
+
}
|
|
10394
|
+
return {
|
|
10395
|
+
host: "claude",
|
|
10396
|
+
serverEntries: [TENDRIL_PLUGIN_PREFIX, FIGMA_PLUGIN_PREFIX],
|
|
10397
|
+
toolEntries: [
|
|
10398
|
+
...TOOLS.map((t) => t.name).filter((n) => LEGAL_TOOL_NAME.test(n)).map((n) => `${TENDRIL_PLUGIN_PREFIX}__${n}`),
|
|
10399
|
+
...figmaTools.map((name) => `${FIGMA_PLUGIN_PREFIX}__${name}`)
|
|
10400
|
+
],
|
|
10401
|
+
serverEntriesCaution: "The server-wide entries allow EVERY tool those servers serve \u2014 for tendril that includes tendril_generate_curated (spends your OpenRouter budget when configured), and for Figma every tool the desktop server exposes, beyond the read tools this pipeline uses. The per-tool list is the recommended default.",
|
|
10402
|
+
directConfigNote: `Installed via claude mcp add instead of the plugin? Replace the prefixes: ${TENDRIL_PLUGIN_PREFIX}__<tool> becomes mcp__<your-server-name>__<tool> (same for Figma).`
|
|
10403
|
+
};
|
|
10404
|
+
}
|
|
10405
|
+
async function runPermissions(flags) {
|
|
10406
|
+
if (flags.describe) {
|
|
10407
|
+
printDescription(PERMISSIONS_DESCRIPTION);
|
|
10408
|
+
return;
|
|
10409
|
+
}
|
|
10410
|
+
const result = await buildPermissions({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} });
|
|
10411
|
+
emitData(flags, result, () => {
|
|
10412
|
+
const quoted = (xs) => xs.map((x) => ` ${JSON.stringify(x)}`).join(",\n");
|
|
10413
|
+
process.stdout.write(
|
|
10414
|
+
`Claude Code allowlist for the Tendril pipeline.
|
|
10415
|
+
Paste into .claude/settings.json under permissions.allow:
|
|
10416
|
+
|
|
10417
|
+
${quoted(result.toolEntries)}
|
|
10418
|
+
|
|
10419
|
+
Shorter but broader \u2014 one entry per server:
|
|
10420
|
+
|
|
10421
|
+
${quoted(result.serverEntries)}
|
|
10422
|
+
|
|
10423
|
+
\u26A0 ${result.serverEntriesCaution}
|
|
10424
|
+
|
|
10425
|
+
${result.directConfigNote}
|
|
10426
|
+
`
|
|
10427
|
+
);
|
|
10428
|
+
});
|
|
10429
|
+
}
|
|
10430
|
+
var FIGMA_TOOL_FALLBACK, PERMISSIONS_DESCRIPTION, TENDRIL_PLUGIN_PREFIX, FIGMA_PLUGIN_PREFIX;
|
|
10431
|
+
var init_permissions = __esm({
|
|
10432
|
+
"packages/cli/src/commands/permissions.ts"() {
|
|
10433
|
+
"use strict";
|
|
10434
|
+
init_src();
|
|
10435
|
+
init_server();
|
|
10436
|
+
init_describe();
|
|
10437
|
+
init_output();
|
|
10438
|
+
init_doctor();
|
|
10439
|
+
FIGMA_TOOL_FALLBACK = ["get_metadata", "get_design_context", "get_screenshot", "get_variable_defs", "get_motion_context", "get_figjam"];
|
|
10440
|
+
PERMISSIONS_DESCRIPTION = {
|
|
10441
|
+
name: "permissions",
|
|
10442
|
+
summary: "Print a paste-ready Claude Code permission allowlist for the Tendril pipeline (tendril + Figma MCP tools).",
|
|
10443
|
+
args: [],
|
|
10444
|
+
flags: [
|
|
10445
|
+
{ flag: "--claude", description: "Claude Code settings.json format (the default and currently only format)" },
|
|
10446
|
+
{ flag: "--mcp-url <url>", description: "Figma MCP endpoint to list live tool names from", default: DEFAULT_MCP_URL },
|
|
10447
|
+
{ flag: "--json", description: "Machine-readable output" }
|
|
10448
|
+
],
|
|
10449
|
+
output: {
|
|
10450
|
+
host: '"claude"',
|
|
10451
|
+
serverEntries: "string[] \u2014 one entry per MCP server (allows every tool it serves)",
|
|
10452
|
+
toolEntries: "string[] \u2014 per-tool entries for selective allowlists",
|
|
10453
|
+
directConfigNote: "string \u2014 prefix rewrite for non-plugin (claude mcp add) installs"
|
|
10454
|
+
},
|
|
10455
|
+
exitCodes: { 0: "always (informational)" },
|
|
10456
|
+
examples: ["tendril permissions --claude", "tendril permissions --claude --json"]
|
|
10457
|
+
};
|
|
10458
|
+
TENDRIL_PLUGIN_PREFIX = "mcp__plugin_tendril_tendril";
|
|
10459
|
+
FIGMA_PLUGIN_PREFIX = "mcp__plugin_figma_figma";
|
|
10460
|
+
}
|
|
10461
|
+
});
|
|
10462
|
+
|
|
10463
|
+
// packages/cli/src/commands/generate-route.ts
|
|
10464
|
+
var generate_route_exports = {};
|
|
10465
|
+
__export(generate_route_exports, {
|
|
9666
10466
|
routeGenerate: () => routeGenerate
|
|
9667
10467
|
});
|
|
9668
10468
|
function routeGenerate(input) {
|
|
@@ -9684,17 +10484,17 @@ __export(generate_recorded_exports, {
|
|
|
9684
10484
|
runGenerateRecorded: () => runGenerateRecorded
|
|
9685
10485
|
});
|
|
9686
10486
|
import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
|
|
9687
|
-
import { existsSync as
|
|
9688
|
-
import
|
|
10487
|
+
import { existsSync as existsSync29, readFileSync as readFileSync26 } from "node:fs";
|
|
10488
|
+
import path35 from "node:path";
|
|
9689
10489
|
async function runGenerateRecorded(opts) {
|
|
9690
10490
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
9691
|
-
const outDirAbs =
|
|
9692
|
-
const recordedAsPath =
|
|
10491
|
+
const outDirAbs = path35.resolve(callerCwd, opts.out);
|
|
10492
|
+
const recordedAsPath = path35.resolve(callerCwd, opts.recorded);
|
|
9693
10493
|
let task;
|
|
9694
10494
|
let taskName;
|
|
9695
10495
|
let authoredApi;
|
|
9696
10496
|
let composition;
|
|
9697
|
-
const isSet =
|
|
10497
|
+
const isSet = existsSync29(path35.join(recordedAsPath, "recording-set.json"));
|
|
9698
10498
|
const registry = TASKS[opts.recorded];
|
|
9699
10499
|
if (registry !== void 0 && !isSet) {
|
|
9700
10500
|
task = registry;
|
|
@@ -9703,7 +10503,7 @@ async function runGenerateRecorded(opts) {
|
|
|
9703
10503
|
try {
|
|
9704
10504
|
const authored = authorTaskFromSet(recordedAsPath);
|
|
9705
10505
|
task = authored.task;
|
|
9706
|
-
taskName =
|
|
10506
|
+
taskName = path35.basename(recordedAsPath);
|
|
9707
10507
|
authoredApi = authored.api;
|
|
9708
10508
|
const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
|
|
9709
10509
|
if (roles.success) composition = roles.data;
|
|
@@ -9729,8 +10529,12 @@ async function runGenerateRecorded(opts) {
|
|
|
9729
10529
|
remediation: fontsUnprovenRemediation(task.set)
|
|
9730
10530
|
});
|
|
9731
10531
|
}
|
|
10532
|
+
const substitutedFamilies = unprovisionedFamilies(task.set);
|
|
10533
|
+
if (substitutedFamilies.length > 0) {
|
|
10534
|
+
warn(opts, `substituted fonts: cache lacks ${substitutedFamilies.map((f) => `"${f}"`).join(", ")} \u2014 scores measure a substitute face and no config can be certified under one (the stamp covers only the provisioned subset); resolve the families to certify`);
|
|
10535
|
+
}
|
|
9732
10536
|
const missing = task.configs.filter(
|
|
9733
|
-
(c) => !
|
|
10537
|
+
(c) => !existsSync29(path35.join(task.set, c.rep, "get_screenshot.json")) || !existsSync29(path35.join(task.set, c.rep, "get_metadata.json")) || !existsSync29(path35.join(task.set, c.rep, "get_design_context.json"))
|
|
9734
10538
|
);
|
|
9735
10539
|
if (missing.length > 0) {
|
|
9736
10540
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -9800,8 +10604,8 @@ async function runGenerateRecorded(opts) {
|
|
|
9800
10604
|
` : `${line}
|
|
9801
10605
|
`);
|
|
9802
10606
|
if (opts.dryRun) {
|
|
9803
|
-
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite:
|
|
9804
|
-
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${
|
|
10607
|
+
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path35.join(outDirAbs, taskName) }, () => {
|
|
10608
|
+
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path35.join(outDirAbs, taskName)})
|
|
9805
10609
|
`);
|
|
9806
10610
|
});
|
|
9807
10611
|
return;
|
|
@@ -9824,10 +10628,10 @@ async function runGenerateRecorded(opts) {
|
|
|
9824
10628
|
});
|
|
9825
10629
|
}
|
|
9826
10630
|
}
|
|
9827
|
-
const bundleDir =
|
|
9828
|
-
if (
|
|
10631
|
+
const bundleDir = path35.join(outDirAbs, taskName);
|
|
10632
|
+
if (existsSync29(path35.join(bundleDir, "component.json"))) {
|
|
9829
10633
|
try {
|
|
9830
|
-
const prior = readBundleManifest(
|
|
10634
|
+
const prior = readBundleManifest(readFileSync26(path35.join(bundleDir, "component.json"), "utf8")).manifest;
|
|
9831
10635
|
if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
|
|
9832
10636
|
fail(opts, ExitCode.InputValidation, {
|
|
9833
10637
|
error: `${bundleDir} already holds a bundle bound to recording set "${prior.provenance.recordingSet.path}" \u2014 generating here against a different set would silently rewrite its verification identity`,
|
|
@@ -9859,7 +10663,7 @@ async function runGenerateRecorded(opts) {
|
|
|
9859
10663
|
});
|
|
9860
10664
|
const statuses = result.finalScores.map((s) => ({
|
|
9861
10665
|
...s,
|
|
9862
|
-
status: s.similarity >= BARS4.cert.sim && s.inkRecall >= BARS4.cert.ink ? "certified" : s.pass ? "pass" : "fail"
|
|
10666
|
+
status: s.similarity >= BARS4.cert.sim && s.inkRecall >= BARS4.cert.ink && substitutedFamilies.length === 0 ? "certified" : s.pass ? "pass" : "fail"
|
|
9863
10667
|
}));
|
|
9864
10668
|
const behaviorFailures = result.finalBehaviors.filter((b) => !b.pass);
|
|
9865
10669
|
const pixelFailures = statuses.filter((s) => s.status === "fail");
|
|
@@ -9878,7 +10682,8 @@ async function runGenerateRecorded(opts) {
|
|
|
9878
10682
|
scores: result.finalScores,
|
|
9879
10683
|
behaviors: result.finalBehaviors,
|
|
9880
10684
|
environment: environmentStamp(taskFontFamilies(task.set)),
|
|
9881
|
-
spentUsd: result.spentUsd
|
|
10685
|
+
spentUsd: result.spentUsd,
|
|
10686
|
+
substitutedFamilies
|
|
9882
10687
|
});
|
|
9883
10688
|
trustStatement = emitted.manifest.trustStatement;
|
|
9884
10689
|
process.stderr.write(opts.json ? `${JSON.stringify({ status: emitted.statusLine })}
|
|
@@ -9925,6 +10730,19 @@ ${certified}/${statuses.length} certified \xB7 ${statuses.length - pixelFailures
|
|
|
9925
10730
|
`);
|
|
9926
10731
|
}
|
|
9927
10732
|
);
|
|
10733
|
+
if (opts.bar === "cert" && substitutedFamilies.length > 0) {
|
|
10734
|
+
const names = substitutedFamilies.map((f) => `"${f}"`).join(", ");
|
|
10735
|
+
if (opts.json) {
|
|
10736
|
+
process.stderr.write(`${JSON.stringify({ error: `font cache lacks ${names} \u2014 certification never measures a substitute face (scores above are pass-capped)`, code: "fonts-unproven", remediation: fontsUnprovenRemediation(task.set) })}
|
|
10737
|
+
`);
|
|
10738
|
+
} else {
|
|
10739
|
+
process.stderr.write(`error (fonts-unproven): font cache lacks ${names} \u2014 certification never measures a substitute face (scores above are pass-capped)
|
|
10740
|
+
\u2192 ${fontsUnprovenRemediation(task.set)}
|
|
10741
|
+
`);
|
|
10742
|
+
}
|
|
10743
|
+
process.exitCode = ExitCode.FontsUnproven;
|
|
10744
|
+
return;
|
|
10745
|
+
}
|
|
9928
10746
|
if (!ok) {
|
|
9929
10747
|
warn(opts, `${pixelFailures.length} config(s) and ${behaviorFailures.length} behavior(s) below the ${opts.bar} bar \u2014 bundle written, verdict honest (Q4)`);
|
|
9930
10748
|
process.exitCode = ExitCode.VerificationFailed;
|
|
@@ -9958,195 +10776,17 @@ import { CommanderError } from "commander";
|
|
|
9958
10776
|
// packages/cli/src/program.ts
|
|
9959
10777
|
init_src3();
|
|
9960
10778
|
init_environment();
|
|
10779
|
+
init_doctor();
|
|
9961
10780
|
import { Command } from "commander";
|
|
9962
10781
|
|
|
9963
|
-
// packages/cli/src/commands/doctor.ts
|
|
9964
|
-
init_src4();
|
|
9965
|
-
init_src();
|
|
9966
|
-
import { existsSync as existsSync17, readFileSync as readFileSync14, readdirSync as readdirSync3 } from "node:fs";
|
|
9967
|
-
import os4 from "node:os";
|
|
9968
|
-
import path22 from "node:path";
|
|
9969
|
-
|
|
9970
|
-
// packages/cli/src/describe.ts
|
|
9971
|
-
var COMMON_EXIT_CODES = {
|
|
9972
|
-
0: "success",
|
|
9973
|
-
1: "general error",
|
|
9974
|
-
2: "authentication error",
|
|
9975
|
-
3: "input validation error",
|
|
9976
|
-
4: "confirmation required (re-run with --yes or answer the prompt)"
|
|
9977
|
-
};
|
|
9978
|
-
function printDescription(description) {
|
|
9979
|
-
process.stdout.write(`${JSON.stringify(description, null, 2)}
|
|
9980
|
-
`);
|
|
9981
|
-
}
|
|
9982
|
-
|
|
9983
|
-
// packages/cli/src/commands/doctor.ts
|
|
9984
|
-
init_env();
|
|
9985
|
-
init_environment();
|
|
9986
|
-
init_output();
|
|
9987
|
-
init_entitlement();
|
|
9988
|
-
var DEFAULT_MCP_URL = "http://127.0.0.1:3845/mcp";
|
|
9989
|
-
var DOCTOR_DESCRIPTION = {
|
|
9990
|
-
name: "doctor",
|
|
9991
|
-
summary: "Check whether this machine can run tendril generate end to end.",
|
|
9992
|
-
args: [],
|
|
9993
|
-
flags: [
|
|
9994
|
-
{ flag: "--mcp-url <url>", description: "Figma MCP endpoint", default: DEFAULT_MCP_URL },
|
|
9995
|
-
{ flag: "--json", description: "Machine-readable output" }
|
|
9996
|
-
],
|
|
9997
|
-
output: {
|
|
9998
|
-
ok: "boolean \u2014 Figma desktop MCP reachable AND a scoring browser found (fonts resolve lazily per design system; keys are informational)",
|
|
9999
|
-
checks: "[{ name, ok, detail, remediation? }]"
|
|
10000
|
-
},
|
|
10001
|
-
exitCodes: { 0: "healthy", 1: "one or more required checks failed" },
|
|
10002
|
-
examples: ["tendril doctor", "tendril doctor --json"]
|
|
10003
|
-
};
|
|
10004
|
-
async function runDoctorChecks(options) {
|
|
10005
|
-
const checks = [];
|
|
10006
|
-
const mcpUrl = options.mcpUrl ?? DEFAULT_MCP_URL;
|
|
10007
|
-
const client = new McpHttpClient({
|
|
10008
|
-
url: mcpUrl,
|
|
10009
|
-
...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
|
|
10010
|
-
});
|
|
10011
|
-
try {
|
|
10012
|
-
const info = await client.initialize();
|
|
10013
|
-
const tools = await client.listTools();
|
|
10014
|
-
checks.push({
|
|
10015
|
-
name: "figma-desktop-mcp",
|
|
10016
|
-
ok: true,
|
|
10017
|
-
detail: `${info.name} ${info.version} (protocol ${info.protocolVersion}), ${tools.length} tools: ${tools.map((t) => t.name).join(", ")}`
|
|
10018
|
-
});
|
|
10019
|
-
} catch (err) {
|
|
10020
|
-
checks.push({
|
|
10021
|
-
name: "figma-desktop-mcp",
|
|
10022
|
-
ok: false,
|
|
10023
|
-
detail: err instanceof Error ? err.message : String(err),
|
|
10024
|
-
remediation: err instanceof McpUnavailableError ? "Open the Figma desktop app, sign in, then Figma menu \u2192 Preferences \u2192 Enable local MCP server (Dev or Full seat on a paid plan)." : "The server answered but the exchange failed \u2014 check the signed-in account's seat, then retry."
|
|
10025
|
-
});
|
|
10026
|
-
}
|
|
10027
|
-
const openrouterKey = resolveCredential("OPENROUTER_API_KEY");
|
|
10028
|
-
checks.push(
|
|
10029
|
-
openrouterKey ? { name: "openrouter-key", ok: true, detail: "OPENROUTER_API_KEY configured" } : {
|
|
10030
|
-
name: "openrouter-key",
|
|
10031
|
-
ok: false,
|
|
10032
|
-
detail: "OPENROUTER_API_KEY not set \u2014 optional; only the curated API-model engine needs it",
|
|
10033
|
-
remediation: "Run `tendril init` to store your OpenRouter key (BYOK) if you plan to use the curated engine."
|
|
10034
|
-
}
|
|
10035
|
-
);
|
|
10036
|
-
try {
|
|
10037
|
-
const chrome = resolveChrome();
|
|
10038
|
-
checks.push({ name: "browser", ok: true, detail: `${chromeVersion() ?? "version unknown"} at ${chrome}` });
|
|
10039
|
-
} catch (err) {
|
|
10040
|
-
checks.push({
|
|
10041
|
-
name: "browser",
|
|
10042
|
-
ok: false,
|
|
10043
|
-
detail: err instanceof Error ? err.message : String(err),
|
|
10044
|
-
remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
|
|
10045
|
-
});
|
|
10046
|
-
}
|
|
10047
|
-
const fontManifest = path22.join(fontCacheDir(), "manifest.json");
|
|
10048
|
-
checks.push(
|
|
10049
|
-
existsSync17(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync14(fontManifest, "utf8")).length} faces)` } : {
|
|
10050
|
-
name: "font-cache",
|
|
10051
|
-
ok: true,
|
|
10052
|
-
detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
|
|
10053
|
-
remediation: "Nothing to do now: `tendril fonts resolve --set <recording-dir>` fetches exactly what a recording declares, and generate/verify name that command when they need it."
|
|
10054
|
-
}
|
|
10055
|
-
);
|
|
10056
|
-
const pluginRoot = path22.join(os4.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
|
|
10057
|
-
if (existsSync17(pluginRoot)) {
|
|
10058
|
-
try {
|
|
10059
|
-
const versions = readdirSync3(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
|
|
10060
|
-
const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
|
|
10061
|
-
if (newest !== void 0) {
|
|
10062
|
-
const skewed = versionIsNewer(newest, cliVersion());
|
|
10063
|
-
checks.push(
|
|
10064
|
-
skewed ? {
|
|
10065
|
-
name: "plugin-skew",
|
|
10066
|
-
ok: false,
|
|
10067
|
-
detail: `Claude Code plugin cache holds v${newest} while this CLI is ${cliVersion()} \u2014 the plugin's skill/agents are STALE and will silently shadow current doctrine`,
|
|
10068
|
-
remediation: "Update the plugin \u2014 REFRESH THE MARKETPLACE FIRST (its local clone goes stale and a reinstall faithfully reinstalls the old version): terminal Claude Code \u2014 `claude plugin marketplace update tendrilapp` then `claude plugin update tendril`; VS Code extension \u2014 /plugins panel \u2192 Marketplaces tab \u2192 refresh tendrilapp, THEN Plugins tab \u2192 uninstall + reinstall tendril, reopen the chat panel. If the refresh doesn't take, remove the tendrilapp marketplace entirely and re-add TendrilApp/claude-plugin (a fresh clone cannot be stale)."
|
|
10069
|
-
} : { name: "plugin-skew", ok: true, detail: `Claude Code plugin v${newest} matches this CLI` }
|
|
10070
|
-
);
|
|
10071
|
-
}
|
|
10072
|
-
} catch {
|
|
10073
|
-
}
|
|
10074
|
-
}
|
|
10075
|
-
const ent = checkEntitlement();
|
|
10076
|
-
checks.push(
|
|
10077
|
-
ent.ok ? ent.mode === "pre-launch" ? { name: "entitlement", ok: true, detail: "pre-launch build \u2014 entitlement gates present but unarmed (no public key shipped); record/generate run freely" } : { name: "entitlement", ok: true, detail: `active plan "${ent.claims.plan}" until ${new Date(ent.claims.exp).toISOString().slice(0, 10)}${ent.stale ? " (stale \u2014 renews at next opportunity)" : ""}` } : { name: "entitlement", ok: false, detail: ent.error, remediation: ent.remediation }
|
|
10078
|
-
);
|
|
10079
|
-
const figmaToken = resolveCredential("FIGMA_TOKEN");
|
|
10080
|
-
checks.push({
|
|
10081
|
-
name: "figma-pat",
|
|
10082
|
-
ok: true,
|
|
10083
|
-
detail: figmaToken ? "FIGMA_TOKEN configured (REST fallback available)" : "FIGMA_TOKEN not set \u2014 optional; only needed for the REST fallback transport"
|
|
10084
|
-
});
|
|
10085
|
-
return { ok: checks.filter((c) => c.name !== "figma-pat" && c.name !== "openrouter-key").every((c) => c.ok), checks };
|
|
10086
|
-
}
|
|
10087
|
-
function versionIsNewer(a, b) {
|
|
10088
|
-
const pa = a.split(".").map(Number);
|
|
10089
|
-
const pb = b.split(".").map(Number);
|
|
10090
|
-
for (let i = 0; i < 3; i++) {
|
|
10091
|
-
if ((pb[i] ?? 0) > (pa[i] ?? 0)) return true;
|
|
10092
|
-
if ((pb[i] ?? 0) < (pa[i] ?? 0)) return false;
|
|
10093
|
-
}
|
|
10094
|
-
return false;
|
|
10095
|
-
}
|
|
10096
|
-
async function latestVersionInfo() {
|
|
10097
|
-
try {
|
|
10098
|
-
const ctl = new AbortController();
|
|
10099
|
-
const timer = setTimeout(() => ctl.abort(), 2500);
|
|
10100
|
-
const res = await fetch("https://registry.npmjs.org/@tendrilapp%2fcli", { signal: ctl.signal });
|
|
10101
|
-
clearTimeout(timer);
|
|
10102
|
-
if (!res.ok) return null;
|
|
10103
|
-
const doc = await res.json();
|
|
10104
|
-
const latest = doc["dist-tags"]?.latest;
|
|
10105
|
-
if (latest === void 0) return null;
|
|
10106
|
-
const stamp = doc.time?.[latest];
|
|
10107
|
-
return { latest, ...stamp !== void 0 ? { publishedAt: stamp.slice(0, 10) } : {} };
|
|
10108
|
-
} catch {
|
|
10109
|
-
return null;
|
|
10110
|
-
}
|
|
10111
|
-
}
|
|
10112
|
-
async function runDoctor(flags) {
|
|
10113
|
-
if (flags.describe) {
|
|
10114
|
-
printDescription(DOCTOR_DESCRIPTION);
|
|
10115
|
-
return;
|
|
10116
|
-
}
|
|
10117
|
-
const [report, latest] = await Promise.all([runDoctorChecks({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} }), latestVersionInfo()]);
|
|
10118
|
-
emitData(flags, { version: cliVersion(), ...latest !== null ? { latest: latest.latest, latestPublishedAt: latest.publishedAt ?? null, updateAvailable: versionIsNewer(cliVersion(), latest.latest) } : {}, ...report }, () => {
|
|
10119
|
-
const version = cliVersion();
|
|
10120
|
-
if (latest === null) {
|
|
10121
|
-
process.stdout.write(`tendril ${version} (latest: unreachable)
|
|
10122
|
-
`);
|
|
10123
|
-
} else if (!versionIsNewer(version, latest.latest)) {
|
|
10124
|
-
process.stdout.write(`tendril ${version} (${latest.latest === version ? "latest" : `ahead of registry ${latest.latest}`}${latest.publishedAt !== void 0 ? `, published ${latest.publishedAt}` : ""})
|
|
10125
|
-
`);
|
|
10126
|
-
} else {
|
|
10127
|
-
process.stdout.write(`tendril ${version} \u2014 UPDATE AVAILABLE: ${latest.latest}${latest.publishedAt !== void 0 ? ` (published ${latest.publishedAt})` : ""}
|
|
10128
|
-
`);
|
|
10129
|
-
process.stdout.write(` \u2192 npm install -g @tendrilapp/cli@latest (plugin MCP users update automatically at next session; a stale npx cache clears with npm cache clean --force)
|
|
10130
|
-
`);
|
|
10131
|
-
}
|
|
10132
|
-
for (const check of report.checks) {
|
|
10133
|
-
process.stdout.write(`${check.ok ? "\u2713" : "\u2717"} ${check.name}: ${check.detail}
|
|
10134
|
-
`);
|
|
10135
|
-
if (check.remediation) process.stdout.write(` \u2192 ${check.remediation}
|
|
10136
|
-
`);
|
|
10137
|
-
}
|
|
10138
|
-
process.stdout.write(report.ok ? "\nready to generate\n" : "\nnot ready \u2014 fix the items above\n");
|
|
10139
|
-
});
|
|
10140
|
-
if (!report.ok) process.exit(1);
|
|
10141
|
-
}
|
|
10142
|
-
|
|
10143
10782
|
// packages/cli/src/commands/init.ts
|
|
10144
10783
|
init_src3();
|
|
10784
|
+
init_describe();
|
|
10785
|
+
init_env();
|
|
10786
|
+
init_output();
|
|
10145
10787
|
import { intro, isCancel, outro, password } from "@clack/prompts";
|
|
10146
10788
|
import fs from "node:fs";
|
|
10147
10789
|
import path23 from "node:path";
|
|
10148
|
-
init_env();
|
|
10149
|
-
init_output();
|
|
10150
10790
|
var INIT_DESCRIPTION = {
|
|
10151
10791
|
name: "init",
|
|
10152
10792
|
summary: "Configure Figma and OpenRouter credentials in .env (idempotent).",
|
|
@@ -10254,11 +10894,12 @@ init_src3();
|
|
|
10254
10894
|
init_src();
|
|
10255
10895
|
init_src5();
|
|
10256
10896
|
init_src2();
|
|
10257
|
-
|
|
10258
|
-
import { readFileSync as readFileSync15, readdirSync as readdirSync4, existsSync as existsSync18 } from "node:fs";
|
|
10897
|
+
init_describe();
|
|
10259
10898
|
init_env();
|
|
10260
10899
|
init_output();
|
|
10261
10900
|
init_entitlement();
|
|
10901
|
+
import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
|
|
10902
|
+
import { readFileSync as readFileSync15, readdirSync as readdirSync4, existsSync as existsSync18 } from "node:fs";
|
|
10262
10903
|
|
|
10263
10904
|
// packages/cli/src/pipeline.ts
|
|
10264
10905
|
init_src2();
|
|
@@ -11005,7 +11646,7 @@ function buildProgram() {
|
|
|
11005
11646
|
await runActivate2({ ...flags, serviceUrl: local["serviceUrl"] });
|
|
11006
11647
|
});
|
|
11007
11648
|
const record = program.command("record").description("Agent-driven recording protocol: plan the queue, get instructions, ingest verbatim envelopes, resume from disk.");
|
|
11008
|
-
record.command("plan").requiredOption("--set <dir>", "recording set directory").requiredOption("--component <name>", "component/system name").
|
|
11649
|
+
record.command("plan").requiredOption("--set <dir>", "recording set directory").requiredOption("--component <name>", "component/system name").option("--metadata <file...>", "verbatim get_metadata envelope(s), optionally <file>@<frameId>").option("--metadata-raw-file <file>", "the get_metadata response TEXT exactly as received (single block); the CLI builds the envelope itself").option("--metadata-raw-parts-file <file>", "a JSON array of the get_metadata response blocks, each verbatim, in order (multi-block responses)").option("--default <axis=value...>", "explicit axis default, e.g. --default State=Rest (repeatable; persisted to the manifest; may re-plan an unrecorded set)").option("--component-set <name>", "when the metadata holds several component sets, record only this one (name = the set label in the plan error)").option("--sample", "cost sampling: anchor + one-factor + conflict crosses only (the FULL variant matrix is the default; sampling is blind to multi-axis interactions)").action(async (_o, cmd) => {
|
|
11009
11650
|
const flags = globalFlags(cmd.parent.parent);
|
|
11010
11651
|
const local = cmd.opts();
|
|
11011
11652
|
const { runRecordPlan: runRecordPlan2 } = await Promise.resolve().then(() => (init_record(), record_exports));
|
|
@@ -11013,8 +11654,10 @@ function buildProgram() {
|
|
|
11013
11654
|
...flags,
|
|
11014
11655
|
setDir: local["set"],
|
|
11015
11656
|
component: local["component"],
|
|
11016
|
-
metadataFiles: local["metadata"],
|
|
11017
11657
|
sample: local["sample"],
|
|
11658
|
+
...local["metadata"] !== void 0 ? { metadataFiles: local["metadata"] } : {},
|
|
11659
|
+
...local["metadataRawFile"] !== void 0 ? { metadataRawFile: local["metadataRawFile"] } : {},
|
|
11660
|
+
...local["metadataRawPartsFile"] !== void 0 ? { metadataRawPartsFile: local["metadataRawPartsFile"] } : {},
|
|
11018
11661
|
...local["default"] !== void 0 ? { defaultSpecs: local["default"] } : {},
|
|
11019
11662
|
...local["componentSet"] !== void 0 ? { componentSet: local["componentSet"] } : {}
|
|
11020
11663
|
});
|
|
@@ -11146,6 +11789,12 @@ function buildProgram() {
|
|
|
11146
11789
|
...local["out"] !== void 0 ? { out: local["out"] } : {}
|
|
11147
11790
|
});
|
|
11148
11791
|
});
|
|
11792
|
+
program.command("permissions").description("Print a paste-ready Claude Code permission allowlist for the Tendril pipeline (tendril + Figma MCP tools) \u2014 no more transcribing tool names from prompts.").option("--claude", "Claude Code settings.json format (the default and currently only format)").option("--mcp-url <url>", "Figma MCP endpoint to list live tool names from").action(async (_o, cmd) => {
|
|
11793
|
+
const flags = globalFlags(cmd.parent);
|
|
11794
|
+
const local = cmd.opts();
|
|
11795
|
+
const { runPermissions: runPermissions2 } = await Promise.resolve().then(() => (init_permissions(), permissions_exports));
|
|
11796
|
+
await runPermissions2({ ...flags, ...local["mcpUrl"] !== void 0 ? { mcpUrl: local["mcpUrl"] } : {} });
|
|
11797
|
+
});
|
|
11149
11798
|
program.command("verify").description("Recompute verification for a generated bundle against its recording set (per-config status, behaviors, honest exit codes).").argument("<bundleDir>", "bundle directory to verify").option("--task <name>", "legacy task adapter for bundles WITHOUT component.json (bundle v1 carries its own prop manifest)").option("--set <dir>", "recording set directory (overrides the bundle's provenance path)").option("--bar <bar>", "target bar: pass (0.95) or cert (0.97)", "pass").action(async (bundleDir, _opts, cmd) => {
|
|
11150
11799
|
const flags = globalFlags(cmd);
|
|
11151
11800
|
const local = cmd.opts();
|