@tendrilapp/cli 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tendril-mcp.js +7 -0
- package/dist/tendril.js +126 -7
- package/package.json +1 -1
package/dist/tendril-mcp.js
CHANGED
|
@@ -46,6 +46,13 @@ var TOOLS = [
|
|
|
46
46
|
...i["componentSet"] !== void 0 ? ["--component-set", i["componentSet"]] : []
|
|
47
47
|
]
|
|
48
48
|
},
|
|
49
|
+
{
|
|
50
|
+
name: "tendril_doctor",
|
|
51
|
+
annotations: { readOnlyHint: true },
|
|
52
|
+
description: "Machine readiness + version status in one shot: installed vs latest version (with publish date and update remediation), browser identity, font-cache state, Figma desktop MCP reachability. Use to self-diagnose before recording/scoring, or whenever versions are in question. Exit 1 = something not ready; the report says exactly what and how to fix it.",
|
|
53
|
+
schema: z.object({}),
|
|
54
|
+
argv: () => ["doctor"]
|
|
55
|
+
},
|
|
49
56
|
{
|
|
50
57
|
name: "tendril_record_next",
|
|
51
58
|
annotations: { readOnlyHint: true },
|
package/dist/tendril.js
CHANGED
|
@@ -7251,6 +7251,14 @@ function authorComponentApi(opts) {
|
|
|
7251
7251
|
props.push({ name: propNameFor(key), kind: "union", values: ordered.map(kebab3), default: kebab3(def) });
|
|
7252
7252
|
}
|
|
7253
7253
|
}
|
|
7254
|
+
const slots = (opts.textSlots ?? []).map((slot) => {
|
|
7255
|
+
let name = slot.prop;
|
|
7256
|
+
if (name !== "children" && (props.some((pr) => pr.name === name) || RESERVED_PROPS.has(name.toLowerCase()))) name = `${name}Text`;
|
|
7257
|
+
let n = 2;
|
|
7258
|
+
while (props.some((pr) => pr.name === name)) name = `${slot.prop}Text${n++}`;
|
|
7259
|
+
return { ...slot, prop: name };
|
|
7260
|
+
});
|
|
7261
|
+
for (const slot of slots) props.push({ name: slot.prop, kind: "string", default: slot.default });
|
|
7254
7262
|
const configs = [];
|
|
7255
7263
|
const seenAssignments = /* @__PURE__ */ new Map();
|
|
7256
7264
|
const componentIdent = pascal(opts.component);
|
|
@@ -7272,6 +7280,10 @@ function authorComponentApi(opts) {
|
|
|
7272
7280
|
}
|
|
7273
7281
|
}
|
|
7274
7282
|
if (tokens.length > 0) assignment["data-tendril-state"] = tokens.join(" ");
|
|
7283
|
+
for (const slot of slots) {
|
|
7284
|
+
const v = slot.overrides[p.slug];
|
|
7285
|
+
if (v !== void 0) assignment[slot.prop] = v;
|
|
7286
|
+
}
|
|
7275
7287
|
const canonical = JSON.stringify(Object.fromEntries(Object.entries(assignment).sort(([a], [b]) => a.localeCompare(b))));
|
|
7276
7288
|
const clash = seenAssignments.get(canonical);
|
|
7277
7289
|
if (clash !== void 0) {
|
|
@@ -7290,7 +7302,7 @@ function authorComponentApi(opts) {
|
|
|
7290
7302
|
name: componentIdent,
|
|
7291
7303
|
props: props.map((pr) => ({
|
|
7292
7304
|
name: pr.name,
|
|
7293
|
-
type: pr.kind === "boolean" ? "boolean" : pr.values.map((v) => `"${v}"`).join(" | "),
|
|
7305
|
+
type: pr.kind === "boolean" ? "boolean" : pr.kind === "string" ? "string" : pr.values.map((v) => `"${v}"`).join(" | "),
|
|
7294
7306
|
required: false,
|
|
7295
7307
|
...pr.default !== void 0 ? { default: pr.default } : {}
|
|
7296
7308
|
})),
|
|
@@ -7298,7 +7310,7 @@ function authorComponentApi(opts) {
|
|
|
7298
7310
|
poseCompleteness: { recordedPoses: opts.poses.length, expressible: configs.length }
|
|
7299
7311
|
};
|
|
7300
7312
|
const propLines = props.map(
|
|
7301
|
-
(pr) => pr.kind === "boolean" ? ` ${pr.name}?: boolean;` : ` ${pr.name}?: ${pr.values.map((v) => `"${v}"`).join(" | ")}; // default "${pr.default}"`
|
|
7313
|
+
(pr) => pr.kind === "boolean" ? ` ${pr.name}?: boolean;` : pr.kind === "string" ? ` ${pr.name}?: string; // CONTENT \u2014 recorded default ${JSON.stringify(pr.default)}` : ` ${pr.name}?: ${pr.values.map((v) => `"${v}"`).join(" | ")}; // default "${pr.default}"`
|
|
7302
7314
|
);
|
|
7303
7315
|
const provided = opts.fonts ?? [];
|
|
7304
7316
|
const recorded = opts.recordedFonts ?? [];
|
|
@@ -7311,7 +7323,8 @@ ${propLines.join("\n")}
|
|
|
7311
7323
|
[key: string]: unknown; // MUST spread unknown props (incl. data-*) onto the root element
|
|
7312
7324
|
})
|
|
7313
7325
|
|
|
7314
|
-
|
|
7326
|
+
${slots.length > 0 ? `CONTENT PROPS: every string prop defaults to its RECORDED text \u2014 render the PROP, never a hardcoded literal. Configs pass per-pose recorded strings wherever the recording varies, and pixels enforce them: a hardcoded string fails those configs. Content presence (a heading that only exists in some poses) follows the AXIS props; the string prop only supplies the text.
|
|
7327
|
+
` : ""}Rules: plain CSS in styles.css (tokens.css optional, loaded first) \u2014 no Tailwind, no imports beyond react/react-dom, and NEVER import the stylesheet from the entry module: the harness injects tokens.css and styles.css itself, and an entry that imports CSS does not compile here (measured cost: one full round, every config 0). The component renders the RECORDED content as its defaults \u2014 reproduce it from the emissions. ${forcingCanon}${fontsLine}The host sizes nothing: the component is its natural recorded size.`;
|
|
7315
7328
|
const mappedTokens = /* @__PURE__ */ new Set([...forcedStates, ...props.filter((p) => p.kind === "boolean").map((p) => p.name)]);
|
|
7316
7329
|
const unmappedInteractionEvidence = interactionEvidence.filter((e) => {
|
|
7317
7330
|
const sel = e.endsWith(" (selection axis)");
|
|
@@ -7421,6 +7434,67 @@ function recordedFontNeeds(setDir) {
|
|
|
7421
7434
|
function recordedFontFamilies(setDir) {
|
|
7422
7435
|
return recordedFontNeeds(setDir).map((n) => n.family);
|
|
7423
7436
|
}
|
|
7437
|
+
function recordedTextSlots(setDir, repSlugs) {
|
|
7438
|
+
const perRep = [];
|
|
7439
|
+
for (const slug of repSlugs) {
|
|
7440
|
+
const f = path26.join(setDir, slug, "get_design_context.json");
|
|
7441
|
+
if (!existsSync20(f)) continue;
|
|
7442
|
+
const code = envelopeText(f);
|
|
7443
|
+
const texts = [];
|
|
7444
|
+
for (const m of code.matchAll(/>([^<>{}]+)</g)) {
|
|
7445
|
+
const t = decodeXmlEntities(m[1]).trim();
|
|
7446
|
+
if (t === "" || !/[A-Za-z0-9]/.test(t)) continue;
|
|
7447
|
+
texts.push(t);
|
|
7448
|
+
}
|
|
7449
|
+
perRep.push({ slug, texts });
|
|
7450
|
+
}
|
|
7451
|
+
if (perRep.length === 0) return [];
|
|
7452
|
+
const slots = [];
|
|
7453
|
+
for (const rep of perRep) {
|
|
7454
|
+
const used = /* @__PURE__ */ new Set();
|
|
7455
|
+
const unmatched = [];
|
|
7456
|
+
for (const t of rep.texts) {
|
|
7457
|
+
const i = slots.findIndex((sl, idx) => !used.has(idx) && [...sl.values.values()].includes(t));
|
|
7458
|
+
if (i >= 0) {
|
|
7459
|
+
used.add(i);
|
|
7460
|
+
slots[i].values.set(rep.slug, t);
|
|
7461
|
+
} else {
|
|
7462
|
+
unmatched.push(t);
|
|
7463
|
+
}
|
|
7464
|
+
}
|
|
7465
|
+
for (const t of unmatched) {
|
|
7466
|
+
const i = slots.findIndex((_, idx) => !used.has(idx) && !slots[idx].values.has(rep.slug));
|
|
7467
|
+
if (i >= 0) {
|
|
7468
|
+
used.add(i);
|
|
7469
|
+
slots[i].values.set(rep.slug, t);
|
|
7470
|
+
} else {
|
|
7471
|
+
const sl = { values: /* @__PURE__ */ new Map() };
|
|
7472
|
+
sl.values.set(rep.slug, t);
|
|
7473
|
+
slots.push(sl);
|
|
7474
|
+
used.add(slots.length - 1);
|
|
7475
|
+
}
|
|
7476
|
+
}
|
|
7477
|
+
}
|
|
7478
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
7479
|
+
return slots.map((sl, idx) => {
|
|
7480
|
+
const counts = /* @__PURE__ */ new Map();
|
|
7481
|
+
for (const v of sl.values.values()) counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
7482
|
+
const def = [...counts.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
7483
|
+
let prop;
|
|
7484
|
+
if (slots.length === 1) {
|
|
7485
|
+
prop = "children";
|
|
7486
|
+
} else {
|
|
7487
|
+
const word = /[A-Za-z][A-Za-z0-9]*/.exec(def)?.[0]?.toLowerCase();
|
|
7488
|
+
prop = word !== void 0 && word.length >= 2 ? word : `text${idx + 1}`;
|
|
7489
|
+
let n = 2;
|
|
7490
|
+
while (usedNames.has(prop)) prop = `${prop}${n++}`;
|
|
7491
|
+
}
|
|
7492
|
+
usedNames.add(prop);
|
|
7493
|
+
const overrides = {};
|
|
7494
|
+
for (const [slug, v] of sl.values) if (v !== def) overrides[slug] = v;
|
|
7495
|
+
return { prop, default: def, overrides, varies: Object.keys(overrides).length > 0 };
|
|
7496
|
+
});
|
|
7497
|
+
}
|
|
7424
7498
|
function authorTaskFromSet(setDir, opts = {}) {
|
|
7425
7499
|
if (opts.fonts === void 0) {
|
|
7426
7500
|
const resolved = resolvedFontFamilies().map((f) => f.family);
|
|
@@ -7449,15 +7523,24 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
7449
7523
|
const latticeNames = manifest.latticeNames ?? (existsSync20(setMeta) ? [...envelopeText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
|
|
7450
7524
|
const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
|
|
7451
7525
|
const recordedFonts = recordedFontFamilies(setDir);
|
|
7526
|
+
const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
|
|
7452
7527
|
const api = authorComponentApi({
|
|
7453
7528
|
component: manifest.component,
|
|
7454
7529
|
poses,
|
|
7455
7530
|
...latticeNames !== void 0 ? { latticeNames } : {},
|
|
7456
7531
|
...opts.fonts !== void 0 ? { fonts: opts.fonts } : {},
|
|
7457
7532
|
...recordedFonts.length > 0 ? { recordedFonts } : {},
|
|
7458
|
-
...Object.keys(defaults).length > 0 ? { defaults } : {}
|
|
7533
|
+
...Object.keys(defaults).length > 0 ? { defaults } : {},
|
|
7534
|
+
...textSlots.length > 0 ? { textSlots } : {}
|
|
7459
7535
|
});
|
|
7460
7536
|
const { behaviors, prelude, disclosures } = authorBehaviors(api);
|
|
7537
|
+
for (const slot of textSlots) {
|
|
7538
|
+
if (!slot.varies) {
|
|
7539
|
+
disclosures.push(
|
|
7540
|
+
`content prop from recorded text is UNEXERCISED: the recording shows one constant string (${JSON.stringify(slot.default)}) for it, so no config pixel-verifies that the prop reaches the render \u2014 wire it anyway; a sentinel check is future work`
|
|
7541
|
+
);
|
|
7542
|
+
}
|
|
7543
|
+
}
|
|
7461
7544
|
if (manifest.notRecorded !== void 0) disclosures.push(`not recorded (from the completeness manifest): ${manifest.notRecorded}`);
|
|
7462
7545
|
const task = {
|
|
7463
7546
|
set: setDir,
|
|
@@ -9322,15 +9405,51 @@ async function runDoctorChecks(options) {
|
|
|
9322
9405
|
});
|
|
9323
9406
|
return { ok: checks.filter((c) => c.name !== "figma-pat" && c.name !== "openrouter-key").every((c) => c.ok), checks };
|
|
9324
9407
|
}
|
|
9408
|
+
function versionIsNewer(a, b) {
|
|
9409
|
+
const pa = a.split(".").map(Number);
|
|
9410
|
+
const pb = b.split(".").map(Number);
|
|
9411
|
+
for (let i = 0; i < 3; i++) {
|
|
9412
|
+
if ((pb[i] ?? 0) > (pa[i] ?? 0)) return true;
|
|
9413
|
+
if ((pb[i] ?? 0) < (pa[i] ?? 0)) return false;
|
|
9414
|
+
}
|
|
9415
|
+
return false;
|
|
9416
|
+
}
|
|
9417
|
+
async function latestVersionInfo() {
|
|
9418
|
+
try {
|
|
9419
|
+
const ctl = new AbortController();
|
|
9420
|
+
const timer = setTimeout(() => ctl.abort(), 2500);
|
|
9421
|
+
const res = await fetch("https://registry.npmjs.org/@tendrilapp%2fcli", { signal: ctl.signal });
|
|
9422
|
+
clearTimeout(timer);
|
|
9423
|
+
if (!res.ok) return null;
|
|
9424
|
+
const doc = await res.json();
|
|
9425
|
+
const latest = doc["dist-tags"]?.latest;
|
|
9426
|
+
if (latest === void 0) return null;
|
|
9427
|
+
const stamp = doc.time?.[latest];
|
|
9428
|
+
return { latest, ...stamp !== void 0 ? { publishedAt: stamp.slice(0, 10) } : {} };
|
|
9429
|
+
} catch {
|
|
9430
|
+
return null;
|
|
9431
|
+
}
|
|
9432
|
+
}
|
|
9325
9433
|
async function runDoctor(flags) {
|
|
9326
9434
|
if (flags.describe) {
|
|
9327
9435
|
printDescription(DOCTOR_DESCRIPTION);
|
|
9328
9436
|
return;
|
|
9329
9437
|
}
|
|
9330
|
-
const report = await runDoctorChecks({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} });
|
|
9331
|
-
emitData(flags, { version: cliVersion(), ...report }, () => {
|
|
9332
|
-
|
|
9438
|
+
const [report, latest] = await Promise.all([runDoctorChecks({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} }), latestVersionInfo()]);
|
|
9439
|
+
emitData(flags, { version: cliVersion(), ...latest !== null ? { latest: latest.latest, latestPublishedAt: latest.publishedAt ?? null, updateAvailable: versionIsNewer(cliVersion(), latest.latest) } : {}, ...report }, () => {
|
|
9440
|
+
const version = cliVersion();
|
|
9441
|
+
if (latest === null) {
|
|
9442
|
+
process.stdout.write(`tendril ${version} (latest: unreachable)
|
|
9333
9443
|
`);
|
|
9444
|
+
} else if (!versionIsNewer(version, latest.latest)) {
|
|
9445
|
+
process.stdout.write(`tendril ${version} (${latest.latest === version ? "latest" : `ahead of registry ${latest.latest}`}${latest.publishedAt !== void 0 ? `, published ${latest.publishedAt}` : ""})
|
|
9446
|
+
`);
|
|
9447
|
+
} else {
|
|
9448
|
+
process.stdout.write(`tendril ${version} \u2014 UPDATE AVAILABLE: ${latest.latest}${latest.publishedAt !== void 0 ? ` (published ${latest.publishedAt})` : ""}
|
|
9449
|
+
`);
|
|
9450
|
+
process.stdout.write(` \u2192 npm install -g @tendrilapp/cli@latest (plugin MCP users update automatically at next session; a stale npx cache clears with npm cache clean --force)
|
|
9451
|
+
`);
|
|
9452
|
+
}
|
|
9334
9453
|
for (const check of report.checks) {
|
|
9335
9454
|
process.stdout.write(`${check.ok ? "\u2713" : "\u2717"} ${check.name}: ${check.detail}
|
|
9336
9455
|
`);
|