@bridge-ds/mcp 0.4.1 → 0.6.0
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/branches.d.ts +9 -1
- package/dist/{chunk-QTO4EP6V.js → chunk-E4D5KPIQ.js} +25 -6
- package/dist/{chunk-MXMMPO7F.js → chunk-M7DUTFIQ.js} +2074 -124
- package/dist/explorations.d.ts +6 -1
- package/dist/figma.d.ts +83 -0
- package/dist/index.js +2 -2
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +6 -2
- package/dist/ops.d.ts +3 -0
- package/dist/pieces.d.ts +122 -0
- package/dist/{src-DHJOM5PQ.js → src-F2PTXW5B.js} +1 -1
- package/package.json +3 -3
|
@@ -71,8 +71,9 @@ import {
|
|
|
71
71
|
unreadableShipTeams,
|
|
72
72
|
unreadableTeams,
|
|
73
73
|
validateLayered,
|
|
74
|
+
variantKeyOf,
|
|
74
75
|
whoCanShipSentence
|
|
75
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-E4D5KPIQ.js";
|
|
76
77
|
|
|
77
78
|
// src/lib.ts
|
|
78
79
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -1571,10 +1572,16 @@ var GitHubClient = class _GitHubClient {
|
|
|
1571
1572
|
async addComment(issueNumber, body) {
|
|
1572
1573
|
await this.request("POST", `${this.repoPath}/issues/${issueNumber}/comments`, { body });
|
|
1573
1574
|
}
|
|
1574
|
-
/**
|
|
1575
|
+
/**
|
|
1576
|
+
* Comments on an issue or pull request, oldest first (first 100: the sticky preview comment is posted early).
|
|
1577
|
+
* `authorAssociation` is GitHub's OWNER, MEMBER, COLLABORATOR, CONTRIBUTOR, NONE... (suggestion approvals read it).
|
|
1578
|
+
*/
|
|
1575
1579
|
async listIssueComments(issueNumber) {
|
|
1576
|
-
const r = await this.request(
|
|
1577
|
-
|
|
1580
|
+
const r = await this.request(
|
|
1581
|
+
"GET",
|
|
1582
|
+
`${this.repoPath}/issues/${issueNumber}/comments?per_page=100`
|
|
1583
|
+
);
|
|
1584
|
+
return r.map((c) => ({ id: c.id, body: c.body ?? "", author: c.user?.login ?? "unknown", updatedAt: c.updated_at, createdAt: c.created_at ?? c.updated_at, authorAssociation: c.author_association ?? "NONE" }));
|
|
1578
1585
|
}
|
|
1579
1586
|
// --- Issues (component change requests) ---------------------------------------------------
|
|
1580
1587
|
async createIssue(input) {
|
|
@@ -1921,6 +1928,17 @@ var GitHubClient = class _GitHubClient {
|
|
|
1921
1928
|
this.assertToken("Updating a branch from main");
|
|
1922
1929
|
await this.request("PUT", `${this.repoPath}/pulls/${n}/update-branch`, { expected_head_sha: expectedHeadSha });
|
|
1923
1930
|
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Merges the branch `head` into the branch `base` (GitHub's merges API): a merge commit on `base`, or nothing when
|
|
1933
|
+
* `base` already has every commit of `head`. A conflict throws a GitHubError with status 409. Refuses protected
|
|
1934
|
+
* branches and, with a branch prefix, a `base` without it.
|
|
1935
|
+
*/
|
|
1936
|
+
async mergeIntoBranch(base, head, message) {
|
|
1937
|
+
this.assertToken("Merging a branch into another");
|
|
1938
|
+
this.assertWritableBranch(base);
|
|
1939
|
+
const r = await this.request("POST", `${this.repoPath}/merges`, { base, head, commit_message: message });
|
|
1940
|
+
return r?.sha ? { status: "merged", sha: r.sha } : { status: "up-to-date", sha: null };
|
|
1941
|
+
}
|
|
1924
1942
|
/** Posts a comment on an issue or pull request and returns its id and link. */
|
|
1925
1943
|
async createIssueComment(n, body) {
|
|
1926
1944
|
this.assertToken("Commenting");
|
|
@@ -2084,8 +2102,8 @@ function firstParagraph(text) {
|
|
|
2084
2102
|
if (!text) return "";
|
|
2085
2103
|
const cleaned = text.replace(/<!--[\s\S]*?-->/g, "");
|
|
2086
2104
|
for (const para of cleaned.split(/\r?\n\s*\r?\n/)) {
|
|
2087
|
-
const
|
|
2088
|
-
const s =
|
|
2105
|
+
const lines2 = para.split(/\r?\n/).filter((l) => l.trim() && !/^\s*#{1,6}\s/.test(l) && !/^\s*[-*]\s*\[[ xX]\]/.test(l) && !/^\s*(Co-Authored-By|Claude-Session|Signed-off-by):/i.test(l));
|
|
2106
|
+
const s = lines2.join(" ").replace(/\s+/g, " ").trim();
|
|
2089
2107
|
if (s && !/^\|/.test(s)) return s.length > 400 ? `${s.slice(0, 397)}...` : s;
|
|
2090
2108
|
}
|
|
2091
2109
|
return "";
|
|
@@ -2251,11 +2269,11 @@ function modeLabel(modes) {
|
|
|
2251
2269
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
2252
2270
|
}
|
|
2253
2271
|
function tokenHistory(entries, path, config) {
|
|
2254
|
-
const
|
|
2272
|
+
const names3 = /* @__PURE__ */ new Set([path]);
|
|
2255
2273
|
const out = [];
|
|
2256
2274
|
const sorted = [...entries].sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
|
|
2257
2275
|
for (const e of sorted) {
|
|
2258
|
-
const hits = e.changes.filter((c) =>
|
|
2276
|
+
const hits = e.changes.filter((c) => names3.has(c.path));
|
|
2259
2277
|
for (const c of hits) {
|
|
2260
2278
|
const modes = config ? fileModes(config, c.file) : {};
|
|
2261
2279
|
out.push({
|
|
@@ -2283,7 +2301,7 @@ function tokenHistory(entries, path, config) {
|
|
|
2283
2301
|
...c.descriptionAfter !== void 0 ? { descriptionAfter: c.descriptionAfter } : {}
|
|
2284
2302
|
});
|
|
2285
2303
|
}
|
|
2286
|
-
for (const c of hits) if (c.kind === "renamed" && c.from)
|
|
2304
|
+
for (const c of hits) if (c.kind === "renamed" && c.from) names3.add(c.from);
|
|
2287
2305
|
}
|
|
2288
2306
|
return out;
|
|
2289
2307
|
}
|
|
@@ -2413,7 +2431,7 @@ function planRevert(input) {
|
|
|
2413
2431
|
}
|
|
2414
2432
|
const prLink = entry.pr ? entry.pr.url ? `[#${entry.pr.number}](${entry.pr.url})` : `#${entry.pr.number}` : `commit ${entry.sha.slice(0, 7)}`;
|
|
2415
2433
|
const title = `Revert: ${entry.title}`;
|
|
2416
|
-
const
|
|
2434
|
+
const lines2 = [
|
|
2417
2435
|
`Reverts ${prLink} ("${entry.title}"), merged ${entry.date.slice(0, 10)} by ${entry.author}.`,
|
|
2418
2436
|
"",
|
|
2419
2437
|
`Original reason: ${entry.why}`,
|
|
@@ -2421,11 +2439,11 @@ function planRevert(input) {
|
|
|
2421
2439
|
"Bridge computed the inverse token edits against the current base branch, not a blind git revert: tokens that changed again since are listed as conflicts and kept unless a reviewer chose to restore them."
|
|
2422
2440
|
];
|
|
2423
2441
|
if (conflicts.length) {
|
|
2424
|
-
|
|
2425
|
-
for (const k of conflicts)
|
|
2442
|
+
lines2.push("", "### Conflicts", "", "| Token | File | Why | Decision |", "| --- | --- | --- | --- |");
|
|
2443
|
+
for (const k of conflicts) lines2.push(`| \`${k.path}\` | ${k.file} | ${k.message.replace(/\|/g, "\\|")}${k.later.length ? ` Later: ${k.later.map((l) => l.pr ? `#${l.pr}` : l.id).join(", ")}.` : ""} | ${k.resolution === "use-before" ? "restore the old value" : "keep current"} |`);
|
|
2426
2444
|
}
|
|
2427
|
-
if (skipped.length)
|
|
2428
|
-
return finishPlan({ ...input, kind: "revert", title, prTitle: title, summary:
|
|
2445
|
+
if (skipped.length) lines2.push("", `${skipped.length} change(s) were already undone on the base branch and are left alone.`);
|
|
2446
|
+
return finishPlan({ ...input, kind: "revert", title, prTitle: title, summary: lines2.join("\n"), planned, conflicts, skipped, source: entry });
|
|
2429
2447
|
}
|
|
2430
2448
|
function planRestore(input) {
|
|
2431
2449
|
const { current, path } = input;
|
|
@@ -2730,7 +2748,7 @@ function toOklch(rgb) {
|
|
|
2730
2748
|
return { L, C, h };
|
|
2731
2749
|
}
|
|
2732
2750
|
var isHex = (s) => typeof s === "string" && /^#[0-9a-f]{6}$/i.test(s.trim());
|
|
2733
|
-
var fromHex = (
|
|
2751
|
+
var fromHex = (hex2) => [1, 3, 5].map((i) => parseInt(hex2.slice(i, i + 2), 16) / 255);
|
|
2734
2752
|
var round3 = (x) => Math.round(x * 1e3) / 1e3;
|
|
2735
2753
|
var toHex = (rgb) => "#" + rgb.map((x) => Math.round(x * 255).toString(16).padStart(2, "0")).join("");
|
|
2736
2754
|
function dtcgColor(rgb, alpha) {
|
|
@@ -2752,20 +2770,20 @@ function nearestStep(rgb) {
|
|
|
2752
2770
|
for (const s of STEPS) if (Math.abs(LIGHTNESS[s] - L) < Math.abs(LIGHTNESS[best] - L)) best = s;
|
|
2753
2771
|
return best;
|
|
2754
2772
|
}
|
|
2755
|
-
function anchoredRamp(
|
|
2756
|
-
const rgb = fromHex(
|
|
2773
|
+
function anchoredRamp(hex2, pinStep) {
|
|
2774
|
+
const rgb = fromHex(hex2);
|
|
2757
2775
|
const { L, C, h } = toOklch(rgb);
|
|
2758
2776
|
const step = pinStep ?? nearestStep(rgb);
|
|
2759
2777
|
const peak = Math.min(0.24, Math.max(0.02, C / BELL[step]));
|
|
2760
|
-
return { hue: h, peak, step, distance: Math.abs(L - LIGHTNESS[step]), ramp: generateRamp({ hue: h, chroma: bellChroma(peak), pins: { [step]:
|
|
2778
|
+
return { hue: h, peak, step, distance: Math.abs(L - LIGHTNESS[step]), ramp: generateRamp({ hue: h, chroma: bellChroma(peak), pins: { [step]: hex2.toUpperCase() } }) };
|
|
2761
2779
|
}
|
|
2762
2780
|
function neutralOffset() {
|
|
2763
2781
|
const off = {};
|
|
2764
|
-
for (const [s,
|
|
2782
|
+
for (const [s, hex2] of Object.entries(NEUTRAL_ANCHORS)) off[s] = toOklch(fromHex(hex2)).L - LIGHTNESS[s];
|
|
2765
2783
|
return (s) => off[s] ?? 0;
|
|
2766
2784
|
}
|
|
2767
|
-
function tintedNeutral(
|
|
2768
|
-
const rgb = fromHex(
|
|
2785
|
+
function tintedNeutral(hex2) {
|
|
2786
|
+
const rgb = fromHex(hex2);
|
|
2769
2787
|
const { C, h } = toOklch(rgb);
|
|
2770
2788
|
const ref = neutralChroma()(nearestStep(rgb));
|
|
2771
2789
|
const scale = C < 4e-3 ? 0 : Math.min(2.5, C / ref);
|
|
@@ -2839,19 +2857,19 @@ function rampFor(input, pinStep, kind) {
|
|
|
2839
2857
|
const ramp = kind === "neutral" ? generateRamp({ hue: hue2, chroma: neutralChroma(), offset: neutralOffset() }) : generateRamp({ hue: hue2, chroma: bellChroma(DEFAULT_PRIMARY_CHROMA) });
|
|
2840
2858
|
return { ramp, anchor: `OKLCH hue ${Math.round(hue2)}` };
|
|
2841
2859
|
}
|
|
2842
|
-
const
|
|
2843
|
-
if (!isHex(
|
|
2860
|
+
const hex2 = String(input).trim();
|
|
2861
|
+
if (!isHex(hex2)) throw new Error(`"${hex2}" is not a color. Use #rrggbb or a hue in degrees (0 to 360).`);
|
|
2844
2862
|
if (kind === "neutral") {
|
|
2845
|
-
const n = tintedNeutral(
|
|
2846
|
-
return { ramp: n.ramp, anchor: `${
|
|
2863
|
+
const n = tintedNeutral(hex2);
|
|
2864
|
+
return { ramp: n.ramp, anchor: `${hex2.toUpperCase()} (hue ${Math.round(n.hue)})` };
|
|
2847
2865
|
}
|
|
2848
|
-
const a = anchoredRamp(
|
|
2866
|
+
const a = anchoredRamp(hex2, pinStep);
|
|
2849
2867
|
const issue = a.distance > PIN_TOLERANCE ? {
|
|
2850
2868
|
level: "warning",
|
|
2851
2869
|
code: "pin-off-ladder",
|
|
2852
|
-
message: `${
|
|
2870
|
+
message: `${hex2.toUpperCase()} is ${a.distance > 0.12 ? "much " : ""}${toOklch(fromHex(hex2)).L > LIGHTNESS[a.step] ? "lighter" : "darker"} than the ${a.step} step it is pinned to, so the ramp around it will not read as one family. Primary actions use this step: pick a ${a.step >= 600 ? "deeper" : "lighter"} color.`
|
|
2853
2871
|
} : void 0;
|
|
2854
|
-
return { ramp: a.ramp, anchor: `${
|
|
2872
|
+
return { ramp: a.ramp, anchor: `${hex2.toUpperCase()} pinned at ${a.step}`, ...issue ? { issue } : {} };
|
|
2855
2873
|
}
|
|
2856
2874
|
function aliasRamp(doc, role, ramp, description) {
|
|
2857
2875
|
const group = getAt(doc, `palette.${role}`);
|
|
@@ -3390,7 +3408,23 @@ function patchDocsJson(original, next) {
|
|
|
3390
3408
|
function cleanDraftText(text) {
|
|
3391
3409
|
return text.replace(/\s*\u2014\s*/g, ", ").replace(/,\s*,/g, ",").replace(/^,\s*/, "").replace(/[ \t]+/g, " ").trim();
|
|
3392
3410
|
}
|
|
3411
|
+
var andList = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
|
|
3393
3412
|
var capital = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
3413
|
+
var VARIANT_FIRST = ["variant", "tone", "intent", "appearance", "kind", "type", "style"];
|
|
3414
|
+
function shortVariant(key) {
|
|
3415
|
+
const pairs = key.split(", ").map((p) => {
|
|
3416
|
+
const at = p.indexOf("=");
|
|
3417
|
+
return at < 0 ? { k: "", v: p.trim() } : { k: p.slice(0, at).trim(), v: p.slice(at + 1).trim() };
|
|
3418
|
+
});
|
|
3419
|
+
const kept = pairs.filter((p) => p.v && !/^default$/i.test(p.v));
|
|
3420
|
+
if (!kept.length) return key;
|
|
3421
|
+
const rank2 = (k) => {
|
|
3422
|
+
const i = VARIANT_FIRST.indexOf(k.toLowerCase());
|
|
3423
|
+
return i < 0 ? VARIANT_FIRST.length : i;
|
|
3424
|
+
};
|
|
3425
|
+
return kept.map((p, i) => ({ ...p, i })).sort((a, b) => rank2(a.k) - rank2(b.k) || a.i - b.i).map((p) => p.v).join(" ");
|
|
3426
|
+
}
|
|
3427
|
+
var shortVariants = (list) => list ? andList(list.split("; ").map(shortVariant)) : "every variant";
|
|
3394
3428
|
var DRAFT_LINE_MAX = 120;
|
|
3395
3429
|
function clipText(text, max) {
|
|
3396
3430
|
const t = text.replace(/\s+/g, " ").trim().replace(/\.$/, "");
|
|
@@ -3434,8 +3468,8 @@ function validateDocsTask(x) {
|
|
|
3434
3468
|
}
|
|
3435
3469
|
var docsTaskKeys = (t) => t.kind === "sections" ? t.keys : [t.key];
|
|
3436
3470
|
function parseDocsReply(text) {
|
|
3437
|
-
const
|
|
3438
|
-
const body =
|
|
3471
|
+
const fenced2 = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
|
|
3472
|
+
const body = fenced2 ? fenced2[1] : text;
|
|
3439
3473
|
const start = body.indexOf("{");
|
|
3440
3474
|
const end = body.lastIndexOf("}");
|
|
3441
3475
|
if (start < 0 || end <= start) throw new Error("The reply holds no JSON object.");
|
|
@@ -3598,7 +3632,7 @@ function explorationPrBody(e, ctx) {
|
|
|
3598
3632
|
const branch = `${EXPLORATION_PREFIX}${slug2}`;
|
|
3599
3633
|
const uc = useCaseLabel(e.useCase);
|
|
3600
3634
|
const experimental = (e.experimental ?? []).map((x) => x.trim()).filter(Boolean);
|
|
3601
|
-
const
|
|
3635
|
+
const lines2 = [
|
|
3602
3636
|
`## Exploration: ${e.name.trim()}`,
|
|
3603
3637
|
"",
|
|
3604
3638
|
`> Not mergeable as is. This draft pull request is a sandbox on \`${branch}\`: promote it to a normal review when the idea is validated, or close it.`,
|
|
@@ -3614,14 +3648,14 @@ function explorationPrBody(e, ctx) {
|
|
|
3614
3648
|
"### Try it in a product staging branch",
|
|
3615
3649
|
""
|
|
3616
3650
|
];
|
|
3617
|
-
|
|
3651
|
+
lines2.push(
|
|
3618
3652
|
'In the product repo, on a branch that deploys to staging (production keeps the released version), install the snapshot with the command in the "Try this exploration in your product" comment below. It appears a few minutes after each push and always matches the latest commit.'
|
|
3619
3653
|
);
|
|
3620
|
-
|
|
3654
|
+
lines2.push("", "### Previews", "");
|
|
3621
3655
|
const links = previewLinks(ctx.apps, { branch, ...ctx.pr ? { pr: ctx.pr } : {} });
|
|
3622
|
-
if (links.length)
|
|
3623
|
-
else
|
|
3624
|
-
|
|
3656
|
+
if (links.length) lines2.push("| App | Preview |", "| --- | --- |", ...links.map((l) => `| ${l.label} | ${l.url} |`));
|
|
3657
|
+
else lines2.push("Preview deploys are not set up: add `previews.apps` to `bridge.config.json` (docs/PREVIEWS.md).");
|
|
3658
|
+
lines2.push(
|
|
3625
3659
|
"",
|
|
3626
3660
|
"### When it is done",
|
|
3627
3661
|
"",
|
|
@@ -3630,7 +3664,7 @@ function explorationPrBody(e, ctx) {
|
|
|
3630
3664
|
"",
|
|
3631
3665
|
`<!-- ${MARK} ${JSON.stringify({ name: e.name.trim(), purpose: e.purpose.trim(), slug: slug2, ...e.useCase ? { useCase: e.useCase } : {}, ...experimental.length ? { experimental } : {}, ...e.author ? { author: e.author } : {}, ...e.startedAt ? { startedAt: e.startedAt } : {}, ...e.source ? { source: e.source } : {}, ...e.agent ? { agent: e.agent } : {} }).replace(/-->/g, "--\\u003e")} -->`
|
|
3632
3666
|
);
|
|
3633
|
-
return
|
|
3667
|
+
return lines2.join("\n");
|
|
3634
3668
|
}
|
|
3635
3669
|
function parseExplorationBody(body) {
|
|
3636
3670
|
if (!body) return void 0;
|
|
@@ -3955,9 +3989,9 @@ function mergeDraftResults(p, results, at = /* @__PURE__ */ new Date()) {
|
|
|
3955
3989
|
continue;
|
|
3956
3990
|
}
|
|
3957
3991
|
const clean = sanitizeDraftText(r);
|
|
3958
|
-
const
|
|
3959
|
-
if (!clean.title && !clean.summary && !
|
|
3960
|
-
next.set(r.id, { id: r.id, title: clean.title, summary: clean.summary, ...
|
|
3992
|
+
const lines2 = sanitizeResultLines(item2, r.lines);
|
|
3993
|
+
if (!clean.title && !clean.summary && !lines2.length) throw new Error(`The result for "${r.id}" has no title and no summary, and no line.`);
|
|
3994
|
+
next.set(r.id, { id: r.id, title: clean.title, summary: clean.summary, ...lines2.length ? { lines: lines2 } : {}, by: "claude", at: at.toISOString() });
|
|
3961
3995
|
}
|
|
3962
3996
|
const merged = p.items.map((i) => next.get(i.id)).filter((r) => !!r);
|
|
3963
3997
|
return { ...p, results: merged, status: merged.length === p.items.length ? "drafted" : "pending" };
|
|
@@ -3974,25 +4008,25 @@ function draftItemBrief(i) {
|
|
|
3974
4008
|
return [`Item ${i.id}: component docs${i.component ? ` of ${i.component}` : ""} from Bridge Studio (${i.docs.path ?? i.docs.slug}): ${what}.`, "", i.docs.prompt].join("\n");
|
|
3975
4009
|
}
|
|
3976
4010
|
const kind = i.kind ? REQUEST_KINDS.find((k) => k.value === i.kind)?.label : void 0;
|
|
3977
|
-
const
|
|
3978
|
-
const
|
|
3979
|
-
if (i.component)
|
|
3980
|
-
if (kind)
|
|
3981
|
-
if (i.components?.length)
|
|
3982
|
-
if (i.changes.length)
|
|
3983
|
-
if (i.variables?.length)
|
|
3984
|
-
if (!i.changes.length && !i.variables?.length && !i.lines?.length)
|
|
3985
|
-
if (i.draft.title || i.draft.summary)
|
|
3986
|
-
if (i.notes?.trim())
|
|
4011
|
+
const cell4 = (s) => (s ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
4012
|
+
const lines2 = [`Item ${i.id}: ${i.scope === "request" ? "a component change request (a GitHub issue developers implement)" : "the description of a pull request with design token (variable) edits from Figma"}.`];
|
|
4013
|
+
if (i.component) lines2.push(`Component: ${i.component}`);
|
|
4014
|
+
if (kind) lines2.push(`Kind: ${kind}`);
|
|
4015
|
+
if (i.components?.length) lines2.push(`Components with a change request: ${i.components.join(", ")}`);
|
|
4016
|
+
if (i.changes.length) lines2.push("", "| What | Now | In Figma |", "| --- | --- | --- |", ...i.changes.map((c) => `| ${cell4(c.field)} | ${cell4(c.before) || "none"} | ${cell4(c.after) || "removed"} |`));
|
|
4017
|
+
if (i.variables?.length) lines2.push("", "| Token | Now | In Figma |", "| --- | --- | --- |", ...i.variables.map((v) => `| ${cell4(v.path)} | ${cell4(v.before) || "none"} | ${cell4(v.after) || "removed"} |`));
|
|
4018
|
+
if (!i.changes.length && !i.variables?.length && !i.lines?.length) lines2.push("", "No change detected: rely on the notes and the images.");
|
|
4019
|
+
if (i.draft.title || i.draft.summary) lines2.push("", "Starting draft, written from the changes only:", `Title: ${i.draft.title}`, `Summary: ${i.draft.summary}`);
|
|
4020
|
+
if (i.notes?.trim()) lines2.push("", "The designer's notes (keep their intent):", i.notes.trim());
|
|
3987
4021
|
if (i.lines?.length) {
|
|
3988
|
-
|
|
4022
|
+
lines2.push("", `Change lines to write (one sentence each, at most ${DRAFT_LINE_MAX} characters, from the before and after images), each with the title of its request (at most ${DRAFT_TITLE_MAX} characters):`);
|
|
3989
4023
|
for (const l of i.lines) {
|
|
3990
|
-
|
|
3991
|
-
for (const c of l.changes)
|
|
4024
|
+
lines2.push(`- Line ${l.id}${l.component ? `, ${l.component}` : ""}${l.kind ? ` (${REQUEST_KINDS.find((k) => k.value === l.kind)?.label ?? l.kind})` : ""}. Starting line: ${l.line || "(none)"}${l.title ? `. Starting title: ${l.title}` : ""}`);
|
|
4025
|
+
for (const c of l.changes) lines2.push(` - ${c.field}: ${c.before || "none"} -> ${c.after || "removed"}`);
|
|
3992
4026
|
}
|
|
3993
4027
|
}
|
|
3994
|
-
if (i.images?.length)
|
|
3995
|
-
return
|
|
4028
|
+
if (i.images?.length) lines2.push("", `Images: ${i.images.map((m) => `${m.label}${m.line ? ` (line ${m.line})` : ""} ${m.url}`).join(", ")}`);
|
|
4029
|
+
return lines2.join("\n");
|
|
3996
4030
|
}
|
|
3997
4031
|
var isConflict = (err) => err instanceof GitHubError ? err.status === 409 || err.status === 422 && /sha/i.test(`${err.detail ?? ""} ${err.message}`) : err?.status === 409;
|
|
3998
4032
|
async function readPendingDrafts(client2, sessionId, branch = REQUESTS_BRANCH) {
|
|
@@ -4380,14 +4414,14 @@ function conflictRoute(o, opts = {}) {
|
|
|
4380
4414
|
return { kind: "tokens", files: o.tokenFiles };
|
|
4381
4415
|
}
|
|
4382
4416
|
function resolutionMessage(input) {
|
|
4383
|
-
const
|
|
4417
|
+
const lines2 = input.conflicts.map((c) => `- ${c.path} (${c.file}): ${choiceFor(c, input.choices) === "theirs" ? `used the ${input.library} value` : "kept this change"}`);
|
|
4384
4418
|
const title = input.conflicts.length ? `Bring in the latest ${input.library} and resolve ${plural(input.conflicts.length, "token conflict")}` : `Bring in the latest ${input.library}`;
|
|
4385
4419
|
const by = input.who ? `
|
|
4386
4420
|
|
|
4387
4421
|
Resolved in Bridge by @${input.who.replace(/^@/, "")}.` : "\n\nResolved in Bridge.";
|
|
4388
|
-
return `${title}${
|
|
4422
|
+
return `${title}${lines2.length ? `
|
|
4389
4423
|
|
|
4390
|
-
${
|
|
4424
|
+
${lines2.join("\n")}` : ""}${by}`;
|
|
4391
4425
|
}
|
|
4392
4426
|
function freshness(input) {
|
|
4393
4427
|
const behindBy = typeof input.compare?.behind_by === "number" ? input.compare.behind_by : null;
|
|
@@ -4576,11 +4610,11 @@ function canAddTo(p, viewer, write) {
|
|
|
4576
4610
|
return write || sameLogin(p.user?.login, viewer);
|
|
4577
4611
|
}
|
|
4578
4612
|
function addedComment(paths, viewer, from = "the Bridge token editor") {
|
|
4579
|
-
const
|
|
4580
|
-
if (paths.length > 20)
|
|
4613
|
+
const lines2 = paths.slice(0, 20).map((p) => `- \`${p}\``);
|
|
4614
|
+
if (paths.length > 20) lines2.push(`- and ${paths.length - 20} more`);
|
|
4581
4615
|
return `${viewer ? `@${viewer} added` : "Added"} token edits from ${from}:
|
|
4582
4616
|
|
|
4583
|
-
${
|
|
4617
|
+
${lines2.join("\n")}`;
|
|
4584
4618
|
}
|
|
4585
4619
|
|
|
4586
4620
|
// ../core/src/jira.ts
|
|
@@ -4603,6 +4637,981 @@ function branchWithJiraKey(branch, key) {
|
|
|
4603
4637
|
return i < 0 ? `${key}-${branch}` : `${branch.slice(0, i + 1)}${key}-${branch.slice(i + 1)}`;
|
|
4604
4638
|
}
|
|
4605
4639
|
|
|
4640
|
+
// ../core/src/figma-push.ts
|
|
4641
|
+
var where = (r) => r.allVariants ? "every variant" : shortVariants(r.variants.join("; "));
|
|
4642
|
+
var cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
4643
|
+
function planComponentPush(d, ctx) {
|
|
4644
|
+
const issues = [...d.unknown];
|
|
4645
|
+
const reasons = [];
|
|
4646
|
+
const edits = /* @__PURE__ */ new Map();
|
|
4647
|
+
const also = /* @__PURE__ */ new Set();
|
|
4648
|
+
for (const r of d.rebinds) {
|
|
4649
|
+
const label = `${cap(r.kind)} in ${where(r)}`;
|
|
4650
|
+
if (!r.after.length) {
|
|
4651
|
+
issues.push({ what: `${label} is no longer bound to a variable: it uses a raw value.`, todo: "Bind it to a variable, or create the variable and push it with the variables, then push again." });
|
|
4652
|
+
continue;
|
|
4653
|
+
}
|
|
4654
|
+
const missing = r.after.filter((p) => !ctx.tierOf(p));
|
|
4655
|
+
if (missing.length) {
|
|
4656
|
+
issues.push({ what: `${label} uses ${missing.join(", ")}, a variable the library does not have yet.`, todo: "Push the variable first (Variables, above), then push this component again." });
|
|
4657
|
+
continue;
|
|
4658
|
+
}
|
|
4659
|
+
if (!r.before.length) {
|
|
4660
|
+
reasons.push(`${label} is bound to ${r.after.join(", ")} where code paints nothing: Claude AI adds it to the CSS.`);
|
|
4661
|
+
continue;
|
|
4662
|
+
}
|
|
4663
|
+
if (r.before.length !== 1 || r.after.length !== 1) {
|
|
4664
|
+
reasons.push(`${label} moved from ${r.before.join(", ")} to ${r.after.join(", ")}: several variables on one property, so Claude AI writes the CSS.`);
|
|
4665
|
+
continue;
|
|
4666
|
+
}
|
|
4667
|
+
const [from, to] = [r.before[0], r.after[0]];
|
|
4668
|
+
if (ctx.tierOf(from) !== "component") {
|
|
4669
|
+
reasons.push(`Code paints ${r.kind} with ${from} directly, not through a ${d.component} token: Claude AI changes the CSS to ${to}.`);
|
|
4670
|
+
continue;
|
|
4671
|
+
}
|
|
4672
|
+
const elsewhere = ctx.usedElsewhere(from, r);
|
|
4673
|
+
if (elsewhere.variants.length) {
|
|
4674
|
+
const v = elsewhere.variants;
|
|
4675
|
+
const list2 = v.length > 3 ? `${shortVariants(v.slice(0, 3).join("; ")).replace(/ and /, ", ")} and ${v.length - 3} more` : shortVariants(v.join("; "));
|
|
4676
|
+
reasons.push(`${from} also paints ${list2}: pointing it to ${to} would change them too, so Claude AI adds a token for ${where(r)}.`);
|
|
4677
|
+
continue;
|
|
4678
|
+
}
|
|
4679
|
+
for (const c of elsewhere.components) also.add(c);
|
|
4680
|
+
const prev = edits.get(from);
|
|
4681
|
+
if (prev && prev.value !== `{${to}}`) {
|
|
4682
|
+
reasons.push(`${from} moved to two different variables (${String(prev.value).slice(1, -1)} and ${to}): Claude AI splits it.`);
|
|
4683
|
+
edits.delete(from);
|
|
4684
|
+
continue;
|
|
4685
|
+
}
|
|
4686
|
+
edits.set(from, { op: "set", path: from, value: `{${to}}` });
|
|
4687
|
+
}
|
|
4688
|
+
const list = [...edits.values()];
|
|
4689
|
+
if (d.needsCode || reasons.length) {
|
|
4690
|
+
return {
|
|
4691
|
+
route: "claude-draft",
|
|
4692
|
+
said: `Claude AI drafts the code from what you changed, then a person reviews it.${list.length ? ` ${list.length === 1 ? "Its token swap goes" : "Its token swaps go"} with it.` : ""}`,
|
|
4693
|
+
edits: list,
|
|
4694
|
+
reasons,
|
|
4695
|
+
issues,
|
|
4696
|
+
alsoChanges: [...also]
|
|
4697
|
+
};
|
|
4698
|
+
}
|
|
4699
|
+
if (list.length) {
|
|
4700
|
+
const n = list.length;
|
|
4701
|
+
const others = [...also];
|
|
4702
|
+
const names3 = others.length > 3 ? [...others.slice(0, 3), `${others.length - 3} more`] : others;
|
|
4703
|
+
const alsoLine = names3.length ? ` It also repaints ${names3.length > 1 ? `${names3.slice(0, -1).join(", ")} and ${names3[names3.length - 1]}` : names3[0]}: they use the same token${n === 1 ? "" : "s"}.` : "";
|
|
4704
|
+
return { route: "token-change", said: `Exact, no AI: ${n === 1 ? `${list[0].path} points` : `${n} ${d.component} tokens point`} to the new variable${n === 1 ? "" : "s"}, checked for contrast and reviewed like any token change.${alsoLine}`, edits: list, reasons, issues, alsoChanges: others };
|
|
4705
|
+
}
|
|
4706
|
+
return { route: "manual", said: issues.length ? "Fix it in Figma first: nothing is sent until Bridge can read it." : "Nothing to send.", edits: [], reasons, issues, alsoChanges: [] };
|
|
4707
|
+
}
|
|
4708
|
+
function specsUsage(specs, component) {
|
|
4709
|
+
const own = specs.components.find((x) => x.name === component) ?? specs.components.find((x) => x.component === component);
|
|
4710
|
+
return (path, r) => {
|
|
4711
|
+
const variants = [];
|
|
4712
|
+
const components = [];
|
|
4713
|
+
for (const c of specs.components) {
|
|
4714
|
+
for (const v of c.variants) {
|
|
4715
|
+
if (!Object.values(v.values).some((fields) => Object.values(fields ?? {}).includes(path))) continue;
|
|
4716
|
+
const key = variantKeyOf(v.props);
|
|
4717
|
+
if (c === own) {
|
|
4718
|
+
if (!r.variants.includes(key)) variants.push(key);
|
|
4719
|
+
} else if (!components.includes(c.name)) components.push(c.name);
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
return { variants, components };
|
|
4723
|
+
};
|
|
4724
|
+
}
|
|
4725
|
+
function componentContrastPairs(edits, pairs, tokens) {
|
|
4726
|
+
const out = /* @__PURE__ */ new Map();
|
|
4727
|
+
for (const e of edits) {
|
|
4728
|
+
const target = tokens.aliasOf(e.path);
|
|
4729
|
+
if (!target) continue;
|
|
4730
|
+
const group = e.path.split(".")[0];
|
|
4731
|
+
const sibling = (other) => tokens.paths.find((p) => p !== e.path && p.split(".")[0] === group && tokens.aliasOf(p) === other) ?? other;
|
|
4732
|
+
for (const p of pairs) {
|
|
4733
|
+
if (p.foreground === target) out.set(`${e.path}|${sibling(p.background)}`, { ...p, foreground: e.path, background: sibling(p.background) });
|
|
4734
|
+
if (p.background === target) out.set(`${sibling(p.foreground)}|${e.path}`, { ...p, foreground: sibling(p.foreground), background: e.path });
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
return [...out.values()];
|
|
4738
|
+
}
|
|
4739
|
+
var FROM_FIGMA_LABEL = "from-figma";
|
|
4740
|
+
var WAITING_FOR_CLAUDE_LABEL = "waiting-for-claude";
|
|
4741
|
+
var DRAFTED_BY_CLAUDE_LABEL = "drafted-by-claude";
|
|
4742
|
+
var FIGMA_SPEC_DIR = ".bridge/figma";
|
|
4743
|
+
var ORIGIN_MARK = "bridge-from-figma";
|
|
4744
|
+
var hidden = (data) => `<!-- ${ORIGIN_MARK} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
|
|
4745
|
+
function parseFigmaOrigin(body) {
|
|
4746
|
+
if (!body) return void 0;
|
|
4747
|
+
const m = new RegExp(`<!-- ${ORIGIN_MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
4748
|
+
if (!m) return void 0;
|
|
4749
|
+
try {
|
|
4750
|
+
const o = JSON.parse(m[1]);
|
|
4751
|
+
return typeof o.component === "string" && (o.route === "token-change" || o.route === "claude-draft") ? o : void 0;
|
|
4752
|
+
} catch {
|
|
4753
|
+
return void 0;
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4756
|
+
function figmaOriginSection(o) {
|
|
4757
|
+
const design = o.url ? `[${o.figmaName}](${o.url})` : `"${o.figmaName}"`;
|
|
4758
|
+
const who = o.requestedBy ? ` by @${o.requestedBy}` : "";
|
|
4759
|
+
const status = o.route === "token-change" ? "Exact token change, no AI: Bridge read the token swap from the Figma component." : o.status === "drafted" ? "**Drafted by Claude AI** from the Figma spec. A person reviews it before it ships." : `**Waiting for Claude AI:** this branch carries the spec only, as a request. Claude writes the code here (\`bridge_get_figma_change\`, or \`bridge figma show <number>\`), then it is labelled ${DRAFTED_BY_CLAUDE_LABEL}.`;
|
|
4760
|
+
return [`**From Figma:** ${design}${who}. Once it ships, a pull in the DS Bridge plugin rebuilds the component from this code.`, "", status, "", hidden(o)].join("\n");
|
|
4761
|
+
}
|
|
4762
|
+
function withFigmaOrigin(body, o) {
|
|
4763
|
+
let b = (body ?? "").trim();
|
|
4764
|
+
const at = b.indexOf("**From Figma:**");
|
|
4765
|
+
const mark = b.indexOf(`<!-- ${ORIGIN_MARK} `);
|
|
4766
|
+
if (at >= 0 && mark > at) {
|
|
4767
|
+
const end = b.indexOf("-->", mark);
|
|
4768
|
+
b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
|
|
4769
|
+
}
|
|
4770
|
+
return `${figmaOriginSection(o)}${b ? `
|
|
4771
|
+
|
|
4772
|
+
${b}` : ""}`;
|
|
4773
|
+
}
|
|
4774
|
+
var figmaSpecSlug = (d) => slugify(`${d.component} ${d.title}`, 48) || "component-change";
|
|
4775
|
+
var figmaSpecPath = (d) => `${FIGMA_SPEC_DIR}/${figmaSpecSlug(d)}.json`;
|
|
4776
|
+
var kindLabel2 = (k) => REQUEST_KINDS.find((x) => x.value === k)?.label ?? "Change";
|
|
4777
|
+
function formatFigmaSpec(d, plan) {
|
|
4778
|
+
return `${JSON.stringify({ ...d, tokenEdits: plan.edits, reasons: plan.reasons, unknown: plan.issues }, null, 2)}
|
|
4779
|
+
`;
|
|
4780
|
+
}
|
|
4781
|
+
function parseFigmaSpec(text) {
|
|
4782
|
+
if (!text) return void 0;
|
|
4783
|
+
try {
|
|
4784
|
+
const s = JSON.parse(text);
|
|
4785
|
+
return s && s.v === 1 && typeof s.component === "string" && Array.isArray(s.changes) ? s : void 0;
|
|
4786
|
+
} catch {
|
|
4787
|
+
return void 0;
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4790
|
+
function figmaDraftBrief(d, ctx) {
|
|
4791
|
+
const dir = `${(ctx.componentsDir ?? "packages/react/src/components").replace(/\/+$/, "")}/${d.component}`;
|
|
4792
|
+
const suggested = "suggestion" in ctx;
|
|
4793
|
+
const variants = Object.entries(d.variantAxes).map(([axis, values]) => `${axis}: ${values.join(", ")}`);
|
|
4794
|
+
const props = d.properties.filter((p) => p.type !== "VARIANT").map((p) => `${p.name} (${p.type.toLowerCase().replace("_", " ")}, default ${p.default || "empty"})`);
|
|
4795
|
+
return [
|
|
4796
|
+
suggested ? "Draft the code for a component change made in Figma, sent as a Bridge suggestion (this issue)." : `Draft the code for a component change made in Figma, on the library branch ${ctx.branch} (draft pull request #${ctx.pr}).`,
|
|
4797
|
+
"",
|
|
4798
|
+
d.coded ? `Component: ${d.component} (${dir})` : `New component: ${d.component} (create ${dir})`,
|
|
4799
|
+
`Kind: ${kindLabel2(d.kind)}`,
|
|
4800
|
+
`Change: ${d.title}`,
|
|
4801
|
+
...d.line?.trim() ? [`What changes: ${d.line.trim()}`] : [],
|
|
4802
|
+
"",
|
|
4803
|
+
d.summary.trim(),
|
|
4804
|
+
"",
|
|
4805
|
+
...d.changes.length ? ["Changes in Figma:", ...d.changes.map((c) => `- ${c.field}: ${c.before ?? "(none)"} -> ${c.after ?? "(removed)"}`), ""] : [],
|
|
4806
|
+
...variants.length ? ["Variant matrix in Figma:", ...variants.map((v) => `- ${v}`), ""] : [],
|
|
4807
|
+
...props.length ? ["Properties in Figma:", ...props.map((p) => `- ${p}`), ""] : [],
|
|
4808
|
+
...d.tokenEdits?.length ? ["Token swaps to make too (exact, with bridge_propose_token_change on this branch or by hand in the token files):", ...d.tokenEdits.map((e) => `- ${e.path} -> ${String(e.value)}`), ""] : [],
|
|
4809
|
+
...d.reasons?.length ? ["Why this needs code:", ...d.reasons.map((r) => `- ${r}`), ""] : [],
|
|
4810
|
+
`Spec: ${ctx.specPath} ${suggested ? "in the suggestion" : "on the branch"} has the bound variables of every variant (token paths), the layers of every variant and what code gave Figma before.`,
|
|
4811
|
+
...d.figma?.url ? [`Design: ${d.figma.url}`] : [],
|
|
4812
|
+
...d.images?.length ? ["Screenshot:", ...d.images.map((u) => `- ${u}`)] : [],
|
|
4813
|
+
"",
|
|
4814
|
+
"Steps:",
|
|
4815
|
+
"suggestion" in ctx ? "1. Apply the suggestion on a new branch: bridge apply <this issue number> (it writes the spec file), or start a branch from the default branch." : `1. git fetch origin && git checkout ${ctx.branch}`,
|
|
4816
|
+
`2. Write the change in ${dir}: the TSX on Base UI, the CSS module with tokens only (the bound variables of the spec, as token paths), the stories (examples), docs.json and a changeset. Follow ${ctx.agentsFile ?? "AGENTS.md"}.`,
|
|
4817
|
+
"3. Run npm run specs so component-specs.json matches the code, then npm test.",
|
|
4818
|
+
"suggestion" in ctx ? '4. Commit, push and open a pull request that says "Closes #<this issue number>", so the suggestion closes when it ships. A person reviews it.' : `4. Commit and push to ${ctx.branch}, then call bridge_mark_figma_change_drafted { pr: ${ctx.pr} } (CLI: bridge figma drafted ${ctx.pr}). The branch is then labelled ${DRAFTED_BY_CLAUDE_LABEL} and a person reviews it.`,
|
|
4819
|
+
"",
|
|
4820
|
+
"Rules:",
|
|
4821
|
+
"- Never invent a token or a value: use the variables the spec lists. A value the library lacks is a token change, said in the pull request.",
|
|
4822
|
+
"- Keep every existing variant and prop unchanged unless the spec changes it.",
|
|
4823
|
+
`- If the spec is unclear, comment on the ${suggested ? "suggestion" : "pull request"} instead of guessing.`
|
|
4824
|
+
].join("\n");
|
|
4825
|
+
}
|
|
4826
|
+
function figmaDraftBody(d, plan, o) {
|
|
4827
|
+
const cell4 = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
4828
|
+
const rows = d.changes.length ? ["| What | Code now | In Figma |", "| --- | --- | --- |", ...d.changes.map((c) => `| ${cell4(c.field)} | ${cell4(c.before ?? "")} | ${cell4(c.after ?? "")} |`), ""] : [];
|
|
4829
|
+
return withFigmaOrigin(
|
|
4830
|
+
[
|
|
4831
|
+
`## ${d.coded ? `${kindLabel2(d.kind)} for ${d.component}` : `New component: ${d.component}`}`,
|
|
4832
|
+
"",
|
|
4833
|
+
d.summary.trim(),
|
|
4834
|
+
"",
|
|
4835
|
+
...d.line?.trim() ? [`**What changes:** ${d.line.trim()}`, ""] : [],
|
|
4836
|
+
...rows,
|
|
4837
|
+
...d.images?.length ? [...d.images.map((u, i) => ``), ""] : [],
|
|
4838
|
+
...plan.edits.length ? ["**Token swaps to make too:**", ...plan.edits.map((e) => `- \`${e.path}\` -> \`${String(e.value)}\``), ""] : [],
|
|
4839
|
+
...plan.reasons.length ? ["**Why this needs code:**", ...plan.reasons.map((r) => `- ${r}`), ""] : [],
|
|
4840
|
+
...plan.issues.length ? ["**Left in Figma (the designer fixes these first):**", ...plan.issues.map((i) => `- ${i.what} ${i.todo}`), ""] : [],
|
|
4841
|
+
`**Spec:** \`${o.spec ?? figmaSpecPath(d)}\` (variant matrix, properties, bound variables and layers of every variant).`
|
|
4842
|
+
].join("\n"),
|
|
4843
|
+
o
|
|
4844
|
+
);
|
|
4845
|
+
}
|
|
4846
|
+
var figmaBranchTitle = (d) => requestIssueTitle(d);
|
|
4847
|
+
function figmaPushStatus(x) {
|
|
4848
|
+
const labels = x.labels ?? [];
|
|
4849
|
+
if (x.state === "merged") return { key: "shipped", label: "Shipped", sentence: "It shipped. Pull to rebuild the component from the reviewed code.", rebuild: true };
|
|
4850
|
+
if (x.state !== "open") return { key: "closed", label: "Closed", sentence: "The library closed it without shipping. Pull to bring the component back to the code.", rebuild: true };
|
|
4851
|
+
if (labels.includes(WAITING_FOR_CLAUDE_LABEL)) return { key: "waiting-for-claude", label: "Waiting for Claude AI", sentence: "The spec is on the library branch. Claude AI writes the code there, then a person reviews it.", rebuild: false };
|
|
4852
|
+
if (labels.includes(DRAFTED_BY_CLAUDE_LABEL)) return { key: "drafted", label: "Drafted by Claude AI", sentence: "Claude AI drafted the code. A person reviews it before it ships.", rebuild: false };
|
|
4853
|
+
return { key: "in-review", label: "In review", sentence: x.route === "token-change" ? "Owners review the token change." : "Owners review it.", rebuild: false };
|
|
4854
|
+
}
|
|
4855
|
+
|
|
4856
|
+
// ../core/src/prod-branch.ts
|
|
4857
|
+
var PIECES_MARK = "bridge-pieces";
|
|
4858
|
+
var NEEDED_MARK = "bridge-needed-by";
|
|
4859
|
+
var SECTION = "### Missing from the library";
|
|
4860
|
+
var NEEDED_BY_LABEL = "needed-by-product";
|
|
4861
|
+
var hiddenJson = (mark, data) => `<!-- ${mark} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
|
|
4862
|
+
var readHidden = (mark, body) => {
|
|
4863
|
+
if (!body) return void 0;
|
|
4864
|
+
const m = new RegExp(`<!-- ${mark} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
4865
|
+
if (!m) return void 0;
|
|
4866
|
+
try {
|
|
4867
|
+
return JSON.parse(m[1]);
|
|
4868
|
+
} catch {
|
|
4869
|
+
return void 0;
|
|
4870
|
+
}
|
|
4871
|
+
};
|
|
4872
|
+
var KIND_WORD = { "token-change": "token change", exploration: "library exploration", request: "component request", suggestion: "suggested token change" };
|
|
4873
|
+
function parsePiecesRecord(body) {
|
|
4874
|
+
const r = readHidden(PIECES_MARK, body);
|
|
4875
|
+
if (!r || !Array.isArray(r.pieces) || !Array.isArray(r.links) || typeof r.checkedAt !== "string") return void 0;
|
|
4876
|
+
return r;
|
|
4877
|
+
}
|
|
4878
|
+
function piecesSection(r) {
|
|
4879
|
+
const link = (p) => r.links.find((l) => l.piece === p.id);
|
|
4880
|
+
const lines2 = r.pieces.length ? r.pieces.map((p) => {
|
|
4881
|
+
const l = link(p);
|
|
4882
|
+
return `- **${p.title}**: ${p.summary}${l ? ` ${cap2(KIND_WORD[l.kind])}: #${l.number}.` : " No library branch yet."}`;
|
|
4883
|
+
}) : ["- Nothing: every value and component comes from the library."];
|
|
4884
|
+
return [SECTION, "", `Checked ${r.checkedAt.slice(0, 10)}${r.headSha ? ` on ${r.headSha.slice(0, 7)}` : ""} by Bridge.`, "", ...lines2, "", hiddenJson(PIECES_MARK, r)].join("\n");
|
|
4885
|
+
}
|
|
4886
|
+
function withPiecesRecord(body, r) {
|
|
4887
|
+
const b = (body ?? "").trimEnd();
|
|
4888
|
+
const start = b.indexOf(SECTION);
|
|
4889
|
+
const markAt = b.indexOf(`<!-- ${PIECES_MARK} `);
|
|
4890
|
+
if (start >= 0 && markAt > start) {
|
|
4891
|
+
const end = b.indexOf("-->", markAt);
|
|
4892
|
+
return `${b.slice(0, start)}${piecesSection(r)}${end >= 0 ? b.slice(end + 3) : ""}`.trimEnd();
|
|
4893
|
+
}
|
|
4894
|
+
return `${b}${b ? "\n\n" : ""}${piecesSection(r)}`;
|
|
4895
|
+
}
|
|
4896
|
+
function mergePiecesRecord(prev, next) {
|
|
4897
|
+
const links = [...prev?.links ?? []];
|
|
4898
|
+
for (const l of next.links ?? []) {
|
|
4899
|
+
const at = links.findIndex((x) => x.piece === l.piece);
|
|
4900
|
+
if (at >= 0) links[at] = l;
|
|
4901
|
+
else links.push(l);
|
|
4902
|
+
}
|
|
4903
|
+
return { ...next, links };
|
|
4904
|
+
}
|
|
4905
|
+
function withLink(r, l) {
|
|
4906
|
+
return { ...r, links: [...r.links.filter((x) => x.piece !== l.piece), l] };
|
|
4907
|
+
}
|
|
4908
|
+
function neededBySection(n) {
|
|
4909
|
+
const who = n.author ? ` for @${n.author}` : "";
|
|
4910
|
+
const near = n.nearest ? ` The closest the library has today is ${n.nearest}${n.nearestToken ? ` (\`${n.nearestToken}\`)` : ""}.` : "";
|
|
4911
|
+
return [`**Needed by:** [${n.title}](${n.url}) (#${n.pr}) in ${n.productName}${who}.${near}`, "", hiddenJson(NEEDED_MARK, n)].join("\n");
|
|
4912
|
+
}
|
|
4913
|
+
function withNeededBy(body, n) {
|
|
4914
|
+
let b = (body ?? "").trim();
|
|
4915
|
+
const at = b.indexOf("**Needed by:**");
|
|
4916
|
+
const mark = b.indexOf(`<!-- ${NEEDED_MARK} `);
|
|
4917
|
+
if (at >= 0 && mark > at) {
|
|
4918
|
+
const end = b.indexOf("-->", mark);
|
|
4919
|
+
b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
|
|
4920
|
+
}
|
|
4921
|
+
return `${neededBySection(n)}${b ? `
|
|
4922
|
+
|
|
4923
|
+
${b}` : ""}`;
|
|
4924
|
+
}
|
|
4925
|
+
var cap2 = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
4926
|
+
var forLine = (b) => `${b.productName} needs it for "${b.title.replace(/"/g, "'")}" (#${b.pr})`;
|
|
4927
|
+
function libraryBranchPlan(piece, branch, opts = {}) {
|
|
4928
|
+
const neededBy = {
|
|
4929
|
+
pr: branch.pr,
|
|
4930
|
+
title: branch.title,
|
|
4931
|
+
branch: branch.branch,
|
|
4932
|
+
url: branch.url,
|
|
4933
|
+
product: branch.productKey,
|
|
4934
|
+
productName: branch.productName,
|
|
4935
|
+
...branch.author ? { author: branch.author } : {},
|
|
4936
|
+
piece: piece.id,
|
|
4937
|
+
pieceTitle: piece.title,
|
|
4938
|
+
...piece.nearest ? { nearest: piece.nearest.label, nearestToken: piece.nearest.token } : {}
|
|
4939
|
+
};
|
|
4940
|
+
const near = piece.nearest ? ` The closest the library has today is ${piece.nearest.label} (${piece.nearest.token}, ${piece.nearest.value}).` : "";
|
|
4941
|
+
if (piece.kind === "token" && piece.token) {
|
|
4942
|
+
const t = piece.token;
|
|
4943
|
+
const files = t.files.length ? t.files : [void 0];
|
|
4944
|
+
const edits = files.map((file) => ({
|
|
4945
|
+
op: "add",
|
|
4946
|
+
path: t.path,
|
|
4947
|
+
type: t.type,
|
|
4948
|
+
value: t.value,
|
|
4949
|
+
description: `${cap2(piece.name)}: ${forLine(branch)}.${files.length > 1 ? " Same value in every theme: review it in each." : ""}`,
|
|
4950
|
+
...file ? { file } : {}
|
|
4951
|
+
}));
|
|
4952
|
+
return { kind: "token-change", title: `Add ${t.path}`, summary: `${forLine(branch)}: ${piece.summary}${near}`, edits, neededBy };
|
|
4953
|
+
}
|
|
4954
|
+
if (piece.kind === "variant" && piece.component && opts.mode === "suggest") {
|
|
4955
|
+
const request2 = {
|
|
4956
|
+
component: piece.component,
|
|
4957
|
+
kind: "new-variant",
|
|
4958
|
+
title: piece.title,
|
|
4959
|
+
summary: `${forLine(branch)}: ${piece.summary}${near}`,
|
|
4960
|
+
changes: piece.values.slice(0, 6).map((v) => ({ field: v.property ?? v.category, after: v.value })),
|
|
4961
|
+
source: opts.source ?? "mcp",
|
|
4962
|
+
...opts.requestedBy ? { requestedBy: opts.requestedBy } : {}
|
|
4963
|
+
};
|
|
4964
|
+
return { kind: "request", title: piece.title, request: request2, neededBy };
|
|
4965
|
+
}
|
|
4966
|
+
if (piece.kind === "variant" && piece.component) {
|
|
4967
|
+
const values = piece.values.map((v) => `${v.property ?? v.category} ${v.value}`).join(", ");
|
|
4968
|
+
const exploration = {
|
|
4969
|
+
name: piece.title,
|
|
4970
|
+
purpose: `${forLine(branch)}: ${/^[aeiou]/i.test(piece.name) ? "an" : "a"} ${piece.name} ${piece.aspect ?? "style"} on ${piece.component} (${values}).${near}`,
|
|
4971
|
+
useCase: "terminal",
|
|
4972
|
+
experimental: [piece.title],
|
|
4973
|
+
source: opts.source === "figma" ? "figma" : opts.source ?? "mcp"
|
|
4974
|
+
};
|
|
4975
|
+
const brief = [
|
|
4976
|
+
`Add the ${piece.name} ${piece.aspect ?? "style"} to ${piece.component} in the library, on this exploration branch.`,
|
|
4977
|
+
`The product branch writes it by hand: ${values}. Express it with tokens (a component or semantic token per value), add it to the component's props, docs and examples, and keep every existing ${piece.component} unchanged.`,
|
|
4978
|
+
piece.nearest ? `Say in the pull request why ${piece.nearest.label} is not enough, or recommend it instead.` : ""
|
|
4979
|
+
].filter(Boolean).join(" ");
|
|
4980
|
+
return { kind: "exploration", title: piece.title, exploration, neededBy, brief };
|
|
4981
|
+
}
|
|
4982
|
+
const name = piece.component ?? cap2(piece.name);
|
|
4983
|
+
const request = {
|
|
4984
|
+
component: name,
|
|
4985
|
+
kind: "new-component",
|
|
4986
|
+
title: `Add ${name}`,
|
|
4987
|
+
summary: `${forLine(branch)}: ${piece.summary}${near}`,
|
|
4988
|
+
changes: piece.values.slice(0, 6).map((v) => ({ field: v.property ?? v.category, after: v.value })),
|
|
4989
|
+
source: opts.source ?? "mcp",
|
|
4990
|
+
...opts.requestedBy ? { requestedBy: opts.requestedBy } : {}
|
|
4991
|
+
};
|
|
4992
|
+
return { kind: "request", title: `New component: ${name}`, request, neededBy };
|
|
4993
|
+
}
|
|
4994
|
+
var STATUS_LABEL = {
|
|
4995
|
+
draft: "Draft",
|
|
4996
|
+
"in-review": "In review",
|
|
4997
|
+
changes: "Changes requested",
|
|
4998
|
+
approved: "Approved",
|
|
4999
|
+
shipped: "Shipped",
|
|
5000
|
+
closed: "Closed",
|
|
5001
|
+
exploring: "Being built",
|
|
5002
|
+
requested: "Requested",
|
|
5003
|
+
"in-progress": "In progress",
|
|
5004
|
+
done: "Done",
|
|
5005
|
+
declined: "Declined",
|
|
5006
|
+
outdated: "Outdated"
|
|
5007
|
+
};
|
|
5008
|
+
var names2 = (people2) => people2.length <= 2 ? people2.map((p) => `@${p}`).join(" and ") : `@${people2[0]} and ${people2.length - 1} more`;
|
|
5009
|
+
var SUGGESTION_LINK_STATUS = {
|
|
5010
|
+
"needs-review": "in-review",
|
|
5011
|
+
"changes-requested": "changes",
|
|
5012
|
+
approved: "approved",
|
|
5013
|
+
applying: "in-progress",
|
|
5014
|
+
applied: "shipped",
|
|
5015
|
+
declined: "declined",
|
|
5016
|
+
outdated: "outdated"
|
|
5017
|
+
};
|
|
5018
|
+
function linkStatus(x) {
|
|
5019
|
+
const make = (key, people2 = [], verb = "reviews") => {
|
|
5020
|
+
const label = STATUS_LABEL[key];
|
|
5021
|
+
const tail = people2.length ? ` \xB7 ${names2(people2)} ${people2.length === 1 ? verb : verb.replace(/s$/, "")}` : "";
|
|
5022
|
+
return { key, label, people: people2, line: `${label}${tail}` };
|
|
5023
|
+
};
|
|
5024
|
+
if (x.kind === "suggestion") {
|
|
5025
|
+
const key = SUGGESTION_LINK_STATUS[x.status];
|
|
5026
|
+
return make(key, key === "in-progress" ? [...x.assignees ?? []] : [], "builds");
|
|
5027
|
+
}
|
|
5028
|
+
if (x.kind === "request") {
|
|
5029
|
+
if (x.state === "closed") return make(x.stateReason === "not_planned" ? "declined" : "done");
|
|
5030
|
+
return x.implemented ? make("in-progress", [...x.assignees ?? []], "builds") : make("requested", [...x.assignees ?? []], "builds");
|
|
5031
|
+
}
|
|
5032
|
+
if (x.state === "merged") return make("shipped");
|
|
5033
|
+
if (x.state !== "open") return make("closed");
|
|
5034
|
+
if ((x.labels ?? []).includes("exploration")) return make("exploring");
|
|
5035
|
+
if (x.draft) return make("draft");
|
|
5036
|
+
const latest = /* @__PURE__ */ new Map();
|
|
5037
|
+
for (const r of x.reviews ?? []) if (r.user && r.state !== "COMMENTED") latest.set(r.user, r.state);
|
|
5038
|
+
const states = [...latest.values()];
|
|
5039
|
+
if (states.includes("CHANGES_REQUESTED")) return make("changes", [...latest].filter(([, s]) => s === "CHANGES_REQUESTED").map(([u]) => u), "asks");
|
|
5040
|
+
const waiting = [...x.requestedReviewers ?? []];
|
|
5041
|
+
if (states.includes("APPROVED") && !waiting.length) return make("approved");
|
|
5042
|
+
return make("in-review", waiting);
|
|
5043
|
+
}
|
|
5044
|
+
function libraryPreviewState(x) {
|
|
5045
|
+
if (x.kind === "suggestion") {
|
|
5046
|
+
if (x.lifecycle === "closed") return { key: "declined", sentence: `The library declined this suggestion.${x.nearest ? ` Ask Claude to use ${x.nearest} instead.` : ""}`, action: null };
|
|
5047
|
+
if (x.lifecycle === "merged") return x.behindMain === 0 ? { key: "switched", sentence: "Your branch uses the published version.", action: null } : { key: "published", sentence: "The library applied the suggestion. Switch your branch to the published version.", action: { kind: "switch", label: "Switch to the published version" } };
|
|
5048
|
+
return { key: "nothing-yet", sentence: "Nothing to preview yet: owners review the suggestion, then a developer applies it. Your branch keeps its own value until then.", action: null };
|
|
5049
|
+
}
|
|
5050
|
+
if (x.kind === "request") {
|
|
5051
|
+
if (x.lifecycle === "closed") return { key: "declined", sentence: `The library declined this request.${x.nearest ? ` Ask Claude to use ${x.nearest} instead.` : ""}`, action: null };
|
|
5052
|
+
if (x.lifecycle === "merged") return x.behindMain === 0 ? { key: "switched", sentence: "Your branch uses the published version.", action: null } : { key: "published", sentence: "The library built it. Switch your branch to the published version, then ask Claude to use it.", action: { kind: "switch", label: "Switch to the published version" } };
|
|
5053
|
+
return { key: "nothing-yet", sentence: "Nothing to preview yet: the library builds it first. Your branch keeps its own version until then.", action: null };
|
|
5054
|
+
}
|
|
5055
|
+
if (x.lifecycle === "merged") {
|
|
5056
|
+
if (x.behindMain === 0) return { key: "switched", sentence: "Your branch uses the published version.", action: null };
|
|
5057
|
+
return { key: "published", sentence: "The library published it. Switch your branch to the published version.", action: { kind: "switch", label: "Switch to the published version" } };
|
|
5058
|
+
}
|
|
5059
|
+
if (x.lifecycle === "closed") return { key: "declined", sentence: `The library closed it without shipping.${x.nearest ? ` Ask Claude to use ${x.nearest} instead.` : ""}`, action: null };
|
|
5060
|
+
if (!x.hasChanges) return { key: "nothing-yet", sentence: "The library branch has no change yet. Claude adds it there, then your branch can use its preview.", action: null };
|
|
5061
|
+
if (x.missing === 0) return { key: "preview", sentence: "Your branch uses a preview of the library branch. When the library publishes it, switch to the real version here in one click.", action: null };
|
|
5062
|
+
if (x.missing !== null && x.total !== null && x.missing < x.total) return { key: "outdated", sentence: "The library branch changed since your branch took its preview.", action: { kind: "refresh-preview", label: "Get the latest preview" } };
|
|
5063
|
+
return { key: "not-used", sentence: "Your branch does not use the library branch yet.", action: { kind: "use-preview", label: "Use its preview in my branch" } };
|
|
5064
|
+
}
|
|
5065
|
+
var nPieces = (n) => `${n} ${n === 1 ? "piece" : "pieces"}`;
|
|
5066
|
+
function productBranchSentence(r, opts = {}) {
|
|
5067
|
+
if (!r) return { lead: "Not checked against the library yet.", strong: "", rest: " Ask Claude what is missing from the library." };
|
|
5068
|
+
const n = r.pieces.length;
|
|
5069
|
+
if (!n) return { lead: "Built with the library.", strong: " Nothing is missing", rest: ": every value and component comes from it." };
|
|
5070
|
+
const linked = r.pieces.filter((p) => r.links.some((l) => l.piece === p.id));
|
|
5071
|
+
const openers = [...new Set(linked.map((p) => r.links.find((l) => l.piece === p.id)).map((l) => l.agent ? opts.agentName ?? "Claude AI" : l.openedBy && l.openedBy === opts.viewer ? "you" : l.openedBy ? `@${l.openedBy}` : "someone"))];
|
|
5072
|
+
const by = openers.length === 1 ? openers[0] : "your team";
|
|
5073
|
+
const was = n === 1 ? "was" : "were";
|
|
5074
|
+
if (linked.length === n) {
|
|
5075
|
+
const what = n === 1 ? "a library branch for it" : "library branches for them";
|
|
5076
|
+
return { lead: "Built with the library. ", strong: `${nPieces(n)} ${was} missing`, rest: `, and ${by} opened ${what}.` };
|
|
5077
|
+
}
|
|
5078
|
+
if (!linked.length) return { lead: "Built with the library. ", strong: `${nPieces(n)} ${n === 1 ? "is" : "are"} missing`, rest: n === 1 ? ": open a library branch for it." : ": open a library branch for each." };
|
|
5079
|
+
return { lead: "Built with the library. ", strong: `${nPieces(n)} ${was} missing`, rest: `: ${linked.length} ${linked.length === 1 ? "has" : "have"} a library branch, ${n - linked.length} still ${n - linked.length === 1 ? "needs" : "need"} one.` };
|
|
5080
|
+
}
|
|
5081
|
+
function pieceId(parts) {
|
|
5082
|
+
return slugify(parts.filter(Boolean).join(" "), 48);
|
|
5083
|
+
}
|
|
5084
|
+
var TONE_WORDS = ["neutral", "success", "warning", "danger", "info", "error", "critical", "accent", "brand", "primary", "secondary"];
|
|
5085
|
+
function tokenLabel(path) {
|
|
5086
|
+
const parts = path.split(".");
|
|
5087
|
+
const tone = parts.find((p) => TONE_WORDS.includes(p));
|
|
5088
|
+
if (tone) return tone;
|
|
5089
|
+
const meaningful = parts.filter((p) => !/^(color|bg|fg|text|border|default|base|value|\d+)$/.test(p));
|
|
5090
|
+
return meaningful[meaningful.length - 1] ?? parts[parts.length - 1] ?? path;
|
|
5091
|
+
}
|
|
5092
|
+
function proposedTokenPath(nearest, name, category) {
|
|
5093
|
+
const slug2 = slugify(name, 32);
|
|
5094
|
+
if (!nearest) return `${category === "color" ? "color" : category}.${slug2}`;
|
|
5095
|
+
const parts = nearest.split(".");
|
|
5096
|
+
const toneAt = parts.findIndex((p) => TONE_WORDS.includes(p));
|
|
5097
|
+
if (toneAt > 0) return [...parts.slice(0, toneAt), slug2, ...parts.slice(toneAt + 1)].join(".");
|
|
5098
|
+
return [...parts.slice(0, -1), slug2].join(".");
|
|
5099
|
+
}
|
|
5100
|
+
|
|
5101
|
+
// ../core/src/suggestions.ts
|
|
5102
|
+
var SUGGESTION_SCHEMA = "bridge.suggestion/1";
|
|
5103
|
+
var SUGGESTION_LABEL = "bridge-suggestion";
|
|
5104
|
+
var SUGGESTION_KINDS = ["token-change", "docs", "component-spec", "brand", "revert", "config"];
|
|
5105
|
+
var SUGGESTION_STATE_LABELS = {
|
|
5106
|
+
"needs-review": "suggestion: needs review",
|
|
5107
|
+
"changes-requested": "suggestion: changes requested",
|
|
5108
|
+
approved: "suggestion: approved",
|
|
5109
|
+
applying: "suggestion: applying"
|
|
5110
|
+
};
|
|
5111
|
+
var SUGGESTION_LIMITS = {
|
|
5112
|
+
body: 65536,
|
|
5113
|
+
/** The readable part of the body (tables are cut with "and N more"). */
|
|
5114
|
+
human: 12e3,
|
|
5115
|
+
/** The JSON block in the body. */
|
|
5116
|
+
inline: 48e3,
|
|
5117
|
+
/** The JSON block of one continuation comment. */
|
|
5118
|
+
part: 6e4,
|
|
5119
|
+
/** Continuation comments at most (about 480 KB of file contents). */
|
|
5120
|
+
maxParts: 8
|
|
5121
|
+
};
|
|
5122
|
+
var SuggestionError = class extends Error {
|
|
5123
|
+
constructor(code2, message) {
|
|
5124
|
+
super(message);
|
|
5125
|
+
this.code = code2;
|
|
5126
|
+
this.name = "SuggestionError";
|
|
5127
|
+
}
|
|
5128
|
+
code;
|
|
5129
|
+
};
|
|
5130
|
+
var SUGGESTION_TOO_LARGE = "Too large for a suggestion: split it, or ask a developer to open it directly.";
|
|
5131
|
+
var utf8 = (text) => new TextEncoder().encode(text);
|
|
5132
|
+
var hex = (words) => words.map((w) => (w >>> 0).toString(16).padStart(8, "0")).join("");
|
|
5133
|
+
function padded(bytes) {
|
|
5134
|
+
const len = bytes.length;
|
|
5135
|
+
const total = Math.ceil((len + 9) / 64) * 64;
|
|
5136
|
+
const buf = new Uint8Array(total);
|
|
5137
|
+
buf.set(bytes);
|
|
5138
|
+
buf[len] = 128;
|
|
5139
|
+
const view = new DataView(buf.buffer);
|
|
5140
|
+
view.setUint32(total - 8, Math.floor(len / 536870912), false);
|
|
5141
|
+
view.setUint32(total - 4, len * 8 >>> 0, false);
|
|
5142
|
+
return view;
|
|
5143
|
+
}
|
|
5144
|
+
var K256 = [
|
|
5145
|
+
1116352408,
|
|
5146
|
+
1899447441,
|
|
5147
|
+
3049323471,
|
|
5148
|
+
3921009573,
|
|
5149
|
+
961987163,
|
|
5150
|
+
1508970993,
|
|
5151
|
+
2453635748,
|
|
5152
|
+
2870763221,
|
|
5153
|
+
3624381080,
|
|
5154
|
+
310598401,
|
|
5155
|
+
607225278,
|
|
5156
|
+
1426881987,
|
|
5157
|
+
1925078388,
|
|
5158
|
+
2162078206,
|
|
5159
|
+
2614888103,
|
|
5160
|
+
3248222580,
|
|
5161
|
+
3835390401,
|
|
5162
|
+
4022224774,
|
|
5163
|
+
264347078,
|
|
5164
|
+
604807628,
|
|
5165
|
+
770255983,
|
|
5166
|
+
1249150122,
|
|
5167
|
+
1555081692,
|
|
5168
|
+
1996064986,
|
|
5169
|
+
2554220882,
|
|
5170
|
+
2821834349,
|
|
5171
|
+
2952996808,
|
|
5172
|
+
3210313671,
|
|
5173
|
+
3336571891,
|
|
5174
|
+
3584528711,
|
|
5175
|
+
113926993,
|
|
5176
|
+
338241895,
|
|
5177
|
+
666307205,
|
|
5178
|
+
773529912,
|
|
5179
|
+
1294757372,
|
|
5180
|
+
1396182291,
|
|
5181
|
+
1695183700,
|
|
5182
|
+
1986661051,
|
|
5183
|
+
2177026350,
|
|
5184
|
+
2456956037,
|
|
5185
|
+
2730485921,
|
|
5186
|
+
2820302411,
|
|
5187
|
+
3259730800,
|
|
5188
|
+
3345764771,
|
|
5189
|
+
3516065817,
|
|
5190
|
+
3600352804,
|
|
5191
|
+
4094571909,
|
|
5192
|
+
275423344,
|
|
5193
|
+
430227734,
|
|
5194
|
+
506948616,
|
|
5195
|
+
659060556,
|
|
5196
|
+
883997877,
|
|
5197
|
+
958139571,
|
|
5198
|
+
1322822218,
|
|
5199
|
+
1537002063,
|
|
5200
|
+
1747873779,
|
|
5201
|
+
1955562222,
|
|
5202
|
+
2024104815,
|
|
5203
|
+
2227730452,
|
|
5204
|
+
2361852424,
|
|
5205
|
+
2428436474,
|
|
5206
|
+
2756734187,
|
|
5207
|
+
3204031479,
|
|
5208
|
+
3329325298
|
|
5209
|
+
];
|
|
5210
|
+
function sha256Hex(text) {
|
|
5211
|
+
const view = padded(utf8(text));
|
|
5212
|
+
const h = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225];
|
|
5213
|
+
const w = new Array(64);
|
|
5214
|
+
const rotr = (x, n) => x >>> n | x << 32 - n;
|
|
5215
|
+
for (let off = 0; off < view.byteLength; off += 64) {
|
|
5216
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4, false);
|
|
5217
|
+
for (let i = 16; i < 64; i++) {
|
|
5218
|
+
const a2 = w[i - 15];
|
|
5219
|
+
const b2 = w[i - 2];
|
|
5220
|
+
const s0 = rotr(a2, 7) ^ rotr(a2, 18) ^ a2 >>> 3;
|
|
5221
|
+
const s1 = rotr(b2, 17) ^ rotr(b2, 19) ^ b2 >>> 10;
|
|
5222
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 | 0;
|
|
5223
|
+
}
|
|
5224
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
5225
|
+
for (let i = 0; i < 64; i++) {
|
|
5226
|
+
const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K256[i] + w[i] | 0;
|
|
5227
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
|
|
5228
|
+
hh = g;
|
|
5229
|
+
g = f;
|
|
5230
|
+
f = e;
|
|
5231
|
+
e = d + t1 | 0;
|
|
5232
|
+
d = c;
|
|
5233
|
+
c = b;
|
|
5234
|
+
b = a;
|
|
5235
|
+
a = t1 + t2 | 0;
|
|
5236
|
+
}
|
|
5237
|
+
h[0] = h[0] + a | 0;
|
|
5238
|
+
h[1] = h[1] + b | 0;
|
|
5239
|
+
h[2] = h[2] + c | 0;
|
|
5240
|
+
h[3] = h[3] + d | 0;
|
|
5241
|
+
h[4] = h[4] + e | 0;
|
|
5242
|
+
h[5] = h[5] + f | 0;
|
|
5243
|
+
h[6] = h[6] + g | 0;
|
|
5244
|
+
h[7] = h[7] + hh | 0;
|
|
5245
|
+
}
|
|
5246
|
+
return hex(h);
|
|
5247
|
+
}
|
|
5248
|
+
function sha1Bytes(bytes) {
|
|
5249
|
+
const view = padded(bytes);
|
|
5250
|
+
const h = [1732584193, 4023233417, 2562383102, 271733878, 3285377520];
|
|
5251
|
+
const w = new Array(80);
|
|
5252
|
+
const rotl = (x, n) => x << n | x >>> 32 - n;
|
|
5253
|
+
for (let off = 0; off < view.byteLength; off += 64) {
|
|
5254
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4, false);
|
|
5255
|
+
for (let i = 16; i < 80; i++) w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
|
|
5256
|
+
let [a, b, c, d, e] = h;
|
|
5257
|
+
for (let i = 0; i < 80; i++) {
|
|
5258
|
+
const [f, k] = i < 20 ? [b & c | ~b & d, 1518500249] : i < 40 ? [b ^ c ^ d, 1859775393] : i < 60 ? [b & c | b & d | c & d, 2400959708] : [b ^ c ^ d, 3395469782];
|
|
5259
|
+
const t = rotl(a, 5) + f + e + k + w[i] | 0;
|
|
5260
|
+
e = d;
|
|
5261
|
+
d = c;
|
|
5262
|
+
c = rotl(b, 30);
|
|
5263
|
+
b = a;
|
|
5264
|
+
a = t;
|
|
5265
|
+
}
|
|
5266
|
+
h[0] = h[0] + a | 0;
|
|
5267
|
+
h[1] = h[1] + b | 0;
|
|
5268
|
+
h[2] = h[2] + c | 0;
|
|
5269
|
+
h[3] = h[3] + d | 0;
|
|
5270
|
+
h[4] = h[4] + e | 0;
|
|
5271
|
+
}
|
|
5272
|
+
return hex(h);
|
|
5273
|
+
}
|
|
5274
|
+
function gitBlobSha(text) {
|
|
5275
|
+
const body = utf8(text);
|
|
5276
|
+
const head = utf8(`blob ${body.length}\0`);
|
|
5277
|
+
const all = new Uint8Array(head.length + body.length);
|
|
5278
|
+
all.set(head);
|
|
5279
|
+
all.set(body, head.length);
|
|
5280
|
+
return sha1Bytes(all);
|
|
5281
|
+
}
|
|
5282
|
+
function canonicalJson(value) {
|
|
5283
|
+
return JSON.stringify(sortKeys(value));
|
|
5284
|
+
}
|
|
5285
|
+
function sortKeys(v) {
|
|
5286
|
+
if (Array.isArray(v)) return v.map((x) => x === void 0 ? null : sortKeys(x));
|
|
5287
|
+
if (v && typeof v === "object" && !(v.toJSON instanceof Function)) {
|
|
5288
|
+
const out = {};
|
|
5289
|
+
for (const k of Object.keys(v).sort()) {
|
|
5290
|
+
const x = v[k];
|
|
5291
|
+
if (x !== void 0) out[k] = sortKeys(x);
|
|
5292
|
+
}
|
|
5293
|
+
return out;
|
|
5294
|
+
}
|
|
5295
|
+
return v;
|
|
5296
|
+
}
|
|
5297
|
+
function shaForm(p) {
|
|
5298
|
+
if (!p.files) return p;
|
|
5299
|
+
return { ...p, files: p.files.map(({ part: _part, ...f }) => f) };
|
|
5300
|
+
}
|
|
5301
|
+
function suggestionPayloadSha(p) {
|
|
5302
|
+
return sha256Hex(canonicalJson(shaForm(p)));
|
|
5303
|
+
}
|
|
5304
|
+
var KIND_SUBJECT = {
|
|
5305
|
+
"token-change": "Tokens",
|
|
5306
|
+
docs: "Docs",
|
|
5307
|
+
"component-spec": "Component",
|
|
5308
|
+
brand: "Brand",
|
|
5309
|
+
revert: "Revert",
|
|
5310
|
+
config: "Settings"
|
|
5311
|
+
};
|
|
5312
|
+
function suggestionSubject(p) {
|
|
5313
|
+
if (p.kind === "component-spec" && p.componentSpec?.component) return p.componentSpec.component;
|
|
5314
|
+
if (p.kind === "docs" && p.docs?.length) {
|
|
5315
|
+
const names3 = [...new Set(p.docs.map((d) => d.component))];
|
|
5316
|
+
return names3.length === 1 ? names3[0] : KIND_SUBJECT.docs;
|
|
5317
|
+
}
|
|
5318
|
+
return KIND_SUBJECT[p.kind];
|
|
5319
|
+
}
|
|
5320
|
+
function suggestionIssueTitle(p) {
|
|
5321
|
+
const t = p.title.trim().replace(/\.$/, "");
|
|
5322
|
+
return `${suggestionSubject(p)}: ${t.charAt(0).toLowerCase()}${t.slice(1)}`;
|
|
5323
|
+
}
|
|
5324
|
+
function suggestionLabels(p, opts = {}) {
|
|
5325
|
+
return [
|
|
5326
|
+
.../* @__PURE__ */ new Set([
|
|
5327
|
+
SUGGESTION_LABEL,
|
|
5328
|
+
`suggestion: ${p.kind}`,
|
|
5329
|
+
`source: ${p.source}`,
|
|
5330
|
+
`tier: ${p.tier.tier}`,
|
|
5331
|
+
SUGGESTION_STATE_LABELS[opts.state ?? "needs-review"],
|
|
5332
|
+
...p.links?.neededBy ? [NEEDED_BY_LABEL] : [],
|
|
5333
|
+
...p.links?.figma || p.source === "figma" ? [FROM_FIGMA_LABEL] : [],
|
|
5334
|
+
...p.links?.exploration ? [EXPLORATION_LABEL] : [],
|
|
5335
|
+
...opts.labels ?? []
|
|
5336
|
+
])
|
|
5337
|
+
];
|
|
5338
|
+
}
|
|
5339
|
+
function suggestionStateFromLabels(labels) {
|
|
5340
|
+
return Object.entries(SUGGESTION_STATE_LABELS).find(([, l]) => labels.includes(l))?.[0];
|
|
5341
|
+
}
|
|
5342
|
+
function suggestionStatus(i) {
|
|
5343
|
+
const prs = i.linkedPrs ?? [];
|
|
5344
|
+
if (i.state === "closed") return prs.some((p) => p.merged) || i.stateReason === "completed" ? "applied" : "declined";
|
|
5345
|
+
if (prs.some((p) => p.state === "open") || i.applied) return "applying";
|
|
5346
|
+
if (i.outdated) return "outdated";
|
|
5347
|
+
return i.review ?? "needs-review";
|
|
5348
|
+
}
|
|
5349
|
+
var bridgeMcpInstallCommand = (repo) => `claude mcp add bridge -s user -- npx -y @bridge-ds/mcp --repo ${repo?.trim() || "<owner/name>"}`;
|
|
5350
|
+
var MARK3 = "bridge-suggestion";
|
|
5351
|
+
var PART_SCHEMA = "bridge.suggestion-part/1";
|
|
5352
|
+
var cell3 = (s) => s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
5353
|
+
var safeText = (s) => s.replace(/<!--/g, "<!--");
|
|
5354
|
+
var sourceLabel2 = (s) => s === "figma" ? "Figma" : s === "studio" ? "Bridge Studio" : s === "cli" ? "the CLI" : "Claude (MCP)";
|
|
5355
|
+
var show2 = (v) => v === void 0 ? "" : typeof v === "string" ? v : JSON.stringify(v);
|
|
5356
|
+
function table(head, rows, max) {
|
|
5357
|
+
if (!rows.length) return [];
|
|
5358
|
+
const shown = rows.slice(0, max);
|
|
5359
|
+
return [
|
|
5360
|
+
`| ${head.join(" | ")} |`,
|
|
5361
|
+
`| ${head.map(() => "---").join(" | ")} |`,
|
|
5362
|
+
...shown.map((r) => `| ${r.map((c) => cell3(c)).join(" | ")} |`),
|
|
5363
|
+
...rows.length > max ? ["", `and ${rows.length - max} more`] : []
|
|
5364
|
+
];
|
|
5365
|
+
}
|
|
5366
|
+
function changesSection(p, max) {
|
|
5367
|
+
const out = [];
|
|
5368
|
+
if (p.tokens?.changes.length) {
|
|
5369
|
+
const rows = p.tokens.changes.map((c) => [
|
|
5370
|
+
`\`${c.path}\`${c.kind === "renamed" && c.from ? ` (was \`${c.from}\`)` : c.kind === "added" ? " (new)" : c.kind === "removed" ? " (removed)" : ""}`,
|
|
5371
|
+
c.beforeCss ?? show2(c.before),
|
|
5372
|
+
c.afterCss ?? show2(c.after)
|
|
5373
|
+
]);
|
|
5374
|
+
out.push(...table(["Token", "Now", "Suggested"], rows, max), "");
|
|
5375
|
+
} else if (p.tokens?.edits.length) {
|
|
5376
|
+
const rows = p.tokens.edits.map((e) => [`\`${e.path}\``, e.op === "rename" ? `rename to \`${e.newPath ?? ""}\`` : e.op, show2(e.value)]);
|
|
5377
|
+
out.push(...table(["Token", "Edit", "Value"], rows, max), "");
|
|
5378
|
+
}
|
|
5379
|
+
if (p.docs?.length) out.push(...table(["Component", "Docs file"], p.docs.map((d) => [d.component, `\`${d.path}\``]), max), "");
|
|
5380
|
+
if (p.componentSpec) out.push(`**Component spec:** ${p.componentSpec.component} (\`${p.componentSpec.specPath}\`)`, "");
|
|
5381
|
+
if (p.files?.length) {
|
|
5382
|
+
const rows = p.files.map((f) => [`\`${f.path}\``, f.beforeSha === null ? "new" : f.afterSha === null ? "deleted" : "changed"]);
|
|
5383
|
+
out.push(...table(["File", "Change"], rows, max), "");
|
|
5384
|
+
}
|
|
5385
|
+
return out;
|
|
5386
|
+
}
|
|
5387
|
+
function checksSection(p, max) {
|
|
5388
|
+
const c = p.checks;
|
|
5389
|
+
const failing = c.contrast.filter((x) => !x.pass);
|
|
5390
|
+
const rows = [...failing, ...c.contrast.filter((x) => x.pass)].map((x) => [x.pair, x.mode ?? "", x.before.toFixed(2), x.after.toFixed(2), String(x.min), x.pass ? "pass" : "**fails**"]);
|
|
5391
|
+
return [
|
|
5392
|
+
"## Checks when suggested",
|
|
5393
|
+
"",
|
|
5394
|
+
`- Token validation: ${c.newErrors ? `**${c.newErrors} new error(s)**` : "no new errors"}`,
|
|
5395
|
+
`- Contrast: ${c.contrast.length ? failing.length ? `**${failing.length} failing pair(s)**` : `${c.contrast.length} pair(s) checked, all pass` : "no pair affected"}`,
|
|
5396
|
+
`- Risk tier: ${p.tier.tier} (\`${p.tier.bump}\`), ${p.tier.tier === 1 ? "one approval" : "design and tech approval"}`,
|
|
5397
|
+
`- Impact: ${c.impact ? `${c.impact} token(s) resolve to a new value` : "no other token changes value"}`,
|
|
5398
|
+
"",
|
|
5399
|
+
...rows.length ? [...table(["Pair", "Mode", "Before", "After", "Min", "Result"], rows, max), ""] : []
|
|
5400
|
+
];
|
|
5401
|
+
}
|
|
5402
|
+
function linksSection(p) {
|
|
5403
|
+
const l = p.links ?? {};
|
|
5404
|
+
const out = [];
|
|
5405
|
+
if (l.neededBy) out.push(`- Needed by ${l.neededBy.productName}: [#${l.neededBy.pr} ${cell3(l.neededBy.title)}](${l.neededBy.url})`);
|
|
5406
|
+
if (l.figma?.url) out.push(`- Design: [Open in Figma](${l.figma.url})`);
|
|
5407
|
+
if (l.exploration) out.push(`- Exploration: \`${l.exploration}\``);
|
|
5408
|
+
if (l.supersedes) out.push(`- Replaces suggestion #${l.supersedes}`);
|
|
5409
|
+
for (const v of p.previews ?? []) out.push(`- ${cell3(v.label)}: ${v.url}`);
|
|
5410
|
+
return out.length ? ["## Previews and links", "", ...out, ""] : [];
|
|
5411
|
+
}
|
|
5412
|
+
function humanPart(p, details, max, repo) {
|
|
5413
|
+
return [
|
|
5414
|
+
`## Suggested change: ${safeText(p.title.trim())}`,
|
|
5415
|
+
"",
|
|
5416
|
+
safeText(p.summary.trim()),
|
|
5417
|
+
"",
|
|
5418
|
+
...changesSection(p, max),
|
|
5419
|
+
...details?.trim() ? [safeText(details.trim()), ""] : [],
|
|
5420
|
+
...checksSection(p, max),
|
|
5421
|
+
...linksSection(p),
|
|
5422
|
+
"## How to apply",
|
|
5423
|
+
"",
|
|
5424
|
+
"- CLI: `bridge apply <this issue number>` (add `--dry-run` to check it first, `--patch` for a patch file)",
|
|
5425
|
+
'- Claude Code with the Bridge MCP server: ask "Apply Bridge suggestion #<this issue number>" (tool `bridge_apply_suggestion`).',
|
|
5426
|
+
` One-time install: \`${bridgeMcpInstallCommand(repo)}\``,
|
|
5427
|
+
"",
|
|
5428
|
+
"The pull request that applies it must say `Closes #<this issue number>`, so this suggestion closes when it merges.",
|
|
5429
|
+
"",
|
|
5430
|
+
`<sub>Suggested from ${sourceLabel2(p.source)}${p.suggestedBy ? ` by @${p.suggestedBy.replace(/^@/, "")}` : ""} with Bridge.</sub>`
|
|
5431
|
+
].join("\n");
|
|
5432
|
+
}
|
|
5433
|
+
function fitHuman(p, details, repo) {
|
|
5434
|
+
for (const max of [50, 20, 8, 3]) {
|
|
5435
|
+
const h2 = humanPart(p, details, max, repo);
|
|
5436
|
+
if (h2.length <= SUGGESTION_LIMITS.human) return h2;
|
|
5437
|
+
}
|
|
5438
|
+
const h = humanPart(p, void 0, 3, repo);
|
|
5439
|
+
if (h.length <= SUGGESTION_LIMITS.human) return h;
|
|
5440
|
+
const note = "\n\n_The description is cut here: the full change is in the machine-readable block below._";
|
|
5441
|
+
return h.slice(0, SUGGESTION_LIMITS.human - note.length) + note;
|
|
5442
|
+
}
|
|
5443
|
+
var pretty = (v) => JSON.stringify(sortKeys(v), null, 2);
|
|
5444
|
+
var fit = (v) => {
|
|
5445
|
+
const p = pretty(v);
|
|
5446
|
+
return p.length <= SUGGESTION_LIMITS.inline ? p : canonicalJson(v);
|
|
5447
|
+
};
|
|
5448
|
+
var fenced = (json) => ["```json", json, "```"].join("\n");
|
|
5449
|
+
function payloadBlock(json, sha, parts) {
|
|
5450
|
+
return [
|
|
5451
|
+
"<details><summary>Machine-readable change (Bridge suggestion v1)</summary>",
|
|
5452
|
+
"",
|
|
5453
|
+
`<!-- ${MARK3}:v1 sha256=${sha}${parts ? ` parts=${parts}` : ""} -->`,
|
|
5454
|
+
fenced(json),
|
|
5455
|
+
"",
|
|
5456
|
+
"</details>"
|
|
5457
|
+
].join("\n");
|
|
5458
|
+
}
|
|
5459
|
+
function partComment(i, n, json) {
|
|
5460
|
+
return [
|
|
5461
|
+
`<details><summary>Machine-readable change, part ${i} of ${n} (Bridge suggestion v1)</summary>`,
|
|
5462
|
+
"",
|
|
5463
|
+
`<!-- ${MARK3}:v1 part ${i}/${n} sha256=${sha256Hex(json)} -->`,
|
|
5464
|
+
fenced(json),
|
|
5465
|
+
"",
|
|
5466
|
+
"</details>"
|
|
5467
|
+
].join("\n");
|
|
5468
|
+
}
|
|
5469
|
+
function escapedEnd(s, from, budget) {
|
|
5470
|
+
let used = 0;
|
|
5471
|
+
let i = from;
|
|
5472
|
+
while (i < s.length) {
|
|
5473
|
+
const c = s.charCodeAt(i);
|
|
5474
|
+
let w = 1;
|
|
5475
|
+
let len = 1;
|
|
5476
|
+
if (c === 34 || c === 92 || c === 8 || c === 12 || c === 10 || c === 13 || c === 9) len = 2;
|
|
5477
|
+
else if (c < 32) len = 6;
|
|
5478
|
+
else if (c >= 55296 && c <= 56319) {
|
|
5479
|
+
const d = s.charCodeAt(i + 1);
|
|
5480
|
+
if (d >= 56320 && d <= 57343) {
|
|
5481
|
+
w = 2;
|
|
5482
|
+
len = 2;
|
|
5483
|
+
} else len = 6;
|
|
5484
|
+
} else if (c >= 56320 && c <= 57343) len = 6;
|
|
5485
|
+
if (used + len > budget) break;
|
|
5486
|
+
used += len;
|
|
5487
|
+
i += w;
|
|
5488
|
+
}
|
|
5489
|
+
return i;
|
|
5490
|
+
}
|
|
5491
|
+
function composeSuggestionIssue(payload, opts = {}) {
|
|
5492
|
+
const problems = suggestionShapeProblems(payload);
|
|
5493
|
+
if (problems.length) throw new SuggestionError("invalid", problems.join(" "));
|
|
5494
|
+
if (payload.checks.blocked) throw new SuggestionError("invalid", `This change cannot be suggested: ${payload.checks.blocked}`);
|
|
5495
|
+
const full = shaForm(payload);
|
|
5496
|
+
const sha = suggestionPayloadSha(full);
|
|
5497
|
+
const human = fitHuman(full, opts.details, opts.repo);
|
|
5498
|
+
const base = { title: suggestionIssueTitle(full), labels: suggestionLabels(full, { state: opts.state, labels: opts.labels }), assignees: opts.assignees ?? [], sha };
|
|
5499
|
+
const inline = fit(full);
|
|
5500
|
+
if (inline.length <= SUGGESTION_LIMITS.inline) {
|
|
5501
|
+
return { ...base, body: `${human}
|
|
5502
|
+
|
|
5503
|
+
${payloadBlock(inline, sha, 0)}
|
|
5504
|
+
`, parts: [] };
|
|
5505
|
+
}
|
|
5506
|
+
const moved = (full.files ?? []).map((f, i) => ({ f, i })).filter(({ f }) => typeof f.content === "string" && f.content.length > 0);
|
|
5507
|
+
const parts = [];
|
|
5508
|
+
const firstPart = /* @__PURE__ */ new Map();
|
|
5509
|
+
const partJson = (chunks, i, n2) => pretty({ schema: PART_SCHEMA, part: i, of: n2, files: chunks });
|
|
5510
|
+
const emptyOverhead = partJson([], 9, 9).length;
|
|
5511
|
+
let cur = [];
|
|
5512
|
+
let room = SUGGESTION_LIMITS.part - emptyOverhead - 64;
|
|
5513
|
+
for (const { f, i } of moved) {
|
|
5514
|
+
const text = f.content;
|
|
5515
|
+
let at = 0;
|
|
5516
|
+
while (at < text.length) {
|
|
5517
|
+
const entryOverhead = partJson([{ i, path: f.path, text: "" }], 9, 9).length - emptyOverhead + 8;
|
|
5518
|
+
if (room - entryOverhead < 256) {
|
|
5519
|
+
parts.push(cur);
|
|
5520
|
+
cur = [];
|
|
5521
|
+
room = SUGGESTION_LIMITS.part - emptyOverhead - 64;
|
|
5522
|
+
}
|
|
5523
|
+
const end = escapedEnd(text, at, room - entryOverhead);
|
|
5524
|
+
if (end === at) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
5525
|
+
if (!firstPart.has(i)) firstPart.set(i, parts.length + 1);
|
|
5526
|
+
const chunk = { i, path: f.path, text: text.slice(at, end) };
|
|
5527
|
+
cur.push(chunk);
|
|
5528
|
+
room -= partJson([chunk], 9, 9).length - emptyOverhead + 8;
|
|
5529
|
+
at = end;
|
|
5530
|
+
}
|
|
5531
|
+
}
|
|
5532
|
+
if (cur.length) parts.push(cur);
|
|
5533
|
+
if (parts.length > SUGGESTION_LIMITS.maxParts) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
5534
|
+
const skeleton = {
|
|
5535
|
+
...full,
|
|
5536
|
+
files: (full.files ?? []).map((f, i) => {
|
|
5537
|
+
if (!firstPart.has(i)) return f;
|
|
5538
|
+
const { content: _content, ...rest } = f;
|
|
5539
|
+
return { ...rest, part: firstPart.get(i) };
|
|
5540
|
+
})
|
|
5541
|
+
};
|
|
5542
|
+
const skeletonJson = fit(skeleton);
|
|
5543
|
+
if (skeletonJson.length > SUGGESTION_LIMITS.inline) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
5544
|
+
const n = parts.length;
|
|
5545
|
+
const partBodies = parts.map((chunks, k) => {
|
|
5546
|
+
const json = partJson(chunks, k + 1, n);
|
|
5547
|
+
if (json.length > SUGGESTION_LIMITS.part) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
5548
|
+
return partComment(k + 1, n, json);
|
|
5549
|
+
});
|
|
5550
|
+
const body = `${human}
|
|
5551
|
+
|
|
5552
|
+
${payloadBlock(skeletonJson, sha, n)}
|
|
5553
|
+
`;
|
|
5554
|
+
if (body.length > SUGGESTION_LIMITS.body || partBodies.some((b) => b.length > SUGGESTION_LIMITS.body)) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
5555
|
+
return { ...base, body, parts: partBodies };
|
|
5556
|
+
}
|
|
5557
|
+
function suggestionShapeProblems(raw) {
|
|
5558
|
+
const out = [];
|
|
5559
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return ["The payload must be a JSON object."];
|
|
5560
|
+
const p = raw;
|
|
5561
|
+
if (p.schema !== SUGGESTION_SCHEMA) out.push(`"schema" must be "${SUGGESTION_SCHEMA}".`);
|
|
5562
|
+
if (!SUGGESTION_KINDS.includes(p.kind)) out.push(`"kind" must be one of ${SUGGESTION_KINDS.join(", ")}.`);
|
|
5563
|
+
if (typeof p.title !== "string" || !p.title.trim()) out.push('"title" must be a sentence.');
|
|
5564
|
+
if (typeof p.summary !== "string") out.push('"summary" must be text.');
|
|
5565
|
+
if (!["figma", "studio", "cli", "mcp"].includes(p.source)) out.push('"source" must be figma, studio, cli or mcp.');
|
|
5566
|
+
if (typeof p.createdAt !== "string") out.push('"createdAt" must be an ISO date.');
|
|
5567
|
+
const base = p.base;
|
|
5568
|
+
if (!base || typeof base.ref !== "string" || typeof base.sha !== "string" || !/^[0-9a-f]{7,64}$/.test(base.sha)) out.push('"base" must be { "ref", "sha" } with the commit the change was planned on.');
|
|
5569
|
+
const tier = p.tier;
|
|
5570
|
+
if (!tier || ![1, 2, 3].includes(tier.tier) || !["patch", "minor", "major"].includes(tier.bump)) out.push('"tier" must be { "tier": 1, 2 or 3, "bump": patch, minor or major }.');
|
|
5571
|
+
const checks = p.checks;
|
|
5572
|
+
if (!checks || typeof checks.newErrors !== "number" || !Array.isArray(checks.contrast) || typeof checks.impact !== "number" || !(checks.blocked === null || typeof checks.blocked === "string"))
|
|
5573
|
+
out.push('"checks" must be { "newErrors", "contrast", "blocked", "impact" }.');
|
|
5574
|
+
const tokens = p.tokens;
|
|
5575
|
+
if (tokens !== void 0 && (!tokens || !Array.isArray(tokens.edits) || !Array.isArray(tokens.changes))) out.push('"tokens" must be { "edits", "changes" }.');
|
|
5576
|
+
if (p.docs !== void 0 && !(Array.isArray(p.docs) && p.docs.every((d) => d && typeof d.component === "string" && typeof d.path === "string"))) out.push('"docs" must be a list of { "component", "path", "after" }.');
|
|
5577
|
+
const spec = p.componentSpec;
|
|
5578
|
+
if (spec !== void 0 && (!spec || typeof spec.component !== "string" || typeof spec.specPath !== "string" || typeof spec.brief !== "string")) out.push('"componentSpec" must be { "component", "specPath", "spec", "brief" }.');
|
|
5579
|
+
if (p.files !== void 0) {
|
|
5580
|
+
if (!Array.isArray(p.files)) out.push('"files" must be a list.');
|
|
5581
|
+
else {
|
|
5582
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5583
|
+
for (const f of p.files) {
|
|
5584
|
+
const ok2 = f && typeof f.path === "string" && (f.beforeSha === null || typeof f.beforeSha === "string") && (f.afterSha === null || typeof f.afterSha === "string") && (f.content === void 0 || f.content === null || typeof f.content === "string") && (f.part === void 0 || Number.isInteger(f.part) && f.part >= 1 && f.part <= SUGGESTION_LIMITS.maxParts);
|
|
5585
|
+
if (!ok2) {
|
|
5586
|
+
out.push('Each of "files" must be { "path", "beforeSha", "afterSha", "content"? }.');
|
|
5587
|
+
break;
|
|
5588
|
+
}
|
|
5589
|
+
if (seen.has(f.path)) out.push(`"${f.path}" is listed twice in "files".`);
|
|
5590
|
+
seen.add(f.path);
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
return out;
|
|
5595
|
+
}
|
|
5596
|
+
|
|
5597
|
+
// ../core/src/suggestions-github.ts
|
|
5598
|
+
async function createSuggestionOnGitHub(gh, payload, opts = {}) {
|
|
5599
|
+
const issue = composeSuggestionIssue(payload, { repo: gh.fullName, ...opts });
|
|
5600
|
+
const created = await gh.createIssue({ title: issue.title, body: issue.body, labels: issue.labels, assignees: issue.assignees });
|
|
5601
|
+
for (const [i, part] of issue.parts.entries()) {
|
|
5602
|
+
try {
|
|
5603
|
+
await gh.createIssueComment(created.number, part);
|
|
5604
|
+
} catch (err) {
|
|
5605
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
5606
|
+
throw new GitHubError(err instanceof GitHubError ? err.status : 500, `Suggestion #${created.number} is incomplete: part ${i + 1} of ${issue.parts.length} could not be posted (${why}). Send it again, then close #${created.number}.`);
|
|
5607
|
+
}
|
|
5608
|
+
}
|
|
5609
|
+
const warnings = [];
|
|
5610
|
+
const missing = issue.labels.filter((l) => !created.labels.includes(l));
|
|
5611
|
+
if (created.labels.length && missing.length) warnings.push(`GitHub did not keep these labels on #${created.number}: ${missing.join(", ")}.`);
|
|
5612
|
+
return { number: created.number, url: created.url, sha: issue.sha, parts: issue.parts.length, warnings };
|
|
5613
|
+
}
|
|
5614
|
+
|
|
4606
5615
|
// ../core/src/docs-proposal.ts
|
|
4607
5616
|
var DOCS_EDITOR_ORIGIN = {
|
|
4608
5617
|
where: "in the Bridge docs editor",
|
|
@@ -4687,10 +5696,10 @@ async function continueDocsProposalOnGitHub(gh, open, edit, opts = {}) {
|
|
|
4687
5696
|
function docsProposal(edits, opts) {
|
|
4688
5697
|
const origin = opts.origin ?? DOCS_EDITOR_ORIGIN;
|
|
4689
5698
|
if (!edits.length) throw new Error("No docs edits in the draft.");
|
|
4690
|
-
const
|
|
4691
|
-
const list =
|
|
4692
|
-
const title = opts.title?.trim() || `docs(${
|
|
4693
|
-
const slug2 = docsSlug(
|
|
5699
|
+
const names3 = edits.map((e) => e.component);
|
|
5700
|
+
const list = names3.length === 1 ? names3[0] : `${names3.slice(0, -1).join(", ")} and ${names3[names3.length - 1]}`;
|
|
5701
|
+
const title = opts.title?.trim() || `docs(${names3.length === 1 ? edits[0].slug : "components"}): usage guidance for ${list}`;
|
|
5702
|
+
const slug2 = docsSlug(names3.length === 1 ? edits[0].slug : `${edits[0].slug}-and-${names3.length - 1}-more`);
|
|
4694
5703
|
const branch = `docs/${slug2}-${slugDate(opts.date)}-${opts.date.getTime().toString(36).slice(-4)}`;
|
|
4695
5704
|
const csPath = `${opts.changesetDir ?? ".changeset"}/docs-${slug2}-${opts.date.getTime().toString(36)}.md`;
|
|
4696
5705
|
const pkg = opts.packageName ?? "@bridge-ds/react";
|
|
@@ -4731,13 +5740,60 @@ ${opts.notes.trim()}
|
|
|
4731
5740
|
].filter((x, i, a) => !(x === "" && a[i - 1] === "")).join("\n");
|
|
4732
5741
|
return { branch, title, body, files, risk, bump: "patch", labels: ["docs", origin.label, `tier-${risk.tier}`] };
|
|
4733
5742
|
}
|
|
5743
|
+
var docsSource = (origin) => origin.label === DOCS_MCP_ORIGIN.label ? "mcp" : "studio";
|
|
5744
|
+
function docsSuggestionPayload(edits, opts) {
|
|
5745
|
+
const origin = opts.origin ?? DOCS_EDITOR_ORIGIN;
|
|
5746
|
+
const date = opts.date ?? /* @__PURE__ */ new Date();
|
|
5747
|
+
const p = docsProposal(edits, { title: opts.title, date, origin, packageName: opts.packageName, changesetDir: opts.changesetDir });
|
|
5748
|
+
const names3 = edits.map((e) => e.component);
|
|
5749
|
+
const list = names3.length === 1 ? names3[0] : `${names3.slice(0, -1).join(", ")} and ${names3[names3.length - 1]}`;
|
|
5750
|
+
const docs = edits.map((e) => {
|
|
5751
|
+
let after;
|
|
5752
|
+
try {
|
|
5753
|
+
after = JSON.parse(e.after);
|
|
5754
|
+
} catch {
|
|
5755
|
+
throw new Error(`${e.path} is not valid JSON: fix it before sending.`);
|
|
5756
|
+
}
|
|
5757
|
+
return { component: e.component, path: e.path, after: cleanDocs(after) };
|
|
5758
|
+
});
|
|
5759
|
+
const docPaths = new Set(edits.map((e) => e.path));
|
|
5760
|
+
const files = p.files.filter((f) => !docPaths.has(f.path)).map((f) => ({ path: f.path, beforeSha: null, afterSha: gitBlobSha(f.content), content: f.content }));
|
|
5761
|
+
const by = opts.suggestedBy?.replace(/^@/, "");
|
|
5762
|
+
return {
|
|
5763
|
+
schema: SUGGESTION_SCHEMA,
|
|
5764
|
+
kind: "docs",
|
|
5765
|
+
title: opts.title?.trim() || (names3.length === 1 ? "Usage guidance" : `Usage guidance for ${list}`),
|
|
5766
|
+
summary: [`Usage guidance for ${list}, written ${origin.where}.`, opts.notes?.trim() ?? ""].filter(Boolean).join("\n\n"),
|
|
5767
|
+
source: opts.source ?? docsSource(origin),
|
|
5768
|
+
...by ? { suggestedBy: by } : {},
|
|
5769
|
+
createdAt: date.toISOString(),
|
|
5770
|
+
base: opts.base,
|
|
5771
|
+
tier: { tier: p.risk.tier, bump: "patch" },
|
|
5772
|
+
docs,
|
|
5773
|
+
files,
|
|
5774
|
+
checks: { newErrors: 0, contrast: [], blocked: null, impact: 0 }
|
|
5775
|
+
};
|
|
5776
|
+
}
|
|
5777
|
+
async function suggestDocsOnGitHub(gh, edits, opts) {
|
|
5778
|
+
if (!edits.length) throw new Error("No docs edits in the draft.");
|
|
5779
|
+
const sha = opts.baseSha ?? await gh.getBranchSha(opts.base);
|
|
5780
|
+
if (!sha) throw new Error(`Branch ${opts.base} not found.`);
|
|
5781
|
+
const origin = opts.origin ?? DOCS_EDITOR_ORIGIN;
|
|
5782
|
+
const payload = docsSuggestionPayload(edits, { base: { ref: opts.base, sha }, title: opts.title, notes: opts.notes, date: opts.date, origin, source: opts.source, suggestedBy: opts.suggestedBy });
|
|
5783
|
+
const p = docsProposal(edits, { date: opts.date ?? /* @__PURE__ */ new Date(), origin });
|
|
5784
|
+
const rows = p.body.split("\n").filter((l) => l.startsWith("- `"));
|
|
5785
|
+
const details = [...rows, "", opts.authorLine?.trim() || defaultDocsFooter(edits, origin)].join("\n");
|
|
5786
|
+
const created = await createSuggestionOnGitHub(gh, payload, { labels: ["docs", ...opts.extraLabels ?? []], details });
|
|
5787
|
+
return { status: "suggested", number: created.number, url: created.url, title: payload.title, sha: created.sha, warnings: created.warnings };
|
|
5788
|
+
}
|
|
4734
5789
|
async function proposeDocsOnGitHub(gh, edits, opts) {
|
|
5790
|
+
if (opts.mode === "suggest") return suggestDocsOnGitHub(gh, edits, opts);
|
|
4735
5791
|
if (opts.continueOpen !== false && edits.length === 1) {
|
|
4736
5792
|
const e = edits[0];
|
|
4737
5793
|
const open = await findOpenDocsProposalOnGitHub(gh, { name: e.component, slug: e.slug }, opts.base).catch(() => null);
|
|
4738
5794
|
if (open) {
|
|
4739
5795
|
const sha = await continueDocsProposalOnGitHub(gh, open, e, { authorLine: opts.authorLine?.trim() || defaultDocsFooter(edits, opts.origin ?? DOCS_EDITOR_ORIGIN) });
|
|
4740
|
-
return { number: open.number, url: open.url, branch: open.branch, title: open.title, continued: true, unchanged: !sha };
|
|
5796
|
+
return { status: "continued", number: open.number, url: open.url, branch: open.branch, title: open.title, continued: true, unchanged: !sha };
|
|
4741
5797
|
}
|
|
4742
5798
|
}
|
|
4743
5799
|
const p = docsProposal(edits, { title: opts.title, notes: opts.notes, date: opts.date ?? /* @__PURE__ */ new Date(), origin: opts.origin, authorLine: opts.authorLine });
|
|
@@ -4752,7 +5808,7 @@ ${opts.commitNote ?? "Proposed with the Bridge docs editor."}`, tree, [mainSha])
|
|
|
4752
5808
|
await gh.createBranch(branch, commit);
|
|
4753
5809
|
const pr = await gh.createPullRequest({ title: prTitle, head: branch, base: opts.base, body: p.body, draft: false });
|
|
4754
5810
|
await gh.addLabels(pr.number, [...p.labels, ...opts.extraLabels ?? []]).catch(() => false);
|
|
4755
|
-
return { number: pr.number, url: pr.url, branch, title: prTitle, continued: false, unchanged: false };
|
|
5811
|
+
return { status: "opened", number: pr.number, url: pr.url, branch, title: prTitle, continued: false, unchanged: false };
|
|
4756
5812
|
}
|
|
4757
5813
|
async function commitFileOnGitHub(gh, input) {
|
|
4758
5814
|
const cur = await gh.getFile(input.path, input.branch);
|
|
@@ -4873,9 +5929,9 @@ var COMBINE_COMPARE_LIMIT = 300;
|
|
|
4873
5929
|
async function readCombineBranch(gh, pull, opts) {
|
|
4874
5930
|
const compare = await gh.compareDetailed(opts.base, pull.headSha);
|
|
4875
5931
|
const files = compare.files;
|
|
4876
|
-
const
|
|
5932
|
+
const names3 = files.flatMap((f) => f.previous_filename ? [f.filename, f.previous_filename] : [f.filename]);
|
|
4877
5933
|
const start = compare.mergeBase;
|
|
4878
|
-
if (!start || files.length >= COMBINE_COMPARE_LIMIT || !onlyTokens(
|
|
5934
|
+
if (!start || files.length >= COMBINE_COMPARE_LIMIT || !onlyTokens(names3, opts.tokensRoot)) return { number: pull.number, title: pull.title, tokens: false, edits: null };
|
|
4879
5935
|
const root = opts.tokensRoot.replace(/^\/+|\/+$/g, "");
|
|
4880
5936
|
const tokenFiles = files.filter((f) => tokenFilesOf([f.filename], root).length);
|
|
4881
5937
|
const source = { number: pull.number, base: {}, head: {}, files: tokenFiles.map((f) => tokenFilesOf([f.filename], root)[0]) };
|
|
@@ -4889,6 +5945,350 @@ async function readCombineBranch(gh, pull, opts) {
|
|
|
4889
5945
|
return { number: pull.number, title: pull.title, tokens: true, edits: changeEdits(source) };
|
|
4890
5946
|
}
|
|
4891
5947
|
|
|
5948
|
+
// ../core/src/prod-branch-github.ts
|
|
5949
|
+
var PRODUCT_FILE = /\.(css|scss|sass|less|tsx|jsx|ts|js|mjs|cjs|mts|cts|astro|vue|svelte)$/;
|
|
5950
|
+
var SKIP_FILE = /\.d\.[cm]?ts$|\.(test|spec|stories|story)\.[cm]?[jt]sx?$|\.min\.(css|js)$/;
|
|
5951
|
+
var PIECES_FILE_LIMIT = 40;
|
|
5952
|
+
var cleanPath = (p) => p.trim().replace(/^\.?\/+|\/+$/g, "");
|
|
5953
|
+
function productOf(products, pull, files) {
|
|
5954
|
+
const product = productOfBranch(products, { labels: pull.labels, files });
|
|
5955
|
+
if (product.key === defaultProduct(products).key) throw new GitHubError(422, `This branch is a library branch (${product.name}), not a product branch: missing pieces are checked on a branch started for a product.`);
|
|
5956
|
+
return product;
|
|
5957
|
+
}
|
|
5958
|
+
async function readProductBranchOnGitHub(gh, pr, opts) {
|
|
5959
|
+
const pull = await gh.getPullRequest(pr);
|
|
5960
|
+
const cmp = await gh.compareDetailed(pull.baseRef, pull.headSha);
|
|
5961
|
+
const product = productOf(opts.products, pull, cmp.files.map((f) => f.filename));
|
|
5962
|
+
const dir = product.path ? `${cleanPath(product.path)}/` : "";
|
|
5963
|
+
const changed = cmp.files.filter((f) => f.status !== "removed" && (!dir || f.filename.startsWith(dir)) && PRODUCT_FILE.test(f.filename) && !SKIP_FILE.test(f.filename) && !f.filename.split("/").includes("node_modules"));
|
|
5964
|
+
const read = changed.slice(0, PIECES_FILE_LIMIT);
|
|
5965
|
+
const start = cmp.mergeBase ?? pull.baseRef;
|
|
5966
|
+
const files = await Promise.all(
|
|
5967
|
+
read.map(async (f) => {
|
|
5968
|
+
const [before, after] = await Promise.all([f.status === "added" ? Promise.resolve(void 0) : gh.getFileText(f.previous_filename ?? f.filename, start), gh.getFileText(f.filename, pull.headSha)]);
|
|
5969
|
+
return { path: f.filename, before: before ?? null, after: after ?? null };
|
|
5970
|
+
})
|
|
5971
|
+
);
|
|
5972
|
+
return { pull, product, files, skipped: changed.length - read.length, record: parsePiecesRecord(pull.body) };
|
|
5973
|
+
}
|
|
5974
|
+
async function recordPiecesOnGitHub(gh, pr, check) {
|
|
5975
|
+
const pull = await gh.getPullRequest(pr);
|
|
5976
|
+
const record = mergePiecesRecord(parsePiecesRecord(pull.body), { checkedAt: check.checkedAt ?? (/* @__PURE__ */ new Date()).toISOString(), headSha: check.headSha ?? pull.headSha, pieces: check.pieces, fixes: check.fixes });
|
|
5977
|
+
await gh.updatePullRequest(pr, { body: withPiecesRecord(pull.body, record) });
|
|
5978
|
+
return record;
|
|
5979
|
+
}
|
|
5980
|
+
var productWriter = (gh, branch) => isExplorationBranch(branch) ? gh.withBranchPrefix(EXPLORATION_PREFIX) : gh;
|
|
5981
|
+
async function useLibraryPreviewOnGitHub(gh, input) {
|
|
5982
|
+
const message = `chore(bridge): use the preview of ${input.libraryBranch} (#${input.libraryNumber})
|
|
5983
|
+
|
|
5984
|
+
The product branch takes the library branch before it ships. Switch to the published version once it does.`;
|
|
5985
|
+
try {
|
|
5986
|
+
const r = await productWriter(gh, input.productBranch).mergeIntoBranch(input.productBranch, input.libraryBranch, message);
|
|
5987
|
+
return { ...r, said: r.status === "merged" ? "Your branch now uses the preview of the library branch." : "Your branch already has the latest preview." };
|
|
5988
|
+
} catch (err) {
|
|
5989
|
+
if (err instanceof GitHubError && err.status === 409) return { status: "conflict", sha: null, said: `Your branch and the library branch change the same lines. A developer merges ${input.libraryBranch} into ${input.productBranch} by hand.` };
|
|
5990
|
+
throw err;
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5993
|
+
function productBranchRef(pull, product) {
|
|
5994
|
+
const name = /<!-- bridge-exploration (\{[\s\S]*?\}) -->/.exec(pull.body ?? "");
|
|
5995
|
+
let title = pull.title.replace(/^[A-Z][A-Z0-9_]*-\d+\s+/, "").replace(/^Exploration:\s*/i, "");
|
|
5996
|
+
let author = pull.author;
|
|
5997
|
+
if (name) {
|
|
5998
|
+
try {
|
|
5999
|
+
const e = JSON.parse(name[1]);
|
|
6000
|
+
if (e.name) title = e.name;
|
|
6001
|
+
if (e.author) author = e.author;
|
|
6002
|
+
} catch {
|
|
6003
|
+
}
|
|
6004
|
+
}
|
|
6005
|
+
return { pr: pull.number, title: title.charAt(0).toUpperCase() + title.slice(1), branch: pull.headRef, url: pull.url, productKey: product.key, productName: product.name, ...author ? { author } : {} };
|
|
6006
|
+
}
|
|
6007
|
+
async function openLibraryBranchOnGitHub(gh, input) {
|
|
6008
|
+
const pull = await gh.getPullRequest(input.pr);
|
|
6009
|
+
const record = parsePiecesRecord(pull.body);
|
|
6010
|
+
const piece = record?.pieces.find((p) => p.id === input.piece);
|
|
6011
|
+
if (!record || !piece) throw new GitHubError(404, record ? `No piece "${input.piece}" on #${input.pr}. Pieces: ${record.pieces.map((p) => p.id).join(", ") || "none"}.` : `#${input.pr} was not checked yet: find its missing pieces first (bridge_missing_pieces with record, or \`bridge pieces ${input.pr} --record\`).`);
|
|
6012
|
+
const product = productOf(input.products, pull, await gh.listPullRequestFiles(input.pr).then((f) => f.map((x) => x.filename), () => []));
|
|
6013
|
+
const mode = input.mode ?? "direct";
|
|
6014
|
+
const plan = libraryBranchPlan(piece, productBranchRef(pull, product), { source: input.source, requestedBy: input.openedBy, mode });
|
|
6015
|
+
const existing = record.links.find((l) => l.piece === piece.id);
|
|
6016
|
+
if (existing) return { status: "already-linked", plan, link: existing, preview: null, warnings: [] };
|
|
6017
|
+
if (input.dryRun) return { status: "dry-run", plan, link: null, preview: null, warnings: [] };
|
|
6018
|
+
if (!gh.canWrite) throw new GitHubError(401, mode === "suggest" ? "Suggesting a library change needs a GitHub token (issues: write)." : "Opening a library branch needs a GitHub token (contents, pull requests and issues: write).");
|
|
6019
|
+
const warnings = [];
|
|
6020
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
6021
|
+
const who = { ...input.openedBy ? { openedBy: input.openedBy } : {}, ...input.agent ? { agent: input.agent } : {} };
|
|
6022
|
+
let link;
|
|
6023
|
+
if (plan.kind === "token-change") {
|
|
6024
|
+
if (!input.writeTokens) throw new GitHubError(422, "This piece is a token: open it where the token files are read (Bridge Studio, bridge_open_library_branch or `bridge pieces open`).");
|
|
6025
|
+
const r = await input.writeTokens(plan, { mode });
|
|
6026
|
+
link = r.status === "suggested" ? { piece: piece.id, kind: "suggestion", number: r.number, url: r.url, title: plan.title, at, ...who } : { piece: piece.id, kind: "token-change", number: r.number, url: r.url, title: plan.title, branch: r.branch, at, ...who };
|
|
6027
|
+
} else if (plan.kind === "exploration") {
|
|
6028
|
+
const r = await startExplorationOnGitHub(gh, { ...plan.exploration, ...input.openedBy ? { author: input.openedBy } : {}, ...input.agent ? { agent: input.agent } : {} }, { base: input.base, ...input.exploration, labels: [NEEDED_BY_LABEL] });
|
|
6029
|
+
link = { piece: piece.id, kind: "exploration", number: r.pr, url: r.url, title: plan.title, branch: r.branch, at, ...who };
|
|
6030
|
+
} else {
|
|
6031
|
+
const issue = composeRequestIssue(plan.request, { labels: [NEEDED_BY_LABEL] });
|
|
6032
|
+
const r = await gh.createIssue(issue);
|
|
6033
|
+
link = { piece: piece.id, kind: "request", number: r.number, url: r.url, title: plan.title, at, ...who };
|
|
6034
|
+
}
|
|
6035
|
+
try {
|
|
6036
|
+
if (link.kind === "request" || link.kind === "suggestion") {
|
|
6037
|
+
const issue = await gh.getIssue(link.number);
|
|
6038
|
+
await gh.updateIssue(link.number, { body: withNeededBy(issue.body, plan.neededBy) });
|
|
6039
|
+
if (link.kind === "suggestion" && !issue.labels.includes(NEEDED_BY_LABEL)) await gh.addLabels(link.number, [NEEDED_BY_LABEL]);
|
|
6040
|
+
} else {
|
|
6041
|
+
const lib = await gh.getPullRequest(link.number);
|
|
6042
|
+
await gh.updatePullRequest(link.number, { body: withNeededBy(lib.body, plan.neededBy) });
|
|
6043
|
+
await gh.addLabels(link.number, [NEEDED_BY_LABEL]);
|
|
6044
|
+
}
|
|
6045
|
+
} catch (err) {
|
|
6046
|
+
if (err instanceof GitHubError && err.auth === "rejected") throw err;
|
|
6047
|
+
warnings.push(`The library side does not say who needs it yet (${err.message}). Add "Needed by #${input.pr}" to #${link.number} on GitHub.`);
|
|
6048
|
+
}
|
|
6049
|
+
try {
|
|
6050
|
+
const fresh = await gh.getPullRequest(input.pr);
|
|
6051
|
+
const next = withLink(parsePiecesRecord(fresh.body) ?? record, link);
|
|
6052
|
+
await gh.updatePullRequest(input.pr, { body: withPiecesRecord(fresh.body, next) });
|
|
6053
|
+
} catch (err) {
|
|
6054
|
+
if (mode !== "suggest") throw err;
|
|
6055
|
+
warnings.push(`#${input.pr} does not list #${link.number} yet (${err.message}). Add "Library: #${link.number}" to #${input.pr} on GitHub, so this piece is not sent twice.`);
|
|
6056
|
+
}
|
|
6057
|
+
let preview = null;
|
|
6058
|
+
if ((input.usePreview ?? true) && link.kind === "token-change" && link.branch) {
|
|
6059
|
+
preview = await useLibraryPreviewOnGitHub(gh, { productBranch: pull.headRef, libraryBranch: link.branch, libraryNumber: link.number }).catch((err) => {
|
|
6060
|
+
warnings.push(`Your branch does not use the preview yet: ${err.message}`);
|
|
6061
|
+
return null;
|
|
6062
|
+
});
|
|
6063
|
+
}
|
|
6064
|
+
return { status: link.kind === "suggestion" ? "suggested" : "opened", plan, link, preview, warnings };
|
|
6065
|
+
}
|
|
6066
|
+
var isNote = (path) => path.startsWith(`${EXPLORATION_NOTES_DIR}/`);
|
|
6067
|
+
async function linkedLibraryBranchOnGitHub(gh, link, product, piece) {
|
|
6068
|
+
const nearest = piece?.nearest?.label;
|
|
6069
|
+
if (link.kind === "suggestion") {
|
|
6070
|
+
const issue = await gh.getIssue(link.number);
|
|
6071
|
+
const cached = suggestionStateFromLabels(issue.labels);
|
|
6072
|
+
const s = suggestionStatus({ state: issue.state, stateReason: issue.stateReason, applied: cached === "applying", review: cached === "applying" ? void 0 : cached });
|
|
6073
|
+
const status2 = linkStatus({ kind: "suggestion", status: s });
|
|
6074
|
+
const lifecycle2 = s === "applied" ? "merged" : issue.state === "closed" ? "closed" : "open";
|
|
6075
|
+
const behindMain2 = lifecycle2 === "merged" ? await gh.compare(product.baseRef, product.headSha).then((c) => c.behindBy, () => null) : null;
|
|
6076
|
+
const preview2 = libraryPreviewState({ kind: "suggestion", lifecycle: lifecycle2, hasChanges: false, missing: null, total: null, behindMain: behindMain2, nearest });
|
|
6077
|
+
return { link, piece, status: status2, preview: preview2, people: status2.people };
|
|
6078
|
+
}
|
|
6079
|
+
if (link.kind === "request") {
|
|
6080
|
+
const issue = await gh.getIssue(link.number);
|
|
6081
|
+
const status2 = linkStatus({ kind: "request", state: issue.state, stateReason: issue.stateReason });
|
|
6082
|
+
const lifecycle2 = issue.state === "closed" ? issue.stateReason === "not_planned" ? "closed" : "merged" : "open";
|
|
6083
|
+
const behindMain2 = lifecycle2 === "merged" ? await gh.compare(product.baseRef, product.headSha).then((c) => c.behindBy, () => null) : null;
|
|
6084
|
+
const preview2 = libraryPreviewState({ kind: "request", lifecycle: lifecycle2, hasChanges: false, missing: null, total: null, behindMain: behindMain2, nearest });
|
|
6085
|
+
return { link, piece, status: status2, preview: preview2, people: status2.people };
|
|
6086
|
+
}
|
|
6087
|
+
const lib = await gh.getPullRequest(link.number);
|
|
6088
|
+
const reviews = await gh.listReviews(link.number).catch(() => []);
|
|
6089
|
+
const status = linkStatus({ kind: link.kind, state: lib.state, draft: lib.draft, labels: lib.labels, requestedReviewers: lib.requestedReviewers, reviews: reviews.map((r) => ({ user: r.user, state: r.state })) });
|
|
6090
|
+
const lifecycle = lib.state === "merged" ? "merged" : lib.state === "open" ? "open" : "closed";
|
|
6091
|
+
let missing = null;
|
|
6092
|
+
let total = null;
|
|
6093
|
+
let behindMain = null;
|
|
6094
|
+
let hasChanges = true;
|
|
6095
|
+
if (lifecycle === "open") {
|
|
6096
|
+
const [files, own, inProduct] = await Promise.all([
|
|
6097
|
+
gh.listPullRequestFiles(link.number).catch(() => null),
|
|
6098
|
+
gh.compare(lib.baseRef, lib.headSha).catch(() => null),
|
|
6099
|
+
gh.compare(product.headSha, lib.headSha).catch(() => null)
|
|
6100
|
+
]);
|
|
6101
|
+
hasChanges = files ? files.some((f) => !isNote(f.filename)) : true;
|
|
6102
|
+
total = own?.aheadBy ?? null;
|
|
6103
|
+
missing = inProduct?.aheadBy ?? null;
|
|
6104
|
+
} else if (lifecycle === "merged") {
|
|
6105
|
+
behindMain = await gh.compare(product.baseRef, product.headSha).then((c) => c.behindBy, () => null);
|
|
6106
|
+
}
|
|
6107
|
+
const preview = libraryPreviewState({ kind: link.kind, lifecycle, hasChanges, missing, total, behindMain, nearest });
|
|
6108
|
+
return { link, piece, status, preview, people: status.people };
|
|
6109
|
+
}
|
|
6110
|
+
async function productBranchViewOnGitHub(gh, pr, opts) {
|
|
6111
|
+
const pull = await gh.getPullRequest(pr);
|
|
6112
|
+
const files = await gh.listPullRequestFiles(pr).then((f) => f.map((x) => x.filename), () => []);
|
|
6113
|
+
const product = productOf(opts.products, pull, files);
|
|
6114
|
+
const record = parsePiecesRecord(pull.body);
|
|
6115
|
+
const linked = await Promise.all(
|
|
6116
|
+
(record?.links ?? []).map((l) => linkedLibraryBranchOnGitHub(gh, l, { headSha: pull.headSha, baseRef: pull.baseRef }, record?.pieces.find((p) => p.id === l.piece)))
|
|
6117
|
+
);
|
|
6118
|
+
const open = (record?.pieces ?? []).filter((p) => !record.links.some((l) => l.piece === p.id));
|
|
6119
|
+
return { pull, product, record, sentence: productBranchSentence(record, { viewer: opts.viewer, agentName: opts.agentName }), linked, open };
|
|
6120
|
+
}
|
|
6121
|
+
async function switchToPublishedOnGitHub(gh, pr) {
|
|
6122
|
+
if (!gh.canWrite) throw new GitHubError(401, "Switching needs a GitHub token (contents and pull requests: write).");
|
|
6123
|
+
const pull = await gh.getPullRequest(pr);
|
|
6124
|
+
if (pull.state !== "open") throw new GitHubError(422, `#${pr} is ${pull.state}: only an open branch switches.`);
|
|
6125
|
+
const cmp = await gh.compare(pull.baseRef, pull.headSha);
|
|
6126
|
+
if (cmp.behindBy === 0) return { status: "up-to-date", said: "Your branch already uses the published version." };
|
|
6127
|
+
try {
|
|
6128
|
+
await gh.updatePullRequestBranch(pr, pull.headSha);
|
|
6129
|
+
} catch (err) {
|
|
6130
|
+
if (err instanceof GitHubError && err.status === 422) throw new GitHubError(422, `Your branch and ${pull.baseRef} change the same lines, so Bridge cannot switch it alone. Use "Update from main" to pick the token values, or ask a developer.`, err.detail);
|
|
6131
|
+
throw err;
|
|
6132
|
+
}
|
|
6133
|
+
return { status: "switched", said: `Your branch now uses the published version from ${pull.baseRef}.` };
|
|
6134
|
+
}
|
|
6135
|
+
|
|
6136
|
+
// ../core/src/token-proposal.ts
|
|
6137
|
+
var issueKey2 = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
|
|
6138
|
+
function planTokenProposal(input) {
|
|
6139
|
+
const { before, config } = input;
|
|
6140
|
+
const r = applyTokenEdits(before, input.edits);
|
|
6141
|
+
const after = new LayeredTokenSet(before.config, r.files);
|
|
6142
|
+
const changes = diffLayered(before, after);
|
|
6143
|
+
if (!changes.length) return null;
|
|
6144
|
+
const prevErrors = new Set(validateLayered(before).map(issueKey2));
|
|
6145
|
+
const newErrors = validateLayered(after).filter((i) => i.level === "error" && !prevErrors.has(issueKey2(i)));
|
|
6146
|
+
const modes = config.contrast?.modes;
|
|
6147
|
+
const contrast = input.pairs.length ? contrastDelta(runLayeredContrast(before, input.pairs, modes), runLayeredContrast(after, input.pairs, modes)) : [];
|
|
6148
|
+
const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
|
|
6149
|
+
const impact = layeredImpact(before, after, changes);
|
|
6150
|
+
const tier = classifyLayered(changes, [], config);
|
|
6151
|
+
const summary2 = input.summary?.trim() || input.title;
|
|
6152
|
+
const body = [composeProposalBody({ summary: summary2, changes, contrast, impact, tier, validationErrors: newErrors.length, tool: input.tool ?? "Bridge MCP" }), ...input.bodyExtra ?? []].filter(Boolean).join("\n\n");
|
|
6153
|
+
const slug2 = slugify(input.title, 48) || "change";
|
|
6154
|
+
const changesetPath = `${config.changesets?.dir ?? ".changeset"}/bridge-${slug2}.md`;
|
|
6155
|
+
const touched = r.touched.map((f) => after.repoPath(f));
|
|
6156
|
+
const files = [
|
|
6157
|
+
...r.touched.map((f) => ({ path: after.repoPath(f), content: after.files.get(f) })),
|
|
6158
|
+
...config.changesets ? [{ path: changesetPath, content: formatChangeset(config.changesets.package, tier.bump, summary2) }] : []
|
|
6159
|
+
];
|
|
6160
|
+
const blocked = newErrors.length || newFailures.length ? [newErrors.length ? `${newErrors.length} new validation error(s)` : "", newFailures.length ? `${newFailures.length} contrast pair(s) would fail` : ""].filter(Boolean).join(" and ") : null;
|
|
6161
|
+
return { title: input.title, summary: summary2, edits: input.edits, before, after, changes, tier, contrast, newFailures, newErrors, impact, notes: r.notes, touched, files, body, slug: slug2, blocked };
|
|
6162
|
+
}
|
|
6163
|
+
async function writeTokenProposalOnGitHub(gh, plan, opts) {
|
|
6164
|
+
if (plan.blocked) throw new Error(`Not opened: ${plan.blocked}. Bridge never opens a branch that breaks the gate.`);
|
|
6165
|
+
const jira = opts.jira ?? null;
|
|
6166
|
+
let branch = branchWithJiraKey(`${opts.branchPrefix}${plan.slug}`, jira);
|
|
6167
|
+
for (let i = 2; await gh.getBranchSha(branch).then(Boolean, () => false); i++) branch = branchWithJiraKey(`${opts.branchPrefix}${plan.slug}-${i}`, jira);
|
|
6168
|
+
const commit = await gh.commitToNewBranch({ base: opts.base, branch, message: `feat(tokens): ${plan.title}
|
|
6169
|
+
|
|
6170
|
+
Proposed through ${opts.tool ?? "Bridge MCP"}. Tier ${plan.tier.tier}.`, files: plan.files });
|
|
6171
|
+
const labels = tokenLabels(plan, opts.labels, opts.labelsAfter);
|
|
6172
|
+
const draft = plan.tier.tier >= 2;
|
|
6173
|
+
const pr = await gh.createPullRequest({ base: opts.base, head: branch, title: titleWithJiraKey(`feat(tokens): ${plan.title}`, jira), body: plan.body, draft });
|
|
6174
|
+
await gh.addLabels(pr.number, labels);
|
|
6175
|
+
return { branch, commit: commit.sha, pr: pr.number, url: pr.url, labels, draft };
|
|
6176
|
+
}
|
|
6177
|
+
var tokenLabels = (plan, before = [], after = []) => [
|
|
6178
|
+
.../* @__PURE__ */ new Set([...before, `tier: ${plan.tier.tier}`, ...plan.tier.tier === 3 ? ["tokens"] : [], ...plan.tier.bump === "major" ? ["breaking"] : [], ...after])
|
|
6179
|
+
];
|
|
6180
|
+
|
|
6181
|
+
// ../core/src/figma-push-github.ts
|
|
6182
|
+
var pushSource = (d) => ["figma", "studio", "cli", "mcp"].includes(d.source) ? d.source : "figma";
|
|
6183
|
+
var specTier = (d) => ({ tier: 2, bump: d.kind === "new-component" || d.kind === "new-variant" || d.kind === "prop-change" ? "minor" : "patch" });
|
|
6184
|
+
function componentSpecSuggestionPayload(d, plan, ctx) {
|
|
6185
|
+
const specPath = figmaSpecPath(d);
|
|
6186
|
+
const spec = JSON.parse(formatFigmaSpec(d, plan));
|
|
6187
|
+
const brief = figmaDraftBrief({ ...d, tokenEdits: plan.edits, reasons: plan.reasons }, { suggestion: true, specPath, componentsDir: ctx.componentsDir });
|
|
6188
|
+
const by = (ctx.suggestedBy ?? d.requestedBy)?.replace(/^@/, "");
|
|
6189
|
+
const figma = d.figma?.fileKey || d.figma?.nodeId || d.figma?.url ? { ...d.figma.fileKey ? { fileKey: d.figma.fileKey } : {}, ...d.figma.nodeId ? { nodeId: d.figma.nodeId } : {}, ...d.figma.url ? { url: d.figma.url } : {} } : void 0;
|
|
6190
|
+
return {
|
|
6191
|
+
schema: SUGGESTION_SCHEMA,
|
|
6192
|
+
kind: "component-spec",
|
|
6193
|
+
title: d.title,
|
|
6194
|
+
summary: d.summary.trim() || d.title,
|
|
6195
|
+
source: pushSource(d),
|
|
6196
|
+
...by ? { suggestedBy: by } : {},
|
|
6197
|
+
createdAt: ctx.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
6198
|
+
base: ctx.base,
|
|
6199
|
+
tier: specTier(d),
|
|
6200
|
+
componentSpec: { component: d.component, specPath, spec, brief },
|
|
6201
|
+
checks: { newErrors: 0, contrast: [], blocked: null, impact: 0 },
|
|
6202
|
+
...figma ? { links: { figma } } : {}
|
|
6203
|
+
};
|
|
6204
|
+
}
|
|
6205
|
+
async function openComponentPushOnGitHub(gh, input) {
|
|
6206
|
+
const d = input.diff;
|
|
6207
|
+
if (!input.plan && !input.tokens) throw new Error("Route the change first: pass the token facts or the plan.");
|
|
6208
|
+
const plan = input.plan ?? planComponentPush(d, input.tokens);
|
|
6209
|
+
if (plan.route === "manual") return { status: "not-sent", plan, link: null };
|
|
6210
|
+
if (input.dryRun) return { status: "dry-run", plan, link: null };
|
|
6211
|
+
const mode = input.mode ?? "direct";
|
|
6212
|
+
if (!gh.canWrite) throw new GitHubError(401, mode === "suggest" ? "Suggesting a component change needs a GitHub token (issues: write)." : "Sending a component change needs a GitHub token (contents and pull requests: write).");
|
|
6213
|
+
const origin = {
|
|
6214
|
+
component: d.component,
|
|
6215
|
+
figmaName: d.figmaName,
|
|
6216
|
+
route: plan.route,
|
|
6217
|
+
kind: d.kind,
|
|
6218
|
+
...d.figma?.nodeId ? { nodeId: d.figma.nodeId } : {},
|
|
6219
|
+
...d.figma?.fileKey ? { fileKey: d.figma.fileKey } : {},
|
|
6220
|
+
...d.figma?.url ? { url: d.figma.url } : {},
|
|
6221
|
+
...d.requestedBy ? { requestedBy: d.requestedBy } : {}
|
|
6222
|
+
};
|
|
6223
|
+
if (plan.route === "token-change") {
|
|
6224
|
+
if (!input.writeTokens) throw new GitHubError(422, "This change is a token change: send it where the token files are read (the DS Bridge plugin, bridge_push_figma_component or `bridge figma push`).");
|
|
6225
|
+
const figma = { ...origin.fileKey ? { fileKey: origin.fileKey } : {}, ...origin.nodeId ? { nodeId: origin.nodeId } : {}, ...origin.url ? { url: origin.url } : {} };
|
|
6226
|
+
const r = await input.writeTokens({ edits: plan.edits, title: figmaBranchTitle(d), summary: d.summary.trim() || d.title, bodyExtra: [figmaOriginSection(origin)], mode, figma });
|
|
6227
|
+
await gh.addLabels(r.number, [FROM_FIGMA_LABEL]);
|
|
6228
|
+
if (r.status === "suggested") return { status: "suggested", plan, link: null, suggestion: { route: "token-change", number: r.number, url: r.url } };
|
|
6229
|
+
return { status: "opened", plan, link: { route: "token-change", number: r.number, url: r.url, branch: r.branch } };
|
|
6230
|
+
}
|
|
6231
|
+
if (mode === "suggest") {
|
|
6232
|
+
const sha = input.baseSha ?? await gh.getBranchSha(input.base);
|
|
6233
|
+
if (!sha) throw new GitHubError(404, `Branch ${input.base} not found. Check the default branch in bridge.config.json.`);
|
|
6234
|
+
const payload = componentSpecSuggestionPayload(d, plan, { base: { ref: input.base, sha }, suggestedBy: input.suggestedBy });
|
|
6235
|
+
const created = await createSuggestionOnGitHub(gh, payload, { labels: [...input.labels ?? [], WAITING_FOR_CLAUDE_LABEL] });
|
|
6236
|
+
return { status: "suggested", plan, link: null, suggestion: { route: "claude-draft", number: created.number, url: created.url } };
|
|
6237
|
+
}
|
|
6238
|
+
const spec = figmaSpecPath(d);
|
|
6239
|
+
const o = { ...origin, spec, status: "spec" };
|
|
6240
|
+
let branch = `${input.branchPrefix}figma-${figmaSpecSlug(d)}`;
|
|
6241
|
+
for (let i = 2; await gh.getBranchSha(branch).then(Boolean, () => false); i++) branch = `${input.branchPrefix}figma-${figmaSpecSlug(d)}-${i}`;
|
|
6242
|
+
await gh.commitToNewBranch({
|
|
6243
|
+
base: input.base,
|
|
6244
|
+
branch,
|
|
6245
|
+
message: `chore(figma): spec for ${figmaBranchTitle(d)}
|
|
6246
|
+
|
|
6247
|
+
Sent from Figma with ${input.tool ?? "the DS Bridge Figma plugin"}. Claude AI drafts the code on this branch; a person reviews it.`,
|
|
6248
|
+
files: [{ path: spec, content: formatFigmaSpec(d, plan) }]
|
|
6249
|
+
});
|
|
6250
|
+
const pr = await gh.createPullRequest({ base: input.base, head: branch, title: figmaBranchTitle(d), body: figmaDraftBody(d, plan, o), draft: true });
|
|
6251
|
+
await gh.addLabels(pr.number, [.../* @__PURE__ */ new Set([...input.labels ?? [], FROM_FIGMA_LABEL, REQUEST_LABEL, WAITING_FOR_CLAUDE_LABEL])]);
|
|
6252
|
+
return { status: "opened", plan, link: { route: "claude-draft", number: pr.number, url: pr.url, branch } };
|
|
6253
|
+
}
|
|
6254
|
+
async function listFigmaChangesOnGitHub(gh, opts = {}) {
|
|
6255
|
+
const pulls = await gh.listPullRequests(opts.state ?? "open");
|
|
6256
|
+
const out = [];
|
|
6257
|
+
for (const p of pulls) {
|
|
6258
|
+
if (!p.labels.includes(FROM_FIGMA_LABEL)) continue;
|
|
6259
|
+
const origin = parseFigmaOrigin(p.body);
|
|
6260
|
+
if (!origin) continue;
|
|
6261
|
+
out.push({ number: p.number, title: p.title, url: p.url, branch: p.headRef, origin, status: figmaPushStatus({ state: p.state, draft: p.draft, labels: p.labels, route: origin.route }) });
|
|
6262
|
+
}
|
|
6263
|
+
return out;
|
|
6264
|
+
}
|
|
6265
|
+
async function figmaChangeOnGitHub(gh, n, opts = {}) {
|
|
6266
|
+
const pull = await gh.getPullRequest(n);
|
|
6267
|
+
const origin = parseFigmaOrigin(pull.body);
|
|
6268
|
+
if (!origin) throw new GitHubError(404, `#${n} was not sent from Figma (no "From Figma" block in its description).`);
|
|
6269
|
+
const status = figmaPushStatus({ state: pull.state, draft: pull.draft, labels: pull.labels, route: origin.route });
|
|
6270
|
+
if (origin.route === "token-change") return { pull, origin, status, spec: void 0, brief: void 0 };
|
|
6271
|
+
const text = origin.spec ? await gh.getFileText(origin.spec, pull.headSha).catch(() => void 0) : void 0;
|
|
6272
|
+
const spec = parseFigmaSpec(text);
|
|
6273
|
+
if (!spec) throw new GitHubError(404, `The spec ${origin.spec ?? FIGMA_SPEC_DIR} is missing from ${pull.headRef}. Send the component from Figma again.`);
|
|
6274
|
+
const brief = figmaDraftBrief(spec, { pr: n, branch: pull.headRef, specPath: origin.spec, componentsDir: opts.componentsDir });
|
|
6275
|
+
return { pull, origin, status, spec, brief };
|
|
6276
|
+
}
|
|
6277
|
+
async function markFigmaChangeDraftedOnGitHub(gh, n) {
|
|
6278
|
+
if (!gh.canWrite) throw new GitHubError(401, "Marking a branch drafted needs a GitHub token (pull requests: write).");
|
|
6279
|
+
const pull = await gh.getPullRequest(n);
|
|
6280
|
+
const origin = parseFigmaOrigin(pull.body);
|
|
6281
|
+
if (!origin || origin.route !== "claude-draft") throw new GitHubError(422, `#${n} is not a Claude draft branch from Figma.`);
|
|
6282
|
+
if (pull.state !== "open") throw new GitHubError(422, `#${n} is ${pull.state}: only an open branch is marked drafted.`);
|
|
6283
|
+
const files = (await gh.listPullRequestFiles(n)).map((f) => f.filename).filter((f) => !f.startsWith(`${FIGMA_SPEC_DIR}/`));
|
|
6284
|
+
if (!files.length) throw new GitHubError(422, `${pull.headRef} only carries the spec: write the component code there first, push it, then mark it drafted.`);
|
|
6285
|
+
await gh.updatePullRequest(n, { body: withFigmaOrigin(pull.body, { ...origin, status: "drafted" }) });
|
|
6286
|
+
await gh.removeLabel(n, WAITING_FOR_CLAUDE_LABEL).catch(() => void 0);
|
|
6287
|
+
await gh.addLabels(n, [DRAFTED_BY_CLAUDE_LABEL]);
|
|
6288
|
+
const labels = [...pull.labels.filter((l) => l !== WAITING_FOR_CLAUDE_LABEL), DRAFTED_BY_CLAUDE_LABEL];
|
|
6289
|
+
return { status: figmaPushStatus({ state: pull.state, labels, route: origin.route }), files };
|
|
6290
|
+
}
|
|
6291
|
+
|
|
4892
6292
|
// ../core/src/node/git-history.ts
|
|
4893
6293
|
var US = "";
|
|
4894
6294
|
var RS = "";
|
|
@@ -5744,8 +7144,8 @@ function tailwindFindings(hits, offset, pos, ds) {
|
|
|
5744
7144
|
else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
|
|
5745
7145
|
if (cat === "color" && !isColor) continue;
|
|
5746
7146
|
const suggestion = ds.suggest(cat, v);
|
|
5747
|
-
const
|
|
5748
|
-
findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...
|
|
7147
|
+
const cap3 = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
|
|
7148
|
+
findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap3 ? { suggestion: cap3 } : {} });
|
|
5749
7149
|
}
|
|
5750
7150
|
return { findings, themeRefs };
|
|
5751
7151
|
}
|
|
@@ -6753,9 +8153,9 @@ var DesignSystemIndex = class {
|
|
|
6753
8153
|
return generic ? this.make(generic, 1, 0.3) : void 0;
|
|
6754
8154
|
}
|
|
6755
8155
|
case "shadow": {
|
|
6756
|
-
const
|
|
8156
|
+
const norm3 = (s) => s.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").trim().toLowerCase();
|
|
6757
8157
|
const cands = this.entries(["shadow"]);
|
|
6758
|
-
const exact = cands.find((e) =>
|
|
8158
|
+
const exact = cands.find((e) => norm3(e.css) === norm3(value));
|
|
6759
8159
|
if (exact) return this.make(exact, 0, 1);
|
|
6760
8160
|
const t = shadowNums(value);
|
|
6761
8161
|
const scored = cands.map((e) => {
|
|
@@ -7181,11 +8581,11 @@ async function listComponentNames(source, config) {
|
|
|
7181
8581
|
}
|
|
7182
8582
|
var findFamily = (stories) => stories ? /title:\s*['"]([^'"/]+)\//.exec(stories)?.[1] : void 0;
|
|
7183
8583
|
async function listComponents(source, config, withSummaries) {
|
|
7184
|
-
const
|
|
7185
|
-
if (!withSummaries) return
|
|
8584
|
+
const names3 = await listComponentNames(source, config);
|
|
8585
|
+
if (!withSummaries) return names3.map((n) => ({ name: n.name }));
|
|
7186
8586
|
const dir = config.components.dir;
|
|
7187
8587
|
return Promise.all(
|
|
7188
|
-
|
|
8588
|
+
names3.map(async ({ name, files }) => {
|
|
7189
8589
|
const docsPath = `${dir}/${name}/${name}${config.components.docsSuffix}`;
|
|
7190
8590
|
const storiesPath = files.find((f) => /\.stories\.\w+$/.test(f));
|
|
7191
8591
|
const [docs, stories] = await Promise.all([files.includes(docsPath) ? source.read(docsPath) : null, storiesPath ? source.read(storiesPath) : null]);
|
|
@@ -7480,6 +8880,7 @@ __export(ops_exports, {
|
|
|
7480
8880
|
auditOp: () => auditOp,
|
|
7481
8881
|
auditUi: () => auditUi,
|
|
7482
8882
|
checkContrast: () => checkContrast,
|
|
8883
|
+
designSystemIndex: () => designSystemIndex,
|
|
7483
8884
|
draftDocsOp: () => draftDocsOp,
|
|
7484
8885
|
getComponentOp: () => getComponentOp,
|
|
7485
8886
|
getConfig: () => getConfig,
|
|
@@ -7503,6 +8904,247 @@ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:f
|
|
|
7503
8904
|
import { mkdir as mkdir2, writeFile as writeFile2, access } from "node:fs/promises";
|
|
7504
8905
|
import { dirname as dirname3, isAbsolute, join as join4, resolve as resolve3 } from "node:path";
|
|
7505
8906
|
|
|
8907
|
+
// ../core/src/audit/missing-pieces.ts
|
|
8908
|
+
var lines = (text) => text.split(/\r?\n/);
|
|
8909
|
+
function lineBag(text) {
|
|
8910
|
+
const bag = /* @__PURE__ */ new Map();
|
|
8911
|
+
for (const l of text ? lines(text) : []) {
|
|
8912
|
+
const k = l.trim();
|
|
8913
|
+
if (k) bag.set(k, (bag.get(k) ?? 0) + 1);
|
|
8914
|
+
}
|
|
8915
|
+
return bag;
|
|
8916
|
+
}
|
|
8917
|
+
var offsetOf = (text, line, column) => {
|
|
8918
|
+
let off = 0;
|
|
8919
|
+
const ls = text.split("\n");
|
|
8920
|
+
for (let i = 0; i < line - 1 && i < ls.length; i++) off += ls[i].length + 1;
|
|
8921
|
+
return off + Math.max(0, column - 1);
|
|
8922
|
+
};
|
|
8923
|
+
function enclosingTag(text, offset) {
|
|
8924
|
+
for (let i = offset - 1; i >= Math.max(0, offset - 4e3); i--) {
|
|
8925
|
+
if (text[i] !== "<" || !/[A-Za-z]/.test(text[i + 1] ?? "")) continue;
|
|
8926
|
+
let depth = 0;
|
|
8927
|
+
let closed = false;
|
|
8928
|
+
for (let j = i + 1; j < offset; j++) {
|
|
8929
|
+
const c = text[j];
|
|
8930
|
+
if (c === "{") depth++;
|
|
8931
|
+
else if (c === "}") depth--;
|
|
8932
|
+
else if (c === ">" && depth <= 0 && text[j - 1] !== "=") {
|
|
8933
|
+
closed = true;
|
|
8934
|
+
break;
|
|
8935
|
+
}
|
|
8936
|
+
}
|
|
8937
|
+
if (closed) return null;
|
|
8938
|
+
const m = /^<([A-Za-z][\w.]*)/.exec(text.slice(i));
|
|
8939
|
+
return m ? { name: m[1], start: i } : null;
|
|
8940
|
+
}
|
|
8941
|
+
return null;
|
|
8942
|
+
}
|
|
8943
|
+
function childText(text, tagStart) {
|
|
8944
|
+
let depth = 0;
|
|
8945
|
+
for (let j = tagStart + 1; j < text.length; j++) {
|
|
8946
|
+
const c = text[j];
|
|
8947
|
+
if (c === "{") depth++;
|
|
8948
|
+
else if (c === "}") depth--;
|
|
8949
|
+
else if (c === ">" && depth <= 0 && text[j - 1] !== "=") {
|
|
8950
|
+
if (text[j - 1] === "/") return null;
|
|
8951
|
+
const rest = /^([^<{]*)</.exec(text.slice(j + 1));
|
|
8952
|
+
const t = rest?.[1]?.trim() ?? "";
|
|
8953
|
+
return t && t.split(/\s+/).length <= 3 && t.length <= 30 ? t : null;
|
|
8954
|
+
}
|
|
8955
|
+
}
|
|
8956
|
+
return null;
|
|
8957
|
+
}
|
|
8958
|
+
function ruleClasses(text, offset) {
|
|
8959
|
+
const open = text.lastIndexOf("{", offset);
|
|
8960
|
+
if (open < 0) return [];
|
|
8961
|
+
const prev = Math.max(text.lastIndexOf("}", open), text.lastIndexOf(";", open));
|
|
8962
|
+
const selector = text.slice(prev + 1, open);
|
|
8963
|
+
return [...selector.matchAll(/\.([A-Za-z_][\w-]*)/g)].map((m) => m[1]);
|
|
8964
|
+
}
|
|
8965
|
+
var GENERIC = /* @__PURE__ */ new Set(["status", "badge", "tag", "pill", "chip", "style", "styles", "root", "wrapper", "container", "item", "label", "text", "color", "bg", "is", "has", "variant", "tone", "custom", "new"]);
|
|
8966
|
+
function nameFromClass(cls, component) {
|
|
8967
|
+
const words = cls.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[\s_-]+/).map((w) => w.toLowerCase()).filter(Boolean);
|
|
8968
|
+
const comp = component?.toLowerCase();
|
|
8969
|
+
const kept = words.filter((w) => w !== comp && !GENERIC.has(w));
|
|
8970
|
+
return kept[kept.length - 1] ?? words[words.length - 1] ?? cls;
|
|
8971
|
+
}
|
|
8972
|
+
var ASPECT = { color: "tone", radius: "shape", spacing: "size", size: "size", "font-size": "size", "line-height": "size" };
|
|
8973
|
+
function aspectOf(values) {
|
|
8974
|
+
for (const a of ["tone", "shape", "size"]) if (values.some((v) => ASPECT[v.category] === a)) return a;
|
|
8975
|
+
return "style";
|
|
8976
|
+
}
|
|
8977
|
+
var norm2 = (f) => f.category === "color" ? f.value.toLowerCase() : f.value.replace(/\s+/g, "");
|
|
8978
|
+
function nearestOf(s) {
|
|
8979
|
+
if (!s) return void 0;
|
|
8980
|
+
return { token: s.token, cssVar: s.cssVar, value: s.value, level: s.level, label: tokenLabel(s.token) };
|
|
8981
|
+
}
|
|
8982
|
+
var TOKEN_TYPE = { color: "color", duration: "duration", "font-weight": "fontWeight", "line-height": "number", "z-index": "number", "font-family": "fontFamily", shadow: "shadow", easing: "cubicBezier" };
|
|
8983
|
+
var valueTitle = (category, value) => {
|
|
8984
|
+
const v = category === "color" && /^#[0-9a-f]+$/i.test(value) ? value.toUpperCase() : value;
|
|
8985
|
+
const word = { color: "color", radius: "radius", spacing: "spacing", size: "size", "font-size": "text size", "font-weight": "font weight", "line-height": "line height", shadow: "shadow", duration: "duration", easing: "easing", "z-index": "layer", "border-width": "border width", "font-family": "font" };
|
|
8986
|
+
return `New ${word[category] ?? category} ${v}`;
|
|
8987
|
+
};
|
|
8988
|
+
function missingPieces(input) {
|
|
8989
|
+
const { ds } = input;
|
|
8990
|
+
const files = input.files.filter((f) => f.after !== null && isAuditable(f.path));
|
|
8991
|
+
const fixes = [];
|
|
8992
|
+
const variants = /* @__PURE__ */ new Map();
|
|
8993
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
8994
|
+
const foreign = /* @__PURE__ */ new Map();
|
|
8995
|
+
const classUse = (cls) => {
|
|
8996
|
+
const re = new RegExp(`<([A-Z][\\w.]*)\\b[^<]*?styles(?:\\.${cls}\\b|\\[['"]${cls}['"]\\])`);
|
|
8997
|
+
for (const f of files) {
|
|
8998
|
+
const m = re.exec(f.after);
|
|
8999
|
+
if (m && ds.components.has(m[1].split(".")[0])) return { component: m[1].split(".")[0], text: childText(f.after, m.index) };
|
|
9000
|
+
}
|
|
9001
|
+
return null;
|
|
9002
|
+
};
|
|
9003
|
+
for (const f of files) {
|
|
9004
|
+
const after = f.after;
|
|
9005
|
+
const report = auditFile(f.path, after, ds);
|
|
9006
|
+
const before = lineBag(f.before);
|
|
9007
|
+
const afterLines = lines(after);
|
|
9008
|
+
const added = (line) => {
|
|
9009
|
+
const k = afterLines[line - 1]?.trim() ?? "";
|
|
9010
|
+
const n = before.get(k) ?? 0;
|
|
9011
|
+
if (n > 0) {
|
|
9012
|
+
before.set(k, n - 1);
|
|
9013
|
+
return false;
|
|
9014
|
+
}
|
|
9015
|
+
return true;
|
|
9016
|
+
};
|
|
9017
|
+
const addedLines = /* @__PURE__ */ new Map();
|
|
9018
|
+
const isNew = (line) => {
|
|
9019
|
+
if (!addedLines.has(line)) addedLines.set(line, added(line));
|
|
9020
|
+
return addedLines.get(line);
|
|
9021
|
+
};
|
|
9022
|
+
const css = report.lang === "css" || report.lang === "scss" || report.lang === "less";
|
|
9023
|
+
for (const finding of report.findings) {
|
|
9024
|
+
if (!isNew(finding.line)) continue;
|
|
9025
|
+
const s = finding.suggestion;
|
|
9026
|
+
const at = { file: f.path, line: finding.line };
|
|
9027
|
+
if (s?.level === "exact" || s?.level === "high" && finding.category !== "color") {
|
|
9028
|
+
fixes.push({ ...at, what: finding.value, use: s.token });
|
|
9029
|
+
continue;
|
|
9030
|
+
}
|
|
9031
|
+
const offset = offsetOf(after, finding.line, finding.column);
|
|
9032
|
+
let component = null;
|
|
9033
|
+
let name = null;
|
|
9034
|
+
if (css) {
|
|
9035
|
+
const classes = ruleClasses(after, offset);
|
|
9036
|
+
for (const cls of [...classes].reverse()) {
|
|
9037
|
+
const use = classUse(cls);
|
|
9038
|
+
if (use) {
|
|
9039
|
+
component = use.component;
|
|
9040
|
+
name = nameFromClass(cls, use.component);
|
|
9041
|
+
break;
|
|
9042
|
+
}
|
|
9043
|
+
}
|
|
9044
|
+
if (!component && classes.length) name = nameFromClass(classes[classes.length - 1]);
|
|
9045
|
+
} else {
|
|
9046
|
+
const tag = enclosingTag(after, offset);
|
|
9047
|
+
const root = tag?.name.split(".")[0];
|
|
9048
|
+
if (tag && root && ds.components.has(root)) {
|
|
9049
|
+
component = root;
|
|
9050
|
+
name = childText(after, tag.start);
|
|
9051
|
+
const cls = /className=\{?["'`]?([\w-]+)/.exec(after.slice(tag.start, offset))?.[1];
|
|
9052
|
+
if (!name && cls) name = nameFromClass(cls, root);
|
|
9053
|
+
}
|
|
9054
|
+
}
|
|
9055
|
+
const hit = { ...at, finding };
|
|
9056
|
+
if (component) {
|
|
9057
|
+
const label = (name ?? "custom").toLowerCase();
|
|
9058
|
+
const key2 = `${component}|${label}`;
|
|
9059
|
+
const v = variants.get(key2) ?? { component, name: label, hits: [] };
|
|
9060
|
+
v.hits.push(hit);
|
|
9061
|
+
variants.set(key2, v);
|
|
9062
|
+
continue;
|
|
9063
|
+
}
|
|
9064
|
+
const key = `${finding.category}|${norm2(finding)}`;
|
|
9065
|
+
const t = tokens.get(key) ?? { category: finding.category, value: finding.value, name, hits: [] };
|
|
9066
|
+
if (!t.name && name) t.name = name;
|
|
9067
|
+
t.hits.push(hit);
|
|
9068
|
+
tokens.set(key, t);
|
|
9069
|
+
}
|
|
9070
|
+
for (const c of report.components) {
|
|
9071
|
+
if (!isNew(c.line)) continue;
|
|
9072
|
+
if (c.suggestion) {
|
|
9073
|
+
fixes.push({ file: f.path, line: c.line, what: c.name, use: c.suggestion });
|
|
9074
|
+
continue;
|
|
9075
|
+
}
|
|
9076
|
+
if (c.kind !== "foreign") continue;
|
|
9077
|
+
const e = foreign.get(c.name) ?? { finding: c, hits: [] };
|
|
9078
|
+
e.hits.push({ file: f.path, line: c.line });
|
|
9079
|
+
foreign.set(c.name, e);
|
|
9080
|
+
}
|
|
9081
|
+
}
|
|
9082
|
+
const where2 = (hits) => hits.slice(0, 5).map((h) => ({ file: h.file, line: h.line }));
|
|
9083
|
+
const pieces = [];
|
|
9084
|
+
for (const v of variants.values()) {
|
|
9085
|
+
const values = [];
|
|
9086
|
+
for (const h of v.hits) {
|
|
9087
|
+
const pv = { category: h.finding.category, ...h.finding.property ? { property: h.finding.property } : {}, value: h.finding.value };
|
|
9088
|
+
if (!values.some((x) => x.category === pv.category && x.property === pv.property && norm2(x) === norm2(pv))) values.push(pv);
|
|
9089
|
+
}
|
|
9090
|
+
const aspect = aspectOf(values);
|
|
9091
|
+
const bg = v.hits.find((h) => /background/.test(h.finding.property ?? ""))?.finding.suggestion;
|
|
9092
|
+
const nearest = nearestOf(bg ?? v.hits.find((h) => h.finding.suggestion)?.finding.suggestion);
|
|
9093
|
+
const shown = values.map((x) => `${x.property ?? x.category} ${x.value}`).join(", ");
|
|
9094
|
+
pieces.push({
|
|
9095
|
+
id: pieceId([v.component, v.name, aspect]),
|
|
9096
|
+
kind: "variant",
|
|
9097
|
+
title: `${v.component}: ${v.name} ${aspect}`,
|
|
9098
|
+
summary: `${v.component} restyled by hand (${shown}).${nearest ? ` ${nearest.level === "high" ? "It looks a lot like" : "The closest in the library is"} ${nearest.label}.` : ""}`,
|
|
9099
|
+
name: v.name,
|
|
9100
|
+
component: v.component,
|
|
9101
|
+
aspect,
|
|
9102
|
+
values,
|
|
9103
|
+
...nearest ? { nearest } : {},
|
|
9104
|
+
where: where2(v.hits)
|
|
9105
|
+
});
|
|
9106
|
+
}
|
|
9107
|
+
for (const [name, e] of foreign) {
|
|
9108
|
+
pieces.push({
|
|
9109
|
+
id: pieceId(["component", name]),
|
|
9110
|
+
kind: "component",
|
|
9111
|
+
title: `New component: ${name}`,
|
|
9112
|
+
summary: `${name} comes from ${e.finding.library}, and the library has nothing like it.`,
|
|
9113
|
+
name,
|
|
9114
|
+
component: name.split(".").pop(),
|
|
9115
|
+
values: [],
|
|
9116
|
+
where: where2(e.hits)
|
|
9117
|
+
});
|
|
9118
|
+
}
|
|
9119
|
+
for (const t of tokens.values()) {
|
|
9120
|
+
const nearest = nearestOf(t.hits[0].finding.suggestion);
|
|
9121
|
+
const pname = t.name ?? `${t.category === "color" ? "custom" : t.category}`;
|
|
9122
|
+
const path = proposedTokenPath(nearest?.token, pname, t.category);
|
|
9123
|
+
const tokenFiles = nearest && input.set ? [...new Set(input.set.defs.filter((d) => d.path === nearest.token).map((d) => d.file))] : [];
|
|
9124
|
+
const n = t.hits.length;
|
|
9125
|
+
pieces.push({
|
|
9126
|
+
id: pieceId([t.category, t.name ?? t.value.replace(/^#/, "")]),
|
|
9127
|
+
kind: "token",
|
|
9128
|
+
title: valueTitle(t.category, t.value),
|
|
9129
|
+
summary: `Written by hand in ${n === 1 ? "one place" : `${n} places`}.${nearest ? ` ${nearest.level === "high" ? "It looks a lot like" : "The closest in the library is"} ${nearest.label} (${nearest.value}).` : " The library has nothing close."}`,
|
|
9130
|
+
name: pname,
|
|
9131
|
+
values: [{ category: t.category, ...t.hits[0].finding.property ? { property: t.hits[0].finding.property } : {}, value: t.value }],
|
|
9132
|
+
...nearest ? { nearest } : {},
|
|
9133
|
+
where: where2(t.hits),
|
|
9134
|
+
token: { path, type: TOKEN_TYPE[t.category] ?? "dimension", value: t.value, files: tokenFiles }
|
|
9135
|
+
});
|
|
9136
|
+
}
|
|
9137
|
+
const order = { variant: 0, component: 1, token: 2 };
|
|
9138
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9139
|
+
const out = pieces.sort((a, b) => order[a.kind] - order[b.kind]).map((p) => {
|
|
9140
|
+
let id = p.id;
|
|
9141
|
+
for (let i = 2; seen.has(id); i++) id = `${p.id}-${i}`;
|
|
9142
|
+
seen.add(id);
|
|
9143
|
+
return id === p.id ? p : { ...p, id };
|
|
9144
|
+
});
|
|
9145
|
+
return { pieces: out, fixes, files: files.length };
|
|
9146
|
+
}
|
|
9147
|
+
|
|
7506
9148
|
// src/audit.ts
|
|
7507
9149
|
var PROPERTY_HINTS = [
|
|
7508
9150
|
[/^(color|fill|stroke)$/, /(^|\.)(text|fg|icon|foreground|link)(\.|-|$)/],
|
|
@@ -7523,8 +9165,8 @@ function auditSnippet(set, code2, max = 3) {
|
|
|
7523
9165
|
});
|
|
7524
9166
|
const rank2 = (tier) => tier === "semantic" ? 0 : tier === "component" ? 1 : tier === "density" ? 2 : tier === "brand" ? 3 : 4;
|
|
7525
9167
|
const findings = [];
|
|
7526
|
-
const
|
|
7527
|
-
|
|
9168
|
+
const lines2 = code2.split("\n");
|
|
9169
|
+
lines2.forEach((line, i) => {
|
|
7528
9170
|
if (/var\(--/.test(line) && !/#[0-9a-f]{3,8}\b|\d(px|rem|ms)\b/i.test(line.replace(/var\([^)]*\)/g, ""))) return;
|
|
7529
9171
|
const stripped = line.replace(/var\([^)]*\)/g, "");
|
|
7530
9172
|
const propAt = (idx) => /([a-zA-Z-]+)['"]?\s*:[^;:,{]*$/.exec(stripped.slice(0, idx))?.[1]?.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`).toLowerCase();
|
|
@@ -7592,17 +9234,17 @@ function auditSnippet(set, code2, max = 3) {
|
|
|
7592
9234
|
// src/markdown.ts
|
|
7593
9235
|
var slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7594
9236
|
function sections(md) {
|
|
7595
|
-
const
|
|
9237
|
+
const lines2 = md.split("\n");
|
|
7596
9238
|
const heads = [];
|
|
7597
9239
|
let fence = false;
|
|
7598
|
-
|
|
9240
|
+
lines2.forEach((l, i) => {
|
|
7599
9241
|
if (/^\s*(```|~~~)/.test(l)) fence = !fence;
|
|
7600
9242
|
const m = !fence && /^(#{1,4})\s+(.+?)\s*#*\s*$/.exec(l);
|
|
7601
9243
|
if (m) heads.push({ level: m[1].length, title: m[2], line: i });
|
|
7602
9244
|
});
|
|
7603
9245
|
return heads.map((h, i) => {
|
|
7604
9246
|
const next = heads.slice(i + 1).find((x) => x.level <= h.level);
|
|
7605
|
-
return { level: h.level, title: h.title, slug: slug(h.title), body:
|
|
9247
|
+
return { level: h.level, title: h.title, slug: slug(h.title), body: lines2.slice(h.line, next ? next.line : lines2.length).join("\n").trim() };
|
|
7606
9248
|
});
|
|
7607
9249
|
}
|
|
7608
9250
|
function toc(md) {
|
|
@@ -7634,6 +9276,7 @@ __export(explorations_exports, {
|
|
|
7634
9276
|
installOf: () => installOf,
|
|
7635
9277
|
listExplorations: () => listExplorations,
|
|
7636
9278
|
promoteExploration: () => promoteExploration,
|
|
9279
|
+
settingsOf: () => settingsOf,
|
|
7637
9280
|
startExploration: () => startExploration,
|
|
7638
9281
|
toExplorationBranch: () => toExplorationBranch,
|
|
7639
9282
|
writeToExploration: () => writeToExploration
|
|
@@ -7658,13 +9301,16 @@ __export(branches_exports, {
|
|
|
7658
9301
|
listProducts: () => listProducts,
|
|
7659
9302
|
mergeBranch: () => mergeBranch,
|
|
7660
9303
|
productMarks: () => productMarks,
|
|
9304
|
+
reader: () => reader,
|
|
7661
9305
|
reopenBranch: () => reopenBranch,
|
|
7662
9306
|
resolveProduct: () => resolveProduct,
|
|
7663
9307
|
reviewBranch: () => reviewBranch,
|
|
7664
9308
|
sendForReview: () => sendForReview,
|
|
7665
9309
|
tryBranch: () => tryBranch,
|
|
7666
9310
|
updateFromMain: () => updateFromMain,
|
|
7667
|
-
|
|
9311
|
+
viewerOf: () => viewerOf,
|
|
9312
|
+
whatNeedsMe: () => whatNeedsMe,
|
|
9313
|
+
writer: () => writer
|
|
7668
9314
|
});
|
|
7669
9315
|
import { relative as relative2 } from "node:path";
|
|
7670
9316
|
var RIGHTS = {
|
|
@@ -7976,11 +9622,11 @@ async function commentOnBranch(ctx, a) {
|
|
|
7976
9622
|
async function listComments(ctx, a) {
|
|
7977
9623
|
const n = branchNumber(a.pr);
|
|
7978
9624
|
const gh = await reader(ctx);
|
|
7979
|
-
const [conversation,
|
|
9625
|
+
const [conversation, lines2] = await Promise.all([gh.listIssueComments(n), gh.listReviewComments(n).catch(() => [])]);
|
|
7980
9626
|
return {
|
|
7981
9627
|
pr: n,
|
|
7982
9628
|
conversation: conversation.filter((c) => !c.body.includes("<!-- bridge-branch-preview -->")).map((c) => ({ id: c.id, author: c.author, body: c.body, at: c.updatedAt })),
|
|
7983
|
-
threads:
|
|
9629
|
+
threads: lines2.filter((c) => !c.inReplyTo).map((c) => ({ id: c.id, author: c.author, file: c.path, line: c.line, body: c.body, url: c.url, replies: lines2.filter((r) => r.inReplyTo === c.id).map((r) => ({ id: r.id, author: r.author, body: r.body })) })),
|
|
7984
9630
|
howToReply: "bridge_comment with replyTo: the thread id."
|
|
7985
9631
|
};
|
|
7986
9632
|
}
|
|
@@ -8432,7 +10078,7 @@ async function checkContrast(ctx, a) {
|
|
|
8432
10078
|
const delta2 = core.contrastDelta(beforeRes, afterRes);
|
|
8433
10079
|
return { ...base, edits: a.edits.length, notes, changed: delta2, newFailures: delta2.filter((d) => d.passBefore && !d.passAfter) };
|
|
8434
10080
|
}
|
|
8435
|
-
var
|
|
10081
|
+
var issueKey3 = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
|
|
8436
10082
|
async function validateTokens(ctx, a) {
|
|
8437
10083
|
const { before, after, notes } = await withEdits(ctx, a.edits);
|
|
8438
10084
|
const issues = core.validate(after);
|
|
@@ -8448,8 +10094,8 @@ async function validateTokens(ctx, a) {
|
|
|
8448
10094
|
...a.includeWarnings ? { warnings } : warnings.length ? { warningSample: warnings.slice(0, 5) } : {}
|
|
8449
10095
|
};
|
|
8450
10096
|
if (!a.edits?.length) return res;
|
|
8451
|
-
const prev = new Set(core.validate(before).map(
|
|
8452
|
-
return { ...res, notes, newErrors: errors.filter((i) => !prev.has(
|
|
10097
|
+
const prev = new Set(core.validate(before).map(issueKey3));
|
|
10098
|
+
return { ...res, notes, newErrors: errors.filter((i) => !prev.has(issueKey3(i))) };
|
|
8453
10099
|
}
|
|
8454
10100
|
async function listComponentsOp(ctx, a) {
|
|
8455
10101
|
const { config } = await ctx.config();
|
|
@@ -8602,25 +10248,13 @@ async function proposeTokenChange(ctx, a) {
|
|
|
8602
10248
|
const target = a.branch ? await explorationContext(ctx, a.branch, { requireCheckout: !a.dryRun }) : null;
|
|
8603
10249
|
if (target) ctx = target.ctx;
|
|
8604
10250
|
const { config } = await ctx.config();
|
|
8605
|
-
const
|
|
8606
|
-
const changes = core.diff(before, after);
|
|
8607
|
-
if (!changes.length) return { status: "no-change", message: "The edits leave every token as it was." };
|
|
8608
|
-
const prevErrors = new Set(core.validate(before).map(issueKey2));
|
|
8609
|
-
const newErrors = core.validate(after).filter((i) => i.level === "error" && !prevErrors.has(issueKey2(i)));
|
|
10251
|
+
const before = await ctx.tokens();
|
|
8610
10252
|
const pairs = await core.readContrastPairs(ctx.source, config).catch(() => []);
|
|
8611
|
-
const delta2 = pairs.length ? core.contrastDelta(core.runContrast(before, pairs, config.contrast?.modes), core.runContrast(after, pairs, config.contrast?.modes)) : [];
|
|
8612
|
-
const newFailures = delta2.filter((d) => d.passBefore && !d.passAfter);
|
|
8613
|
-
const imp = core.impact(before, after, changes);
|
|
8614
|
-
const tier = core.classify(changes, [], config);
|
|
8615
|
-
const summary2 = a.summary?.trim() || a.title;
|
|
8616
10253
|
const marks = productMarks(productsOf(config), product);
|
|
8617
|
-
const
|
|
8618
|
-
|
|
8619
|
-
const
|
|
8620
|
-
const
|
|
8621
|
-
...touched.map((f) => ({ path: prefixPath(after, f), content: after.files.get(f) })),
|
|
8622
|
-
...config.changesets ? [{ path: changesetPath, content: core.changeset(config.changesets.package, tier.bump, summary2) }] : []
|
|
8623
|
-
];
|
|
10254
|
+
const plan = planTokenProposal({ before, pairs, edits: a.edits, title: a.title, summary: a.summary, config, tool: "Bridge MCP", bodyExtra: [marks.line] });
|
|
10255
|
+
if (!plan) return { status: "no-change", message: "The edits leave every token as it was." };
|
|
10256
|
+
const { after, changes, tier, contrast: delta2, newFailures, newErrors, impact: imp, notes, files, body, slug: slugTitle } = plan;
|
|
10257
|
+
const summary2 = plan.summary;
|
|
8624
10258
|
const report = {
|
|
8625
10259
|
title: a.title,
|
|
8626
10260
|
tier: tier.tier,
|
|
@@ -8669,15 +10303,12 @@ Tier ${tier.tier}. Proposed through Bridge MCP (source: mcp).`;
|
|
|
8669
10303
|
}
|
|
8670
10304
|
const gh = await ctx.github();
|
|
8671
10305
|
if (!ctx.canWriteRemote) return { status: "preview", message: "Read-only: set GITHUB_TOKEN to open this as a pull request.", ...preview, fileContents: files };
|
|
8672
|
-
const commitMsg = `feat(tokens): ${a.title}
|
|
8673
|
-
|
|
8674
|
-
Proposed through Bridge MCP (source: mcp). Tier ${tier.tier}.`;
|
|
8675
10306
|
const base = ctx.ref ?? baseBranchOf(config);
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
const
|
|
8679
|
-
const pr =
|
|
8680
|
-
|
|
10307
|
+
const opened = await writeTokenProposalOnGitHub(gh, plan, { base, branchPrefix: config.governance.branchPrefix, jira, labels: config.governance.labels, labelsAfter: marks.labels, tool: "Bridge MCP (source: mcp)" });
|
|
10308
|
+
branch = opened.branch;
|
|
10309
|
+
const commit = { sha: opened.commit };
|
|
10310
|
+
const pr = { number: opened.pr, url: opened.url };
|
|
10311
|
+
const labels = opened.labels;
|
|
8681
10312
|
return { status: "pr-opened", ...preview, branch, commit: commit.sha, pr: pr.number, url: pr.url, labels, draft: tier.tier >= 2, product: product.name };
|
|
8682
10313
|
}
|
|
8683
10314
|
async function addToBranch(ctx, a) {
|
|
@@ -8688,8 +10319,8 @@ async function addToBranch(ctx, a) {
|
|
|
8688
10319
|
const { before, after, touched, notes } = await withEdits(at, a.edits);
|
|
8689
10320
|
const changes = core.diff(before, after);
|
|
8690
10321
|
if (!changes.length) return { status: "no-change", pr: n, message: `#${n} already has these values.` };
|
|
8691
|
-
const prevErrors = new Set(core.validate(before).map(
|
|
8692
|
-
const newErrors = core.validate(after).filter((i) => i.level === "error" && !prevErrors.has(
|
|
10322
|
+
const prevErrors = new Set(core.validate(before).map(issueKey3));
|
|
10323
|
+
const newErrors = core.validate(after).filter((i) => i.level === "error" && !prevErrors.has(issueKey3(i)));
|
|
8693
10324
|
const pairs = await core.readContrastPairs(at.source, config).catch(() => []);
|
|
8694
10325
|
const delta2 = pairs.length ? core.contrastDelta(core.runContrast(before, pairs, config.contrast?.modes), core.runContrast(after, pairs, config.contrast?.modes)) : [];
|
|
8695
10326
|
const newFailures = delta2.filter((d) => d.passBefore && !d.passAfter);
|
|
@@ -8786,7 +10417,7 @@ async function getReview(ctx, a) {
|
|
|
8786
10417
|
const changes = core.diff(before, after);
|
|
8787
10418
|
const pairs = await core.readContrastPairs(headSrc, config).catch(() => []);
|
|
8788
10419
|
const delta2 = pairs.length ? core.contrastDelta(core.runContrast(before, pairs, config.contrast?.modes), core.runContrast(after, pairs, config.contrast?.modes)) : [];
|
|
8789
|
-
const prevErrors = new Set(core.validate(before).map(
|
|
10420
|
+
const prevErrors = new Set(core.validate(before).map(issueKey3));
|
|
8790
10421
|
const errors = core.validate(after).filter((i) => i.level === "error");
|
|
8791
10422
|
const tokenRoot = config.tokens.root.replace(/\/$/, "");
|
|
8792
10423
|
const otherFiles = changedFiles.filter((f) => !f.startsWith(`${tokenRoot}/`) && !f.startsWith(`${config.changesets?.dir ?? ".changeset"}/`));
|
|
@@ -8804,7 +10435,7 @@ async function getReview(ctx, a) {
|
|
|
8804
10435
|
diffTable: core.tokenTable(changes.map((c) => ({ ...c, file: prefixPath(after, c.file) }))),
|
|
8805
10436
|
contrastTable: core.contrastTable(delta2),
|
|
8806
10437
|
contrastNewFailures: delta2.filter((d) => d.passBefore && !d.passAfter).length,
|
|
8807
|
-
validation: { errors: errors.length, newErrors: errors.filter((i) => !prevErrors.has(
|
|
10438
|
+
validation: { errors: errors.length, newErrors: errors.filter((i) => !prevErrors.has(issueKey3(i))) },
|
|
8808
10439
|
impact: imp
|
|
8809
10440
|
};
|
|
8810
10441
|
}
|
|
@@ -8864,7 +10495,7 @@ async function visualImpact(ctx, a, run2) {
|
|
|
8864
10495
|
if (ctx.mode !== "local" || !ctx.root) throw new Error("Visual impact needs a local checkout (start Bridge with --root): it builds Storybook and the app at both refs.");
|
|
8865
10496
|
if (!ctx.isGit) throw new Error(`${ctx.root} is not a git repository.`);
|
|
8866
10497
|
const out = a.out ? resolve3(ctx.root, a.out) : join4(ctx.root, "visual-impact");
|
|
8867
|
-
const runImpact = run2 ?? (await import("./src-
|
|
10498
|
+
const runImpact = run2 ?? (await import("./src-F2PTXW5B.js")).runImpact;
|
|
8868
10499
|
const r = await runImpact({
|
|
8869
10500
|
root: ctx.root,
|
|
8870
10501
|
base: a.base ?? "main",
|
|
@@ -9327,15 +10958,15 @@ function requestFromIssue(issue) {
|
|
|
9327
10958
|
const parsed = parseRequestIssue(issue.body);
|
|
9328
10959
|
if (parsed) return parsed;
|
|
9329
10960
|
const m = /^\s*([^:]{1,60}):\s*(.+)$/.exec(issue.title);
|
|
9330
|
-
const
|
|
9331
|
-
const
|
|
10961
|
+
const kindLabel3 = issue.labels.map((l) => /^request:\s*(.+)$/.exec(l)?.[1]?.trim()).find((k) => k && KINDS2.has(k));
|
|
10962
|
+
const sourceLabel3 = issue.labels.map((l) => /^source:\s*(.+)$/.exec(l)?.[1]?.trim()).find((s) => s === "figma" || s === "studio" || s === "cli" || s === "mcp");
|
|
9332
10963
|
return {
|
|
9333
10964
|
component: m ? m[1].trim() : "Unknown",
|
|
9334
|
-
kind:
|
|
10965
|
+
kind: kindLabel3 ?? "other",
|
|
9335
10966
|
title: m ? m[2].trim() : issue.title,
|
|
9336
10967
|
summary: issue.body.trim() || issue.title,
|
|
9337
10968
|
changes: [],
|
|
9338
|
-
source:
|
|
10969
|
+
source: sourceLabel3 ?? "studio"
|
|
9339
10970
|
};
|
|
9340
10971
|
}
|
|
9341
10972
|
function statusOf(issue, prs) {
|
|
@@ -9632,8 +11263,232 @@ async function figmaCoverage(ctx, a = {}) {
|
|
|
9632
11263
|
};
|
|
9633
11264
|
}
|
|
9634
11265
|
|
|
11266
|
+
// src/pieces.ts
|
|
11267
|
+
var pieces_exports = {};
|
|
11268
|
+
__export(pieces_exports, {
|
|
11269
|
+
missingPiecesOp: () => missingPiecesOp,
|
|
11270
|
+
openLibraryBranchOp: () => openLibraryBranchOp,
|
|
11271
|
+
productBranchOp: () => productBranchOp,
|
|
11272
|
+
switchToPublishedOp: () => switchToPublishedOp,
|
|
11273
|
+
useLibraryPreviewOp: () => useLibraryPreviewOp
|
|
11274
|
+
});
|
|
11275
|
+
var NEED = "Contents, Pull requests and Issues: read and write";
|
|
11276
|
+
var short = (p) => ({
|
|
11277
|
+
id: p.id,
|
|
11278
|
+
kind: p.kind,
|
|
11279
|
+
title: p.title,
|
|
11280
|
+
summary: p.summary,
|
|
11281
|
+
...p.component ? { component: p.component } : {},
|
|
11282
|
+
...p.nearest ? { nearest: `${p.nearest.label} (${p.nearest.token}, ${p.nearest.value})` } : {},
|
|
11283
|
+
...p.token ? { token: p.token.path } : {},
|
|
11284
|
+
where: p.where.map((w) => `${w.file}:${w.line}`),
|
|
11285
|
+
opens: p.kind === "token" ? "a token change" : p.kind === "variant" ? "a library exploration" : "a component request"
|
|
11286
|
+
});
|
|
11287
|
+
async function missingPiecesOp(ctx, a) {
|
|
11288
|
+
const n = branchNumber(a.pr);
|
|
11289
|
+
const gh = a.record ? await writer(ctx, "Recording the missing pieces", NEED) : await reader(ctx);
|
|
11290
|
+
const { config } = await ctx.config();
|
|
11291
|
+
const read = await readProductBranchOnGitHub(gh, n, { products: productsOf(config) });
|
|
11292
|
+
const [ds, set] = await Promise.all([designSystemIndex(ctx), ctx.tokens()]);
|
|
11293
|
+
const r = missingPieces({ files: read.files, ds, set });
|
|
11294
|
+
const recorded = a.record ? await recordPiecesOnGitHub(gh, n, { pieces: r.pieces, fixes: r.fixes.length, headSha: read.pull.headSha }) : null;
|
|
11295
|
+
const linked = recorded?.links ?? read.record?.links ?? [];
|
|
11296
|
+
return {
|
|
11297
|
+
pr: n,
|
|
11298
|
+
branch: read.pull.headRef,
|
|
11299
|
+
product: read.product.name,
|
|
11300
|
+
files: r.files,
|
|
11301
|
+
...read.skipped ? { skipped: `${read.skipped} more product files were not read (limit ${r.files}).` } : {},
|
|
11302
|
+
pieces: r.pieces.map((p) => ({ ...short(p), ...linked.some((l) => l.piece === p.id) ? { linked: linked.find((l) => l.piece === p.id).number } : {} })),
|
|
11303
|
+
fixes: { count: r.fixes.length, sample: r.fixes.slice(0, 10).map((f) => `${f.file}:${f.line} ${f.what} -> use ${f.use}`) },
|
|
11304
|
+
recorded: Boolean(recorded),
|
|
11305
|
+
next: r.pieces.length ? `${a.record ? "" : "Record them (record: true) so Studio shows them, then "}open a library branch for each with bridge_open_library_branch { pr: ${n}, piece: "<id>" }. Fixes are values and components the library already has: use them in the product code instead.` : "Nothing is missing from the library. Fix the values that equal a token, if any."
|
|
11306
|
+
};
|
|
11307
|
+
}
|
|
11308
|
+
function tokenWriter(ctx, gh, source) {
|
|
11309
|
+
return async (plan) => {
|
|
11310
|
+
const { config } = await ctx.config();
|
|
11311
|
+
const [before, pairs] = await Promise.all([ctx.tokens(), core.readContrastPairs(ctx.source, config).catch(() => [])]);
|
|
11312
|
+
const p = planTokenProposal({ before, pairs, edits: plan.edits, title: plan.title, summary: plan.summary, config, tool: source === "mcp" ? "Bridge MCP" : "the bridge CLI" });
|
|
11313
|
+
if (!p) throw new Error("The token already exists with that value: nothing to add.");
|
|
11314
|
+
if (p.blocked) throw new Error(`The library branch was not opened: ${p.blocked}. Change the value (bridge_propose_token_change with dryRun shows why).`);
|
|
11315
|
+
const opened = await writeTokenProposalOnGitHub(gh, p, { base: baseBranchOf(config), branchPrefix: config.governance.branchPrefix, labels: config.governance.labels, labelsAfter: [NEEDED_BY_LABEL], tool: source === "mcp" ? "Bridge MCP (source: mcp)" : "the bridge CLI" });
|
|
11316
|
+
return { number: opened.pr, url: opened.url, branch: opened.branch };
|
|
11317
|
+
};
|
|
11318
|
+
}
|
|
11319
|
+
async function openLibraryBranchOp(ctx, a) {
|
|
11320
|
+
const n = branchNumber(a.pr);
|
|
11321
|
+
const source = a.source ?? "mcp";
|
|
11322
|
+
const gh = a.dryRun ? await reader(ctx) : await writer(ctx, "Opening a library branch", NEED);
|
|
11323
|
+
const { config } = await ctx.config();
|
|
11324
|
+
const viewer = await viewerOf(ctx, gh);
|
|
11325
|
+
const s = settingsOf(config);
|
|
11326
|
+
const r = await openLibraryBranchOnGitHub(gh, {
|
|
11327
|
+
pr: n,
|
|
11328
|
+
piece: a.piece,
|
|
11329
|
+
base: s.base,
|
|
11330
|
+
products: productsOf(config),
|
|
11331
|
+
exploration: { packages: s.packages, apps: s.apps },
|
|
11332
|
+
writeTokens: tokenWriter(ctx, gh, source),
|
|
11333
|
+
...viewer ? { openedBy: viewer } : {},
|
|
11334
|
+
...source === "mcp" ? { agent: "claude" } : {},
|
|
11335
|
+
source,
|
|
11336
|
+
usePreview: a.usePreview,
|
|
11337
|
+
dryRun: a.dryRun
|
|
11338
|
+
});
|
|
11339
|
+
const plan = r.plan;
|
|
11340
|
+
const what = plan.kind === "token-change" ? "token change" : plan.kind === "exploration" ? "library exploration" : "component request";
|
|
11341
|
+
const next = r.status === "dry-run" ? `Run it without dryRun to open the ${what}.` : plan.kind === "exploration" ? `Build it on ${r.link?.branch}: ${plan.brief} Then bridge_use_library_preview { pr: ${n}, piece: "${a.piece}" } so the product branch shows it.` : plan.kind === "request" ? "A person or Claude builds the component from the request (bridge_get_request). The product branch keeps its own version until it ships." : r.preview?.status === "conflict" ? r.preview.said : "Design and tech owners review the token change. Once it ships, bridge_switch_to_published moves the product branch to the real version.";
|
|
11342
|
+
return {
|
|
11343
|
+
status: r.status,
|
|
11344
|
+
pr: n,
|
|
11345
|
+
piece: a.piece,
|
|
11346
|
+
opens: what,
|
|
11347
|
+
title: plan.title,
|
|
11348
|
+
...plan.kind === "token-change" ? { edits: plan.edits } : {},
|
|
11349
|
+
...plan.kind === "exploration" ? { exploration: plan.exploration.name, brief: plan.brief } : {},
|
|
11350
|
+
...plan.kind === "request" ? { request: plan.request.title } : {},
|
|
11351
|
+
neededBy: `${plan.neededBy.title} (#${plan.neededBy.pr}) in ${plan.neededBy.productName}`,
|
|
11352
|
+
...r.link ? { library: { kind: r.link.kind, number: r.link.number, url: r.link.url, ...r.link.branch ? { branch: r.link.branch } : {} } } : {},
|
|
11353
|
+
...r.preview ? { preview: r.preview } : {},
|
|
11354
|
+
...r.warnings.length ? { warnings: r.warnings } : {},
|
|
11355
|
+
next
|
|
11356
|
+
};
|
|
11357
|
+
}
|
|
11358
|
+
async function productBranchOp(ctx, a) {
|
|
11359
|
+
const n = branchNumber(a.pr);
|
|
11360
|
+
const gh = await reader(ctx);
|
|
11361
|
+
const { config } = await ctx.config();
|
|
11362
|
+
const viewer = await viewerOf(ctx, gh);
|
|
11363
|
+
const v = await productBranchViewOnGitHub(gh, n, { products: productsOf(config), viewer });
|
|
11364
|
+
return {
|
|
11365
|
+
pr: n,
|
|
11366
|
+
title: v.pull.title,
|
|
11367
|
+
product: v.product.name,
|
|
11368
|
+
said: `${v.sentence.lead}${v.sentence.strong}${v.sentence.rest}`,
|
|
11369
|
+
checked: v.record ? v.record.checkedAt : null,
|
|
11370
|
+
linked: v.linked.map((l) => ({
|
|
11371
|
+
piece: l.piece?.title ?? l.link.piece,
|
|
11372
|
+
library: { kind: l.link.kind, number: l.link.number, url: l.link.url },
|
|
11373
|
+
status: l.status.line,
|
|
11374
|
+
preview: l.preview.sentence,
|
|
11375
|
+
...l.preview.action ? { action: l.preview.action.label, tool: l.preview.action.kind === "switch" ? `bridge_switch_to_published { pr: ${n} }` : `bridge_use_library_preview { pr: ${n}, piece: "${l.link.piece}" }` } : {}
|
|
11376
|
+
})),
|
|
11377
|
+
notLinked: v.open.map(short),
|
|
11378
|
+
...v.record ? {} : { next: `Find its missing pieces: bridge_missing_pieces { pr: ${n}, record: true }.` }
|
|
11379
|
+
};
|
|
11380
|
+
}
|
|
11381
|
+
async function useLibraryPreviewOp(ctx, a) {
|
|
11382
|
+
const n = branchNumber(a.pr);
|
|
11383
|
+
const gh = await writer(ctx, "Using a library preview", "Contents: read and write");
|
|
11384
|
+
const { config } = await ctx.config();
|
|
11385
|
+
const v = await productBranchViewOnGitHub(gh, n, { products: productsOf(config) });
|
|
11386
|
+
const l = v.linked.find((x) => x.link.piece === a.piece);
|
|
11387
|
+
if (!l) throw new Error(`No library branch for "${a.piece}" on #${n}. Open one first: bridge_open_library_branch.`);
|
|
11388
|
+
if (l.link.kind === "request" || !l.link.branch) throw new Error(`"${l.link.title}" is a request: there is nothing to preview until it is built.`);
|
|
11389
|
+
if (l.status.key === "shipped") throw new Error("The library already shipped it: use bridge_switch_to_published instead.");
|
|
11390
|
+
if (l.status.key === "closed") throw new Error("The library branch is closed: there is nothing to preview.");
|
|
11391
|
+
const r = await useLibraryPreviewOnGitHub(gh, { productBranch: v.pull.headRef, libraryBranch: l.link.branch, libraryNumber: l.link.number });
|
|
11392
|
+
return { pr: n, piece: a.piece, library: l.link.number, ...r };
|
|
11393
|
+
}
|
|
11394
|
+
async function switchToPublishedOp(ctx, a) {
|
|
11395
|
+
const n = branchNumber(a.pr);
|
|
11396
|
+
const gh = await writer(ctx, "Switching to the published version", "Contents and Pull requests: read and write");
|
|
11397
|
+
return { pr: n, ...await switchToPublishedOnGitHub(gh, n) };
|
|
11398
|
+
}
|
|
11399
|
+
|
|
11400
|
+
// src/figma.ts
|
|
11401
|
+
var figma_exports = {};
|
|
11402
|
+
__export(figma_exports, {
|
|
11403
|
+
figmaChangesOp: () => figmaChangesOp,
|
|
11404
|
+
getFigmaChangeOp: () => getFigmaChangeOp,
|
|
11405
|
+
markFigmaDraftedOp: () => markFigmaDraftedOp,
|
|
11406
|
+
pushFigmaComponentOp: () => pushFigmaComponentOp
|
|
11407
|
+
});
|
|
11408
|
+
var NEED2 = "Contents and Pull requests: read and write";
|
|
11409
|
+
async function tokenFacts(ctx, set, component) {
|
|
11410
|
+
const { config } = await ctx.config();
|
|
11411
|
+
const text = await ctx.source.read(config.components?.specs ?? DEFAULT_COMPONENT_SPECS_FILE).catch(() => void 0);
|
|
11412
|
+
const usage = text ? specsUsage(parseComponentSpecs(text), component) : () => ({ variants: [], components: [] });
|
|
11413
|
+
return { tierOf: (p) => set.tierOf(p), usedElsewhere: usage };
|
|
11414
|
+
}
|
|
11415
|
+
async function figmaChangesOp(ctx, a = {}) {
|
|
11416
|
+
const gh = await reader(ctx);
|
|
11417
|
+
const rows = await listFigmaChangesOnGitHub(gh, { state: a.state ?? "open" });
|
|
11418
|
+
return {
|
|
11419
|
+
changes: rows.map((r) => ({ pr: r.number, title: r.title, url: r.url, branch: r.branch, component: r.origin.component, route: r.origin.route === "token-change" ? "exact token change" : "Claude AI drafts", status: r.status.label, said: r.status.sentence })),
|
|
11420
|
+
next: rows.some((r) => r.status.key === "waiting-for-claude") ? "Draft one: bridge_get_figma_change { pr } gives the spec and the steps." : rows.length ? "Owners review them on GitHub or in Bridge Studio." : "Nothing sent from Figma is open."
|
|
11421
|
+
};
|
|
11422
|
+
}
|
|
11423
|
+
async function getFigmaChangeOp(ctx, a) {
|
|
11424
|
+
const n = branchNumber(a.pr);
|
|
11425
|
+
const gh = await reader(ctx);
|
|
11426
|
+
const { config } = await ctx.config();
|
|
11427
|
+
const r = await figmaChangeOnGitHub(gh, n, { componentsDir: config.components?.dir });
|
|
11428
|
+
return {
|
|
11429
|
+
pr: n,
|
|
11430
|
+
title: r.pull.title,
|
|
11431
|
+
url: r.pull.url,
|
|
11432
|
+
branch: r.pull.headRef,
|
|
11433
|
+
component: r.origin.component,
|
|
11434
|
+
route: r.origin.route === "token-change" ? "exact token change" : "Claude AI drafts",
|
|
11435
|
+
status: r.status.label,
|
|
11436
|
+
said: r.status.sentence,
|
|
11437
|
+
...r.spec ? { spec: { path: r.origin.spec, changes: r.spec.changes, variantAxes: r.spec.variantAxes, properties: r.spec.properties, tokenEdits: r.spec.tokenEdits ?? [], reasons: r.spec.reasons ?? [] } } : {},
|
|
11438
|
+
...r.brief ? { brief: r.brief } : {},
|
|
11439
|
+
next: r.brief && r.status.key === "waiting-for-claude" ? "Follow the brief, push the code to the branch, then bridge_mark_figma_change_drafted." : r.status.sentence
|
|
11440
|
+
};
|
|
11441
|
+
}
|
|
11442
|
+
async function markFigmaDraftedOp(ctx, a) {
|
|
11443
|
+
const n = branchNumber(a.pr);
|
|
11444
|
+
const gh = await writer(ctx, "Marking a Figma change drafted", NEED2);
|
|
11445
|
+
const r = await markFigmaChangeDraftedOnGitHub(gh, n);
|
|
11446
|
+
return { pr: n, status: r.status.label, said: r.status.sentence, files: r.files, next: "Send it for review when it is ready (bridge_send_for_review), or tell the designer it is ready to look at." };
|
|
11447
|
+
}
|
|
11448
|
+
async function pushFigmaComponentOp(ctx, a) {
|
|
11449
|
+
const d = a.diff;
|
|
11450
|
+
if (!d || d.v !== 1 || typeof d.component !== "string" || !Array.isArray(d.changes)) throw new Error("The diff is not a Bridge component diff (v 1): see the spec of a branch from bridge_get_figma_change for its shape.");
|
|
11451
|
+
const source = a.source ?? "mcp";
|
|
11452
|
+
const tool = source === "mcp" ? "Bridge MCP" : "the bridge CLI";
|
|
11453
|
+
const { config } = await ctx.config();
|
|
11454
|
+
const set = await ctx.tokens();
|
|
11455
|
+
const tokens = await tokenFacts(ctx, set, d.component);
|
|
11456
|
+
const gh = a.dryRun ? await reader(ctx) : await writer(ctx, "Sending a component change from Figma", NEED2);
|
|
11457
|
+
const pairs = await core.readContrastPairs(ctx.source, config).catch(() => []);
|
|
11458
|
+
const aliasOfPath = (p) => aliasOf(set.defsOf(p)[0]?.value) ?? void 0;
|
|
11459
|
+
const r = await openComponentPushOnGitHub(gh, {
|
|
11460
|
+
diff: { ...d, source: d.source ?? source },
|
|
11461
|
+
tokens,
|
|
11462
|
+
base: baseBranchOf(config),
|
|
11463
|
+
branchPrefix: config.governance.branchPrefix,
|
|
11464
|
+
labels: [...config.governance.labels, `source: ${source}`],
|
|
11465
|
+
tool,
|
|
11466
|
+
dryRun: a.dryRun,
|
|
11467
|
+
writeTokens: async ({ edits, title, summary: summary2, bodyExtra }) => {
|
|
11468
|
+
const all = [...pairs, ...componentContrastPairs(edits, pairs, { aliasOf: aliasOfPath, paths: set.allPaths() })];
|
|
11469
|
+
const p = planTokenProposal({ before: set, pairs: all, edits, title, summary: summary2, config, tool, bodyExtra });
|
|
11470
|
+
if (!p) throw new Error("The library already paints it this way: nothing to send.");
|
|
11471
|
+
if (p.blocked) throw new Error(`Not sent: ${p.blocked}. Pick another variable.`);
|
|
11472
|
+
const opened = await writeTokenProposalOnGitHub(gh, p, { base: baseBranchOf(config), branchPrefix: config.governance.branchPrefix, labels: config.governance.labels, labelsAfter: [`source: ${source}`], tool: `${tool} (source: ${source})` });
|
|
11473
|
+
return { number: opened.pr, url: opened.url, branch: opened.branch };
|
|
11474
|
+
}
|
|
11475
|
+
});
|
|
11476
|
+
const plan = r.plan;
|
|
11477
|
+
return {
|
|
11478
|
+
status: r.status,
|
|
11479
|
+
component: d.component,
|
|
11480
|
+
route: plan.route === "token-change" ? "exact token change" : plan.route === "claude-draft" ? "Claude AI drafts" : "fix in Figma",
|
|
11481
|
+
said: plan.said,
|
|
11482
|
+
...plan.edits.length ? { edits: plan.edits } : {},
|
|
11483
|
+
...plan.reasons.length ? { reasons: plan.reasons } : {},
|
|
11484
|
+
...plan.issues.length ? { fixInFigma: plan.issues } : {},
|
|
11485
|
+
...r.link ? { library: r.link } : {},
|
|
11486
|
+
next: r.status === "dry-run" ? "Run it without dryRun to open the library branch." : r.status === "not-sent" ? "Nothing was sent: fix these in Figma, then push again." : plan.route === "claude-draft" ? `Draft the code: bridge_get_figma_change { pr: ${r.link.number} }.` : "Design and tech owners review the token change. Once it ships, a pull in the DS Bridge plugin rebuilds the component."
|
|
11487
|
+
};
|
|
11488
|
+
}
|
|
11489
|
+
|
|
9635
11490
|
// src/server.ts
|
|
9636
|
-
var VERSION = true ? "0.
|
|
11491
|
+
var VERSION = true ? "0.6.0" : "dev";
|
|
9637
11492
|
var ok = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
|
|
9638
11493
|
var fail = (e) => ({ content: [{ type: "text", text: `Error: ${e.message ?? String(e)}` }], isError: true });
|
|
9639
11494
|
var run = (fn) => async (a) => {
|
|
@@ -9673,6 +11528,8 @@ function createServer(ctx) {
|
|
|
9673
11528
|
`Change texts from Figma and docs from Bridge Studio: when the user says "${DRAFT_PHRASE}" (or asks you to draft their Bridge changes or docs), call bridge_pending_drafts. For change items, look at the before and after images and write each title, summary and change line in the Bridge voice. For docs items (scope "docs"), follow the brief: write only the sections it asks for, grounded in the props, CSS module classes and source it quotes, never an invented prop or behavior, never anatomy. Then call bridge_write_drafts. The designer reviews them in the DS Bridge Figma plugin or on the Library page.`,
|
|
9674
11529
|
"Studio parity: a designer can do in Claude what they do in Bridge Studio. Branches in review: bridge_what_needs_me (the queue), bridge_branch_status, bridge_send_for_review, bridge_back_to_draft, bridge_review_branch, bridge_ask_someone_else, bridge_comment, bridge_update_from_main (token conflicts too), bridge_try_branch (try it in a product, share it), bridge_combine_branches, bridge_close_branch, bridge_reopen_branch, bridge_merge_branch (same gate as Studio, owner override with a reason). Docs: bridge_save_docs. Writes act under the person's GitHub token: only do them when the person asked, and merge only when they explicitly asked to merge.",
|
|
9675
11530
|
'Products: work starts for a product (bridge_list_products). Tools that start work take "product"; without it Bridge uses the product of the current folder or repo. Ask the person when it is not obvious.',
|
|
11531
|
+
"Product branches and the library: when a designer explores a product feature with you (an exploration for a product), check it with bridge_missing_pieces (record: true) before they share it; for each piece the library lacks, offer bridge_open_library_branch, which opens the library side linked to their branch. bridge_product_branch says where each library branch stands; bridge_switch_to_published moves the product branch to the real version once the library ships.",
|
|
11532
|
+
"Components from Figma: the DS Bridge plugin sends a component changed in Figma to a library branch. Token swaps go as exact token changes; new variants, properties and components go to a branch carrying the spec, waiting for Claude AI. When the person asks you to draft one, bridge_figma_changes lists them and bridge_get_figma_change gives the spec and the steps; write the code on that branch, push it, then bridge_mark_figma_change_drafted. Never claim a branch is drafted before its code is pushed.",
|
|
9676
11533
|
'Explorations try experimental variables and components in a product staging branch without touching main: an explore/<slug> branch with a draft pull request labeled exploration, previews and a snapshot package. bridge_start_exploration starts one, bridge_list_explorations lists them. To work on one, pass "branch": "explore/<slug>" to bridge_propose_token_change, bridge_draft_docs and bridge_get_request, and never open a pull request to main from it: a person promotes it to review or closes it.'
|
|
9677
11534
|
].join(" ")
|
|
9678
11535
|
}
|
|
@@ -10170,6 +12027,97 @@ function createServer(ctx) {
|
|
|
10170
12027
|
const Pr = z.union([z.number().int().positive(), z.string().min(1)]).describe('The branch number Studio shows (12 or "#12"), or its GitHub link.');
|
|
10171
12028
|
const WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
|
|
10172
12029
|
const READ = { readOnlyHint: true, openWorldHint: true };
|
|
12030
|
+
const Piece = z.string().min(1).describe('The piece id bridge_missing_pieces returned ("badge-overdue-tone").');
|
|
12031
|
+
server.registerTool(
|
|
12032
|
+
"bridge_missing_pieces",
|
|
12033
|
+
{
|
|
12034
|
+
title: "Find what a product branch adds outside the library",
|
|
12035
|
+
description: 'Check a product branch (an exploration started for a product) against the library: the adoption audit on each product file it changes, at its start and at its head, keeps what the branch added. New hardcoded values become token pieces beside the closest token, values written on a library component become a variant of it ("Badge: overdue tone"), components from another UI library with no library equivalent become new components. Values equal to a token and components the library has are fixes, not pieces. With record: true, saves the result in the branch description, where Bridge Studio shows it (needs GITHUB_TOKEN, pull requests: write). Run it when a designer finishes exploring with you, then offer to open a library branch for each piece.',
|
|
12036
|
+
inputSchema: { pr: Pr, record: z.boolean().optional().describe("Save the pieces in the product branch description (Studio reads them there). Keeps the library branches already opened.") },
|
|
12037
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
12038
|
+
},
|
|
12039
|
+
run((a) => missingPiecesOp(ctx, a))
|
|
12040
|
+
);
|
|
12041
|
+
server.registerTool(
|
|
12042
|
+
"bridge_open_library_branch",
|
|
12043
|
+
{
|
|
12044
|
+
title: "Open a library branch for a missing piece",
|
|
12045
|
+
description: 'Turn one recorded missing piece into a library branch linked to the product branch, as Bridge Studio does: a token piece opens a token change (same gate and body as bridge_propose_token_change, a draft for design and tech review) and the product branch takes its preview; a variant opens a library exploration with the brief to build it (build it there, then bridge_use_library_preview); a new component opens a component request. Both sides link: the library side says "Needed by" the product branch (label needed-by-product), the product branch records it. Needs GITHUB_TOKEN (contents, pull requests and issues: write). dryRun shows what it would open. Only when the person asked.',
|
|
12046
|
+
inputSchema: { pr: Pr, piece: Piece, usePreview: z.boolean().optional().describe("Merge the new library branch into the product branch when it already changes something (default true)."), dryRun: z.boolean().optional() },
|
|
12047
|
+
annotations: WRITE
|
|
12048
|
+
},
|
|
12049
|
+
run((a) => openLibraryBranchOp(ctx, { ...a, source: "mcp" }))
|
|
12050
|
+
);
|
|
12051
|
+
server.registerTool(
|
|
12052
|
+
"bridge_product_branch",
|
|
12053
|
+
{
|
|
12054
|
+
title: "Product branch and its library branches",
|
|
12055
|
+
description: "A product branch as Studio shows it: the summary sentence, each linked library branch with its status and reviewers, what its preview means for the product branch and the next action, and the pieces with no library branch yet.",
|
|
12056
|
+
inputSchema: { pr: Pr },
|
|
12057
|
+
annotations: READ
|
|
12058
|
+
},
|
|
12059
|
+
run((a) => productBranchOp(ctx, a))
|
|
12060
|
+
);
|
|
12061
|
+
server.registerTool(
|
|
12062
|
+
"bridge_use_library_preview",
|
|
12063
|
+
{
|
|
12064
|
+
title: "Use a library branch preview in a product branch",
|
|
12065
|
+
description: "The product branch takes the latest commits of the library branch of a piece (Bridge merges it in), so the product preview shows the real library change before it ships. A conflict is left to a developer. Needs GITHUB_TOKEN (contents: write). Only when the person asked.",
|
|
12066
|
+
inputSchema: { pr: Pr, piece: Piece },
|
|
12067
|
+
annotations: WRITE
|
|
12068
|
+
},
|
|
12069
|
+
run((a) => useLibraryPreviewOp(ctx, a))
|
|
12070
|
+
);
|
|
12071
|
+
server.registerTool(
|
|
12072
|
+
"bridge_switch_to_published",
|
|
12073
|
+
{
|
|
12074
|
+
title: "Switch a product branch to the published version",
|
|
12075
|
+
description: "Once its library branches shipped, bring main into the product branch (GitHub update branch): it stops carrying the library change and uses the shipped one. Token conflicts go through bridge_update_from_main. Needs GITHUB_TOKEN (contents and pull requests: write). Only when the person asked.",
|
|
12076
|
+
inputSchema: { pr: Pr },
|
|
12077
|
+
annotations: WRITE
|
|
12078
|
+
},
|
|
12079
|
+
run((a) => switchToPublishedOp(ctx, a))
|
|
12080
|
+
);
|
|
12081
|
+
server.registerTool(
|
|
12082
|
+
"bridge_figma_changes",
|
|
12083
|
+
{
|
|
12084
|
+
title: "Component changes sent from Figma",
|
|
12085
|
+
description: "The library branches the DS Bridge Figma plugin opened for components changed in Figma (label from-figma), with where each stands: an exact token change (no AI, contrast gated), or a branch carrying the spec, waiting for Claude AI to draft the code, then drafted by Claude AI and reviewed by a person.",
|
|
12086
|
+
inputSchema: { state: z.enum(["open", "closed", "all"]).optional().describe("Default open.") },
|
|
12087
|
+
annotations: READ
|
|
12088
|
+
},
|
|
12089
|
+
run((a) => figmaChangesOp(ctx, a))
|
|
12090
|
+
);
|
|
12091
|
+
server.registerTool(
|
|
12092
|
+
"bridge_get_figma_change",
|
|
12093
|
+
{
|
|
12094
|
+
title: "Get a component change from Figma, to draft it",
|
|
12095
|
+
description: "One library branch sent from Figma: its spec (variant matrix, properties, bound variables per variant, layers, what code gave Figma before, the exact token swaps) and the brief to draft the code on that branch. Check out the branch, write the TSX, CSS module, examples, docs and changeset, run npm run specs and the tests, push, then call bridge_mark_figma_change_drafted.",
|
|
12096
|
+
inputSchema: { pr: Pr },
|
|
12097
|
+
annotations: READ
|
|
12098
|
+
},
|
|
12099
|
+
run((a) => getFigmaChangeOp(ctx, a))
|
|
12100
|
+
);
|
|
12101
|
+
server.registerTool(
|
|
12102
|
+
"bridge_mark_figma_change_drafted",
|
|
12103
|
+
{
|
|
12104
|
+
title: "Mark a Figma change drafted by Claude AI",
|
|
12105
|
+
description: "After you pushed the code for a component change from Figma to its branch: label it drafted-by-claude (no longer waiting-for-claude) and say so in its description, for a person to review. Refused while the branch only carries the spec. Needs GITHUB_TOKEN (pull requests: write).",
|
|
12106
|
+
inputSchema: { pr: Pr },
|
|
12107
|
+
annotations: WRITE
|
|
12108
|
+
},
|
|
12109
|
+
run((a) => markFigmaDraftedOp(ctx, a))
|
|
12110
|
+
);
|
|
12111
|
+
server.registerTool(
|
|
12112
|
+
"bridge_push_figma_component",
|
|
12113
|
+
{
|
|
12114
|
+
title: "Send a component change from Figma",
|
|
12115
|
+
description: "What the DS Bridge plugin Push tab does, from a structured diff of a Figma component (the v 1 spec shape: component, figmaName, coded, kind, title, summary, changes, rebinds, needsCode, unknown, variantAxes, properties, bindings, anatomy). An exact token swap on a component token used nowhere else opens a token change (contrast gated, reviewed like any token change); anything that needs code opens a draft branch with the spec, waiting for Claude AI; detached layers, raw values and variables the library lacks are listed with what to do and never sent. dryRun shows the route. Needs GITHUB_TOKEN (contents and pull requests: write). Only when the person asked.",
|
|
12116
|
+
inputSchema: { diff: z.record(z.string(), z.unknown()).describe("The structured component diff (v 1)."), dryRun: z.boolean().optional() },
|
|
12117
|
+
annotations: WRITE
|
|
12118
|
+
},
|
|
12119
|
+
run((a) => pushFigmaComponentOp(ctx, { diff: a.diff, dryRun: a.dryRun, source: "mcp" }))
|
|
12120
|
+
);
|
|
10173
12121
|
server.registerTool(
|
|
10174
12122
|
"bridge_list_products",
|
|
10175
12123
|
{
|
|
@@ -10406,8 +12354,8 @@ function createServer(ctx) {
|
|
|
10406
12354
|
new ResourceTemplate("bridge://components/{name}", {
|
|
10407
12355
|
list: async () => {
|
|
10408
12356
|
const { config } = await ctx.config();
|
|
10409
|
-
const
|
|
10410
|
-
return { resources:
|
|
12357
|
+
const names3 = await listComponentNames(ctx.source, config);
|
|
12358
|
+
return { resources: names3.map((n) => ({ uri: `bridge://components/${n.name}`, name: n.name, mimeType: "application/json" })) };
|
|
10411
12359
|
},
|
|
10412
12360
|
complete: {
|
|
10413
12361
|
name: async (value) => {
|
|
@@ -10595,6 +12543,8 @@ export {
|
|
|
10595
12543
|
requests_exports,
|
|
10596
12544
|
drafts_exports,
|
|
10597
12545
|
studio_exports,
|
|
12546
|
+
pieces_exports,
|
|
12547
|
+
figma_exports,
|
|
10598
12548
|
VERSION,
|
|
10599
12549
|
createServer,
|
|
10600
12550
|
serveStdio,
|