@tendrilapp/cli 0.1.14 → 0.1.16
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 +29 -10
- package/dist/tendril-mcp.js +2 -2
- package/dist/tendril.js +538 -236
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -996,7 +996,7 @@ function sessionStatus(setDir) {
|
|
|
996
996
|
const manifest = loadManifest(setDir);
|
|
997
997
|
const reps = manifest.reps.map((rep) => {
|
|
998
998
|
const recorded = RECORD_TOOLS.filter((t) => existsSync(path.join(setDir, rep.slug, `${t}.json`)));
|
|
999
|
-
const missing = requiredToolsFor(manifest, rep.slug).filter((t) => !recorded.includes(t));
|
|
999
|
+
const missing = byProtocolOrder(requiredToolsFor(manifest, rep.slug).filter((t) => !recorded.includes(t)));
|
|
1000
1000
|
return { slug: rep.slug, nodeId: rep.nodeId, recorded, missing };
|
|
1001
1001
|
});
|
|
1002
1002
|
return { reps, complete: reps.every((r) => r.missing.length === 0) };
|
|
@@ -1066,7 +1066,7 @@ function ingestAsset(setDir, slug, name, content) {
|
|
|
1066
1066
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
1067
1067
|
writeFileSync(file, content);
|
|
1068
1068
|
}
|
|
1069
|
-
var RECORD_TOOLS, INTERIOR_TOOL, REQUIRED_TOOLS, SessionManifestSchema, manifestPath;
|
|
1069
|
+
var RECORD_TOOLS, INTERIOR_TOOL, REQUIRED_TOOLS, SessionManifestSchema, manifestPath, PROTOCOL_ORDER, byProtocolOrder;
|
|
1070
1070
|
var init_session = __esm({
|
|
1071
1071
|
"packages/figma/src/recording/session.ts"() {
|
|
1072
1072
|
"use strict";
|
|
@@ -1108,6 +1108,8 @@ var init_session = __esm({
|
|
|
1108
1108
|
roles: z4.unknown().optional()
|
|
1109
1109
|
});
|
|
1110
1110
|
manifestPath = (setDir) => path.join(setDir, "recording-set.json");
|
|
1111
|
+
PROTOCOL_ORDER = ["get_metadata", "get_design_context", "get_screenshot", "get_variable_defs", "get_metadata_interior"];
|
|
1112
|
+
byProtocolOrder = (tools) => [...tools].sort((a, b) => PROTOCOL_ORDER.indexOf(a) - PROTOCOL_ORDER.indexOf(b));
|
|
1111
1113
|
}
|
|
1112
1114
|
});
|
|
1113
1115
|
|
|
@@ -1212,8 +1214,8 @@ var init_src = __esm({
|
|
|
1212
1214
|
function variableNameToPath(name) {
|
|
1213
1215
|
return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
|
|
1214
1216
|
}
|
|
1215
|
-
function tokenPathToCssVar(
|
|
1216
|
-
return `--${
|
|
1217
|
+
function tokenPathToCssVar(path35) {
|
|
1218
|
+
return `--${path35.join("-")}`;
|
|
1217
1219
|
}
|
|
1218
1220
|
function toDtcgToken(variable, defaultMode) {
|
|
1219
1221
|
const modes = Object.keys(variable.valuesByMode);
|
|
@@ -1257,11 +1259,11 @@ function toDtcgToken(variable, defaultMode) {
|
|
|
1257
1259
|
}
|
|
1258
1260
|
function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
1259
1261
|
const entries = variables.map((variable) => {
|
|
1260
|
-
const
|
|
1261
|
-
if (
|
|
1262
|
+
const path35 = variableNameToPath(variable.name);
|
|
1263
|
+
if (path35.length === 0) {
|
|
1262
1264
|
throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
|
|
1263
1265
|
}
|
|
1264
|
-
return { variable, path:
|
|
1266
|
+
return { variable, path: path35 };
|
|
1265
1267
|
});
|
|
1266
1268
|
const groupPrefixes = /* @__PURE__ */ new Set();
|
|
1267
1269
|
for (const e of entries) {
|
|
@@ -1282,21 +1284,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
|
1282
1284
|
}
|
|
1283
1285
|
const tokens = {};
|
|
1284
1286
|
const flat = [];
|
|
1285
|
-
for (const { variable, path:
|
|
1287
|
+
for (const { variable, path: path35 } of entries) {
|
|
1286
1288
|
const token = toDtcgToken(variable, defaultMode);
|
|
1287
1289
|
let group = tokens;
|
|
1288
|
-
for (const segment of
|
|
1290
|
+
for (const segment of path35.slice(0, -1)) {
|
|
1289
1291
|
const existing = group[segment];
|
|
1290
1292
|
group = existing ?? (group[segment] = {});
|
|
1291
1293
|
}
|
|
1292
|
-
const leaf =
|
|
1294
|
+
const leaf = path35[path35.length - 1];
|
|
1293
1295
|
if (group[leaf] !== void 0) {
|
|
1294
|
-
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${
|
|
1296
|
+
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path35.join(".")}" (variable ${variable.id})`);
|
|
1295
1297
|
}
|
|
1296
1298
|
group[leaf] = token;
|
|
1297
1299
|
flat.push({
|
|
1298
|
-
path:
|
|
1299
|
-
cssVar: tokenPathToCssVar(
|
|
1300
|
+
path: path35.join("."),
|
|
1301
|
+
cssVar: tokenPathToCssVar(path35),
|
|
1300
1302
|
type: token.$type,
|
|
1301
1303
|
value: token.$value
|
|
1302
1304
|
});
|
|
@@ -1485,9 +1487,9 @@ function boundId(value) {
|
|
|
1485
1487
|
return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
|
|
1486
1488
|
}
|
|
1487
1489
|
function resolveBinding(ctx, id) {
|
|
1488
|
-
const
|
|
1489
|
-
if (
|
|
1490
|
-
return
|
|
1490
|
+
const path35 = ctx.pathById.get(id);
|
|
1491
|
+
if (path35 === void 0) ctx.unresolved.add(id);
|
|
1492
|
+
return path35;
|
|
1491
1493
|
}
|
|
1492
1494
|
function parseVariantProps(name) {
|
|
1493
1495
|
if (!name.includes("=")) return void 0;
|
|
@@ -1522,8 +1524,8 @@ function walk(ctx, raw) {
|
|
|
1522
1524
|
if (!isObject(paint) || paint["visible"] === false) continue;
|
|
1523
1525
|
const id = boundId(paint);
|
|
1524
1526
|
if (id !== void 0) {
|
|
1525
|
-
const
|
|
1526
|
-
if (
|
|
1527
|
+
const path35 = resolveBinding(ctx, id);
|
|
1528
|
+
if (path35 !== void 0) tokens.add(path35);
|
|
1527
1529
|
} else if (typeof paint["color"] === "string") {
|
|
1528
1530
|
ctx.hardcoded.push({ node: name, property, value: paint["color"] });
|
|
1529
1531
|
}
|
|
@@ -1531,8 +1533,8 @@ function walk(ctx, raw) {
|
|
|
1531
1533
|
}
|
|
1532
1534
|
const radiusId = boundId(raw["cornerRadius"]);
|
|
1533
1535
|
if (radiusId !== void 0) {
|
|
1534
|
-
const
|
|
1535
|
-
if (
|
|
1536
|
+
const path35 = resolveBinding(ctx, radiusId);
|
|
1537
|
+
if (path35 !== void 0) tokens.add(path35);
|
|
1536
1538
|
} else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
|
|
1537
1539
|
ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
|
|
1538
1540
|
}
|
|
@@ -1542,10 +1544,10 @@ function walk(ctx, raw) {
|
|
|
1542
1544
|
layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
|
|
1543
1545
|
const gapId = boundId(raw["itemSpacing"]);
|
|
1544
1546
|
if (gapId !== void 0) {
|
|
1545
|
-
const
|
|
1546
|
-
if (
|
|
1547
|
-
layout.gap =
|
|
1548
|
-
tokens.add(
|
|
1547
|
+
const path35 = resolveBinding(ctx, gapId);
|
|
1548
|
+
if (path35 !== void 0) {
|
|
1549
|
+
layout.gap = path35;
|
|
1550
|
+
tokens.add(path35);
|
|
1549
1551
|
}
|
|
1550
1552
|
} else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
|
|
1551
1553
|
ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
|
|
@@ -1554,10 +1556,10 @@ function walk(ctx, raw) {
|
|
|
1554
1556
|
for (const field of PADDING_FIELDS) {
|
|
1555
1557
|
const id = boundId(raw[field]);
|
|
1556
1558
|
if (id !== void 0) {
|
|
1557
|
-
const
|
|
1558
|
-
if (
|
|
1559
|
-
paddingPaths.push(
|
|
1560
|
-
tokens.add(
|
|
1559
|
+
const path35 = resolveBinding(ctx, id);
|
|
1560
|
+
if (path35 !== void 0) {
|
|
1561
|
+
paddingPaths.push(path35);
|
|
1562
|
+
tokens.add(path35);
|
|
1561
1563
|
}
|
|
1562
1564
|
} else if (typeof raw[field] === "number" && raw[field] !== 0) {
|
|
1563
1565
|
ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
|
|
@@ -1977,7 +1979,13 @@ async function runTokenLint(css, fileLabel = "generated.css", definedVars2) {
|
|
|
1977
1979
|
file: fileLabel,
|
|
1978
1980
|
line: w.line,
|
|
1979
1981
|
property: w.rule,
|
|
1980
|
-
|
|
1982
|
+
// Run 7: a single-pose token map left 15 of 20 colors with no
|
|
1983
|
+
// token to reference, and 22 strict-value findings read as
|
|
1984
|
+
// must-fix while the brief forbade inventing names — unfixable
|
|
1985
|
+
// noise in the highest-attention output. The finding stays (a
|
|
1986
|
+
// matching token SHOULD be referenced when one exists); the
|
|
1987
|
+
// message now carries the escape hatch.
|
|
1988
|
+
message: w.rule === "scale-unlimited/declaration-strict-value" ? `${w.text} \u2014 ADVISORY: reference the design token if the kit provides one; if it does not, the literal is CORRECT \u2014 never invent a token name (the recorded token map captures one pose's variables and is known-partial)` : w.text
|
|
1981
1989
|
}))
|
|
1982
1990
|
);
|
|
1983
1991
|
const ownDefs = /* @__PURE__ */ new Set();
|
|
@@ -2324,6 +2332,23 @@ function detectBackdrop(png, opts = {}) {
|
|
|
2324
2332
|
const { modal, consensus } = ringConsensus(png);
|
|
2325
2333
|
if (consensus < 0.9) return [255, 255, 255];
|
|
2326
2334
|
if (Math.min(...modal) < 200 && opts.trustDark !== true) return [255, 255, 255];
|
|
2335
|
+
const decoded = PNG.sync.read(Buffer.from(png));
|
|
2336
|
+
if (decoded.width >= 4 && decoded.height >= 4) {
|
|
2337
|
+
const blocks = cornerBlocks(decoded, 2);
|
|
2338
|
+
const first = blocks[0];
|
|
2339
|
+
const cornersAgree = blocks.every((c) => Math.abs(c[0] - first[0]) + Math.abs(c[1] - first[1]) + Math.abs(c[2] - first[2]) <= INK_DELTA);
|
|
2340
|
+
const cornersDisagreeWithRing = Math.abs(first[0] - modal[0]) + Math.abs(first[1] - modal[1]) + Math.abs(first[2] - modal[2]) > INK_DELTA;
|
|
2341
|
+
if (cornersAgree && cornersDisagreeWithRing) {
|
|
2342
|
+
const extremes = cornerBlocks(decoded, 1);
|
|
2343
|
+
const value = [
|
|
2344
|
+
Math.round(extremes.reduce((a, c) => a + c[0], 0) / 4),
|
|
2345
|
+
Math.round(extremes.reduce((a, c) => a + c[1], 0) / 4),
|
|
2346
|
+
Math.round(extremes.reduce((a, c) => a + c[2], 0) / 4)
|
|
2347
|
+
];
|
|
2348
|
+
if (Math.min(...value) < 200 && opts.trustDark !== true) return [255, 255, 255];
|
|
2349
|
+
return value;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2327
2352
|
return modal;
|
|
2328
2353
|
}
|
|
2329
2354
|
function pngDimensions(png) {
|
|
@@ -5118,6 +5143,107 @@ var init_output = __esm({
|
|
|
5118
5143
|
}
|
|
5119
5144
|
});
|
|
5120
5145
|
|
|
5146
|
+
// packages/cli/src/entitlement.ts
|
|
5147
|
+
import { chmodSync, existsSync as existsSync16, mkdirSync as mkdirSync4, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "node:fs";
|
|
5148
|
+
import crypto from "node:crypto";
|
|
5149
|
+
import os3 from "node:os";
|
|
5150
|
+
import path21 from "node:path";
|
|
5151
|
+
function entitlementPath() {
|
|
5152
|
+
return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path21.join(os3.homedir(), ".tendril", "entitlement.json");
|
|
5153
|
+
}
|
|
5154
|
+
function readStoredEntitlement(file = entitlementPath()) {
|
|
5155
|
+
if (!existsSync16(file)) return void 0;
|
|
5156
|
+
try {
|
|
5157
|
+
const parsed = JSON.parse(readFileSync13(file, "utf8"));
|
|
5158
|
+
if (typeof parsed.token !== "string" || typeof parsed.lastRefreshAt !== "number") return void 0;
|
|
5159
|
+
return { token: parsed.token, lastRefreshAt: parsed.lastRefreshAt };
|
|
5160
|
+
} catch {
|
|
5161
|
+
return void 0;
|
|
5162
|
+
}
|
|
5163
|
+
}
|
|
5164
|
+
function writeStoredEntitlement(stored, file = entitlementPath()) {
|
|
5165
|
+
mkdirSync4(path21.dirname(file), { recursive: true });
|
|
5166
|
+
writeFileSync7(file, `${JSON.stringify(stored, null, 2)}
|
|
5167
|
+
`);
|
|
5168
|
+
chmodSync(file, 384);
|
|
5169
|
+
}
|
|
5170
|
+
function parseEntitlementToken(token) {
|
|
5171
|
+
if (!token.startsWith(ENT_PREFIX)) return { error: "not a tendril entitlement token" };
|
|
5172
|
+
const parts = token.slice(ENT_PREFIX.length).split(".");
|
|
5173
|
+
if (parts.length !== 2 || parts[0] === "" || parts[1] === "") return { error: "malformed token (expected payload.signature)" };
|
|
5174
|
+
let claims;
|
|
5175
|
+
try {
|
|
5176
|
+
claims = JSON.parse(b64urlDecode(parts[0]).toString("utf8"));
|
|
5177
|
+
} catch {
|
|
5178
|
+
return { error: "payload is not valid JSON" };
|
|
5179
|
+
}
|
|
5180
|
+
if (typeof claims.sub !== "string" || typeof claims.plan !== "string" || typeof claims.iat !== "number" || typeof claims.exp !== "number" || typeof claims.kid !== "string") {
|
|
5181
|
+
return { error: "payload is missing required claims" };
|
|
5182
|
+
}
|
|
5183
|
+
return { claims, signedData: b64urlDecode(parts[0]), signature: b64urlDecode(parts[1]) };
|
|
5184
|
+
}
|
|
5185
|
+
function checkEntitlement(opts = {}) {
|
|
5186
|
+
const keys = opts.keys ?? PUBLIC_KEYS;
|
|
5187
|
+
if (Object.keys(keys).length === 0) return { ok: true, mode: "pre-launch" };
|
|
5188
|
+
const now = opts.now ?? Date.now();
|
|
5189
|
+
const stored = "stored" in opts ? opts.stored : readStoredEntitlement();
|
|
5190
|
+
if (stored === void 0) {
|
|
5191
|
+
return { ok: false, code: "entitlement-required", error: "no entitlement on this machine \u2014 record and generate need an active Tendril plan (verify stays free, always)", remediation: ACTIVATE_REMEDIATION };
|
|
5192
|
+
}
|
|
5193
|
+
const parsed = parseEntitlementToken(stored.token);
|
|
5194
|
+
if ("error" in parsed) {
|
|
5195
|
+
return { ok: false, code: "entitlement-invalid", error: `stored entitlement is unreadable: ${parsed.error}`, remediation: ACTIVATE_REMEDIATION };
|
|
5196
|
+
}
|
|
5197
|
+
const pem = keys[parsed.claims.kid];
|
|
5198
|
+
const valid = pem !== void 0 && (() => {
|
|
5199
|
+
try {
|
|
5200
|
+
return crypto.verify(null, parsed.signedData, crypto.createPublicKey(pem), parsed.signature);
|
|
5201
|
+
} catch {
|
|
5202
|
+
return false;
|
|
5203
|
+
}
|
|
5204
|
+
})();
|
|
5205
|
+
if (!valid) {
|
|
5206
|
+
return { ok: false, code: "entitlement-invalid", error: "stored entitlement failed signature verification", remediation: ACTIVATE_REMEDIATION };
|
|
5207
|
+
}
|
|
5208
|
+
if (now > parsed.claims.exp + TOLERANCE_MS) {
|
|
5209
|
+
return {
|
|
5210
|
+
ok: false,
|
|
5211
|
+
code: "entitlement-expired",
|
|
5212
|
+
error: "entitlement expired (and the offline tolerance window has passed)",
|
|
5213
|
+
remediation: `Reconnect and ${ACTIVATE_REMEDIATION}`
|
|
5214
|
+
};
|
|
5215
|
+
}
|
|
5216
|
+
if (now < stored.lastRefreshAt - CLOCK_ROLLBACK_MS) {
|
|
5217
|
+
return {
|
|
5218
|
+
ok: false,
|
|
5219
|
+
code: "entitlement-clock",
|
|
5220
|
+
error: "system clock sits more than a day before the last entitlement refresh",
|
|
5221
|
+
remediation: `Fix the system clock, then ${ACTIVATE_REMEDIATION}`
|
|
5222
|
+
};
|
|
5223
|
+
}
|
|
5224
|
+
return { ok: true, mode: "active", claims: parsed.claims, stale: now > parsed.claims.exp };
|
|
5225
|
+
}
|
|
5226
|
+
function requireEntitlement(flags) {
|
|
5227
|
+
const status = checkEntitlement();
|
|
5228
|
+
if (!status.ok) {
|
|
5229
|
+
fail(flags, ExitCode.Auth, { error: status.error, code: status.code, remediation: status.remediation });
|
|
5230
|
+
}
|
|
5231
|
+
}
|
|
5232
|
+
var ENT_PREFIX, PUBLIC_KEYS, TOLERANCE_MS, CLOCK_ROLLBACK_MS, b64urlDecode, ACTIVATE_REMEDIATION;
|
|
5233
|
+
var init_entitlement = __esm({
|
|
5234
|
+
"packages/cli/src/entitlement.ts"() {
|
|
5235
|
+
"use strict";
|
|
5236
|
+
init_src3();
|
|
5237
|
+
init_output();
|
|
5238
|
+
ENT_PREFIX = "tendril-ent.v1.";
|
|
5239
|
+
PUBLIC_KEYS = {};
|
|
5240
|
+
TOLERANCE_MS = 24 * 60 * 60 * 1e3;
|
|
5241
|
+
CLOCK_ROLLBACK_MS = 24 * 60 * 60 * 1e3;
|
|
5242
|
+
b64urlDecode = (s) => Buffer.from(s, "base64url");
|
|
5243
|
+
ACTIVATE_REMEDIATION = "Run `tendril activate` in your terminal (a browser approval \u2014 never paste license material into an agent chat).";
|
|
5244
|
+
}
|
|
5245
|
+
});
|
|
5246
|
+
|
|
5121
5247
|
// packages/llm/src/model-config.ts
|
|
5122
5248
|
import { z as z6 } from "zod";
|
|
5123
5249
|
function resolveModel(config, requestedId) {
|
|
@@ -6309,6 +6435,100 @@ var init_src6 = __esm({
|
|
|
6309
6435
|
}
|
|
6310
6436
|
});
|
|
6311
6437
|
|
|
6438
|
+
// packages/cli/src/commands/activate.ts
|
|
6439
|
+
var activate_exports = {};
|
|
6440
|
+
__export(activate_exports, {
|
|
6441
|
+
ENTITLEMENT_SERVICE_URL: () => ENTITLEMENT_SERVICE_URL,
|
|
6442
|
+
runActivate: () => runActivate
|
|
6443
|
+
});
|
|
6444
|
+
async function runActivate(flags) {
|
|
6445
|
+
const base = flags.serviceUrl ?? ENTITLEMENT_SERVICE_URL;
|
|
6446
|
+
if (base === void 0) {
|
|
6447
|
+
fail(flags, ExitCode.Auth, {
|
|
6448
|
+
error: "the Tendril entitlement service is not live yet (pre-launch build) \u2014 there is nothing to activate against",
|
|
6449
|
+
code: "entitlement-service-unavailable",
|
|
6450
|
+
remediation: "Nothing to do: pre-launch builds run record/generate without activation. This command becomes meaningful at launch."
|
|
6451
|
+
});
|
|
6452
|
+
}
|
|
6453
|
+
let device;
|
|
6454
|
+
try {
|
|
6455
|
+
const res = await fetch(new URL("/v1/device/code", base), { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
|
|
6456
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
6457
|
+
device = await res.json();
|
|
6458
|
+
if (typeof device.deviceCode !== "string" || typeof device.userCode !== "string" || typeof device.verificationUri !== "string") throw new Error("malformed device-code response");
|
|
6459
|
+
} catch (err) {
|
|
6460
|
+
fail(flags, ExitCode.Auth, {
|
|
6461
|
+
error: `could not start activation: ${err instanceof Error ? err.message : String(err)}`,
|
|
6462
|
+
code: "entitlement-service-unreachable",
|
|
6463
|
+
remediation: "Check your connection and retry `tendril activate`."
|
|
6464
|
+
});
|
|
6465
|
+
}
|
|
6466
|
+
process.stderr.write(`To activate Tendril, visit:
|
|
6467
|
+
|
|
6468
|
+
${device.verificationUri}
|
|
6469
|
+
|
|
6470
|
+
and enter the code: ${device.userCode}
|
|
6471
|
+
|
|
6472
|
+
Waiting for approval\u2026
|
|
6473
|
+
`);
|
|
6474
|
+
const interval = Math.max(1e3, device.intervalMs ?? 5e3);
|
|
6475
|
+
const deadline = Date.now() + (device.expiresInMs ?? 10 * 60 * 1e3);
|
|
6476
|
+
let token;
|
|
6477
|
+
while (Date.now() < deadline) {
|
|
6478
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
6479
|
+
const res = await fetch(new URL("/v1/device/token", base), {
|
|
6480
|
+
method: "POST",
|
|
6481
|
+
headers: { "content-type": "application/json" },
|
|
6482
|
+
body: JSON.stringify({ deviceCode: device.deviceCode })
|
|
6483
|
+
}).catch(() => void 0);
|
|
6484
|
+
if (res === void 0) continue;
|
|
6485
|
+
if (res.status === 428) continue;
|
|
6486
|
+
if (!res.ok) {
|
|
6487
|
+
fail(flags, ExitCode.Auth, {
|
|
6488
|
+
error: `activation was not approved (HTTP ${res.status})`,
|
|
6489
|
+
code: "entitlement-denied",
|
|
6490
|
+
remediation: "Retry `tendril activate`; if it persists, check the account's plan in the portal."
|
|
6491
|
+
});
|
|
6492
|
+
}
|
|
6493
|
+
token = (await res.json()).token;
|
|
6494
|
+
break;
|
|
6495
|
+
}
|
|
6496
|
+
if (token === void 0) {
|
|
6497
|
+
fail(flags, ExitCode.Auth, {
|
|
6498
|
+
error: "activation timed out before the browser approval arrived",
|
|
6499
|
+
code: "entitlement-timeout",
|
|
6500
|
+
remediation: "Run `tendril activate` again and complete the browser step within the shown window."
|
|
6501
|
+
});
|
|
6502
|
+
}
|
|
6503
|
+
const parsed = parseEntitlementToken(token);
|
|
6504
|
+
if ("error" in parsed) {
|
|
6505
|
+
fail(flags, ExitCode.Auth, {
|
|
6506
|
+
error: `service returned an unusable token: ${parsed.error}`,
|
|
6507
|
+
code: "entitlement-invalid",
|
|
6508
|
+
remediation: "Retry `tendril activate`; report this if it persists \u2014 it is a service-side fault."
|
|
6509
|
+
});
|
|
6510
|
+
}
|
|
6511
|
+
if (Object.keys(PUBLIC_KEYS).length > 0) {
|
|
6512
|
+
const status = checkEntitlement({ now: Date.now(), stored: { token, lastRefreshAt: Date.now() } });
|
|
6513
|
+
if (!status.ok) fail(flags, ExitCode.Auth, { error: `service returned a token this build cannot verify: ${status.error}`, code: status.code, remediation: status.remediation });
|
|
6514
|
+
}
|
|
6515
|
+
writeStoredEntitlement({ token, lastRefreshAt: Date.now() });
|
|
6516
|
+
emitData(flags, { activated: true, plan: parsed.claims.plan, sub: parsed.claims.sub, expiresAt: new Date(parsed.claims.exp).toISOString(), storedAt: entitlementPath() }, () => {
|
|
6517
|
+
process.stdout.write(`activated: plan "${parsed.claims.plan}" until ${new Date(parsed.claims.exp).toISOString().slice(0, 10)} (stored at ${entitlementPath()})
|
|
6518
|
+
`);
|
|
6519
|
+
});
|
|
6520
|
+
}
|
|
6521
|
+
var ENTITLEMENT_SERVICE_URL;
|
|
6522
|
+
var init_activate = __esm({
|
|
6523
|
+
"packages/cli/src/commands/activate.ts"() {
|
|
6524
|
+
"use strict";
|
|
6525
|
+
init_src3();
|
|
6526
|
+
init_entitlement();
|
|
6527
|
+
init_output();
|
|
6528
|
+
ENTITLEMENT_SERVICE_URL = void 0;
|
|
6529
|
+
}
|
|
6530
|
+
});
|
|
6531
|
+
|
|
6312
6532
|
// packages/cli/src/commands/record.ts
|
|
6313
6533
|
var record_exports = {};
|
|
6314
6534
|
__export(record_exports, {
|
|
@@ -6325,11 +6545,11 @@ __export(record_exports, {
|
|
|
6325
6545
|
runRecordPlan: () => runRecordPlan,
|
|
6326
6546
|
runRecordStatus: () => runRecordStatus
|
|
6327
6547
|
});
|
|
6328
|
-
import { existsSync as
|
|
6329
|
-
import
|
|
6330
|
-
import { writeFileSync as
|
|
6548
|
+
import { existsSync as existsSync19, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "node:fs";
|
|
6549
|
+
import path25 from "node:path";
|
|
6550
|
+
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
6331
6551
|
function symbolsFromMetadataEnvelope(file, sourceFrame) {
|
|
6332
|
-
const env = JSON.parse(
|
|
6552
|
+
const env = JSON.parse(readFileSync16(file, "utf8"));
|
|
6333
6553
|
const text = env.content.map((c) => c.text ?? "").join("\n");
|
|
6334
6554
|
const symbols = [];
|
|
6335
6555
|
const walk2 = (node, ancestor) => {
|
|
@@ -6359,6 +6579,7 @@ function instanceLeads(text) {
|
|
|
6359
6579
|
return [...seen.entries()].map(([name, nodeId]) => ({ name, nodeId }));
|
|
6360
6580
|
}
|
|
6361
6581
|
function runRecordPlan(opts) {
|
|
6582
|
+
requireEntitlement(opts);
|
|
6362
6583
|
const defaults = {};
|
|
6363
6584
|
for (const spec of opts.defaultSpecs ?? []) {
|
|
6364
6585
|
const eq = spec.indexOf("=");
|
|
@@ -6376,7 +6597,7 @@ function runRecordPlan(opts) {
|
|
|
6376
6597
|
for (const spec of opts.metadataFiles) {
|
|
6377
6598
|
const [file, frame] = spec.split("@");
|
|
6378
6599
|
try {
|
|
6379
|
-
const parsed = symbolsFromMetadataEnvelope(
|
|
6600
|
+
const parsed = symbolsFromMetadataEnvelope(path25.resolve(file), frame);
|
|
6380
6601
|
symbols.push(...parsed.symbols);
|
|
6381
6602
|
if (parsed.truncated) metadataTruncated = true;
|
|
6382
6603
|
} catch (err) {
|
|
@@ -6410,7 +6631,7 @@ function runRecordPlan(opts) {
|
|
|
6410
6631
|
const leads = opts.metadataFiles.flatMap((spec) => {
|
|
6411
6632
|
const [file] = spec.split("@");
|
|
6412
6633
|
try {
|
|
6413
|
-
const env = JSON.parse(
|
|
6634
|
+
const env = JSON.parse(readFileSync16(path25.resolve(file), "utf8"));
|
|
6414
6635
|
return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
|
|
6415
6636
|
} catch {
|
|
6416
6637
|
return [];
|
|
@@ -6467,6 +6688,10 @@ function runRecordPlan(opts) {
|
|
|
6467
6688
|
...toppedUp !== void 0 ? { toppedUp } : {},
|
|
6468
6689
|
notRecorded: manifest.notRecorded ?? null,
|
|
6469
6690
|
figmaCallEstimate: { reps: manifest.reps.length, calls: `~${callLow}\u2013${callHigh}` },
|
|
6691
|
+
// Cold-start fix (run 7): the first instruction rides the plan
|
|
6692
|
+
// response, so record_next is never needed to begin — it exists
|
|
6693
|
+
// only for resuming.
|
|
6694
|
+
next: nextPayload(opts.setDir),
|
|
6470
6695
|
...toConfirm.length > 0 ? {
|
|
6471
6696
|
defaultsToConfirm: {
|
|
6472
6697
|
instruction: "HEURISTIC defaults: the designer named no literal Default and no override was given, so the code guessed the component's zero point \u2014 the pose an empty-props mount shows and the pose behaviour checks aim at. When a USER is present, ask each question below BEFORE recording; if they pick a different value, re-run this exact plan command with --default <Axis>=<Value> (allowed until the first envelope is ingested; frozen with the recording after). Non-interactive: proceed with the resolved values and state them in your report.",
|
|
@@ -6511,7 +6736,7 @@ function nextPayload(setDir) {
|
|
|
6511
6736
|
const instruction = nextInstruction(setDir);
|
|
6512
6737
|
const status = sessionStatus(setDir);
|
|
6513
6738
|
const progress = { recordedReps: status.reps.filter((x) => x.missing.length === 0).length, totalReps: status.reps.length };
|
|
6514
|
-
if (instruction === null && !
|
|
6739
|
+
if (instruction === null && !existsSync19(path25.join(setDir, "get_variable_defs.json"))) {
|
|
6515
6740
|
const manifest = loadManifest(setDir);
|
|
6516
6741
|
const frameNode = Object.keys(manifest.sourceFrames ?? {})[0] ?? manifest.reps[0]?.nodeId ?? "";
|
|
6517
6742
|
return { slug: "__set__", nodeId: frameNode, tool: "get_variable_defs", note: `SET-LEVEL: call get_variable_defs on the component frame and ingest with --rep __set__. ${ENVELOPE_HELP}`, progress };
|
|
@@ -6602,7 +6827,7 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
6602
6827
|
const skipped = [];
|
|
6603
6828
|
const failed = [];
|
|
6604
6829
|
for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
|
|
6605
|
-
if (
|
|
6830
|
+
if (existsSync19(path25.join(setDir, rep, name))) {
|
|
6606
6831
|
skipped.push(name);
|
|
6607
6832
|
continue;
|
|
6608
6833
|
}
|
|
@@ -6624,16 +6849,16 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
6624
6849
|
}
|
|
6625
6850
|
function rawEnvelopeFromFile(file, parts) {
|
|
6626
6851
|
if (parts) {
|
|
6627
|
-
const blocks = JSON.parse(
|
|
6852
|
+
const blocks = JSON.parse(readFileSync16(path25.resolve(file), "utf8"));
|
|
6628
6853
|
if (!Array.isArray(blocks) || blocks.length === 0 || blocks.some((p) => typeof p !== "string")) throw new Error("parts file must be a non-empty JSON array of strings");
|
|
6629
6854
|
return { content: blocks.map((text) => ({ type: "text", text })) };
|
|
6630
6855
|
}
|
|
6631
|
-
return { content: [{ type: "text", text:
|
|
6856
|
+
return { content: [{ type: "text", text: readFileSync16(path25.resolve(file), "utf8") }] };
|
|
6632
6857
|
}
|
|
6633
6858
|
async function runRecordIngest(opts) {
|
|
6634
6859
|
let payload;
|
|
6635
6860
|
try {
|
|
6636
|
-
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(
|
|
6861
|
+
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync16(path25.resolve(opts.file), "utf8"));
|
|
6637
6862
|
} catch (err) {
|
|
6638
6863
|
fail(opts, ExitCode.InputValidation, {
|
|
6639
6864
|
error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -6657,7 +6882,7 @@ async function runRecordIngest(opts) {
|
|
|
6657
6882
|
remediation: "Save the get_variable_defs response verbatim as a text envelope."
|
|
6658
6883
|
});
|
|
6659
6884
|
}
|
|
6660
|
-
|
|
6885
|
+
writeFileSync9(path25.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
|
|
6661
6886
|
`);
|
|
6662
6887
|
emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
|
|
6663
6888
|
process.stdout.write("set-level get_variable_defs ingested\n");
|
|
@@ -6753,8 +6978,8 @@ async function runRecordIngestRep(opts) {
|
|
|
6753
6978
|
}
|
|
6754
6979
|
function runRecordAsset(opts) {
|
|
6755
6980
|
if (opts.dir !== void 0) {
|
|
6756
|
-
const dir =
|
|
6757
|
-
const names =
|
|
6981
|
+
const dir = path25.resolve(opts.dir);
|
|
6982
|
+
const names = readdirSync5(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
|
|
6758
6983
|
if (names.length === 0) {
|
|
6759
6984
|
fail(opts, ExitCode.InputValidation, {
|
|
6760
6985
|
error: `no asset-*.<ext> files found in ${dir}`,
|
|
@@ -6765,7 +6990,7 @@ function runRecordAsset(opts) {
|
|
|
6765
6990
|
const ingested = [];
|
|
6766
6991
|
try {
|
|
6767
6992
|
for (const name of names) {
|
|
6768
|
-
ingestAsset(opts.setDir, opts.rep, name,
|
|
6993
|
+
ingestAsset(opts.setDir, opts.rep, name, readFileSync16(path25.join(dir, name)));
|
|
6769
6994
|
ingested.push(name);
|
|
6770
6995
|
}
|
|
6771
6996
|
} catch (err) {
|
|
@@ -6789,7 +7014,7 @@ function runRecordAsset(opts) {
|
|
|
6789
7014
|
});
|
|
6790
7015
|
}
|
|
6791
7016
|
try {
|
|
6792
|
-
ingestAsset(opts.setDir, opts.rep, opts.name,
|
|
7017
|
+
ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync16(path25.resolve(opts.file)));
|
|
6793
7018
|
emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
|
|
6794
7019
|
process.stdout.write(`ingested ${opts.rep}/${opts.name}
|
|
6795
7020
|
`);
|
|
@@ -6817,7 +7042,7 @@ ${status.reps.filter((r) => r.missing.length > 0).length} rep(s) pending
|
|
|
6817
7042
|
function runRecordFinish(opts) {
|
|
6818
7043
|
const manifest = loadManifest(opts.setDir);
|
|
6819
7044
|
const derived = deriveRoles(opts.setDir, manifest);
|
|
6820
|
-
const roles = opts.rolesFile !== void 0 ? { ...JSON.parse(
|
|
7045
|
+
const roles = opts.rolesFile !== void 0 ? { ...JSON.parse(readFileSync16(path25.resolve(opts.rolesFile), "utf8")), humanOverride: true } : { main: derived.main, parts: derived.parts, external: derived.external, humanOverride: false };
|
|
6821
7046
|
emitData(opts, { derived, confirmed: opts.confirmRoles }, () => {
|
|
6822
7047
|
process.stdout.write(`derived mains: ${derived.main.join(", ") || "(none)"}
|
|
6823
7048
|
`);
|
|
@@ -6844,7 +7069,7 @@ function runRecordFinish(opts) {
|
|
|
6844
7069
|
});
|
|
6845
7070
|
}
|
|
6846
7071
|
const updated = { ...manifest, roles };
|
|
6847
|
-
|
|
7072
|
+
writeFileSync9(path25.join(opts.setDir, "recording-set.json"), `${JSON.stringify(updated, null, 1)}
|
|
6848
7073
|
`);
|
|
6849
7074
|
if (!opts.json) process.stdout.write("roles written to recording-set.json\n");
|
|
6850
7075
|
}
|
|
@@ -6855,6 +7080,7 @@ var init_record = __esm({
|
|
|
6855
7080
|
init_src3();
|
|
6856
7081
|
init_src();
|
|
6857
7082
|
init_output();
|
|
7083
|
+
init_entitlement();
|
|
6858
7084
|
ENVELOPE_HELP = 'Envelope format: text tools save {"content":[{"type":"text","text":"<VERBATIM response text incl. any Currently-selected-nodes block>"}]}; get_screenshot: do NOT download the image yourself \u2014 pass its image_url to `tendril record fetch` (MCP: tendril_record_fetch), which pulls the bytes to disk directly. That keeps the pixel ground truth out of your context and costs one approval instead of a shell command per asset. Assets are asset-<first-8-hex-of-figma-uuid>.<ext>; their URLs appear as const declarations inside the design-context text and also expire.';
|
|
6859
7085
|
isAutoFetchAssetUrl = (url) => isFigmaAssetUrl(url) || isLocalAssetUrl(url);
|
|
6860
7086
|
}
|
|
@@ -7047,8 +7273,8 @@ var init_engine_curated = __esm({
|
|
|
7047
7273
|
});
|
|
7048
7274
|
|
|
7049
7275
|
// packages/generate/src/loop.ts
|
|
7050
|
-
import { existsSync as
|
|
7051
|
-
import
|
|
7276
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync6, readFileSync as readFileSync17, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
|
|
7277
|
+
import path26 from "node:path";
|
|
7052
7278
|
import { z as z11 } from "zod";
|
|
7053
7279
|
function objective(scores, behaviors) {
|
|
7054
7280
|
const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
|
|
@@ -7079,9 +7305,9 @@ ${preludeLines.join("\n")}` : ""}
|
|
|
7079
7305
|
Fix the FAIL configs (region coordinates are in the recorded screenshot's frame; ink<1 means recorded foreground pixels your render does not cover). Do not regress PASS configs. ${mode === "files" ? "Update the files in your candidate directory and re-run the score." : "Reply with the complete corrected files in the same FILE format."}`;
|
|
7080
7306
|
}
|
|
7081
7307
|
function archivePriorRun(outDir) {
|
|
7082
|
-
if (!
|
|
7308
|
+
if (!existsSync20(path26.join(outDir, "run-log.json")) && !existsSync20(path26.join(outDir, "loop-state.json"))) return void 0;
|
|
7083
7309
|
let n = 1;
|
|
7084
|
-
while (
|
|
7310
|
+
while (existsSync20(`${outDir}-prev-${n}`)) n += 1;
|
|
7085
7311
|
renameSync(outDir, `${outDir}-prev-${n}`);
|
|
7086
7312
|
return `${outDir}-prev-${n}`;
|
|
7087
7313
|
}
|
|
@@ -7090,14 +7316,14 @@ async function runEngineLoop(opts) {
|
|
|
7090
7316
|
const plateau = opts.plateau ?? 2;
|
|
7091
7317
|
const progress = opts.onProgress ?? (() => {
|
|
7092
7318
|
});
|
|
7093
|
-
const statePath =
|
|
7094
|
-
const resuming = opts.resume === true &&
|
|
7319
|
+
const statePath = path26.join(opts.outDir, "loop-state.json");
|
|
7320
|
+
const resuming = opts.resume === true && existsSync20(statePath);
|
|
7095
7321
|
if (!resuming) {
|
|
7096
7322
|
const archived = archivePriorRun(opts.outDir);
|
|
7097
7323
|
if (archived !== void 0) progress(`previous run archived to ${archived}`);
|
|
7098
7324
|
}
|
|
7099
|
-
|
|
7100
|
-
const scratch =
|
|
7325
|
+
mkdirSync6(opts.outDir, { recursive: true });
|
|
7326
|
+
const scratch = path26.join(opts.outDir, ".candidate");
|
|
7101
7327
|
let attempts = [];
|
|
7102
7328
|
let log = [];
|
|
7103
7329
|
let best;
|
|
@@ -7105,7 +7331,7 @@ async function runEngineLoop(opts) {
|
|
|
7105
7331
|
let nonAccepted = 0;
|
|
7106
7332
|
let stopReason = "max-iterations";
|
|
7107
7333
|
if (resuming) {
|
|
7108
|
-
const restored = LoopStateSchema.parse(JSON.parse(
|
|
7334
|
+
const restored = LoopStateSchema.parse(JSON.parse(readFileSync17(statePath, "utf8")));
|
|
7109
7335
|
attempts = restored.attempts;
|
|
7110
7336
|
log = restored.iterations;
|
|
7111
7337
|
spentUsd = restored.spentUsd;
|
|
@@ -7120,12 +7346,12 @@ async function runEngineLoop(opts) {
|
|
|
7120
7346
|
progress(`resumed: ${log.length} iteration(s), $${spentUsd.toFixed(3)} spent, best pass=${best?.objective[0] ?? 0}`);
|
|
7121
7347
|
}
|
|
7122
7348
|
const persist = () => {
|
|
7123
|
-
|
|
7349
|
+
writeFileSync10(statePath, `${JSON.stringify({ version: 1, spentUsd, attempts, iterations: log }, null, 1)}
|
|
7124
7350
|
`);
|
|
7125
7351
|
};
|
|
7126
7352
|
const writeCandidate = (files) => {
|
|
7127
|
-
|
|
7128
|
-
for (const [name, content] of Object.entries(files))
|
|
7353
|
+
mkdirSync6(scratch, { recursive: true });
|
|
7354
|
+
for (const [name, content] of Object.entries(files)) writeFileSync10(path26.join(scratch, name), content);
|
|
7129
7355
|
};
|
|
7130
7356
|
const scoreCandidate = async (candidate, iter, usd, modelMs) => {
|
|
7131
7357
|
writeCandidate(candidate.files);
|
|
@@ -7183,8 +7409,8 @@ async function runEngineLoop(opts) {
|
|
|
7183
7409
|
const usd = candidate.usage?.usd ?? 0;
|
|
7184
7410
|
spentUsd += usd;
|
|
7185
7411
|
if (candidate.raw !== void 0) {
|
|
7186
|
-
|
|
7187
|
-
|
|
7412
|
+
mkdirSync6(path26.join(opts.outDir, "responses"), { recursive: true });
|
|
7413
|
+
writeFileSync10(path26.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
|
|
7188
7414
|
}
|
|
7189
7415
|
if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
|
|
7190
7416
|
const finish = candidate.usage?.finishReason ?? "?";
|
|
@@ -7210,10 +7436,10 @@ async function runEngineLoop(opts) {
|
|
|
7210
7436
|
}
|
|
7211
7437
|
}
|
|
7212
7438
|
}
|
|
7213
|
-
if (best !== void 0) for (const [name, content] of Object.entries(best.files))
|
|
7439
|
+
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path26.join(opts.outDir, name), content);
|
|
7214
7440
|
const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
|
|
7215
|
-
|
|
7216
|
-
|
|
7441
|
+
writeFileSync10(
|
|
7442
|
+
path26.join(opts.outDir, "run-log.json"),
|
|
7217
7443
|
`${JSON.stringify(
|
|
7218
7444
|
{
|
|
7219
7445
|
...opts.meta,
|
|
@@ -7280,8 +7506,8 @@ var init_loop2 = __esm({
|
|
|
7280
7506
|
});
|
|
7281
7507
|
|
|
7282
7508
|
// packages/generate/src/brief.ts
|
|
7283
|
-
import { existsSync as
|
|
7284
|
-
import
|
|
7509
|
+
import { existsSync as existsSync21, readFileSync as readFileSync18 } from "node:fs";
|
|
7510
|
+
import path27 from "node:path";
|
|
7285
7511
|
function singleAxes2(name) {
|
|
7286
7512
|
const parsed = parseVariantAxes(name);
|
|
7287
7513
|
if (parsed === void 0) return void 0;
|
|
@@ -7407,8 +7633,8 @@ function authorComponentApi(opts) {
|
|
|
7407
7633
|
configs.push({ rep: p.slug, component: componentIdent, props: {} });
|
|
7408
7634
|
}
|
|
7409
7635
|
if (inexpressible.length > 0) throw new PoseCompletenessError(inexpressible);
|
|
7410
|
-
if (opts.dismissible
|
|
7411
|
-
props.push({ name: "onDismiss", kind: "callback" });
|
|
7636
|
+
if (opts.dismissible !== void 0) {
|
|
7637
|
+
props.push({ name: "onDismiss", kind: "callback", default: opts.dismissible });
|
|
7412
7638
|
}
|
|
7413
7639
|
const entry = `${componentIdent}.tsx`;
|
|
7414
7640
|
const apiPin = {
|
|
@@ -7417,13 +7643,15 @@ function authorComponentApi(opts) {
|
|
|
7417
7643
|
name: pr.name,
|
|
7418
7644
|
type: pr.kind === "boolean" ? "boolean" : pr.kind === "string" ? "string" : pr.kind === "callback" ? "() => void" : pr.values.map((v) => `"${v}"`).join(" | "),
|
|
7419
7645
|
required: false,
|
|
7420
|
-
|
|
7646
|
+
// A callback's `default` field carries detection EVIDENCE for the
|
|
7647
|
+
// prop line, not a value — it never enters the pin.
|
|
7648
|
+
...pr.default !== void 0 && pr.kind !== "callback" ? { default: pr.default } : {}
|
|
7421
7649
|
})),
|
|
7422
7650
|
forcedStates,
|
|
7423
7651
|
poseCompleteness: { recordedPoses: opts.poses.length, expressible: configs.length }
|
|
7424
7652
|
};
|
|
7425
7653
|
const propLines = props.map(
|
|
7426
|
-
(pr) => pr.kind === "boolean" ? ` ${pr.name}?: boolean;` : pr.kind === "string" ? ` ${pr.name}?: string; // CONTENT \u2014 recorded default ${JSON.stringify(pr.default)}` : pr.kind === "callback" ? ` ${pr.name}?: () => void; // NOTIFICATION \u2014
|
|
7654
|
+
(pr) => pr.kind === "boolean" ? ` ${pr.name}?: boolean;` : pr.kind === "string" ? ` ${pr.name}?: string; // CONTENT \u2014 recorded default ${JSON.stringify(pr.default)}` : pr.kind === "callback" ? ` ${pr.name}?: () => void; // NOTIFICATION \u2014 recorded evidence: ${pr.default ?? "the recorded affordance"}; the component owns its state (it hides/updates itself) and the callback informs, never controls` : ` ${pr.name}?: ${pr.values.map((v) => `"${v}"`).join(" | ")}; // default "${pr.default}"`
|
|
7427
7655
|
);
|
|
7428
7656
|
const provided = opts.fonts ?? [];
|
|
7429
7657
|
const recorded = opts.recordedFonts ?? [];
|
|
@@ -7496,7 +7724,7 @@ function authorBehaviors(api, extras = {}) {
|
|
|
7496
7724
|
behaviors.push({
|
|
7497
7725
|
id: `content-prop-renders(${sentinel.prop})`,
|
|
7498
7726
|
config: sentinel.config,
|
|
7499
|
-
props: { [sentinel.prop]: marker },
|
|
7727
|
+
props: { ...sentinel.props ?? {}, [sentinel.prop]: marker },
|
|
7500
7728
|
steps: [{ assertTextVisible: marker }]
|
|
7501
7729
|
});
|
|
7502
7730
|
}
|
|
@@ -7514,15 +7742,18 @@ function authorBehaviors(api, extras = {}) {
|
|
|
7514
7742
|
return { behaviors, prelude: { controls: interactive ? ["> *"] : [], textInputs: [] }, disclosures };
|
|
7515
7743
|
}
|
|
7516
7744
|
function envelopeText(file) {
|
|
7517
|
-
return envelopeFirstTextPart(JSON.parse(
|
|
7745
|
+
return envelopeFirstTextPart(JSON.parse(readFileSync18(file, "utf8")));
|
|
7518
7746
|
}
|
|
7519
7747
|
function dismissEvidence(setDir, repSlugs) {
|
|
7520
7748
|
for (const slug of repSlugs) {
|
|
7521
|
-
const f =
|
|
7522
|
-
if (!
|
|
7523
|
-
|
|
7749
|
+
const f = path27.join(setDir, slug, "get_design_context.json");
|
|
7750
|
+
if (!existsSync21(f)) continue;
|
|
7751
|
+
const text = envelopeText(f);
|
|
7752
|
+
const propHit = /[{,]\s*(\w*dismiss\w*)\s*=\s*(?:true|false)\b/i.exec(text) ?? /\b(\w*dismiss\w*)\??\s*:\s*boolean\b/i.exec(text);
|
|
7753
|
+
if (propHit !== null) return `emission prop "${propHit[1]}"`;
|
|
7754
|
+
for (const m of text.matchAll(/data-name="([^"]+)"/g)) {
|
|
7524
7755
|
const norm = m[1].toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
7525
|
-
if (DISMISS_NAMES.has(norm)) return m[1]
|
|
7756
|
+
if (DISMISS_NAMES.has(norm)) return `layer ${JSON.stringify(m[1])}`;
|
|
7526
7757
|
}
|
|
7527
7758
|
}
|
|
7528
7759
|
return void 0;
|
|
@@ -7560,13 +7791,13 @@ function recordedFontNeeds(setDir) {
|
|
|
7560
7791
|
}
|
|
7561
7792
|
};
|
|
7562
7793
|
const manifest = loadManifest(setDir);
|
|
7563
|
-
const setDefs =
|
|
7564
|
-
if (
|
|
7794
|
+
const setDefs = path27.join(setDir, "get_variable_defs.json");
|
|
7795
|
+
if (existsSync21(setDefs)) fromDefs(envelopeText(setDefs));
|
|
7565
7796
|
for (const rep of manifest.reps) {
|
|
7566
|
-
const ctx =
|
|
7567
|
-
if (
|
|
7568
|
-
const defs =
|
|
7569
|
-
if (
|
|
7797
|
+
const ctx = path27.join(setDir, rep.slug, "get_design_context.json");
|
|
7798
|
+
if (existsSync21(ctx)) fromEmission(envelopeText(ctx));
|
|
7799
|
+
const defs = path27.join(setDir, rep.slug, "get_variable_defs.json");
|
|
7800
|
+
if (existsSync21(defs)) fromDefs(envelopeText(defs));
|
|
7570
7801
|
}
|
|
7571
7802
|
return [...byFamily.entries()].map(([family, paired]) => {
|
|
7572
7803
|
const weights = /* @__PURE__ */ new Set([...paired, ...unpaired]);
|
|
@@ -7589,8 +7820,8 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
7589
7820
|
const propRep = [];
|
|
7590
7821
|
const perRep = [];
|
|
7591
7822
|
for (const slug of repSlugs) {
|
|
7592
|
-
const f =
|
|
7593
|
-
if (!
|
|
7823
|
+
const f = path27.join(setDir, slug, "get_design_context.json");
|
|
7824
|
+
if (!existsSync21(f)) continue;
|
|
7594
7825
|
const code = envelopeText(f);
|
|
7595
7826
|
const props = /* @__PURE__ */ new Map();
|
|
7596
7827
|
for (const m of code.matchAll(/[{,]\s*(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"/g)) {
|
|
@@ -7606,12 +7837,17 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
7606
7837
|
if (t === "" || !/[A-Za-z0-9]/.test(t)) continue;
|
|
7607
7838
|
texts.push(t);
|
|
7608
7839
|
}
|
|
7609
|
-
|
|
7840
|
+
for (const m of code.matchAll(/>\{[`'"]([^`'"]+)[`'"]\}</g)) {
|
|
7841
|
+
const t = decodeXmlEntities(m[1]).trim();
|
|
7842
|
+
if (t === "" || !/[A-Za-z0-9]/.test(t)) continue;
|
|
7843
|
+
texts.push(t);
|
|
7844
|
+
}
|
|
7845
|
+
perRep.push({ slug, texts, code });
|
|
7610
7846
|
}
|
|
7611
7847
|
const axisValuesBySlug = /* @__PURE__ */ new Map();
|
|
7612
7848
|
for (const slug of repSlugs) {
|
|
7613
|
-
const metaFile =
|
|
7614
|
-
if (!
|
|
7849
|
+
const metaFile = path27.join(setDir, slug, "get_metadata.json");
|
|
7850
|
+
if (!existsSync21(metaFile)) continue;
|
|
7615
7851
|
const name = symbolName(envelopeText(metaFile));
|
|
7616
7852
|
if (name === void 0) continue;
|
|
7617
7853
|
const values = /* @__PURE__ */ new Set();
|
|
@@ -7647,9 +7883,11 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
7647
7883
|
}
|
|
7648
7884
|
const visibleIn = perRep.filter((r) => {
|
|
7649
7885
|
const v = propRep.find((pr) => pr.slug === r.slug)?.props.get(name) ?? def;
|
|
7650
|
-
return r.texts.some((t) => t.includes(v));
|
|
7886
|
+
return r.texts.some((t) => t.includes(v)) || r.texts.join(" ").replace(/\s+/g, " ").includes(v.replace(/\s+/g, " ").trim());
|
|
7651
7887
|
}).map((r) => r.slug);
|
|
7652
|
-
|
|
7888
|
+
const refPattern = new RegExp(`\\{\\s*${name}\\s*\\}`);
|
|
7889
|
+
const referencedIn = perRep.filter((r) => refPattern.test(r.code)).map((r) => r.slug);
|
|
7890
|
+
return { prop: name, default: def, overrides, varies: Object.keys(overrides).length > 0, visibleIn, referencedIn };
|
|
7653
7891
|
});
|
|
7654
7892
|
}
|
|
7655
7893
|
if (perRep.length === 0) return [];
|
|
@@ -7696,7 +7934,7 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
7696
7934
|
usedNames.add(prop);
|
|
7697
7935
|
const overrides = {};
|
|
7698
7936
|
for (const [slug, v] of sl.values) if (v !== def) overrides[slug] = v;
|
|
7699
|
-
return { prop, default: def, overrides, varies: Object.keys(overrides).length > 0, visibleIn: [...sl.values.keys()] };
|
|
7937
|
+
return { prop, default: def, overrides, varies: Object.keys(overrides).length > 0, visibleIn: [...sl.values.keys()], referencedIn: [] };
|
|
7700
7938
|
});
|
|
7701
7939
|
}
|
|
7702
7940
|
function authorTaskFromSet(setDir, opts = {}) {
|
|
@@ -7708,8 +7946,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
7708
7946
|
const poses = [];
|
|
7709
7947
|
const missing = [];
|
|
7710
7948
|
for (const rep of manifest.reps) {
|
|
7711
|
-
const metaFile =
|
|
7712
|
-
if (!
|
|
7949
|
+
const metaFile = path27.join(setDir, rep.slug, "get_metadata.json");
|
|
7950
|
+
if (!existsSync21(metaFile)) {
|
|
7713
7951
|
missing.push(rep.slug);
|
|
7714
7952
|
continue;
|
|
7715
7953
|
}
|
|
@@ -7723,8 +7961,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
7723
7961
|
if (missing.length > 0) {
|
|
7724
7962
|
throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
|
|
7725
7963
|
}
|
|
7726
|
-
const setMeta =
|
|
7727
|
-
const latticeNames = manifest.latticeNames ?? (
|
|
7964
|
+
const setMeta = path27.join(setDir, "get_metadata.json");
|
|
7965
|
+
const latticeNames = manifest.latticeNames ?? (existsSync21(setMeta) ? [...envelopeText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
|
|
7728
7966
|
const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
|
|
7729
7967
|
const recordedFonts = recordedFontFamilies(setDir);
|
|
7730
7968
|
const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
|
|
@@ -7737,26 +7975,48 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
7737
7975
|
...recordedFonts.length > 0 ? { recordedFonts } : {},
|
|
7738
7976
|
...Object.keys(defaults).length > 0 ? { defaults } : {},
|
|
7739
7977
|
...textSlots.length > 0 ? { textSlots } : {},
|
|
7740
|
-
...dismissName !== void 0 ? { dismissible:
|
|
7978
|
+
...dismissName !== void 0 ? { dismissible: dismissName } : {}
|
|
7741
7979
|
});
|
|
7742
7980
|
const anchorSlug = api.configs.find((c) => Object.keys(c.props).length === 0)?.rep ?? api.configs[0]?.rep;
|
|
7743
|
-
const sentinels = textSlots.filter((slot) => !slot.varies
|
|
7981
|
+
const sentinels = textSlots.filter((slot) => !slot.varies).map((slot) => {
|
|
7744
7982
|
const authored = api.props.find((pr) => pr.kind === "string" && pr.default === slot.default);
|
|
7745
|
-
|
|
7983
|
+
if (authored === void 0) return void 0;
|
|
7984
|
+
if (slot.visibleIn.length > 0) {
|
|
7985
|
+
const entry = {
|
|
7986
|
+
prop: authored.name,
|
|
7987
|
+
config: anchorSlug !== void 0 && slot.visibleIn.includes(anchorSlug) ? anchorSlug : slot.visibleIn[0]
|
|
7988
|
+
};
|
|
7989
|
+
return entry;
|
|
7990
|
+
}
|
|
7991
|
+
if (slot.referencedIn.length > 0) {
|
|
7992
|
+
const toggle = api.props.find(
|
|
7993
|
+
(pr) => pr.kind === "boolean" && authored.name.toLowerCase().startsWith(pr.name.toLowerCase()) && authored.name.length > pr.name.length
|
|
7994
|
+
);
|
|
7995
|
+
if (toggle !== void 0 && anchorSlug !== void 0) {
|
|
7996
|
+
const entry = { prop: authored.name, config: anchorSlug, props: { [toggle.name]: true } };
|
|
7997
|
+
return entry;
|
|
7998
|
+
}
|
|
7999
|
+
}
|
|
8000
|
+
return void 0;
|
|
7746
8001
|
}).filter((x) => x !== void 0);
|
|
7747
8002
|
const { behaviors, prelude, disclosures } = authorBehaviors(api, { ...sentinels.length > 0 ? { sentinels } : {} });
|
|
7748
8003
|
if (dismissName !== void 0) {
|
|
7749
|
-
disclosures.push(`dismiss affordance detected from the recording (
|
|
8004
|
+
disclosures.push(`dismiss affordance detected from the recording (${dismissName}) \u2014 onDismiss authored; dismiss-notifies-and-commits enforces the notification contract`);
|
|
7750
8005
|
}
|
|
7751
8006
|
for (const combo of api.syntheticCombos) {
|
|
7752
8007
|
disclosures.push(`API split created a reachable pose with NO recorded truth: ${combo} (the design's exclusive axis cannot express it) \u2014 composed behavior only, disclosed to consumers`);
|
|
7753
8008
|
}
|
|
7754
8009
|
for (const slot of textSlots) {
|
|
7755
8010
|
if (slot.varies) continue;
|
|
7756
|
-
|
|
8011
|
+
const sentineled = sentinels.some((sn) => api.props.some((pr) => pr.name === sn.prop && pr.default === slot.default));
|
|
8012
|
+
if (sentineled) {
|
|
7757
8013
|
disclosures.push(
|
|
7758
8014
|
`constant content prop (recorded ${JSON.stringify(slot.default)}) is sentinel-checked: a behavior mounts it with a sentinel string and asserts it renders \u2014 hardcoding the recorded literal fails that check`
|
|
7759
8015
|
);
|
|
8016
|
+
} else if (slot.referencedIn.length > 0) {
|
|
8017
|
+
disclosures.push(
|
|
8018
|
+
`content prop (recorded ${JSON.stringify(slot.default)}) is rendered by REFERENCE in the emission but has no visibility evidence and no boolean toggle to reveal it \u2014 sentinel skipped (it could fail honest components whose pose hides the node); wire the prop anyway`
|
|
8019
|
+
);
|
|
7760
8020
|
} else {
|
|
7761
8021
|
disclosures.push(
|
|
7762
8022
|
`content prop from recorded text is UNEXERCISED AND UNRENDERED: NO recorded pose visibly renders ${JSON.stringify(slot.default)} (hidden node) \u2014 prescribed for completeness; wire it behind its visibility toggle; no sentinel can honestly run`
|
|
@@ -7781,7 +8041,7 @@ ALL prose instructions live ABOVE the task payload \u2014 the payload contains o
|
|
|
7781
8041
|
|
|
7782
8042
|
ASSETS: inline the SVG assets you RENDER byte-verbatim, unchanged \u2014 never redraw or approximate an icon. Recorded assets that no scored config displays may be omitted. Two techniques reconcile that rule with reuse (both measured at 1.000): a glyph recorded once but shown in several colors keeps its bytes (fill attribute included) and is repainted with a CSS fill rule \u2014 a CSS declaration outranks an SVG presentation attribute, so one verbatim copy serves every tone; and multi-part glyphs needing fractional placement can nest each verbatim asset as a child <svg x= y=> inside one integer-origin frame (SVG user-space coordinates are exact), an alternative to the transform: scale() pattern.
|
|
7783
8043
|
|
|
7784
|
-
GEOMETRY ARBITRATION (when emission styling and the recorded box disagree, the BOX wins): Figma strokes are INSIDE the box \u2014 border+padding sums that overshoot a recorded dimension mean use an inset box-shadow or subtract the border from the padding. That rule covers strokes AT the box edge only: when a config's reference PNG is LARGER than its recorded box, the recording itself proves an OUTWARD effect \u2014 a hover ring, glow, or shadow past the frame \u2014 and the outward part is drawn outward (box-shadow spread, outline), never forced inside. On such configs the score report carries a "seat" field ({x, y} \u2014 where the recorded box sits inside the larger reference), derived from the recording's own effect geometry (shadow offset/radius/spread) \u2014 informational, so an offset shadow's asymmetric bleed is not misread as a registration error. Following strokes-inside against a padded reference contradicts the recorded pad and cost a measured five configs a full round. The harness mounts each config at its recorded box (width \xD7 height, floors not clamps); build to those dimensions, not to guessed viewports. Known rasterizer delta: Chrome often seats small text ONE PIXEL HIGHER than Figma in an identically sized box. Correct it with a PAINT-ONLY offset on those text runs \u2014 position: relative with top: 1px \u2014 never with padding or margin, which would grow the recorded box this same paragraph calls truth. TREAT IT AS A MEASUREMENT, NOT A RULE: measured cases are 12px/16px and 14px/20px needing the nudge and 14px/18px not, so font size alone does not predict it and neither does any formula we can currently defend.
|
|
8044
|
+
GEOMETRY ARBITRATION (when emission styling and the recorded box disagree, the BOX wins): Figma strokes are INSIDE the box \u2014 border+padding sums that overshoot a recorded dimension mean use an inset box-shadow or subtract the border from the padding. That rule covers strokes AT the box edge only: when a config's reference PNG is LARGER than its recorded box, the recording itself proves an OUTWARD effect \u2014 a hover ring, glow, or shadow past the frame \u2014 and the outward part is drawn outward (box-shadow spread, outline), never forced inside. On such configs the score report carries a "seat" field ({x, y} \u2014 where the recorded box sits inside the larger reference), derived from the recording's own effect geometry (shadow offset/radius/spread) \u2014 informational, so an offset shadow's asymmetric bleed is not misread as a registration error. Following strokes-inside against a padded reference contradicts the recorded pad and cost a measured five configs a full round. The harness mounts each config at its recorded box (width \xD7 height, floors not clamps); build to those dimensions, not to guessed viewports. Known rasterizer delta: Chrome often seats small text ONE PIXEL HIGHER than Figma in an identically sized box. Correct it with a PAINT-ONLY offset on those text runs \u2014 position: relative with top: 1px \u2014 never with padding or margin, which would grow the recorded box this same paragraph calls truth. TREAT IT AS A MEASUREMENT, NOT A RULE: measured cases are 12px/16px and 14px/20px needing the nudge and 14px/18px not, so font size alone does not predict it and neither does any formula we can currently defend. RULE ORDER when both could apply (run 7: a config sat 0.0004 under the bar AND showed uniform spread \u2014 the rules pointed opposite ways): read the DIFF FIRST; the uniform-spread stop-rule below OUTRANKS the try-it rule here. Only when the diff shows a shifted band: apply the nudge, re-score, and keep it only if it helped. Four measured signatures, so do not expect one: on one kit it drove ink recall to 1.000; on another ink was already 1.000 and only similarity moved (mean 0.961 \u2192 0.976, four configs from 0.003 above the bar to 0.028); on a third it REGRESSED a passing bundle from 9/9 to 3/9 configs and had to be reverted; on a fourth it made every FAILING config worse too (tinted in-section surfaces: ink dropped ~0.011 across all eight configs and pushed a passing one under the bar). It is a hypothesis to score, never a default \u2014 NEVER apply it to a bundle that already passes, and when the failing family's diff shows a uniform low-density spread across the glyph band rather than a shifted band, the cause is rasterization weight, not seating: the nudge cannot help and a heavier hypothesis has no recorded truth to justify it \u2014 report the honest sub-bar result instead. Design-token NAMES in the payload are Figma names \u2014 canonicalize to valid CSS idents (lowercase kebab, e.g. "Text/text-primary" \u2192 --text-text-primary) if you emit tokens.css; literal values are equally acceptable. NEVER invent a token to satisfy a lint finding: quality findings are advisory, and a recorded value the kit has no token for is CORRECT as a literal \u2014 a made-up token name corrupts the tokens file as a record of the kit.
|
|
7785
8045
|
|
|
7786
8046
|
RECORDED BOXES ARE TRUTH even when inconsistent: the same string may
|
|
7787
8047
|
have different recorded widths across variants (designer resizing) \u2014 a
|
|
@@ -7869,10 +8129,10 @@ var init_brief = __esm({
|
|
|
7869
8129
|
});
|
|
7870
8130
|
|
|
7871
8131
|
// packages/generate/src/segments.ts
|
|
7872
|
-
import { existsSync as
|
|
7873
|
-
import
|
|
8132
|
+
import { existsSync as existsSync22, readFileSync as readFileSync19, readdirSync as readdirSync6 } from "node:fs";
|
|
8133
|
+
import path28 from "node:path";
|
|
7874
8134
|
function repText(set, rep, tool) {
|
|
7875
|
-
const env = JSON.parse(
|
|
8135
|
+
const env = JSON.parse(readFileSync19(path28.join(set, rep, `${tool}.json`), "utf8"));
|
|
7876
8136
|
return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
|
|
7877
8137
|
}
|
|
7878
8138
|
function stripFigmaInstructions(emission) {
|
|
@@ -7933,17 +8193,17 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
|
|
|
7933
8193
|
function buildSegments(task, mode = "fenced") {
|
|
7934
8194
|
const SET = task.set;
|
|
7935
8195
|
let rawDefs = {};
|
|
7936
|
-
if (
|
|
7937
|
-
const text = envelopeFirstTextPart(JSON.parse(
|
|
8196
|
+
if (existsSync22(path28.join(SET, "get_variable_defs.json"))) {
|
|
8197
|
+
const text = envelopeFirstTextPart(JSON.parse(readFileSync19(path28.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
|
|
7938
8198
|
try {
|
|
7939
8199
|
rawDefs = JSON.parse(text);
|
|
7940
8200
|
} catch {
|
|
7941
8201
|
}
|
|
7942
8202
|
} else {
|
|
7943
8203
|
for (const cfg of task.configs) {
|
|
7944
|
-
const f =
|
|
7945
|
-
if (!
|
|
7946
|
-
const text = envelopeFirstTextPart(JSON.parse(
|
|
8204
|
+
const f = path28.join(SET, cfg.rep, "get_variable_defs.json");
|
|
8205
|
+
if (!existsSync22(f)) continue;
|
|
8206
|
+
const text = envelopeFirstTextPart(JSON.parse(readFileSync19(f, "utf8"))) || "{}";
|
|
7947
8207
|
try {
|
|
7948
8208
|
for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
|
|
7949
8209
|
} catch {
|
|
@@ -7951,8 +8211,8 @@ function buildSegments(task, mode = "fenced") {
|
|
|
7951
8211
|
}
|
|
7952
8212
|
}
|
|
7953
8213
|
const emissionTexts = task.configs.map((cfg) => {
|
|
7954
|
-
const f =
|
|
7955
|
-
return
|
|
8214
|
+
const f = path28.join(SET, cfg.rep, "get_design_context.json");
|
|
8215
|
+
return existsSync22(f) ? envelopeFirstTextPart(JSON.parse(readFileSync19(f, "utf8"))) : "";
|
|
7956
8216
|
});
|
|
7957
8217
|
const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
|
|
7958
8218
|
const defs = JSON.stringify(map, null, 1);
|
|
@@ -7967,9 +8227,9 @@ ${defs}
|
|
|
7967
8227
|
for (const cfg of task.configs) {
|
|
7968
8228
|
const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
|
|
7969
8229
|
const emission = stripFigmaInstructions(repText(SET, cfg.rep, "get_design_context"));
|
|
7970
|
-
const assets =
|
|
8230
|
+
const assets = readdirSync6(path28.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
|
|
7971
8231
|
\`\`\`svg
|
|
7972
|
-
${
|
|
8232
|
+
${readFileSync19(path28.join(SET, cfg.rep, f), "utf8")}
|
|
7973
8233
|
\`\`\``).join("\n");
|
|
7974
8234
|
parts.push(`
|
|
7975
8235
|
## Config ${cfg.rep} \u2192 ${cfg.component} ${JSON.stringify(cfg.props)}
|
|
@@ -8062,8 +8322,8 @@ var init_adapter = __esm({
|
|
|
8062
8322
|
|
|
8063
8323
|
// packages/generate/src/bundle-emit.ts
|
|
8064
8324
|
import { createHash as createHash3 } from "node:crypto";
|
|
8065
|
-
import { existsSync as
|
|
8066
|
-
import
|
|
8325
|
+
import { existsSync as existsSync23, readFileSync as readFileSync20, readdirSync as readdirSync7, writeFileSync as writeFileSync11 } from "node:fs";
|
|
8326
|
+
import path29 from "node:path";
|
|
8067
8327
|
function pinFromConfigs(configs) {
|
|
8068
8328
|
const domains = /* @__PURE__ */ new Map();
|
|
8069
8329
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -8112,22 +8372,22 @@ function cssFontFamilies(css) {
|
|
|
8112
8372
|
return [...out];
|
|
8113
8373
|
}
|
|
8114
8374
|
function countLatticeSymbols(setDir) {
|
|
8115
|
-
const manifestFile =
|
|
8116
|
-
if (
|
|
8375
|
+
const manifestFile = path29.join(setDir, "recording-set.json");
|
|
8376
|
+
if (existsSync23(manifestFile)) {
|
|
8117
8377
|
try {
|
|
8118
|
-
const lattice = JSON.parse(
|
|
8378
|
+
const lattice = JSON.parse(readFileSync20(manifestFile, "utf8")).latticeNames;
|
|
8119
8379
|
if (lattice !== void 0 && lattice.length > 0) return lattice.length;
|
|
8120
8380
|
} catch {
|
|
8121
8381
|
}
|
|
8122
8382
|
}
|
|
8123
8383
|
const files = [
|
|
8124
|
-
|
|
8125
|
-
...
|
|
8126
|
-
].filter((f) =>
|
|
8384
|
+
path29.join(setDir, "get_metadata.json"),
|
|
8385
|
+
...existsSync23(setDir) ? readdirSync7(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path29.join(setDir, f)) : []
|
|
8386
|
+
].filter((f) => existsSync23(f));
|
|
8127
8387
|
if (files.length === 0) return null;
|
|
8128
8388
|
let count = 0;
|
|
8129
8389
|
for (const f of files) {
|
|
8130
|
-
const text = envelopeTextContent(JSON.parse(
|
|
8390
|
+
const text = envelopeTextContent(JSON.parse(readFileSync20(f, "utf8")));
|
|
8131
8391
|
count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
|
|
8132
8392
|
}
|
|
8133
8393
|
return count > 0 ? count : null;
|
|
@@ -8135,21 +8395,21 @@ function countLatticeSymbols(setDir) {
|
|
|
8135
8395
|
function recordingSetHash(setDir, configs) {
|
|
8136
8396
|
const relPaths = [];
|
|
8137
8397
|
for (const name of ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json"]) {
|
|
8138
|
-
if (
|
|
8398
|
+
if (existsSync23(path29.join(setDir, name))) relPaths.push(name);
|
|
8139
8399
|
}
|
|
8140
8400
|
for (const cfg of configs) {
|
|
8141
8401
|
for (const f of ["get_design_context.json", "get_metadata.json", "get_screenshot.json", "get_variable_defs.json"]) {
|
|
8142
|
-
if (
|
|
8402
|
+
if (existsSync23(path29.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
|
|
8143
8403
|
}
|
|
8144
|
-
if (
|
|
8145
|
-
for (const asset of
|
|
8404
|
+
if (existsSync23(path29.join(setDir, cfg.rep))) {
|
|
8405
|
+
for (const asset of readdirSync7(path29.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
|
|
8146
8406
|
relPaths.push(`${cfg.rep}/${asset}`);
|
|
8147
8407
|
}
|
|
8148
8408
|
}
|
|
8149
8409
|
}
|
|
8150
8410
|
return hashRecordingSet(
|
|
8151
8411
|
relPaths,
|
|
8152
|
-
(p) => new Uint8Array(
|
|
8412
|
+
(p) => new Uint8Array(readFileSync20(path29.join(setDir, p))),
|
|
8153
8413
|
(chunks) => {
|
|
8154
8414
|
const h = createHash3("sha256");
|
|
8155
8415
|
for (const c of chunks) h.update(c);
|
|
@@ -8161,14 +8421,14 @@ function statusOf(s) {
|
|
|
8161
8421
|
return tierOf(s, BARS.cert);
|
|
8162
8422
|
}
|
|
8163
8423
|
function emitBundleV1(opts) {
|
|
8164
|
-
const statuses = opts.scores.map((s) => ({ rep: s.rep, similarity: s.similarity, inkRecall: s.inkRecall, status: statusOf(s) }));
|
|
8424
|
+
const statuses = opts.scores.map((s) => ({ rep: s.rep, similarity: s.similarity, inkRecall: s.inkRecall, ...s.exact !== void 0 ? { exact: s.exact } : {}, status: statusOf(s) }));
|
|
8165
8425
|
const certified = statuses.filter((s) => s.status === "certified").length;
|
|
8166
8426
|
const pass = statuses.filter((s) => s.status !== "fail").length;
|
|
8167
8427
|
const lattice = countLatticeSymbols(opts.task.set);
|
|
8168
8428
|
const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:"));
|
|
8169
8429
|
const prelude = opts.behaviors.filter((b) => b.id.startsWith("prelude:"));
|
|
8170
|
-
const cssFiles = ["styles.css", "tokens.css"].map((f) =>
|
|
8171
|
-
const families = cssFontFamilies(cssFiles.map((f) =>
|
|
8430
|
+
const cssFiles = ["styles.css", "tokens.css"].map((f) => path29.join(opts.bundleDir, f)).filter((f) => existsSync23(f));
|
|
8431
|
+
const families = cssFontFamilies(cssFiles.map((f) => readFileSync20(f, "utf8")).join("\n"));
|
|
8172
8432
|
const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
|
|
8173
8433
|
family: f.family,
|
|
8174
8434
|
weight: f.weight,
|
|
@@ -8197,7 +8457,7 @@ function emitBundleV1(opts) {
|
|
|
8197
8457
|
// resolvable via verify's --set override).
|
|
8198
8458
|
path: (() => {
|
|
8199
8459
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
8200
|
-
const rel =
|
|
8460
|
+
const rel = path29.relative(base, opts.task.set);
|
|
8201
8461
|
return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
|
|
8202
8462
|
})(),
|
|
8203
8463
|
component: opts.componentName,
|
|
@@ -8221,16 +8481,16 @@ function emitBundleV1(opts) {
|
|
|
8221
8481
|
})
|
|
8222
8482
|
};
|
|
8223
8483
|
const written = [];
|
|
8224
|
-
const manifestPath2 =
|
|
8225
|
-
|
|
8484
|
+
const manifestPath2 = path29.join(opts.bundleDir, "component.json");
|
|
8485
|
+
writeFileSync11(manifestPath2, `${JSON.stringify(manifest, null, 2)}
|
|
8226
8486
|
`);
|
|
8227
8487
|
written.push(manifestPath2);
|
|
8228
|
-
const stylesPath =
|
|
8229
|
-
if (
|
|
8488
|
+
const stylesPath = path29.join(opts.bundleDir, "styles.css");
|
|
8489
|
+
if (existsSync23(stylesPath)) {
|
|
8230
8490
|
const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
|
|
8231
|
-
const current =
|
|
8491
|
+
const current = readFileSync20(stylesPath, "utf8");
|
|
8232
8492
|
const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
|
|
8233
|
-
|
|
8493
|
+
writeFileSync11(stylesPath, `${comment}
|
|
8234
8494
|
${stripped}`);
|
|
8235
8495
|
written.push(stylesPath);
|
|
8236
8496
|
}
|
|
@@ -8278,8 +8538,8 @@ __export(fonts_exports, {
|
|
|
8278
8538
|
runFontsResolveSet: () => runFontsResolveSet,
|
|
8279
8539
|
runFontsStatus: () => runFontsStatus
|
|
8280
8540
|
});
|
|
8281
|
-
import { existsSync as
|
|
8282
|
-
import
|
|
8541
|
+
import { existsSync as existsSync24, readFileSync as readFileSync21 } from "node:fs";
|
|
8542
|
+
import path30 from "node:path";
|
|
8283
8543
|
async function runFontsResolve(opts) {
|
|
8284
8544
|
const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
|
|
8285
8545
|
emitData(opts, result, () => {
|
|
@@ -8294,7 +8554,7 @@ async function runFontsResolve(opts) {
|
|
|
8294
8554
|
}
|
|
8295
8555
|
}
|
|
8296
8556
|
async function runFontsResolveSet(opts) {
|
|
8297
|
-
const setDir =
|
|
8557
|
+
const setDir = path30.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
|
|
8298
8558
|
let needs = [];
|
|
8299
8559
|
try {
|
|
8300
8560
|
needs = recordedFontNeeds(setDir);
|
|
@@ -8343,16 +8603,16 @@ async function runFontsResolveSet(opts) {
|
|
|
8343
8603
|
}
|
|
8344
8604
|
}
|
|
8345
8605
|
function runFontsStatus(opts) {
|
|
8346
|
-
const manifestPath2 =
|
|
8347
|
-
if (!
|
|
8606
|
+
const manifestPath2 = path30.join(opts.cacheDir, "manifest.json");
|
|
8607
|
+
if (!existsSync24(manifestPath2)) {
|
|
8348
8608
|
fail(opts, ExitCode.FontsUnproven, {
|
|
8349
8609
|
error: `no font cache at ${opts.cacheDir}`,
|
|
8350
8610
|
code: "fonts-unresolved",
|
|
8351
8611
|
remediation: 'Run `tendril fonts resolve --set <recording-dir>` (or `tendril fonts resolve "<Family>" --weights 400 500 600`) first.'
|
|
8352
8612
|
});
|
|
8353
8613
|
}
|
|
8354
|
-
const faces = JSON.parse(
|
|
8355
|
-
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(
|
|
8614
|
+
const faces = JSON.parse(readFileSync21(manifestPath2, "utf8"));
|
|
8615
|
+
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path30.resolve(opts.lock), opts.cacheDir) : null;
|
|
8356
8616
|
emitData(opts, { faces, lockVerdicts }, () => {
|
|
8357
8617
|
for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
|
|
8358
8618
|
`);
|
|
@@ -8437,8 +8697,8 @@ __export(verify_exports, {
|
|
|
8437
8697
|
interactionCoverage: () => interactionCoverage,
|
|
8438
8698
|
runVerify: () => runVerify
|
|
8439
8699
|
});
|
|
8440
|
-
import { existsSync as
|
|
8441
|
-
import
|
|
8700
|
+
import { existsSync as existsSync25, readFileSync as readFileSync22 } from "node:fs";
|
|
8701
|
+
import path31 from "node:path";
|
|
8442
8702
|
function interactionCoverage(behaviors) {
|
|
8443
8703
|
const interaction = behaviors.filter((b) => !b.id.startsWith("prelude:"));
|
|
8444
8704
|
return {
|
|
@@ -8453,7 +8713,7 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
8453
8713
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
8454
8714
|
const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
8455
8715
|
const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
|
|
8456
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
8716
|
+
const registry = Object.values(TASKS).find((t) => path31.resolve(t.set) === path31.resolve(setDir));
|
|
8457
8717
|
const authored = (() => {
|
|
8458
8718
|
if (registry !== void 0) return void 0;
|
|
8459
8719
|
try {
|
|
@@ -8485,19 +8745,19 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
8485
8745
|
}
|
|
8486
8746
|
async function runVerify(opts) {
|
|
8487
8747
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
8488
|
-
const setOverride = opts.set !== void 0 ?
|
|
8489
|
-
opts = { ...opts, bundleDir:
|
|
8490
|
-
if (!
|
|
8748
|
+
const setOverride = opts.set !== void 0 ? path31.resolve(callerCwd, opts.set) : void 0;
|
|
8749
|
+
opts = { ...opts, bundleDir: path31.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
|
|
8750
|
+
if (!existsSync25(opts.bundleDir)) {
|
|
8491
8751
|
fail(opts, ExitCode.InputValidation, {
|
|
8492
8752
|
error: `bundle directory not found: ${opts.bundleDir}`,
|
|
8493
8753
|
code: "bundle-missing",
|
|
8494
8754
|
remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
|
|
8495
8755
|
});
|
|
8496
8756
|
}
|
|
8497
|
-
const manifestPath2 =
|
|
8757
|
+
const manifestPath2 = path31.join(opts.bundleDir, "component.json");
|
|
8498
8758
|
let manifest;
|
|
8499
|
-
if (
|
|
8500
|
-
const { manifest: parsed, issues } = readBundleManifest(
|
|
8759
|
+
if (existsSync25(manifestPath2)) {
|
|
8760
|
+
const { manifest: parsed, issues } = readBundleManifest(readFileSync22(manifestPath2, "utf8"));
|
|
8501
8761
|
if (issues.length > 0) {
|
|
8502
8762
|
fail(opts, ExitCode.InputValidation, {
|
|
8503
8763
|
error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -8524,21 +8784,21 @@ async function runVerify(opts) {
|
|
|
8524
8784
|
task = registry;
|
|
8525
8785
|
} else if (manifest !== void 0) {
|
|
8526
8786
|
const resolveSetDir = (p) => {
|
|
8527
|
-
if (
|
|
8528
|
-
const fromRepo =
|
|
8529
|
-
if (
|
|
8530
|
-
return
|
|
8787
|
+
if (path31.isAbsolute(p)) return p;
|
|
8788
|
+
const fromRepo = path31.resolve(REPO_ROOT, p);
|
|
8789
|
+
if (existsSync25(fromRepo)) return fromRepo;
|
|
8790
|
+
return path31.resolve(callerCwd, p);
|
|
8531
8791
|
};
|
|
8532
8792
|
const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
|
|
8533
|
-
if (!
|
|
8793
|
+
if (!existsSync25(path31.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path31.resolve(t.set) === path31.resolve(setDir))) {
|
|
8534
8794
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
8535
8795
|
error: `recording set not found or unmanifested: ${setDir}`,
|
|
8536
8796
|
code: "recording-set-missing",
|
|
8537
8797
|
remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
|
|
8538
8798
|
});
|
|
8539
8799
|
}
|
|
8540
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
8541
|
-
if (registry !== void 0 && !
|
|
8800
|
+
const registry = Object.values(TASKS).find((t) => path31.resolve(t.set) === path31.resolve(setDir));
|
|
8801
|
+
if (registry !== void 0 && !existsSync25(path31.join(setDir, "recording-set.json"))) {
|
|
8542
8802
|
const recordedSlugs = registry.configs.map((c) => c.rep);
|
|
8543
8803
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
8544
8804
|
unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
@@ -8562,9 +8822,9 @@ async function runVerify(opts) {
|
|
|
8562
8822
|
warn(opts, `recording set content differs from the bundle's provenance stamp (${hash.slice(0, 12)}\u2026 vs ${manifest.provenance.recordingSet.hash.slice(0, 12)}\u2026) \u2014 scores apply to the CURRENT set`);
|
|
8563
8823
|
}
|
|
8564
8824
|
for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
|
|
8565
|
-
const p =
|
|
8566
|
-
if (!
|
|
8567
|
-
const issues = checkBundleSourceFile(name, new Uint8Array(
|
|
8825
|
+
const p = path31.join(opts.bundleDir, name);
|
|
8826
|
+
if (!existsSync25(p)) continue;
|
|
8827
|
+
const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync22(p)));
|
|
8568
8828
|
if (issues.length > 0) {
|
|
8569
8829
|
fail(opts, ExitCode.InputValidation, {
|
|
8570
8830
|
error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -8588,7 +8848,7 @@ async function runVerify(opts) {
|
|
|
8588
8848
|
});
|
|
8589
8849
|
}
|
|
8590
8850
|
const missing = task.configs.filter(
|
|
8591
|
-
(c) => !
|
|
8851
|
+
(c) => !existsSync25(path31.join(task.set, c.rep, "get_screenshot.json")) || !existsSync25(path31.join(task.set, c.rep, "get_metadata.json"))
|
|
8592
8852
|
);
|
|
8593
8853
|
if (missing.length > 0) {
|
|
8594
8854
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -8598,10 +8858,10 @@ async function runVerify(opts) {
|
|
|
8598
8858
|
});
|
|
8599
8859
|
}
|
|
8600
8860
|
const bar = BARS2[opts.bar];
|
|
8601
|
-
const evidenceDir =
|
|
8861
|
+
const evidenceDir = path31.join(opts.bundleDir, "verify-evidence");
|
|
8602
8862
|
const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
8603
8863
|
const quality = await checkBundleQuality(opts.bundleDir, task.entry);
|
|
8604
|
-
const bundleCss = ["tokens.css", "styles.css"].map((f) =>
|
|
8864
|
+
const bundleCss = ["tokens.css", "styles.css"].map((f) => path31.join(opts.bundleDir, f)).filter((f) => existsSync25(f)).map((f) => readFileSync22(f, "utf8")).join("\n");
|
|
8605
8865
|
const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
|
|
8606
8866
|
const parity = await checkHoverParity(task, opts.bundleDir);
|
|
8607
8867
|
const behaviors = [...await checkBehaviors(task, opts.bundleDir), ...parity];
|
|
@@ -8818,18 +9078,18 @@ __export(engine_exports, {
|
|
|
8818
9078
|
runEngineBrief: () => runEngineBrief,
|
|
8819
9079
|
runEngineScore: () => runEngineScore
|
|
8820
9080
|
});
|
|
8821
|
-
import { existsSync as
|
|
8822
|
-
import
|
|
9081
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync7, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
|
|
9082
|
+
import path32 from "node:path";
|
|
8823
9083
|
function resolveEngineTask(opts, callerCwd) {
|
|
8824
|
-
const asPath =
|
|
8825
|
-
const isSet =
|
|
9084
|
+
const asPath = path32.resolve(callerCwd, opts.taskOrSet);
|
|
9085
|
+
const isSet = existsSync26(path32.join(asPath, "recording-set.json"));
|
|
8826
9086
|
const registry = TASKS[opts.taskOrSet];
|
|
8827
9087
|
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet };
|
|
8828
9088
|
if (isSet) {
|
|
8829
9089
|
try {
|
|
8830
9090
|
const authored = authorTaskFromSet(asPath);
|
|
8831
9091
|
for (const d of authored.disclosures) warn(opts, d);
|
|
8832
|
-
return { task: authored.task, name:
|
|
9092
|
+
return { task: authored.task, name: path32.basename(asPath), apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
|
|
8833
9093
|
} catch (err) {
|
|
8834
9094
|
fail(opts, ExitCode.InputValidation, {
|
|
8835
9095
|
error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -8845,15 +9105,16 @@ function resolveEngineTask(opts, callerCwd) {
|
|
|
8845
9105
|
});
|
|
8846
9106
|
}
|
|
8847
9107
|
function runEngineBrief(opts) {
|
|
9108
|
+
requireEntitlement(opts);
|
|
8848
9109
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
8849
9110
|
const { task, name } = resolveEngineTask(opts, callerCwd);
|
|
8850
9111
|
const bar = BARS3[opts.bar];
|
|
8851
9112
|
const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" });
|
|
8852
9113
|
const segments = buildSegments(task, "files");
|
|
8853
9114
|
let notRecorded;
|
|
8854
|
-
const manifestPath2 =
|
|
8855
|
-
if (
|
|
8856
|
-
notRecorded = JSON.parse(
|
|
9115
|
+
const manifestPath2 = path32.join(task.set, "recording-set.json");
|
|
9116
|
+
if (existsSync26(manifestPath2)) {
|
|
9117
|
+
notRecorded = JSON.parse(readFileSync23(manifestPath2, "utf8")).notRecorded;
|
|
8857
9118
|
}
|
|
8858
9119
|
const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
|
|
8859
9120
|
|
|
@@ -8861,7 +9122,7 @@ function runEngineBrief(opts) {
|
|
|
8861
9122
|
DO NOT INVENT PIXELS FOR THESE POSES. Implement them ONLY as the composition of recorded per-axis truth (the custom-property composition rule: one paint axis sets variables, the other consumes) and list every such pose in your report as UNRECORDED-COMPOSED. Recording them is the real fix: re-plan the set (full matrix is the default) and record the missing poses.
|
|
8862
9123
|
${notRecorded}` : "";
|
|
8863
9124
|
let fontProvisioning;
|
|
8864
|
-
if (
|
|
9125
|
+
if (existsSync26(manifestPath2)) {
|
|
8865
9126
|
const provided = resolvedFontFamilies().map((f) => f.family);
|
|
8866
9127
|
const unprovided = recordedFontFamilies(task.set).filter((r) => !provided.some((p) => p.toLowerCase() === r.toLowerCase()));
|
|
8867
9128
|
if (unprovided.length > 0) {
|
|
@@ -8883,9 +9144,9 @@ ${notRecorded}` : "";
|
|
|
8883
9144
|
|
|
8884
9145
|
=== TASK PAYLOAD (recorded truth, verbatim) ===
|
|
8885
9146
|
${segments}`;
|
|
8886
|
-
const payloadFile =
|
|
8887
|
-
|
|
8888
|
-
|
|
9147
|
+
const payloadFile = path32.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
|
|
9148
|
+
mkdirSync7(path32.dirname(payloadFile), { recursive: true });
|
|
9149
|
+
writeFileSync12(payloadFile, payload);
|
|
8889
9150
|
emitData(
|
|
8890
9151
|
opts,
|
|
8891
9152
|
{
|
|
@@ -8927,10 +9188,11 @@ ${segments}`;
|
|
|
8927
9188
|
);
|
|
8928
9189
|
}
|
|
8929
9190
|
async function runEngineScore(opts) {
|
|
9191
|
+
requireEntitlement(opts);
|
|
8930
9192
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
8931
|
-
const candidateDir =
|
|
9193
|
+
const candidateDir = path32.resolve(callerCwd, opts.candidateDir);
|
|
8932
9194
|
const { task, name, apiPin } = resolveEngineTask(opts, callerCwd);
|
|
8933
|
-
if (!
|
|
9195
|
+
if (!existsSync26(candidateDir)) {
|
|
8934
9196
|
fail(opts, ExitCode.InputValidation, {
|
|
8935
9197
|
error: `candidate directory not found: ${candidateDir}`,
|
|
8936
9198
|
code: "candidate-missing",
|
|
@@ -8944,10 +9206,10 @@ async function runEngineScore(opts) {
|
|
|
8944
9206
|
remediation: fontsUnprovenRemediation(task.set)
|
|
8945
9207
|
});
|
|
8946
9208
|
}
|
|
8947
|
-
if (opts.rebind !== true &&
|
|
9209
|
+
if (opts.rebind !== true && existsSync26(path32.join(candidateDir, "component.json"))) {
|
|
8948
9210
|
const prior = (() => {
|
|
8949
9211
|
try {
|
|
8950
|
-
const read = readBundleManifest(
|
|
9212
|
+
const read = readBundleManifest(readFileSync23(path32.join(candidateDir, "component.json"), "utf8"));
|
|
8951
9213
|
return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
|
|
8952
9214
|
} catch {
|
|
8953
9215
|
return { unreadable: true };
|
|
@@ -8969,7 +9231,7 @@ async function runEngineScore(opts) {
|
|
|
8969
9231
|
}
|
|
8970
9232
|
}
|
|
8971
9233
|
const bar = BARS3[opts.bar];
|
|
8972
|
-
const evidenceDir =
|
|
9234
|
+
const evidenceDir = path32.join(candidateDir, "verify-evidence");
|
|
8973
9235
|
const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
8974
9236
|
const parity = await checkHoverParity(task, candidateDir);
|
|
8975
9237
|
const behaviors = [...await checkBehaviors(task, candidateDir), ...parity];
|
|
@@ -8984,11 +9246,16 @@ ${[
|
|
|
8984
9246
|
...quality.findings.map((f) => `- ${f.kind} ${f.file}${f.line === void 0 ? "" : `:${f.line}`} \u2014 ${f.message}`),
|
|
8985
9247
|
...quality.tokensAbsent ? ["- no design tokens: no tokens.css and no var(--\u2026) reference; every value is hardcoded"] : []
|
|
8986
9248
|
].join("\n")}`;
|
|
8987
|
-
const feedback = buildFeedback(scores, behaviors, bar, "files") + qualityFeedback;
|
|
8988
9249
|
const allPass = obj[0] === total && total > 0;
|
|
8989
9250
|
const certBar = BARS3["cert"];
|
|
8990
|
-
const
|
|
9251
|
+
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);
|
|
8991
9253
|
const certifiedSet = new Set(certifiedReps);
|
|
9254
|
+
const certificationFeedback = `
|
|
9255
|
+
|
|
9256
|
+
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
|
+
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.`;
|
|
9258
|
+
const feedback = buildFeedback(scores, behaviors, bar, "files") + certificationFeedback + qualityFeedback;
|
|
8992
9259
|
const emitted = emitBundleV1({
|
|
8993
9260
|
bundleDir: candidateDir,
|
|
8994
9261
|
task,
|
|
@@ -9019,7 +9286,7 @@ ${[
|
|
|
9019
9286
|
bundleManifest: emitted.written[0],
|
|
9020
9287
|
note: "styles.css was stamped with the bundle provenance comment on line 1 \u2014 preserve it in any post-score edit",
|
|
9021
9288
|
allPass,
|
|
9022
|
-
certification: { certified: certifiedReps.length, total, bar: certBar }
|
|
9289
|
+
certification: { certified: certifiedReps.length, total: scores.length, bar: certBar, note: "tierOf on exact values + parity demotion; verify's composition checks can demote further" }
|
|
9023
9290
|
},
|
|
9024
9291
|
() => {
|
|
9025
9292
|
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}]` : ""}
|
|
@@ -9049,7 +9316,9 @@ var init_engine2 = __esm({
|
|
|
9049
9316
|
init_font_guidance();
|
|
9050
9317
|
init_src4();
|
|
9051
9318
|
init_output();
|
|
9319
|
+
init_entitlement();
|
|
9052
9320
|
init_verify();
|
|
9321
|
+
init_src4();
|
|
9053
9322
|
BARS3 = {
|
|
9054
9323
|
pass: { sim: 0.95, ink: 0.95 },
|
|
9055
9324
|
cert: { sim: 0.97, ink: 0.95 }
|
|
@@ -9062,11 +9331,11 @@ var codeconnect_exports = {};
|
|
|
9062
9331
|
__export(codeconnect_exports, {
|
|
9063
9332
|
runCodeConnect: () => runCodeConnect
|
|
9064
9333
|
});
|
|
9065
|
-
import { existsSync as
|
|
9066
|
-
import
|
|
9334
|
+
import { existsSync as existsSync27, readFileSync as readFileSync24, writeFileSync as writeFileSync13 } from "node:fs";
|
|
9335
|
+
import path33 from "node:path";
|
|
9067
9336
|
function runCodeConnect(opts) {
|
|
9068
9337
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
9069
|
-
const bundleDir =
|
|
9338
|
+
const bundleDir = path33.resolve(callerCwd, opts.bundleDir);
|
|
9070
9339
|
let url;
|
|
9071
9340
|
try {
|
|
9072
9341
|
url = new URL(opts.figmaUrl);
|
|
@@ -9082,7 +9351,7 @@ function runCodeConnect(opts) {
|
|
|
9082
9351
|
}
|
|
9083
9352
|
let manifest;
|
|
9084
9353
|
try {
|
|
9085
|
-
const read = readBundleManifest(
|
|
9354
|
+
const read = readBundleManifest(readFileSync24(path33.join(bundleDir, "component.json"), "utf8"));
|
|
9086
9355
|
if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
|
|
9087
9356
|
manifest = read.manifest;
|
|
9088
9357
|
} catch (err) {
|
|
@@ -9092,8 +9361,8 @@ function runCodeConnect(opts) {
|
|
|
9092
9361
|
remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
|
|
9093
9362
|
});
|
|
9094
9363
|
}
|
|
9095
|
-
const setDir =
|
|
9096
|
-
if (!
|
|
9364
|
+
const setDir = path33.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
|
|
9365
|
+
if (!existsSync27(path33.join(setDir, "recording-set.json"))) {
|
|
9097
9366
|
fail(opts, ExitCode.InputValidation, {
|
|
9098
9367
|
error: `recording set not found at ${setDir}`,
|
|
9099
9368
|
code: "codeconnect-no-set",
|
|
@@ -9114,10 +9383,10 @@ function runCodeConnect(opts) {
|
|
|
9114
9383
|
const component = api.component;
|
|
9115
9384
|
const recManifest = loadManifest(setDir);
|
|
9116
9385
|
const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
|
|
9117
|
-
const meta =
|
|
9118
|
-
if (!
|
|
9386
|
+
const meta = path33.join(setDir, r.slug, "get_metadata.json");
|
|
9387
|
+
if (!existsSync27(meta)) return void 0;
|
|
9119
9388
|
try {
|
|
9120
|
-
return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(
|
|
9389
|
+
return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(readFileSync24(meta, "utf8"))))?.[1];
|
|
9121
9390
|
} catch {
|
|
9122
9391
|
return void 0;
|
|
9123
9392
|
}
|
|
@@ -9175,7 +9444,7 @@ function runCodeConnect(opts) {
|
|
|
9175
9444
|
axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
|
|
9176
9445
|
fragmentVars.push(varName);
|
|
9177
9446
|
}
|
|
9178
|
-
const entryRel =
|
|
9447
|
+
const entryRel = path33.relative(callerCwd, path33.join(bundleDir, manifest.entry));
|
|
9179
9448
|
const trust = manifest.trustStatement.split("\n")[0] ?? "";
|
|
9180
9449
|
const lines = [
|
|
9181
9450
|
`// url=${opts.figmaUrl}`,
|
|
@@ -9196,8 +9465,8 @@ function runCodeConnect(opts) {
|
|
|
9196
9465
|
`}`,
|
|
9197
9466
|
``
|
|
9198
9467
|
].join("\n");
|
|
9199
|
-
const outFile =
|
|
9200
|
-
|
|
9468
|
+
const outFile = path33.resolve(callerCwd, opts.out ?? path33.join(bundleDir, `${component}.figma.ts`));
|
|
9469
|
+
writeFileSync13(outFile, lines);
|
|
9201
9470
|
emitData(
|
|
9202
9471
|
opts,
|
|
9203
9472
|
{
|
|
@@ -9257,17 +9526,17 @@ __export(generate_recorded_exports, {
|
|
|
9257
9526
|
runGenerateRecorded: () => runGenerateRecorded
|
|
9258
9527
|
});
|
|
9259
9528
|
import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
|
|
9260
|
-
import { existsSync as
|
|
9261
|
-
import
|
|
9529
|
+
import { existsSync as existsSync28, readFileSync as readFileSync25 } from "node:fs";
|
|
9530
|
+
import path34 from "node:path";
|
|
9262
9531
|
async function runGenerateRecorded(opts) {
|
|
9263
9532
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
9264
|
-
const outDirAbs =
|
|
9265
|
-
const recordedAsPath =
|
|
9533
|
+
const outDirAbs = path34.resolve(callerCwd, opts.out);
|
|
9534
|
+
const recordedAsPath = path34.resolve(callerCwd, opts.recorded);
|
|
9266
9535
|
let task;
|
|
9267
9536
|
let taskName;
|
|
9268
9537
|
let authoredApi;
|
|
9269
9538
|
let composition;
|
|
9270
|
-
const isSet =
|
|
9539
|
+
const isSet = existsSync28(path34.join(recordedAsPath, "recording-set.json"));
|
|
9271
9540
|
const registry = TASKS[opts.recorded];
|
|
9272
9541
|
if (registry !== void 0 && !isSet) {
|
|
9273
9542
|
task = registry;
|
|
@@ -9276,7 +9545,7 @@ async function runGenerateRecorded(opts) {
|
|
|
9276
9545
|
try {
|
|
9277
9546
|
const authored = authorTaskFromSet(recordedAsPath);
|
|
9278
9547
|
task = authored.task;
|
|
9279
|
-
taskName =
|
|
9548
|
+
taskName = path34.basename(recordedAsPath);
|
|
9280
9549
|
authoredApi = authored.api;
|
|
9281
9550
|
const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
|
|
9282
9551
|
if (roles.success) composition = roles.data;
|
|
@@ -9303,7 +9572,7 @@ async function runGenerateRecorded(opts) {
|
|
|
9303
9572
|
});
|
|
9304
9573
|
}
|
|
9305
9574
|
const missing = task.configs.filter(
|
|
9306
|
-
(c) => !
|
|
9575
|
+
(c) => !existsSync28(path34.join(task.set, c.rep, "get_screenshot.json")) || !existsSync28(path34.join(task.set, c.rep, "get_metadata.json")) || !existsSync28(path34.join(task.set, c.rep, "get_design_context.json"))
|
|
9307
9576
|
);
|
|
9308
9577
|
if (missing.length > 0) {
|
|
9309
9578
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -9373,8 +9642,8 @@ async function runGenerateRecorded(opts) {
|
|
|
9373
9642
|
` : `${line}
|
|
9374
9643
|
`);
|
|
9375
9644
|
if (opts.dryRun) {
|
|
9376
|
-
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite:
|
|
9377
|
-
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${
|
|
9645
|
+
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path34.join(outDirAbs, taskName) }, () => {
|
|
9646
|
+
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path34.join(outDirAbs, taskName)})
|
|
9378
9647
|
`);
|
|
9379
9648
|
});
|
|
9380
9649
|
return;
|
|
@@ -9397,10 +9666,10 @@ async function runGenerateRecorded(opts) {
|
|
|
9397
9666
|
});
|
|
9398
9667
|
}
|
|
9399
9668
|
}
|
|
9400
|
-
const bundleDir =
|
|
9401
|
-
if (
|
|
9669
|
+
const bundleDir = path34.join(outDirAbs, taskName);
|
|
9670
|
+
if (existsSync28(path34.join(bundleDir, "component.json"))) {
|
|
9402
9671
|
try {
|
|
9403
|
-
const prior = readBundleManifest(
|
|
9672
|
+
const prior = readBundleManifest(readFileSync25(path34.join(bundleDir, "component.json"), "utf8")).manifest;
|
|
9404
9673
|
if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
|
|
9405
9674
|
fail(opts, ExitCode.InputValidation, {
|
|
9406
9675
|
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`,
|
|
@@ -9536,8 +9805,9 @@ import { Command } from "commander";
|
|
|
9536
9805
|
// packages/cli/src/commands/doctor.ts
|
|
9537
9806
|
init_src4();
|
|
9538
9807
|
init_src();
|
|
9539
|
-
import { existsSync as
|
|
9540
|
-
import
|
|
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";
|
|
9541
9811
|
|
|
9542
9812
|
// packages/cli/src/describe.ts
|
|
9543
9813
|
var COMMON_EXIT_CODES = {
|
|
@@ -9556,6 +9826,7 @@ function printDescription(description) {
|
|
|
9556
9826
|
init_env();
|
|
9557
9827
|
init_environment();
|
|
9558
9828
|
init_output();
|
|
9829
|
+
init_entitlement();
|
|
9559
9830
|
var DEFAULT_MCP_URL = "http://127.0.0.1:3845/mcp";
|
|
9560
9831
|
var DOCTOR_DESCRIPTION = {
|
|
9561
9832
|
name: "doctor",
|
|
@@ -9615,15 +9886,38 @@ async function runDoctorChecks(options) {
|
|
|
9615
9886
|
remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
|
|
9616
9887
|
});
|
|
9617
9888
|
}
|
|
9618
|
-
const fontManifest =
|
|
9889
|
+
const fontManifest = path22.join(fontCacheDir(), "manifest.json");
|
|
9619
9890
|
checks.push(
|
|
9620
|
-
|
|
9891
|
+
existsSync17(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync14(fontManifest, "utf8")).length} faces)` } : {
|
|
9621
9892
|
name: "font-cache",
|
|
9622
9893
|
ok: true,
|
|
9623
9894
|
detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
|
|
9624
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."
|
|
9625
9896
|
}
|
|
9626
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
|
+
);
|
|
9627
9921
|
const figmaToken = resolveCredential("FIGMA_TOKEN");
|
|
9628
9922
|
checks.push({
|
|
9629
9923
|
name: "figma-pat",
|
|
@@ -9692,7 +9986,7 @@ async function runDoctor(flags) {
|
|
|
9692
9986
|
init_src3();
|
|
9693
9987
|
import { intro, isCancel, outro, password } from "@clack/prompts";
|
|
9694
9988
|
import fs from "node:fs";
|
|
9695
|
-
import
|
|
9989
|
+
import path23 from "node:path";
|
|
9696
9990
|
init_env();
|
|
9697
9991
|
init_output();
|
|
9698
9992
|
var INIT_DESCRIPTION = {
|
|
@@ -9731,7 +10025,7 @@ async function runInit(flags) {
|
|
|
9731
10025
|
printDescription(INIT_DESCRIPTION);
|
|
9732
10026
|
return;
|
|
9733
10027
|
}
|
|
9734
|
-
const envPath =
|
|
10028
|
+
const envPath = path23.resolve(process.cwd(), ".env");
|
|
9735
10029
|
const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
|
|
9736
10030
|
let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
|
|
9737
10031
|
let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
|
|
@@ -9752,7 +10046,7 @@ async function runInit(flags) {
|
|
|
9752
10046
|
next.set(ENV_KEYS.figma, figmaToken);
|
|
9753
10047
|
next.set(ENV_KEYS.openrouter, openrouterKey);
|
|
9754
10048
|
const changed = existing.get(ENV_KEYS.figma) !== next.get(ENV_KEYS.figma) || existing.get(ENV_KEYS.openrouter) !== next.get(ENV_KEYS.openrouter);
|
|
9755
|
-
const gitignorePath =
|
|
10049
|
+
const gitignorePath = path23.resolve(process.cwd(), ".gitignore");
|
|
9756
10050
|
const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
9757
10051
|
const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
|
|
9758
10052
|
if (flags.dryRun) {
|
|
@@ -9803,16 +10097,17 @@ init_src();
|
|
|
9803
10097
|
init_src5();
|
|
9804
10098
|
init_src2();
|
|
9805
10099
|
import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
|
|
9806
|
-
import { readFileSync as
|
|
10100
|
+
import { readFileSync as readFileSync15, readdirSync as readdirSync4, existsSync as existsSync18 } from "node:fs";
|
|
9807
10101
|
init_env();
|
|
9808
10102
|
init_output();
|
|
10103
|
+
init_entitlement();
|
|
9809
10104
|
|
|
9810
10105
|
// packages/cli/src/pipeline.ts
|
|
9811
10106
|
init_src2();
|
|
9812
10107
|
init_src4();
|
|
9813
10108
|
init_src6();
|
|
9814
|
-
import { mkdirSync as
|
|
9815
|
-
import
|
|
10109
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
10110
|
+
import path24 from "node:path";
|
|
9816
10111
|
|
|
9817
10112
|
// packages/cli/src/assets-module.ts
|
|
9818
10113
|
init_src();
|
|
@@ -10148,8 +10443,8 @@ async function runGenerationPipeline(input) {
|
|
|
10148
10443
|
});
|
|
10149
10444
|
const written = [];
|
|
10150
10445
|
if (!input.dryRun) {
|
|
10151
|
-
const dir =
|
|
10152
|
-
|
|
10446
|
+
const dir = path24.resolve(input.outDir, semantics.componentName);
|
|
10447
|
+
mkdirSync5(dir, { recursive: true });
|
|
10153
10448
|
const files = {
|
|
10154
10449
|
// Bundle-local tokens: THE emission the component's CSS resolves
|
|
10155
10450
|
// against — the same artifact the verify harness injects. A preview
|
|
@@ -10172,14 +10467,14 @@ async function runGenerationPipeline(input) {
|
|
|
10172
10467
|
`
|
|
10173
10468
|
};
|
|
10174
10469
|
for (const [name, content] of Object.entries(files)) {
|
|
10175
|
-
const filePath =
|
|
10176
|
-
|
|
10470
|
+
const filePath = path24.join(dir, name);
|
|
10471
|
+
writeFileSync8(filePath, content);
|
|
10177
10472
|
written.push(filePath);
|
|
10178
10473
|
}
|
|
10179
10474
|
for (const artifact of emitTokenArtifacts(input.mapping)) {
|
|
10180
|
-
const filePath =
|
|
10181
|
-
|
|
10182
|
-
|
|
10475
|
+
const filePath = path24.resolve(input.outDir, artifact.path);
|
|
10476
|
+
mkdirSync5(path24.dirname(filePath), { recursive: true });
|
|
10477
|
+
writeFileSync8(filePath, artifact.content);
|
|
10183
10478
|
written.push(filePath);
|
|
10184
10479
|
}
|
|
10185
10480
|
}
|
|
@@ -10237,7 +10532,7 @@ var GENERATE_DESCRIPTION = {
|
|
|
10237
10532
|
function resolveProvidedSource(flags, contextFile) {
|
|
10238
10533
|
let raw;
|
|
10239
10534
|
try {
|
|
10240
|
-
raw =
|
|
10535
|
+
raw = readFileSync15(contextFile, "utf8");
|
|
10241
10536
|
} catch {
|
|
10242
10537
|
fail(flags, ExitCode.InputValidation, {
|
|
10243
10538
|
error: `Cannot read context file "${contextFile}".`,
|
|
@@ -10271,6 +10566,7 @@ function resolveSource(flags, url) {
|
|
|
10271
10566
|
});
|
|
10272
10567
|
}
|
|
10273
10568
|
async function runGenerate(url, flags) {
|
|
10569
|
+
requireEntitlement(flags);
|
|
10274
10570
|
if (flags.describe) {
|
|
10275
10571
|
printDescription(GENERATE_DESCRIPTION);
|
|
10276
10572
|
return;
|
|
@@ -10356,11 +10652,11 @@ token mapping (${mapping.flat.length} variables):
|
|
|
10356
10652
|
let initialCode;
|
|
10357
10653
|
let initialSemantics;
|
|
10358
10654
|
try {
|
|
10359
|
-
if (
|
|
10360
|
-
for (const entry of
|
|
10655
|
+
if (existsSync18(flags.out)) {
|
|
10656
|
+
for (const entry of readdirSync4(flags.out)) {
|
|
10361
10657
|
const cjPath = `${flags.out}/${entry}/component.json`;
|
|
10362
|
-
if (!
|
|
10363
|
-
const cj = JSON.parse(
|
|
10658
|
+
if (!existsSync18(cjPath)) continue;
|
|
10659
|
+
const cj = JSON.parse(readFileSync15(cjPath, "utf8"));
|
|
10364
10660
|
if (cj.name !== void 0 && Array.isArray(cj.props)) {
|
|
10365
10661
|
previousApi = JSON.stringify({
|
|
10366
10662
|
componentName: cj.name,
|
|
@@ -10368,14 +10664,14 @@ token mapping (${mapping.flat.length} variables):
|
|
|
10368
10664
|
});
|
|
10369
10665
|
const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
|
|
10370
10666
|
const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
|
|
10371
|
-
if (flags.refine &&
|
|
10667
|
+
if (flags.refine && existsSync18(tsxPath) && existsSync18(cssPath)) {
|
|
10372
10668
|
initialCode = {
|
|
10373
|
-
tsx:
|
|
10374
|
-
css:
|
|
10669
|
+
tsx: readFileSync15(tsxPath, "utf8"),
|
|
10670
|
+
css: readFileSync15(cssPath, "utf8")
|
|
10375
10671
|
};
|
|
10376
10672
|
const semPath = `${flags.out}/${entry}/semantics.json`;
|
|
10377
|
-
if (
|
|
10378
|
-
initialSemantics = JSON.parse(
|
|
10673
|
+
if (existsSync18(semPath)) {
|
|
10674
|
+
initialSemantics = JSON.parse(readFileSync15(semPath, "utf8"));
|
|
10379
10675
|
}
|
|
10380
10676
|
}
|
|
10381
10677
|
break;
|
|
@@ -10544,6 +10840,12 @@ function buildProgram() {
|
|
|
10544
10840
|
const local = cmd.opts();
|
|
10545
10841
|
await runDoctor({ ...flags, mcpUrl: local["mcpUrl"] });
|
|
10546
10842
|
});
|
|
10843
|
+
program.command("activate").description("Activate Tendril on this machine (browser approval; nothing to do on pre-launch builds).").option("--service-url <url>", "override the entitlement service (tests only)").action(async (_o, cmd) => {
|
|
10844
|
+
const flags = globalFlags(cmd);
|
|
10845
|
+
const local = cmd.opts();
|
|
10846
|
+
const { runActivate: runActivate2 } = await Promise.resolve().then(() => (init_activate(), activate_exports));
|
|
10847
|
+
await runActivate2({ ...flags, serviceUrl: local["serviceUrl"] });
|
|
10848
|
+
});
|
|
10547
10849
|
const record = program.command("record").description("Agent-driven recording protocol: plan the queue, get instructions, ingest verbatim envelopes, resume from disk.");
|
|
10548
10850
|
record.command("plan").requiredOption("--set <dir>", "recording set directory").requiredOption("--component <name>", "component/system name").requiredOption("--metadata <file...>", "verbatim get_metadata envelope(s), optionally <file>@<frameId>").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) => {
|
|
10549
10851
|
const flags = globalFlags(cmd.parent.parent);
|