@tendrilapp/cli 0.1.16 → 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 +1075 -268
- 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,14 +1968,15 @@ 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 = recordedMapEmpty || definedVars2 !== void 0 && definedVars2.size === 0;
|
|
1977
1978
|
const violations = result.results.flatMap(
|
|
1978
|
-
(r) => r.warnings.map((w) => ({
|
|
1979
|
+
(r) => r.warnings.filter((w) => !(mapKnownEmpty && w.rule === "scale-unlimited/declaration-strict-value")).map((w) => ({
|
|
1979
1980
|
file: fileLabel,
|
|
1980
1981
|
line: w.line,
|
|
1981
1982
|
property: w.rule,
|
|
@@ -2392,6 +2393,111 @@ function cornersMatchCanvas(ref, canvas) {
|
|
|
2392
2393
|
(c) => Math.abs(c[0] - canvas[0]) + Math.abs(c[1] - canvas[1]) + Math.abs(c[2] - canvas[2]) <= INK_DELTA
|
|
2393
2394
|
);
|
|
2394
2395
|
}
|
|
2396
|
+
function absentInkClusters(render, reference, background = [255, 255, 255]) {
|
|
2397
|
+
const a = PNG.sync.read(Buffer.from(render));
|
|
2398
|
+
const b = PNG.sync.read(Buffer.from(reference));
|
|
2399
|
+
const width = Math.max(a.width, b.width);
|
|
2400
|
+
const height = Math.max(a.height, b.height);
|
|
2401
|
+
const canvasA = onCanvas(a, width, height);
|
|
2402
|
+
const canvasB = onCanvas(b, width, height);
|
|
2403
|
+
const ring = ringModal(canvasB);
|
|
2404
|
+
const backgrounds = channelDistance(ring, background) > INK_DELTA ? [background, ring] : [background];
|
|
2405
|
+
const found = [];
|
|
2406
|
+
for (const bg of backgrounds) {
|
|
2407
|
+
const renderInk = new Uint8Array(width * height);
|
|
2408
|
+
for (let y = 0; y < height; y++) {
|
|
2409
|
+
for (let x = 0; x < width; x++) {
|
|
2410
|
+
if (isInk(canvasA.data, (width * y + x) * 4, bg)) renderInk[width * y + x] = 1;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
const candidate = new Uint8Array(width * height);
|
|
2414
|
+
for (let y = 0; y < height; y++) {
|
|
2415
|
+
for (let x = 0; x < width; x++) {
|
|
2416
|
+
if (!isInk(canvasB.data, (width * y + x) * 4, bg)) continue;
|
|
2417
|
+
let covered = false;
|
|
2418
|
+
scan: for (let dy = -ABSENT_RADIUS; dy <= ABSENT_RADIUS; dy++) {
|
|
2419
|
+
for (let dx = -ABSENT_RADIUS; dx <= ABSENT_RADIUS; dx++) {
|
|
2420
|
+
const nx = x + dx;
|
|
2421
|
+
const ny = y + dy;
|
|
2422
|
+
if (nx < 0 || ny < 0 || nx >= width || ny >= height) continue;
|
|
2423
|
+
if (renderInk[width * ny + nx] === 1) {
|
|
2424
|
+
covered = true;
|
|
2425
|
+
break scan;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
if (!covered) candidate[width * y + x] = 1;
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
const seen = new Uint8Array(width * height);
|
|
2433
|
+
for (let sy = 0; sy < height; sy++) {
|
|
2434
|
+
for (let sx = 0; sx < width; sx++) {
|
|
2435
|
+
const si = width * sy + sx;
|
|
2436
|
+
if (candidate[si] !== 1 || seen[si] === 1) continue;
|
|
2437
|
+
let x0 = sx;
|
|
2438
|
+
let y0 = sy;
|
|
2439
|
+
let x1 = sx;
|
|
2440
|
+
let y1 = sy;
|
|
2441
|
+
let px = 0;
|
|
2442
|
+
const stack = [si];
|
|
2443
|
+
seen[si] = 1;
|
|
2444
|
+
while (stack.length > 0) {
|
|
2445
|
+
const i = stack.pop();
|
|
2446
|
+
const cx = i % width;
|
|
2447
|
+
const cy = (i - cx) / width;
|
|
2448
|
+
px += 1;
|
|
2449
|
+
if (cx < x0) x0 = cx;
|
|
2450
|
+
if (cy < y0) y0 = cy;
|
|
2451
|
+
if (cx > x1) x1 = cx;
|
|
2452
|
+
if (cy > y1) y1 = cy;
|
|
2453
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
2454
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2455
|
+
const nx = cx + dx;
|
|
2456
|
+
const ny = cy + dy;
|
|
2457
|
+
if (nx < 0 || ny < 0 || nx >= width || ny >= height) continue;
|
|
2458
|
+
const ni = width * ny + nx;
|
|
2459
|
+
if (candidate[ni] === 1 && seen[ni] === 0) {
|
|
2460
|
+
seen[ni] = 1;
|
|
2461
|
+
stack.push(ni);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
if (px >= ABSENT_MIN_PX) found.push({ x: x0, y: y0, w: x1 - x0 + 1, h: y1 - y0 + 1, px, memberX: sx, memberY: sy });
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
const merged = [];
|
|
2471
|
+
for (const c of found.sort((c1, c2) => c2.px - c1.px)) {
|
|
2472
|
+
const overlaps = merged.some((m) => c.x < m.x + m.w && m.x < c.x + c.w && c.y < m.y + m.h && m.y < c.y + c.h);
|
|
2473
|
+
if (!overlaps) merged.push(c);
|
|
2474
|
+
}
|
|
2475
|
+
return merged;
|
|
2476
|
+
}
|
|
2477
|
+
function zoomCrop(png, rect, scale = 8, pad = 6) {
|
|
2478
|
+
const src = PNG.sync.read(Buffer.from(png));
|
|
2479
|
+
const x0 = Math.max(0, rect.x - pad);
|
|
2480
|
+
const y0 = Math.max(0, rect.y - pad);
|
|
2481
|
+
const x1 = Math.min(src.width, rect.x + rect.w + pad);
|
|
2482
|
+
const y1 = Math.min(src.height, rect.y + rect.h + pad);
|
|
2483
|
+
const w = Math.max(1, x1 - x0);
|
|
2484
|
+
const h = Math.max(1, y1 - y0);
|
|
2485
|
+
const out = new PNG({ width: w * scale, height: h * scale });
|
|
2486
|
+
for (let y = 0; y < h * scale; y++) {
|
|
2487
|
+
for (let x = 0; x < w * scale; x++) {
|
|
2488
|
+
const sx = x0 + Math.floor(x / scale);
|
|
2489
|
+
const sy = y0 + Math.floor(y / scale);
|
|
2490
|
+
const si = (src.width * sy + sx) * 4;
|
|
2491
|
+
const di = (out.width * y + x) * 4;
|
|
2492
|
+
const alpha = src.data[si + 3] ?? 0;
|
|
2493
|
+
out.data[di] = Math.round(((src.data[si] ?? 0) * alpha + 255 * (255 - alpha)) / 255);
|
|
2494
|
+
out.data[di + 1] = Math.round(((src.data[si + 1] ?? 0) * alpha + 255 * (255 - alpha)) / 255);
|
|
2495
|
+
out.data[di + 2] = Math.round(((src.data[si + 2] ?? 0) * alpha + 255 * (255 - alpha)) / 255);
|
|
2496
|
+
out.data[di + 3] = 255;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
return Uint8Array.from(PNG.sync.write(out));
|
|
2500
|
+
}
|
|
2395
2501
|
function measureCanvasCoupling(ref, renderA, renderB, backdrop, box) {
|
|
2396
2502
|
const rRaw = PNG.sync.read(Buffer.from(ref));
|
|
2397
2503
|
const r = onCanvas(rRaw, rRaw.width, rRaw.height);
|
|
@@ -2606,11 +2712,13 @@ function cropPng(png, x, y, width, height) {
|
|
|
2606
2712
|
}
|
|
2607
2713
|
return new Uint8Array(PNG.sync.write(out));
|
|
2608
2714
|
}
|
|
2609
|
-
var INK_DELTA;
|
|
2715
|
+
var INK_DELTA, ABSENT_RADIUS, ABSENT_MIN_PX;
|
|
2610
2716
|
var init_image_diff = __esm({
|
|
2611
2717
|
"packages/verify/src/image-diff.ts"() {
|
|
2612
2718
|
"use strict";
|
|
2613
2719
|
INK_DELTA = 30;
|
|
2720
|
+
ABSENT_RADIUS = 3;
|
|
2721
|
+
ABSENT_MIN_PX = 6;
|
|
2614
2722
|
}
|
|
2615
2723
|
});
|
|
2616
2724
|
|
|
@@ -3301,7 +3409,29 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3301
3409
|
function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3302
3410
|
const mPath = path9.join(cacheDir, "manifest.json");
|
|
3303
3411
|
const manifest = existsSync5(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3304
|
-
|
|
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) }));
|
|
3305
3435
|
}
|
|
3306
3436
|
var DEFAULT_FONT_CACHE, UA;
|
|
3307
3437
|
var init_font_resolve = __esm({
|
|
@@ -3313,11 +3443,21 @@ var init_font_resolve = __esm({
|
|
|
3313
3443
|
});
|
|
3314
3444
|
|
|
3315
3445
|
// packages/verify/src/font-faces.ts
|
|
3446
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3316
3447
|
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
3317
3448
|
import path10 from "node:path";
|
|
3318
3449
|
function fontFaceCss(manifestPath2 = path10.join(fontCacheDir(), "manifest.json")) {
|
|
3319
3450
|
if (!existsSync6(manifestPath2)) return "";
|
|
3320
|
-
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
|
+
});
|
|
3321
3461
|
const resolveFile = (f) => {
|
|
3322
3462
|
if (path10.isAbsolute(f) && existsSync6(f)) return f;
|
|
3323
3463
|
return path10.resolve(path10.dirname(manifestPath2), path10.basename(f));
|
|
@@ -4088,7 +4228,28 @@ function definedVars(tokensCss) {
|
|
|
4088
4228
|
for (const m of tokensCss.matchAll(/(--[\w-]+)\s*:/g)) names.add(m[1]);
|
|
4089
4229
|
return names;
|
|
4090
4230
|
}
|
|
4091
|
-
|
|
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) {
|
|
4092
4253
|
const findings = [];
|
|
4093
4254
|
const entryPath = path14.join(bundleDir, entry);
|
|
4094
4255
|
const cssPath = path14.join(bundleDir, "styles.css");
|
|
@@ -4108,7 +4269,8 @@ async function checkBundleQuality(bundleDir, entry) {
|
|
|
4108
4269
|
}
|
|
4109
4270
|
}
|
|
4110
4271
|
if (css !== "") {
|
|
4111
|
-
|
|
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) {
|
|
4112
4274
|
findings.push({ kind: "token-lint", file: "styles.css", line: v.line, message: v.message });
|
|
4113
4275
|
}
|
|
4114
4276
|
}
|
|
@@ -4117,6 +4279,7 @@ async function checkBundleQuality(bundleDir, entry) {
|
|
|
4117
4279
|
var init_bundle_quality = __esm({
|
|
4118
4280
|
"packages/verify/src/bundle-quality.ts"() {
|
|
4119
4281
|
"use strict";
|
|
4282
|
+
init_src();
|
|
4120
4283
|
init_runtime();
|
|
4121
4284
|
init_tsc_check();
|
|
4122
4285
|
init_token_lint();
|
|
@@ -4215,6 +4378,31 @@ function chooseSeat(cropAt, ref, padW, padH, backdrop, origin, extents) {
|
|
|
4215
4378
|
}
|
|
4216
4379
|
return { comparison, seat, scored: cropAt(origin - seat.x, origin - seat.y) };
|
|
4217
4380
|
}
|
|
4381
|
+
function deepestNodeNameAt(set, rep, px, py) {
|
|
4382
|
+
let root;
|
|
4383
|
+
try {
|
|
4384
|
+
const text = JSON.parse(readFileSync8(path15.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4385
|
+
root = parseMetadataStructure(text);
|
|
4386
|
+
} catch {
|
|
4387
|
+
return void 0;
|
|
4388
|
+
}
|
|
4389
|
+
let best;
|
|
4390
|
+
const walk2 = (node, ox, oy, isRoot) => {
|
|
4391
|
+
if (node.hidden === true) return;
|
|
4392
|
+
const nx = isRoot ? 0 : ox + (node.x ?? 0);
|
|
4393
|
+
const ny = isRoot ? 0 : oy + (node.y ?? 0);
|
|
4394
|
+
const w = node.width ?? 0;
|
|
4395
|
+
const h = node.height ?? 0;
|
|
4396
|
+
const contains = px >= nx && py >= ny && px < nx + w && py < ny + h;
|
|
4397
|
+
if (contains && !isRoot && node.name !== "") {
|
|
4398
|
+
const area = w * h;
|
|
4399
|
+
if (best === void 0 || area <= best.area) best = { name: `${node.name} (${node.id})`, area };
|
|
4400
|
+
}
|
|
4401
|
+
for (const child of node.children) walk2(child, nx, ny, false);
|
|
4402
|
+
};
|
|
4403
|
+
walk2(root, 0, 0, true);
|
|
4404
|
+
return best?.name;
|
|
4405
|
+
}
|
|
4218
4406
|
function repMeta(set, rep) {
|
|
4219
4407
|
const text = JSON.parse(readFileSync8(path15.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4220
4408
|
const root = parseMetadataStructure(text);
|
|
@@ -4335,10 +4523,18 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
|
|
|
4335
4523
|
const slab = realPadBoth && coupling.padCorners.masked >= 8 && coupling.padCorners.opaque / coupling.padCorners.masked >= 0.9;
|
|
4336
4524
|
const pass = r.similarity >= bar.sim && r.inkRecall >= bar.ink && !slab;
|
|
4337
4525
|
const region = pass ? void 0 : localizeDifference(scored, ref);
|
|
4526
|
+
const absent = absentInkClusters(scored, ref, backdrop).map((c) => {
|
|
4527
|
+
const name = deepestNodeNameAt(task.set, cfg.rep, c.memberX - seat.x, c.memberY - seat.y);
|
|
4528
|
+
return name === void 0 ? c : { ...c, name };
|
|
4529
|
+
});
|
|
4338
4530
|
if (opts.evidenceDir !== void 0) {
|
|
4339
4531
|
writeFileSync6(path15.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
|
|
4340
4532
|
writeFileSync6(path15.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
|
|
4341
4533
|
writeFileSync6(path15.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref));
|
|
4534
|
+
for (const [i, c] of absent.slice(0, 3).entries()) {
|
|
4535
|
+
writeFileSync6(path15.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
|
|
4536
|
+
writeFileSync6(path15.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
|
|
4537
|
+
}
|
|
4342
4538
|
}
|
|
4343
4539
|
return {
|
|
4344
4540
|
rep: cfg.rep,
|
|
@@ -4349,6 +4545,7 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
|
|
|
4349
4545
|
...slab ? { error: `paints the page canvas to the pad corners (${coupling.padCorners.opaque}/${coupling.padCorners.masked} opaque) \u2014 a square slab carrying the recorded canvas past the frame, wrong on any real page` } : {},
|
|
4350
4546
|
...region === void 0 ? {} : { region: { x0: region.x0, y0: region.y0, x1: region.x1, y1: region.y1, density: Math.round(region.density * 100) / 100 } },
|
|
4351
4547
|
...seat.x === 0 && seat.y === 0 ? {} : { seat },
|
|
4548
|
+
...absent.length > 0 ? { absentInk: absent } : {},
|
|
4352
4549
|
...coupling.padMasked + coupling.inBoxMasked > 0 ? { canvasCoupling: coupling } : {}
|
|
4353
4550
|
};
|
|
4354
4551
|
} finally {
|
|
@@ -5032,7 +5229,7 @@ var init_src4 = __esm({
|
|
|
5032
5229
|
// packages/cli/src/environment.ts
|
|
5033
5230
|
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
|
|
5034
5231
|
import path19 from "node:path";
|
|
5035
|
-
import { createHash as
|
|
5232
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
5036
5233
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
5037
5234
|
function cliVersion() {
|
|
5038
5235
|
try {
|
|
@@ -5049,7 +5246,7 @@ function environmentStamp(taskFamilies) {
|
|
|
5049
5246
|
const entries = JSON.parse(readFileSync11(manifestPath2, "utf8"));
|
|
5050
5247
|
const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
|
|
5051
5248
|
const faces = scoped.map((f) => `${f.family}:${f.weight}:${f.sha256}`).sort();
|
|
5052
|
-
fontsHash = faces.length === 0 ? null :
|
|
5249
|
+
fontsHash = faces.length === 0 ? null : createHash3("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
|
|
5053
5250
|
} catch {
|
|
5054
5251
|
fontsHash = null;
|
|
5055
5252
|
}
|
|
@@ -5068,6 +5265,25 @@ var init_environment = __esm({
|
|
|
5068
5265
|
}
|
|
5069
5266
|
});
|
|
5070
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
|
+
|
|
5071
5287
|
// packages/cli/src/env.ts
|
|
5072
5288
|
import { existsSync as existsSync15, readFileSync as readFileSync12 } from "node:fs";
|
|
5073
5289
|
import path20 from "node:path";
|
|
@@ -5244,6 +5460,211 @@ var init_entitlement = __esm({
|
|
|
5244
5460
|
}
|
|
5245
5461
|
});
|
|
5246
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
|
+
|
|
5247
5668
|
// packages/llm/src/model-config.ts
|
|
5248
5669
|
import { z as z6 } from "zod";
|
|
5249
5670
|
function resolveModel(config, requestedId) {
|
|
@@ -6545,7 +6966,8 @@ __export(record_exports, {
|
|
|
6545
6966
|
runRecordPlan: () => runRecordPlan,
|
|
6546
6967
|
runRecordStatus: () => runRecordStatus
|
|
6547
6968
|
});
|
|
6548
|
-
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";
|
|
6549
6971
|
import path25 from "node:path";
|
|
6550
6972
|
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
6551
6973
|
function symbolsFromMetadataEnvelope(file, sourceFrame) {
|
|
@@ -6592,19 +7014,51 @@ function runRecordPlan(opts) {
|
|
|
6592
7014
|
}
|
|
6593
7015
|
defaults[spec.slice(0, eq)] = spec.slice(eq + 1);
|
|
6594
7016
|
}
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
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) => {
|
|
6598
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) {
|
|
6599
7030
|
try {
|
|
6600
|
-
const
|
|
6601
|
-
|
|
6602
|
-
|
|
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 });
|
|
6603
7035
|
} catch (err) {
|
|
6604
7036
|
fail(opts, ExitCode.InputValidation, {
|
|
6605
|
-
error: `could not read metadata
|
|
7037
|
+
error: `could not read raw metadata ${rawFile}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
6606
7038
|
code: "bad-envelope",
|
|
6607
|
-
remediation:
|
|
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
|
+
}
|
|
7050
|
+
let symbols = [];
|
|
7051
|
+
let metadataTruncated = false;
|
|
7052
|
+
for (const { file, frame } of metadataEntries) {
|
|
7053
|
+
try {
|
|
7054
|
+
const parsed = symbolsFromMetadataEnvelope(path25.resolve(file), frame);
|
|
7055
|
+
symbols.push(...parsed.symbols);
|
|
7056
|
+
if (parsed.truncated) metadataTruncated = true;
|
|
7057
|
+
} catch (err) {
|
|
7058
|
+
fail(opts, ExitCode.InputValidation, {
|
|
7059
|
+
error: `could not read metadata envelope ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
7060
|
+
code: "bad-envelope",
|
|
7061
|
+
remediation: 'Save the get_metadata TOOL RESPONSE verbatim as JSON \u2014 shape {"content":[{"type":"text","text":"<frame \u2026>\u2026"}]} \u2014 and pass that file. Raw copied text is not an envelope.'
|
|
6608
7062
|
});
|
|
6609
7063
|
}
|
|
6610
7064
|
}
|
|
@@ -6628,8 +7082,7 @@ function runRecordPlan(opts) {
|
|
|
6628
7082
|
});
|
|
6629
7083
|
}
|
|
6630
7084
|
if (symbols.length === 0) {
|
|
6631
|
-
const leads =
|
|
6632
|
-
const [file] = spec.split("@");
|
|
7085
|
+
const leads = metadataEntries.flatMap(({ file }) => {
|
|
6633
7086
|
try {
|
|
6634
7087
|
const env = JSON.parse(readFileSync16(path25.resolve(file), "utf8"));
|
|
6635
7088
|
return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
|
|
@@ -8052,6 +8505,7 @@ OVERLAYS GO IN THE TOP LAYER, NOT ON A Z-INDEX. The prelude requires isolation:
|
|
|
8052
8505
|
|
|
8053
8506
|
PAINT & PLATFORM TRAPS (each measured on a paid run; every one passed typecheck, token lint, and all behaviour checks and was caught only by pixels or by use):
|
|
8054
8507
|
- A composite custom property resolves on the element that DECLARES it. A composite token (e.g. --ring: 0 0 0 var(--w) var(--c)) may only reference variables declared at or above its own declaration site \u2014 declared higher while its parts are set on the component, it is invalid-at-computed-value-time and the property silently computes to nothing (measured: a hover ring vanished; three configs at ~0.78).
|
|
8508
|
+
- An <svg> inside an absolutely-positioned glyph layer defaults to display: inline \u2014 it seats on a text baseline, its painted content lands BELOW the box the layout allocated, and an overflow: hidden ancestor clips the displaced ink away entirely. Large glyphs survive; small marks (a 2px dot, a 1.5px stem) vanish while every geometry probe reports the boxes correct (measured: 12/16 configs shipped icons missing their inner marks, certified at ink 0.9995). Set display: block on every glyph svg.
|
|
8055
8509
|
- Chrome pixel-snaps an <svg> root's paint offset. A fractional inset on an svg element floors to a whole pixel and moves a hairline a full pixel; express fractional placement as transform: scale() about an integer-origin frame (measured: 0.943 \u2192 1.000 on six configs).
|
|
8056
8510
|
- Pre-composite translucent values over the ACTUAL parent surface, read off the reference pixels \u2014 a control sitting on an elevated surface composites against that surface, not against the page.
|
|
8057
8511
|
- A native <dialog> carries user-agent padding: 1em. Override every side, or the root grows past its recorded box and every band inside shifts.
|
|
@@ -8321,8 +8775,8 @@ var init_adapter = __esm({
|
|
|
8321
8775
|
});
|
|
8322
8776
|
|
|
8323
8777
|
// packages/generate/src/bundle-emit.ts
|
|
8324
|
-
import { createHash as
|
|
8325
|
-
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";
|
|
8326
8780
|
import path29 from "node:path";
|
|
8327
8781
|
function pinFromConfigs(configs) {
|
|
8328
8782
|
const domains = /* @__PURE__ */ new Map();
|
|
@@ -8371,6 +8825,37 @@ function cssFontFamilies(css) {
|
|
|
8371
8825
|
}
|
|
8372
8826
|
return [...out];
|
|
8373
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
|
+
}
|
|
8374
8859
|
function countLatticeSymbols(setDir) {
|
|
8375
8860
|
const manifestFile = path29.join(setDir, "recording-set.json");
|
|
8376
8861
|
if (existsSync23(manifestFile)) {
|
|
@@ -8411,7 +8896,7 @@ function recordingSetHash(setDir, configs) {
|
|
|
8411
8896
|
relPaths,
|
|
8412
8897
|
(p) => new Uint8Array(readFileSync20(path29.join(setDir, p))),
|
|
8413
8898
|
(chunks) => {
|
|
8414
|
-
const h =
|
|
8899
|
+
const h = createHash4("sha256");
|
|
8415
8900
|
for (const c of chunks) h.update(c);
|
|
8416
8901
|
return h.digest("hex");
|
|
8417
8902
|
}
|
|
@@ -8421,7 +8906,11 @@ function statusOf(s) {
|
|
|
8421
8906
|
return tierOf(s, BARS.cert);
|
|
8422
8907
|
}
|
|
8423
8908
|
function emitBundleV1(opts) {
|
|
8424
|
-
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
|
+
});
|
|
8425
8914
|
const certified = statuses.filter((s) => s.status === "certified").length;
|
|
8426
8915
|
const pass = statuses.filter((s) => s.status !== "fail").length;
|
|
8427
8916
|
const lattice = countLatticeSymbols(opts.task.set);
|
|
@@ -8494,17 +8983,26 @@ function emitBundleV1(opts) {
|
|
|
8494
8983
|
${stripped}`);
|
|
8495
8984
|
written.push(stylesPath);
|
|
8496
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
|
+
}
|
|
8497
8994
|
const unrecorded = lattice === null ? null : Math.max(0, lattice - statuses.length);
|
|
8498
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\``;
|
|
8499
8996
|
return { manifest, statusLine, written };
|
|
8500
8997
|
}
|
|
8501
|
-
var BARS;
|
|
8998
|
+
var FONT_FORMATS, BARS;
|
|
8502
8999
|
var init_bundle_emit = __esm({
|
|
8503
9000
|
"packages/generate/src/bundle-emit.ts"() {
|
|
8504
9001
|
"use strict";
|
|
8505
9002
|
init_src6();
|
|
8506
9003
|
init_src();
|
|
8507
9004
|
init_src4();
|
|
9005
|
+
FONT_FORMATS = { ".woff2": "woff2", ".woff": "woff", ".ttf": "truetype", ".otf": "opentype" };
|
|
8508
9006
|
BARS = {
|
|
8509
9007
|
pass: { sim: 0.95, ink: 0.95 },
|
|
8510
9008
|
cert: { sim: 0.97, ink: 0.95 }
|
|
@@ -8683,6 +9181,12 @@ function taskFontFamilies(setDir) {
|
|
|
8683
9181
|
return null;
|
|
8684
9182
|
}
|
|
8685
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
|
+
}
|
|
8686
9190
|
var init_font_guidance = __esm({
|
|
8687
9191
|
"packages/cli/src/font-guidance.ts"() {
|
|
8688
9192
|
"use strict";
|
|
@@ -8847,6 +9351,12 @@ async function runVerify(opts) {
|
|
|
8847
9351
|
remediation: fontsUnprovenRemediation(task.set)
|
|
8848
9352
|
});
|
|
8849
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
|
+
}
|
|
8850
9360
|
const missing = task.configs.filter(
|
|
8851
9361
|
(c) => !existsSync25(path31.join(task.set, c.rep, "get_screenshot.json")) || !existsSync25(path31.join(task.set, c.rep, "get_metadata.json"))
|
|
8852
9362
|
);
|
|
@@ -8860,7 +9370,7 @@ async function runVerify(opts) {
|
|
|
8860
9370
|
const bar = BARS2[opts.bar];
|
|
8861
9371
|
const evidenceDir = path31.join(opts.bundleDir, "verify-evidence");
|
|
8862
9372
|
const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
8863
|
-
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) });
|
|
8864
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");
|
|
8865
9375
|
const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
|
|
8866
9376
|
const parity = await checkHoverParity(task, opts.bundleDir);
|
|
@@ -8880,9 +9390,19 @@ async function runVerify(opts) {
|
|
|
8880
9390
|
const statuses = [
|
|
8881
9391
|
...scores.map((s) => {
|
|
8882
9392
|
const { exact: _exact, ...reported } = s;
|
|
8883
|
-
|
|
9393
|
+
let status = tierOf(s, BARS2.cert);
|
|
9394
|
+
const certDemote = [];
|
|
9395
|
+
if (status === "certified" && s.absentInk !== void 0 && s.absentInk.length > 0) {
|
|
9396
|
+
status = "pass";
|
|
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)`));
|
|
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
|
+
}
|
|
9403
|
+
const base = certDemote.length > 0 ? { ...reported, status, demotedBy: certDemote } : { ...reported, status };
|
|
8884
9404
|
const reasons = demotions.get(s.rep);
|
|
8885
|
-
return reasons === void 0 ? base : { ...base, status: "fail", demotedBy: reasons };
|
|
9405
|
+
return reasons === void 0 ? base : { ...base, status: "fail", demotedBy: [...certDemote.length > 0 ? certDemote : [], ...reasons] };
|
|
8886
9406
|
}),
|
|
8887
9407
|
// ADR-010 §2 anti-gaming: recorded configs the adapter does not map
|
|
8888
9408
|
// are FAILs, never silently absent.
|
|
@@ -9042,9 +9562,27 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
9042
9562
|
}
|
|
9043
9563
|
process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
|
|
9044
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
|
+
}
|
|
9045
9570
|
process.stdout.write(`evidence: ${evidenceDir} (render/ref/diff per config)
|
|
9046
9571
|
`);
|
|
9047
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
|
+
}
|
|
9048
9586
|
if (!ok) {
|
|
9049
9587
|
warn(
|
|
9050
9588
|
opts,
|
|
@@ -9078,18 +9616,18 @@ __export(engine_exports, {
|
|
|
9078
9616
|
runEngineBrief: () => runEngineBrief,
|
|
9079
9617
|
runEngineScore: () => runEngineScore
|
|
9080
9618
|
});
|
|
9081
|
-
import { existsSync as existsSync26, mkdirSync as
|
|
9619
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync8, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
|
|
9082
9620
|
import path32 from "node:path";
|
|
9083
9621
|
function resolveEngineTask(opts, callerCwd) {
|
|
9084
9622
|
const asPath = path32.resolve(callerCwd, opts.taskOrSet);
|
|
9085
9623
|
const isSet = existsSync26(path32.join(asPath, "recording-set.json"));
|
|
9086
9624
|
const registry = TASKS[opts.taskOrSet];
|
|
9087
|
-
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet };
|
|
9625
|
+
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, disclosures: [] };
|
|
9088
9626
|
if (isSet) {
|
|
9089
9627
|
try {
|
|
9090
9628
|
const authored = authorTaskFromSet(asPath);
|
|
9091
9629
|
for (const d of authored.disclosures) warn(opts, d);
|
|
9092
|
-
return { task: authored.task, name: path32.basename(asPath), apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
|
|
9630
|
+
return { task: authored.task, name: path32.basename(asPath), disclosures: authored.disclosures, apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
|
|
9093
9631
|
} catch (err) {
|
|
9094
9632
|
fail(opts, ExitCode.InputValidation, {
|
|
9095
9633
|
error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -9107,9 +9645,13 @@ function resolveEngineTask(opts, callerCwd) {
|
|
|
9107
9645
|
function runEngineBrief(opts) {
|
|
9108
9646
|
requireEntitlement(opts);
|
|
9109
9647
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
9110
|
-
const { task, name } = resolveEngineTask(opts, callerCwd);
|
|
9648
|
+
const { task, name, disclosures } = resolveEngineTask(opts, callerCwd);
|
|
9111
9649
|
const bar = BARS3[opts.bar];
|
|
9112
|
-
const
|
|
9650
|
+
const disclosureBlock = disclosures.length > 0 ? `
|
|
9651
|
+
|
|
9652
|
+
=== RECORDED-SET DISCLOSURES (facts about this task's coverage \u2014 read them) ===
|
|
9653
|
+
${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
9654
|
+
const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock;
|
|
9113
9655
|
const segments = buildSegments(task, "files");
|
|
9114
9656
|
let notRecorded;
|
|
9115
9657
|
const manifestPath2 = path32.join(task.set, "recording-set.json");
|
|
@@ -9123,7 +9665,7 @@ DO NOT INVENT PIXELS FOR THESE POSES. Implement them ONLY as the composition of
|
|
|
9123
9665
|
${notRecorded}` : "";
|
|
9124
9666
|
let fontProvisioning;
|
|
9125
9667
|
if (existsSync26(manifestPath2)) {
|
|
9126
|
-
const provided =
|
|
9668
|
+
const provided = verifiedFontFamilies().map((f) => f.family);
|
|
9127
9669
|
const unprovided = recordedFontFamilies(task.set).filter((r) => !provided.some((p) => p.toLowerCase() === r.toLowerCase()));
|
|
9128
9670
|
if (unprovided.length > 0) {
|
|
9129
9671
|
const names = unprovided.map((f) => `'${f}'`).join(", ");
|
|
@@ -9133,10 +9675,10 @@ ${notRecorded}` : "";
|
|
|
9133
9675
|
prompt: `This design's text uses ${names}, which is not in the local font kit. How should it be handled?`,
|
|
9134
9676
|
options: [
|
|
9135
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.`,
|
|
9136
|
-
|
|
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.'
|
|
9137
9679
|
]
|
|
9138
9680
|
},
|
|
9139
|
-
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."
|
|
9140
9682
|
};
|
|
9141
9683
|
}
|
|
9142
9684
|
}
|
|
@@ -9145,7 +9687,7 @@ ${notRecorded}` : "";
|
|
|
9145
9687
|
=== TASK PAYLOAD (recorded truth, verbatim) ===
|
|
9146
9688
|
${segments}`;
|
|
9147
9689
|
const payloadFile = path32.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
|
|
9148
|
-
|
|
9690
|
+
mkdirSync8(path32.dirname(payloadFile), { recursive: true });
|
|
9149
9691
|
writeFileSync12(payloadFile, payload);
|
|
9150
9692
|
emitData(
|
|
9151
9693
|
opts,
|
|
@@ -9206,6 +9748,10 @@ async function runEngineScore(opts) {
|
|
|
9206
9748
|
remediation: fontsUnprovenRemediation(task.set)
|
|
9207
9749
|
});
|
|
9208
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
|
+
}
|
|
9209
9755
|
if (opts.rebind !== true && existsSync26(path32.join(candidateDir, "component.json"))) {
|
|
9210
9756
|
const prior = (() => {
|
|
9211
9757
|
try {
|
|
@@ -9238,7 +9784,7 @@ async function runEngineScore(opts) {
|
|
|
9238
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)";
|
|
9239
9785
|
const obj = objective(scores, behaviors);
|
|
9240
9786
|
const total = scores.length + behaviors.length;
|
|
9241
|
-
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) });
|
|
9242
9788
|
const qualityFeedback = quality.findings.length === 0 && !quality.tokensAbsent ? "" : `
|
|
9243
9789
|
|
|
9244
9790
|
QUALITY (does not affect the bar \u2014 fix alongside the failing configs):
|
|
@@ -9249,9 +9795,14 @@ ${[
|
|
|
9249
9795
|
const allPass = obj[0] === total && total > 0;
|
|
9250
9796
|
const certBar = BARS3["cert"];
|
|
9251
9797
|
const parityDemoted = new Set(parity.filter((p) => !p.pass).map((p) => p.id.replace(/^parity:/, "")));
|
|
9252
|
-
const certifiedReps = scores.filter((sc) => tierOf(sc, certBar) === "certified" && !parityDemoted.has(sc.rep)).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);
|
|
9253
9799
|
const certifiedSet = new Set(certifiedReps);
|
|
9254
|
-
const
|
|
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.`));
|
|
9801
|
+
const absentBlock = absentFindings.length > 0 ? `
|
|
9802
|
+
|
|
9803
|
+
MISSING FEATURES (absent-ink clusters \u2014 recorded ink your render leaves nowhere near covered; fix these first, the global numbers cannot see them):
|
|
9804
|
+
${absentFindings.join("\n")}` : "";
|
|
9805
|
+
const certificationFeedback = `${absentBlock}
|
|
9255
9806
|
|
|
9256
9807
|
CERTIFICATION: ${certifiedReps.length}/${scores.length} configs at the certification bar (sim \u2265${certBar.sim} AND ink \u2265${certBar.ink}, exact values, after parity demotion \u2014 composition checks at verify can demote further).${certifiedReps.length < scores.length ? ` Below cert: ${scores.filter((sc) => !certifiedSet.has(sc.rep)).map((sc) => sc.rep).join(", ")}.` : ""}
|
|
9257
9808
|
METRIC DEADBAND (read before iterating on near-misses): the scored similarity/ink deliberately tolerate \xB11px edge shift and antialiased-edge differences \u2014 cross-rasterizer noise absorption. A change entirely inside that band moves these numbers by EXACTLY ZERO (working as designed, not a stuck scorer). The per-config \`exact\` fields in the JSON are tolerance-free and move first: compare exact across rounds to confirm a small fix landed, and stop iterating when only exact moves \u2014 the bar reads the tolerant numbers.`;
|
|
@@ -9265,7 +9816,8 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
9265
9816
|
model: `${opts.model} (self-reported${opts.host !== void 0 ? ` via ${opts.host}` : ""})`,
|
|
9266
9817
|
scores,
|
|
9267
9818
|
behaviors,
|
|
9268
|
-
environment: environmentStamp(taskFontFamilies(task.set))
|
|
9819
|
+
environment: environmentStamp(taskFontFamilies(task.set)),
|
|
9820
|
+
substitutedFamilies
|
|
9269
9821
|
});
|
|
9270
9822
|
emitData(
|
|
9271
9823
|
opts,
|
|
@@ -9286,7 +9838,7 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
9286
9838
|
bundleManifest: emitted.written[0],
|
|
9287
9839
|
note: "styles.css was stamped with the bundle provenance comment on line 1 \u2014 preserve it in any post-score edit",
|
|
9288
9840
|
allPass,
|
|
9289
|
-
certification: { certified: certifiedReps.length, total: scores.length, bar: certBar, note: "tierOf on exact values + parity demotion; verify's composition checks can demote further" }
|
|
9841
|
+
certification: { certified: certifiedReps.length, total: scores.length, bar: certBar, note: "tierOf on exact values + parity and absent-ink demotion; verify's composition checks can demote further" }
|
|
9290
9842
|
},
|
|
9291
9843
|
() => {
|
|
9292
9844
|
for (const s of scores) process.stdout.write(`${s.pass ? certifiedSet.has(s.rep) ? "CERT" : "PASS" : "FAIL"} ${s.rep}: sim=${s.similarity} ink=${s.inkRecall}${s.error !== void 0 ? ` [${s.error}]` : ""}
|
|
@@ -9303,6 +9855,19 @@ ${obj[0]}/${total} \xB7 certified ${certifiedReps.length}/${total} \xB7 floor=${
|
|
|
9303
9855
|
}
|
|
9304
9856
|
}
|
|
9305
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
|
+
}
|
|
9306
9871
|
if (!allPass) process.exitCode = ExitCode.VerificationFailed;
|
|
9307
9872
|
}
|
|
9308
9873
|
var BARS3;
|
|
@@ -9502,6 +10067,399 @@ var init_codeconnect = __esm({
|
|
|
9502
10067
|
}
|
|
9503
10068
|
});
|
|
9504
10069
|
|
|
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
|
+
|
|
9505
10463
|
// packages/cli/src/commands/generate-route.ts
|
|
9506
10464
|
var generate_route_exports = {};
|
|
9507
10465
|
__export(generate_route_exports, {
|
|
@@ -9526,17 +10484,17 @@ __export(generate_recorded_exports, {
|
|
|
9526
10484
|
runGenerateRecorded: () => runGenerateRecorded
|
|
9527
10485
|
});
|
|
9528
10486
|
import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
|
|
9529
|
-
import { existsSync as
|
|
9530
|
-
import
|
|
10487
|
+
import { existsSync as existsSync29, readFileSync as readFileSync26 } from "node:fs";
|
|
10488
|
+
import path35 from "node:path";
|
|
9531
10489
|
async function runGenerateRecorded(opts) {
|
|
9532
10490
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
9533
|
-
const outDirAbs =
|
|
9534
|
-
const recordedAsPath =
|
|
10491
|
+
const outDirAbs = path35.resolve(callerCwd, opts.out);
|
|
10492
|
+
const recordedAsPath = path35.resolve(callerCwd, opts.recorded);
|
|
9535
10493
|
let task;
|
|
9536
10494
|
let taskName;
|
|
9537
10495
|
let authoredApi;
|
|
9538
10496
|
let composition;
|
|
9539
|
-
const isSet =
|
|
10497
|
+
const isSet = existsSync29(path35.join(recordedAsPath, "recording-set.json"));
|
|
9540
10498
|
const registry = TASKS[opts.recorded];
|
|
9541
10499
|
if (registry !== void 0 && !isSet) {
|
|
9542
10500
|
task = registry;
|
|
@@ -9545,7 +10503,7 @@ async function runGenerateRecorded(opts) {
|
|
|
9545
10503
|
try {
|
|
9546
10504
|
const authored = authorTaskFromSet(recordedAsPath);
|
|
9547
10505
|
task = authored.task;
|
|
9548
|
-
taskName =
|
|
10506
|
+
taskName = path35.basename(recordedAsPath);
|
|
9549
10507
|
authoredApi = authored.api;
|
|
9550
10508
|
const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
|
|
9551
10509
|
if (roles.success) composition = roles.data;
|
|
@@ -9571,8 +10529,12 @@ async function runGenerateRecorded(opts) {
|
|
|
9571
10529
|
remediation: fontsUnprovenRemediation(task.set)
|
|
9572
10530
|
});
|
|
9573
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
|
+
}
|
|
9574
10536
|
const missing = task.configs.filter(
|
|
9575
|
-
(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"))
|
|
9576
10538
|
);
|
|
9577
10539
|
if (missing.length > 0) {
|
|
9578
10540
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -9642,8 +10604,8 @@ async function runGenerateRecorded(opts) {
|
|
|
9642
10604
|
` : `${line}
|
|
9643
10605
|
`);
|
|
9644
10606
|
if (opts.dryRun) {
|
|
9645
|
-
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite:
|
|
9646
|
-
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)})
|
|
9647
10609
|
`);
|
|
9648
10610
|
});
|
|
9649
10611
|
return;
|
|
@@ -9666,10 +10628,10 @@ async function runGenerateRecorded(opts) {
|
|
|
9666
10628
|
});
|
|
9667
10629
|
}
|
|
9668
10630
|
}
|
|
9669
|
-
const bundleDir =
|
|
9670
|
-
if (
|
|
10631
|
+
const bundleDir = path35.join(outDirAbs, taskName);
|
|
10632
|
+
if (existsSync29(path35.join(bundleDir, "component.json"))) {
|
|
9671
10633
|
try {
|
|
9672
|
-
const prior = readBundleManifest(
|
|
10634
|
+
const prior = readBundleManifest(readFileSync26(path35.join(bundleDir, "component.json"), "utf8")).manifest;
|
|
9673
10635
|
if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
|
|
9674
10636
|
fail(opts, ExitCode.InputValidation, {
|
|
9675
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`,
|
|
@@ -9701,7 +10663,7 @@ async function runGenerateRecorded(opts) {
|
|
|
9701
10663
|
});
|
|
9702
10664
|
const statuses = result.finalScores.map((s) => ({
|
|
9703
10665
|
...s,
|
|
9704
|
-
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"
|
|
9705
10667
|
}));
|
|
9706
10668
|
const behaviorFailures = result.finalBehaviors.filter((b) => !b.pass);
|
|
9707
10669
|
const pixelFailures = statuses.filter((s) => s.status === "fail");
|
|
@@ -9720,7 +10682,8 @@ async function runGenerateRecorded(opts) {
|
|
|
9720
10682
|
scores: result.finalScores,
|
|
9721
10683
|
behaviors: result.finalBehaviors,
|
|
9722
10684
|
environment: environmentStamp(taskFontFamilies(task.set)),
|
|
9723
|
-
spentUsd: result.spentUsd
|
|
10685
|
+
spentUsd: result.spentUsd,
|
|
10686
|
+
substitutedFamilies
|
|
9724
10687
|
});
|
|
9725
10688
|
trustStatement = emitted.manifest.trustStatement;
|
|
9726
10689
|
process.stderr.write(opts.json ? `${JSON.stringify({ status: emitted.statusLine })}
|
|
@@ -9767,6 +10730,19 @@ ${certified}/${statuses.length} certified \xB7 ${statuses.length - pixelFailures
|
|
|
9767
10730
|
`);
|
|
9768
10731
|
}
|
|
9769
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
|
+
}
|
|
9770
10746
|
if (!ok) {
|
|
9771
10747
|
warn(opts, `${pixelFailures.length} config(s) and ${behaviorFailures.length} behavior(s) below the ${opts.bar} bar \u2014 bundle written, verdict honest (Q4)`);
|
|
9772
10748
|
process.exitCode = ExitCode.VerificationFailed;
|
|
@@ -9800,195 +10776,17 @@ import { CommanderError } from "commander";
|
|
|
9800
10776
|
// packages/cli/src/program.ts
|
|
9801
10777
|
init_src3();
|
|
9802
10778
|
init_environment();
|
|
10779
|
+
init_doctor();
|
|
9803
10780
|
import { Command } from "commander";
|
|
9804
10781
|
|
|
9805
|
-
// packages/cli/src/commands/doctor.ts
|
|
9806
|
-
init_src4();
|
|
9807
|
-
init_src();
|
|
9808
|
-
import { existsSync as existsSync17, readFileSync as readFileSync14, readdirSync as readdirSync3 } from "node:fs";
|
|
9809
|
-
import os4 from "node:os";
|
|
9810
|
-
import path22 from "node:path";
|
|
9811
|
-
|
|
9812
|
-
// packages/cli/src/describe.ts
|
|
9813
|
-
var COMMON_EXIT_CODES = {
|
|
9814
|
-
0: "success",
|
|
9815
|
-
1: "general error",
|
|
9816
|
-
2: "authentication error",
|
|
9817
|
-
3: "input validation error",
|
|
9818
|
-
4: "confirmation required (re-run with --yes or answer the prompt)"
|
|
9819
|
-
};
|
|
9820
|
-
function printDescription(description) {
|
|
9821
|
-
process.stdout.write(`${JSON.stringify(description, null, 2)}
|
|
9822
|
-
`);
|
|
9823
|
-
}
|
|
9824
|
-
|
|
9825
|
-
// packages/cli/src/commands/doctor.ts
|
|
9826
|
-
init_env();
|
|
9827
|
-
init_environment();
|
|
9828
|
-
init_output();
|
|
9829
|
-
init_entitlement();
|
|
9830
|
-
var DEFAULT_MCP_URL = "http://127.0.0.1:3845/mcp";
|
|
9831
|
-
var DOCTOR_DESCRIPTION = {
|
|
9832
|
-
name: "doctor",
|
|
9833
|
-
summary: "Check whether this machine can run tendril generate end to end.",
|
|
9834
|
-
args: [],
|
|
9835
|
-
flags: [
|
|
9836
|
-
{ flag: "--mcp-url <url>", description: "Figma MCP endpoint", default: DEFAULT_MCP_URL },
|
|
9837
|
-
{ flag: "--json", description: "Machine-readable output" }
|
|
9838
|
-
],
|
|
9839
|
-
output: {
|
|
9840
|
-
ok: "boolean \u2014 Figma desktop MCP reachable AND a scoring browser found (fonts resolve lazily per design system; keys are informational)",
|
|
9841
|
-
checks: "[{ name, ok, detail, remediation? }]"
|
|
9842
|
-
},
|
|
9843
|
-
exitCodes: { 0: "healthy", 1: "one or more required checks failed" },
|
|
9844
|
-
examples: ["tendril doctor", "tendril doctor --json"]
|
|
9845
|
-
};
|
|
9846
|
-
async function runDoctorChecks(options) {
|
|
9847
|
-
const checks = [];
|
|
9848
|
-
const mcpUrl = options.mcpUrl ?? DEFAULT_MCP_URL;
|
|
9849
|
-
const client = new McpHttpClient({
|
|
9850
|
-
url: mcpUrl,
|
|
9851
|
-
...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
|
|
9852
|
-
});
|
|
9853
|
-
try {
|
|
9854
|
-
const info = await client.initialize();
|
|
9855
|
-
const tools = await client.listTools();
|
|
9856
|
-
checks.push({
|
|
9857
|
-
name: "figma-desktop-mcp",
|
|
9858
|
-
ok: true,
|
|
9859
|
-
detail: `${info.name} ${info.version} (protocol ${info.protocolVersion}), ${tools.length} tools: ${tools.map((t) => t.name).join(", ")}`
|
|
9860
|
-
});
|
|
9861
|
-
} catch (err) {
|
|
9862
|
-
checks.push({
|
|
9863
|
-
name: "figma-desktop-mcp",
|
|
9864
|
-
ok: false,
|
|
9865
|
-
detail: err instanceof Error ? err.message : String(err),
|
|
9866
|
-
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."
|
|
9867
|
-
});
|
|
9868
|
-
}
|
|
9869
|
-
const openrouterKey = resolveCredential("OPENROUTER_API_KEY");
|
|
9870
|
-
checks.push(
|
|
9871
|
-
openrouterKey ? { name: "openrouter-key", ok: true, detail: "OPENROUTER_API_KEY configured" } : {
|
|
9872
|
-
name: "openrouter-key",
|
|
9873
|
-
ok: false,
|
|
9874
|
-
detail: "OPENROUTER_API_KEY not set \u2014 optional; only the curated API-model engine needs it",
|
|
9875
|
-
remediation: "Run `tendril init` to store your OpenRouter key (BYOK) if you plan to use the curated engine."
|
|
9876
|
-
}
|
|
9877
|
-
);
|
|
9878
|
-
try {
|
|
9879
|
-
const chrome = resolveChrome();
|
|
9880
|
-
checks.push({ name: "browser", ok: true, detail: `${chromeVersion() ?? "version unknown"} at ${chrome}` });
|
|
9881
|
-
} catch (err) {
|
|
9882
|
-
checks.push({
|
|
9883
|
-
name: "browser",
|
|
9884
|
-
ok: false,
|
|
9885
|
-
detail: err instanceof Error ? err.message : String(err),
|
|
9886
|
-
remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
|
|
9887
|
-
});
|
|
9888
|
-
}
|
|
9889
|
-
const fontManifest = path22.join(fontCacheDir(), "manifest.json");
|
|
9890
|
-
checks.push(
|
|
9891
|
-
existsSync17(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync14(fontManifest, "utf8")).length} faces)` } : {
|
|
9892
|
-
name: "font-cache",
|
|
9893
|
-
ok: true,
|
|
9894
|
-
detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
|
|
9895
|
-
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."
|
|
9896
|
-
}
|
|
9897
|
-
);
|
|
9898
|
-
const pluginRoot = path22.join(os4.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
|
|
9899
|
-
if (existsSync17(pluginRoot)) {
|
|
9900
|
-
try {
|
|
9901
|
-
const versions = readdirSync3(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
|
|
9902
|
-
const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
|
|
9903
|
-
if (newest !== void 0) {
|
|
9904
|
-
const skewed = versionIsNewer(newest, cliVersion());
|
|
9905
|
-
checks.push(
|
|
9906
|
-
skewed ? {
|
|
9907
|
-
name: "plugin-skew",
|
|
9908
|
-
ok: false,
|
|
9909
|
-
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`,
|
|
9910
|
-
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)."
|
|
9911
|
-
} : { name: "plugin-skew", ok: true, detail: `Claude Code plugin v${newest} matches this CLI` }
|
|
9912
|
-
);
|
|
9913
|
-
}
|
|
9914
|
-
} catch {
|
|
9915
|
-
}
|
|
9916
|
-
}
|
|
9917
|
-
const ent = checkEntitlement();
|
|
9918
|
-
checks.push(
|
|
9919
|
-
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 }
|
|
9920
|
-
);
|
|
9921
|
-
const figmaToken = resolveCredential("FIGMA_TOKEN");
|
|
9922
|
-
checks.push({
|
|
9923
|
-
name: "figma-pat",
|
|
9924
|
-
ok: true,
|
|
9925
|
-
detail: figmaToken ? "FIGMA_TOKEN configured (REST fallback available)" : "FIGMA_TOKEN not set \u2014 optional; only needed for the REST fallback transport"
|
|
9926
|
-
});
|
|
9927
|
-
return { ok: checks.filter((c) => c.name !== "figma-pat" && c.name !== "openrouter-key").every((c) => c.ok), checks };
|
|
9928
|
-
}
|
|
9929
|
-
function versionIsNewer(a, b) {
|
|
9930
|
-
const pa = a.split(".").map(Number);
|
|
9931
|
-
const pb = b.split(".").map(Number);
|
|
9932
|
-
for (let i = 0; i < 3; i++) {
|
|
9933
|
-
if ((pb[i] ?? 0) > (pa[i] ?? 0)) return true;
|
|
9934
|
-
if ((pb[i] ?? 0) < (pa[i] ?? 0)) return false;
|
|
9935
|
-
}
|
|
9936
|
-
return false;
|
|
9937
|
-
}
|
|
9938
|
-
async function latestVersionInfo() {
|
|
9939
|
-
try {
|
|
9940
|
-
const ctl = new AbortController();
|
|
9941
|
-
const timer = setTimeout(() => ctl.abort(), 2500);
|
|
9942
|
-
const res = await fetch("https://registry.npmjs.org/@tendrilapp%2fcli", { signal: ctl.signal });
|
|
9943
|
-
clearTimeout(timer);
|
|
9944
|
-
if (!res.ok) return null;
|
|
9945
|
-
const doc = await res.json();
|
|
9946
|
-
const latest = doc["dist-tags"]?.latest;
|
|
9947
|
-
if (latest === void 0) return null;
|
|
9948
|
-
const stamp = doc.time?.[latest];
|
|
9949
|
-
return { latest, ...stamp !== void 0 ? { publishedAt: stamp.slice(0, 10) } : {} };
|
|
9950
|
-
} catch {
|
|
9951
|
-
return null;
|
|
9952
|
-
}
|
|
9953
|
-
}
|
|
9954
|
-
async function runDoctor(flags) {
|
|
9955
|
-
if (flags.describe) {
|
|
9956
|
-
printDescription(DOCTOR_DESCRIPTION);
|
|
9957
|
-
return;
|
|
9958
|
-
}
|
|
9959
|
-
const [report, latest] = await Promise.all([runDoctorChecks({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} }), latestVersionInfo()]);
|
|
9960
|
-
emitData(flags, { version: cliVersion(), ...latest !== null ? { latest: latest.latest, latestPublishedAt: latest.publishedAt ?? null, updateAvailable: versionIsNewer(cliVersion(), latest.latest) } : {}, ...report }, () => {
|
|
9961
|
-
const version = cliVersion();
|
|
9962
|
-
if (latest === null) {
|
|
9963
|
-
process.stdout.write(`tendril ${version} (latest: unreachable)
|
|
9964
|
-
`);
|
|
9965
|
-
} else if (!versionIsNewer(version, latest.latest)) {
|
|
9966
|
-
process.stdout.write(`tendril ${version} (${latest.latest === version ? "latest" : `ahead of registry ${latest.latest}`}${latest.publishedAt !== void 0 ? `, published ${latest.publishedAt}` : ""})
|
|
9967
|
-
`);
|
|
9968
|
-
} else {
|
|
9969
|
-
process.stdout.write(`tendril ${version} \u2014 UPDATE AVAILABLE: ${latest.latest}${latest.publishedAt !== void 0 ? ` (published ${latest.publishedAt})` : ""}
|
|
9970
|
-
`);
|
|
9971
|
-
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)
|
|
9972
|
-
`);
|
|
9973
|
-
}
|
|
9974
|
-
for (const check of report.checks) {
|
|
9975
|
-
process.stdout.write(`${check.ok ? "\u2713" : "\u2717"} ${check.name}: ${check.detail}
|
|
9976
|
-
`);
|
|
9977
|
-
if (check.remediation) process.stdout.write(` \u2192 ${check.remediation}
|
|
9978
|
-
`);
|
|
9979
|
-
}
|
|
9980
|
-
process.stdout.write(report.ok ? "\nready to generate\n" : "\nnot ready \u2014 fix the items above\n");
|
|
9981
|
-
});
|
|
9982
|
-
if (!report.ok) process.exit(1);
|
|
9983
|
-
}
|
|
9984
|
-
|
|
9985
10782
|
// packages/cli/src/commands/init.ts
|
|
9986
10783
|
init_src3();
|
|
10784
|
+
init_describe();
|
|
10785
|
+
init_env();
|
|
10786
|
+
init_output();
|
|
9987
10787
|
import { intro, isCancel, outro, password } from "@clack/prompts";
|
|
9988
10788
|
import fs from "node:fs";
|
|
9989
10789
|
import path23 from "node:path";
|
|
9990
|
-
init_env();
|
|
9991
|
-
init_output();
|
|
9992
10790
|
var INIT_DESCRIPTION = {
|
|
9993
10791
|
name: "init",
|
|
9994
10792
|
summary: "Configure Figma and OpenRouter credentials in .env (idempotent).",
|
|
@@ -10096,11 +10894,12 @@ init_src3();
|
|
|
10096
10894
|
init_src();
|
|
10097
10895
|
init_src5();
|
|
10098
10896
|
init_src2();
|
|
10099
|
-
|
|
10100
|
-
import { readFileSync as readFileSync15, readdirSync as readdirSync4, existsSync as existsSync18 } from "node:fs";
|
|
10897
|
+
init_describe();
|
|
10101
10898
|
init_env();
|
|
10102
10899
|
init_output();
|
|
10103
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";
|
|
10104
10903
|
|
|
10105
10904
|
// packages/cli/src/pipeline.ts
|
|
10106
10905
|
init_src2();
|
|
@@ -10847,7 +11646,7 @@ function buildProgram() {
|
|
|
10847
11646
|
await runActivate2({ ...flags, serviceUrl: local["serviceUrl"] });
|
|
10848
11647
|
});
|
|
10849
11648
|
const record = program.command("record").description("Agent-driven recording protocol: plan the queue, get instructions, ingest verbatim envelopes, resume from disk.");
|
|
10850
|
-
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) => {
|
|
10851
11650
|
const flags = globalFlags(cmd.parent.parent);
|
|
10852
11651
|
const local = cmd.opts();
|
|
10853
11652
|
const { runRecordPlan: runRecordPlan2 } = await Promise.resolve().then(() => (init_record(), record_exports));
|
|
@@ -10855,8 +11654,10 @@ function buildProgram() {
|
|
|
10855
11654
|
...flags,
|
|
10856
11655
|
setDir: local["set"],
|
|
10857
11656
|
component: local["component"],
|
|
10858
|
-
metadataFiles: local["metadata"],
|
|
10859
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"] } : {},
|
|
10860
11661
|
...local["default"] !== void 0 ? { defaultSpecs: local["default"] } : {},
|
|
10861
11662
|
...local["componentSet"] !== void 0 ? { componentSet: local["componentSet"] } : {}
|
|
10862
11663
|
});
|
|
@@ -10988,6 +11789,12 @@ function buildProgram() {
|
|
|
10988
11789
|
...local["out"] !== void 0 ? { out: local["out"] } : {}
|
|
10989
11790
|
});
|
|
10990
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
|
+
});
|
|
10991
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) => {
|
|
10992
11799
|
const flags = globalFlags(cmd);
|
|
10993
11800
|
const local = cmd.opts();
|