@tendrilapp/cli 0.1.53 → 0.1.54
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 +22 -1
- package/dist/tendril-mcp.js +11 -2
- package/dist/tendril.js +345 -33
- package/package.json +1 -1
package/dist/SKILL.md
CHANGED
|
@@ -253,7 +253,13 @@ Batch runs (several components in one session):
|
|
|
253
253
|
5. COMPOSITION: read the status output's `composition` block. If it
|
|
254
254
|
lists unconfirmed partner pairs, this recording references OTHER
|
|
255
255
|
recorded components (a segmented control built from a recorded
|
|
256
|
-
Control, a dialog holding a recorded Button).
|
|
256
|
+
Control, a dialog holding a recorded Button). A pair the human
|
|
257
|
+
ALREADY confirmed can legitimately re-open as a GROWTH ask
|
|
258
|
+
(`growsConfirmed: true`): new evidence derived MORE instances of
|
|
259
|
+
the same pairing — say so as an addition, never as a contradiction
|
|
260
|
+
of the settled decision; confirming adds the new instances, the
|
|
261
|
+
standing confirmation is untouched, and growth asks cannot be
|
|
262
|
+
declined (leave them unconfirmed and they simply stay local). Order matters:
|
|
257
263
|
generate the PARTNER bundle first, then put the pairing decision in
|
|
258
264
|
front of the human — it is human-only on either channel, and you
|
|
259
265
|
can never make it. CLICK PATH (lead with this): `tendril_compose`
|
|
@@ -388,6 +394,13 @@ Batch runs (several components in one session):
|
|
|
388
394
|
genuinely headless runs; it cannot tell "nobody is there" from "the
|
|
389
395
|
harness said nobody is there", so it must at least say what it did,
|
|
390
396
|
loudly and first.)
|
|
397
|
+
PRESENCE PROOF OVERRIDES THE NON-INTERACTIVE BRANCH: a browser
|
|
398
|
+
approval decided mid-run — a compose card approved, a publish
|
|
399
|
+
card clicked — is a human at the keyboard, whatever the harness
|
|
400
|
+
said. From the moment one lands, ask the consent gates that
|
|
401
|
+
remain instead of auto-resolving them (measured, 2026-08-30: a
|
|
402
|
+
run auto-resolved the model pick for its THIRD generation while
|
|
403
|
+
the user was demonstrably present, having just clicked Approve).
|
|
391
404
|
Weak models produce honest sub-bar reports, never false passes.
|
|
392
405
|
6. Tell the user roughly what a run costs them: organism-scale
|
|
393
406
|
components have measured 0.3–0.7M tokens of their plan.
|
|
@@ -527,6 +540,14 @@ live at <URL>" (or "done — waiting for your Approve at <link>, as
|
|
|
527
540
|
<account>"), before scores, caveats and file lists. The user should
|
|
528
541
|
never have to ask where their component is.
|
|
529
542
|
|
|
543
|
+
And invite the ONE check only the user can run: right after the
|
|
544
|
+
done-line, ask them to open the live page and say whether the demo
|
|
545
|
+
renders. You cannot execute the published page — a browser can, and
|
|
546
|
+
a served-page defect is invisible to every check this pipeline runs
|
|
547
|
+
(measured, 2026-08-30: two publishes reported "done — live" while
|
|
548
|
+
both live demos showed an error banner nobody had seen). One line,
|
|
549
|
+
no urging: "open it and tell me if anything looks off."
|
|
550
|
+
|
|
530
551
|
A green verify may also print COMPOSE lines: other recorded components
|
|
531
552
|
that embed the one just verified. Relay them after the done-line as
|
|
532
553
|
DISCLOSURES (zero questions — the confirm decision is human-only, in
|
package/dist/tendril-mcp.js
CHANGED
|
@@ -125,9 +125,18 @@ var TOOLS = [
|
|
|
125
125
|
description: "Publish a VERIFIED bundle to the user's portal \u2014 phase one of the browser-approved publish. Re-publishing an already-published component completes in one call. A component's FIRST publish is a human-only decision the portal enforces: this call requests the approval and returns the approve-page link \u2014 RELAY IT to the user verbatim, along with which account the result says to be signed in as (they click Approve in the browser; approving includes accepting the design system's publishing terms, which is their decision to make, never yours to urge). Then call tendril_publish_wait to finish. You cannot approve this yourself: the portal only accepts the decision from their signed-in browser, never from this machine's token. Requires a green verify (the CLI refuses a declined run) and a portal session (tendril_login).",
|
|
126
126
|
schema: z.object({
|
|
127
127
|
bundleDir: str("bundle directory (verified \u2014 carries component.json and verify-evidence)"),
|
|
128
|
-
portal: optStr("portal origin override for self-hosted portals (defaults to the stored session's portal)")
|
|
128
|
+
portal: optStr("portal origin override for self-hosted portals (defaults to the stored session's portal)"),
|
|
129
|
+
designSystemName: optStr(
|
|
130
|
+
"first publish only: a PROPOSED display name for the design system, prefilled on the approve card where the human names or picks it \u2014 pass the design's human name when you know it (the Figma file's title, the design system the user named); without a proposal the card prefills the raw Figma file key, and a plain Approve makes that key the permanent name"
|
|
131
|
+
)
|
|
129
132
|
}),
|
|
130
|
-
argv: (i) => [
|
|
133
|
+
argv: (i) => [
|
|
134
|
+
"publish",
|
|
135
|
+
i["bundleDir"],
|
|
136
|
+
"--approve-start",
|
|
137
|
+
...typeof i["designSystemName"] === "string" ? ["--design-system-name", i["designSystemName"]] : [],
|
|
138
|
+
...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []
|
|
139
|
+
]
|
|
131
140
|
},
|
|
132
141
|
{
|
|
133
142
|
name: "tendril_publish_wait",
|
package/dist/tendril.js
CHANGED
|
@@ -1940,18 +1940,22 @@ function sameComponent(a, b) {
|
|
|
1940
1940
|
for (const id of a.variantNodeIds) if (b.variantNodeIds.has(id)) return true;
|
|
1941
1941
|
return false;
|
|
1942
1942
|
}
|
|
1943
|
-
function
|
|
1943
|
+
function emissionText(setDir, repSlug) {
|
|
1944
1944
|
const file = path4.join(setDir, repSlug, "get_design_context.json");
|
|
1945
|
-
if (!existsSync4(file)) return
|
|
1945
|
+
if (!existsSync4(file)) return void 0;
|
|
1946
1946
|
let text;
|
|
1947
1947
|
try {
|
|
1948
1948
|
text = envelopeTextContent(JSON.parse(readFileSync3(file, "utf8")));
|
|
1949
1949
|
} catch {
|
|
1950
|
-
return
|
|
1950
|
+
return void 0;
|
|
1951
1951
|
}
|
|
1952
1952
|
const cut = text.search(FOOTER);
|
|
1953
|
-
|
|
1953
|
+
return cut === -1 ? text : text.slice(0, cut);
|
|
1954
|
+
}
|
|
1955
|
+
function emissionTails(setDir, repSlug) {
|
|
1956
|
+
const text = emissionText(setDir, repSlug);
|
|
1954
1957
|
const byHead = /* @__PURE__ */ new Map();
|
|
1958
|
+
if (text === void 0) return byHead;
|
|
1955
1959
|
for (const m of text.matchAll(/data-node-id="I([^"]+)"/g)) {
|
|
1956
1960
|
const segs = m[1].split(";");
|
|
1957
1961
|
const head = segs[0];
|
|
@@ -1964,6 +1968,171 @@ function emissionTails(setDir, repSlug) {
|
|
|
1964
1968
|
}
|
|
1965
1969
|
return byHead;
|
|
1966
1970
|
}
|
|
1971
|
+
function declarationEnd(text, afterOpenParen) {
|
|
1972
|
+
let i = afterOpenParen;
|
|
1973
|
+
let parens = 1;
|
|
1974
|
+
for (; i < text.length && parens > 0; i++) {
|
|
1975
|
+
const ch = text[i];
|
|
1976
|
+
if (ch === '"') {
|
|
1977
|
+
i = text.indexOf('"', i + 1);
|
|
1978
|
+
if (i === -1) return text.length;
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
if (ch === "(") parens++;
|
|
1982
|
+
else if (ch === ")") parens--;
|
|
1983
|
+
}
|
|
1984
|
+
let body = text.indexOf("{", i);
|
|
1985
|
+
if (body === -1) return text.length;
|
|
1986
|
+
let depth = 0;
|
|
1987
|
+
for (; body < text.length; body++) {
|
|
1988
|
+
const ch = text[body];
|
|
1989
|
+
if (ch === '"') {
|
|
1990
|
+
body = text.indexOf('"', body + 1);
|
|
1991
|
+
if (body === -1) return text.length;
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
if (ch === "{") depth++;
|
|
1995
|
+
else if (ch === "}") {
|
|
1996
|
+
depth--;
|
|
1997
|
+
if (depth === 0) return body + 1;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
return text.length;
|
|
2001
|
+
}
|
|
2002
|
+
function hoistedCallAnalysis(text, root, hostIds) {
|
|
2003
|
+
const matches = [...text.matchAll(/function\s+([A-Za-z_$][\w$]*)\s*\(/g)];
|
|
2004
|
+
const decls = matches.map((d) => ({
|
|
2005
|
+
name: d[1],
|
|
2006
|
+
start: d.index ?? 0,
|
|
2007
|
+
bodyStart: (d.index ?? 0) + d[0].length,
|
|
2008
|
+
end: declarationEnd(text, (d.index ?? 0) + d[0].length)
|
|
2009
|
+
}));
|
|
2010
|
+
const names = decls.map((d) => d.name);
|
|
2011
|
+
const dup = names.find((n, i) => names.indexOf(n) !== i);
|
|
2012
|
+
if (dup !== void 0) return { refusal: `hoisted function name ${dup} is not unique` };
|
|
2013
|
+
const rootElementId = (d) => /return\s*\(?\s*<[^>]*?\bdata-node-id="([^"]+)"/.exec(text.slice(d.bodyStart, d.end))?.[1];
|
|
2014
|
+
const owner = decls.find((d) => rootElementId(d) === root);
|
|
2015
|
+
if (owner === void 0) return { refusal: "the pose root is not any hoisted function's ROOT element" };
|
|
2016
|
+
if (/\bid=\{/.test(text.slice(owner.start, owner.end))) {
|
|
2017
|
+
return { refusal: "the hoisted function is multi-variant (conditional element ids) \u2014 one function does not mean one pose" };
|
|
2018
|
+
}
|
|
2019
|
+
const byName = new Map(decls.map((d) => [d.name, d]));
|
|
2020
|
+
let top = "";
|
|
2021
|
+
let cursor = 0;
|
|
2022
|
+
for (const d of decls) {
|
|
2023
|
+
if (d.start > cursor) top += text.slice(cursor, d.start);
|
|
2024
|
+
cursor = Math.max(cursor, d.end);
|
|
2025
|
+
}
|
|
2026
|
+
top += text.slice(cursor);
|
|
2027
|
+
const inDegree = new Map(decls.map((d) => [d.name, 0]));
|
|
2028
|
+
const callSources = [top, ...decls.map((d) => text.slice(d.bodyStart, d.end))];
|
|
2029
|
+
for (const source of callSources) {
|
|
2030
|
+
for (const m of source.matchAll(/<([A-Z][\w$]*)(?![\w$])/g)) {
|
|
2031
|
+
if (inDegree.has(m[1])) inDegree.set(m[1], inDegree.get(m[1]) + 1);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
const regions = [top];
|
|
2035
|
+
const entered = /* @__PURE__ */ new Set();
|
|
2036
|
+
for (const d of decls) {
|
|
2037
|
+
const rid = rootElementId(d);
|
|
2038
|
+
if (inDegree.get(d.name) === 0 && rid !== void 0 && hostIds.has(rid)) {
|
|
2039
|
+
entered.add(d.name);
|
|
2040
|
+
regions.push(text.slice(d.bodyStart, d.end));
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
for (let i = 0; i < regions.length; i++) {
|
|
2044
|
+
for (const m of regions[i].matchAll(/<([A-Z][\w$]*)(?![\w$])/g)) {
|
|
2045
|
+
const d = byName.get(m[1]);
|
|
2046
|
+
if (d !== void 0 && !entered.has(d.name)) {
|
|
2047
|
+
entered.add(d.name);
|
|
2048
|
+
regions.push(text.slice(d.bodyStart, d.end));
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
let count = 0;
|
|
2053
|
+
for (const region of regions) {
|
|
2054
|
+
for (const c of region.matchAll(new RegExp(`<${owner.name}(?![\\w$])([^>]*?)/?>`, "g"))) {
|
|
2055
|
+
const props = c[1].replace(/\/$/, "").trim();
|
|
2056
|
+
if (props !== "" && !/^className=("[^"]*"|\{[^}]*\})$/.test(props)) {
|
|
2057
|
+
return { refusal: `a call site passes props beyond className \u2014 it may render a different pose or content (<${owner.name} ${props.slice(0, 48)}\u2026>)` };
|
|
2058
|
+
}
|
|
2059
|
+
count++;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
return { calls: count };
|
|
2063
|
+
}
|
|
2064
|
+
function hoistedRecoveryFor(host, slug, index, residuals) {
|
|
2065
|
+
if (residuals.length === 0) return {};
|
|
2066
|
+
const text = emissionText(host.dir, slug);
|
|
2067
|
+
if (text === void 0) return {};
|
|
2068
|
+
const foreign = /* @__PURE__ */ new Set();
|
|
2069
|
+
for (const m of text.matchAll(/data-node-id="([^"]+)"/g)) {
|
|
2070
|
+
if (!m[1].startsWith("I") && !host.ownIds.has(m[1])) foreign.add(m[1]);
|
|
2071
|
+
}
|
|
2072
|
+
const hits = [];
|
|
2073
|
+
const libraryRefused = [];
|
|
2074
|
+
for (const c of index) {
|
|
2075
|
+
if (c.dir === host.dir || sameComponent(c, host)) continue;
|
|
2076
|
+
const roots = [...foreign].filter((id) => c.variantNodeIds.has(id));
|
|
2077
|
+
if (roots.length === 0) continue;
|
|
2078
|
+
if (host.figmaFile !== void 0 && c.figmaFile !== void 0 && c.figmaFile !== host.figmaFile) {
|
|
2079
|
+
libraryRefused.push(c);
|
|
2080
|
+
continue;
|
|
2081
|
+
}
|
|
2082
|
+
hits.push({ entry: c, roots });
|
|
2083
|
+
}
|
|
2084
|
+
if (hits.length === 0) {
|
|
2085
|
+
return libraryRefused.length === 0 ? {} : {
|
|
2086
|
+
note: `LIBRARY-EDGE (not auto-joined): a hoisted pose root names ${kitLabel(libraryRefused[0])} recorded from a DIFFERENT file \u2014 the same-file rule refuses; the future cross-file ADR owns this class`
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
const groups = [];
|
|
2090
|
+
for (const h of hits) {
|
|
2091
|
+
const g = groups.find((grp) => grp.members.some((m) => sameComponent(m, h.entry)));
|
|
2092
|
+
if (g) {
|
|
2093
|
+
g.members.push(h.entry);
|
|
2094
|
+
for (const r of h.roots) g.roots.add(r);
|
|
2095
|
+
} else groups.push({ members: [h.entry], roots: new Set(h.roots) });
|
|
2096
|
+
}
|
|
2097
|
+
const label = (g) => `${g.members[0].displayName} (${[...g.roots].join(", ")})`;
|
|
2098
|
+
if (groups.length > 1 || groups[0].roots.size > 1) {
|
|
2099
|
+
return {
|
|
2100
|
+
note: `HOISTED CONTENT, NOT FORCED: this pose's emission carries hoisted partner pose root(s) ${groups.map(label).join(" / ")} without per-instance node ids \u2014 ${groups.length > 1 ? "multiple components" : "multiple poses"} are hoisted, so per-instance attribution is not forced and no join is proposed from it (a human decision channel for the multi-root shape is future work)`
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
const group = groups[0];
|
|
2104
|
+
const root = [...group.roots][0];
|
|
2105
|
+
const identityNames = new Set(group.members.map((m) => norm(m.figmaComponentName ?? m.displayName)));
|
|
2106
|
+
if (identityNames.size > 1) {
|
|
2107
|
+
return {
|
|
2108
|
+
note: `HOISTED CONTENT, NOT FORCED: pose root ${root} joins recordings whose identity names DISAGREE (${group.members.map((m) => m.figmaComponentName ?? m.displayName).join(" / ")}) \u2014 no name agreement is derivable, no join is proposed`
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
const identityName = group.members[0].figmaComponentName ?? group.members[0].displayName;
|
|
2112
|
+
const disagreeing = residuals.filter((r) => norm(r.name) !== norm(identityName));
|
|
2113
|
+
if (disagreeing.length > 0) {
|
|
2114
|
+
return {
|
|
2115
|
+
note: `HOISTED CONTENT, NOT FORCED: pose root ${root} of ${kitLabel(group.members[0])} is hoisted here, but ${String(disagreeing.length)} residual instance(s) carry a different name (${disagreeing.map((r) => `"${r.name}"`).join(", ")}) \u2014 attribution is not forced, no join is proposed`
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
const analysis = hoistedCallAnalysis(text, root, host.ownIds);
|
|
2119
|
+
if (analysis.calls === void 0 || analysis.calls !== residuals.length) {
|
|
2120
|
+
return {
|
|
2121
|
+
note: `HOISTED CONTENT, NOT FORCED: pose root ${root} of ${kitLabel(group.members[0])} is hoisted here, but the assignment is not forced (${analysis.refusal ?? `${String(analysis.calls)} call site(s) in this pose's root region vs ${String(residuals.length)} residual instance(s)`}) \u2014 no join is proposed from it`
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
const disclosures = [];
|
|
2125
|
+
const uncaptured = [host.figmaFile === void 0 ? "the host set" : void 0, ...group.members.map((m) => m.figmaFile === void 0 ? path4.basename(m.dir) : void 0)].filter(
|
|
2126
|
+
(x) => x !== void 0
|
|
2127
|
+
);
|
|
2128
|
+
if (uncaptured.length > 0) {
|
|
2129
|
+
disclosures.push(`identity unproven: file identity is not captured on ${uncaptured.join(", ")} (recorded before slice 1) \u2014 the join rests on id evidence alone`);
|
|
2130
|
+
}
|
|
2131
|
+
if (group.members.every((m) => m.figmaComponentName === void 0)) {
|
|
2132
|
+
disclosures.push(`identity name not captured on any partner recording \u2014 the name agreement rests on plan-time display names`);
|
|
2133
|
+
}
|
|
2134
|
+
return { forced: { group: group.members, root, identityName, disclosures, instanceIds: new Set(residuals.map((r) => r.id)) } };
|
|
2135
|
+
}
|
|
1967
2136
|
function bindingsFromEntry(entry) {
|
|
1968
2137
|
const out = /* @__PURE__ */ new Map();
|
|
1969
2138
|
const components = entry?.components ?? {};
|
|
@@ -2049,6 +2218,10 @@ function composeReport(index) {
|
|
|
2049
2218
|
const tailsByHead = emissionTails(host.dir, slug);
|
|
2050
2219
|
const cidByInstance = restInstancePoses(host.dir, slug, variantNodeId);
|
|
2051
2220
|
const repIsMcpRecorded = !resolveRepEnvelopePathIn(path4.join(host.dir, slug), "metadata").endsWith(REST_METADATA_FILE);
|
|
2221
|
+
const residuals = instances.filter(
|
|
2222
|
+
(i) => i.visible && (tailsByHead.get(i.id)?.size ?? 0) === 0 && cidByInstance.bindings.get(i.id)?.componentId === void 0
|
|
2223
|
+
);
|
|
2224
|
+
const hoisted = hoistedRecoveryFor(host, slug, index, residuals);
|
|
2052
2225
|
for (const inst of instances) {
|
|
2053
2226
|
if (visibleEver.get(inst.id) !== true) {
|
|
2054
2227
|
edges.push({
|
|
@@ -2124,6 +2297,34 @@ function composeReport(index) {
|
|
|
2124
2297
|
return norm(identityName) === norm(inst.name);
|
|
2125
2298
|
});
|
|
2126
2299
|
if (eligible.length === 0) {
|
|
2300
|
+
if (hoisted.forced !== void 0 && hoisted.forced.instanceIds.has(inst.id)) {
|
|
2301
|
+
const f = hoisted.forced;
|
|
2302
|
+
edges.push({
|
|
2303
|
+
hostSet: host.dir,
|
|
2304
|
+
hostRep: slug,
|
|
2305
|
+
instanceId: inst.id,
|
|
2306
|
+
instanceName: inst.name,
|
|
2307
|
+
kind: "substitution",
|
|
2308
|
+
partners: f.group.map((p) => ({ dir: p.dir, displayName: p.displayName, ...p.figmaFile !== void 0 ? { figmaFile: p.figmaFile } : {} })),
|
|
2309
|
+
pose: {
|
|
2310
|
+
variantNodeId: f.root,
|
|
2311
|
+
reps: f.group.filter((m) => m.repSlugByVariantNode.has(f.root)).map((m) => ({ dir: m.dir, slug: m.repSlugByVariantNode.get(f.root) }))
|
|
2312
|
+
},
|
|
2313
|
+
disclosures: [
|
|
2314
|
+
...disclosures,
|
|
2315
|
+
...f.disclosures,
|
|
2316
|
+
// Plain-language lead, then the technical basis — the
|
|
2317
|
+
// human confirm gate is the one backstop in front of a
|
|
2318
|
+
// wrong forced join, so the sentence must be readable
|
|
2319
|
+
// by the designer who decides (audit, frame lens).
|
|
2320
|
+
`substitution-grade (HOISTED-ROOT RECOVERY: this pose renders the partner through a deduplicated helper Figma emitted without per-instance node ids, and the evidence pins it to exactly one recorded partner pose \u2014 pose node id ${f.root} on the helper's root, prop-free call sites in this pose equal to the unattributed instances, every name in agreement); pixel-neutrality is NOT asserted here \u2014 instance overrides are measured real, and a per-region check is still future work (whole-frame pixels remain the evidence for composed regions)`
|
|
2321
|
+
]
|
|
2322
|
+
});
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
if (hoisted.note !== void 0 && residuals.some((r) => r.id === inst.id)) {
|
|
2326
|
+
disclosures.push(hoisted.note);
|
|
2327
|
+
}
|
|
2127
2328
|
if (nameProps.length > 0) {
|
|
2128
2329
|
edges.push({
|
|
2129
2330
|
hostSet: host.dir,
|
|
@@ -6878,6 +7079,17 @@ function classifyBundleSurface(files, opts) {
|
|
|
6878
7079
|
unknown.push(path66);
|
|
6879
7080
|
continue;
|
|
6880
7081
|
}
|
|
7082
|
+
if (path66.startsWith("composed/")) {
|
|
7083
|
+
const rest = path66.slice("composed/".length);
|
|
7084
|
+
const slash = rest.indexOf("/");
|
|
7085
|
+
const partner = slash === -1 ? "" : rest.slice(0, slash);
|
|
7086
|
+
const fname = slash === -1 ? "" : rest.slice(slash + 1);
|
|
7087
|
+
const partnerOk = /^[A-Za-z0-9][A-Za-z0-9._ -]*$/.test(partner) && !partner.includes("..");
|
|
7088
|
+
const fileOk = fname === "styles.css" || fname === "tokens.css" || fname === "fonts.css" || /^[A-Za-z0-9][A-Za-z0-9._ -]*\.tsx$/.test(fname) && !fname.includes("..");
|
|
7089
|
+
if (partnerOk && fileOk && !fname.includes("/")) published.push({ path: path66, role: "composed" });
|
|
7090
|
+
else unknown.push(path66);
|
|
7091
|
+
continue;
|
|
7092
|
+
}
|
|
6881
7093
|
const name = inEvidence ? path66.slice(EVIDENCE_DIR.length + 1) : path66;
|
|
6882
7094
|
if (name.includes("/")) {
|
|
6883
7095
|
unknown.push(path66);
|
|
@@ -6947,6 +7159,13 @@ var init_published_surface = __esm({
|
|
|
6947
7159
|
// imports it and the scorer compiles it, so a swapped icons.tsx
|
|
6948
7160
|
// after verify must break the tie exactly as a swapped entry does.
|
|
6949
7161
|
"icons",
|
|
7162
|
+
// Composed pins: the partner module's bytes are graded artifact too —
|
|
7163
|
+
// module-verbatim hashes them, the mounts inject their CSS, and the
|
|
7164
|
+
// demo executes them in a stranger's browser. Left unscored, a
|
|
7165
|
+
// post-verify swap of composed/<Name>/<file> would ride a passing
|
|
7166
|
+
// module-verbatim in the stored report — the entry-swap class
|
|
7167
|
+
// (ADR-018 §1c) reopened one directory down.
|
|
7168
|
+
"composed",
|
|
6950
7169
|
"evidence-triple",
|
|
6951
7170
|
"evidence-absent-crop",
|
|
6952
7171
|
"diff-legend"
|
|
@@ -10033,9 +10252,18 @@ var init_server = __esm({
|
|
|
10033
10252
|
description: "Publish a VERIFIED bundle to the user's portal \u2014 phase one of the browser-approved publish. Re-publishing an already-published component completes in one call. A component's FIRST publish is a human-only decision the portal enforces: this call requests the approval and returns the approve-page link \u2014 RELAY IT to the user verbatim, along with which account the result says to be signed in as (they click Approve in the browser; approving includes accepting the design system's publishing terms, which is their decision to make, never yours to urge). Then call tendril_publish_wait to finish. You cannot approve this yourself: the portal only accepts the decision from their signed-in browser, never from this machine's token. Requires a green verify (the CLI refuses a declined run) and a portal session (tendril_login).",
|
|
10034
10253
|
schema: z13.object({
|
|
10035
10254
|
bundleDir: str("bundle directory (verified \u2014 carries component.json and verify-evidence)"),
|
|
10036
|
-
portal: optStr("portal origin override for self-hosted portals (defaults to the stored session's portal)")
|
|
10255
|
+
portal: optStr("portal origin override for self-hosted portals (defaults to the stored session's portal)"),
|
|
10256
|
+
designSystemName: optStr(
|
|
10257
|
+
"first publish only: a PROPOSED display name for the design system, prefilled on the approve card where the human names or picks it \u2014 pass the design's human name when you know it (the Figma file's title, the design system the user named); without a proposal the card prefills the raw Figma file key, and a plain Approve makes that key the permanent name"
|
|
10258
|
+
)
|
|
10037
10259
|
}),
|
|
10038
|
-
argv: (i) => [
|
|
10260
|
+
argv: (i) => [
|
|
10261
|
+
"publish",
|
|
10262
|
+
i["bundleDir"],
|
|
10263
|
+
"--approve-start",
|
|
10264
|
+
...typeof i["designSystemName"] === "string" ? ["--design-system-name", i["designSystemName"]] : [],
|
|
10265
|
+
...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []
|
|
10266
|
+
]
|
|
10039
10267
|
},
|
|
10040
10268
|
{
|
|
10041
10269
|
name: "tendril_publish_wait",
|
|
@@ -14136,7 +14364,7 @@ function composedPins(hostSet, libraryRoots) {
|
|
|
14136
14364
|
}
|
|
14137
14365
|
const moduleFiles = [];
|
|
14138
14366
|
let fileIssue;
|
|
14139
|
-
for (const name of [manifest.entry, "styles.css", "tokens.css", "fonts.css"]) {
|
|
14367
|
+
for (const name of [manifest.entry, "styles.css", "tokens.css", "fonts.css", "icons.tsx"]) {
|
|
14140
14368
|
const file = path39.join(bundleDir, name);
|
|
14141
14369
|
if (!existsSync32(file)) {
|
|
14142
14370
|
if (name === manifest.entry || name === "styles.css") {
|
|
@@ -16203,6 +16431,7 @@ __export(compose_exports, {
|
|
|
16203
16431
|
IMPLICIT_PARENT_SCAN_MAX_ENTRIES: () => IMPLICIT_PARENT_SCAN_MAX_ENTRIES,
|
|
16204
16432
|
buildCompositionEntries: () => buildCompositionEntries,
|
|
16205
16433
|
compositionPairsFor: () => compositionPairsFor,
|
|
16434
|
+
openPairsAgainstStanding: () => openPairsAgainstStanding,
|
|
16206
16435
|
renderBindingsRemediation: () => renderBindingsRemediation,
|
|
16207
16436
|
runCompose: () => runCompose,
|
|
16208
16437
|
substitutionPairs: () => substitutionPairs,
|
|
@@ -16211,6 +16440,31 @@ __export(compose_exports, {
|
|
|
16211
16440
|
import { createHash as createHash13 } from "node:crypto";
|
|
16212
16441
|
import { existsSync as existsSync39, readFileSync as readFileSync35, readdirSync as readdirSync16 } from "node:fs";
|
|
16213
16442
|
import path48 from "node:path";
|
|
16443
|
+
function openPairsAgainstStanding(pairs, standing) {
|
|
16444
|
+
const byKey = new Map(standing.map((c) => [fromStoredRel(c.partner.key), c]));
|
|
16445
|
+
const open = [];
|
|
16446
|
+
for (const p of pairs) {
|
|
16447
|
+
const decided = byKey.get(p.key);
|
|
16448
|
+
if (decided === void 0) {
|
|
16449
|
+
open.push(p);
|
|
16450
|
+
continue;
|
|
16451
|
+
}
|
|
16452
|
+
if (decided.status !== "confirmed") continue;
|
|
16453
|
+
const covered = new Set(decided.instances.map((i) => `${i.hostRep}\0${i.instanceId}`));
|
|
16454
|
+
const delta = p.instances.filter((i) => !covered.has(`${i.hostRep}\0${i.instanceId}`));
|
|
16455
|
+
if (delta.length === 0) continue;
|
|
16456
|
+
open.push({
|
|
16457
|
+
...p,
|
|
16458
|
+
instances: delta,
|
|
16459
|
+
growsConfirmed: true,
|
|
16460
|
+
disclosures: [
|
|
16461
|
+
`GROWTH: this pair stands CONFIRMED with ${String(decided.instances.length)} instance(s), and the recordings now derive ${String(delta.length)} more (evidence can grow a set \u2014 a new evidence class, a bindings enrichment). Confirming ADDS the new instance(s) to the standing decision; the original confirmation \u2014 and the partner bytes it pinned \u2014 stays exactly as decided. Not confirming leaves them unjoined and this ask returns.`,
|
|
16462
|
+
...p.disclosures
|
|
16463
|
+
]
|
|
16464
|
+
});
|
|
16465
|
+
}
|
|
16466
|
+
return open;
|
|
16467
|
+
}
|
|
16214
16468
|
function renderBindingsRemediation(r) {
|
|
16215
16469
|
if (r === void 0) return void 0;
|
|
16216
16470
|
const hostQ = quoteArg(path48.resolve(r.hostSet));
|
|
@@ -16242,7 +16496,6 @@ function compositionPairsFor(hostSet, roots) {
|
|
|
16242
16496
|
if (parsed.success) standing.push(parsed.data);
|
|
16243
16497
|
else invalid++;
|
|
16244
16498
|
}
|
|
16245
|
-
const decidedKeys = new Set(standing.map((c) => fromStoredRel(c.partner.key)));
|
|
16246
16499
|
const proposalRows = /* @__PURE__ */ new Map();
|
|
16247
16500
|
for (const e of edges) {
|
|
16248
16501
|
if (e.hostSet !== hostSet || e.kind !== "proposal") continue;
|
|
@@ -16253,7 +16506,7 @@ function compositionPairsFor(hostSet, roots) {
|
|
|
16253
16506
|
if (row.remediation === void 0 && e.remediation !== void 0) row.remediation = e.remediation;
|
|
16254
16507
|
proposalRows.set(key, row);
|
|
16255
16508
|
}
|
|
16256
|
-
return { open: pairs
|
|
16509
|
+
return { open: openPairsAgainstStanding(pairs, standing), proposals: [...proposalRows.values()], standing, invalid, ...skippedParent !== void 0 ? { skippedParent } : {} };
|
|
16257
16510
|
}
|
|
16258
16511
|
function substitutionPairs(edges, hostSet) {
|
|
16259
16512
|
const pairs = /* @__PURE__ */ new Map();
|
|
@@ -16357,8 +16610,7 @@ function runComposeConfirm(flags, hostSet, roots) {
|
|
|
16357
16610
|
}
|
|
16358
16611
|
standing.push(parsed.data);
|
|
16359
16612
|
}
|
|
16360
|
-
const
|
|
16361
|
-
const open = pairs.filter((p) => !decidedKeys.has(p.key));
|
|
16613
|
+
const open = openPairsAgainstStanding(pairs, standing);
|
|
16362
16614
|
const printProposal = () => {
|
|
16363
16615
|
for (const s of standing) process.stdout.write(`standing ${s.status.toUpperCase()}: ${s.partner.displayName} [${s.partner.key}] (${s.instances.length} instance(s)) \u2014 asked once, not re-asked
|
|
16364
16616
|
`);
|
|
@@ -16432,6 +16684,13 @@ PAIR ${p.displayName}${p.figmaFile !== void 0 ? ` (file ${p.figmaFile})` : " \u2
|
|
|
16432
16684
|
remediation: "Spell the pair key exactly as printed in brackets."
|
|
16433
16685
|
});
|
|
16434
16686
|
}
|
|
16687
|
+
if (open.some((p) => p.key === k && p.growsConfirmed === true)) {
|
|
16688
|
+
fail(flags, ExitCode.InputValidation, {
|
|
16689
|
+
error: `--decline "${k}" names a GROWTH ask on a pair that stands CONFIRMED \u2014 a standing confirmation is not re-litigated by a decline`,
|
|
16690
|
+
code: "composition-growth-not-declinable",
|
|
16691
|
+
remediation: "Leave the new instance(s) unconfirmed (they stay unjoined and the ask returns), or confirm to add them to the standing decision."
|
|
16692
|
+
});
|
|
16693
|
+
}
|
|
16435
16694
|
}
|
|
16436
16695
|
const entries = buildCompositionEntries(hostSet, open, declineKeys);
|
|
16437
16696
|
const decided = flags.confirmCompositions === true ? entries : entries.filter((e) => declineKeys.has(e.partner.key));
|
|
@@ -16480,11 +16739,38 @@ function writeCompositionDecisions(hostSet, decided) {
|
|
|
16480
16739
|
if (!parsed.success) return { ok: false, refusal: `compositions[${i}] in the host manifest is not a valid v1 entry \u2014 a standing human decision could not be re-validated, so nothing was written` };
|
|
16481
16740
|
standing.push(parsed.data);
|
|
16482
16741
|
}
|
|
16483
|
-
const
|
|
16484
|
-
const
|
|
16485
|
-
|
|
16486
|
-
|
|
16487
|
-
|
|
16742
|
+
const normKeys = standing.map((c) => fromStoredRel(c.partner.key));
|
|
16743
|
+
const dupKey = normKeys.find((k, i) => normKeys.indexOf(k) !== i);
|
|
16744
|
+
if (dupKey !== void 0) {
|
|
16745
|
+
return { ok: false, refusal: `two standing composition entries normalize to one pair key (${dupKey}) \u2014 repair the manifest by hand before deciding anything new; nothing was written` };
|
|
16746
|
+
}
|
|
16747
|
+
const byIndex = new Map(normKeys.map((k, i) => [k, i]));
|
|
16748
|
+
const fresh = [];
|
|
16749
|
+
const grown = [];
|
|
16750
|
+
const alreadyDecided = [];
|
|
16751
|
+
for (const e of decided) {
|
|
16752
|
+
const at = byIndex.get(fromStoredRel(e.partner.key));
|
|
16753
|
+
if (at === void 0) {
|
|
16754
|
+
fresh.push(e);
|
|
16755
|
+
continue;
|
|
16756
|
+
}
|
|
16757
|
+
const held = standing[at];
|
|
16758
|
+
if (held.status === "confirmed" && e.status === "confirmed") {
|
|
16759
|
+
const covered = new Set(held.instances.map((i) => `${i.hostRep}\0${i.instanceId}`));
|
|
16760
|
+
const delta = e.instances.filter((i) => !covered.has(`${i.hostRep}\0${i.instanceId}`));
|
|
16761
|
+
if (delta.length === 0) {
|
|
16762
|
+
alreadyDecided.push(e.partner.key);
|
|
16763
|
+
continue;
|
|
16764
|
+
}
|
|
16765
|
+
const merged = { ...held, instances: [...held.instances, ...delta] };
|
|
16766
|
+
standing[at] = merged;
|
|
16767
|
+
grown.push(merged);
|
|
16768
|
+
continue;
|
|
16769
|
+
}
|
|
16770
|
+
alreadyDecided.push(e.partner.key);
|
|
16771
|
+
}
|
|
16772
|
+
if (fresh.length > 0 || grown.length > 0) writeManifest(hostSet, { ...raw, compositions: [...standing, ...fresh] });
|
|
16773
|
+
return { ok: true, written: [...fresh, ...grown], alreadyDecided };
|
|
16488
16774
|
}
|
|
16489
16775
|
var COMPOSE_DESCRIPTION, NOTE2, IMPLICIT_PARENT_SCAN_MAX_ENTRIES;
|
|
16490
16776
|
var init_compose2 = __esm({
|
|
@@ -16510,7 +16796,7 @@ var init_compose2 = __esm({
|
|
|
16510
16796
|
output: {
|
|
16511
16797
|
sets: "string[] \u2014 recording sets indexed (--list)",
|
|
16512
16798
|
edges: "per-instance edges: kind (substitution | nested | ask | proposal | external | hidden \u2014 hidden = invisible in every recorded pose, disclosed and never confirmable), partners, pose (variant node id + per-set rep slugs), disclosures (--list)",
|
|
16513
|
-
openPairs: "with --set:
|
|
16799
|
+
openPairs: "with --set: id-backed pairs awaiting a human decision \u2014 undecided pairs, plus GROWTH deltas (growsConfirmed: true) on confirmed pairs whose derivable instance set grew; a growth confirm merges into the standing entry, and --decline refuses growth asks (composition-growth-not-declinable)",
|
|
16514
16800
|
standing: "with --set: persisted confirmations/declines (asked once)",
|
|
16515
16801
|
note: "string \u2014 what this command does NOT do"
|
|
16516
16802
|
},
|
|
@@ -17648,7 +17934,7 @@ function runRecordStatus(opts) {
|
|
|
17648
17934
|
const setDir = path49.resolve(opts.setDir);
|
|
17649
17935
|
const { open, proposals, standing, invalid } = compositionPairsFor(setDir, [opts.library !== void 0 ? path49.resolve(opts.library) : path49.dirname(setDir)]);
|
|
17650
17936
|
return {
|
|
17651
|
-
openPairs: open.map((p) => ({ displayName: p.displayName, pairKey: p.key, instances: p.instances.length })),
|
|
17937
|
+
openPairs: open.map((p) => ({ displayName: p.displayName, pairKey: p.key, instances: p.instances.length, ...p.growsConfirmed === true ? { growsConfirmed: true } : {} })),
|
|
17652
17938
|
// Name-only proposals (field, 2026-08-26): the host-side
|
|
17653
17939
|
// surface an MCP-recorded host embedding a later partner never
|
|
17654
17940
|
// had — not confirmable, but never silent either; the
|
|
@@ -17679,14 +17965,25 @@ function runRecordStatus(opts) {
|
|
|
17679
17965
|
if ("unavailable" in composition) {
|
|
17680
17966
|
process.stdout.write(`COMPOSITION discovery UNAVAILABLE \u2014 ${composition.unavailable}
|
|
17681
17967
|
`);
|
|
17682
|
-
} else
|
|
17683
|
-
|
|
17684
|
-
|
|
17968
|
+
} else {
|
|
17969
|
+
const growth = composition.openPairs.filter((p) => p.growsConfirmed === true);
|
|
17970
|
+
const fresh = composition.openPairs.filter((p) => p.growsConfirmed !== true);
|
|
17971
|
+
if (fresh.length > 0) {
|
|
17972
|
+
process.stdout.write(
|
|
17973
|
+
`COMPOSITION ${fresh.length} unconfirmed partner pair(s): ${fresh.map((p) => `${p.displayName} [pair-key ${p.pairKey}] (${p.instances} instance(s))`).join(", ")} \u2014 this set's recording references other recorded components. Before generating the HOST, record/generate the partner and have a human decide the pairing: \`${tendrilCommand(`compose --set ${path49.resolve(opts.setDir)}`)}\` lists it; confirmation is human-only.
|
|
17685
17974
|
`
|
|
17686
|
-
|
|
17687
|
-
|
|
17688
|
-
|
|
17975
|
+
);
|
|
17976
|
+
}
|
|
17977
|
+
if (growth.length > 0) {
|
|
17978
|
+
process.stdout.write(
|
|
17979
|
+
`COMPOSITION GROWTH: ${growth.map((p) => `${p.displayName} [pair-key ${p.pairKey}] has ${p.instances} newly-derived instance(s) awaiting the same human decision`).join(", ")} \u2014 the standing confirmation is untouched; confirming adds them: \`${tendrilCommand(`compose --set ${path49.resolve(opts.setDir)}`)}\`
|
|
17980
|
+
`
|
|
17981
|
+
);
|
|
17982
|
+
}
|
|
17983
|
+
if (composition.confirmed > 0) {
|
|
17984
|
+
process.stdout.write(`COMPOSITION ${composition.confirmed} confirmed partner pair(s) on this set (composed pins apply at generation)
|
|
17689
17985
|
`);
|
|
17986
|
+
}
|
|
17690
17987
|
}
|
|
17691
17988
|
if (!("unavailable" in composition) && composition.nameOnly.length > 0) {
|
|
17692
17989
|
for (const p of composition.nameOnly) {
|
|
@@ -19727,10 +20024,17 @@ function runEngineBrief(opts) {
|
|
|
19727
20024
|
`COMPOSITION DISCOVERY PARTIAL: the set's parent directory (${skippedParent.dir}) holds ${String(skippedParent.entries)} entries and was not scanned as a recordings library \u2014 sibling sets there are invisible to pairing. Pass --library <dir> to scan a specific library deliberately.`
|
|
19728
20025
|
);
|
|
19729
20026
|
}
|
|
19730
|
-
|
|
19731
|
-
|
|
20027
|
+
const fresh = open.filter((p) => p.growsConfirmed !== true);
|
|
20028
|
+
const growth = open.filter((p) => p.growsConfirmed === true);
|
|
20029
|
+
if (fresh.length > 0) {
|
|
20030
|
+
const componentNames = [...new Set(fresh.map((p) => p.displayName))];
|
|
20031
|
+
disclosures.push(
|
|
20032
|
+
`COMPOSITION PAIRS UNDECIDED: this recording references ${componentNames.length} other recorded component(s) (${fresh.map((p) => `${p.displayName} [pair-key ${p.key}]: ${p.instances.length} instance(s)`).join("; ")}) and no human has decided the pairing. This brief treats the component as SELF-CONTAINED \u2014 you will re-implement the partner's pixels locally. If composition is wanted: generate the partner bundle first, have a human run \`${tendrilCommand(`compose --set ${path55.resolve(task.set)}`)}\` in their terminal, and re-emit this brief. Proceeding as-is is valid; it just is not composition.`
|
|
20033
|
+
);
|
|
20034
|
+
}
|
|
20035
|
+
if (growth.length > 0) {
|
|
19732
20036
|
disclosures.push(
|
|
19733
|
-
`COMPOSITION
|
|
20037
|
+
`COMPOSITION PARTIALLY CONFIRMED: ${growth.map((p) => `${p.displayName} [pair-key ${p.key}] \u2014 ${p.instances.length} newly-derived instance(s) (${p.instances.map((i) => `${i.hostRep}/${i.instanceId}`).join(", ")}) are NOT yet confirmed`).join("; ")}. The confirmed instances compose via the pinned partner module below; the unconfirmed ones you implement LOCALLY from the recorded pose. To compose them too: a human confirms the growth (\`${tendrilCommand(`compose --set ${path55.resolve(task.set)}`)}\`) and this brief is re-emitted.`
|
|
19734
20038
|
);
|
|
19735
20039
|
}
|
|
19736
20040
|
if (proposals.length > 0) {
|
|
@@ -20723,6 +21027,7 @@ async function runPublish(opts) {
|
|
|
20723
21027
|
figmaFile: opened.value.figmaFile,
|
|
20724
21028
|
rulerVersion: opened.value.rulerVersion,
|
|
20725
21029
|
resumed: opened.value.resumed === true,
|
|
21030
|
+
approvalSpent: opened.value.approvalSpent === true,
|
|
20726
21031
|
files: uploaded,
|
|
20727
21032
|
recordings: carried === void 0 ? { carried: false, ...recordingNote === void 0 ? {} : { note: recordingNote } } : {
|
|
20728
21033
|
carried: true,
|
|
@@ -20737,6 +21042,10 @@ async function runPublish(opts) {
|
|
|
20737
21042
|
const reused = uploaded.filter((u) => u.deduplicated).length;
|
|
20738
21043
|
if (opened.value.resumed === true) process.stdout.write(`resumed the unfinished publish of ${componentName}
|
|
20739
21044
|
`);
|
|
21045
|
+
if (opened.value.approvalSpent === true) {
|
|
21046
|
+
process.stdout.write(`your earlier browser Approve of ${componentName} authorized this first publish
|
|
21047
|
+
`);
|
|
21048
|
+
}
|
|
20740
21049
|
process.stdout.write(`published ${componentName} \u2014 ${String(uploaded.length)} files`);
|
|
20741
21050
|
process.stdout.write(reused > 0 ? ` (${String(reused)} you already had)
|
|
20742
21051
|
` : "\n");
|
|
@@ -20858,7 +21167,11 @@ function pendingApprovalPath() {
|
|
|
20858
21167
|
return path58.join(path58.dirname(sessionPath()), "pending-publish.json");
|
|
20859
21168
|
}
|
|
20860
21169
|
async function runApprovalFlow(opts, client, bundleDir, input) {
|
|
20861
|
-
const requested = await client.requestApproval({
|
|
21170
|
+
const requested = await client.requestApproval({
|
|
21171
|
+
componentName: input.componentName,
|
|
21172
|
+
figmaFile: input.figmaFile,
|
|
21173
|
+
...opts.designSystemName === void 0 ? {} : { proposedDesignSystemName: opts.designSystemName }
|
|
21174
|
+
});
|
|
20862
21175
|
if (!requested.ok) refuse(opts, requested, "approval-request-refused");
|
|
20863
21176
|
const approval = requested.value;
|
|
20864
21177
|
const who = await client.whoami?.();
|
|
@@ -21092,10 +21405,8 @@ function openPairsFor(hostSet, roots) {
|
|
|
21092
21405
|
const pairs = substitutionPairs(edges, hostSet);
|
|
21093
21406
|
const { raw } = readManifestFile(hostSet);
|
|
21094
21407
|
const rawStanding = Array.isArray(raw["compositions"]) ? raw["compositions"] : [];
|
|
21095
|
-
const
|
|
21096
|
-
|
|
21097
|
-
);
|
|
21098
|
-
return pairs.filter((p) => !decidedKeys.has(p.key));
|
|
21408
|
+
const standing = rawStanding.map((e) => CompositionEntrySchema.safeParse(e)).filter((p) => p.success).map((p) => p.data);
|
|
21409
|
+
return openPairsAgainstStanding(pairs, standing);
|
|
21099
21410
|
}
|
|
21100
21411
|
async function runComposeApproveStart(flags, hostSet, roots) {
|
|
21101
21412
|
if (!existsSync49(path59.join(hostSet, "recording-set.json"))) {
|
|
@@ -24419,7 +24730,7 @@ function buildProgram() {
|
|
|
24419
24730
|
...local["revoke"] !== void 0 ? { revoke: local["revoke"] } : {}
|
|
24420
24731
|
});
|
|
24421
24732
|
});
|
|
24422
|
-
program.command("publish").description("Put a verified bundle and its evidence on a page, at a URL you can send to someone. Refuses a run the ruler declined.").argument("<bundleDir>", "bundle directory that has already been verified").option("--to <url>", "the portal to publish to (or set TENDRIL_PORTAL_URL)").option("--name <name>", "library label for this component (default: the bundle manifest's own name)").option("--approve-start", "first publish only: request the browser approval, print the link, persist the pending state and exit \u2014 the tendril_publish tool's phase one").option("--approve-wait", "resume a pending approval: poll until the human decides in the browser, then publish \u2014 phase two").option("--wait-window <seconds>", "with --approve-wait: return after this many undecided seconds (exit 0, status approval-pending, the pending slot kept) instead of blocking to the 31-minute cap \u2014 the MCP bridge's bounded-poll shape").option("--recordings <dir>", "the recording set to carry, when the bundle's stamped path is stale (a bundle moved between machines usually has one)").option("--no-recordings", "publish without carrying the recording set \u2014 the component still publishes, but no other machine can pull the recordings it was measured against").action(async (bundleDir, _o, cmd) => {
|
|
24733
|
+
program.command("publish").description("Put a verified bundle and its evidence on a page, at a URL you can send to someone. Refuses a run the ruler declined.").argument("<bundleDir>", "bundle directory that has already been verified").option("--to <url>", "the portal to publish to (or set TENDRIL_PORTAL_URL)").option("--name <name>", "library label for this component (default: the bundle manifest's own name)").option("--design-system-name <name>", "first publish only: propose a display name for the design system on the approve card (the human names or picks it there; without a proposal the card prefills the raw Figma file key)").option("--approve-start", "first publish only: request the browser approval, print the link, persist the pending state and exit \u2014 the tendril_publish tool's phase one").option("--approve-wait", "resume a pending approval: poll until the human decides in the browser, then publish \u2014 phase two").option("--wait-window <seconds>", "with --approve-wait: return after this many undecided seconds (exit 0, status approval-pending, the pending slot kept) instead of blocking to the 31-minute cap \u2014 the MCP bridge's bounded-poll shape").option("--recordings <dir>", "the recording set to carry, when the bundle's stamped path is stale (a bundle moved between machines usually has one)").option("--no-recordings", "publish without carrying the recording set \u2014 the component still publishes, but no other machine can pull the recordings it was measured against").action(async (bundleDir, _o, cmd) => {
|
|
24423
24734
|
const flags = globalFlags(cmd.parent);
|
|
24424
24735
|
const local = cmd.opts();
|
|
24425
24736
|
const argv = cmd.parent?.args ?? process.argv;
|
|
@@ -24429,6 +24740,7 @@ function buildProgram() {
|
|
|
24429
24740
|
bundleDir,
|
|
24430
24741
|
...local["to"] !== void 0 ? { to: local["to"] } : {},
|
|
24431
24742
|
...local["name"] !== void 0 ? { name: local["name"] } : {},
|
|
24743
|
+
...local["designSystemName"] !== void 0 ? { designSystemName: local["designSystemName"] } : {},
|
|
24432
24744
|
...local["approveStart"] !== void 0 ? { approveStart: local["approveStart"] } : {},
|
|
24433
24745
|
...local["approveWait"] !== void 0 ? { approveWait: local["approveWait"] } : {},
|
|
24434
24746
|
// Parsed, never filtered: a bad value must refuse loudly in
|