@tendrilapp/cli 0.1.50 → 0.1.51
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 +17 -0
- package/dist/tendril.js +1167 -348
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -2584,8 +2584,8 @@ var init_src = __esm({
|
|
|
2584
2584
|
function variableNameToPath(name) {
|
|
2585
2585
|
return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
|
|
2586
2586
|
}
|
|
2587
|
-
function tokenPathToCssVar(
|
|
2588
|
-
return `--${
|
|
2587
|
+
function tokenPathToCssVar(path62) {
|
|
2588
|
+
return `--${path62.join("-")}`;
|
|
2589
2589
|
}
|
|
2590
2590
|
function toDtcgToken(variable, defaultMode) {
|
|
2591
2591
|
const modes = Object.keys(variable.valuesByMode);
|
|
@@ -2629,11 +2629,11 @@ function toDtcgToken(variable, defaultMode) {
|
|
|
2629
2629
|
}
|
|
2630
2630
|
function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
2631
2631
|
const entries = variables.map((variable) => {
|
|
2632
|
-
const
|
|
2633
|
-
if (
|
|
2632
|
+
const path62 = variableNameToPath(variable.name);
|
|
2633
|
+
if (path62.length === 0) {
|
|
2634
2634
|
throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
|
|
2635
2635
|
}
|
|
2636
|
-
return { variable, path:
|
|
2636
|
+
return { variable, path: path62 };
|
|
2637
2637
|
});
|
|
2638
2638
|
const groupPrefixes = /* @__PURE__ */ new Set();
|
|
2639
2639
|
for (const e of entries) {
|
|
@@ -2654,21 +2654,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
|
2654
2654
|
}
|
|
2655
2655
|
const tokens = {};
|
|
2656
2656
|
const flat = [];
|
|
2657
|
-
for (const { variable, path:
|
|
2657
|
+
for (const { variable, path: path62 } of entries) {
|
|
2658
2658
|
const token = toDtcgToken(variable, defaultMode);
|
|
2659
2659
|
let group = tokens;
|
|
2660
|
-
for (const segment of
|
|
2660
|
+
for (const segment of path62.slice(0, -1)) {
|
|
2661
2661
|
const existing = group[segment];
|
|
2662
2662
|
group = existing ?? (group[segment] = {});
|
|
2663
2663
|
}
|
|
2664
|
-
const leaf =
|
|
2664
|
+
const leaf = path62[path62.length - 1];
|
|
2665
2665
|
if (group[leaf] !== void 0) {
|
|
2666
|
-
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${
|
|
2666
|
+
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path62.join(".")}" (variable ${variable.id})`);
|
|
2667
2667
|
}
|
|
2668
2668
|
group[leaf] = token;
|
|
2669
2669
|
flat.push({
|
|
2670
|
-
path:
|
|
2671
|
-
cssVar: tokenPathToCssVar(
|
|
2670
|
+
path: path62.join("."),
|
|
2671
|
+
cssVar: tokenPathToCssVar(path62),
|
|
2672
2672
|
type: token.$type,
|
|
2673
2673
|
value: token.$value
|
|
2674
2674
|
});
|
|
@@ -2857,9 +2857,9 @@ function boundId(value) {
|
|
|
2857
2857
|
return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
|
|
2858
2858
|
}
|
|
2859
2859
|
function resolveBinding(ctx, id) {
|
|
2860
|
-
const
|
|
2861
|
-
if (
|
|
2862
|
-
return
|
|
2860
|
+
const path62 = ctx.pathById.get(id);
|
|
2861
|
+
if (path62 === void 0) ctx.unresolved.add(id);
|
|
2862
|
+
return path62;
|
|
2863
2863
|
}
|
|
2864
2864
|
function parseVariantProps(name) {
|
|
2865
2865
|
if (!name.includes("=")) return void 0;
|
|
@@ -2894,8 +2894,8 @@ function walk(ctx, raw) {
|
|
|
2894
2894
|
if (!isObject(paint) || paint["visible"] === false) continue;
|
|
2895
2895
|
const id = boundId(paint);
|
|
2896
2896
|
if (id !== void 0) {
|
|
2897
|
-
const
|
|
2898
|
-
if (
|
|
2897
|
+
const path62 = resolveBinding(ctx, id);
|
|
2898
|
+
if (path62 !== void 0) tokens.add(path62);
|
|
2899
2899
|
} else if (typeof paint["color"] === "string") {
|
|
2900
2900
|
ctx.hardcoded.push({ node: name, property, value: paint["color"] });
|
|
2901
2901
|
}
|
|
@@ -2903,8 +2903,8 @@ function walk(ctx, raw) {
|
|
|
2903
2903
|
}
|
|
2904
2904
|
const radiusId = boundId(raw["cornerRadius"]);
|
|
2905
2905
|
if (radiusId !== void 0) {
|
|
2906
|
-
const
|
|
2907
|
-
if (
|
|
2906
|
+
const path62 = resolveBinding(ctx, radiusId);
|
|
2907
|
+
if (path62 !== void 0) tokens.add(path62);
|
|
2908
2908
|
} else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
|
|
2909
2909
|
ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
|
|
2910
2910
|
}
|
|
@@ -2914,10 +2914,10 @@ function walk(ctx, raw) {
|
|
|
2914
2914
|
layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
|
|
2915
2915
|
const gapId = boundId(raw["itemSpacing"]);
|
|
2916
2916
|
if (gapId !== void 0) {
|
|
2917
|
-
const
|
|
2918
|
-
if (
|
|
2919
|
-
layout.gap =
|
|
2920
|
-
tokens.add(
|
|
2917
|
+
const path62 = resolveBinding(ctx, gapId);
|
|
2918
|
+
if (path62 !== void 0) {
|
|
2919
|
+
layout.gap = path62;
|
|
2920
|
+
tokens.add(path62);
|
|
2921
2921
|
}
|
|
2922
2922
|
} else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
|
|
2923
2923
|
ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
|
|
@@ -2926,10 +2926,10 @@ function walk(ctx, raw) {
|
|
|
2926
2926
|
for (const field of PADDING_FIELDS) {
|
|
2927
2927
|
const id = boundId(raw[field]);
|
|
2928
2928
|
if (id !== void 0) {
|
|
2929
|
-
const
|
|
2930
|
-
if (
|
|
2931
|
-
paddingPaths.push(
|
|
2932
|
-
tokens.add(
|
|
2929
|
+
const path62 = resolveBinding(ctx, id);
|
|
2930
|
+
if (path62 !== void 0) {
|
|
2931
|
+
paddingPaths.push(path62);
|
|
2932
|
+
tokens.add(path62);
|
|
2933
2933
|
}
|
|
2934
2934
|
} else if (typeof raw[field] === "number" && raw[field] !== 0) {
|
|
2935
2935
|
ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
|
|
@@ -5179,10 +5179,10 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
5179
5179
|
continue;
|
|
5180
5180
|
}
|
|
5181
5181
|
const bytes = new Uint8Array(await fileRes.arrayBuffer());
|
|
5182
|
-
const
|
|
5182
|
+
const sha2562 = createHash4("sha256").update(bytes).digest("hex");
|
|
5183
5183
|
const file = path14.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}.woff2`);
|
|
5184
5184
|
writeFileSync3(file, bytes);
|
|
5185
|
-
resolved.push({ family, weight, source: url, sha256, file, license: meta.license, ...meta.family !== null ? { servedFamily: meta.family } : {} });
|
|
5185
|
+
resolved.push({ family, weight, source: url, sha256: sha2562, file, license: meta.license, ...meta.family !== null ? { servedFamily: meta.family } : {} });
|
|
5186
5186
|
} catch (err) {
|
|
5187
5187
|
failures.push({ family, weight, reason: `download failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
5188
5188
|
}
|
|
@@ -5220,10 +5220,10 @@ ${shown}`
|
|
|
5220
5220
|
storedExt = ".ttf";
|
|
5221
5221
|
}
|
|
5222
5222
|
mkdirSync2(cacheDir, { recursive: true });
|
|
5223
|
-
const
|
|
5223
|
+
const sha2562 = createHash4("sha256").update(bytes).digest("hex");
|
|
5224
5224
|
const file = path14.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}${storedExt}`);
|
|
5225
5225
|
writeFileSync3(file, bytes);
|
|
5226
|
-
const face = { family, weight, source: `${provenance}:${path14.basename(src)}`, sha256, file, license: "unknown" };
|
|
5226
|
+
const face = { family, weight, source: `${provenance}:${path14.basename(src)}`, sha256: sha2562, file, license: "unknown" };
|
|
5227
5227
|
const mPath = path14.join(cacheDir, "manifest.json");
|
|
5228
5228
|
const prior = existsSync10(mPath) ? JSON.parse(readFileSync7(mPath, "utf8")) : [];
|
|
5229
5229
|
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path14.basename(file) }];
|
|
@@ -6719,7 +6719,7 @@ function cssProvenanceComment(input) {
|
|
|
6719
6719
|
const unrecorded = input.latticeConfigs === null ? "; coverage denominator unknown" : `; ${Math.max(0, input.latticeConfigs - input.scored)} lattice configs unverified`;
|
|
6720
6720
|
return `/* tendril bundle v${BUNDLE_VERSION} \u2014 ${input.pass}/${input.scored} recorded configs \u2265 pass bar, ${input.certified} certified${unrecorded}. Non-authoritative claim AT STAMP TIME \u2014 any edit invalidates it; recompute with \`tendril verify\`. */`;
|
|
6721
6721
|
}
|
|
6722
|
-
function hashRecordingSet(relPaths, readFile,
|
|
6722
|
+
function hashRecordingSet(relPaths, readFile, sha2562) {
|
|
6723
6723
|
const sorted = [...relPaths].sort();
|
|
6724
6724
|
const chunks = [];
|
|
6725
6725
|
const encoder = new TextEncoder();
|
|
@@ -6728,7 +6728,7 @@ function hashRecordingSet(relPaths, readFile, sha256) {
|
|
|
6728
6728
|
`));
|
|
6729
6729
|
chunks.push(readFile(p));
|
|
6730
6730
|
}
|
|
6731
|
-
return
|
|
6731
|
+
return sha2562(chunks);
|
|
6732
6732
|
}
|
|
6733
6733
|
var BUNDLE_VERSION, ConfigStatusSchema, PinnedPropSchema, PropAdapterSchema, RequiredFontSchema, ConfigClaimSchema, BehaviorClaimSchema, BundleProvenanceSchema, BundleComponentJsonSchema, MAX_BUNDLE_MANIFEST_BYTES, MAX_BUNDLE_SOURCE_BYTES;
|
|
6734
6734
|
var init_bundle = __esm({
|
|
@@ -6857,43 +6857,43 @@ function classifyBundleSurface(files, opts) {
|
|
|
6857
6857
|
const excluded = [];
|
|
6858
6858
|
const unknown = [];
|
|
6859
6859
|
for (const raw of files) {
|
|
6860
|
-
const
|
|
6861
|
-
const inEvidence =
|
|
6862
|
-
if (
|
|
6863
|
-
const fname =
|
|
6860
|
+
const path62 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6861
|
+
const inEvidence = path62.startsWith(`${EVIDENCE_DIR}/`);
|
|
6862
|
+
if (path62.startsWith("fonts/")) {
|
|
6863
|
+
const fname = path62.slice("fonts/".length);
|
|
6864
6864
|
if (!fname.includes("/") && (/\.(woff2?|ttf|otf)$/i.test(fname) || /^(NOTICE|LICENSE|LICENCE)[^/]*\.txt$/i.test(fname))) {
|
|
6865
|
-
excluded.push({ path:
|
|
6865
|
+
excluded.push({ path: path62, reason: "font payload \u2014 not published (fonts policy pending); faces are sha-pinned in component.json requiredFonts" });
|
|
6866
6866
|
continue;
|
|
6867
6867
|
}
|
|
6868
|
-
unknown.push(
|
|
6868
|
+
unknown.push(path62);
|
|
6869
6869
|
continue;
|
|
6870
6870
|
}
|
|
6871
|
-
const name = inEvidence ?
|
|
6871
|
+
const name = inEvidence ? path62.slice(EVIDENCE_DIR.length + 1) : path62;
|
|
6872
6872
|
if (name.includes("/")) {
|
|
6873
|
-
unknown.push(
|
|
6873
|
+
unknown.push(path62);
|
|
6874
6874
|
continue;
|
|
6875
6875
|
}
|
|
6876
6876
|
if (inEvidence) {
|
|
6877
|
-
if (name === "verify-report.json") published.push({ path:
|
|
6878
|
-
else if (name === "diff-legend.txt") published.push({ path:
|
|
6879
|
-
else if (name === "inspect.html") published.push({ path:
|
|
6880
|
-
else if (/-mount-failure\.png$/.test(name)) excluded.push({ path:
|
|
6877
|
+
if (name === "verify-report.json") published.push({ path: path62, role: "verify-report" });
|
|
6878
|
+
else if (name === "diff-legend.txt") published.push({ path: path62, role: "diff-legend" });
|
|
6879
|
+
else if (name === "inspect.html") published.push({ path: path62, role: "inspect-sheet" });
|
|
6880
|
+
else if (/-mount-failure\.png$/.test(name)) excluded.push({ path: path62, reason: "harness failure diagnostic (regenerated every verify run, never published)" });
|
|
6881
6881
|
else {
|
|
6882
6882
|
const hit = EVIDENCE_PATTERNS.find((p) => p.re.test(name));
|
|
6883
|
-
if (hit !== void 0) published.push({ path:
|
|
6884
|
-
else unknown.push(
|
|
6883
|
+
if (hit !== void 0) published.push({ path: path62, role: hit.role });
|
|
6884
|
+
else unknown.push(path62);
|
|
6885
6885
|
}
|
|
6886
6886
|
continue;
|
|
6887
6887
|
}
|
|
6888
|
-
if (name === opts.entry) published.push({ path:
|
|
6889
|
-
else if (name === "styles.css") published.push({ path:
|
|
6890
|
-
else if (name === "tokens.css") published.push({ path:
|
|
6891
|
-
else if (name === "fonts.css") published.push({ path:
|
|
6892
|
-
else if (name === "component.json") published.push({ path:
|
|
6888
|
+
if (name === opts.entry) published.push({ path: path62, role: "entry" });
|
|
6889
|
+
else if (name === "styles.css") published.push({ path: path62, role: "styles" });
|
|
6890
|
+
else if (name === "tokens.css") published.push({ path: path62, role: "tokens" });
|
|
6891
|
+
else if (name === "fonts.css") published.push({ path: path62, role: "fonts" });
|
|
6892
|
+
else if (name === "component.json") published.push({ path: path62, role: "manifest" });
|
|
6893
6893
|
else {
|
|
6894
6894
|
const skip = EXCLUDED.find((e) => e.test(name));
|
|
6895
|
-
if (skip !== void 0) excluded.push({ path:
|
|
6896
|
-
else unknown.push(
|
|
6895
|
+
if (skip !== void 0) excluded.push({ path: path62, reason: skip.reason });
|
|
6896
|
+
else unknown.push(path62);
|
|
6897
6897
|
}
|
|
6898
6898
|
}
|
|
6899
6899
|
const roles = new Set(published.map((p) => p.role));
|
|
@@ -6903,8 +6903,8 @@ function missingInspectCrops(sheetText, publishedPaths) {
|
|
|
6903
6903
|
const held = new Set(publishedPaths);
|
|
6904
6904
|
const missing = /* @__PURE__ */ new Set();
|
|
6905
6905
|
for (const [, name] of sheetText.matchAll(/src="\.\/([^"]+)"/g)) {
|
|
6906
|
-
const
|
|
6907
|
-
if (!held.has(
|
|
6906
|
+
const path62 = `${EVIDENCE_DIR}/${name}`;
|
|
6907
|
+
if (!held.has(path62)) missing.add(path62);
|
|
6908
6908
|
}
|
|
6909
6909
|
return [...missing].sort();
|
|
6910
6910
|
}
|
|
@@ -7012,10 +7012,10 @@ function readScoredFiles(report) {
|
|
|
7012
7012
|
const entries = Object.entries(value);
|
|
7013
7013
|
if (entries.length === 0) return void 0;
|
|
7014
7014
|
const out = {};
|
|
7015
|
-
for (const [
|
|
7016
|
-
if (
|
|
7015
|
+
for (const [path62, digest] of entries) {
|
|
7016
|
+
if (path62 === "" || path62.startsWith("/") || path62.includes("..")) return void 0;
|
|
7017
7017
|
if (!isSetHash(digest)) return void 0;
|
|
7018
|
-
out[
|
|
7018
|
+
out[path62] = digest;
|
|
7019
7019
|
}
|
|
7020
7020
|
return out;
|
|
7021
7021
|
}
|
|
@@ -7023,11 +7023,11 @@ function compareScoredFiles(recorded, actual) {
|
|
|
7023
7023
|
const missing = [];
|
|
7024
7024
|
const unscored = [];
|
|
7025
7025
|
const changed = [];
|
|
7026
|
-
for (const [
|
|
7027
|
-
if (!(
|
|
7028
|
-
else if (actual[
|
|
7026
|
+
for (const [path62, digest] of Object.entries(recorded)) {
|
|
7027
|
+
if (!(path62 in actual)) missing.push(path62);
|
|
7028
|
+
else if (actual[path62] !== digest) changed.push(path62);
|
|
7029
7029
|
}
|
|
7030
|
-
for (const
|
|
7030
|
+
for (const path62 of Object.keys(actual)) if (!(path62 in recorded)) unscored.push(path62);
|
|
7031
7031
|
return { missing: missing.sort(), unscored: unscored.sort(), changed: changed.sort() };
|
|
7032
7032
|
}
|
|
7033
7033
|
function scoredRecordingSetHash(report) {
|
|
@@ -7068,8 +7068,318 @@ var init_motion_css = __esm({
|
|
|
7068
7068
|
}
|
|
7069
7069
|
});
|
|
7070
7070
|
|
|
7071
|
-
// packages/metadata/src/
|
|
7071
|
+
// packages/metadata/src/recording-archive.ts
|
|
7072
7072
|
import { z as z11 } from "zod";
|
|
7073
|
+
function bytesToBase64(bytes) {
|
|
7074
|
+
let out = "";
|
|
7075
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
7076
|
+
const a = bytes[i];
|
|
7077
|
+
const b = i + 1 < bytes.length ? bytes[i + 1] : 0;
|
|
7078
|
+
const c = i + 2 < bytes.length ? bytes[i + 2] : 0;
|
|
7079
|
+
out += B64_ALPHABET[a >> 2] + B64_ALPHABET[(a & 3) << 4 | b >> 4];
|
|
7080
|
+
out += i + 1 < bytes.length ? B64_ALPHABET[(b & 15) << 2 | c >> 6] : "=";
|
|
7081
|
+
out += i + 2 < bytes.length ? B64_ALPHABET[c & 63] : "=";
|
|
7082
|
+
}
|
|
7083
|
+
return out;
|
|
7084
|
+
}
|
|
7085
|
+
function base64ToBytes(text) {
|
|
7086
|
+
if (text.length % 4 !== 0) return void 0;
|
|
7087
|
+
const padIdx = text.indexOf("=");
|
|
7088
|
+
const pad = padIdx === -1 ? 0 : text.length - padIdx;
|
|
7089
|
+
if (pad > 2 || padIdx !== -1 && !/^={1,2}$/.test(text.slice(padIdx))) return void 0;
|
|
7090
|
+
const body = padIdx === -1 ? text : text.slice(0, padIdx);
|
|
7091
|
+
const out = new Uint8Array(text.length / 4 * 3 - pad);
|
|
7092
|
+
let o = 0;
|
|
7093
|
+
let buffer = 0;
|
|
7094
|
+
let bits = 0;
|
|
7095
|
+
for (const ch of body) {
|
|
7096
|
+
const v = B64_ALPHABET.indexOf(ch);
|
|
7097
|
+
if (v === -1) return void 0;
|
|
7098
|
+
buffer = buffer << 6 | v;
|
|
7099
|
+
bits += 6;
|
|
7100
|
+
if (bits >= 8) {
|
|
7101
|
+
bits -= 8;
|
|
7102
|
+
out[o++] = buffer >> bits & 255;
|
|
7103
|
+
}
|
|
7104
|
+
}
|
|
7105
|
+
if (bits > 0 && (buffer & (1 << bits) - 1) !== 0) return void 0;
|
|
7106
|
+
return out;
|
|
7107
|
+
}
|
|
7108
|
+
function readSetShape(manifestBytes) {
|
|
7109
|
+
let parsed;
|
|
7110
|
+
try {
|
|
7111
|
+
parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(manifestBytes));
|
|
7112
|
+
} catch {
|
|
7113
|
+
return { ok: false, refusal: "the carried recording-set.json is not readable JSON" };
|
|
7114
|
+
}
|
|
7115
|
+
const m = parsed;
|
|
7116
|
+
if (typeof m.component !== "string" || m.component === "") return { ok: false, refusal: "the carried recording-set.json names no component" };
|
|
7117
|
+
if (!Array.isArray(m.reps) || m.reps.length === 0) return { ok: false, refusal: "the carried recording-set.json declares no reps" };
|
|
7118
|
+
const reps = [];
|
|
7119
|
+
for (const rep of m.reps) {
|
|
7120
|
+
const slug = rep.slug;
|
|
7121
|
+
if (typeof slug !== "string" || !safeSegment(slug)) {
|
|
7122
|
+
return { ok: false, refusal: `the carried recording-set.json declares a rep slug that is not a safe directory name (${JSON.stringify(slug).slice(0, 80)})` };
|
|
7123
|
+
}
|
|
7124
|
+
if (reps.includes(slug)) return { ok: false, refusal: `the carried recording-set.json declares the rep ${slug} twice` };
|
|
7125
|
+
reps.push(slug);
|
|
7126
|
+
}
|
|
7127
|
+
return {
|
|
7128
|
+
ok: true,
|
|
7129
|
+
shape: {
|
|
7130
|
+
component: m.component,
|
|
7131
|
+
...typeof m.figmaFile === "string" ? { figmaFile: m.figmaFile } : {},
|
|
7132
|
+
channeled: m.channel !== void 0,
|
|
7133
|
+
reps
|
|
7134
|
+
}
|
|
7135
|
+
};
|
|
7136
|
+
}
|
|
7137
|
+
function recordingSetEnumeration(input, fs4) {
|
|
7138
|
+
const relPaths = [];
|
|
7139
|
+
const setLevel = ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json", ...input.channeled ? ["get_motion_context.json"] : []];
|
|
7140
|
+
for (const name of setLevel) {
|
|
7141
|
+
if (fs4.exists(name)) relPaths.push(name);
|
|
7142
|
+
}
|
|
7143
|
+
const perRep = [
|
|
7144
|
+
"get_design_context.json",
|
|
7145
|
+
"get_metadata.json",
|
|
7146
|
+
"get_screenshot.json",
|
|
7147
|
+
"get_variable_defs.json",
|
|
7148
|
+
...input.channeled ? ["get_metadata_interior.json", "rest_nodes.json", "rest_metadata.json", "rest_screenshot.json"] : []
|
|
7149
|
+
];
|
|
7150
|
+
for (const rep of input.reps) {
|
|
7151
|
+
for (const f of perRep) {
|
|
7152
|
+
if (fs4.exists(`${rep}/${f}`)) relPaths.push(`${rep}/${f}`);
|
|
7153
|
+
}
|
|
7154
|
+
for (const asset of fs4.listRep(rep).filter((f) => f.startsWith("asset-"))) {
|
|
7155
|
+
relPaths.push(`${rep}/${asset}`);
|
|
7156
|
+
}
|
|
7157
|
+
}
|
|
7158
|
+
return relPaths;
|
|
7159
|
+
}
|
|
7160
|
+
function archiveMemberAllowed(shape, memberPath) {
|
|
7161
|
+
const setLevel = /* @__PURE__ */ new Set([
|
|
7162
|
+
"recording-set.json",
|
|
7163
|
+
"completeness-manifest.json",
|
|
7164
|
+
"get_variable_defs.json",
|
|
7165
|
+
"get_metadata.json",
|
|
7166
|
+
"get_motion_context.json",
|
|
7167
|
+
...SET_LEVEL_EXTRAS
|
|
7168
|
+
]);
|
|
7169
|
+
if (setLevel.has(memberPath)) return true;
|
|
7170
|
+
const slash = memberPath.indexOf("/");
|
|
7171
|
+
if (slash === -1) return false;
|
|
7172
|
+
const rep = memberPath.slice(0, slash);
|
|
7173
|
+
const name = memberPath.slice(slash + 1);
|
|
7174
|
+
if (!shape.reps.includes(rep)) return false;
|
|
7175
|
+
const perRep = /* @__PURE__ */ new Set([
|
|
7176
|
+
"get_design_context.json",
|
|
7177
|
+
"get_metadata.json",
|
|
7178
|
+
"get_screenshot.json",
|
|
7179
|
+
"get_variable_defs.json",
|
|
7180
|
+
"get_metadata_interior.json",
|
|
7181
|
+
"rest_nodes.json",
|
|
7182
|
+
"rest_metadata.json",
|
|
7183
|
+
"rest_screenshot.json",
|
|
7184
|
+
...PER_REP_EXTRAS
|
|
7185
|
+
]);
|
|
7186
|
+
if (perRep.has(name)) return true;
|
|
7187
|
+
return name.startsWith("asset-") && safeSegment(name);
|
|
7188
|
+
}
|
|
7189
|
+
function carriedExtrasHash(members, enumeration, sha2562) {
|
|
7190
|
+
const hashed = new Set(enumeration);
|
|
7191
|
+
const extras = [...members.keys()].filter((p) => !hashed.has(p)).sort();
|
|
7192
|
+
return hashRecordingSet(extras, (p) => members.get(p), sha2562);
|
|
7193
|
+
}
|
|
7194
|
+
function validateRecordingArchive(envelopeText2, opts) {
|
|
7195
|
+
let parsed;
|
|
7196
|
+
try {
|
|
7197
|
+
parsed = JSON.parse(envelopeText2);
|
|
7198
|
+
} catch {
|
|
7199
|
+
return { ok: false, refusal: "the recording archive is not readable JSON" };
|
|
7200
|
+
}
|
|
7201
|
+
const env = RecordingArchiveEnvelopeSchema.safeParse(parsed);
|
|
7202
|
+
if (!env.success) {
|
|
7203
|
+
return { ok: false, refusal: `the recording archive is not a v${String(RECORDING_ARCHIVE_VERSION)} envelope (${env.error.issues[0]?.message ?? "invalid"})` };
|
|
7204
|
+
}
|
|
7205
|
+
const members = /* @__PURE__ */ new Map();
|
|
7206
|
+
const folded = /* @__PURE__ */ new Set();
|
|
7207
|
+
let previous = "";
|
|
7208
|
+
for (const f of env.data.files) {
|
|
7209
|
+
if (f.path <= previous) {
|
|
7210
|
+
return { ok: false, refusal: `the archive's members are not in canonical order (${JSON.stringify(f.path)} after ${JSON.stringify(previous)})` };
|
|
7211
|
+
}
|
|
7212
|
+
previous = f.path;
|
|
7213
|
+
const lower = f.path.toLowerCase();
|
|
7214
|
+
if (folded.has(lower)) return { ok: false, refusal: `the archive carries two members that collide on a case-insensitive filesystem (${f.path})` };
|
|
7215
|
+
folded.add(lower);
|
|
7216
|
+
const bytes = base64ToBytes(f.bytes);
|
|
7217
|
+
if (bytes === void 0) return { ok: false, refusal: `the archive member ${f.path} is not canonical base64` };
|
|
7218
|
+
if (bytes.length > MAX_MEMBER_BYTES) return { ok: false, refusal: `the archive member ${f.path} exceeds ${String(MAX_MEMBER_BYTES)} bytes` };
|
|
7219
|
+
members.set(f.path, bytes);
|
|
7220
|
+
}
|
|
7221
|
+
const manifestBytes = members.get("recording-set.json");
|
|
7222
|
+
if (manifestBytes === void 0) return { ok: false, refusal: "the archive carries no recording-set.json \u2014 there is no set to reconstruct" };
|
|
7223
|
+
const read = readSetShape(manifestBytes);
|
|
7224
|
+
if (!read.ok) return read;
|
|
7225
|
+
const shape = read.shape;
|
|
7226
|
+
for (const memberPath of members.keys()) {
|
|
7227
|
+
if (!archiveMemberAllowed(shape, memberPath)) {
|
|
7228
|
+
return { ok: false, refusal: `the archive carries a file outside the recording-set vocabulary (${memberPath}) \u2014 refused, never silently unpacked` };
|
|
7229
|
+
}
|
|
7230
|
+
}
|
|
7231
|
+
const enumeration = recordingSetEnumeration(
|
|
7232
|
+
{ channeled: shape.channeled, reps: shape.reps },
|
|
7233
|
+
{
|
|
7234
|
+
exists: (p) => members.has(p),
|
|
7235
|
+
listRep: (rep) => [...members.keys()].filter((p) => p.startsWith(`${rep}/`)).map((p) => p.slice(rep.length + 1))
|
|
7236
|
+
}
|
|
7237
|
+
);
|
|
7238
|
+
const enumerated = new Set(enumeration);
|
|
7239
|
+
for (const memberPath of members.keys()) {
|
|
7240
|
+
if (enumerated.has(memberPath)) continue;
|
|
7241
|
+
const isExtra = SET_LEVEL_EXTRAS.includes(memberPath) || PER_REP_EXTRAS.some((name) => shape.reps.some((rep) => memberPath === `${rep}/${name}`));
|
|
7242
|
+
if (!isExtra) return { ok: false, refusal: `the archive member ${memberPath} is neither hashed nor a named extra for this set's channel` };
|
|
7243
|
+
}
|
|
7244
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
7245
|
+
for (const [memberPath, bytes] of members) {
|
|
7246
|
+
if (!memberPath.endsWith(".json")) continue;
|
|
7247
|
+
try {
|
|
7248
|
+
JSON.parse(utf8.decode(bytes));
|
|
7249
|
+
} catch {
|
|
7250
|
+
return { ok: false, refusal: `the archive member ${memberPath} is not readable JSON \u2014 these are not verbatim recorded envelopes` };
|
|
7251
|
+
}
|
|
7252
|
+
}
|
|
7253
|
+
for (const rep of shape.reps) {
|
|
7254
|
+
if (!members.has(`${rep}/get_metadata.json`)) {
|
|
7255
|
+
return { ok: false, refusal: `the archive carries no get_metadata.json for the rep ${rep} \u2014 a set missing recorded metadata cannot be reconstructed` };
|
|
7256
|
+
}
|
|
7257
|
+
}
|
|
7258
|
+
const derived = hashRecordingSet(enumeration, (p) => members.get(p), opts.sha256);
|
|
7259
|
+
if (derived !== env.data.recordingSetHash) {
|
|
7260
|
+
return { ok: false, refusal: `the archive does not re-derive its own declared recording-set hash (${derived.slice(0, 12)}\u2026 vs ${env.data.recordingSetHash.slice(0, 12)}\u2026)` };
|
|
7261
|
+
}
|
|
7262
|
+
if (opts.expectedSetHash !== void 0 && derived !== opts.expectedSetHash) {
|
|
7263
|
+
return { ok: false, refusal: `the archive re-derives ${derived.slice(0, 12)}\u2026 but the publication's scored recording set is ${opts.expectedSetHash.slice(0, 12)}\u2026 \u2014 these are not the recordings the verdict was measured against` };
|
|
7264
|
+
}
|
|
7265
|
+
const extras = carriedExtrasHash(members, enumeration, opts.sha256);
|
|
7266
|
+
if (extras !== env.data.carriedExtrasHash) {
|
|
7267
|
+
return {
|
|
7268
|
+
ok: false,
|
|
7269
|
+
refusal: `the archive does not re-derive the digest of the recordings it carries outside the scored set (${extras.slice(0, 12)}\u2026 vs ${env.data.carriedExtrasHash.slice(0, 12)}\u2026) \u2014 the motion, interior geometry or bindings travelling with this set are not the ones it was packed with`
|
|
7270
|
+
};
|
|
7271
|
+
}
|
|
7272
|
+
if (env.data.component !== shape.component) {
|
|
7273
|
+
return { ok: false, refusal: `the archive claims component ${JSON.stringify(env.data.component)} but its recording-set.json says ${JSON.stringify(shape.component)}` };
|
|
7274
|
+
}
|
|
7275
|
+
if ((env.data.figmaFile ?? void 0) !== (shape.figmaFile ?? void 0)) {
|
|
7276
|
+
return { ok: false, refusal: "the archive's declared figmaFile disagrees with its recording-set.json" };
|
|
7277
|
+
}
|
|
7278
|
+
for (const [memberPath, bytes] of members) {
|
|
7279
|
+
if (!memberPath.endsWith("/rest_instances.json")) continue;
|
|
7280
|
+
const rep = memberPath.slice(0, -"/rest_instances.json".length);
|
|
7281
|
+
const check = restInstancesSelfCheck(rep, bytes, members, opts.sha256);
|
|
7282
|
+
if (check !== void 0) return { ok: false, refusal: check };
|
|
7283
|
+
}
|
|
7284
|
+
return { ok: true, shape, setHash: derived, members };
|
|
7285
|
+
}
|
|
7286
|
+
function restInstancesSelfCheck(rep, bytes, members, sha2562) {
|
|
7287
|
+
let parsed;
|
|
7288
|
+
try {
|
|
7289
|
+
parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
7290
|
+
} catch {
|
|
7291
|
+
return `${rep}/rest_instances.json is not readable JSON`;
|
|
7292
|
+
}
|
|
7293
|
+
const va = parsed.verifiedAgainst;
|
|
7294
|
+
if (va === void 0 || typeof va.metadataSha256 !== "string") return `${rep}/rest_instances.json carries no verifiedAgainst record`;
|
|
7295
|
+
const pairs = [
|
|
7296
|
+
[va.metadataSha256, `${rep}/get_metadata.json`],
|
|
7297
|
+
...va.screenshotSha256 !== void 0 ? [[va.screenshotSha256, `${rep}/get_screenshot.json`]] : [],
|
|
7298
|
+
...va.contextSha256 !== void 0 ? [[va.contextSha256, `${rep}/get_design_context.json`]] : []
|
|
7299
|
+
];
|
|
7300
|
+
for (const [declared, memberPath] of pairs) {
|
|
7301
|
+
const target = members.get(memberPath);
|
|
7302
|
+
if (target === void 0) return `${rep}/rest_instances.json is verified against ${memberPath}, which the archive does not carry`;
|
|
7303
|
+
if (typeof declared !== "string" || !SHA64.test(declared) || sha2562([target]) !== declared) {
|
|
7304
|
+
return `${rep}/rest_instances.json does not verify against the carried ${memberPath} \u2014 carried enrichment must not outrank carried truth`;
|
|
7305
|
+
}
|
|
7306
|
+
}
|
|
7307
|
+
return void 0;
|
|
7308
|
+
}
|
|
7309
|
+
function packRecordingArchive(fs4, opts) {
|
|
7310
|
+
if (!fs4.exists("recording-set.json")) return { ok: false, refusal: "this directory holds no recording-set.json" };
|
|
7311
|
+
const read = readSetShape(fs4.read("recording-set.json"));
|
|
7312
|
+
if (!read.ok) return read;
|
|
7313
|
+
const shape = read.shape;
|
|
7314
|
+
const listRepSafe = (rep) => fs4.listRep(rep).filter((name) => safeSegment(name));
|
|
7315
|
+
const enumeration = recordingSetEnumeration({ channeled: shape.channeled, reps: shape.reps }, { exists: fs4.exists, listRep: listRepSafe });
|
|
7316
|
+
const paths = new Set(enumeration);
|
|
7317
|
+
for (const extra of SET_LEVEL_EXTRAS) {
|
|
7318
|
+
if (fs4.exists(extra)) paths.add(extra);
|
|
7319
|
+
}
|
|
7320
|
+
for (const rep of shape.reps) {
|
|
7321
|
+
for (const extra of PER_REP_EXTRAS) {
|
|
7322
|
+
if (fs4.exists(`${rep}/${extra}`)) paths.add(`${rep}/${extra}`);
|
|
7323
|
+
}
|
|
7324
|
+
}
|
|
7325
|
+
const members = /* @__PURE__ */ new Map();
|
|
7326
|
+
for (const p of paths) members.set(p, fs4.read(p));
|
|
7327
|
+
const staleEnrichment = [];
|
|
7328
|
+
for (const memberPath of [...members.keys()]) {
|
|
7329
|
+
if (!memberPath.endsWith("/rest_instances.json")) continue;
|
|
7330
|
+
const rep = memberPath.slice(0, -"/rest_instances.json".length);
|
|
7331
|
+
if (restInstancesSelfCheck(rep, members.get(memberPath), members, opts.sha256) !== void 0) {
|
|
7332
|
+
members.delete(memberPath);
|
|
7333
|
+
staleEnrichment.push(memberPath);
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
7336
|
+
const setHash = hashRecordingSet(enumeration, (p) => members.get(p), opts.sha256);
|
|
7337
|
+
const envelope = {
|
|
7338
|
+
version: RECORDING_ARCHIVE_VERSION,
|
|
7339
|
+
component: shape.component,
|
|
7340
|
+
...shape.figmaFile !== void 0 ? { figmaFile: shape.figmaFile } : {},
|
|
7341
|
+
recordingSetHash: setHash,
|
|
7342
|
+
// Computed AFTER stale enrichment is dropped, so the digest covers
|
|
7343
|
+
// what actually travels rather than what was on disk.
|
|
7344
|
+
carriedExtrasHash: carriedExtrasHash(members, enumeration, opts.sha256),
|
|
7345
|
+
files: [...members.keys()].sort().map((p) => ({ path: p, bytes: bytesToBase64(members.get(p)) }))
|
|
7346
|
+
};
|
|
7347
|
+
const envelopeText2 = JSON.stringify(envelope);
|
|
7348
|
+
const check = validateRecordingArchive(envelopeText2, { sha256: opts.sha256, expectedSetHash: setHash });
|
|
7349
|
+
if (!check.ok) return { ok: false, refusal: `the packed archive failed its own gate: ${check.refusal}` };
|
|
7350
|
+
return { ok: true, envelopeText: envelopeText2, setHash, staleEnrichment };
|
|
7351
|
+
}
|
|
7352
|
+
var RECORDING_ARCHIVE_VERSION, MAX_ARCHIVE_MEMBERS, MAX_MEMBER_BYTES, SHA64, B64_ALPHABET, safeSegment, SET_LEVEL_EXTRAS, PER_REP_EXTRAS, RecordingArchiveEnvelopeSchema;
|
|
7353
|
+
var init_recording_archive = __esm({
|
|
7354
|
+
"packages/metadata/src/recording-archive.ts"() {
|
|
7355
|
+
"use strict";
|
|
7356
|
+
init_bundle();
|
|
7357
|
+
RECORDING_ARCHIVE_VERSION = 1;
|
|
7358
|
+
MAX_ARCHIVE_MEMBERS = 4096;
|
|
7359
|
+
MAX_MEMBER_BYTES = 5e6;
|
|
7360
|
+
SHA64 = /^[0-9a-f]{64}$/;
|
|
7361
|
+
B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
7362
|
+
safeSegment = (s) => s !== "" && s !== "." && s !== ".." && s.length <= 200 && !/[/\\\0\s]/.test(s) && !s.startsWith(".");
|
|
7363
|
+
SET_LEVEL_EXTRAS = ["get_motion_context.json"];
|
|
7364
|
+
PER_REP_EXTRAS = ["get_metadata_interior.json", "rest_instances.json"];
|
|
7365
|
+
RecordingArchiveEnvelopeSchema = z11.object({
|
|
7366
|
+
version: z11.literal(RECORDING_ARCHIVE_VERSION),
|
|
7367
|
+
component: z11.string().min(1).max(256),
|
|
7368
|
+
figmaFile: z11.string().min(1).max(128).optional(),
|
|
7369
|
+
recordingSetHash: z11.string().regex(SHA64),
|
|
7370
|
+
/** The carried-but-unhashed members, digested (see
|
|
7371
|
+
* `carriedExtrasHash`). Required: no v1 envelope has ever existed
|
|
7372
|
+
* without it — the carrier has never shipped and migration 0011 is
|
|
7373
|
+
* applied nowhere — so this is additive at zero cost rather than a
|
|
7374
|
+
* version bump nobody would be able to interpret later. */
|
|
7375
|
+
carriedExtrasHash: z11.string().regex(SHA64),
|
|
7376
|
+
files: z11.array(z11.object({ path: z11.string().min(1).max(1024), bytes: z11.string() }).strict()).min(1).max(MAX_ARCHIVE_MEMBERS)
|
|
7377
|
+
}).strict();
|
|
7378
|
+
}
|
|
7379
|
+
});
|
|
7380
|
+
|
|
7381
|
+
// packages/metadata/src/profile.ts
|
|
7382
|
+
import { z as z12 } from "zod";
|
|
7073
7383
|
function readCodebaseProfile(raw) {
|
|
7074
7384
|
return CodebaseProfileSchema.parse(JSON.parse(raw));
|
|
7075
7385
|
}
|
|
@@ -7104,11 +7414,11 @@ var init_profile = __esm({
|
|
|
7104
7414
|
maxCustomProperties: 5e3,
|
|
7105
7415
|
maxComponents: 5e3
|
|
7106
7416
|
};
|
|
7107
|
-
CssCustomPropertyName =
|
|
7108
|
-
RelPath =
|
|
7417
|
+
CssCustomPropertyName = z12.string().min(3).max(200).regex(/^--[^\s{}();:'"]+$/u);
|
|
7418
|
+
RelPath = z12.string().min(1).max(400).refine((p) => !p.startsWith("/") && !p.includes("\\") && !p.includes(".."), {
|
|
7109
7419
|
message: "path must be repo-relative POSIX"
|
|
7110
7420
|
});
|
|
7111
|
-
StylingDialectSchema =
|
|
7421
|
+
StylingDialectSchema = z12.enum([
|
|
7112
7422
|
"tailwind-v3",
|
|
7113
7423
|
"tailwind-v4",
|
|
7114
7424
|
"css-modules",
|
|
@@ -7123,26 +7433,26 @@ var init_profile = __esm({
|
|
|
7123
7433
|
"styled-jsx",
|
|
7124
7434
|
"shadcn-style"
|
|
7125
7435
|
]);
|
|
7126
|
-
DialectEvidenceSchema =
|
|
7436
|
+
DialectEvidenceSchema = z12.object({
|
|
7127
7437
|
dialect: StylingDialectSchema,
|
|
7128
7438
|
/** A file proves a dialect is USED. A dependency only proves it is
|
|
7129
7439
|
* INSTALLED — kept separate because a stale devDependency is not a
|
|
7130
7440
|
* convention, and only file evidence may set `primary`. */
|
|
7131
|
-
source:
|
|
7132
|
-
detail:
|
|
7441
|
+
source: z12.enum(["file", "dependency"]),
|
|
7442
|
+
detail: z12.string().max(200)
|
|
7133
7443
|
});
|
|
7134
|
-
CustomPropertyDeclarationSchema =
|
|
7444
|
+
CustomPropertyDeclarationSchema = z12.object({
|
|
7135
7445
|
file: RelPath,
|
|
7136
|
-
line:
|
|
7137
|
-
selector:
|
|
7138
|
-
scope:
|
|
7446
|
+
line: z12.number().int().nonnegative(),
|
|
7447
|
+
selector: z12.string().max(300),
|
|
7448
|
+
scope: z12.enum(["root", "theme-block", "class", "id", "attribute", "media", "supports", "other"]),
|
|
7139
7449
|
/** Present only for conditional at-rules, e.g. "(prefers-color-scheme: dark)". */
|
|
7140
|
-
condition:
|
|
7141
|
-
value:
|
|
7450
|
+
condition: z12.string().max(300).optional(),
|
|
7451
|
+
value: z12.string().max(500),
|
|
7142
7452
|
/** A terminal literal, when the var() chain could be followed without
|
|
7143
7453
|
* leaving this declaration's scope group. */
|
|
7144
|
-
resolved:
|
|
7145
|
-
resolvedVia:
|
|
7454
|
+
resolved: z12.string().max(500).nullable(),
|
|
7455
|
+
resolvedVia: z12.enum([
|
|
7146
7456
|
"literal",
|
|
7147
7457
|
"alias-chain",
|
|
7148
7458
|
/** Target is declared in the scan, but only under a different scope —
|
|
@@ -7157,11 +7467,11 @@ var init_profile = __esm({
|
|
|
7157
7467
|
"unresolved-function"
|
|
7158
7468
|
])
|
|
7159
7469
|
});
|
|
7160
|
-
CustomPropertySchema =
|
|
7470
|
+
CustomPropertySchema = z12.object({
|
|
7161
7471
|
name: CssCustomPropertyName,
|
|
7162
|
-
declarations:
|
|
7472
|
+
declarations: z12.array(CustomPropertyDeclarationSchema).min(1).max(64)
|
|
7163
7473
|
});
|
|
7164
|
-
ComponentOutcomeSchema =
|
|
7474
|
+
ComponentOutcomeSchema = z12.enum([
|
|
7165
7475
|
"parsed",
|
|
7166
7476
|
"parsed-no-props",
|
|
7167
7477
|
"no-component-found",
|
|
@@ -7169,7 +7479,7 @@ var init_profile = __esm({
|
|
|
7169
7479
|
"skipped-too-large",
|
|
7170
7480
|
"skipped-cap"
|
|
7171
7481
|
]);
|
|
7172
|
-
ComponentBlindSpotSchema =
|
|
7482
|
+
ComponentBlindSpotSchema = z12.enum([
|
|
7173
7483
|
"multiple-components-in-file",
|
|
7174
7484
|
"re-export-only",
|
|
7175
7485
|
"cva-variants",
|
|
@@ -7177,40 +7487,40 @@ var init_profile = __esm({
|
|
|
7177
7487
|
"hoc-wrapper",
|
|
7178
7488
|
"props-from-imported-types"
|
|
7179
7489
|
]);
|
|
7180
|
-
ComponentEntrySchema =
|
|
7490
|
+
ComponentEntrySchema = z12.object({
|
|
7181
7491
|
file: RelPath,
|
|
7182
7492
|
outcome: ComponentOutcomeSchema,
|
|
7183
7493
|
/** Identifier-shaped, from the source. Absent for anonymous exports. */
|
|
7184
|
-
names:
|
|
7185
|
-
propCount:
|
|
7186
|
-
exportStyle:
|
|
7187
|
-
blindSpots:
|
|
7188
|
-
});
|
|
7189
|
-
HistogramSchema =
|
|
7190
|
-
counts:
|
|
7191
|
-
total:
|
|
7192
|
-
dominant:
|
|
7193
|
-
});
|
|
7194
|
-
FormattingSchema =
|
|
7195
|
-
source:
|
|
7196
|
-
indentStyle:
|
|
7197
|
-
indentWidth:
|
|
7198
|
-
quotes:
|
|
7199
|
-
semicolons:
|
|
7200
|
-
trailingComma:
|
|
7201
|
-
});
|
|
7202
|
-
CodebaseProfileSchema =
|
|
7203
|
-
profileVersion:
|
|
7494
|
+
names: z12.array(z12.string().max(120).regex(/^[A-Za-z_$][A-Za-z0-9_$]*$/)).max(40),
|
|
7495
|
+
propCount: z12.number().int().nonnegative(),
|
|
7496
|
+
exportStyle: z12.enum(["named", "default", "both", "none"]),
|
|
7497
|
+
blindSpots: z12.array(ComponentBlindSpotSchema).max(8)
|
|
7498
|
+
});
|
|
7499
|
+
HistogramSchema = z12.object({
|
|
7500
|
+
counts: z12.record(z12.string().max(60), z12.number().int().nonnegative()),
|
|
7501
|
+
total: z12.number().int().nonnegative(),
|
|
7502
|
+
dominant: z12.string().max(60).nullable()
|
|
7503
|
+
});
|
|
7504
|
+
FormattingSchema = z12.object({
|
|
7505
|
+
source: z12.enum(["prettier-config", "editorconfig", "source-histogram", "none"]),
|
|
7506
|
+
indentStyle: z12.enum(["space", "tab"]).nullable(),
|
|
7507
|
+
indentWidth: z12.number().int().min(1).max(16).nullable(),
|
|
7508
|
+
quotes: z12.enum(["single", "double"]).nullable(),
|
|
7509
|
+
semicolons: z12.boolean().nullable(),
|
|
7510
|
+
trailingComma: z12.enum(["none", "es5", "all"]).nullable()
|
|
7511
|
+
});
|
|
7512
|
+
CodebaseProfileSchema = z12.object({
|
|
7513
|
+
profileVersion: z12.literal(CODEBASE_PROFILE_VERSION),
|
|
7204
7514
|
/** The scan's identity. Sha256 over every file actually read (path + bytes),
|
|
7205
7515
|
* in sorted order — so two scans of the same tree agree and a changed tree
|
|
7206
7516
|
* does not. */
|
|
7207
|
-
inputsHash:
|
|
7208
|
-
scannedAt:
|
|
7517
|
+
inputsHash: z12.string().regex(/^[0-9a-f]{64}$/),
|
|
7518
|
+
scannedAt: z12.string(),
|
|
7209
7519
|
/** Whether the walk finished without hitting a cap. When false,
|
|
7210
7520
|
* `limitsHit` names which. */
|
|
7211
|
-
complete:
|
|
7212
|
-
limitsHit:
|
|
7213
|
-
|
|
7521
|
+
complete: z12.boolean(),
|
|
7522
|
+
limitsHit: z12.array(
|
|
7523
|
+
z12.enum([
|
|
7214
7524
|
"files",
|
|
7215
7525
|
"depth",
|
|
7216
7526
|
"totalBytes",
|
|
@@ -7221,51 +7531,51 @@ var init_profile = __esm({
|
|
|
7221
7531
|
"stylingEvidence"
|
|
7222
7532
|
])
|
|
7223
7533
|
).max(8),
|
|
7224
|
-
read:
|
|
7225
|
-
files:
|
|
7226
|
-
bytes:
|
|
7227
|
-
packages:
|
|
7534
|
+
read: z12.object({
|
|
7535
|
+
files: z12.number().int().nonnegative(),
|
|
7536
|
+
bytes: z12.number().int().nonnegative(),
|
|
7537
|
+
packages: z12.number().int().nonnegative(),
|
|
7228
7538
|
/** Files the walk saw but could not parse, with why. */
|
|
7229
|
-
unparsed:
|
|
7230
|
-
|
|
7539
|
+
unparsed: z12.array(
|
|
7540
|
+
z12.object({
|
|
7231
7541
|
file: RelPath,
|
|
7232
|
-
reason:
|
|
7542
|
+
reason: z12.enum(["preprocessor-syntax", "parse-error", "too-large", "unreadable"])
|
|
7233
7543
|
})
|
|
7234
7544
|
).max(500)
|
|
7235
7545
|
}),
|
|
7236
|
-
styling:
|
|
7546
|
+
styling: z12.object({
|
|
7237
7547
|
/** Set ONLY when exactly one dialect has FILE evidence. Never guessed —
|
|
7238
7548
|
* a repo with three styling systems gets null and `ambiguous: true`,
|
|
7239
7549
|
* which is the truth. */
|
|
7240
7550
|
primary: StylingDialectSchema.nullable(),
|
|
7241
|
-
ambiguous:
|
|
7242
|
-
detected:
|
|
7551
|
+
ambiguous: z12.boolean(),
|
|
7552
|
+
detected: z12.array(DialectEvidenceSchema).max(40)
|
|
7243
7553
|
}),
|
|
7244
|
-
css:
|
|
7245
|
-
customProperties:
|
|
7554
|
+
css: z12.object({
|
|
7555
|
+
customProperties: z12.array(CustomPropertySchema).max(PROFILE_LIMITS.maxCustomProperties),
|
|
7246
7556
|
/** Names rejected by the ident class, and values too long to carry.
|
|
7247
7557
|
* Counted so their absence is never read as "the repo declares none". */
|
|
7248
|
-
droppedNames:
|
|
7249
|
-
droppedValues:
|
|
7558
|
+
droppedNames: z12.number().int().nonnegative(),
|
|
7559
|
+
droppedValues: z12.number().int().nonnegative()
|
|
7250
7560
|
}),
|
|
7251
|
-
components:
|
|
7252
|
-
entries:
|
|
7561
|
+
components: z12.object({
|
|
7562
|
+
entries: z12.array(ComponentEntrySchema).max(PROFILE_LIMITS.maxComponents),
|
|
7253
7563
|
fileNaming: HistogramSchema,
|
|
7254
7564
|
directoryLayout: HistogramSchema,
|
|
7255
7565
|
exportStyle: HistogramSchema,
|
|
7256
7566
|
classNameStyle: HistogramSchema,
|
|
7257
7567
|
colocation: HistogramSchema,
|
|
7258
|
-
barrelFiles:
|
|
7259
|
-
refForwarding:
|
|
7260
|
-
forwardRef:
|
|
7261
|
-
asChild:
|
|
7568
|
+
barrelFiles: z12.number().int().nonnegative(),
|
|
7569
|
+
refForwarding: z12.object({
|
|
7570
|
+
forwardRef: z12.number().int().nonnegative(),
|
|
7571
|
+
asChild: z12.number().int().nonnegative()
|
|
7262
7572
|
}),
|
|
7263
7573
|
formatting: FormattingSchema
|
|
7264
7574
|
}),
|
|
7265
7575
|
/** Capability limits this scan hit, in the user's words. A Tailwind v3
|
|
7266
7576
|
* theme lives in a JS config we refuse to execute; that is a real,
|
|
7267
7577
|
* disclosed limit, not an oversight. */
|
|
7268
|
-
disclosures:
|
|
7578
|
+
disclosures: z12.array(z12.string().max(400)).max(30)
|
|
7269
7579
|
});
|
|
7270
7580
|
}
|
|
7271
7581
|
});
|
|
@@ -7282,6 +7592,7 @@ var init_src4 = __esm({
|
|
|
7282
7592
|
init_verify_report();
|
|
7283
7593
|
init_published_surface();
|
|
7284
7594
|
init_motion_css();
|
|
7595
|
+
init_recording_archive();
|
|
7285
7596
|
init_profile();
|
|
7286
7597
|
}
|
|
7287
7598
|
});
|
|
@@ -9106,6 +9417,20 @@ var init_publish_client = __esm({
|
|
|
9106
9417
|
retryable: true
|
|
9107
9418
|
});
|
|
9108
9419
|
}
|
|
9420
|
+
resolveRecordings(input) {
|
|
9421
|
+
const query = new URLSearchParams({ component: input.component, ...input.figmaFile === void 0 ? {} : { figmaFile: input.figmaFile } });
|
|
9422
|
+
return this.request("GET", `/api/recordings?${query.toString()}`, { body: "", contentType: "application/json", retryable: true });
|
|
9423
|
+
}
|
|
9424
|
+
downloadRecordings(input) {
|
|
9425
|
+
return this.requestBytes("GET", `/api/publications/${encodeURIComponent(input.publicationId)}/recordings`);
|
|
9426
|
+
}
|
|
9427
|
+
carryRecordings(input) {
|
|
9428
|
+
return this.request("PUT", `/api/publications/${encodeURIComponent(input.publicationId)}/recordings`, {
|
|
9429
|
+
body: new TextEncoder().encode(input.envelopeText),
|
|
9430
|
+
contentType: "application/octet-stream",
|
|
9431
|
+
retryable: true
|
|
9432
|
+
});
|
|
9433
|
+
}
|
|
9109
9434
|
commit(input) {
|
|
9110
9435
|
return this.json("POST", `/api/publications/${encodeURIComponent(input.publicationId)}/commit`, {}, true);
|
|
9111
9436
|
}
|
|
@@ -9132,7 +9457,15 @@ var init_publish_client = __esm({
|
|
|
9132
9457
|
json(method, pathname, body, retryable = false) {
|
|
9133
9458
|
return this.request(method, pathname, { body: JSON.stringify(body), contentType: "application/json", retryable });
|
|
9134
9459
|
}
|
|
9135
|
-
|
|
9460
|
+
/**
|
|
9461
|
+
* The transport, minus any opinion about what comes back.
|
|
9462
|
+
*
|
|
9463
|
+
* Split out when the recordings carrier needed a response read as
|
|
9464
|
+
* BYTES rather than JSON: retries, timeouts and the unreachable-host
|
|
9465
|
+
* refusal are properties of the call, not of the body format, and two
|
|
9466
|
+
* copies of a retry policy is two policies.
|
|
9467
|
+
*/
|
|
9468
|
+
async attempt(method, pathname, init) {
|
|
9136
9469
|
let response;
|
|
9137
9470
|
let lastError = "";
|
|
9138
9471
|
const attempts = init.retryable === true ? RETRIES : 1;
|
|
@@ -9168,6 +9501,12 @@ var init_publish_client = __esm({
|
|
|
9168
9501
|
}
|
|
9169
9502
|
break;
|
|
9170
9503
|
}
|
|
9504
|
+
return { ok: true, response };
|
|
9505
|
+
}
|
|
9506
|
+
async request(method, pathname, init) {
|
|
9507
|
+
const sent = await this.attempt(method, pathname, init);
|
|
9508
|
+
if (!sent.ok) return sent;
|
|
9509
|
+
const { response } = sent;
|
|
9171
9510
|
const text = await response.text();
|
|
9172
9511
|
let parsed;
|
|
9173
9512
|
try {
|
|
@@ -9179,15 +9518,46 @@ var init_publish_client = __esm({
|
|
|
9179
9518
|
refusal: `${this.origin} answered with ${String(response.status)} and something that is not JSON, so it is probably not a Tendril portal`
|
|
9180
9519
|
};
|
|
9181
9520
|
}
|
|
9182
|
-
const record = parsed ?? {};
|
|
9183
9521
|
if (response.ok) return { ok: true, value: parsed };
|
|
9522
|
+
return this.refusalFrom(response.status, parsed);
|
|
9523
|
+
}
|
|
9524
|
+
/**
|
|
9525
|
+
* A response read as BYTES — the recording archive, which is a
|
|
9526
|
+
* document to store rather than a shape this client owns.
|
|
9527
|
+
*
|
|
9528
|
+
* The SUCCESS path never parses; the refusal path still does, because
|
|
9529
|
+
* a portal refusal is JSON whatever the caller asked for.
|
|
9530
|
+
*/
|
|
9531
|
+
async requestBytes(method, pathname) {
|
|
9532
|
+
const sent = await this.attempt(method, pathname, { body: "", contentType: "application/json", retryable: true });
|
|
9533
|
+
if (!sent.ok) return sent;
|
|
9534
|
+
const { response } = sent;
|
|
9535
|
+
if (response.ok) return { ok: true, value: new Uint8Array(await response.arrayBuffer()) };
|
|
9536
|
+
const text = await response.text();
|
|
9537
|
+
let parsed;
|
|
9538
|
+
try {
|
|
9539
|
+
parsed = JSON.parse(text);
|
|
9540
|
+
} catch {
|
|
9541
|
+
return {
|
|
9542
|
+
ok: false,
|
|
9543
|
+
status: response.status,
|
|
9544
|
+
refusal: `${this.origin} answered with ${String(response.status)} and something that is not JSON, so it is probably not a Tendril portal`
|
|
9545
|
+
};
|
|
9546
|
+
}
|
|
9547
|
+
return this.refusalFrom(response.status, parsed);
|
|
9548
|
+
}
|
|
9549
|
+
refusalFrom(status, parsed) {
|
|
9550
|
+
const record = parsed ?? {};
|
|
9184
9551
|
return {
|
|
9185
9552
|
ok: false,
|
|
9186
|
-
status
|
|
9187
|
-
refusal: typeof record["refusal"] === "string" ? record["refusal"] : `the portal refused with ${String(
|
|
9553
|
+
status,
|
|
9554
|
+
refusal: typeof record["refusal"] === "string" ? record["refusal"] : `the portal refused with ${String(status)}`,
|
|
9188
9555
|
...Array.isArray(record["detail"]) ? { detail: record["detail"] } : {},
|
|
9189
9556
|
...Array.isArray(record["missing"]) ? { missing: record["missing"] } : {},
|
|
9190
|
-
...isRecord(record["needsConfirmation"]) ? { needsConfirmation: record["needsConfirmation"] } : {}
|
|
9557
|
+
...isRecord(record["needsConfirmation"]) ? { needsConfirmation: record["needsConfirmation"] } : {},
|
|
9558
|
+
...Array.isArray(record["candidates"]) ? { candidates: record["candidates"] } : {},
|
|
9559
|
+
...record["alreadyCarried"] === true ? { alreadyCarried: true } : {},
|
|
9560
|
+
...typeof record["setHash"] === "string" ? { setHash: record["setHash"] } : {}
|
|
9191
9561
|
};
|
|
9192
9562
|
}
|
|
9193
9563
|
};
|
|
@@ -9642,7 +10012,7 @@ var init_doctor = __esm({
|
|
|
9642
10012
|
});
|
|
9643
10013
|
|
|
9644
10014
|
// packages/llm/src/model-config.ts
|
|
9645
|
-
import { z as
|
|
10015
|
+
import { z as z13 } from "zod";
|
|
9646
10016
|
function resolveModel(config, requestedId) {
|
|
9647
10017
|
const entry = config.allowlist.find((m) => m.id === requestedId);
|
|
9648
10018
|
if (entry) return { ok: true, entry };
|
|
@@ -9664,46 +10034,46 @@ var ModelStatusSchema, ModelEntrySchema, ModelConfigSchema, DEFAULT_MODEL_CONFIG
|
|
|
9664
10034
|
var init_model_config = __esm({
|
|
9665
10035
|
"packages/llm/src/model-config.ts"() {
|
|
9666
10036
|
"use strict";
|
|
9667
|
-
ModelStatusSchema =
|
|
9668
|
-
ModelEntrySchema =
|
|
10037
|
+
ModelStatusSchema = z13.enum(["verified", "degraded"]);
|
|
10038
|
+
ModelEntrySchema = z13.object({
|
|
9669
10039
|
/** OpenRouter model id, e.g. "deepseek/deepseek-v4-flash". */
|
|
9670
|
-
id:
|
|
10040
|
+
id: z13.string(),
|
|
9671
10041
|
status: ModelStatusSchema,
|
|
9672
10042
|
/** Roles this model may fill in the pipeline. */
|
|
9673
|
-
roles:
|
|
10043
|
+
roles: z13.array(z13.enum(["bulk", "polish"])),
|
|
9674
10044
|
/** Accepts image input. Text-only models get the screenshot dropped with a
|
|
9675
10045
|
* warning instead of a provider 404 (Carbon run 1 finding). */
|
|
9676
|
-
vision:
|
|
10046
|
+
vision: z13.boolean().default(false),
|
|
9677
10047
|
/** Engine-adapter params shipped WITH the entry (ADR-011 §2): the
|
|
9678
10048
|
* eval-proven settings ride the allowlist so users never rediscover a
|
|
9679
10049
|
* model's quirks at their own expense (K2.7-code burned its whole
|
|
9680
10050
|
* completion budget on mandatory reasoning and emitted nothing). */
|
|
9681
|
-
adapter:
|
|
10051
|
+
adapter: z13.object({
|
|
9682
10052
|
/** Minimum completion budget the model needs to emit full files. */
|
|
9683
|
-
maxTokens:
|
|
10053
|
+
maxTokens: z13.number().optional(),
|
|
9684
10054
|
/** "mandatory": provider refuses reasoning-off; budget accordingly. */
|
|
9685
|
-
reasoning:
|
|
10055
|
+
reasoning: z13.enum(["mandatory", "optional", "none"]).optional()
|
|
9686
10056
|
}).optional(),
|
|
9687
10057
|
/** One-line pointer to the evidence behind `status` (which eval, when).
|
|
9688
10058
|
* Pricing intentionally has no static field — the resolution path is
|
|
9689
10059
|
* the provider's models endpoint at run time (ADR-011 §2). */
|
|
9690
|
-
evidence:
|
|
10060
|
+
evidence: z13.string().optional()
|
|
9691
10061
|
});
|
|
9692
|
-
ModelConfigSchema =
|
|
10062
|
+
ModelConfigSchema = z13.object({
|
|
9693
10063
|
/** Default model for codegen + repair (pipeline steps 4–5, facts path). */
|
|
9694
|
-
bulk:
|
|
10064
|
+
bulk: z13.string(),
|
|
9695
10065
|
/** Optional premium polish model (pipeline step 6, aesthetics/naming only). */
|
|
9696
|
-
polish:
|
|
10066
|
+
polish: z13.string().optional(),
|
|
9697
10067
|
/** ADR-011 §2 default-model pointer for the curated recorded-truth
|
|
9698
10068
|
* engine (a separate axis from the facts-era `bulk`; `generate` flips
|
|
9699
10069
|
* to it in ADR-010 slice 6). Moves only via the reference-corpus gate. */
|
|
9700
|
-
curatedDefault:
|
|
10070
|
+
curatedDefault: z13.string().optional(),
|
|
9701
10071
|
/** ADR-011 §3a Rung-1 escalation target: the model the curated loop
|
|
9702
10072
|
* offers for configs still sub-bar at plateau. Designated ONLY via
|
|
9703
10073
|
* the register's gate (a real curated cert-target run on the measured
|
|
9704
10074
|
* escalation case); empty means no Rung-1 offer, never a default. */
|
|
9705
|
-
escalationTarget:
|
|
9706
|
-
allowlist:
|
|
10075
|
+
escalationTarget: z13.string().optional(),
|
|
10076
|
+
allowlist: z13.array(ModelEntrySchema)
|
|
9707
10077
|
});
|
|
9708
10078
|
DEFAULT_MODEL_CONFIG = {
|
|
9709
10079
|
bulk: "google/gemini-3.1-flash-lite",
|
|
@@ -9882,7 +10252,7 @@ var init_openrouter = __esm({
|
|
|
9882
10252
|
});
|
|
9883
10253
|
|
|
9884
10254
|
// packages/llm/src/semantics.ts
|
|
9885
|
-
import { z as
|
|
10255
|
+
import { z as z14 } from "zod";
|
|
9886
10256
|
function portable(node) {
|
|
9887
10257
|
if (Array.isArray(node)) return node.map(portable);
|
|
9888
10258
|
if (typeof node !== "object" || node === null) return node;
|
|
@@ -9906,7 +10276,7 @@ function portable(node) {
|
|
|
9906
10276
|
return out;
|
|
9907
10277
|
}
|
|
9908
10278
|
function toPortableJsonSchema(schema) {
|
|
9909
|
-
return portable(
|
|
10279
|
+
return portable(z14.toJSONSchema(schema));
|
|
9910
10280
|
}
|
|
9911
10281
|
function semanticsJsonSchema() {
|
|
9912
10282
|
return toPortableJsonSchema(SemanticsSchema);
|
|
@@ -9921,18 +10291,18 @@ var PropTypeSchema, SemanticsSchema, CodegenOutputSchema, DocsOutputSchema;
|
|
|
9921
10291
|
var init_semantics = __esm({
|
|
9922
10292
|
"packages/llm/src/semantics.ts"() {
|
|
9923
10293
|
"use strict";
|
|
9924
|
-
PropTypeSchema =
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
10294
|
+
PropTypeSchema = z14.discriminatedUnion("kind", [
|
|
10295
|
+
z14.object({ kind: z14.literal("enum"), values: z14.array(z14.string()).min(1) }),
|
|
10296
|
+
z14.object({ kind: z14.literal("boolean") }),
|
|
10297
|
+
z14.object({ kind: z14.literal("string") }),
|
|
10298
|
+
z14.object({ kind: z14.literal("reactNode") }),
|
|
10299
|
+
z14.object({ kind: z14.literal("handler") })
|
|
9930
10300
|
]);
|
|
9931
|
-
SemanticsSchema =
|
|
10301
|
+
SemanticsSchema = z14.object({
|
|
9932
10302
|
/** PascalCase component name. */
|
|
9933
|
-
componentName:
|
|
10303
|
+
componentName: z14.string().regex(/^[A-Z][A-Za-z0-9]*$/),
|
|
9934
10304
|
/** Semantic HTML element for the root node. */
|
|
9935
|
-
element:
|
|
10305
|
+
element: z14.enum([
|
|
9936
10306
|
"button",
|
|
9937
10307
|
"a",
|
|
9938
10308
|
"div",
|
|
@@ -9946,33 +10316,33 @@ var init_semantics = __esm({
|
|
|
9946
10316
|
"ul",
|
|
9947
10317
|
"li"
|
|
9948
10318
|
]),
|
|
9949
|
-
props:
|
|
9950
|
-
|
|
9951
|
-
name:
|
|
10319
|
+
props: z14.array(
|
|
10320
|
+
z14.object({
|
|
10321
|
+
name: z14.string().regex(/^[a-z][A-Za-z0-9]*$/),
|
|
9952
10322
|
type: PropTypeSchema,
|
|
9953
|
-
required:
|
|
10323
|
+
required: z14.boolean(),
|
|
9954
10324
|
/** nullish, not optional: models routinely encode "no default" as
|
|
9955
10325
|
* null (observed live: kimi-k2.7-code, Carbon run 8). */
|
|
9956
|
-
defaultValue:
|
|
10326
|
+
defaultValue: z14.string().nullish()
|
|
9957
10327
|
})
|
|
9958
10328
|
),
|
|
9959
10329
|
/** Variant axes that become discriminated unions (invalid combos must not compile). */
|
|
9960
|
-
discriminatedUnions:
|
|
9961
|
-
|
|
9962
|
-
discriminant:
|
|
9963
|
-
arms:
|
|
9964
|
-
|
|
9965
|
-
value:
|
|
10330
|
+
discriminatedUnions: z14.array(
|
|
10331
|
+
z14.object({
|
|
10332
|
+
discriminant: z14.string(),
|
|
10333
|
+
arms: z14.array(
|
|
10334
|
+
z14.object({
|
|
10335
|
+
value: z14.string(),
|
|
9966
10336
|
/** Props only valid for this arm. */
|
|
9967
|
-
extraProps:
|
|
10337
|
+
extraProps: z14.array(z14.string())
|
|
9968
10338
|
})
|
|
9969
10339
|
)
|
|
9970
10340
|
})
|
|
9971
10341
|
),
|
|
9972
|
-
a11y:
|
|
9973
|
-
role:
|
|
9974
|
-
keyboard:
|
|
9975
|
-
aria:
|
|
10342
|
+
a11y: z14.object({
|
|
10343
|
+
role: z14.string().nullish(),
|
|
10344
|
+
keyboard: z14.array(z14.string()),
|
|
10345
|
+
aria: z14.array(z14.string())
|
|
9976
10346
|
}),
|
|
9977
10347
|
/**
|
|
9978
10348
|
* How each Figma axis value is realized through the generated API —
|
|
@@ -9981,29 +10351,29 @@ var init_semantics = __esm({
|
|
|
9981
10351
|
* The visual fact check verifies these claims against real renders, so a
|
|
9982
10352
|
* wrong mapping fails measurement instead of being trusted.
|
|
9983
10353
|
*/
|
|
9984
|
-
variantMapping:
|
|
9985
|
-
|
|
9986
|
-
axis:
|
|
9987
|
-
value:
|
|
9988
|
-
prop:
|
|
9989
|
-
propValue:
|
|
10354
|
+
variantMapping: z14.array(
|
|
10355
|
+
z14.object({
|
|
10356
|
+
axis: z14.string(),
|
|
10357
|
+
value: z14.string(),
|
|
10358
|
+
prop: z14.string(),
|
|
10359
|
+
propValue: z14.union([z14.string(), z14.boolean()])
|
|
9990
10360
|
})
|
|
9991
10361
|
)
|
|
9992
10362
|
});
|
|
9993
|
-
CodegenOutputSchema =
|
|
10363
|
+
CodegenOutputSchema = z14.object({
|
|
9994
10364
|
/** Component source (.tsx). Token-only styling via the provided CSS file. */
|
|
9995
|
-
tsx:
|
|
10365
|
+
tsx: z14.string().min(1),
|
|
9996
10366
|
/** Stylesheet (.css) — themable properties reference var(--token) only. */
|
|
9997
|
-
css:
|
|
10367
|
+
css: z14.string().min(1)
|
|
9998
10368
|
});
|
|
9999
|
-
DocsOutputSchema =
|
|
10000
|
-
usageMd:
|
|
10001
|
-
aiHints:
|
|
10002
|
-
antiPatterns:
|
|
10003
|
-
|
|
10369
|
+
DocsOutputSchema = z14.object({
|
|
10370
|
+
usageMd: z14.string().min(1),
|
|
10371
|
+
aiHints: z14.object({ selectionCriteria: z14.array(z14.string()).min(1) }),
|
|
10372
|
+
antiPatterns: z14.array(
|
|
10373
|
+
z14.object({ scenario: z14.string(), reason: z14.string(), alternative: z14.string() })
|
|
10004
10374
|
),
|
|
10005
|
-
composition:
|
|
10006
|
-
examples:
|
|
10375
|
+
composition: z14.array(z14.string()),
|
|
10376
|
+
examples: z14.array(z14.object({ title: z14.string(), code: z14.string() }))
|
|
10007
10377
|
});
|
|
10008
10378
|
}
|
|
10009
10379
|
});
|
|
@@ -10729,7 +11099,7 @@ var init_engine_curated = __esm({
|
|
|
10729
11099
|
// packages/generate/src/loop.ts
|
|
10730
11100
|
import { existsSync as existsSync27, mkdirSync as mkdirSync8, readFileSync as readFileSync24, renameSync, writeFileSync as writeFileSync12 } from "node:fs";
|
|
10731
11101
|
import path34 from "node:path";
|
|
10732
|
-
import { z as
|
|
11102
|
+
import { z as z15 } from "zod";
|
|
10733
11103
|
function objective(scores, behaviors) {
|
|
10734
11104
|
const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
|
|
10735
11105
|
return [
|
|
@@ -10928,42 +11298,42 @@ var init_loop2 = __esm({
|
|
|
10928
11298
|
"packages/generate/src/loop.ts"() {
|
|
10929
11299
|
"use strict";
|
|
10930
11300
|
better = (a, b) => a[0] !== b[0] ? a[0] > b[0] : a[1] !== b[1] ? a[1] > b[1] : a[2] > b[2];
|
|
10931
|
-
RegionSchema =
|
|
10932
|
-
ConfigScoreSchema =
|
|
10933
|
-
rep:
|
|
10934
|
-
similarity:
|
|
10935
|
-
inkRecall:
|
|
10936
|
-
pass:
|
|
10937
|
-
error:
|
|
11301
|
+
RegionSchema = z15.object({ x0: z15.number(), y0: z15.number(), x1: z15.number(), y1: z15.number(), density: z15.number() });
|
|
11302
|
+
ConfigScoreSchema = z15.object({
|
|
11303
|
+
rep: z15.string(),
|
|
11304
|
+
similarity: z15.number(),
|
|
11305
|
+
inkRecall: z15.number(),
|
|
11306
|
+
pass: z15.boolean(),
|
|
11307
|
+
error: z15.string().optional(),
|
|
10938
11308
|
region: RegionSchema.optional()
|
|
10939
11309
|
});
|
|
10940
|
-
BehaviorResultSchema =
|
|
10941
|
-
LoopStateSchema =
|
|
10942
|
-
version:
|
|
10943
|
-
spentUsd:
|
|
10944
|
-
attempts:
|
|
10945
|
-
|
|
10946
|
-
candidate:
|
|
10947
|
-
files:
|
|
10948
|
-
raw:
|
|
10949
|
-
usage:
|
|
11310
|
+
BehaviorResultSchema = z15.object({ id: z15.string(), pass: z15.boolean(), detail: z15.string().optional() });
|
|
11311
|
+
LoopStateSchema = z15.object({
|
|
11312
|
+
version: z15.literal(1),
|
|
11313
|
+
spentUsd: z15.number(),
|
|
11314
|
+
attempts: z15.array(
|
|
11315
|
+
z15.object({
|
|
11316
|
+
candidate: z15.object({
|
|
11317
|
+
files: z15.record(z15.string(), z15.string()),
|
|
11318
|
+
raw: z15.string().optional(),
|
|
11319
|
+
usage: z15.object({ usd: z15.number(), inTokens: z15.number(), outTokens: z15.number(), modelMs: z15.number(), finishReason: z15.string().optional() }).optional()
|
|
10950
11320
|
}),
|
|
10951
|
-
rulerScore:
|
|
10952
|
-
accepted:
|
|
10953
|
-
feedback:
|
|
11321
|
+
rulerScore: z15.object({ passCount: z15.number(), floor: z15.number(), mean: z15.number() }),
|
|
11322
|
+
accepted: z15.boolean(),
|
|
11323
|
+
feedback: z15.string()
|
|
10954
11324
|
})
|
|
10955
11325
|
),
|
|
10956
|
-
iterations:
|
|
10957
|
-
|
|
10958
|
-
iter:
|
|
10959
|
-
usd:
|
|
10960
|
-
modelMs:
|
|
10961
|
-
scoreMs:
|
|
10962
|
-
objective:
|
|
10963
|
-
accepted:
|
|
10964
|
-
scores:
|
|
10965
|
-
behaviors:
|
|
10966
|
-
parseError:
|
|
11326
|
+
iterations: z15.array(
|
|
11327
|
+
z15.object({
|
|
11328
|
+
iter: z15.number(),
|
|
11329
|
+
usd: z15.number(),
|
|
11330
|
+
modelMs: z15.number().optional(),
|
|
11331
|
+
scoreMs: z15.number().optional(),
|
|
11332
|
+
objective: z15.tuple([z15.number(), z15.number(), z15.number()]),
|
|
11333
|
+
accepted: z15.boolean(),
|
|
11334
|
+
scores: z15.array(ConfigScoreSchema),
|
|
11335
|
+
behaviors: z15.array(BehaviorResultSchema).optional(),
|
|
11336
|
+
parseError: z15.string().optional()
|
|
10967
11337
|
})
|
|
10968
11338
|
)
|
|
10969
11339
|
});
|
|
@@ -12355,28 +12725,13 @@ function recordingSetHash(setDir, configs) {
|
|
|
12355
12725
|
channeled = JSON.parse(readFileSync27(path37.join(setDir, "recording-set.json"), "utf8")).channel !== void 0;
|
|
12356
12726
|
} catch {
|
|
12357
12727
|
}
|
|
12358
|
-
const relPaths =
|
|
12359
|
-
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
12363
|
-
const perRep = [
|
|
12364
|
-
"get_design_context.json",
|
|
12365
|
-
"get_metadata.json",
|
|
12366
|
-
"get_screenshot.json",
|
|
12367
|
-
"get_variable_defs.json",
|
|
12368
|
-
...channeled ? ["get_metadata_interior.json", "rest_nodes.json", "rest_metadata.json", "rest_screenshot.json"] : []
|
|
12369
|
-
];
|
|
12370
|
-
for (const cfg of configs) {
|
|
12371
|
-
for (const f of perRep) {
|
|
12372
|
-
if (existsSync30(path37.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
|
|
12373
|
-
}
|
|
12374
|
-
if (existsSync30(path37.join(setDir, cfg.rep))) {
|
|
12375
|
-
for (const asset of readdirSync11(path37.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
|
|
12376
|
-
relPaths.push(`${cfg.rep}/${asset}`);
|
|
12377
|
-
}
|
|
12728
|
+
const relPaths = recordingSetEnumeration(
|
|
12729
|
+
{ channeled, reps: configs.map((c) => c.rep) },
|
|
12730
|
+
{
|
|
12731
|
+
exists: (p) => existsSync30(path37.join(setDir, p)),
|
|
12732
|
+
listRep: (rep) => existsSync30(path37.join(setDir, rep)) ? readdirSync11(path37.join(setDir, rep)) : []
|
|
12378
12733
|
}
|
|
12379
|
-
|
|
12734
|
+
);
|
|
12380
12735
|
return hashRecordingSet(
|
|
12381
12736
|
relPaths,
|
|
12382
12737
|
(p) => new Uint8Array(readFileSync27(path37.join(setDir, p))),
|
|
@@ -13043,7 +13398,7 @@ function composedPins(hostSet, libraryRoots) {
|
|
|
13043
13398
|
failures.push(`${rel}: partner bundle manifest at ${bundleDir} became unreadable`);
|
|
13044
13399
|
continue;
|
|
13045
13400
|
}
|
|
13046
|
-
if (!
|
|
13401
|
+
if (!safeSegment2(manifest.name) || !safeSegment2(manifest.entry)) {
|
|
13047
13402
|
failures.push(
|
|
13048
13403
|
`${rel}: partner bundle at ${bundleDir} declares an unsafe name/entry (${JSON.stringify(manifest.name)} / ${JSON.stringify(manifest.entry)}) \u2014 refused; both must be a single plain path segment`
|
|
13049
13404
|
);
|
|
@@ -13148,7 +13503,7 @@ function composedChecks(candidateDir, hostEntry, pins) {
|
|
|
13148
13503
|
}
|
|
13149
13504
|
const wrong = [];
|
|
13150
13505
|
for (const f of pin.moduleFiles) {
|
|
13151
|
-
if (!
|
|
13506
|
+
if (!safeSegment2(f.name)) {
|
|
13152
13507
|
wrong.push(`${f.name} refused (not a plain path segment)`);
|
|
13153
13508
|
continue;
|
|
13154
13509
|
}
|
|
@@ -13207,7 +13562,7 @@ function regionOverrides(hostSet, partnerSet, instances) {
|
|
|
13207
13562
|
}
|
|
13208
13563
|
return out;
|
|
13209
13564
|
}
|
|
13210
|
-
var composedModuleDir,
|
|
13565
|
+
var composedModuleDir, safeSegment2, MAX_PINNED_FILE_BYTES, CONTEXT_LAYOUT_TOKENS;
|
|
13211
13566
|
var init_compose_pins = __esm({
|
|
13212
13567
|
"packages/generate/src/compose-pins.ts"() {
|
|
13213
13568
|
"use strict";
|
|
@@ -13216,7 +13571,7 @@ var init_compose_pins = __esm({
|
|
|
13216
13571
|
init_brief();
|
|
13217
13572
|
init_bundle_emit();
|
|
13218
13573
|
composedModuleDir = (partnerName) => path38.posix.join("composed", partnerName);
|
|
13219
|
-
|
|
13574
|
+
safeSegment2 = (s) => /^[A-Za-z0-9][A-Za-z0-9._ -]*$/.test(s) && !s.includes("..");
|
|
13220
13575
|
MAX_PINNED_FILE_BYTES = 1024 * 1024;
|
|
13221
13576
|
CONTEXT_LAYOUT_TOKENS = /* @__PURE__ */ new Set(["shrink-0", "grow", "flex-1", "basis-0", "self-stretch", "w-full", "h-full"]);
|
|
13222
13577
|
}
|
|
@@ -18750,7 +19105,7 @@ import { existsSync as existsSync43, mkdtempSync as mkdtempSync3, readFileSync a
|
|
|
18750
19105
|
import os8 from "node:os";
|
|
18751
19106
|
import path53 from "node:path";
|
|
18752
19107
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
18753
|
-
import { z as
|
|
19108
|
+
import { z as z16 } from "zod";
|
|
18754
19109
|
function sourceHash() {
|
|
18755
19110
|
const dir = path53.dirname(fileURLToPath6(import.meta.url));
|
|
18756
19111
|
const h = createHash12("sha256");
|
|
@@ -18768,23 +19123,23 @@ var init_server = __esm({
|
|
|
18768
19123
|
CLI_BIN = path53.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
|
|
18769
19124
|
BUNDLED_CLI = path53.join(path53.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
|
|
18770
19125
|
CLI_SPAWN = existsSync43(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
|
|
18771
|
-
str = (d) =>
|
|
18772
|
-
optStr = (d) =>
|
|
19126
|
+
str = (d) => z16.string().describe(d);
|
|
19127
|
+
optStr = (d) => z16.string().optional().describe(d);
|
|
18773
19128
|
TOOLS = [
|
|
18774
19129
|
{
|
|
18775
19130
|
name: "tendril_record_plan",
|
|
18776
19131
|
description: "ENTRY POINT for implementing/building a React component from a Figma design or figma.com URL \u2014 start the Tendril pipeline here (after loading the tendril skill, if installed). Plans a recording session: computes the rep queue (anchor + one-factor + conflict crosses) from the verbatim get_metadata response (pass its blocks via metadataParts \u2014 no file to write) and persists the set manifest. Resumes if the set already exists. The output may carry USER QUESTIONS \u2014 defaultsToConfirm (which pose is the component's default) or a multiple-component-sets error (which set to record): render them to a present user and apply the answers via `defaults` / `componentSet`; non-interactive runs follow each question's stated fallback. It may also carry `interactionStatesToConfirm` \u2014 the recording holds no hover/focus/pressed state, so nothing shows how the component behaves when someone uses it: say its `statement` and `designFix` in that SAME one message (the fix is a Figma variant, not code). It is a disclosure, not a gate \u2014 no answer is required and recording proceeds regardless. The output also carries `feasibilityCheck`: the call arithmetic for this queue plus the free `whoami` check that turns it into a verdict \u2014 complete that handshake BEFORE the first recording call, and surface the verdict to the user when the set does not fit their daily allowance.",
|
|
18777
|
-
schema:
|
|
19132
|
+
schema: z16.object({
|
|
18778
19133
|
setDir: str("recording set directory to create/resume"),
|
|
18779
19134
|
component: str("component/system name"),
|
|
18780
19135
|
// Parts array FIRST, same reason as ingest_rep: real responses
|
|
18781
19136
|
// are usually multi-block, and the file param made plan the ONE
|
|
18782
19137
|
// remaining hand-built-envelope entry point (run 10: the agent
|
|
18783
19138
|
// wrote the file twice — once as text, once as JSON envelope).
|
|
18784
|
-
metadataParts:
|
|
19139
|
+
metadataParts: z16.array(z16.string()).optional().describe("the frame-level get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join or save them yourself; this is the NORMAL param"),
|
|
18785
19140
|
metadata: optStr("ONLY when get_metadata genuinely returned one single block: its text verbatim (otherwise use metadataParts)"),
|
|
18786
|
-
metadataFiles:
|
|
18787
|
-
defaults:
|
|
19141
|
+
metadataFiles: z16.array(z16.string()).optional().describe('saved verbatim get_metadata envelope file paths \u2014 JSON shape {"content":[{"type":"text","text":"<frame \u2026>"}]}; optionally <file>@<frameId>. Prefer metadataParts: no file to write'),
|
|
19142
|
+
defaults: z16.array(z16.string()).optional().describe(`axis defaults as "Axis=Value" (from the user's defaultsToConfirm answers; may re-plan a set with nothing recorded yet)`),
|
|
18788
19143
|
componentSet: optStr("record only this component set (the user's pick from a multiple-component-sets error)"),
|
|
18789
19144
|
figmaFile: optStr("the Figma file key from the design URL you were handed \u2014 figma.com/design/<KEY>/\u2026, pass exactly <KEY>. ALWAYS pass it on a fresh plan: it is the set's recorded file identity for cross-bundle composition (ADR-013), captured at plan time only and never backfillable later")
|
|
18790
19145
|
}),
|
|
@@ -18820,10 +19175,10 @@ var init_server = __esm({
|
|
|
18820
19175
|
{
|
|
18821
19176
|
name: "tendril_permissions",
|
|
18822
19177
|
description: "Install the Claude Code permission allowlist for the Tendril pipeline (write: true \u2014 merges the MCP per-tool entries, the run's shell surface, and project-scoped Write/Edit with deny guards for ./.claude and ./.git into the project's .claude/settings.local.json, idempotent, never touches other keys), or list the entries without writing. The shell/Write grants are CONVENIENCE, not a security boundary \u2014 the output's note names exactly what they trade; RELAY it with the offer. OFFER THIS AT PIPELINE START whenever NO merged Claude settings file (project .claude/settings.local.json or .claude/settings.json, or user ~/.claude/settings.json) contains tendril MCP entries \u2014 plugin installs use mcp__plugin_tendril_tendril__*, direct claude-mcp-add installs use mcp__<server>__* \u2014 when this session's tool names differ from the plugin defaults, pass figmaPrefix/tendrilPrefix with the prefixes you actually see, or the written entries never match. A FILE check, never prompt-watching \u2014 agents cannot observe permission prompts. ONE approval here replaces a prompt per pipeline call. Never run it unoffered; the user must reload the session for new settings to apply \u2014 say so.",
|
|
18823
|
-
schema:
|
|
18824
|
-
write:
|
|
18825
|
-
figmaPrefix:
|
|
18826
|
-
tendrilPrefix:
|
|
19178
|
+
schema: z16.object({
|
|
19179
|
+
write: z16.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries"),
|
|
19180
|
+
figmaPrefix: z16.string().optional().describe("The Figma server's entry prefix AS THIS SESSION NAMES ITS TOOLS \u2014 the part before __get_metadata (e.g. mcp__figma-remote, mcp__plugin_figma_figma). Pass it whenever the session's Figma tools are not mcp__plugin_figma_figma__* \u2014 entries written under the wrong prefix never match anything (measured: six dead entries in a figma-remote session)."),
|
|
19181
|
+
tendrilPrefix: z16.string().optional().describe("Same for the tendril server when this session's tendril tools are not mcp__plugin_tendril_tendril__* (e.g. mcp__tendril for claude mcp add installs).")
|
|
18827
19182
|
}),
|
|
18828
19183
|
argv: (i) => [
|
|
18829
19184
|
"permissions",
|
|
@@ -18837,13 +19192,13 @@ var init_server = __esm({
|
|
|
18837
19192
|
name: "tendril_doctor",
|
|
18838
19193
|
annotations: { readOnlyHint: true },
|
|
18839
19194
|
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, and whether this machine holds a LIVE portal session (checked against the portal itself; informational \u2014 publishing needs it, verification never does; when absent, offer tendril_login). 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.",
|
|
18840
|
-
schema:
|
|
19195
|
+
schema: z16.object({}),
|
|
18841
19196
|
argv: () => ["doctor"]
|
|
18842
19197
|
},
|
|
18843
19198
|
{
|
|
18844
19199
|
name: "tendril_login",
|
|
18845
19200
|
description: "Connect this machine to the user's Tendril portal account WITHOUT the user touching a terminal \u2014 phase one of the browser-approve sign-in. Starts the handshake, opens their browser to the approve page, and returns a verification link plus a short code. RELAY BOTH to the user verbatim: they click Approve on the page, and they must check the page shows EXACTLY this code (that check is the phishing defence \u2014 an attacker can send someone else's approve link). Then call tendril_login_wait to finish. Offer this whenever tendril_doctor reports no portal session and the user wants to publish or share; publishing needs it, verification never does.",
|
|
18846
|
-
schema:
|
|
19201
|
+
schema: z16.object({
|
|
18847
19202
|
portal: optStr("portal origin override for self-hosted portals (defaults to https://app.trytendril.com)")
|
|
18848
19203
|
}),
|
|
18849
19204
|
argv: (i) => ["login", "--device-start", ...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []]
|
|
@@ -18851,13 +19206,13 @@ var init_server = __esm({
|
|
|
18851
19206
|
{
|
|
18852
19207
|
name: "tendril_login_wait",
|
|
18853
19208
|
description: "Phase two of the browser-approve sign-in: waits (up to ten minutes, with live progress) for the user to click Approve on the page tendril_login opened, then stores the session on this machine. Call it right after relaying the link and code. A denial, a lapse, and success each come back as their own sentence; confirm the result to the user, and on success they are ready to publish.",
|
|
18854
|
-
schema:
|
|
19209
|
+
schema: z16.object({}),
|
|
18855
19210
|
argv: () => ["login", "--device-wait"]
|
|
18856
19211
|
},
|
|
18857
19212
|
{
|
|
18858
19213
|
name: "tendril_figma_connect",
|
|
18859
19214
|
description: "Grant this machine read access to the user's Figma files over Figma's API \u2014 phase one of the browser Figma connection. Recording over the Figma MCP transport pays a hard per-day call quota; this connection lets recording fetch over Figma's REST API instead (per-minute, batched), so large component sets stop hitting daily limits. Starts the handshake and returns a connect link \u2014 RELAY IT to the user verbatim: they sign in to their portal account if asked, then click Allow on Figma's consent screen (read-only file access; revocable any time in their Figma settings). Then call tendril_figma_connect_wait to finish. You cannot consent for them: the portal only accepts the browser's signed-in click, never this machine's token. Offer it when a recording plan warns about Figma call limits; requires a portal session (tendril_login).",
|
|
18860
|
-
schema:
|
|
19215
|
+
schema: z16.object({
|
|
18861
19216
|
portal: optStr("portal origin override for self-hosted portals (defaults to the stored session's portal)")
|
|
18862
19217
|
}),
|
|
18863
19218
|
argv: (i) => ["figma-connect", "--start", ...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []]
|
|
@@ -18865,13 +19220,13 @@ var init_server = __esm({
|
|
|
18865
19220
|
{
|
|
18866
19221
|
name: "tendril_figma_connect_wait",
|
|
18867
19222
|
description: "Phase two of the browser Figma connection: waits (with live progress) for the user's Allow click on Figma's consent screen, then stores the credential on this machine (it renews itself from then on). Call it right after relaying the connect link. A decline, a lapse, and success each come back as their own sentence \u2014 report the outcome to the user.",
|
|
18868
|
-
schema:
|
|
19223
|
+
schema: z16.object({}),
|
|
18869
19224
|
argv: () => ["figma-connect", "--wait"]
|
|
18870
19225
|
},
|
|
18871
19226
|
{
|
|
18872
19227
|
name: "tendril_publish",
|
|
18873
19228
|
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).",
|
|
18874
|
-
schema:
|
|
19229
|
+
schema: z16.object({
|
|
18875
19230
|
bundleDir: str("bundle directory (verified \u2014 carries component.json and verify-evidence)"),
|
|
18876
19231
|
portal: optStr("portal origin override for self-hosted portals (defaults to the stored session's portal)")
|
|
18877
19232
|
}),
|
|
@@ -18880,38 +19235,55 @@ var init_server = __esm({
|
|
|
18880
19235
|
{
|
|
18881
19236
|
name: "tendril_publish_wait",
|
|
18882
19237
|
description: 'Phase two of the browser-approved publish. Waits in a BOUNDED window (~1 minute per call) for the user\'s Approve click on the page tendril_publish returned, then uploads the bundle, commits it, and returns the live publication URL. Call it right after relaying the approve link. While the human has not decided, each call returns `status: "approval-pending"` \u2014 that is a heartbeat, not a failure: tell the user in one short line that you are still waiting (restate the approve link and the account to approve as, ONLY if they seem lost; never urge the decision), then call this again to keep waiting. The request stays live for ~30 minutes. A denial, a lapse, and success each come back as their own sentence \u2014 report the outcome, and on success lead with the live URL. A decided approval continues straight into upload and commit INSIDE the same call \u2014 that phase can take minutes and may render no progress in some hosts; that is normal, not stuck. Some hosts render no progress at all during a call; the bounded window IS the liveness, so never describe an in-flight call as stuck \u2014 and a brief liveness line roughly every few minutes is enough, you need not narrate every heartbeat (each pending return carries waitedTotalSeconds/remainingSeconds to say where the wait stands).',
|
|
18883
|
-
schema:
|
|
19238
|
+
schema: z16.object({
|
|
18884
19239
|
bundleDir: str("the same bundle directory tendril_publish was called with"),
|
|
18885
19240
|
portal: optStr("portal origin override (must match tendril_publish's)")
|
|
18886
19241
|
}),
|
|
18887
19242
|
argv: (i) => ["publish", i["bundleDir"], "--approve-wait", "--wait-window", "55", ...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []]
|
|
18888
19243
|
},
|
|
19244
|
+
{
|
|
19245
|
+
name: "tendril_pull",
|
|
19246
|
+
description: "Bring a published component's RECORDING SET onto this machine \u2014 the recorded design truth its verdict was measured against. Use it when the recordings are not here: a fresh machine, a clone with no recordings/ directory, or a component someone else on the team published. ONE bounded call, no wait twin. Needs a portal session (tendril_login) because a recording set belongs to the account that published it; everything afterwards \u2014 verify, compose, republish \u2014 is local, offline and account-less as always. The name is EXACT and the portal's refusals list the user's real component names, so a name that does not resolve is a one-step correction rather than a dead end; when one name spans two design systems, pass `figmaFile`. It writes to ./recordings/<component>/ and never overwrites a different set already there. AFTER pulling, call tendril_record_status on each set you pulled, then continue the connect prompt from its step 3.",
|
|
19247
|
+
schema: z16.object({
|
|
19248
|
+
component: str("the component name, exactly as the user's library shows it"),
|
|
19249
|
+
figmaFile: optStr("the design system's Figma file key \u2014 only when one name spans several of the user's design systems (the portal's refusal says when)"),
|
|
19250
|
+
dest: optStr("where the set lands (default: ./recordings/<component>)"),
|
|
19251
|
+
portal: optStr("portal origin override for self-hosted portals (defaults to the stored session's portal)")
|
|
19252
|
+
}),
|
|
19253
|
+
argv: (i) => [
|
|
19254
|
+
"pull",
|
|
19255
|
+
i["component"],
|
|
19256
|
+
...typeof i["figmaFile"] === "string" ? ["--figma-file", i["figmaFile"]] : [],
|
|
19257
|
+
...typeof i["dest"] === "string" ? ["--dest", i["dest"]] : [],
|
|
19258
|
+
...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []
|
|
19259
|
+
]
|
|
19260
|
+
},
|
|
18889
19261
|
{
|
|
18890
19262
|
name: "tendril_record_next",
|
|
18891
19263
|
annotations: { readOnlyHint: true },
|
|
18892
19264
|
description: "Get the next pending recording instruction (which Figma MCP tool to call for which node, and how to save it). RARELY NEEDED: every ingest/fetch response already carries `next` \u2014 use this only to resume an interrupted session. The full queue is known from plan, so independent reps may be recorded in any order (and in parallel).",
|
|
18893
|
-
schema:
|
|
19265
|
+
schema: z16.object({ setDir: str("recording set directory") }),
|
|
18894
19266
|
argv: (i) => ["record", "next", "--set", i["setDir"]]
|
|
18895
19267
|
},
|
|
18896
19268
|
{
|
|
18897
19269
|
name: "tendril_record_rest_fetch",
|
|
18898
19270
|
description: "Fetch a recording set's pixels+geometry over Figma's REST API \u2014 the step `record next` names as rest_fetch when the set's REST channel is active. The CLI makes these requests itself with the user's connected Figma credential (tendril_figma_connect); you make NO Figma call for it and none can substitute. It first self-checks one or two probe poses against their MCP-recorded references (zero-pixel tolerance) and falls the whole set back to MCP recording if the transports disagree; then it bulk-fetches the rest, batched and paced, spending no MCP quota. Requires the probe poses to be recorded first \u2014 record next tells you when.",
|
|
18899
|
-
schema:
|
|
19271
|
+
schema: z16.object({ setDir: str("recording set directory") }),
|
|
18900
19272
|
argv: (i) => ["record", "rest-fetch", "--set", i["setDir"]]
|
|
18901
19273
|
},
|
|
18902
19274
|
{
|
|
18903
19275
|
name: "tendril_record_bindings",
|
|
18904
19276
|
description: "Fetch Figma's instance\u2192component bindings for an EXISTING MCP-recorded set \u2014 one or two batched REST calls, no re-recording, pixels/geometry/identity untouched. This is how components that are ALREADY recorded and generated become connectable: the bindings make cross-component pairing id-backed, so `tendril compose` can propose it for the human's confirm. Every binding is congruence-verified against the recorded metadata (a design that changed since recording refuses per pose, named). The CLI makes the requests itself with the user's connected Figma credential (tendril_figma_connect); you make NO Figma call. The result names the next step \u2014 including when no partner set is visible in the scanned root (co-locate the sets or pass compose --library). For sets recorded before file identity was captured, pass the design's file key as `file`.",
|
|
18905
|
-
schema:
|
|
19277
|
+
schema: z16.object({ setDir: str("recording set directory"), file: optStr("the design's file key (figma.com/design/<KEY>/\u2026) \u2014 only for sets whose manifest lacks figmaFile; an operator assertion, congruence still gates every binding") }),
|
|
18906
19278
|
argv: (i) => ["record", "bindings", "--set", i["setDir"], ...typeof i["file"] === "string" ? ["--file", i["file"]] : []]
|
|
18907
19279
|
},
|
|
18908
19280
|
{
|
|
18909
19281
|
name: "tendril_compose",
|
|
18910
19282
|
description: "Put ONE composition pairing in front of the user's BROWSER \u2014 phase one of the browser-approved connect (invariant-5 migration; the terminal `--confirm-compositions` path remains for humans at a TTY). Requires a portal session (tendril_login \u2014 one browser Approve). Returns the approve-page link: RELAY IT verbatim, name the account the result says to approve as, and never urge the decision \u2014 the card carries the engine's honest disclosures and the human reads them. Deny on the card is NOT NOW (this request only, never a permanent decline). You cannot decide this yourself on any channel: the portal accepts the decision only from the user's signed-in browser, and the terminal flag only from an interactive TTY. A host with several open pairings needs `pair` (one card = one decision). Then finish with tendril_compose_wait. Both recording sets must be visible in one scanned workspace \u2014 co-locate them or pass `library`.",
|
|
18911
|
-
schema:
|
|
19283
|
+
schema: z16.object({
|
|
18912
19284
|
setDir: str("the HOST recording set directory (the component that embeds the partner)"),
|
|
18913
19285
|
pair: optStr("the pair-key to request when the host has several open pairings"),
|
|
18914
|
-
library:
|
|
19286
|
+
library: z16.array(z16.string()).optional().describe("workspace root(s) holding the partner recording set(s) \u2014 required when they live in another project root"),
|
|
18915
19287
|
portal: optStr("portal origin override (defaults to the stored session's portal)")
|
|
18916
19288
|
}),
|
|
18917
19289
|
argv: (i) => [
|
|
@@ -18927,7 +19299,7 @@ var init_server = __esm({
|
|
|
18927
19299
|
{
|
|
18928
19300
|
name: "tendril_compose_wait",
|
|
18929
19301
|
description: 'Phase two of the browser-approved connect. Waits in a BOUNDED window (~1 minute per call) for the user\'s decision on the card tendril_compose returned; on Approve it records the SAME manifest entry the terminal confirm writes \u2014 after re-deriving the pairing from the CURRENT recordings and refusing if anything changed underneath the click (the click is then not wrong; the project moved \u2014 run the connect again). While undecided, each call returns `status: "approval-pending"` \u2014 a heartbeat, not a failure: one short liveness line to the user, then call again; the request stays live ~30 minutes, and a decided card can take moments to land. NOT NOW, a lapse, and success each arrive as their own sentence \u2014 report the one you got. After success: regenerate the HOST bundle with the same library roots (brief \u2192 generate \u2192 score \u2192 verify), then republish \u2014 announce the republish in one line first. Compose waits follow the same heartbeat contract as publish waits: never urge, never call an in-flight wait stuck.',
|
|
18930
|
-
schema:
|
|
19302
|
+
schema: z16.object({
|
|
18931
19303
|
setDir: str("the same HOST set directory tendril_compose was called with"),
|
|
18932
19304
|
portal: optStr("portal origin override (must match tendril_compose's)")
|
|
18933
19305
|
}),
|
|
@@ -18936,10 +19308,10 @@ var init_server = __esm({
|
|
|
18936
19308
|
{
|
|
18937
19309
|
name: "tendril_record_fetch",
|
|
18938
19310
|
description: "Download a Figma asset URL (from get_screenshot's image_url) straight to disk and ingest it as the rep's envelope \u2014 the fallback when only the screenshot piece needs (re-)recording; for a rep's standard three recordings PREFER tendril_record_ingest_rep. Never download the image yourself: the bytes must not pass through your context.",
|
|
18939
|
-
schema:
|
|
19311
|
+
schema: z16.object({
|
|
18940
19312
|
setDir: str("recording set directory"),
|
|
18941
19313
|
rep: str("planned rep slug"),
|
|
18942
|
-
tool:
|
|
19314
|
+
tool: z16.enum(["get_screenshot"]).describe("get_screenshot"),
|
|
18943
19315
|
url: str("image_url from the Figma response, verbatim")
|
|
18944
19316
|
}),
|
|
18945
19317
|
argv: (i) => ["record", "fetch", "--set", i["setDir"], "--rep", i["rep"], "--tool", i["tool"], "--url", i["url"]]
|
|
@@ -18947,15 +19319,15 @@ var init_server = __esm({
|
|
|
18947
19319
|
{
|
|
18948
19320
|
name: "tendril_record_ingest_rep",
|
|
18949
19321
|
description: "Ingest a rep's ENTIRE recording in ONE call \u2014 the get_metadata response, the get_design_context response, and the get_screenshot image_url together. Make the three Figma calls first, in protocol order (get_metadata, then get_design_context with excludeScreenshot=true, then get_screenshot with contentsOnly=true and maxDimension=4096 \u2014 isolation keeps the editor's component-set chrome out of the padded export, where it would otherwise score as unpaintable reference ink), then pass all three here VERBATIM. PREFER THIS over three separate ingest/fetch calls: one approvable operation per rep instead of three. Pieces land independently: on a partial failure the error names exactly which piece(s) to re-record \u2014 the rest are already on disk. The response carries `next` and, for design context, `assets` (auto-fetched server-side; only listed failures need record_asset).",
|
|
18950
|
-
schema:
|
|
19322
|
+
schema: z16.object({
|
|
18951
19323
|
setDir: str("recording set directory"),
|
|
18952
19324
|
rep: str("planned rep slug"),
|
|
18953
19325
|
// Parts arrays FIRST: in the field, EVERY real Figma response is
|
|
18954
19326
|
// multi-block (run 6: 49/49 reps — metadata 2 blocks, design
|
|
18955
19327
|
// context 5-6), so the arrays are the norm and the single-string
|
|
18956
19328
|
// params the rare case, not the reverse.
|
|
18957
|
-
metadataParts:
|
|
18958
|
-
contextParts:
|
|
19329
|
+
metadataParts: z16.array(z16.string()).optional().describe("get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join blocks yourself. Multi-block responses are common (run 6: 49/49 reps); a single block wrapped in a one-element array is equally fine."),
|
|
19330
|
+
contextParts: z16.array(z16.string()).optional().describe("get_design_context response blocks, every block in order, each verbatim \u2014 the NORMAL param (real responses arrive as 5-6 blocks)"),
|
|
18959
19331
|
screenshotUrl: optStr("image_url from the get_screenshot response, verbatim \u2014 the CLI downloads it; the bytes never pass through your context"),
|
|
18960
19332
|
metadata: optStr("ONLY when get_metadata genuinely returned one single block: its text verbatim (otherwise use metadataParts)"),
|
|
18961
19333
|
context: optStr("ONLY when get_design_context genuinely returned one single block: its text verbatim (otherwise use contextParts)")
|
|
@@ -18988,7 +19360,7 @@ var init_server = __esm({
|
|
|
18988
19360
|
{
|
|
18989
19361
|
name: "tendril_record_ingest",
|
|
18990
19362
|
description: "Single-piece ingest of a VERBATIM Figma tool-response \u2014 the fallback path (re-recording one failed piece, the set-level get_variable_defs and get_motion_context steps, get_metadata_interior for mains); for a rep's standard three recordings PREFER tendril_record_ingest_rep, which takes them all in one call. Pass `text` (single block) or `texts` (response split into multiple output blocks \u2014 each block verbatim, in order; NEVER hand-join them): the CLI constructs the envelope from the same bytes. The response includes `next` and, for get_design_context, `assets` (auto-fetched; only listed failures need manual handling).",
|
|
18991
|
-
schema:
|
|
19363
|
+
schema: z16.object({
|
|
18992
19364
|
setDir: str("recording set directory"),
|
|
18993
19365
|
rep: str("planned rep slug, or __set__ for the set-level steps (get_variable_defs, get_motion_context)"),
|
|
18994
19366
|
// Enumerated, not a free string: this value reaches a file path.
|
|
@@ -19001,9 +19373,9 @@ var init_server = __esm({
|
|
|
19001
19373
|
// dead-ended every MCP-driven recording at the motion step with
|
|
19002
19374
|
// no MCP path to complete (the CLI accepted what the primary
|
|
19003
19375
|
// surface could not send). The pin lives in server.test.ts.
|
|
19004
|
-
tool:
|
|
19376
|
+
tool: z16.enum(["get_design_context", "get_metadata", "get_screenshot", "get_variable_defs", "get_metadata_interior", "get_motion_context"]),
|
|
19005
19377
|
text: optStr("the tool response text VERBATIM \u2014 exactly as returned, unmodified (single-block responses; text tools only \u2014 screenshots go through record_fetch)"),
|
|
19006
|
-
texts:
|
|
19378
|
+
texts: z16.array(z16.string()).optional().describe("when the response arrived as MULTIPLE output blocks: every block, in order, each verbatim \u2014 never hand-join blocks yourself"),
|
|
19007
19379
|
file: optStr("path to a saved envelope JSON (alternative to text/texts)")
|
|
19008
19380
|
}),
|
|
19009
19381
|
// The text rides a temp file, never argv: Windows caps a command
|
|
@@ -19027,7 +19399,7 @@ var init_server = __esm({
|
|
|
19027
19399
|
{
|
|
19028
19400
|
name: "tendril_record_asset",
|
|
19029
19401
|
description: "FALLBACK ONLY \u2014 ingest auto-fetches design-context assets; use this just for assets listed in an ingest response's `assets.failed`. Batch mode: pass `dir` to ingest every asset-*.<ext> in a directory in ONE call. SVGs with active content are rejected; sizes are capped.",
|
|
19030
|
-
schema:
|
|
19402
|
+
schema: z16.object({
|
|
19031
19403
|
setDir: str("recording set directory"),
|
|
19032
19404
|
rep: str("planned rep slug"),
|
|
19033
19405
|
name: optStr("asset-<id>.<ext> (single-asset mode)"),
|
|
@@ -19048,13 +19420,13 @@ var init_server = __esm({
|
|
|
19048
19420
|
name: "tendril_record_status",
|
|
19049
19421
|
annotations: { readOnlyHint: true },
|
|
19050
19422
|
description: "Recording-set completeness: per-rep recorded/missing tools, files that exist but are unusable (invalid \u2014 re-record those), and whether the set-level steps are recorded (get_variable_defs always; get_motion_context when the plan asked \u2014 ADR-016). `complete` means the RECORDING is usable by the next pipeline step \u2014 it does NOT mean nothing is left to decide: read the `composition` field too (always present: openPairs lists unconfirmed partner pairs with pair-keys \u2014 surface those to the user BEFORE generating the host, partner-first; {unavailable} means discovery could not run, which is unknown, not 'no pairs').",
|
|
19051
|
-
schema:
|
|
19423
|
+
schema: z16.object({ setDir: str("recording set directory") }),
|
|
19052
19424
|
argv: (i) => ["record", "status", "--set", i["setDir"]]
|
|
19053
19425
|
},
|
|
19054
19426
|
{
|
|
19055
19427
|
name: "tendril_engine_brief",
|
|
19056
19428
|
description: "AGENT-HARNESS engine, step 1: emits the task payload file (system brief + every recorded config's emission, box, assets, tokens) and the protocol. YOU (the calling agent) implement the bundle; the CLI is the only judge. Read the payload file completely before proposing.",
|
|
19057
|
-
schema:
|
|
19429
|
+
schema: z16.object({
|
|
19058
19430
|
taskOrSet: str("a recording-set directory (from tendril_record), or a reference task name \u2014 an unknown name returns the valid list in the error"),
|
|
19059
19431
|
// Required by design, not convenience: the model choice must be
|
|
19060
19432
|
// settled BEFORE generation starts. A smoke run picked its own
|
|
@@ -19081,13 +19453,13 @@ var init_server = __esm({
|
|
|
19081
19453
|
{
|
|
19082
19454
|
name: "tendril_engine_score",
|
|
19083
19455
|
description: "AGENT-HARNESS engine, step 2 (the oracle): scores a candidate bundle directory against recorded truth \u2014 per-config pixels, behaviors, state parity (recording-selected \u2014 a bundle cannot unschedule it) \u2014 and returns feedback plus evidence artifacts. Iterate until allPass or two non-improving rounds. Only THIS tool's output counts as a score; never claim numbers yourself. allPass ends the LOOP, not the run: finish with tendril_verify, and a green verify's terminal step is publishing \u2014 the component is done when it is LIVE, not when it scores.",
|
|
19084
|
-
schema:
|
|
19456
|
+
schema: z16.object({
|
|
19085
19457
|
taskOrSet: str("reference task name or recording-set directory"),
|
|
19086
19458
|
candidateDir: str("directory containing the proposed bundle files"),
|
|
19087
19459
|
bar: optStr("pass (default) or cert"),
|
|
19088
19460
|
host: optStr("your host identity (e.g. claude-code, cursor, codex) \u2014 recorded as self-reported provenance"),
|
|
19089
19461
|
model: str("the model that proposed the candidate \u2014 REQUIRED; recorded as self-reported provenance, and the CLI refuses to score without it"),
|
|
19090
|
-
rebind:
|
|
19462
|
+
rebind: z16.boolean().optional().describe("explicitly re-bind an already-bound bundle to a DIFFERENT recording set \u2014 scoring refuses this otherwise, because rebinding silently rewrites the bundle's verification identity; only pass after telling the user"),
|
|
19091
19463
|
library: optStr("workspace root holding confirmed partners generated bundles (ADR-013 composed pins; default: the server's working directory) \u2014 pass the same root the brief used")
|
|
19092
19464
|
}),
|
|
19093
19465
|
argv: (i) => [
|
|
@@ -19106,7 +19478,7 @@ var init_server = __esm({
|
|
|
19106
19478
|
{
|
|
19107
19479
|
name: "tendril_codeconnect",
|
|
19108
19480
|
description: "Emit a Figma Code Connect template (.figma.ts) for a certified bundle \u2014 every Figma variant value mapped to its verified prop fragment from recorded truth, stamped with the bundle's trust statement. EXTRA VALUE step after verify passes: offer it to the user. Publishing is the USER'S action (their Figma token, Organization/Enterprise plan) \u2014 via npx @figma/code-connect connect publish, or the Figma MCP's own add_code_connect_map/send_code_connect_mappings tools if available in this session.",
|
|
19109
|
-
schema:
|
|
19481
|
+
schema: z16.object({
|
|
19110
19482
|
bundleDir: str("bundle directory (carries component.json)"),
|
|
19111
19483
|
figmaUrl: str("figma.com /design/ URL of the COMPONENT SET, with node-id (ask the user to Copy link to selection if you don't have it)"),
|
|
19112
19484
|
set: optStr("recording set override (default: the bundle's provenance path)"),
|
|
@@ -19124,7 +19496,7 @@ var init_server = __esm({
|
|
|
19124
19496
|
{
|
|
19125
19497
|
name: "tendril_verify",
|
|
19126
19498
|
description: "Verify/check that a component matches its Figma design \u2014 use when the user asks whether an implementation is faithful to the design, or to re-certify an existing Tendril bundle. Recomputes full verification (per-config status, behaviors, composition, evidence artifacts). Free, account-less, network-less \u2014 the trust anchor. Exit 5 means below the target bar with an honest report \u2014 sub-bar scores, or at bar cert a config demoted by absent-ink clusters. A GREEN run writes the inspect sheet itself and returns `next`: its terminal step is publishing (tendril_publish \u2192 tendril_publish_wait) \u2014 a verified component reachable only on local disk is an unfinished run; a sub-bar run never publishes.",
|
|
19127
|
-
schema:
|
|
19499
|
+
schema: z16.object({
|
|
19128
19500
|
bundleDir: str("bundle directory to verify"),
|
|
19129
19501
|
bar: optStr("pass (default) or cert"),
|
|
19130
19502
|
set: optStr("recording-set directory override"),
|
|
@@ -19141,12 +19513,12 @@ var init_server = __esm({
|
|
|
19141
19513
|
{
|
|
19142
19514
|
name: "tendril_generate_curated",
|
|
19143
19515
|
description: "CURATED engine (explicit alternative path): generation by an allowlisted API model over the user's OpenRouter-compatible key, with cost consent, hard spend caps, and resume. Use ONLY when the user asks for API-model generation instead of implementing it yourself.",
|
|
19144
|
-
schema:
|
|
19516
|
+
schema: z16.object({
|
|
19145
19517
|
input: str("reference task name or recording-set directory"),
|
|
19146
19518
|
model: optStr("OpenRouter model id (default: allowlist pointer)"),
|
|
19147
19519
|
bar: optStr("pass (default) or cert"),
|
|
19148
19520
|
cap: optStr("spend cap in USD (default 1.50)"),
|
|
19149
|
-
yes:
|
|
19521
|
+
yes: z16.boolean().optional().describe("accept the cost consent (the user must have approved the spend)")
|
|
19150
19522
|
}),
|
|
19151
19523
|
argv: (i) => [
|
|
19152
19524
|
"generate",
|
|
@@ -19411,6 +19783,57 @@ var init_permissions = __esm({
|
|
|
19411
19783
|
}
|
|
19412
19784
|
});
|
|
19413
19785
|
|
|
19786
|
+
// packages/cli/src/commands/publish-recordings.ts
|
|
19787
|
+
import { existsSync as existsSync45, readFileSync as readFileSync41, readdirSync as readdirSync18, statSync as statSync6 } from "node:fs";
|
|
19788
|
+
import path55 from "node:path";
|
|
19789
|
+
import { createHash as createHash13 } from "node:crypto";
|
|
19790
|
+
function sha256Sync(chunks) {
|
|
19791
|
+
const h = createHash13("sha256");
|
|
19792
|
+
for (const chunk of chunks) h.update(chunk);
|
|
19793
|
+
return h.digest("hex");
|
|
19794
|
+
}
|
|
19795
|
+
function planRecordingCarry(input) {
|
|
19796
|
+
const chosen = input.override ?? input.provenancePath;
|
|
19797
|
+
const setDir = path55.resolve(input.cwd, chosen);
|
|
19798
|
+
const named = input.override === void 0 ? "the recording set this bundle names" : "the recording set you named";
|
|
19799
|
+
const without = (why) => ({
|
|
19800
|
+
carried: false,
|
|
19801
|
+
note: `publishing without the recording set: ${why} (looked in ${setDir})`
|
|
19802
|
+
});
|
|
19803
|
+
if (!existsSync45(setDir)) return without(`${named} is not on this machine`);
|
|
19804
|
+
try {
|
|
19805
|
+
if (!statSync6(setDir).isDirectory()) return without(`${named} is not a directory`);
|
|
19806
|
+
} catch (error) {
|
|
19807
|
+
return without(`${named} could not be read (${error.message})`);
|
|
19808
|
+
}
|
|
19809
|
+
let packed;
|
|
19810
|
+
try {
|
|
19811
|
+
packed = packRecordingArchive(
|
|
19812
|
+
{
|
|
19813
|
+
exists: (relPath) => existsSync45(path55.join(setDir, relPath)),
|
|
19814
|
+
read: (relPath) => new Uint8Array(readFileSync41(path55.join(setDir, relPath))),
|
|
19815
|
+
listRep: (rep) => existsSync45(path55.join(setDir, rep)) ? readdirSync18(path55.join(setDir, rep)) : []
|
|
19816
|
+
},
|
|
19817
|
+
{ sha256: sha256Sync }
|
|
19818
|
+
);
|
|
19819
|
+
} catch (error) {
|
|
19820
|
+
return without(`${named} could not be packed (${error.message})`);
|
|
19821
|
+
}
|
|
19822
|
+
if (!packed.ok) return without(packed.refusal);
|
|
19823
|
+
if (packed.setHash !== input.scoredSetHash) {
|
|
19824
|
+
return without(
|
|
19825
|
+
`${named} is not the one this verdict was measured against (the set here derives ${packed.setHash.slice(0, 12)}\u2026, the report was scored on ${input.scoredSetHash.slice(0, 12)}\u2026)`
|
|
19826
|
+
);
|
|
19827
|
+
}
|
|
19828
|
+
return { carried: true, envelopeText: packed.envelopeText, setHash: packed.setHash, setDir, staleEnrichment: packed.staleEnrichment };
|
|
19829
|
+
}
|
|
19830
|
+
var init_publish_recordings = __esm({
|
|
19831
|
+
"packages/cli/src/commands/publish-recordings.ts"() {
|
|
19832
|
+
"use strict";
|
|
19833
|
+
init_src4();
|
|
19834
|
+
}
|
|
19835
|
+
});
|
|
19836
|
+
|
|
19414
19837
|
// packages/cli/src/commands/publish.ts
|
|
19415
19838
|
var publish_exports = {};
|
|
19416
19839
|
__export(publish_exports, {
|
|
@@ -19419,8 +19842,8 @@ __export(publish_exports, {
|
|
|
19419
19842
|
runPublish: () => runPublish,
|
|
19420
19843
|
spendPendingApproval: () => spendPendingApproval
|
|
19421
19844
|
});
|
|
19422
|
-
import { existsSync as
|
|
19423
|
-
import
|
|
19845
|
+
import { existsSync as existsSync46, readFileSync as readFileSync42, rmSync as rmSync8, writeFileSync as writeFileSync23 } from "node:fs";
|
|
19846
|
+
import path56 from "node:path";
|
|
19424
19847
|
async function runPublish(opts) {
|
|
19425
19848
|
if (opts.waitWindowSeconds !== void 0 && opts.approveWait !== true) {
|
|
19426
19849
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -19436,7 +19859,7 @@ async function runPublish(opts) {
|
|
|
19436
19859
|
remediation: "Pass e.g. --wait-window 55."
|
|
19437
19860
|
});
|
|
19438
19861
|
}
|
|
19439
|
-
const bundleDir =
|
|
19862
|
+
const bundleDir = path56.resolve(opts.bundleDir);
|
|
19440
19863
|
const bundle = readBundle(opts, bundleDir);
|
|
19441
19864
|
const report = bundle.report;
|
|
19442
19865
|
if (typeof report["rulerExit"] !== "number" || !Number.isInteger(report["rulerExit"])) {
|
|
@@ -19481,7 +19904,7 @@ async function runPublish(opts) {
|
|
|
19481
19904
|
const sheetEntry = surface.published.find((p) => p.role === "inspect-sheet");
|
|
19482
19905
|
if (sheetEntry !== void 0) {
|
|
19483
19906
|
const missingCrops = missingInspectCrops(
|
|
19484
|
-
|
|
19907
|
+
readFileSync42(path56.join(bundleDir, sheetEntry.path), "utf8"),
|
|
19485
19908
|
surface.published.map((p) => p.path)
|
|
19486
19909
|
);
|
|
19487
19910
|
if (missingCrops.length > 0) {
|
|
@@ -19583,8 +20006,8 @@ async function runPublish(opts) {
|
|
|
19583
20006
|
if (opts.approveWait === true) spendPendingApproval();
|
|
19584
20007
|
const uploaded = [];
|
|
19585
20008
|
for (const object of opened.value.plan.objects) {
|
|
19586
|
-
const file =
|
|
19587
|
-
if (!
|
|
20009
|
+
const file = path56.join(bundleDir, object.relPath);
|
|
20010
|
+
if (!existsSync46(file)) {
|
|
19588
20011
|
fail(opts, ExitCode.InputValidation, {
|
|
19589
20012
|
error: `the portal expects ${object.relPath}, and it is not in ${opts.bundleDir}`,
|
|
19590
20013
|
code: "planned-file-missing",
|
|
@@ -19594,12 +20017,49 @@ async function runPublish(opts) {
|
|
|
19594
20017
|
const sent = await client.upload({
|
|
19595
20018
|
publicationId: opened.value.publicationId,
|
|
19596
20019
|
relPath: object.relPath,
|
|
19597
|
-
bytes: new Uint8Array(
|
|
20020
|
+
bytes: new Uint8Array(readFileSync42(file))
|
|
19598
20021
|
});
|
|
19599
20022
|
if (!sent.ok) refuse(opts, sent, "upload-refused", true);
|
|
19600
20023
|
uploaded.push({ relPath: object.relPath, deduplicated: sent.value.deduplicated });
|
|
19601
20024
|
emitProgress(uploaded.length, opened.value.plan.objects.length, `uploading ${object.relPath}`);
|
|
19602
20025
|
}
|
|
20026
|
+
let recordingNote;
|
|
20027
|
+
let carried;
|
|
20028
|
+
if (opts.noRecordings === true) {
|
|
20029
|
+
} else if (client.carryRecordings === void 0) {
|
|
20030
|
+
recordingNote = "publishing without the recording set: this client cannot carry recordings";
|
|
20031
|
+
} else {
|
|
20032
|
+
const plan = planRecordingCarry({
|
|
20033
|
+
bundleDir,
|
|
20034
|
+
cwd: process.cwd(),
|
|
20035
|
+
...opts.recordings === void 0 ? {} : { override: opts.recordings },
|
|
20036
|
+
provenancePath: bundle.manifest.provenance.recordingSet.path,
|
|
20037
|
+
scoredSetHash: scoredRecordingSetHash(report)
|
|
20038
|
+
});
|
|
20039
|
+
if (!plan.carried) {
|
|
20040
|
+
recordingNote = plan.note;
|
|
20041
|
+
} else {
|
|
20042
|
+
emitProgress(uploaded.length, uploaded.length, "carrying the recording set");
|
|
20043
|
+
let sent;
|
|
20044
|
+
try {
|
|
20045
|
+
sent = await client.carryRecordings({ publicationId: opened.value.publicationId, envelopeText: plan.envelopeText });
|
|
20046
|
+
} catch (error) {
|
|
20047
|
+
sent = { ok: false, status: 0, refusal: `the recording set could not be sent (${error.message})` };
|
|
20048
|
+
}
|
|
20049
|
+
if (sent.ok) {
|
|
20050
|
+
carried = { setHash: plan.setHash, setDir: plan.setDir, sizeBytes: plan.envelopeText.length, staleEnrichment: plan.staleEnrichment };
|
|
20051
|
+
} else if (sent.alreadyCarried === true) {
|
|
20052
|
+
carried = { setHash: sent.setHash ?? plan.setHash, setDir: plan.setDir, sizeBytes: plan.envelopeText.length, staleEnrichment: plan.staleEnrichment };
|
|
20053
|
+
recordingNote = "this publication already carried its recording set from an earlier attempt, and that copy stands \u2014 the set on this machine has since changed outside the scored hash (a `record bindings` re-run does this), which changes nothing about the verdict";
|
|
20054
|
+
} else {
|
|
20055
|
+
recordingNote = sent.status === 404 ? "publishing without the recording set: this portal does not carry recording sets yet \u2014 the component published normally" : `publishing without the recording set: ${sent.refusal}`;
|
|
20056
|
+
recordingNote += ` (the set here is ${plan.setDir}, ${plan.setHash.slice(0, 12)}\u2026)`;
|
|
20057
|
+
if (sent.needsConfirmation?.approvePath !== void 0) {
|
|
20058
|
+
recordingNote += ` \u2014 approve it once at ${origin}${sent.needsConfirmation.approvePath}, then publish again`;
|
|
20059
|
+
}
|
|
20060
|
+
}
|
|
20061
|
+
}
|
|
20062
|
+
}
|
|
19603
20063
|
emitProgress(uploaded.length, uploaded.length, "upload complete \u2014 committing the publication");
|
|
19604
20064
|
const committed = await client.commit({ publicationId: opened.value.publicationId });
|
|
19605
20065
|
if (committed.ok) {
|
|
@@ -19624,7 +20084,15 @@ async function runPublish(opts) {
|
|
|
19624
20084
|
figmaFile: opened.value.figmaFile,
|
|
19625
20085
|
rulerVersion: opened.value.rulerVersion,
|
|
19626
20086
|
resumed: opened.value.resumed === true,
|
|
19627
|
-
files: uploaded
|
|
20087
|
+
files: uploaded,
|
|
20088
|
+
recordings: carried === void 0 ? { carried: false, ...recordingNote === void 0 ? {} : { note: recordingNote } } : {
|
|
20089
|
+
carried: true,
|
|
20090
|
+
setHash: carried.setHash,
|
|
20091
|
+
setDir: carried.setDir,
|
|
20092
|
+
sizeBytes: carried.sizeBytes,
|
|
20093
|
+
staleEnrichment: carried.staleEnrichment,
|
|
20094
|
+
...recordingNote === void 0 ? {} : { note: recordingNote }
|
|
20095
|
+
}
|
|
19628
20096
|
},
|
|
19629
20097
|
() => {
|
|
19630
20098
|
const reused = uploaded.filter((u) => u.deduplicated).length;
|
|
@@ -19637,27 +20105,42 @@ async function runPublish(opts) {
|
|
|
19637
20105
|
`);
|
|
19638
20106
|
process.stdout.write(` verdict as scored by ruler ${opened.value.rulerVersion}
|
|
19639
20107
|
`);
|
|
20108
|
+
if (carried !== void 0) {
|
|
20109
|
+
process.stdout.write(` recording set carried \u2014 pull it on another machine with \`${tendrilCommand(`pull ${JSON.stringify(componentName)}`)}\`
|
|
20110
|
+
`);
|
|
20111
|
+
if (recordingNote !== void 0) process.stdout.write(` ${recordingNote}
|
|
20112
|
+
`);
|
|
20113
|
+
if (carried.staleEnrichment.length > 0) {
|
|
20114
|
+
process.stdout.write(
|
|
20115
|
+
` left ${String(carried.staleEnrichment.length)} stale binding record(s) behind (${carried.staleEnrichment.slice(0, 3).join(", ")}) \u2014 re-run \`${tendrilCommand("record bindings")}\` after pulling
|
|
20116
|
+
`
|
|
20117
|
+
);
|
|
20118
|
+
}
|
|
20119
|
+
} else if (recordingNote !== void 0) {
|
|
20120
|
+
process.stdout.write(` ${recordingNote}
|
|
20121
|
+
`);
|
|
20122
|
+
}
|
|
19640
20123
|
}
|
|
19641
20124
|
);
|
|
19642
20125
|
}
|
|
19643
20126
|
function readBundle(opts, bundleDir) {
|
|
19644
|
-
const manifestPath2 =
|
|
19645
|
-
const reportPath =
|
|
19646
|
-
if (!
|
|
20127
|
+
const manifestPath2 = path56.join(bundleDir, "component.json");
|
|
20128
|
+
const reportPath = path56.join(bundleDir, EVIDENCE_DIR, VERIFY_REPORT_FILENAME);
|
|
20129
|
+
if (!existsSync46(manifestPath2)) {
|
|
19647
20130
|
fail(opts, ExitCode.InputValidation, {
|
|
19648
20131
|
error: `there is no component.json in ${opts.bundleDir}, so this is not a bundle`,
|
|
19649
20132
|
code: "not-a-bundle",
|
|
19650
20133
|
remediation: `Point publish at a directory a Tendril run produced. \`${tendrilCommand("generate --help")}\` shows how one is made.`
|
|
19651
20134
|
});
|
|
19652
20135
|
}
|
|
19653
|
-
if (!
|
|
20136
|
+
if (!existsSync46(reportPath)) {
|
|
19654
20137
|
fail(opts, ExitCode.InputValidation, {
|
|
19655
20138
|
error: `this bundle has never been verified \u2014 there is no ${EVIDENCE_DIR}/${VERIFY_REPORT_FILENAME}`,
|
|
19656
20139
|
code: "bundle-not-verified",
|
|
19657
20140
|
remediation: `Run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` first. Verification is free and needs no account; publishing without it would put a page up with no verdict on it.`
|
|
19658
20141
|
});
|
|
19659
20142
|
}
|
|
19660
|
-
const { manifest } = readBundleManifest(
|
|
20143
|
+
const { manifest } = readBundleManifest(readFileSync42(manifestPath2, "utf8"));
|
|
19661
20144
|
if (manifest === void 0) {
|
|
19662
20145
|
fail(opts, ExitCode.InputValidation, {
|
|
19663
20146
|
error: "component.json did not parse as a bundle manifest",
|
|
@@ -19665,7 +20148,7 @@ function readBundle(opts, bundleDir) {
|
|
|
19665
20148
|
remediation: `Re-emit the bundle \u2014 \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` rewrites component.json \u2014 then publish.`
|
|
19666
20149
|
});
|
|
19667
20150
|
}
|
|
19668
|
-
const reportText =
|
|
20151
|
+
const reportText = readFileSync42(reportPath, "utf8");
|
|
19669
20152
|
let report;
|
|
19670
20153
|
try {
|
|
19671
20154
|
report = JSON.parse(reportText);
|
|
@@ -19733,7 +20216,7 @@ function refuse(opts, sent, code, rejoins = false) {
|
|
|
19733
20216
|
});
|
|
19734
20217
|
}
|
|
19735
20218
|
function pendingApprovalPath() {
|
|
19736
|
-
return
|
|
20219
|
+
return path56.join(path56.dirname(sessionPath()), "pending-publish.json");
|
|
19737
20220
|
}
|
|
19738
20221
|
async function runApprovalFlow(opts, client, bundleDir, input) {
|
|
19739
20222
|
const requested = await client.requestApproval({ componentName: input.componentName, figmaFile: input.figmaFile });
|
|
@@ -19780,9 +20263,9 @@ async function runApprovalFlow(opts, client, bundleDir, input) {
|
|
|
19780
20263
|
async function approveWaitPhase(opts, client, bundleDir, subject) {
|
|
19781
20264
|
const file = pendingApprovalPath();
|
|
19782
20265
|
let pending;
|
|
19783
|
-
if (
|
|
20266
|
+
if (existsSync46(file)) {
|
|
19784
20267
|
try {
|
|
19785
|
-
const parsed = JSON.parse(
|
|
20268
|
+
const parsed = JSON.parse(readFileSync42(file, "utf8"));
|
|
19786
20269
|
if (typeof parsed.approvalId === "string" && typeof parsed.approveUrl === "string" && typeof parsed.expiresAt === "string") {
|
|
19787
20270
|
pending = { pollSeconds: 3, bundleDir, ...parsed };
|
|
19788
20271
|
}
|
|
@@ -19871,6 +20354,7 @@ var init_publish = __esm({
|
|
|
19871
20354
|
init_src4();
|
|
19872
20355
|
init_invocation();
|
|
19873
20356
|
init_output();
|
|
20357
|
+
init_publish_recordings();
|
|
19874
20358
|
init_publish_client();
|
|
19875
20359
|
init_run_presence();
|
|
19876
20360
|
APPROVAL_WAIT_CAP_MS = 31 * 6e4;
|
|
@@ -19886,9 +20370,9 @@ __export(compose_approve_exports, {
|
|
|
19886
20370
|
runComposeApproveStart: () => runComposeApproveStart,
|
|
19887
20371
|
runComposeApproveWait: () => runComposeApproveWait
|
|
19888
20372
|
});
|
|
19889
|
-
import { createHash as
|
|
19890
|
-
import { existsSync as
|
|
19891
|
-
import
|
|
20373
|
+
import { createHash as createHash14 } from "node:crypto";
|
|
20374
|
+
import { existsSync as existsSync47, mkdirSync as mkdirSync14, readFileSync as readFileSync43, rmSync as rmSync9, writeFileSync as writeFileSync24 } from "node:fs";
|
|
20375
|
+
import path57 from "node:path";
|
|
19892
20376
|
function composeSubjectDigest(subject) {
|
|
19893
20377
|
const preimage = JSON.stringify([
|
|
19894
20378
|
subject.hostComponent,
|
|
@@ -19899,16 +20383,16 @@ function composeSubjectDigest(subject) {
|
|
|
19899
20383
|
subject.instances.map((i) => [i.hostRep, i.instanceId, i.poseVariantNodeId]),
|
|
19900
20384
|
subject.disclosures
|
|
19901
20385
|
]);
|
|
19902
|
-
return
|
|
20386
|
+
return createHash14("sha256").update(preimage, "utf8").digest("hex");
|
|
19903
20387
|
}
|
|
19904
20388
|
function composeSubjectFor(hostSet, pair) {
|
|
19905
20389
|
const manifest = loadManifest(hostSet);
|
|
19906
20390
|
const canonical = {
|
|
19907
20391
|
hostComponent: manifest.component,
|
|
19908
20392
|
hostFigmaFile: manifest.figmaFile ?? "unidentified",
|
|
19909
|
-
hostManifestSha256:
|
|
20393
|
+
hostManifestSha256: createHash14("sha256").update(readFileSync43(path57.join(hostSet, "recording-set.json"))).digest("hex"),
|
|
19910
20394
|
pairKey: pair.key,
|
|
19911
|
-
partnerManifestSha256: pair.partnerDirs.map((d) => [fromStoredRel(
|
|
20395
|
+
partnerManifestSha256: pair.partnerDirs.map((d) => [fromStoredRel(path57.relative(hostSet, d)), createHash14("sha256").update(readFileSync43(path57.join(d, "recording-set.json"))).digest("hex")]).sort((a, b) => a[0] < b[0] ? -1 : 1),
|
|
19912
20396
|
instances: [...pair.instances].map((i) => ({ hostRep: i.hostRep, instanceId: i.instanceId, poseVariantNodeId: i.poseVariantNodeId })).sort((a, b) => a.hostRep + a.instanceId < b.hostRep + b.instanceId ? -1 : 1),
|
|
19913
20397
|
disclosures: [...pair.disclosures]
|
|
19914
20398
|
};
|
|
@@ -19961,10 +20445,10 @@ function composePortalClient(flags) {
|
|
|
19961
20445
|
return new HttpPublishClient({ origin, token: found.token });
|
|
19962
20446
|
}
|
|
19963
20447
|
function pendingComposePath() {
|
|
19964
|
-
return
|
|
20448
|
+
return path57.join(path57.dirname(sessionPath()), "pending-compose.json");
|
|
19965
20449
|
}
|
|
19966
20450
|
function openPairsFor(hostSet, roots) {
|
|
19967
|
-
const scanRoots = [.../* @__PURE__ */ new Set([...roots,
|
|
20451
|
+
const scanRoots = [.../* @__PURE__ */ new Set([...roots, path57.dirname(hostSet)])];
|
|
19968
20452
|
const edges = composeReport(buildComposeIndex(scanRoots));
|
|
19969
20453
|
const pairs = substitutionPairs(edges, hostSet);
|
|
19970
20454
|
const { raw } = readManifestFile(hostSet);
|
|
@@ -19975,7 +20459,7 @@ function openPairsFor(hostSet, roots) {
|
|
|
19975
20459
|
return pairs.filter((p) => !decidedKeys.has(p.key));
|
|
19976
20460
|
}
|
|
19977
20461
|
async function runComposeApproveStart(flags, hostSet, roots) {
|
|
19978
|
-
if (!
|
|
20462
|
+
if (!existsSync47(path57.join(hostSet, "recording-set.json"))) {
|
|
19979
20463
|
fail(flags, ExitCode.InputValidation, { error: `no recording-set.json in ${hostSet}`, code: "no-recording-set", remediation: "Point --set at a recorded host set." });
|
|
19980
20464
|
}
|
|
19981
20465
|
const open = openPairsFor(hostSet, roots);
|
|
@@ -20030,7 +20514,7 @@ async function runComposeApproveStart(flags, hostSet, roots) {
|
|
|
20030
20514
|
roots,
|
|
20031
20515
|
requestedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
20032
20516
|
};
|
|
20033
|
-
mkdirSync14(
|
|
20517
|
+
mkdirSync14(path57.dirname(pendingComposePath()), { recursive: true });
|
|
20034
20518
|
writeFileSync24(pendingComposePath(), `${JSON.stringify(pending, null, 2)}
|
|
20035
20519
|
`, { mode: 384 });
|
|
20036
20520
|
emitData(
|
|
@@ -20058,9 +20542,9 @@ async function runComposeApproveStart(flags, hostSet, roots) {
|
|
|
20058
20542
|
async function runComposeApproveWait(flags, hostSet) {
|
|
20059
20543
|
const file = pendingComposePath();
|
|
20060
20544
|
let pending;
|
|
20061
|
-
if (
|
|
20545
|
+
if (existsSync47(file)) {
|
|
20062
20546
|
try {
|
|
20063
|
-
const parsed = JSON.parse(
|
|
20547
|
+
const parsed = JSON.parse(readFileSync43(file, "utf8"));
|
|
20064
20548
|
if (typeof parsed.approvalId === "string" && typeof parsed.subjectDigest === "string" && typeof parsed.hostSet === "string" && typeof parsed.pairKey === "string") {
|
|
20065
20549
|
pending = parsed;
|
|
20066
20550
|
}
|
|
@@ -20074,7 +20558,7 @@ async function runComposeApproveWait(flags, hostSet) {
|
|
|
20074
20558
|
remediation: `Start one first: ${tendrilCommand(`compose --set ${quoteArg(hostSet)} --approve-start`)} (the tendril_compose tool).`
|
|
20075
20559
|
});
|
|
20076
20560
|
}
|
|
20077
|
-
if (
|
|
20561
|
+
if (path57.resolve(pending.hostSet) !== path57.resolve(hostSet)) {
|
|
20078
20562
|
fail(flags, ExitCode.InputValidation, {
|
|
20079
20563
|
error: `the waiting approval is for ${pending.hostSet}, and this wait is for ${hostSet}`,
|
|
20080
20564
|
code: "pending-compose-mismatch",
|
|
@@ -20206,7 +20690,7 @@ async function runComposeApproveWait(flags, hostSet) {
|
|
|
20206
20690
|
}
|
|
20207
20691
|
async function runComposeApprove(flags) {
|
|
20208
20692
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
20209
|
-
const roots = flags.library !== void 0 && flags.library.length > 0 ? flags.library.map((d) =>
|
|
20693
|
+
const roots = flags.library !== void 0 && flags.library.length > 0 ? flags.library.map((d) => path57.resolve(base, d)) : [base];
|
|
20210
20694
|
if (flags.set === void 0) {
|
|
20211
20695
|
fail(flags, ExitCode.InputValidation, {
|
|
20212
20696
|
error: "a compose decision flag requires --set <host> \u2014 a decision needs the host set it decides for",
|
|
@@ -20228,7 +20712,7 @@ async function runComposeApprove(flags) {
|
|
|
20228
20712
|
remediation: "Pass e.g. --wait-window 55."
|
|
20229
20713
|
});
|
|
20230
20714
|
}
|
|
20231
|
-
const hostSet =
|
|
20715
|
+
const hostSet = path57.resolve(base, flags.set);
|
|
20232
20716
|
if (flags.approveStart === true) {
|
|
20233
20717
|
await runComposeApproveStart(flags, hostSet, roots);
|
|
20234
20718
|
return;
|
|
@@ -20259,8 +20743,8 @@ __export(login_exports, {
|
|
|
20259
20743
|
runLogout: () => runLogout
|
|
20260
20744
|
});
|
|
20261
20745
|
import { spawn } from "node:child_process";
|
|
20262
|
-
import { existsSync as
|
|
20263
|
-
import
|
|
20746
|
+
import { existsSync as existsSync48, mkdirSync as mkdirSync15, readFileSync as readFileSync44, rmSync as rmSync10, writeFileSync as writeFileSync25 } from "node:fs";
|
|
20747
|
+
import path58 from "node:path";
|
|
20264
20748
|
import { isCancel as isCancel3, password as password2 } from "@clack/prompts";
|
|
20265
20749
|
async function runLogin(opts, deps) {
|
|
20266
20750
|
const origin = (opts.to ?? process.env["TENDRIL_PORTAL_URL"] ?? DEFAULT_PORTAL_ORIGIN).replace(/\/+$/, "");
|
|
@@ -20354,12 +20838,12 @@ function settleDecision(opts, origin, outcome) {
|
|
|
20354
20838
|
}
|
|
20355
20839
|
}
|
|
20356
20840
|
function pendingLoginPath() {
|
|
20357
|
-
return
|
|
20841
|
+
return path58.join(path58.dirname(sessionPath()), "pending-login.json");
|
|
20358
20842
|
}
|
|
20359
20843
|
async function deviceStartPhase(opts, origin, deps) {
|
|
20360
20844
|
const started = await startHandshake(opts, origin, deps);
|
|
20361
20845
|
const file = pendingLoginPath();
|
|
20362
|
-
mkdirSync15(
|
|
20846
|
+
mkdirSync15(path58.dirname(file), { recursive: true });
|
|
20363
20847
|
writeFileSync25(file, `${JSON.stringify({ origin, ...started }, null, 2)}
|
|
20364
20848
|
`, { mode: 384 });
|
|
20365
20849
|
deps.openBrowser(started.verificationUrl);
|
|
@@ -20385,9 +20869,9 @@ async function deviceStartPhase(opts, origin, deps) {
|
|
|
20385
20869
|
async function deviceWaitPhase(opts, deps) {
|
|
20386
20870
|
const file = pendingLoginPath();
|
|
20387
20871
|
let pending;
|
|
20388
|
-
if (
|
|
20872
|
+
if (existsSync48(file)) {
|
|
20389
20873
|
try {
|
|
20390
|
-
const parsed = JSON.parse(
|
|
20874
|
+
const parsed = JSON.parse(readFileSync44(file, "utf8"));
|
|
20391
20875
|
if (typeof parsed.origin === "string" && typeof parsed.deviceCode === "string" && typeof parsed.intervalSeconds === "number") {
|
|
20392
20876
|
pending = parsed;
|
|
20393
20877
|
}
|
|
@@ -20529,10 +21013,10 @@ var figma_connect_exports = {};
|
|
|
20529
21013
|
__export(figma_connect_exports, {
|
|
20530
21014
|
runFigmaConnect: () => runFigmaConnect
|
|
20531
21015
|
});
|
|
20532
|
-
import { existsSync as
|
|
20533
|
-
import
|
|
21016
|
+
import { existsSync as existsSync49, mkdirSync as mkdirSync16, readFileSync as readFileSync45, rmSync as rmSync11, writeFileSync as writeFileSync26 } from "node:fs";
|
|
21017
|
+
import path59 from "node:path";
|
|
20534
21018
|
function pendingConnectPath() {
|
|
20535
|
-
return
|
|
21019
|
+
return path59.join(path59.dirname(sessionPath()), "pending-figma-connect.json");
|
|
20536
21020
|
}
|
|
20537
21021
|
function resolveOrigin2(opts) {
|
|
20538
21022
|
const named = (opts.to ?? process.env["TENDRIL_PORTAL_URL"] ?? "").replace(/\/+$/, "");
|
|
@@ -20569,7 +21053,7 @@ async function runFigmaConnect(opts) {
|
|
|
20569
21053
|
const started = await startConnect(opts, send, origin, token);
|
|
20570
21054
|
if (opts.start === true) {
|
|
20571
21055
|
const pending = { origin, ...started };
|
|
20572
|
-
mkdirSync16(
|
|
21056
|
+
mkdirSync16(path59.dirname(pendingConnectPath()), { recursive: true });
|
|
20573
21057
|
writeFileSync26(pendingConnectPath(), `${JSON.stringify(pending, null, 2)}
|
|
20574
21058
|
`, { mode: 384 });
|
|
20575
21059
|
(opts.openBrowser ?? (() => {
|
|
@@ -20623,9 +21107,9 @@ async function startConnect(opts, send, origin, token) {
|
|
|
20623
21107
|
async function waitPhase(opts, send) {
|
|
20624
21108
|
const file = pendingConnectPath();
|
|
20625
21109
|
let pending;
|
|
20626
|
-
if (
|
|
21110
|
+
if (existsSync49(file)) {
|
|
20627
21111
|
try {
|
|
20628
|
-
const parsed = JSON.parse(
|
|
21112
|
+
const parsed = JSON.parse(readFileSync45(file, "utf8"));
|
|
20629
21113
|
if (typeof parsed.origin === "string" && typeof parsed.connectId === "string") pending = parsed;
|
|
20630
21114
|
} catch {
|
|
20631
21115
|
}
|
|
@@ -20823,6 +21307,314 @@ var init_share = __esm({
|
|
|
20823
21307
|
}
|
|
20824
21308
|
});
|
|
20825
21309
|
|
|
21310
|
+
// packages/cli/src/commands/pull.ts
|
|
21311
|
+
var pull_exports = {};
|
|
21312
|
+
__export(pull_exports, {
|
|
21313
|
+
recordingSlug: () => recordingSlug,
|
|
21314
|
+
runPull: () => runPull
|
|
21315
|
+
});
|
|
21316
|
+
import { createHash as createHash15 } from "node:crypto";
|
|
21317
|
+
import { existsSync as existsSync50, mkdirSync as mkdirSync17, mkdtempSync as mkdtempSync4, readFileSync as readFileSync46, readdirSync as readdirSync19, renameSync as renameSync2, rmSync as rmSync12, writeFileSync as writeFileSync27 } from "node:fs";
|
|
21318
|
+
import { tmpdir } from "node:os";
|
|
21319
|
+
import path60 from "node:path";
|
|
21320
|
+
function setHashFromDisk(dir) {
|
|
21321
|
+
const manifest = path60.join(dir, "recording-set.json");
|
|
21322
|
+
if (!existsSync50(manifest)) return { ok: false, refusal: "it carries no recording-set.json" };
|
|
21323
|
+
const shape = readSetShape(new Uint8Array(readFileSync46(manifest)));
|
|
21324
|
+
if (!shape.ok) return { ok: false, refusal: shape.refusal };
|
|
21325
|
+
const enumeration = recordingSetEnumeration(
|
|
21326
|
+
{ channeled: shape.shape.channeled, reps: shape.shape.reps },
|
|
21327
|
+
{
|
|
21328
|
+
exists: (relPath) => existsSync50(path60.join(dir, relPath)),
|
|
21329
|
+
listRep: (rep) => existsSync50(path60.join(dir, rep)) ? readdirSync19(path60.join(dir, rep)) : []
|
|
21330
|
+
}
|
|
21331
|
+
);
|
|
21332
|
+
return { ok: true, setHash: hashRecordingSet(enumeration, (relPath) => new Uint8Array(readFileSync46(path60.join(dir, relPath))), sha256) };
|
|
21333
|
+
}
|
|
21334
|
+
function recordingSlug(componentName) {
|
|
21335
|
+
const collapsed = componentName.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
21336
|
+
return collapsed === "" ? "recording-set" : collapsed;
|
|
21337
|
+
}
|
|
21338
|
+
function portalClient(opts) {
|
|
21339
|
+
if (opts.client !== void 0) return opts.client;
|
|
21340
|
+
const origin = resolveOrigin({ to: opts.to });
|
|
21341
|
+
if (origin === "") {
|
|
21342
|
+
fail(opts, ExitCode.InputValidation, {
|
|
21343
|
+
error: "no portal to pull from",
|
|
21344
|
+
code: "no-portal-configured",
|
|
21345
|
+
remediation: "Pass --to <url> or set TENDRIL_PORTAL_URL. Recording sets live in the account that published them."
|
|
21346
|
+
});
|
|
21347
|
+
}
|
|
21348
|
+
if (!isSecureOrigin(origin)) {
|
|
21349
|
+
fail(opts, ExitCode.InputValidation, {
|
|
21350
|
+
error: `${origin} is not https, so a session token sent there would travel in the clear`,
|
|
21351
|
+
code: "portal-not-https",
|
|
21352
|
+
remediation: "Use the https:// address of your portal. Only 127.0.0.1 and localhost are exempt, for local development."
|
|
21353
|
+
});
|
|
21354
|
+
}
|
|
21355
|
+
const found = tokenFor(origin);
|
|
21356
|
+
if (!found.ok && found.reason === "origin-mismatch") {
|
|
21357
|
+
fail(opts, ExitCode.Auth, {
|
|
21358
|
+
error: `the session available here belongs to ${found.boundTo}, and this would ask ${origin}`,
|
|
21359
|
+
code: "session-belongs-to-another-portal",
|
|
21360
|
+
remediation: "Sign in to the portal that holds these recordings."
|
|
21361
|
+
});
|
|
21362
|
+
}
|
|
21363
|
+
if (!found.ok) {
|
|
21364
|
+
fail(opts, ExitCode.Auth, {
|
|
21365
|
+
error: `no session for ${origin}`,
|
|
21366
|
+
code: "not-signed-in",
|
|
21367
|
+
remediation: "A recording set belongs to the account that published it, so this one call needs you signed in. Agents: run tendril_login \u2014 the user signs in with ONE browser Approve, then re-run this. (Verifying a bundle you already have needs no account, ever.)"
|
|
21368
|
+
});
|
|
21369
|
+
}
|
|
21370
|
+
return new HttpPublishClient({ origin, token: found.token });
|
|
21371
|
+
}
|
|
21372
|
+
async function runPull(opts) {
|
|
21373
|
+
const component = opts.component.trim();
|
|
21374
|
+
if (component === "") {
|
|
21375
|
+
fail(opts, ExitCode.InputValidation, {
|
|
21376
|
+
error: "name the component whose recording set you want",
|
|
21377
|
+
code: "pull-names-no-component",
|
|
21378
|
+
remediation: `Use the name your library shows, e.g. \`${tendrilCommand('pull "Radio group"')}\`.`
|
|
21379
|
+
});
|
|
21380
|
+
}
|
|
21381
|
+
const client = portalClient(opts);
|
|
21382
|
+
if (client.resolveRecordings === void 0 || client.downloadRecordings === void 0) {
|
|
21383
|
+
fail(opts, ExitCode.General, {
|
|
21384
|
+
error: "this client cannot pull recordings",
|
|
21385
|
+
code: "pull-unsupported-client",
|
|
21386
|
+
remediation: "Update the CLI (npx resolves @latest on its own; a pinned install wants a newer @tendrilapp/cli)."
|
|
21387
|
+
});
|
|
21388
|
+
}
|
|
21389
|
+
emitProgress(0, 3, "asking the portal which publication holds these recordings");
|
|
21390
|
+
const resolved = await client.resolveRecordings({ component, ...opts.figmaFile === void 0 ? {} : { figmaFile: opts.figmaFile } });
|
|
21391
|
+
if (!resolved.ok) {
|
|
21392
|
+
const authFailed = resolved.status === 401 || resolved.status === 403;
|
|
21393
|
+
if (resolved.status === 404 && resolved.candidates === void 0) {
|
|
21394
|
+
fail(opts, ExitCode.General, {
|
|
21395
|
+
error: "this portal does not carry recording sets yet",
|
|
21396
|
+
code: "pull-unsupported-portal",
|
|
21397
|
+
remediation: "Nothing is wrong with what you asked for. The recordings have to come from the machine that recorded them until this portal is updated \u2014 copy the recording-set directory across, or re-publish from that machine once it is."
|
|
21398
|
+
});
|
|
21399
|
+
}
|
|
21400
|
+
fail(opts, authFailed ? ExitCode.Auth : ExitCode.InputValidation, {
|
|
21401
|
+
error: resolved.refusal,
|
|
21402
|
+
code: authFailed ? "pull-not-signed-in" : "pull-not-resolved",
|
|
21403
|
+
remediation: authFailed ? "This session is no longer valid for that portal. Agents: run tendril_login \u2014 the user signs in with ONE browser Approve, then re-run this. (Verifying a bundle you already have needs no account, ever.)" : resolved.candidates === void 0 || resolved.candidates.length === 0 ? `Publish the component from the machine that holds its recordings first \u2014 a publish carries them by default.` : `Pull one of these instead: ${resolved.candidates.map((c) => `${quoteArg(c.componentName)}${c.figmaFile === "unidentified" ? "" : ` (--figma-file ${c.figmaFile})`}${c.carried ? "" : " \u2014 carries no recording set"}`).join("; ")}`
|
|
21404
|
+
});
|
|
21405
|
+
}
|
|
21406
|
+
const answer = resolved.value;
|
|
21407
|
+
emitProgress(1, 3, `downloading ${String(answer.sizeBytes)} bytes of recorded design`);
|
|
21408
|
+
const downloaded = await client.downloadRecordings({ publicationId: answer.publicationId });
|
|
21409
|
+
if (!downloaded.ok) {
|
|
21410
|
+
fail(opts, downloaded.status === 401 || downloaded.status === 403 ? ExitCode.Auth : ExitCode.General, {
|
|
21411
|
+
error: downloaded.refusal,
|
|
21412
|
+
code: "pull-download-refused",
|
|
21413
|
+
remediation: "Try again; if the portal keeps refusing, re-publish the component from the machine that holds its recordings."
|
|
21414
|
+
});
|
|
21415
|
+
}
|
|
21416
|
+
const bytes = downloaded.value;
|
|
21417
|
+
if (bytes.length > MAX_ARCHIVE_BYTES) {
|
|
21418
|
+
fail(opts, ExitCode.General, {
|
|
21419
|
+
error: `the portal returned ${String(bytes.length)} bytes for a recording set, and no archive may exceed ${String(MAX_ARCHIVE_BYTES)}`,
|
|
21420
|
+
code: "pull-archive-too-large",
|
|
21421
|
+
remediation: "Nothing was written. Check that TENDRIL_PORTAL_URL points at your portal and not at something in between."
|
|
21422
|
+
});
|
|
21423
|
+
}
|
|
21424
|
+
const actual = sha256([bytes]);
|
|
21425
|
+
if (actual !== answer.contentSha256) {
|
|
21426
|
+
fail(opts, ExitCode.General, {
|
|
21427
|
+
error: `the downloaded recording set is not the one the portal named (got ${actual.slice(0, 12)}\u2026, expected ${answer.contentSha256.slice(0, 12)}\u2026)`,
|
|
21428
|
+
code: "pull-bytes-do-not-match",
|
|
21429
|
+
remediation: "Run this again. If it repeats, something between you and the portal is changing the response."
|
|
21430
|
+
});
|
|
21431
|
+
}
|
|
21432
|
+
let text;
|
|
21433
|
+
try {
|
|
21434
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
21435
|
+
} catch {
|
|
21436
|
+
fail(opts, ExitCode.General, {
|
|
21437
|
+
error: "the downloaded recording set is not UTF-8 text",
|
|
21438
|
+
code: "pull-archive-unreadable",
|
|
21439
|
+
remediation: "Run this again; if it repeats, re-publish the component."
|
|
21440
|
+
});
|
|
21441
|
+
return;
|
|
21442
|
+
}
|
|
21443
|
+
const validated = validateRecordingArchive(text, { sha256, expectedSetHash: answer.setHash });
|
|
21444
|
+
if (!validated.ok) {
|
|
21445
|
+
fail(opts, ExitCode.General, {
|
|
21446
|
+
error: validated.refusal,
|
|
21447
|
+
code: "pull-archive-refused",
|
|
21448
|
+
remediation: "Nothing was written. Re-publish the component from the machine that holds its recordings, then pull again."
|
|
21449
|
+
});
|
|
21450
|
+
return;
|
|
21451
|
+
}
|
|
21452
|
+
const carried = validated.shape;
|
|
21453
|
+
const carriedKit = carried.figmaFile?.trim();
|
|
21454
|
+
if (carriedKit !== void 0 && carriedKit !== "" && answer.figmaFile !== "unidentified" && carriedKit !== answer.figmaFile.trim()) {
|
|
21455
|
+
fail(opts, ExitCode.General, {
|
|
21456
|
+
error: `the portal says this recording set belongs to design system ${answer.figmaFile}, and the set itself says ${carried.figmaFile}`,
|
|
21457
|
+
code: "pull-identity-disagrees",
|
|
21458
|
+
remediation: "Nothing was written. Re-publish the component from the machine that holds its recordings; if this repeats, the portal is answering about a different publication than the one it served."
|
|
21459
|
+
});
|
|
21460
|
+
return;
|
|
21461
|
+
}
|
|
21462
|
+
const slug = recordingSlug(component);
|
|
21463
|
+
const dest = path60.resolve(process.cwd(), opts.dest ?? path60.join("recordings", slug));
|
|
21464
|
+
if (existsSync50(dest)) {
|
|
21465
|
+
const existing = existingSetHash(dest);
|
|
21466
|
+
if (existing === validated.setHash) {
|
|
21467
|
+
emitData(
|
|
21468
|
+
opts,
|
|
21469
|
+
{
|
|
21470
|
+
component: carried.component,
|
|
21471
|
+
...answer.componentName === carried.component ? {} : { libraryLabel: answer.componentName },
|
|
21472
|
+
figmaFile: carried.figmaFile ?? "unidentified",
|
|
21473
|
+
setHash: validated.setHash,
|
|
21474
|
+
dir: dest,
|
|
21475
|
+
files: validated.members.size,
|
|
21476
|
+
alreadyHere: true
|
|
21477
|
+
},
|
|
21478
|
+
() => {
|
|
21479
|
+
process.stdout.write(`already here \u2014 ${dest}
|
|
21480
|
+
`);
|
|
21481
|
+
process.stdout.write(` the same recording set (${validated.setHash.slice(0, 12)}\u2026), nothing to do
|
|
21482
|
+
`);
|
|
21483
|
+
}
|
|
21484
|
+
);
|
|
21485
|
+
return;
|
|
21486
|
+
}
|
|
21487
|
+
fail(opts, ExitCode.InputValidation, {
|
|
21488
|
+
error: existing === void 0 ? `${dest} already exists and is not a recording set \u2014 pulling would write into it` : `${dest} already holds a DIFFERENT recording set (${existing.slice(0, 12)}\u2026 here, ${validated.setHash.slice(0, 12)}\u2026 on the portal)`,
|
|
21489
|
+
code: "pull-destination-occupied",
|
|
21490
|
+
remediation: `Move it aside and pull again, or pull somewhere else with \`--dest <dir>\`. Nothing is overwritten \u2014 a recording set is the ground truth a verdict was measured against, and replacing one silently is how a verdict comes to describe bytes nobody has.`
|
|
21491
|
+
});
|
|
21492
|
+
}
|
|
21493
|
+
emitProgress(2, 3, "unpacking and re-deriving the set hash from disk");
|
|
21494
|
+
const staging = mkdtempSync4(path60.join(tmpdir(), "tendril-pull-"));
|
|
21495
|
+
let refusal;
|
|
21496
|
+
try {
|
|
21497
|
+
for (const [relPath, memberBytes] of validated.members) {
|
|
21498
|
+
const target = path60.join(staging, relPath);
|
|
21499
|
+
mkdirSync17(path60.dirname(target), { recursive: true });
|
|
21500
|
+
writeFileSync27(target, memberBytes);
|
|
21501
|
+
}
|
|
21502
|
+
const fromDisk = setHashFromDisk(staging);
|
|
21503
|
+
if (!fromDisk.ok) {
|
|
21504
|
+
refusal = {
|
|
21505
|
+
exit: ExitCode.General,
|
|
21506
|
+
error: `the recording set did not survive being written to this filesystem \u2014 ${fromDisk.refusal}`,
|
|
21507
|
+
code: "pull-set-changed-on-disk",
|
|
21508
|
+
remediation: "Nothing was written where you asked, and nothing was left behind. This means the filesystem altered the files on the way down \u2014 pull onto a different volume with `--dest <dir>`."
|
|
21509
|
+
};
|
|
21510
|
+
} else if (fromDisk.setHash !== validated.setHash) {
|
|
21511
|
+
refusal = {
|
|
21512
|
+
exit: ExitCode.General,
|
|
21513
|
+
error: `the recording set does not survive being written to this filesystem (on disk it derives ${fromDisk.setHash.slice(0, 12)}\u2026, the portal's is ${validated.setHash.slice(0, 12)}\u2026)`,
|
|
21514
|
+
code: "pull-set-changed-on-disk",
|
|
21515
|
+
remediation: "Nothing was written where you asked, and nothing was left behind. This usually means the filesystem renamed a file \u2014 pull onto a different volume with `--dest <dir>`."
|
|
21516
|
+
};
|
|
21517
|
+
} else {
|
|
21518
|
+
mkdirSync17(path60.dirname(dest), { recursive: true });
|
|
21519
|
+
try {
|
|
21520
|
+
renameSync2(staging, dest);
|
|
21521
|
+
} catch (error) {
|
|
21522
|
+
const code = error.code;
|
|
21523
|
+
if (code === "EEXIST" || code === "ENOTEMPTY") {
|
|
21524
|
+
refusal = {
|
|
21525
|
+
exit: ExitCode.InputValidation,
|
|
21526
|
+
error: `${dest} appeared while this was verifying \u2014 on a case-insensitive filesystem it may be a directory whose name differs only in capitals`,
|
|
21527
|
+
code: "pull-destination-collided",
|
|
21528
|
+
remediation: `Pull somewhere else with \`--dest <dir>\`, or move the existing directory aside.`
|
|
21529
|
+
};
|
|
21530
|
+
} else if (code === "EXDEV") {
|
|
21531
|
+
const beside = `${dest}.tendril-partial`;
|
|
21532
|
+
rmSync12(beside, { recursive: true, force: true });
|
|
21533
|
+
try {
|
|
21534
|
+
copyTree(staging, beside);
|
|
21535
|
+
renameSync2(beside, dest);
|
|
21536
|
+
} finally {
|
|
21537
|
+
rmSync12(beside, { recursive: true, force: true });
|
|
21538
|
+
}
|
|
21539
|
+
} else {
|
|
21540
|
+
throw error;
|
|
21541
|
+
}
|
|
21542
|
+
}
|
|
21543
|
+
}
|
|
21544
|
+
} finally {
|
|
21545
|
+
rmSync12(staging, { recursive: true, force: true });
|
|
21546
|
+
}
|
|
21547
|
+
if (refusal !== void 0) {
|
|
21548
|
+
fail(opts, refusal.exit, { error: refusal.error, code: refusal.code, remediation: refusal.remediation });
|
|
21549
|
+
}
|
|
21550
|
+
emitData(
|
|
21551
|
+
opts,
|
|
21552
|
+
{
|
|
21553
|
+
// THE CARRIED MANIFEST'S OWN NAME, not the portal's label — the
|
|
21554
|
+
// pinned identity rather than the steerable one. `libraryLabel`
|
|
21555
|
+
// carries the portal's word for it, named as what it is, and only
|
|
21556
|
+
// when the two differ (`publish --name` makes that legitimate).
|
|
21557
|
+
component: carried.component,
|
|
21558
|
+
...answer.componentName === carried.component ? {} : { libraryLabel: answer.componentName },
|
|
21559
|
+
figmaFile: carried.figmaFile ?? "unidentified",
|
|
21560
|
+
publicationId: answer.publicationId,
|
|
21561
|
+
setHash: validated.setHash,
|
|
21562
|
+
dir: dest,
|
|
21563
|
+
files: validated.members.size,
|
|
21564
|
+
alreadyHere: false
|
|
21565
|
+
},
|
|
21566
|
+
() => {
|
|
21567
|
+
process.stdout.write(`pulled ${carried.component} \u2014 ${String(validated.members.size)} recorded files
|
|
21568
|
+
`);
|
|
21569
|
+
if (answer.componentName !== carried.component) {
|
|
21570
|
+
process.stdout.write(` your library calls it ${quoteArg(answer.componentName)}; the recording set names itself ${quoteArg(carried.component)}
|
|
21571
|
+
`);
|
|
21572
|
+
}
|
|
21573
|
+
process.stdout.write(` ${dest}
|
|
21574
|
+
`);
|
|
21575
|
+
process.stdout.write(` set ${validated.setHash.slice(0, 12)}\u2026 \u2014 the recordings this component's verdict was measured against
|
|
21576
|
+
`);
|
|
21577
|
+
if (recordingSlug(component) !== component && opts.dest === void 0) {
|
|
21578
|
+
process.stdout.write(` (${quoteArg(component)} lands in the directory ${slug})
|
|
21579
|
+
`);
|
|
21580
|
+
}
|
|
21581
|
+
process.stdout.write(` next: \`${tendrilCommand(`record status --set ${quoteArg(path60.relative(process.cwd(), dest) || dest)}`)}\`
|
|
21582
|
+
`);
|
|
21583
|
+
}
|
|
21584
|
+
);
|
|
21585
|
+
}
|
|
21586
|
+
function existingSetHash(dir) {
|
|
21587
|
+
const derived = setHashFromDisk(dir);
|
|
21588
|
+
return derived.ok ? derived.setHash : void 0;
|
|
21589
|
+
}
|
|
21590
|
+
function copyTree(from, to) {
|
|
21591
|
+
mkdirSync17(to, { recursive: true });
|
|
21592
|
+
for (const entry of readdirSync19(from, { withFileTypes: true })) {
|
|
21593
|
+
const src = path60.join(from, entry.name);
|
|
21594
|
+
const dst = path60.join(to, entry.name);
|
|
21595
|
+
if (entry.isDirectory()) copyTree(src, dst);
|
|
21596
|
+
else writeFileSync27(dst, readFileSync46(src));
|
|
21597
|
+
}
|
|
21598
|
+
}
|
|
21599
|
+
var MAX_ARCHIVE_BYTES, sha256;
|
|
21600
|
+
var init_pull = __esm({
|
|
21601
|
+
"packages/cli/src/commands/pull.ts"() {
|
|
21602
|
+
"use strict";
|
|
21603
|
+
init_src3();
|
|
21604
|
+
init_src4();
|
|
21605
|
+
init_invocation();
|
|
21606
|
+
init_output();
|
|
21607
|
+
init_publish_client();
|
|
21608
|
+
init_publish();
|
|
21609
|
+
MAX_ARCHIVE_BYTES = 2e7;
|
|
21610
|
+
sha256 = (chunks) => {
|
|
21611
|
+
const h = createHash15("sha256");
|
|
21612
|
+
for (const chunk of chunks) h.update(chunk);
|
|
21613
|
+
return h.digest("hex");
|
|
21614
|
+
};
|
|
21615
|
+
}
|
|
21616
|
+
});
|
|
21617
|
+
|
|
20826
21618
|
// packages/cli/src/commands/generate-route.ts
|
|
20827
21619
|
var generate_route_exports = {};
|
|
20828
21620
|
__export(generate_route_exports, {
|
|
@@ -20847,17 +21639,17 @@ __export(generate_recorded_exports, {
|
|
|
20847
21639
|
runGenerateRecorded: () => runGenerateRecorded
|
|
20848
21640
|
});
|
|
20849
21641
|
import { confirm as confirm3, isCancel as isCancel4 } from "@clack/prompts";
|
|
20850
|
-
import { existsSync as
|
|
20851
|
-
import
|
|
21642
|
+
import { existsSync as existsSync51, readFileSync as readFileSync47 } from "node:fs";
|
|
21643
|
+
import path61 from "node:path";
|
|
20852
21644
|
async function runGenerateRecorded(opts) {
|
|
20853
21645
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
20854
|
-
const outDirAbs =
|
|
20855
|
-
const recordedAsPath =
|
|
21646
|
+
const outDirAbs = path61.resolve(callerCwd, opts.out);
|
|
21647
|
+
const recordedAsPath = path61.resolve(callerCwd, opts.recorded);
|
|
20856
21648
|
let task;
|
|
20857
21649
|
let taskName;
|
|
20858
21650
|
let authoredApi;
|
|
20859
21651
|
let composition;
|
|
20860
|
-
const isSet =
|
|
21652
|
+
const isSet = existsSync51(path61.join(recordedAsPath, "recording-set.json"));
|
|
20861
21653
|
const registry = TASKS[opts.recorded];
|
|
20862
21654
|
if (registry !== void 0 && !isSet) {
|
|
20863
21655
|
task = registry;
|
|
@@ -20866,7 +21658,7 @@ async function runGenerateRecorded(opts) {
|
|
|
20866
21658
|
try {
|
|
20867
21659
|
const authored = authorTaskFromSet(recordedAsPath);
|
|
20868
21660
|
task = authored.task;
|
|
20869
|
-
taskName =
|
|
21661
|
+
taskName = path61.basename(recordedAsPath);
|
|
20870
21662
|
authoredApi = authored.api;
|
|
20871
21663
|
const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
|
|
20872
21664
|
if (roles.success) composition = roles.data;
|
|
@@ -20900,7 +21692,7 @@ async function runGenerateRecorded(opts) {
|
|
|
20900
21692
|
warn(opts, `font weights (advisory): the recording implies weights ${g.declared.join(", ")} for "${g.family}" \u2014 cache provides ${g.provided.join(", ")}; nearest-weight rendering may shift stroke density (certification stays family-gated; implied weights can leak across families)`);
|
|
20901
21693
|
}
|
|
20902
21694
|
const missing = task.configs.filter(
|
|
20903
|
-
(c) => !repEnvelopeExists(task.set, c.rep, "screenshot") || !repEnvelopeExists(task.set, c.rep, "metadata") || !
|
|
21695
|
+
(c) => !repEnvelopeExists(task.set, c.rep, "screenshot") || !repEnvelopeExists(task.set, c.rep, "metadata") || !existsSync51(path61.join(task.set, c.rep, "get_design_context.json"))
|
|
20904
21696
|
);
|
|
20905
21697
|
if (missing.length > 0) {
|
|
20906
21698
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -20970,8 +21762,8 @@ async function runGenerateRecorded(opts) {
|
|
|
20970
21762
|
` : `${line}
|
|
20971
21763
|
`);
|
|
20972
21764
|
if (opts.dryRun) {
|
|
20973
|
-
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite:
|
|
20974
|
-
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${
|
|
21765
|
+
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path61.join(outDirAbs, taskName) }, () => {
|
|
21766
|
+
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path61.join(outDirAbs, taskName)})
|
|
20975
21767
|
`);
|
|
20976
21768
|
});
|
|
20977
21769
|
return;
|
|
@@ -20994,10 +21786,10 @@ async function runGenerateRecorded(opts) {
|
|
|
20994
21786
|
});
|
|
20995
21787
|
}
|
|
20996
21788
|
}
|
|
20997
|
-
const bundleDir =
|
|
20998
|
-
if (
|
|
21789
|
+
const bundleDir = path61.join(outDirAbs, taskName);
|
|
21790
|
+
if (existsSync51(path61.join(bundleDir, "component.json"))) {
|
|
20999
21791
|
try {
|
|
21000
|
-
const prior = readBundleManifest(
|
|
21792
|
+
const prior = readBundleManifest(readFileSync47(path61.join(bundleDir, "component.json"), "utf8")).manifest;
|
|
21001
21793
|
if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
|
|
21002
21794
|
fail(opts, ExitCode.InputValidation, {
|
|
21003
21795
|
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`,
|
|
@@ -22329,9 +23121,10 @@ function buildProgram() {
|
|
|
22329
23121
|
...local["revoke"] !== void 0 ? { revoke: local["revoke"] } : {}
|
|
22330
23122
|
});
|
|
22331
23123
|
});
|
|
22332
|
-
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").action(async (bundleDir, _o, cmd) => {
|
|
23124
|
+
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) => {
|
|
22333
23125
|
const flags = globalFlags(cmd.parent);
|
|
22334
23126
|
const local = cmd.opts();
|
|
23127
|
+
const argv = cmd.parent?.args ?? process.argv;
|
|
22335
23128
|
const { runPublish: runPublish2 } = await Promise.resolve().then(() => (init_publish(), publish_exports));
|
|
22336
23129
|
await runPublish2({
|
|
22337
23130
|
...flags,
|
|
@@ -22342,7 +23135,33 @@ function buildProgram() {
|
|
|
22342
23135
|
...local["approveWait"] !== void 0 ? { approveWait: local["approveWait"] } : {},
|
|
22343
23136
|
// Parsed, never filtered: a bad value must refuse loudly in
|
|
22344
23137
|
// runPublish, not silently become the unbounded wait.
|
|
22345
|
-
...local["waitWindow"] !== void 0 ? { waitWindowSeconds: Number.parseFloat(local["waitWindow"]) } : {}
|
|
23138
|
+
...local["waitWindow"] !== void 0 ? { waitWindowSeconds: Number.parseFloat(local["waitWindow"]) } : {},
|
|
23139
|
+
// OPT-OUT WINS, whatever the order. Commander merges these into
|
|
23140
|
+
// one option with last-flag-wins, so `--no-recordings
|
|
23141
|
+
// --recordings X` CARRIED — a user who wrote the opt-out and
|
|
23142
|
+
// then a directory got the custody expansion anyway. For a flag
|
|
23143
|
+
// whose whole job is declining to upload the customer's design
|
|
23144
|
+
// structure, argument order is not a thing to be clever about.
|
|
23145
|
+
...argv.includes("--no-recordings") ? { noRecordings: true } : {},
|
|
23146
|
+
// `--recordings <dir>` and `--no-recordings` are ONE commander
|
|
23147
|
+
// option, so the value is a string, `false`, or the default
|
|
23148
|
+
// `true`. Splitting it here keeps the two meanings apart in the
|
|
23149
|
+
// options object rather than making the command re-read
|
|
23150
|
+
// commander's convention.
|
|
23151
|
+
...typeof local["recordings"] === "string" ? { recordings: local["recordings"] } : {},
|
|
23152
|
+
...local["recordings"] === false ? { noRecordings: true } : {}
|
|
23153
|
+
});
|
|
23154
|
+
});
|
|
23155
|
+
program.command("pull").description("Bring a published component's RECORDING SET onto this machine \u2014 the design truth its verdict was measured against, so a machine with only portal access can carry the work on.").argument("<component>", "the component name, exactly as your library shows it").option("--figma-file <key>", "the design system, when one name spans several of yours").option("--dest <dir>", "where the set lands (default: ./recordings/<component>)").option("--to <url>", "the portal to pull from (or set TENDRIL_PORTAL_URL)").action(async (component, _o, cmd) => {
|
|
23156
|
+
const flags = globalFlags(cmd.parent);
|
|
23157
|
+
const local = cmd.opts();
|
|
23158
|
+
const { runPull: runPull2 } = await Promise.resolve().then(() => (init_pull(), pull_exports));
|
|
23159
|
+
await runPull2({
|
|
23160
|
+
...flags,
|
|
23161
|
+
component,
|
|
23162
|
+
...local["figmaFile"] !== void 0 ? { figmaFile: local["figmaFile"] } : {},
|
|
23163
|
+
...local["dest"] !== void 0 ? { dest: local["dest"] } : {},
|
|
23164
|
+
...local["to"] !== void 0 ? { to: local["to"] } : {}
|
|
22346
23165
|
});
|
|
22347
23166
|
});
|
|
22348
23167
|
program.command("verify").description("Recompute verification for a generated bundle against its recording set (per-config status, behaviors, honest exit codes).").argument("<bundleDir>", "bundle directory to verify").option("--task <name>", "legacy task adapter for bundles WITHOUT component.json (bundle v1 carries its own prop manifest)").option("--set <dir>", "recording set directory (overrides the bundle's provenance path)").option("--bar <bar>", "target bar: pass (0.95) or cert (0.97)", "pass").option("--library <dir>", "workspace root holding confirmed partners generated bundles (ADR-013 composed pins; default: current directory)").option("--profile <file>", "a `tendril profile` artifact; reports whether the bundle followed your codebase conventions (never gates the verdict)").option("--hover-timeout <ms>", "hover actionability budget in ms (default 2000) \u2014 for diagnosing a slow machine; a non-default value is recorded in the report as a verdict caveat, never silently").action(async (bundleDir, _opts, cmd) => {
|