@bridge-ds/mcp 0.5.0 → 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 +1 -1
- package/dist/{chunk-QTO4EP6V.js → chunk-E4D5KPIQ.js} +25 -6
- package/dist/{chunk-MHCXMHCL.js → chunk-M7DUTFIQ.js} +1515 -421
- package/dist/figma.d.ts +83 -0
- package/dist/index.js +2 -2
- package/dist/lib.d.ts +1 -0
- package/dist/lib.js +4 -2
- package/dist/pieces.d.ts +1 -1
- 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) {
|
|
@@ -2741,7 +2748,7 @@ function toOklch(rgb) {
|
|
|
2741
2748
|
return { L, C, h };
|
|
2742
2749
|
}
|
|
2743
2750
|
var isHex = (s) => typeof s === "string" && /^#[0-9a-f]{6}$/i.test(s.trim());
|
|
2744
|
-
var fromHex = (
|
|
2751
|
+
var fromHex = (hex2) => [1, 3, 5].map((i) => parseInt(hex2.slice(i, i + 2), 16) / 255);
|
|
2745
2752
|
var round3 = (x) => Math.round(x * 1e3) / 1e3;
|
|
2746
2753
|
var toHex = (rgb) => "#" + rgb.map((x) => Math.round(x * 255).toString(16).padStart(2, "0")).join("");
|
|
2747
2754
|
function dtcgColor(rgb, alpha) {
|
|
@@ -2763,20 +2770,20 @@ function nearestStep(rgb) {
|
|
|
2763
2770
|
for (const s of STEPS) if (Math.abs(LIGHTNESS[s] - L) < Math.abs(LIGHTNESS[best] - L)) best = s;
|
|
2764
2771
|
return best;
|
|
2765
2772
|
}
|
|
2766
|
-
function anchoredRamp(
|
|
2767
|
-
const rgb = fromHex(
|
|
2773
|
+
function anchoredRamp(hex2, pinStep) {
|
|
2774
|
+
const rgb = fromHex(hex2);
|
|
2768
2775
|
const { L, C, h } = toOklch(rgb);
|
|
2769
2776
|
const step = pinStep ?? nearestStep(rgb);
|
|
2770
2777
|
const peak = Math.min(0.24, Math.max(0.02, C / BELL[step]));
|
|
2771
|
-
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() } }) };
|
|
2772
2779
|
}
|
|
2773
2780
|
function neutralOffset() {
|
|
2774
2781
|
const off = {};
|
|
2775
|
-
for (const [s,
|
|
2782
|
+
for (const [s, hex2] of Object.entries(NEUTRAL_ANCHORS)) off[s] = toOklch(fromHex(hex2)).L - LIGHTNESS[s];
|
|
2776
2783
|
return (s) => off[s] ?? 0;
|
|
2777
2784
|
}
|
|
2778
|
-
function tintedNeutral(
|
|
2779
|
-
const rgb = fromHex(
|
|
2785
|
+
function tintedNeutral(hex2) {
|
|
2786
|
+
const rgb = fromHex(hex2);
|
|
2780
2787
|
const { C, h } = toOklch(rgb);
|
|
2781
2788
|
const ref = neutralChroma()(nearestStep(rgb));
|
|
2782
2789
|
const scale = C < 4e-3 ? 0 : Math.min(2.5, C / ref);
|
|
@@ -2850,19 +2857,19 @@ function rampFor(input, pinStep, kind) {
|
|
|
2850
2857
|
const ramp = kind === "neutral" ? generateRamp({ hue: hue2, chroma: neutralChroma(), offset: neutralOffset() }) : generateRamp({ hue: hue2, chroma: bellChroma(DEFAULT_PRIMARY_CHROMA) });
|
|
2851
2858
|
return { ramp, anchor: `OKLCH hue ${Math.round(hue2)}` };
|
|
2852
2859
|
}
|
|
2853
|
-
const
|
|
2854
|
-
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).`);
|
|
2855
2862
|
if (kind === "neutral") {
|
|
2856
|
-
const n = tintedNeutral(
|
|
2857
|
-
return { ramp: n.ramp, anchor: `${
|
|
2863
|
+
const n = tintedNeutral(hex2);
|
|
2864
|
+
return { ramp: n.ramp, anchor: `${hex2.toUpperCase()} (hue ${Math.round(n.hue)})` };
|
|
2858
2865
|
}
|
|
2859
|
-
const a = anchoredRamp(
|
|
2866
|
+
const a = anchoredRamp(hex2, pinStep);
|
|
2860
2867
|
const issue = a.distance > PIN_TOLERANCE ? {
|
|
2861
2868
|
level: "warning",
|
|
2862
2869
|
code: "pin-off-ladder",
|
|
2863
|
-
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.`
|
|
2864
2871
|
} : void 0;
|
|
2865
|
-
return { ramp: a.ramp, anchor: `${
|
|
2872
|
+
return { ramp: a.ramp, anchor: `${hex2.toUpperCase()} pinned at ${a.step}`, ...issue ? { issue } : {} };
|
|
2866
2873
|
}
|
|
2867
2874
|
function aliasRamp(doc, role, ramp, description) {
|
|
2868
2875
|
const group = getAt(doc, `palette.${role}`);
|
|
@@ -3401,7 +3408,23 @@ function patchDocsJson(original, next) {
|
|
|
3401
3408
|
function cleanDraftText(text) {
|
|
3402
3409
|
return text.replace(/\s*\u2014\s*/g, ", ").replace(/,\s*,/g, ",").replace(/^,\s*/, "").replace(/[ \t]+/g, " ").trim();
|
|
3403
3410
|
}
|
|
3411
|
+
var andList = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
|
|
3404
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";
|
|
3405
3428
|
var DRAFT_LINE_MAX = 120;
|
|
3406
3429
|
function clipText(text, max) {
|
|
3407
3430
|
const t = text.replace(/\s+/g, " ").trim().replace(/\.$/, "");
|
|
@@ -3445,8 +3468,8 @@ function validateDocsTask(x) {
|
|
|
3445
3468
|
}
|
|
3446
3469
|
var docsTaskKeys = (t) => t.kind === "sections" ? t.keys : [t.key];
|
|
3447
3470
|
function parseDocsReply(text) {
|
|
3448
|
-
const
|
|
3449
|
-
const body =
|
|
3471
|
+
const fenced2 = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
|
|
3472
|
+
const body = fenced2 ? fenced2[1] : text;
|
|
3450
3473
|
const start = body.indexOf("{");
|
|
3451
3474
|
const end = body.lastIndexOf("}");
|
|
3452
3475
|
if (start < 0 || end <= start) throw new Error("The reply holds no JSON object.");
|
|
@@ -3985,13 +4008,13 @@ function draftItemBrief(i) {
|
|
|
3985
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");
|
|
3986
4009
|
}
|
|
3987
4010
|
const kind = i.kind ? REQUEST_KINDS.find((k) => k.value === i.kind)?.label : void 0;
|
|
3988
|
-
const
|
|
4011
|
+
const cell4 = (s) => (s ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
3989
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"}.`];
|
|
3990
4013
|
if (i.component) lines2.push(`Component: ${i.component}`);
|
|
3991
4014
|
if (kind) lines2.push(`Kind: ${kind}`);
|
|
3992
4015
|
if (i.components?.length) lines2.push(`Components with a change request: ${i.components.join(", ")}`);
|
|
3993
|
-
if (i.changes.length) lines2.push("", "| What | Now | In Figma |", "| --- | --- | --- |", ...i.changes.map((c) => `| ${
|
|
3994
|
-
if (i.variables?.length) lines2.push("", "| Token | Now | In Figma |", "| --- | --- | --- |", ...i.variables.map((v) => `| ${
|
|
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"} |`));
|
|
3995
4018
|
if (!i.changes.length && !i.variables?.length && !i.lines?.length) lines2.push("", "No change detected: rely on the notes and the images.");
|
|
3996
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}`);
|
|
3997
4020
|
if (i.notes?.trim()) lines2.push("", "The designer's notes (keep their intent):", i.notes.trim());
|
|
@@ -4614,321 +4637,251 @@ function branchWithJiraKey(branch, key) {
|
|
|
4614
4637
|
return i < 0 ? `${key}-${branch}` : `${branch.slice(0, i + 1)}${key}-${branch.slice(i + 1)}`;
|
|
4615
4638
|
}
|
|
4616
4639
|
|
|
4617
|
-
// ../core/src/
|
|
4618
|
-
var
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
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 };
|
|
4652
4723
|
};
|
|
4653
4724
|
}
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
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 });
|
|
4664
4735
|
}
|
|
4665
|
-
}
|
|
4666
|
-
return
|
|
4736
|
+
}
|
|
4737
|
+
return [...out.values()];
|
|
4667
4738
|
}
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
${
|
|
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
|
+
}
|
|
4674
4755
|
}
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
const
|
|
4678
|
-
|
|
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");
|
|
4679
4761
|
}
|
|
4680
|
-
function
|
|
4681
|
-
|
|
4682
|
-
|
|
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}` : ""}`;
|
|
4683
4773
|
}
|
|
4684
|
-
|
|
4685
|
-
|
|
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
|
+
`;
|
|
4686
4780
|
}
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
await gh.getPullRequest(open.number).then((pr) => gh.updatePullRequest(open.number, { body: withAuthorLine(pr.body, line) })).catch(() => void 0);
|
|
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;
|
|
4695
4788
|
}
|
|
4696
|
-
return sha;
|
|
4697
4789
|
}
|
|
4698
|
-
function
|
|
4699
|
-
const
|
|
4700
|
-
|
|
4701
|
-
const
|
|
4702
|
-
const
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
const branch = `docs/${slug2}-${slugDate(opts.date)}-${opts.date.getTime().toString(36).slice(-4)}`;
|
|
4706
|
-
const csPath = `${opts.changesetDir ?? ".changeset"}/docs-${slug2}-${opts.date.getTime().toString(36)}.md`;
|
|
4707
|
-
const pkg = opts.packageName ?? "@bridge-ds/react";
|
|
4708
|
-
const changeset = `---
|
|
4709
|
-
"${pkg}": patch
|
|
4710
|
-
---
|
|
4711
|
-
|
|
4712
|
-
Usage guidance for ${list}.
|
|
4713
|
-
`;
|
|
4714
|
-
const files = [...edits.map((e) => ({ path: e.path, content: e.after })), { path: csPath, content: changeset }];
|
|
4715
|
-
const risk = classifyRisk({ files: files.map((f) => ({ path: f.path, status: "modified" })) });
|
|
4716
|
-
const rows = edits.map((e) => {
|
|
4717
|
-
let state = "edited";
|
|
4718
|
-
try {
|
|
4719
|
-
const d = JSON.parse(e.after);
|
|
4720
|
-
const gaps = missingProse(d);
|
|
4721
|
-
state = gaps.length ? `still missing: ${gaps.join(", ")}` : "every section filled";
|
|
4722
|
-
} catch {
|
|
4723
|
-
state = "not valid JSON";
|
|
4724
|
-
}
|
|
4725
|
-
return `- \`${e.path}\` (${e.before === null ? "new" : "changed"}, ${state})`;
|
|
4726
|
-
});
|
|
4727
|
-
const body = [
|
|
4728
|
-
`## Docs change`,
|
|
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}).`,
|
|
4729
4797
|
"",
|
|
4730
|
-
|
|
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()}`] : [],
|
|
4731
4802
|
"",
|
|
4732
|
-
|
|
4803
|
+
d.summary.trim(),
|
|
4733
4804
|
"",
|
|
4734
|
-
|
|
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}`)] : [],
|
|
4735
4813
|
"",
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
${
|
|
4739
|
-
|
|
4740
|
-
"
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
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");
|
|
4744
4825
|
}
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
const pr = await gh.createPullRequest({ title: prTitle, head: branch, base: opts.base, body: p.body, draft: false });
|
|
4765
|
-
await gh.addLabels(pr.number, [...p.labels, ...opts.extraLabels ?? []]).catch(() => false);
|
|
4766
|
-
return { number: pr.number, url: pr.url, branch, title: prTitle, continued: false, unchanged: false };
|
|
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
|
+
);
|
|
4767
4845
|
}
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
if (
|
|
4773
|
-
|
|
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 };
|
|
4774
4854
|
}
|
|
4775
4855
|
|
|
4776
|
-
// ../core/src/
|
|
4777
|
-
var
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
}
|
|
4782
|
-
|
|
4783
|
-
if (!
|
|
4784
|
-
const
|
|
4785
|
-
if (
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
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;
|
|
4789
4870
|
}
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
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;
|
|
4794
4877
|
}
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
case "failed":
|
|
4803
|
-
return "The preview version could not be prepared for the latest update. Update the branch to try again, or ask a developer to run it again on GitHub.";
|
|
4804
|
-
case "no-packages":
|
|
4805
|
-
return "This branch does not touch the library tokens or components, so there is no preview version to install.";
|
|
4806
|
-
case "shipped":
|
|
4807
|
-
return s.stillWorks === true ? "This branch was merged. Its preview version still installs, and products get it with the next library release." : s.stillWorks === false ? "This branch was merged, so its preview version was removed. Products get it with the next library release." : "This branch was merged. Products get it with the next library release.";
|
|
4808
|
-
case "closed":
|
|
4809
|
-
return s.stillWorks === true ? "This branch was closed without merging. Its preview version still installs, but it will not reach the library." : s.stillWorks === false ? "This branch was closed without merging, so its preview version was removed." : "This branch was closed without merging.";
|
|
4810
|
-
}
|
|
4811
|
-
}
|
|
4812
|
-
var quote = (t) => `"${t.trim().replace(/"/g, "'")}"`;
|
|
4813
|
-
function shareMessage(x) {
|
|
4814
|
-
const back = stableCommand(x.packages);
|
|
4815
|
-
const s = x.state;
|
|
4816
|
-
if (s.kind === "ready") {
|
|
4817
|
-
const lasts = s.source === "pkg.pr.new" ? "This preview stays available. Each update to the change makes a new one: ask me for the latest command." : "This preview is replaced on each update to the change and removed when it ships or closes: ask me for the latest command if the install fails.";
|
|
4818
|
-
return {
|
|
4819
|
-
subject: `Try the design system change ${quote(x.title)} before it ships`,
|
|
4820
|
-
body: [
|
|
4821
|
-
"Hi,",
|
|
4822
|
-
"",
|
|
4823
|
-
`Could you try the design system change ${quote(x.title)} in our product before it ships?`,
|
|
4824
|
-
...x.tryUrl ? ["", `See it in the demo product first: ${x.tryUrl}`] : [],
|
|
4825
|
-
"",
|
|
4826
|
-
"Install the preview version in a staging branch (not production):",
|
|
4827
|
-
s.command,
|
|
4828
|
-
"",
|
|
4829
|
-
lasts,
|
|
4830
|
-
"",
|
|
4831
|
-
"To go back to the stable version:",
|
|
4832
|
-
back,
|
|
4833
|
-
"",
|
|
4834
|
-
`Details for developers: ${x.url}`
|
|
4835
|
-
].join("\n")
|
|
4836
|
-
};
|
|
4837
|
-
}
|
|
4838
|
-
if ((s.kind === "shipped" || s.kind === "closed") && s.command) {
|
|
4839
|
-
const what = s.kind === "shipped" ? `The design system change ${quote(x.title)} has shipped. You get it with the next library release.` : `The design system change ${quote(x.title)} was closed without shipping.`;
|
|
4840
|
-
const preview = s.stillWorks === false ? "Its preview version was removed, so reinstalling it fails." : s.stillWorks === true ? "Its preview version still installs, but it is not the released version." : "";
|
|
4841
|
-
return {
|
|
4842
|
-
subject: `Design system change ${quote(x.title)} ${s.kind === "shipped" ? "has shipped" : "was closed"}`,
|
|
4843
|
-
body: ["Hi,", "", [what, preview].filter(Boolean).join(" "), "", "If you installed its preview, go back to the stable version:", back, "", `Details for developers: ${x.url}`].join("\n")
|
|
4844
|
-
};
|
|
4845
|
-
}
|
|
4846
|
-
return null;
|
|
4847
|
-
}
|
|
4848
|
-
|
|
4849
|
-
// ../core/src/try-in-product.ts
|
|
4850
|
-
function tryInProductUrl(preview, pr, screen) {
|
|
4851
|
-
const url = new URL(preview);
|
|
4852
|
-
url.pathname = "/";
|
|
4853
|
-
url.search = "";
|
|
4854
|
-
url.searchParams.set("bridge", String(pr));
|
|
4855
|
-
if (screen) {
|
|
4856
|
-
url.searchParams.set("tenant", screen.tenant);
|
|
4857
|
-
url.searchParams.set("theme", screen.theme);
|
|
4858
|
-
}
|
|
4859
|
-
url.hash = screen ? screen.route : "";
|
|
4860
|
-
return url.href;
|
|
4861
|
-
}
|
|
4862
|
-
|
|
4863
|
-
// ../core/src/combine-branches.ts
|
|
4864
|
-
var MIN_TOGETHER = 2;
|
|
4865
|
-
var MAX_TOGETHER = 4;
|
|
4866
|
-
function onlyTokens(files, tokensRoot) {
|
|
4867
|
-
const root = `${tokensRoot.replace(/^\/+|\/+$/g, "")}/`;
|
|
4868
|
-
return files.length > 0 && files.every((f) => f.startsWith(root) && f.endsWith(".tokens.json") || /contrast\.checks\.json$/.test(f) || f.startsWith(".changeset/"));
|
|
4869
|
-
}
|
|
4870
|
-
function tokenFilesOf(files, tokensRoot) {
|
|
4871
|
-
const root = `${tokensRoot.replace(/^\/+|\/+$/g, "")}/`;
|
|
4872
|
-
return files.filter((f) => f.startsWith(root) && f.endsWith(".tokens.json")).map((f) => f.slice(root.length));
|
|
4873
|
-
}
|
|
4874
|
-
var changeList = (numbers) => numbers.join(",");
|
|
4875
|
-
function tryTogetherUrl(admin, numbers, picks) {
|
|
4876
|
-
const url = new URL(admin);
|
|
4877
|
-
url.pathname = "/";
|
|
4878
|
-
url.hash = "";
|
|
4879
|
-
const use = encodePicks(picks);
|
|
4880
|
-
url.search = `?bridge=${changeList(numbers)}${use ? `&bridge-use=${encodeURIComponent(use)}` : ""}`;
|
|
4881
|
-
return url.href;
|
|
4882
|
-
}
|
|
4883
|
-
var COMBINE_COMPARE_LIMIT = 300;
|
|
4884
|
-
async function readCombineBranch(gh, pull, opts) {
|
|
4885
|
-
const compare = await gh.compareDetailed(opts.base, pull.headSha);
|
|
4886
|
-
const files = compare.files;
|
|
4887
|
-
const names3 = files.flatMap((f) => f.previous_filename ? [f.filename, f.previous_filename] : [f.filename]);
|
|
4888
|
-
const start = compare.mergeBase;
|
|
4889
|
-
if (!start || files.length >= COMBINE_COMPARE_LIMIT || !onlyTokens(names3, opts.tokensRoot)) return { number: pull.number, title: pull.title, tokens: false, edits: null };
|
|
4890
|
-
const root = opts.tokensRoot.replace(/^\/+|\/+$/g, "");
|
|
4891
|
-
const tokenFiles = files.filter((f) => tokenFilesOf([f.filename], root).length);
|
|
4892
|
-
const source = { number: pull.number, base: {}, head: {}, files: tokenFiles.map((f) => tokenFilesOf([f.filename], root)[0]) };
|
|
4893
|
-
await Promise.all(
|
|
4894
|
-
tokenFiles.map(async (f) => {
|
|
4895
|
-
const rel = tokenFilesOf([f.filename], root)[0];
|
|
4896
|
-
source.base[rel] = f.status === "added" ? void 0 : await gh.readFile(f.previous_filename ?? f.filename, start);
|
|
4897
|
-
source.head[rel] = f.status === "removed" ? void 0 : await gh.readFile(f.filename, pull.headSha);
|
|
4898
|
-
})
|
|
4899
|
-
);
|
|
4900
|
-
return { number: pull.number, title: pull.title, tokens: true, edits: changeEdits(source) };
|
|
4901
|
-
}
|
|
4902
|
-
|
|
4903
|
-
// ../core/src/prod-branch.ts
|
|
4904
|
-
var PIECES_MARK = "bridge-pieces";
|
|
4905
|
-
var NEEDED_MARK = "bridge-needed-by";
|
|
4906
|
-
var SECTION = "### Missing from the library";
|
|
4907
|
-
var NEEDED_BY_LABEL = "needed-by-product";
|
|
4908
|
-
var hiddenJson = (mark, data) => `<!-- ${mark} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
|
|
4909
|
-
var readHidden = (mark, body) => {
|
|
4910
|
-
if (!body) return void 0;
|
|
4911
|
-
const m = new RegExp(`<!-- ${mark} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
4912
|
-
if (!m) return void 0;
|
|
4913
|
-
try {
|
|
4914
|
-
return JSON.parse(m[1]);
|
|
4915
|
-
} catch {
|
|
4916
|
-
return void 0;
|
|
4917
|
-
}
|
|
4918
|
-
};
|
|
4919
|
-
var KIND_WORD = { "token-change": "token change", exploration: "library exploration", request: "component request" };
|
|
4920
|
-
function parsePiecesRecord(body) {
|
|
4921
|
-
const r = readHidden(PIECES_MARK, body);
|
|
4922
|
-
if (!r || !Array.isArray(r.pieces) || !Array.isArray(r.links) || typeof r.checkedAt !== "string") return void 0;
|
|
4923
|
-
return r;
|
|
4924
|
-
}
|
|
4925
|
-
function piecesSection(r) {
|
|
4926
|
-
const link = (p) => r.links.find((l) => l.piece === p.id);
|
|
4927
|
-
const lines2 = r.pieces.length ? r.pieces.map((p) => {
|
|
4928
|
-
const l = link(p);
|
|
4929
|
-
return `- **${p.title}**: ${p.summary}${l ? ` ${cap(KIND_WORD[l.kind])}: #${l.number}.` : " No library branch yet."}`;
|
|
4930
|
-
}) : ["- Nothing: every value and component comes from the library."];
|
|
4931
|
-
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");
|
|
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");
|
|
4932
4885
|
}
|
|
4933
4886
|
function withPiecesRecord(body, r) {
|
|
4934
4887
|
const b = (body ?? "").trimEnd();
|
|
@@ -4969,7 +4922,7 @@ function withNeededBy(body, n) {
|
|
|
4969
4922
|
|
|
4970
4923
|
${b}` : ""}`;
|
|
4971
4924
|
}
|
|
4972
|
-
var
|
|
4925
|
+
var cap2 = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
4973
4926
|
var forLine = (b) => `${b.productName} needs it for "${b.title.replace(/"/g, "'")}" (#${b.pr})`;
|
|
4974
4927
|
function libraryBranchPlan(piece, branch, opts = {}) {
|
|
4975
4928
|
const neededBy = {
|
|
@@ -4993,11 +4946,23 @@ function libraryBranchPlan(piece, branch, opts = {}) {
|
|
|
4993
4946
|
path: t.path,
|
|
4994
4947
|
type: t.type,
|
|
4995
4948
|
value: t.value,
|
|
4996
|
-
description: `${
|
|
4949
|
+
description: `${cap2(piece.name)}: ${forLine(branch)}.${files.length > 1 ? " Same value in every theme: review it in each." : ""}`,
|
|
4997
4950
|
...file ? { file } : {}
|
|
4998
4951
|
}));
|
|
4999
4952
|
return { kind: "token-change", title: `Add ${t.path}`, summary: `${forLine(branch)}: ${piece.summary}${near}`, edits, neededBy };
|
|
5000
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
|
+
}
|
|
5001
4966
|
if (piece.kind === "variant" && piece.component) {
|
|
5002
4967
|
const values = piece.values.map((v) => `${v.property ?? v.category} ${v.value}`).join(", ");
|
|
5003
4968
|
const exploration = {
|
|
@@ -5014,7 +4979,7 @@ function libraryBranchPlan(piece, branch, opts = {}) {
|
|
|
5014
4979
|
].filter(Boolean).join(" ");
|
|
5015
4980
|
return { kind: "exploration", title: piece.title, exploration, neededBy, brief };
|
|
5016
4981
|
}
|
|
5017
|
-
const name = piece.component ??
|
|
4982
|
+
const name = piece.component ?? cap2(piece.name);
|
|
5018
4983
|
const request = {
|
|
5019
4984
|
component: name,
|
|
5020
4985
|
kind: "new-component",
|
|
@@ -5026,92 +4991,958 @@ function libraryBranchPlan(piece, branch, opts = {}) {
|
|
|
5026
4991
|
};
|
|
5027
4992
|
return { kind: "request", title: `New component: ${name}`, request, neededBy };
|
|
5028
4993
|
}
|
|
5029
|
-
var STATUS_LABEL = {
|
|
5030
|
-
draft: "Draft",
|
|
5031
|
-
"in-review": "In review",
|
|
5032
|
-
changes: "Changes requested",
|
|
5033
|
-
approved: "Approved",
|
|
5034
|
-
shipped: "Shipped",
|
|
5035
|
-
closed: "Closed",
|
|
5036
|
-
exploring: "Being built",
|
|
5037
|
-
requested: "Requested",
|
|
5038
|
-
"in-progress": "In progress",
|
|
5039
|
-
done: "Done",
|
|
5040
|
-
declined: "Declined"
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
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
|
+
|
|
5615
|
+
// ../core/src/docs-proposal.ts
|
|
5616
|
+
var DOCS_EDITOR_ORIGIN = {
|
|
5617
|
+
where: "in the Bridge docs editor",
|
|
5618
|
+
footer: "Written by a person in the Bridge docs editor. No AI involved in this text.",
|
|
5619
|
+
label: "source: bridge-web"
|
|
5620
|
+
};
|
|
5621
|
+
var DOCS_MCP_ORIGIN = {
|
|
5622
|
+
where: "with Claude through the Bridge MCP server",
|
|
5623
|
+
footer: 'Proposed with the Bridge MCP server on behalf of a person. Docs Claude drafted say "draftedBy": "claude" until a reviewer accepts them.',
|
|
5624
|
+
label: "source: mcp"
|
|
5625
|
+
};
|
|
5626
|
+
var EM_DASH = /\u2014/;
|
|
5627
|
+
function validateDocs(d) {
|
|
5628
|
+
const out = [];
|
|
5629
|
+
if (!d.summary.trim()) out.push({ field: "summary", message: "Add a summary: one or two sentences on what it is for." });
|
|
5630
|
+
const lists = ["whenToUse", "whenNotToUse", "accessibility", "contentGuidelines"];
|
|
5631
|
+
for (const k of lists) (d[k] ?? []).forEach((x, i) => !x.trim() && out.push({ field: `${k}.${i}`, message: "Fill or remove the empty item." }));
|
|
5632
|
+
for (const k of ["dos", "donts"]) d[k].forEach((g, i) => (!g.title.trim() || !g.text.trim()) && out.push({ field: `${k}.${i}`, message: "Give every item a title and a sentence." }));
|
|
5633
|
+
const prose = JSON.stringify([d.summary, d.whenToUse, d.whenNotToUse, d.dos, d.donts, d.accessibility, d.contentGuidelines ?? [], d.anatomy ?? {}]);
|
|
5634
|
+
if (EM_DASH.test(prose)) out.push({ field: "voice", message: "Replace the em dashes with a colon, a comma or two sentences." });
|
|
5635
|
+
return out;
|
|
5636
|
+
}
|
|
5637
|
+
function cleanDocs(d) {
|
|
5638
|
+
const list = (xs) => (xs ?? []).map((x) => x.trim()).filter(Boolean);
|
|
5639
|
+
const guide = (xs) => xs.map((g) => ({ ...g, title: g.title.trim(), text: g.text.trim() })).filter((g) => g.title || g.text);
|
|
5640
|
+
return {
|
|
5641
|
+
...d,
|
|
5642
|
+
summary: d.summary.trim(),
|
|
5643
|
+
whenToUse: list(d.whenToUse),
|
|
5644
|
+
whenNotToUse: list(d.whenNotToUse),
|
|
5645
|
+
dos: guide(d.dos),
|
|
5646
|
+
donts: guide(d.donts),
|
|
5647
|
+
accessibility: list(d.accessibility),
|
|
5648
|
+
contentGuidelines: list(d.contentGuidelines),
|
|
5649
|
+
anatomy: Object.fromEntries(Object.entries(d.anatomy ?? {}).filter(([k, v]) => k.trim() && v.trim()))
|
|
5650
|
+
};
|
|
5651
|
+
}
|
|
5652
|
+
var docsFileText = (d, original) => patchDocsJson(original, cleanDocs(d));
|
|
5653
|
+
var slugDate = (d) => d.toISOString().slice(0, 10).replace(/-/g, "");
|
|
5654
|
+
var docsSlug = (nameOrSlug) => nameOrSlug.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9-]+/g, "-").slice(0, 40);
|
|
5655
|
+
function defaultDocsFooter(edits, origin) {
|
|
5656
|
+
if (origin.label !== DOCS_EDITOR_ORIGIN.label) return origin.footer;
|
|
5657
|
+
const claude = edits.some((e) => {
|
|
5658
|
+
try {
|
|
5659
|
+
return JSON.parse(e.after).draftedBy === "claude";
|
|
5660
|
+
} catch {
|
|
5661
|
+
return false;
|
|
5662
|
+
}
|
|
5663
|
+
});
|
|
5664
|
+
return claude ? `Drafted by Claude AI (the docs say "draftedBy": "claude"), sent by a person ${origin.where}.` : origin.footer;
|
|
5665
|
+
}
|
|
5666
|
+
function withAuthorLine(body, line) {
|
|
5667
|
+
const i = body.lastIndexOf("\n---\n");
|
|
5668
|
+
return i < 0 ? `${body.trimEnd()}
|
|
5669
|
+
|
|
5670
|
+
---
|
|
5671
|
+
${line}` : `${body.slice(0, i + 5)}${line}`;
|
|
5672
|
+
}
|
|
5673
|
+
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5674
|
+
function isDocsProposalBranch(branch, component) {
|
|
5675
|
+
const slugs = /* @__PURE__ */ new Set([docsSlug(component.slug ?? component.name), docsSlug(component.name), docsSlug(component.name.toLowerCase())]);
|
|
5676
|
+
return [...slugs].some((s) => new RegExp(`^docs/(?:[A-Z][A-Z0-9]*-\\d+-)?${escapeRe(s)}-\\d{8}-[a-z0-9]+$`).test(branch));
|
|
5677
|
+
}
|
|
5678
|
+
function findOpenDocsProposal(pulls, component, base) {
|
|
5679
|
+
const hit = pulls.filter((p) => p.state === "open" && isDocsProposalBranch(p.headRef, component) && (!base || !p.baseRef || p.baseRef === base)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
|
|
5680
|
+
return hit ? { number: hit.number, url: hit.url, branch: hit.headRef, author: hit.author, title: hit.title, updatedAt: hit.updatedAt } : null;
|
|
5681
|
+
}
|
|
5682
|
+
async function findOpenDocsProposalOnGitHub(gh, component, base) {
|
|
5683
|
+
return findOpenDocsProposal(await gh.listPullRequests("open"), component, base);
|
|
5684
|
+
}
|
|
5685
|
+
async function continueDocsProposalOnGitHub(gh, open, edit, opts = {}) {
|
|
5686
|
+
const message = opts.message ?? `docs(${docsSlug(edit.component)}): edit usage guidance in the Bridge docs editor`;
|
|
5687
|
+
const cur = await gh.getFile(edit.path, open.branch);
|
|
5688
|
+
if (cur?.text === edit.after) return "";
|
|
5689
|
+
const sha = await gh.putFile({ path: edit.path, content: edit.after, branch: open.branch, message, sha: cur?.sha });
|
|
5690
|
+
if (opts.authorLine) {
|
|
5691
|
+
const line = opts.authorLine;
|
|
5692
|
+
await gh.getPullRequest(open.number).then((pr) => gh.updatePullRequest(open.number, { body: withAuthorLine(pr.body, line) })).catch(() => void 0);
|
|
5693
|
+
}
|
|
5694
|
+
return sha;
|
|
5695
|
+
}
|
|
5696
|
+
function docsProposal(edits, opts) {
|
|
5697
|
+
const origin = opts.origin ?? DOCS_EDITOR_ORIGIN;
|
|
5698
|
+
if (!edits.length) throw new Error("No docs edits in the draft.");
|
|
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`);
|
|
5703
|
+
const branch = `docs/${slug2}-${slugDate(opts.date)}-${opts.date.getTime().toString(36).slice(-4)}`;
|
|
5704
|
+
const csPath = `${opts.changesetDir ?? ".changeset"}/docs-${slug2}-${opts.date.getTime().toString(36)}.md`;
|
|
5705
|
+
const pkg = opts.packageName ?? "@bridge-ds/react";
|
|
5706
|
+
const changeset = `---
|
|
5707
|
+
"${pkg}": patch
|
|
5708
|
+
---
|
|
5709
|
+
|
|
5710
|
+
Usage guidance for ${list}.
|
|
5711
|
+
`;
|
|
5712
|
+
const files = [...edits.map((e) => ({ path: e.path, content: e.after })), { path: csPath, content: changeset }];
|
|
5713
|
+
const risk = classifyRisk({ files: files.map((f) => ({ path: f.path, status: "modified" })) });
|
|
5714
|
+
const rows = edits.map((e) => {
|
|
5715
|
+
let state = "edited";
|
|
5716
|
+
try {
|
|
5717
|
+
const d = JSON.parse(e.after);
|
|
5718
|
+
const gaps = missingProse(d);
|
|
5719
|
+
state = gaps.length ? `still missing: ${gaps.join(", ")}` : "every section filled";
|
|
5720
|
+
} catch {
|
|
5721
|
+
state = "not valid JSON";
|
|
5722
|
+
}
|
|
5723
|
+
return `- \`${e.path}\` (${e.before === null ? "new" : "changed"}, ${state})`;
|
|
5724
|
+
});
|
|
5725
|
+
const body = [
|
|
5726
|
+
`## Docs change`,
|
|
5727
|
+
"",
|
|
5728
|
+
`Usage guidance for ${list}, written ${origin.where}.`,
|
|
5729
|
+
"",
|
|
5730
|
+
...rows,
|
|
5731
|
+
"",
|
|
5732
|
+
`**Risk:** ${risk.label}. Docs only: the published components do not change. Changeset: patch.`,
|
|
5733
|
+
"",
|
|
5734
|
+
opts.notes?.trim() ? `## Notes
|
|
5735
|
+
|
|
5736
|
+
${opts.notes.trim()}
|
|
5737
|
+
` : "",
|
|
5738
|
+
"---",
|
|
5739
|
+
opts.authorLine?.trim() || defaultDocsFooter(edits, origin)
|
|
5740
|
+
].filter((x, i, a) => !(x === "" && a[i - 1] === "")).join("\n");
|
|
5741
|
+
return { branch, title, body, files, risk, bump: "patch", labels: ["docs", origin.label, `tier-${risk.tier}`] };
|
|
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
|
+
}
|
|
5789
|
+
async function proposeDocsOnGitHub(gh, edits, opts) {
|
|
5790
|
+
if (opts.mode === "suggest") return suggestDocsOnGitHub(gh, edits, opts);
|
|
5791
|
+
if (opts.continueOpen !== false && edits.length === 1) {
|
|
5792
|
+
const e = edits[0];
|
|
5793
|
+
const open = await findOpenDocsProposalOnGitHub(gh, { name: e.component, slug: e.slug }, opts.base).catch(() => null);
|
|
5794
|
+
if (open) {
|
|
5795
|
+
const sha = await continueDocsProposalOnGitHub(gh, open, e, { authorLine: opts.authorLine?.trim() || defaultDocsFooter(edits, opts.origin ?? DOCS_EDITOR_ORIGIN) });
|
|
5796
|
+
return { status: "continued", number: open.number, url: open.url, branch: open.branch, title: open.title, continued: true, unchanged: !sha };
|
|
5797
|
+
}
|
|
5798
|
+
}
|
|
5799
|
+
const p = docsProposal(edits, { title: opts.title, notes: opts.notes, date: opts.date ?? /* @__PURE__ */ new Date(), origin: opts.origin, authorLine: opts.authorLine });
|
|
5800
|
+
const prTitle = titleWithJiraKey(p.title, opts.jira ?? null);
|
|
5801
|
+
const branch = branchWithJiraKey(p.branch, opts.jira ?? null);
|
|
5802
|
+
const mainSha = await gh.getBranchSha(opts.base);
|
|
5803
|
+
if (!mainSha) throw new Error(`Branch ${opts.base} not found.`);
|
|
5804
|
+
const tree = await gh.createTree(await gh.getCommitTree(mainSha), p.files.map((f) => ({ path: f.path, content: f.content })));
|
|
5805
|
+
const commit = await gh.createCommit(`${prTitle}
|
|
5806
|
+
|
|
5807
|
+
${opts.commitNote ?? "Proposed with the Bridge docs editor."}`, tree, [mainSha]);
|
|
5808
|
+
await gh.createBranch(branch, commit);
|
|
5809
|
+
const pr = await gh.createPullRequest({ title: prTitle, head: branch, base: opts.base, body: p.body, draft: false });
|
|
5810
|
+
await gh.addLabels(pr.number, [...p.labels, ...opts.extraLabels ?? []]).catch(() => false);
|
|
5811
|
+
return { status: "opened", number: pr.number, url: pr.url, branch, title: prTitle, continued: false, unchanged: false };
|
|
5812
|
+
}
|
|
5813
|
+
async function commitFileOnGitHub(gh, input) {
|
|
5814
|
+
const cur = await gh.getFile(input.path, input.branch);
|
|
5815
|
+
if (!cur) throw new Error(`${input.path} is not on ${input.branch}. Check the component name and the branch.`);
|
|
5816
|
+
const content = input.next(cur.text);
|
|
5817
|
+
if (content === cur.text) return "";
|
|
5818
|
+
return gh.putFile({ path: input.path, content, branch: input.branch, message: input.message, sha: cur.sha });
|
|
5819
|
+
}
|
|
5820
|
+
|
|
5821
|
+
// ../core/src/share-branch.ts
|
|
5822
|
+
var packageDir = (name) => `packages/${name.replace(/^@[^/]+\//, "")}/`;
|
|
5823
|
+
function touchesPackages(files, packages) {
|
|
5824
|
+
const dirs = packages.map(packageDir);
|
|
5825
|
+
return files.some((f) => dirs.some((d) => f.startsWith(d)));
|
|
5826
|
+
}
|
|
5827
|
+
function shareState(x) {
|
|
5828
|
+
if (!touchesPackages(x.files, x.packages)) return { kind: "no-packages" };
|
|
5829
|
+
const c = readPreviewComment(x.comment);
|
|
5830
|
+
if (x.lifecycle === "merged" || x.lifecycle === "closed") {
|
|
5831
|
+
const kind = x.lifecycle === "merged" ? "shipped" : "closed";
|
|
5832
|
+
if (!c || !c.command || c.source === "none") return { kind, stillWorks: null, command: null };
|
|
5833
|
+
return { kind, stillWorks: c.source === "pkg.pr.new" ? true : c.source === "release" ? false : null, command: c.command };
|
|
5834
|
+
}
|
|
5835
|
+
if (!c) return { kind: "building" };
|
|
5836
|
+
if (c.sha7 && !x.headSha.startsWith(c.sha7)) return { kind: "building" };
|
|
5837
|
+
if (!c.command || c.source === "none") return { kind: "failed" };
|
|
5838
|
+
return { kind: "ready", command: c.command, source: c.source };
|
|
5839
|
+
}
|
|
5840
|
+
var stableCommand = (packages) => `npm i ${packages.map((p) => `${p}@latest`).join(" ")}`;
|
|
5841
|
+
function stateSentence(s) {
|
|
5842
|
+
switch (s.kind) {
|
|
5843
|
+
case "ready":
|
|
5844
|
+
return null;
|
|
5845
|
+
case "building":
|
|
5846
|
+
return "The preview version is being prepared. It is usually ready a couple of minutes after each update to the branch.";
|
|
5847
|
+
case "failed":
|
|
5848
|
+
return "The preview version could not be prepared for the latest update. Update the branch to try again, or ask a developer to run it again on GitHub.";
|
|
5849
|
+
case "no-packages":
|
|
5850
|
+
return "This branch does not touch the library tokens or components, so there is no preview version to install.";
|
|
5851
|
+
case "shipped":
|
|
5852
|
+
return s.stillWorks === true ? "This branch was merged. Its preview version still installs, and products get it with the next library release." : s.stillWorks === false ? "This branch was merged, so its preview version was removed. Products get it with the next library release." : "This branch was merged. Products get it with the next library release.";
|
|
5853
|
+
case "closed":
|
|
5854
|
+
return s.stillWorks === true ? "This branch was closed without merging. Its preview version still installs, but it will not reach the library." : s.stillWorks === false ? "This branch was closed without merging, so its preview version was removed." : "This branch was closed without merging.";
|
|
5052
5855
|
}
|
|
5053
|
-
if (x.state === "merged") return make("shipped");
|
|
5054
|
-
if (x.state !== "open") return make("closed");
|
|
5055
|
-
if ((x.labels ?? []).includes("exploration")) return make("exploring");
|
|
5056
|
-
if (x.draft) return make("draft");
|
|
5057
|
-
const latest = /* @__PURE__ */ new Map();
|
|
5058
|
-
for (const r of x.reviews ?? []) if (r.user && r.state !== "COMMENTED") latest.set(r.user, r.state);
|
|
5059
|
-
const states = [...latest.values()];
|
|
5060
|
-
if (states.includes("CHANGES_REQUESTED")) return make("changes", [...latest].filter(([, s]) => s === "CHANGES_REQUESTED").map(([u]) => u), "asks");
|
|
5061
|
-
const waiting = [...x.requestedReviewers ?? []];
|
|
5062
|
-
if (states.includes("APPROVED") && !waiting.length) return make("approved");
|
|
5063
|
-
return make("in-review", waiting);
|
|
5064
5856
|
}
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5857
|
+
var quote = (t) => `"${t.trim().replace(/"/g, "'")}"`;
|
|
5858
|
+
function shareMessage(x) {
|
|
5859
|
+
const back = stableCommand(x.packages);
|
|
5860
|
+
const s = x.state;
|
|
5861
|
+
if (s.kind === "ready") {
|
|
5862
|
+
const lasts = s.source === "pkg.pr.new" ? "This preview stays available. Each update to the change makes a new one: ask me for the latest command." : "This preview is replaced on each update to the change and removed when it ships or closes: ask me for the latest command if the install fails.";
|
|
5863
|
+
return {
|
|
5864
|
+
subject: `Try the design system change ${quote(x.title)} before it ships`,
|
|
5865
|
+
body: [
|
|
5866
|
+
"Hi,",
|
|
5867
|
+
"",
|
|
5868
|
+
`Could you try the design system change ${quote(x.title)} in our product before it ships?`,
|
|
5869
|
+
...x.tryUrl ? ["", `See it in the demo product first: ${x.tryUrl}`] : [],
|
|
5870
|
+
"",
|
|
5871
|
+
"Install the preview version in a staging branch (not production):",
|
|
5872
|
+
s.command,
|
|
5873
|
+
"",
|
|
5874
|
+
lasts,
|
|
5875
|
+
"",
|
|
5876
|
+
"To go back to the stable version:",
|
|
5877
|
+
back,
|
|
5878
|
+
"",
|
|
5879
|
+
`Details for developers: ${x.url}`
|
|
5880
|
+
].join("\n")
|
|
5881
|
+
};
|
|
5070
5882
|
}
|
|
5071
|
-
if (
|
|
5072
|
-
|
|
5073
|
-
|
|
5883
|
+
if ((s.kind === "shipped" || s.kind === "closed") && s.command) {
|
|
5884
|
+
const what = s.kind === "shipped" ? `The design system change ${quote(x.title)} has shipped. You get it with the next library release.` : `The design system change ${quote(x.title)} was closed without shipping.`;
|
|
5885
|
+
const preview = s.stillWorks === false ? "Its preview version was removed, so reinstalling it fails." : s.stillWorks === true ? "Its preview version still installs, but it is not the released version." : "";
|
|
5886
|
+
return {
|
|
5887
|
+
subject: `Design system change ${quote(x.title)} ${s.kind === "shipped" ? "has shipped" : "was closed"}`,
|
|
5888
|
+
body: ["Hi,", "", [what, preview].filter(Boolean).join(" "), "", "If you installed its preview, go back to the stable version:", back, "", `Details for developers: ${x.url}`].join("\n")
|
|
5889
|
+
};
|
|
5074
5890
|
}
|
|
5075
|
-
|
|
5076
|
-
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 };
|
|
5077
|
-
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 };
|
|
5078
|
-
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" } };
|
|
5079
|
-
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" } };
|
|
5891
|
+
return null;
|
|
5080
5892
|
}
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
const
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
const what = n === 1 ? "a library branch for it" : "library branches for them";
|
|
5092
|
-
return { lead: "Built with the library. ", strong: `${nPieces(n)} ${was} missing`, rest: `, and ${by} opened ${what}.` };
|
|
5893
|
+
|
|
5894
|
+
// ../core/src/try-in-product.ts
|
|
5895
|
+
function tryInProductUrl(preview, pr, screen) {
|
|
5896
|
+
const url = new URL(preview);
|
|
5897
|
+
url.pathname = "/";
|
|
5898
|
+
url.search = "";
|
|
5899
|
+
url.searchParams.set("bridge", String(pr));
|
|
5900
|
+
if (screen) {
|
|
5901
|
+
url.searchParams.set("tenant", screen.tenant);
|
|
5902
|
+
url.searchParams.set("theme", screen.theme);
|
|
5093
5903
|
}
|
|
5094
|
-
|
|
5095
|
-
return
|
|
5904
|
+
url.hash = screen ? screen.route : "";
|
|
5905
|
+
return url.href;
|
|
5096
5906
|
}
|
|
5097
|
-
|
|
5098
|
-
|
|
5907
|
+
|
|
5908
|
+
// ../core/src/combine-branches.ts
|
|
5909
|
+
var MIN_TOGETHER = 2;
|
|
5910
|
+
var MAX_TOGETHER = 4;
|
|
5911
|
+
function onlyTokens(files, tokensRoot) {
|
|
5912
|
+
const root = `${tokensRoot.replace(/^\/+|\/+$/g, "")}/`;
|
|
5913
|
+
return files.length > 0 && files.every((f) => f.startsWith(root) && f.endsWith(".tokens.json") || /contrast\.checks\.json$/.test(f) || f.startsWith(".changeset/"));
|
|
5099
5914
|
}
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
const tone = parts.find((p) => TONE_WORDS.includes(p));
|
|
5104
|
-
if (tone) return tone;
|
|
5105
|
-
const meaningful = parts.filter((p) => !/^(color|bg|fg|text|border|default|base|value|\d+)$/.test(p));
|
|
5106
|
-
return meaningful[meaningful.length - 1] ?? parts[parts.length - 1] ?? path;
|
|
5915
|
+
function tokenFilesOf(files, tokensRoot) {
|
|
5916
|
+
const root = `${tokensRoot.replace(/^\/+|\/+$/g, "")}/`;
|
|
5917
|
+
return files.filter((f) => f.startsWith(root) && f.endsWith(".tokens.json")).map((f) => f.slice(root.length));
|
|
5107
5918
|
}
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5919
|
+
var changeList = (numbers) => numbers.join(",");
|
|
5920
|
+
function tryTogetherUrl(admin, numbers, picks) {
|
|
5921
|
+
const url = new URL(admin);
|
|
5922
|
+
url.pathname = "/";
|
|
5923
|
+
url.hash = "";
|
|
5924
|
+
const use = encodePicks(picks);
|
|
5925
|
+
url.search = `?bridge=${changeList(numbers)}${use ? `&bridge-use=${encodeURIComponent(use)}` : ""}`;
|
|
5926
|
+
return url.href;
|
|
5927
|
+
}
|
|
5928
|
+
var COMBINE_COMPARE_LIMIT = 300;
|
|
5929
|
+
async function readCombineBranch(gh, pull, opts) {
|
|
5930
|
+
const compare = await gh.compareDetailed(opts.base, pull.headSha);
|
|
5931
|
+
const files = compare.files;
|
|
5932
|
+
const names3 = files.flatMap((f) => f.previous_filename ? [f.filename, f.previous_filename] : [f.filename]);
|
|
5933
|
+
const start = compare.mergeBase;
|
|
5934
|
+
if (!start || files.length >= COMBINE_COMPARE_LIMIT || !onlyTokens(names3, opts.tokensRoot)) return { number: pull.number, title: pull.title, tokens: false, edits: null };
|
|
5935
|
+
const root = opts.tokensRoot.replace(/^\/+|\/+$/g, "");
|
|
5936
|
+
const tokenFiles = files.filter((f) => tokenFilesOf([f.filename], root).length);
|
|
5937
|
+
const source = { number: pull.number, base: {}, head: {}, files: tokenFiles.map((f) => tokenFilesOf([f.filename], root)[0]) };
|
|
5938
|
+
await Promise.all(
|
|
5939
|
+
tokenFiles.map(async (f) => {
|
|
5940
|
+
const rel = tokenFilesOf([f.filename], root)[0];
|
|
5941
|
+
source.base[rel] = f.status === "added" ? void 0 : await gh.readFile(f.previous_filename ?? f.filename, start);
|
|
5942
|
+
source.head[rel] = f.status === "removed" ? void 0 : await gh.readFile(f.filename, pull.headSha);
|
|
5943
|
+
})
|
|
5944
|
+
);
|
|
5945
|
+
return { number: pull.number, title: pull.title, tokens: true, edits: changeEdits(source) };
|
|
5115
5946
|
}
|
|
5116
5947
|
|
|
5117
5948
|
// ../core/src/prod-branch-github.ts
|
|
@@ -5179,19 +6010,20 @@ async function openLibraryBranchOnGitHub(gh, input) {
|
|
|
5179
6010
|
const piece = record?.pieces.find((p) => p.id === input.piece);
|
|
5180
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\`).`);
|
|
5181
6012
|
const product = productOf(input.products, pull, await gh.listPullRequestFiles(input.pr).then((f) => f.map((x) => x.filename), () => []));
|
|
5182
|
-
const
|
|
6013
|
+
const mode = input.mode ?? "direct";
|
|
6014
|
+
const plan = libraryBranchPlan(piece, productBranchRef(pull, product), { source: input.source, requestedBy: input.openedBy, mode });
|
|
5183
6015
|
const existing = record.links.find((l) => l.piece === piece.id);
|
|
5184
6016
|
if (existing) return { status: "already-linked", plan, link: existing, preview: null, warnings: [] };
|
|
5185
6017
|
if (input.dryRun) return { status: "dry-run", plan, link: null, preview: null, warnings: [] };
|
|
5186
|
-
if (!gh.canWrite) throw new GitHubError(401, "Opening a library branch needs a GitHub token (contents, pull requests and issues: write).");
|
|
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).");
|
|
5187
6019
|
const warnings = [];
|
|
5188
6020
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
5189
6021
|
const who = { ...input.openedBy ? { openedBy: input.openedBy } : {}, ...input.agent ? { agent: input.agent } : {} };
|
|
5190
6022
|
let link;
|
|
5191
6023
|
if (plan.kind === "token-change") {
|
|
5192
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`).");
|
|
5193
|
-
const r = await input.writeTokens(plan);
|
|
5194
|
-
link = { piece: piece.id, kind: "token-change", number: r.number, url: r.url, title: plan.title, branch: r.branch, at, ...who };
|
|
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 };
|
|
5195
6027
|
} else if (plan.kind === "exploration") {
|
|
5196
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] });
|
|
5197
6029
|
link = { piece: piece.id, kind: "exploration", number: r.pr, url: r.url, title: plan.title, branch: r.branch, at, ...who };
|
|
@@ -5201,9 +6033,10 @@ async function openLibraryBranchOnGitHub(gh, input) {
|
|
|
5201
6033
|
link = { piece: piece.id, kind: "request", number: r.number, url: r.url, title: plan.title, at, ...who };
|
|
5202
6034
|
}
|
|
5203
6035
|
try {
|
|
5204
|
-
if (link.kind === "request") {
|
|
6036
|
+
if (link.kind === "request" || link.kind === "suggestion") {
|
|
5205
6037
|
const issue = await gh.getIssue(link.number);
|
|
5206
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]);
|
|
5207
6040
|
} else {
|
|
5208
6041
|
const lib = await gh.getPullRequest(link.number);
|
|
5209
6042
|
await gh.updatePullRequest(link.number, { body: withNeededBy(lib.body, plan.neededBy) });
|
|
@@ -5213,9 +6046,14 @@ async function openLibraryBranchOnGitHub(gh, input) {
|
|
|
5213
6046
|
if (err instanceof GitHubError && err.auth === "rejected") throw err;
|
|
5214
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.`);
|
|
5215
6048
|
}
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
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
|
+
}
|
|
5219
6057
|
let preview = null;
|
|
5220
6058
|
if ((input.usePreview ?? true) && link.kind === "token-change" && link.branch) {
|
|
5221
6059
|
preview = await useLibraryPreviewOnGitHub(gh, { productBranch: pull.headRef, libraryBranch: link.branch, libraryNumber: link.number }).catch((err) => {
|
|
@@ -5223,11 +6061,21 @@ async function openLibraryBranchOnGitHub(gh, input) {
|
|
|
5223
6061
|
return null;
|
|
5224
6062
|
});
|
|
5225
6063
|
}
|
|
5226
|
-
return { status: "opened", plan, link, preview, warnings };
|
|
6064
|
+
return { status: link.kind === "suggestion" ? "suggested" : "opened", plan, link, preview, warnings };
|
|
5227
6065
|
}
|
|
5228
6066
|
var isNote = (path) => path.startsWith(`${EXPLORATION_NOTES_DIR}/`);
|
|
5229
6067
|
async function linkedLibraryBranchOnGitHub(gh, link, product, piece) {
|
|
5230
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
|
+
}
|
|
5231
6079
|
if (link.kind === "request") {
|
|
5232
6080
|
const issue = await gh.getIssue(link.number);
|
|
5233
6081
|
const status2 = linkStatus({ kind: "request", state: issue.state, stateReason: issue.stateReason });
|
|
@@ -5310,7 +6158,7 @@ function planTokenProposal(input) {
|
|
|
5310
6158
|
...config.changesets ? [{ path: changesetPath, content: formatChangeset(config.changesets.package, tier.bump, summary2) }] : []
|
|
5311
6159
|
];
|
|
5312
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;
|
|
5313
|
-
return { title: input.title, summary: summary2, before, after, changes, tier, contrast, newFailures, newErrors, impact, notes: r.notes, touched, files, body, slug: slug2, blocked };
|
|
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 };
|
|
5314
6162
|
}
|
|
5315
6163
|
async function writeTokenProposalOnGitHub(gh, plan, opts) {
|
|
5316
6164
|
if (plan.blocked) throw new Error(`Not opened: ${plan.blocked}. Bridge never opens a branch that breaks the gate.`);
|
|
@@ -5320,12 +6168,126 @@ async function writeTokenProposalOnGitHub(gh, plan, opts) {
|
|
|
5320
6168
|
const commit = await gh.commitToNewBranch({ base: opts.base, branch, message: `feat(tokens): ${plan.title}
|
|
5321
6169
|
|
|
5322
6170
|
Proposed through ${opts.tool ?? "Bridge MCP"}. Tier ${plan.tier.tier}.`, files: plan.files });
|
|
5323
|
-
const labels =
|
|
6171
|
+
const labels = tokenLabels(plan, opts.labels, opts.labelsAfter);
|
|
5324
6172
|
const draft = plan.tier.tier >= 2;
|
|
5325
6173
|
const pr = await gh.createPullRequest({ base: opts.base, head: branch, title: titleWithJiraKey(`feat(tokens): ${plan.title}`, jira), body: plan.body, draft });
|
|
5326
6174
|
await gh.addLabels(pr.number, labels);
|
|
5327
6175
|
return { branch, commit: commit.sha, pr: pr.number, url: pr.url, labels, draft };
|
|
5328
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
|
+
}
|
|
5329
6291
|
|
|
5330
6292
|
// ../core/src/node/git-history.ts
|
|
5331
6293
|
var US = "";
|
|
@@ -6182,8 +7144,8 @@ function tailwindFindings(hits, offset, pos, ds) {
|
|
|
6182
7144
|
else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
|
|
6183
7145
|
if (cat === "color" && !isColor) continue;
|
|
6184
7146
|
const suggestion = ds.suggest(cat, v);
|
|
6185
|
-
const
|
|
6186
|
-
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 } : {} });
|
|
6187
7149
|
}
|
|
6188
7150
|
return { findings, themeRefs };
|
|
6189
7151
|
}
|
|
@@ -8117,7 +9079,7 @@ function missingPieces(input) {
|
|
|
8117
9079
|
foreign.set(c.name, e);
|
|
8118
9080
|
}
|
|
8119
9081
|
}
|
|
8120
|
-
const
|
|
9082
|
+
const where2 = (hits) => hits.slice(0, 5).map((h) => ({ file: h.file, line: h.line }));
|
|
8121
9083
|
const pieces = [];
|
|
8122
9084
|
for (const v of variants.values()) {
|
|
8123
9085
|
const values = [];
|
|
@@ -8139,7 +9101,7 @@ function missingPieces(input) {
|
|
|
8139
9101
|
aspect,
|
|
8140
9102
|
values,
|
|
8141
9103
|
...nearest ? { nearest } : {},
|
|
8142
|
-
where:
|
|
9104
|
+
where: where2(v.hits)
|
|
8143
9105
|
});
|
|
8144
9106
|
}
|
|
8145
9107
|
for (const [name, e] of foreign) {
|
|
@@ -8151,7 +9113,7 @@ function missingPieces(input) {
|
|
|
8151
9113
|
name,
|
|
8152
9114
|
component: name.split(".").pop(),
|
|
8153
9115
|
values: [],
|
|
8154
|
-
where:
|
|
9116
|
+
where: where2(e.hits)
|
|
8155
9117
|
});
|
|
8156
9118
|
}
|
|
8157
9119
|
for (const t of tokens.values()) {
|
|
@@ -8168,7 +9130,7 @@ function missingPieces(input) {
|
|
|
8168
9130
|
name: pname,
|
|
8169
9131
|
values: [{ category: t.category, ...t.hits[0].finding.property ? { property: t.hits[0].finding.property } : {}, value: t.value }],
|
|
8170
9132
|
...nearest ? { nearest } : {},
|
|
8171
|
-
where:
|
|
9133
|
+
where: where2(t.hits),
|
|
8172
9134
|
token: { path, type: TOKEN_TYPE[t.category] ?? "dimension", value: t.value, files: tokenFiles }
|
|
8173
9135
|
});
|
|
8174
9136
|
}
|
|
@@ -9533,7 +10495,7 @@ async function visualImpact(ctx, a, run2) {
|
|
|
9533
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.");
|
|
9534
10496
|
if (!ctx.isGit) throw new Error(`${ctx.root} is not a git repository.`);
|
|
9535
10497
|
const out = a.out ? resolve3(ctx.root, a.out) : join4(ctx.root, "visual-impact");
|
|
9536
|
-
const runImpact = run2 ?? (await import("./src-
|
|
10498
|
+
const runImpact = run2 ?? (await import("./src-F2PTXW5B.js")).runImpact;
|
|
9537
10499
|
const r = await runImpact({
|
|
9538
10500
|
root: ctx.root,
|
|
9539
10501
|
base: a.base ?? "main",
|
|
@@ -9996,15 +10958,15 @@ function requestFromIssue(issue) {
|
|
|
9996
10958
|
const parsed = parseRequestIssue(issue.body);
|
|
9997
10959
|
if (parsed) return parsed;
|
|
9998
10960
|
const m = /^\s*([^:]{1,60}):\s*(.+)$/.exec(issue.title);
|
|
9999
|
-
const
|
|
10000
|
-
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");
|
|
10001
10963
|
return {
|
|
10002
10964
|
component: m ? m[1].trim() : "Unknown",
|
|
10003
|
-
kind:
|
|
10965
|
+
kind: kindLabel3 ?? "other",
|
|
10004
10966
|
title: m ? m[2].trim() : issue.title,
|
|
10005
10967
|
summary: issue.body.trim() || issue.title,
|
|
10006
10968
|
changes: [],
|
|
10007
|
-
source:
|
|
10969
|
+
source: sourceLabel3 ?? "studio"
|
|
10008
10970
|
};
|
|
10009
10971
|
}
|
|
10010
10972
|
function statusOf(issue, prs) {
|
|
@@ -10435,8 +11397,98 @@ async function switchToPublishedOp(ctx, a) {
|
|
|
10435
11397
|
return { pr: n, ...await switchToPublishedOnGitHub(gh, n) };
|
|
10436
11398
|
}
|
|
10437
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
|
+
|
|
10438
11490
|
// src/server.ts
|
|
10439
|
-
var VERSION = true ? "0.
|
|
11491
|
+
var VERSION = true ? "0.6.0" : "dev";
|
|
10440
11492
|
var ok = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
|
|
10441
11493
|
var fail = (e) => ({ content: [{ type: "text", text: `Error: ${e.message ?? String(e)}` }], isError: true });
|
|
10442
11494
|
var run = (fn) => async (a) => {
|
|
@@ -10477,6 +11529,7 @@ function createServer(ctx) {
|
|
|
10477
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.",
|
|
10478
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.',
|
|
10479
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.",
|
|
10480
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.'
|
|
10481
11534
|
].join(" ")
|
|
10482
11535
|
}
|
|
@@ -11025,6 +12078,46 @@ function createServer(ctx) {
|
|
|
11025
12078
|
},
|
|
11026
12079
|
run((a) => switchToPublishedOp(ctx, a))
|
|
11027
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
|
+
);
|
|
11028
12121
|
server.registerTool(
|
|
11029
12122
|
"bridge_list_products",
|
|
11030
12123
|
{
|
|
@@ -11451,6 +12544,7 @@ export {
|
|
|
11451
12544
|
drafts_exports,
|
|
11452
12545
|
studio_exports,
|
|
11453
12546
|
pieces_exports,
|
|
12547
|
+
figma_exports,
|
|
11454
12548
|
VERSION,
|
|
11455
12549
|
createServer,
|
|
11456
12550
|
serveStdio,
|