@bridge-ds/mcp 0.4.1 → 0.5.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.
@@ -1921,6 +1921,17 @@ var GitHubClient = class _GitHubClient {
1921
1921
  this.assertToken("Updating a branch from main");
1922
1922
  await this.request("PUT", `${this.repoPath}/pulls/${n}/update-branch`, { expected_head_sha: expectedHeadSha });
1923
1923
  }
1924
+ /**
1925
+ * Merges the branch `head` into the branch `base` (GitHub's merges API): a merge commit on `base`, or nothing when
1926
+ * `base` already has every commit of `head`. A conflict throws a GitHubError with status 409. Refuses protected
1927
+ * branches and, with a branch prefix, a `base` without it.
1928
+ */
1929
+ async mergeIntoBranch(base, head, message) {
1930
+ this.assertToken("Merging a branch into another");
1931
+ this.assertWritableBranch(base);
1932
+ const r = await this.request("POST", `${this.repoPath}/merges`, { base, head, commit_message: message });
1933
+ return r?.sha ? { status: "merged", sha: r.sha } : { status: "up-to-date", sha: null };
1934
+ }
1924
1935
  /** Posts a comment on an issue or pull request and returns its id and link. */
1925
1936
  async createIssueComment(n, body) {
1926
1937
  this.assertToken("Commenting");
@@ -2084,8 +2095,8 @@ function firstParagraph(text) {
2084
2095
  if (!text) return "";
2085
2096
  const cleaned = text.replace(/<!--[\s\S]*?-->/g, "");
2086
2097
  for (const para of cleaned.split(/\r?\n\s*\r?\n/)) {
2087
- const lines = para.split(/\r?\n/).filter((l) => l.trim() && !/^\s*#{1,6}\s/.test(l) && !/^\s*[-*]\s*\[[ xX]\]/.test(l) && !/^\s*(Co-Authored-By|Claude-Session|Signed-off-by):/i.test(l));
2088
- const s = lines.join(" ").replace(/\s+/g, " ").trim();
2098
+ const lines2 = para.split(/\r?\n/).filter((l) => l.trim() && !/^\s*#{1,6}\s/.test(l) && !/^\s*[-*]\s*\[[ xX]\]/.test(l) && !/^\s*(Co-Authored-By|Claude-Session|Signed-off-by):/i.test(l));
2099
+ const s = lines2.join(" ").replace(/\s+/g, " ").trim();
2089
2100
  if (s && !/^\|/.test(s)) return s.length > 400 ? `${s.slice(0, 397)}...` : s;
2090
2101
  }
2091
2102
  return "";
@@ -2251,11 +2262,11 @@ function modeLabel(modes) {
2251
2262
  return s.charAt(0).toUpperCase() + s.slice(1);
2252
2263
  }
2253
2264
  function tokenHistory(entries, path, config) {
2254
- const names2 = /* @__PURE__ */ new Set([path]);
2265
+ const names3 = /* @__PURE__ */ new Set([path]);
2255
2266
  const out = [];
2256
2267
  const sorted = [...entries].sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
2257
2268
  for (const e of sorted) {
2258
- const hits = e.changes.filter((c) => names2.has(c.path));
2269
+ const hits = e.changes.filter((c) => names3.has(c.path));
2259
2270
  for (const c of hits) {
2260
2271
  const modes = config ? fileModes(config, c.file) : {};
2261
2272
  out.push({
@@ -2283,7 +2294,7 @@ function tokenHistory(entries, path, config) {
2283
2294
  ...c.descriptionAfter !== void 0 ? { descriptionAfter: c.descriptionAfter } : {}
2284
2295
  });
2285
2296
  }
2286
- for (const c of hits) if (c.kind === "renamed" && c.from) names2.add(c.from);
2297
+ for (const c of hits) if (c.kind === "renamed" && c.from) names3.add(c.from);
2287
2298
  }
2288
2299
  return out;
2289
2300
  }
@@ -2413,7 +2424,7 @@ function planRevert(input) {
2413
2424
  }
2414
2425
  const prLink = entry.pr ? entry.pr.url ? `[#${entry.pr.number}](${entry.pr.url})` : `#${entry.pr.number}` : `commit ${entry.sha.slice(0, 7)}`;
2415
2426
  const title = `Revert: ${entry.title}`;
2416
- const lines = [
2427
+ const lines2 = [
2417
2428
  `Reverts ${prLink} ("${entry.title}"), merged ${entry.date.slice(0, 10)} by ${entry.author}.`,
2418
2429
  "",
2419
2430
  `Original reason: ${entry.why}`,
@@ -2421,11 +2432,11 @@ function planRevert(input) {
2421
2432
  "Bridge computed the inverse token edits against the current base branch, not a blind git revert: tokens that changed again since are listed as conflicts and kept unless a reviewer chose to restore them."
2422
2433
  ];
2423
2434
  if (conflicts.length) {
2424
- lines.push("", "### Conflicts", "", "| Token | File | Why | Decision |", "| --- | --- | --- | --- |");
2425
- for (const k of conflicts) lines.push(`| \`${k.path}\` | ${k.file} | ${k.message.replace(/\|/g, "\\|")}${k.later.length ? ` Later: ${k.later.map((l) => l.pr ? `#${l.pr}` : l.id).join(", ")}.` : ""} | ${k.resolution === "use-before" ? "restore the old value" : "keep current"} |`);
2435
+ lines2.push("", "### Conflicts", "", "| Token | File | Why | Decision |", "| --- | --- | --- | --- |");
2436
+ for (const k of conflicts) lines2.push(`| \`${k.path}\` | ${k.file} | ${k.message.replace(/\|/g, "\\|")}${k.later.length ? ` Later: ${k.later.map((l) => l.pr ? `#${l.pr}` : l.id).join(", ")}.` : ""} | ${k.resolution === "use-before" ? "restore the old value" : "keep current"} |`);
2426
2437
  }
2427
- if (skipped.length) lines.push("", `${skipped.length} change(s) were already undone on the base branch and are left alone.`);
2428
- return finishPlan({ ...input, kind: "revert", title, prTitle: title, summary: lines.join("\n"), planned, conflicts, skipped, source: entry });
2438
+ if (skipped.length) lines2.push("", `${skipped.length} change(s) were already undone on the base branch and are left alone.`);
2439
+ return finishPlan({ ...input, kind: "revert", title, prTitle: title, summary: lines2.join("\n"), planned, conflicts, skipped, source: entry });
2429
2440
  }
2430
2441
  function planRestore(input) {
2431
2442
  const { current, path } = input;
@@ -3598,7 +3609,7 @@ function explorationPrBody(e, ctx) {
3598
3609
  const branch = `${EXPLORATION_PREFIX}${slug2}`;
3599
3610
  const uc = useCaseLabel(e.useCase);
3600
3611
  const experimental = (e.experimental ?? []).map((x) => x.trim()).filter(Boolean);
3601
- const lines = [
3612
+ const lines2 = [
3602
3613
  `## Exploration: ${e.name.trim()}`,
3603
3614
  "",
3604
3615
  `> Not mergeable as is. This draft pull request is a sandbox on \`${branch}\`: promote it to a normal review when the idea is validated, or close it.`,
@@ -3614,14 +3625,14 @@ function explorationPrBody(e, ctx) {
3614
3625
  "### Try it in a product staging branch",
3615
3626
  ""
3616
3627
  ];
3617
- lines.push(
3628
+ lines2.push(
3618
3629
  'In the product repo, on a branch that deploys to staging (production keeps the released version), install the snapshot with the command in the "Try this exploration in your product" comment below. It appears a few minutes after each push and always matches the latest commit.'
3619
3630
  );
3620
- lines.push("", "### Previews", "");
3631
+ lines2.push("", "### Previews", "");
3621
3632
  const links = previewLinks(ctx.apps, { branch, ...ctx.pr ? { pr: ctx.pr } : {} });
3622
- if (links.length) lines.push("| App | Preview |", "| --- | --- |", ...links.map((l) => `| ${l.label} | ${l.url} |`));
3623
- else lines.push("Preview deploys are not set up: add `previews.apps` to `bridge.config.json` (docs/PREVIEWS.md).");
3624
- lines.push(
3633
+ if (links.length) lines2.push("| App | Preview |", "| --- | --- |", ...links.map((l) => `| ${l.label} | ${l.url} |`));
3634
+ else lines2.push("Preview deploys are not set up: add `previews.apps` to `bridge.config.json` (docs/PREVIEWS.md).");
3635
+ lines2.push(
3625
3636
  "",
3626
3637
  "### When it is done",
3627
3638
  "",
@@ -3630,7 +3641,7 @@ function explorationPrBody(e, ctx) {
3630
3641
  "",
3631
3642
  `<!-- ${MARK} ${JSON.stringify({ name: e.name.trim(), purpose: e.purpose.trim(), slug: slug2, ...e.useCase ? { useCase: e.useCase } : {}, ...experimental.length ? { experimental } : {}, ...e.author ? { author: e.author } : {}, ...e.startedAt ? { startedAt: e.startedAt } : {}, ...e.source ? { source: e.source } : {}, ...e.agent ? { agent: e.agent } : {} }).replace(/-->/g, "--\\u003e")} -->`
3632
3643
  );
3633
- return lines.join("\n");
3644
+ return lines2.join("\n");
3634
3645
  }
3635
3646
  function parseExplorationBody(body) {
3636
3647
  if (!body) return void 0;
@@ -3955,9 +3966,9 @@ function mergeDraftResults(p, results, at = /* @__PURE__ */ new Date()) {
3955
3966
  continue;
3956
3967
  }
3957
3968
  const clean = sanitizeDraftText(r);
3958
- const lines = sanitizeResultLines(item2, r.lines);
3959
- if (!clean.title && !clean.summary && !lines.length) throw new Error(`The result for "${r.id}" has no title and no summary, and no line.`);
3960
- next.set(r.id, { id: r.id, title: clean.title, summary: clean.summary, ...lines.length ? { lines } : {}, by: "claude", at: at.toISOString() });
3969
+ const lines2 = sanitizeResultLines(item2, r.lines);
3970
+ if (!clean.title && !clean.summary && !lines2.length) throw new Error(`The result for "${r.id}" has no title and no summary, and no line.`);
3971
+ next.set(r.id, { id: r.id, title: clean.title, summary: clean.summary, ...lines2.length ? { lines: lines2 } : {}, by: "claude", at: at.toISOString() });
3961
3972
  }
3962
3973
  const merged = p.items.map((i) => next.get(i.id)).filter((r) => !!r);
3963
3974
  return { ...p, results: merged, status: merged.length === p.items.length ? "drafted" : "pending" };
@@ -3975,24 +3986,24 @@ function draftItemBrief(i) {
3975
3986
  }
3976
3987
  const kind = i.kind ? REQUEST_KINDS.find((k) => k.value === i.kind)?.label : void 0;
3977
3988
  const cell3 = (s) => (s ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
3978
- const lines = [`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"}.`];
3979
- if (i.component) lines.push(`Component: ${i.component}`);
3980
- if (kind) lines.push(`Kind: ${kind}`);
3981
- if (i.components?.length) lines.push(`Components with a change request: ${i.components.join(", ")}`);
3982
- if (i.changes.length) lines.push("", "| What | Now | In Figma |", "| --- | --- | --- |", ...i.changes.map((c) => `| ${cell3(c.field)} | ${cell3(c.before) || "none"} | ${cell3(c.after) || "removed"} |`));
3983
- if (i.variables?.length) lines.push("", "| Token | Now | In Figma |", "| --- | --- | --- |", ...i.variables.map((v) => `| ${cell3(v.path)} | ${cell3(v.before) || "none"} | ${cell3(v.after) || "removed"} |`));
3984
- if (!i.changes.length && !i.variables?.length && !i.lines?.length) lines.push("", "No change detected: rely on the notes and the images.");
3985
- if (i.draft.title || i.draft.summary) lines.push("", "Starting draft, written from the changes only:", `Title: ${i.draft.title}`, `Summary: ${i.draft.summary}`);
3986
- if (i.notes?.trim()) lines.push("", "The designer's notes (keep their intent):", i.notes.trim());
3989
+ 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
+ if (i.component) lines2.push(`Component: ${i.component}`);
3991
+ if (kind) lines2.push(`Kind: ${kind}`);
3992
+ 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) => `| ${cell3(c.field)} | ${cell3(c.before) || "none"} | ${cell3(c.after) || "removed"} |`));
3994
+ if (i.variables?.length) lines2.push("", "| Token | Now | In Figma |", "| --- | --- | --- |", ...i.variables.map((v) => `| ${cell3(v.path)} | ${cell3(v.before) || "none"} | ${cell3(v.after) || "removed"} |`));
3995
+ if (!i.changes.length && !i.variables?.length && !i.lines?.length) lines2.push("", "No change detected: rely on the notes and the images.");
3996
+ 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
+ if (i.notes?.trim()) lines2.push("", "The designer's notes (keep their intent):", i.notes.trim());
3987
3998
  if (i.lines?.length) {
3988
- lines.push("", `Change lines to write (one sentence each, at most ${DRAFT_LINE_MAX} characters, from the before and after images), each with the title of its request (at most ${DRAFT_TITLE_MAX} characters):`);
3999
+ lines2.push("", `Change lines to write (one sentence each, at most ${DRAFT_LINE_MAX} characters, from the before and after images), each with the title of its request (at most ${DRAFT_TITLE_MAX} characters):`);
3989
4000
  for (const l of i.lines) {
3990
- lines.push(`- Line ${l.id}${l.component ? `, ${l.component}` : ""}${l.kind ? ` (${REQUEST_KINDS.find((k) => k.value === l.kind)?.label ?? l.kind})` : ""}. Starting line: ${l.line || "(none)"}${l.title ? `. Starting title: ${l.title}` : ""}`);
3991
- for (const c of l.changes) lines.push(` - ${c.field}: ${c.before || "none"} -> ${c.after || "removed"}`);
4001
+ lines2.push(`- Line ${l.id}${l.component ? `, ${l.component}` : ""}${l.kind ? ` (${REQUEST_KINDS.find((k) => k.value === l.kind)?.label ?? l.kind})` : ""}. Starting line: ${l.line || "(none)"}${l.title ? `. Starting title: ${l.title}` : ""}`);
4002
+ for (const c of l.changes) lines2.push(` - ${c.field}: ${c.before || "none"} -> ${c.after || "removed"}`);
3992
4003
  }
3993
4004
  }
3994
- if (i.images?.length) lines.push("", `Images: ${i.images.map((m) => `${m.label}${m.line ? ` (line ${m.line})` : ""} ${m.url}`).join(", ")}`);
3995
- return lines.join("\n");
4005
+ if (i.images?.length) lines2.push("", `Images: ${i.images.map((m) => `${m.label}${m.line ? ` (line ${m.line})` : ""} ${m.url}`).join(", ")}`);
4006
+ return lines2.join("\n");
3996
4007
  }
3997
4008
  var isConflict = (err) => err instanceof GitHubError ? err.status === 409 || err.status === 422 && /sha/i.test(`${err.detail ?? ""} ${err.message}`) : err?.status === 409;
3998
4009
  async function readPendingDrafts(client2, sessionId, branch = REQUESTS_BRANCH) {
@@ -4380,14 +4391,14 @@ function conflictRoute(o, opts = {}) {
4380
4391
  return { kind: "tokens", files: o.tokenFiles };
4381
4392
  }
4382
4393
  function resolutionMessage(input) {
4383
- const lines = input.conflicts.map((c) => `- ${c.path} (${c.file}): ${choiceFor(c, input.choices) === "theirs" ? `used the ${input.library} value` : "kept this change"}`);
4394
+ const lines2 = input.conflicts.map((c) => `- ${c.path} (${c.file}): ${choiceFor(c, input.choices) === "theirs" ? `used the ${input.library} value` : "kept this change"}`);
4384
4395
  const title = input.conflicts.length ? `Bring in the latest ${input.library} and resolve ${plural(input.conflicts.length, "token conflict")}` : `Bring in the latest ${input.library}`;
4385
4396
  const by = input.who ? `
4386
4397
 
4387
4398
  Resolved in Bridge by @${input.who.replace(/^@/, "")}.` : "\n\nResolved in Bridge.";
4388
- return `${title}${lines.length ? `
4399
+ return `${title}${lines2.length ? `
4389
4400
 
4390
- ${lines.join("\n")}` : ""}${by}`;
4401
+ ${lines2.join("\n")}` : ""}${by}`;
4391
4402
  }
4392
4403
  function freshness(input) {
4393
4404
  const behindBy = typeof input.compare?.behind_by === "number" ? input.compare.behind_by : null;
@@ -4576,11 +4587,11 @@ function canAddTo(p, viewer, write) {
4576
4587
  return write || sameLogin(p.user?.login, viewer);
4577
4588
  }
4578
4589
  function addedComment(paths, viewer, from = "the Bridge token editor") {
4579
- const lines = paths.slice(0, 20).map((p) => `- \`${p}\``);
4580
- if (paths.length > 20) lines.push(`- and ${paths.length - 20} more`);
4590
+ const lines2 = paths.slice(0, 20).map((p) => `- \`${p}\``);
4591
+ if (paths.length > 20) lines2.push(`- and ${paths.length - 20} more`);
4581
4592
  return `${viewer ? `@${viewer} added` : "Added"} token edits from ${from}:
4582
4593
 
4583
- ${lines.join("\n")}`;
4594
+ ${lines2.join("\n")}`;
4584
4595
  }
4585
4596
 
4586
4597
  // ../core/src/jira.ts
@@ -4687,10 +4698,10 @@ async function continueDocsProposalOnGitHub(gh, open, edit, opts = {}) {
4687
4698
  function docsProposal(edits, opts) {
4688
4699
  const origin = opts.origin ?? DOCS_EDITOR_ORIGIN;
4689
4700
  if (!edits.length) throw new Error("No docs edits in the draft.");
4690
- const names2 = edits.map((e) => e.component);
4691
- const list = names2.length === 1 ? names2[0] : `${names2.slice(0, -1).join(", ")} and ${names2[names2.length - 1]}`;
4692
- const title = opts.title?.trim() || `docs(${names2.length === 1 ? edits[0].slug : "components"}): usage guidance for ${list}`;
4693
- const slug2 = docsSlug(names2.length === 1 ? edits[0].slug : `${edits[0].slug}-and-${names2.length - 1}-more`);
4701
+ const names3 = edits.map((e) => e.component);
4702
+ const list = names3.length === 1 ? names3[0] : `${names3.slice(0, -1).join(", ")} and ${names3[names3.length - 1]}`;
4703
+ const title = opts.title?.trim() || `docs(${names3.length === 1 ? edits[0].slug : "components"}): usage guidance for ${list}`;
4704
+ const slug2 = docsSlug(names3.length === 1 ? edits[0].slug : `${edits[0].slug}-and-${names3.length - 1}-more`);
4694
4705
  const branch = `docs/${slug2}-${slugDate(opts.date)}-${opts.date.getTime().toString(36).slice(-4)}`;
4695
4706
  const csPath = `${opts.changesetDir ?? ".changeset"}/docs-${slug2}-${opts.date.getTime().toString(36)}.md`;
4696
4707
  const pkg = opts.packageName ?? "@bridge-ds/react";
@@ -4873,9 +4884,9 @@ var COMBINE_COMPARE_LIMIT = 300;
4873
4884
  async function readCombineBranch(gh, pull, opts) {
4874
4885
  const compare = await gh.compareDetailed(opts.base, pull.headSha);
4875
4886
  const files = compare.files;
4876
- const names2 = files.flatMap((f) => f.previous_filename ? [f.filename, f.previous_filename] : [f.filename]);
4887
+ const names3 = files.flatMap((f) => f.previous_filename ? [f.filename, f.previous_filename] : [f.filename]);
4877
4888
  const start = compare.mergeBase;
4878
- if (!start || files.length >= COMBINE_COMPARE_LIMIT || !onlyTokens(names2, opts.tokensRoot)) return { number: pull.number, title: pull.title, tokens: false, edits: null };
4889
+ if (!start || files.length >= COMBINE_COMPARE_LIMIT || !onlyTokens(names3, opts.tokensRoot)) return { number: pull.number, title: pull.title, tokens: false, edits: null };
4879
4890
  const root = opts.tokensRoot.replace(/^\/+|\/+$/g, "");
4880
4891
  const tokenFiles = files.filter((f) => tokenFilesOf([f.filename], root).length);
4881
4892
  const source = { number: pull.number, base: {}, head: {}, files: tokenFiles.map((f) => tokenFilesOf([f.filename], root)[0]) };
@@ -4889,6 +4900,433 @@ async function readCombineBranch(gh, pull, opts) {
4889
4900
  return { number: pull.number, title: pull.title, tokens: true, edits: changeEdits(source) };
4890
4901
  }
4891
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");
4932
+ }
4933
+ function withPiecesRecord(body, r) {
4934
+ const b = (body ?? "").trimEnd();
4935
+ const start = b.indexOf(SECTION);
4936
+ const markAt = b.indexOf(`<!-- ${PIECES_MARK} `);
4937
+ if (start >= 0 && markAt > start) {
4938
+ const end = b.indexOf("-->", markAt);
4939
+ return `${b.slice(0, start)}${piecesSection(r)}${end >= 0 ? b.slice(end + 3) : ""}`.trimEnd();
4940
+ }
4941
+ return `${b}${b ? "\n\n" : ""}${piecesSection(r)}`;
4942
+ }
4943
+ function mergePiecesRecord(prev, next) {
4944
+ const links = [...prev?.links ?? []];
4945
+ for (const l of next.links ?? []) {
4946
+ const at = links.findIndex((x) => x.piece === l.piece);
4947
+ if (at >= 0) links[at] = l;
4948
+ else links.push(l);
4949
+ }
4950
+ return { ...next, links };
4951
+ }
4952
+ function withLink(r, l) {
4953
+ return { ...r, links: [...r.links.filter((x) => x.piece !== l.piece), l] };
4954
+ }
4955
+ function neededBySection(n) {
4956
+ const who = n.author ? ` for @${n.author}` : "";
4957
+ const near = n.nearest ? ` The closest the library has today is ${n.nearest}${n.nearestToken ? ` (\`${n.nearestToken}\`)` : ""}.` : "";
4958
+ return [`**Needed by:** [${n.title}](${n.url}) (#${n.pr}) in ${n.productName}${who}.${near}`, "", hiddenJson(NEEDED_MARK, n)].join("\n");
4959
+ }
4960
+ function withNeededBy(body, n) {
4961
+ let b = (body ?? "").trim();
4962
+ const at = b.indexOf("**Needed by:**");
4963
+ const mark = b.indexOf(`<!-- ${NEEDED_MARK} `);
4964
+ if (at >= 0 && mark > at) {
4965
+ const end = b.indexOf("-->", mark);
4966
+ b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
4967
+ }
4968
+ return `${neededBySection(n)}${b ? `
4969
+
4970
+ ${b}` : ""}`;
4971
+ }
4972
+ var cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
4973
+ var forLine = (b) => `${b.productName} needs it for "${b.title.replace(/"/g, "'")}" (#${b.pr})`;
4974
+ function libraryBranchPlan(piece, branch, opts = {}) {
4975
+ const neededBy = {
4976
+ pr: branch.pr,
4977
+ title: branch.title,
4978
+ branch: branch.branch,
4979
+ url: branch.url,
4980
+ product: branch.productKey,
4981
+ productName: branch.productName,
4982
+ ...branch.author ? { author: branch.author } : {},
4983
+ piece: piece.id,
4984
+ pieceTitle: piece.title,
4985
+ ...piece.nearest ? { nearest: piece.nearest.label, nearestToken: piece.nearest.token } : {}
4986
+ };
4987
+ const near = piece.nearest ? ` The closest the library has today is ${piece.nearest.label} (${piece.nearest.token}, ${piece.nearest.value}).` : "";
4988
+ if (piece.kind === "token" && piece.token) {
4989
+ const t = piece.token;
4990
+ const files = t.files.length ? t.files : [void 0];
4991
+ const edits = files.map((file) => ({
4992
+ op: "add",
4993
+ path: t.path,
4994
+ type: t.type,
4995
+ value: t.value,
4996
+ description: `${cap(piece.name)}: ${forLine(branch)}.${files.length > 1 ? " Same value in every theme: review it in each." : ""}`,
4997
+ ...file ? { file } : {}
4998
+ }));
4999
+ return { kind: "token-change", title: `Add ${t.path}`, summary: `${forLine(branch)}: ${piece.summary}${near}`, edits, neededBy };
5000
+ }
5001
+ if (piece.kind === "variant" && piece.component) {
5002
+ const values = piece.values.map((v) => `${v.property ?? v.category} ${v.value}`).join(", ");
5003
+ const exploration = {
5004
+ name: piece.title,
5005
+ purpose: `${forLine(branch)}: ${/^[aeiou]/i.test(piece.name) ? "an" : "a"} ${piece.name} ${piece.aspect ?? "style"} on ${piece.component} (${values}).${near}`,
5006
+ useCase: "terminal",
5007
+ experimental: [piece.title],
5008
+ source: opts.source === "figma" ? "figma" : opts.source ?? "mcp"
5009
+ };
5010
+ const brief = [
5011
+ `Add the ${piece.name} ${piece.aspect ?? "style"} to ${piece.component} in the library, on this exploration branch.`,
5012
+ `The product branch writes it by hand: ${values}. Express it with tokens (a component or semantic token per value), add it to the component's props, docs and examples, and keep every existing ${piece.component} unchanged.`,
5013
+ piece.nearest ? `Say in the pull request why ${piece.nearest.label} is not enough, or recommend it instead.` : ""
5014
+ ].filter(Boolean).join(" ");
5015
+ return { kind: "exploration", title: piece.title, exploration, neededBy, brief };
5016
+ }
5017
+ const name = piece.component ?? cap(piece.name);
5018
+ const request = {
5019
+ component: name,
5020
+ kind: "new-component",
5021
+ title: `Add ${name}`,
5022
+ summary: `${forLine(branch)}: ${piece.summary}${near}`,
5023
+ changes: piece.values.slice(0, 6).map((v) => ({ field: v.property ?? v.category, after: v.value })),
5024
+ source: opts.source ?? "mcp",
5025
+ ...opts.requestedBy ? { requestedBy: opts.requestedBy } : {}
5026
+ };
5027
+ return { kind: "request", title: `New component: ${name}`, request, neededBy };
5028
+ }
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
+ var names2 = (people2) => people2.length <= 2 ? people2.map((p) => `@${p}`).join(" and ") : `@${people2[0]} and ${people2.length - 1} more`;
5043
+ function linkStatus(x) {
5044
+ const make = (key, people2 = [], verb = "reviews") => {
5045
+ const label = STATUS_LABEL[key];
5046
+ const tail = people2.length ? ` \xB7 ${names2(people2)} ${people2.length === 1 ? verb : verb.replace(/s$/, "")}` : "";
5047
+ return { key, label, people: people2, line: `${label}${tail}` };
5048
+ };
5049
+ if (x.kind === "request") {
5050
+ if (x.state === "closed") return make(x.stateReason === "not_planned" ? "declined" : "done");
5051
+ return x.implemented ? make("in-progress", [...x.assignees ?? []], "builds") : make("requested", [...x.assignees ?? []], "builds");
5052
+ }
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
+ }
5065
+ function libraryPreviewState(x) {
5066
+ if (x.kind === "request") {
5067
+ if (x.lifecycle === "closed") return { key: "declined", sentence: `The library declined this request.${x.nearest ? ` Ask Claude to use ${x.nearest} instead.` : ""}`, action: null };
5068
+ 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" } };
5069
+ return { key: "nothing-yet", sentence: "Nothing to preview yet: the library builds it first. Your branch keeps its own version until then.", action: null };
5070
+ }
5071
+ if (x.lifecycle === "merged") {
5072
+ if (x.behindMain === 0) return { key: "switched", sentence: "Your branch uses the published version.", action: null };
5073
+ return { key: "published", sentence: "The library published it. Switch your branch to the published version.", action: { kind: "switch", label: "Switch to the published version" } };
5074
+ }
5075
+ 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 };
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" } };
5080
+ }
5081
+ var nPieces = (n) => `${n} ${n === 1 ? "piece" : "pieces"}`;
5082
+ function productBranchSentence(r, opts = {}) {
5083
+ if (!r) return { lead: "Not checked against the library yet.", strong: "", rest: " Ask Claude what is missing from the library." };
5084
+ const n = r.pieces.length;
5085
+ if (!n) return { lead: "Built with the library.", strong: " Nothing is missing", rest: ": every value and component comes from it." };
5086
+ const linked = r.pieces.filter((p) => r.links.some((l) => l.piece === p.id));
5087
+ 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"))];
5088
+ const by = openers.length === 1 ? openers[0] : "your team";
5089
+ const was = n === 1 ? "was" : "were";
5090
+ if (linked.length === n) {
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}.` };
5093
+ }
5094
+ 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." };
5095
+ 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.` };
5096
+ }
5097
+ function pieceId(parts) {
5098
+ return slugify(parts.filter(Boolean).join(" "), 48);
5099
+ }
5100
+ var TONE_WORDS = ["neutral", "success", "warning", "danger", "info", "error", "critical", "accent", "brand", "primary", "secondary"];
5101
+ function tokenLabel(path) {
5102
+ const parts = path.split(".");
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;
5107
+ }
5108
+ function proposedTokenPath(nearest, name, category) {
5109
+ const slug2 = slugify(name, 32);
5110
+ if (!nearest) return `${category === "color" ? "color" : category}.${slug2}`;
5111
+ const parts = nearest.split(".");
5112
+ const toneAt = parts.findIndex((p) => TONE_WORDS.includes(p));
5113
+ if (toneAt > 0) return [...parts.slice(0, toneAt), slug2, ...parts.slice(toneAt + 1)].join(".");
5114
+ return [...parts.slice(0, -1), slug2].join(".");
5115
+ }
5116
+
5117
+ // ../core/src/prod-branch-github.ts
5118
+ var PRODUCT_FILE = /\.(css|scss|sass|less|tsx|jsx|ts|js|mjs|cjs|mts|cts|astro|vue|svelte)$/;
5119
+ var SKIP_FILE = /\.d\.[cm]?ts$|\.(test|spec|stories|story)\.[cm]?[jt]sx?$|\.min\.(css|js)$/;
5120
+ var PIECES_FILE_LIMIT = 40;
5121
+ var cleanPath = (p) => p.trim().replace(/^\.?\/+|\/+$/g, "");
5122
+ function productOf(products, pull, files) {
5123
+ const product = productOfBranch(products, { labels: pull.labels, files });
5124
+ if (product.key === defaultProduct(products).key) throw new GitHubError(422, `This branch is a library branch (${product.name}), not a product branch: missing pieces are checked on a branch started for a product.`);
5125
+ return product;
5126
+ }
5127
+ async function readProductBranchOnGitHub(gh, pr, opts) {
5128
+ const pull = await gh.getPullRequest(pr);
5129
+ const cmp = await gh.compareDetailed(pull.baseRef, pull.headSha);
5130
+ const product = productOf(opts.products, pull, cmp.files.map((f) => f.filename));
5131
+ const dir = product.path ? `${cleanPath(product.path)}/` : "";
5132
+ const changed = cmp.files.filter((f) => f.status !== "removed" && (!dir || f.filename.startsWith(dir)) && PRODUCT_FILE.test(f.filename) && !SKIP_FILE.test(f.filename) && !f.filename.split("/").includes("node_modules"));
5133
+ const read = changed.slice(0, PIECES_FILE_LIMIT);
5134
+ const start = cmp.mergeBase ?? pull.baseRef;
5135
+ const files = await Promise.all(
5136
+ read.map(async (f) => {
5137
+ const [before, after] = await Promise.all([f.status === "added" ? Promise.resolve(void 0) : gh.getFileText(f.previous_filename ?? f.filename, start), gh.getFileText(f.filename, pull.headSha)]);
5138
+ return { path: f.filename, before: before ?? null, after: after ?? null };
5139
+ })
5140
+ );
5141
+ return { pull, product, files, skipped: changed.length - read.length, record: parsePiecesRecord(pull.body) };
5142
+ }
5143
+ async function recordPiecesOnGitHub(gh, pr, check) {
5144
+ const pull = await gh.getPullRequest(pr);
5145
+ const record = mergePiecesRecord(parsePiecesRecord(pull.body), { checkedAt: check.checkedAt ?? (/* @__PURE__ */ new Date()).toISOString(), headSha: check.headSha ?? pull.headSha, pieces: check.pieces, fixes: check.fixes });
5146
+ await gh.updatePullRequest(pr, { body: withPiecesRecord(pull.body, record) });
5147
+ return record;
5148
+ }
5149
+ var productWriter = (gh, branch) => isExplorationBranch(branch) ? gh.withBranchPrefix(EXPLORATION_PREFIX) : gh;
5150
+ async function useLibraryPreviewOnGitHub(gh, input) {
5151
+ const message = `chore(bridge): use the preview of ${input.libraryBranch} (#${input.libraryNumber})
5152
+
5153
+ The product branch takes the library branch before it ships. Switch to the published version once it does.`;
5154
+ try {
5155
+ const r = await productWriter(gh, input.productBranch).mergeIntoBranch(input.productBranch, input.libraryBranch, message);
5156
+ return { ...r, said: r.status === "merged" ? "Your branch now uses the preview of the library branch." : "Your branch already has the latest preview." };
5157
+ } catch (err) {
5158
+ if (err instanceof GitHubError && err.status === 409) return { status: "conflict", sha: null, said: `Your branch and the library branch change the same lines. A developer merges ${input.libraryBranch} into ${input.productBranch} by hand.` };
5159
+ throw err;
5160
+ }
5161
+ }
5162
+ function productBranchRef(pull, product) {
5163
+ const name = /<!-- bridge-exploration (\{[\s\S]*?\}) -->/.exec(pull.body ?? "");
5164
+ let title = pull.title.replace(/^[A-Z][A-Z0-9_]*-\d+\s+/, "").replace(/^Exploration:\s*/i, "");
5165
+ let author = pull.author;
5166
+ if (name) {
5167
+ try {
5168
+ const e = JSON.parse(name[1]);
5169
+ if (e.name) title = e.name;
5170
+ if (e.author) author = e.author;
5171
+ } catch {
5172
+ }
5173
+ }
5174
+ return { pr: pull.number, title: title.charAt(0).toUpperCase() + title.slice(1), branch: pull.headRef, url: pull.url, productKey: product.key, productName: product.name, ...author ? { author } : {} };
5175
+ }
5176
+ async function openLibraryBranchOnGitHub(gh, input) {
5177
+ const pull = await gh.getPullRequest(input.pr);
5178
+ const record = parsePiecesRecord(pull.body);
5179
+ const piece = record?.pieces.find((p) => p.id === input.piece);
5180
+ 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
+ const product = productOf(input.products, pull, await gh.listPullRequestFiles(input.pr).then((f) => f.map((x) => x.filename), () => []));
5182
+ const plan = libraryBranchPlan(piece, productBranchRef(pull, product), { source: input.source, requestedBy: input.openedBy });
5183
+ const existing = record.links.find((l) => l.piece === piece.id);
5184
+ if (existing) return { status: "already-linked", plan, link: existing, preview: null, warnings: [] };
5185
+ 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).");
5187
+ const warnings = [];
5188
+ const at = (/* @__PURE__ */ new Date()).toISOString();
5189
+ const who = { ...input.openedBy ? { openedBy: input.openedBy } : {}, ...input.agent ? { agent: input.agent } : {} };
5190
+ let link;
5191
+ if (plan.kind === "token-change") {
5192
+ 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 };
5195
+ } else if (plan.kind === "exploration") {
5196
+ 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
+ link = { piece: piece.id, kind: "exploration", number: r.pr, url: r.url, title: plan.title, branch: r.branch, at, ...who };
5198
+ } else {
5199
+ const issue = composeRequestIssue(plan.request, { labels: [NEEDED_BY_LABEL] });
5200
+ const r = await gh.createIssue(issue);
5201
+ link = { piece: piece.id, kind: "request", number: r.number, url: r.url, title: plan.title, at, ...who };
5202
+ }
5203
+ try {
5204
+ if (link.kind === "request") {
5205
+ const issue = await gh.getIssue(link.number);
5206
+ await gh.updateIssue(link.number, { body: withNeededBy(issue.body, plan.neededBy) });
5207
+ } else {
5208
+ const lib = await gh.getPullRequest(link.number);
5209
+ await gh.updatePullRequest(link.number, { body: withNeededBy(lib.body, plan.neededBy) });
5210
+ await gh.addLabels(link.number, [NEEDED_BY_LABEL]);
5211
+ }
5212
+ } catch (err) {
5213
+ if (err instanceof GitHubError && err.auth === "rejected") throw err;
5214
+ 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
+ }
5216
+ const fresh = await gh.getPullRequest(input.pr);
5217
+ const next = withLink(parsePiecesRecord(fresh.body) ?? record, link);
5218
+ await gh.updatePullRequest(input.pr, { body: withPiecesRecord(fresh.body, next) });
5219
+ let preview = null;
5220
+ if ((input.usePreview ?? true) && link.kind === "token-change" && link.branch) {
5221
+ preview = await useLibraryPreviewOnGitHub(gh, { productBranch: pull.headRef, libraryBranch: link.branch, libraryNumber: link.number }).catch((err) => {
5222
+ warnings.push(`Your branch does not use the preview yet: ${err.message}`);
5223
+ return null;
5224
+ });
5225
+ }
5226
+ return { status: "opened", plan, link, preview, warnings };
5227
+ }
5228
+ var isNote = (path) => path.startsWith(`${EXPLORATION_NOTES_DIR}/`);
5229
+ async function linkedLibraryBranchOnGitHub(gh, link, product, piece) {
5230
+ const nearest = piece?.nearest?.label;
5231
+ if (link.kind === "request") {
5232
+ const issue = await gh.getIssue(link.number);
5233
+ const status2 = linkStatus({ kind: "request", state: issue.state, stateReason: issue.stateReason });
5234
+ const lifecycle2 = issue.state === "closed" ? issue.stateReason === "not_planned" ? "closed" : "merged" : "open";
5235
+ const behindMain2 = lifecycle2 === "merged" ? await gh.compare(product.baseRef, product.headSha).then((c) => c.behindBy, () => null) : null;
5236
+ const preview2 = libraryPreviewState({ kind: "request", lifecycle: lifecycle2, hasChanges: false, missing: null, total: null, behindMain: behindMain2, nearest });
5237
+ return { link, piece, status: status2, preview: preview2, people: status2.people };
5238
+ }
5239
+ const lib = await gh.getPullRequest(link.number);
5240
+ const reviews = await gh.listReviews(link.number).catch(() => []);
5241
+ const status = linkStatus({ kind: link.kind, state: lib.state, draft: lib.draft, labels: lib.labels, requestedReviewers: lib.requestedReviewers, reviews: reviews.map((r) => ({ user: r.user, state: r.state })) });
5242
+ const lifecycle = lib.state === "merged" ? "merged" : lib.state === "open" ? "open" : "closed";
5243
+ let missing = null;
5244
+ let total = null;
5245
+ let behindMain = null;
5246
+ let hasChanges = true;
5247
+ if (lifecycle === "open") {
5248
+ const [files, own, inProduct] = await Promise.all([
5249
+ gh.listPullRequestFiles(link.number).catch(() => null),
5250
+ gh.compare(lib.baseRef, lib.headSha).catch(() => null),
5251
+ gh.compare(product.headSha, lib.headSha).catch(() => null)
5252
+ ]);
5253
+ hasChanges = files ? files.some((f) => !isNote(f.filename)) : true;
5254
+ total = own?.aheadBy ?? null;
5255
+ missing = inProduct?.aheadBy ?? null;
5256
+ } else if (lifecycle === "merged") {
5257
+ behindMain = await gh.compare(product.baseRef, product.headSha).then((c) => c.behindBy, () => null);
5258
+ }
5259
+ const preview = libraryPreviewState({ kind: link.kind, lifecycle, hasChanges, missing, total, behindMain, nearest });
5260
+ return { link, piece, status, preview, people: status.people };
5261
+ }
5262
+ async function productBranchViewOnGitHub(gh, pr, opts) {
5263
+ const pull = await gh.getPullRequest(pr);
5264
+ const files = await gh.listPullRequestFiles(pr).then((f) => f.map((x) => x.filename), () => []);
5265
+ const product = productOf(opts.products, pull, files);
5266
+ const record = parsePiecesRecord(pull.body);
5267
+ const linked = await Promise.all(
5268
+ (record?.links ?? []).map((l) => linkedLibraryBranchOnGitHub(gh, l, { headSha: pull.headSha, baseRef: pull.baseRef }, record?.pieces.find((p) => p.id === l.piece)))
5269
+ );
5270
+ const open = (record?.pieces ?? []).filter((p) => !record.links.some((l) => l.piece === p.id));
5271
+ return { pull, product, record, sentence: productBranchSentence(record, { viewer: opts.viewer, agentName: opts.agentName }), linked, open };
5272
+ }
5273
+ async function switchToPublishedOnGitHub(gh, pr) {
5274
+ if (!gh.canWrite) throw new GitHubError(401, "Switching needs a GitHub token (contents and pull requests: write).");
5275
+ const pull = await gh.getPullRequest(pr);
5276
+ if (pull.state !== "open") throw new GitHubError(422, `#${pr} is ${pull.state}: only an open branch switches.`);
5277
+ const cmp = await gh.compare(pull.baseRef, pull.headSha);
5278
+ if (cmp.behindBy === 0) return { status: "up-to-date", said: "Your branch already uses the published version." };
5279
+ try {
5280
+ await gh.updatePullRequestBranch(pr, pull.headSha);
5281
+ } catch (err) {
5282
+ if (err instanceof GitHubError && err.status === 422) throw new GitHubError(422, `Your branch and ${pull.baseRef} change the same lines, so Bridge cannot switch it alone. Use "Update from main" to pick the token values, or ask a developer.`, err.detail);
5283
+ throw err;
5284
+ }
5285
+ return { status: "switched", said: `Your branch now uses the published version from ${pull.baseRef}.` };
5286
+ }
5287
+
5288
+ // ../core/src/token-proposal.ts
5289
+ var issueKey2 = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
5290
+ function planTokenProposal(input) {
5291
+ const { before, config } = input;
5292
+ const r = applyTokenEdits(before, input.edits);
5293
+ const after = new LayeredTokenSet(before.config, r.files);
5294
+ const changes = diffLayered(before, after);
5295
+ if (!changes.length) return null;
5296
+ const prevErrors = new Set(validateLayered(before).map(issueKey2));
5297
+ const newErrors = validateLayered(after).filter((i) => i.level === "error" && !prevErrors.has(issueKey2(i)));
5298
+ const modes = config.contrast?.modes;
5299
+ const contrast = input.pairs.length ? contrastDelta(runLayeredContrast(before, input.pairs, modes), runLayeredContrast(after, input.pairs, modes)) : [];
5300
+ const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
5301
+ const impact = layeredImpact(before, after, changes);
5302
+ const tier = classifyLayered(changes, [], config);
5303
+ const summary2 = input.summary?.trim() || input.title;
5304
+ const body = [composeProposalBody({ summary: summary2, changes, contrast, impact, tier, validationErrors: newErrors.length, tool: input.tool ?? "Bridge MCP" }), ...input.bodyExtra ?? []].filter(Boolean).join("\n\n");
5305
+ const slug2 = slugify(input.title, 48) || "change";
5306
+ const changesetPath = `${config.changesets?.dir ?? ".changeset"}/bridge-${slug2}.md`;
5307
+ const touched = r.touched.map((f) => after.repoPath(f));
5308
+ const files = [
5309
+ ...r.touched.map((f) => ({ path: after.repoPath(f), content: after.files.get(f) })),
5310
+ ...config.changesets ? [{ path: changesetPath, content: formatChangeset(config.changesets.package, tier.bump, summary2) }] : []
5311
+ ];
5312
+ 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 };
5314
+ }
5315
+ async function writeTokenProposalOnGitHub(gh, plan, opts) {
5316
+ if (plan.blocked) throw new Error(`Not opened: ${plan.blocked}. Bridge never opens a branch that breaks the gate.`);
5317
+ const jira = opts.jira ?? null;
5318
+ let branch = branchWithJiraKey(`${opts.branchPrefix}${plan.slug}`, jira);
5319
+ for (let i = 2; await gh.getBranchSha(branch).then(Boolean, () => false); i++) branch = branchWithJiraKey(`${opts.branchPrefix}${plan.slug}-${i}`, jira);
5320
+ const commit = await gh.commitToNewBranch({ base: opts.base, branch, message: `feat(tokens): ${plan.title}
5321
+
5322
+ Proposed through ${opts.tool ?? "Bridge MCP"}. Tier ${plan.tier.tier}.`, files: plan.files });
5323
+ const labels = [.../* @__PURE__ */ new Set([...opts.labels ?? [], `tier: ${plan.tier.tier}`, ...plan.tier.tier === 3 ? ["tokens"] : [], ...plan.tier.bump === "major" ? ["breaking"] : [], ...opts.labelsAfter ?? []])];
5324
+ const draft = plan.tier.tier >= 2;
5325
+ const pr = await gh.createPullRequest({ base: opts.base, head: branch, title: titleWithJiraKey(`feat(tokens): ${plan.title}`, jira), body: plan.body, draft });
5326
+ await gh.addLabels(pr.number, labels);
5327
+ return { branch, commit: commit.sha, pr: pr.number, url: pr.url, labels, draft };
5328
+ }
5329
+
4892
5330
  // ../core/src/node/git-history.ts
4893
5331
  var US = "";
4894
5332
  var RS = "";
@@ -5744,8 +6182,8 @@ function tailwindFindings(hits, offset, pos, ds) {
5744
6182
  else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
5745
6183
  if (cat === "color" && !isColor) continue;
5746
6184
  const suggestion = ds.suggest(cat, v);
5747
- const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
5748
- findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
6185
+ const cap2 = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
6186
+ findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap2 ? { suggestion: cap2 } : {} });
5749
6187
  }
5750
6188
  return { findings, themeRefs };
5751
6189
  }
@@ -6753,9 +7191,9 @@ var DesignSystemIndex = class {
6753
7191
  return generic ? this.make(generic, 1, 0.3) : void 0;
6754
7192
  }
6755
7193
  case "shadow": {
6756
- const norm2 = (s) => s.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").trim().toLowerCase();
7194
+ const norm3 = (s) => s.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").trim().toLowerCase();
6757
7195
  const cands = this.entries(["shadow"]);
6758
- const exact = cands.find((e) => norm2(e.css) === norm2(value));
7196
+ const exact = cands.find((e) => norm3(e.css) === norm3(value));
6759
7197
  if (exact) return this.make(exact, 0, 1);
6760
7198
  const t = shadowNums(value);
6761
7199
  const scored = cands.map((e) => {
@@ -7181,11 +7619,11 @@ async function listComponentNames(source, config) {
7181
7619
  }
7182
7620
  var findFamily = (stories) => stories ? /title:\s*['"]([^'"/]+)\//.exec(stories)?.[1] : void 0;
7183
7621
  async function listComponents(source, config, withSummaries) {
7184
- const names2 = await listComponentNames(source, config);
7185
- if (!withSummaries) return names2.map((n) => ({ name: n.name }));
7622
+ const names3 = await listComponentNames(source, config);
7623
+ if (!withSummaries) return names3.map((n) => ({ name: n.name }));
7186
7624
  const dir = config.components.dir;
7187
7625
  return Promise.all(
7188
- names2.map(async ({ name, files }) => {
7626
+ names3.map(async ({ name, files }) => {
7189
7627
  const docsPath = `${dir}/${name}/${name}${config.components.docsSuffix}`;
7190
7628
  const storiesPath = files.find((f) => /\.stories\.\w+$/.test(f));
7191
7629
  const [docs, stories] = await Promise.all([files.includes(docsPath) ? source.read(docsPath) : null, storiesPath ? source.read(storiesPath) : null]);
@@ -7480,6 +7918,7 @@ __export(ops_exports, {
7480
7918
  auditOp: () => auditOp,
7481
7919
  auditUi: () => auditUi,
7482
7920
  checkContrast: () => checkContrast,
7921
+ designSystemIndex: () => designSystemIndex,
7483
7922
  draftDocsOp: () => draftDocsOp,
7484
7923
  getComponentOp: () => getComponentOp,
7485
7924
  getConfig: () => getConfig,
@@ -7503,6 +7942,247 @@ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:f
7503
7942
  import { mkdir as mkdir2, writeFile as writeFile2, access } from "node:fs/promises";
7504
7943
  import { dirname as dirname3, isAbsolute, join as join4, resolve as resolve3 } from "node:path";
7505
7944
 
7945
+ // ../core/src/audit/missing-pieces.ts
7946
+ var lines = (text) => text.split(/\r?\n/);
7947
+ function lineBag(text) {
7948
+ const bag = /* @__PURE__ */ new Map();
7949
+ for (const l of text ? lines(text) : []) {
7950
+ const k = l.trim();
7951
+ if (k) bag.set(k, (bag.get(k) ?? 0) + 1);
7952
+ }
7953
+ return bag;
7954
+ }
7955
+ var offsetOf = (text, line, column) => {
7956
+ let off = 0;
7957
+ const ls = text.split("\n");
7958
+ for (let i = 0; i < line - 1 && i < ls.length; i++) off += ls[i].length + 1;
7959
+ return off + Math.max(0, column - 1);
7960
+ };
7961
+ function enclosingTag(text, offset) {
7962
+ for (let i = offset - 1; i >= Math.max(0, offset - 4e3); i--) {
7963
+ if (text[i] !== "<" || !/[A-Za-z]/.test(text[i + 1] ?? "")) continue;
7964
+ let depth = 0;
7965
+ let closed = false;
7966
+ for (let j = i + 1; j < offset; j++) {
7967
+ const c = text[j];
7968
+ if (c === "{") depth++;
7969
+ else if (c === "}") depth--;
7970
+ else if (c === ">" && depth <= 0 && text[j - 1] !== "=") {
7971
+ closed = true;
7972
+ break;
7973
+ }
7974
+ }
7975
+ if (closed) return null;
7976
+ const m = /^<([A-Za-z][\w.]*)/.exec(text.slice(i));
7977
+ return m ? { name: m[1], start: i } : null;
7978
+ }
7979
+ return null;
7980
+ }
7981
+ function childText(text, tagStart) {
7982
+ let depth = 0;
7983
+ for (let j = tagStart + 1; j < text.length; j++) {
7984
+ const c = text[j];
7985
+ if (c === "{") depth++;
7986
+ else if (c === "}") depth--;
7987
+ else if (c === ">" && depth <= 0 && text[j - 1] !== "=") {
7988
+ if (text[j - 1] === "/") return null;
7989
+ const rest = /^([^<{]*)</.exec(text.slice(j + 1));
7990
+ const t = rest?.[1]?.trim() ?? "";
7991
+ return t && t.split(/\s+/).length <= 3 && t.length <= 30 ? t : null;
7992
+ }
7993
+ }
7994
+ return null;
7995
+ }
7996
+ function ruleClasses(text, offset) {
7997
+ const open = text.lastIndexOf("{", offset);
7998
+ if (open < 0) return [];
7999
+ const prev = Math.max(text.lastIndexOf("}", open), text.lastIndexOf(";", open));
8000
+ const selector = text.slice(prev + 1, open);
8001
+ return [...selector.matchAll(/\.([A-Za-z_][\w-]*)/g)].map((m) => m[1]);
8002
+ }
8003
+ var GENERIC = /* @__PURE__ */ new Set(["status", "badge", "tag", "pill", "chip", "style", "styles", "root", "wrapper", "container", "item", "label", "text", "color", "bg", "is", "has", "variant", "tone", "custom", "new"]);
8004
+ function nameFromClass(cls, component) {
8005
+ const words = cls.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[\s_-]+/).map((w) => w.toLowerCase()).filter(Boolean);
8006
+ const comp = component?.toLowerCase();
8007
+ const kept = words.filter((w) => w !== comp && !GENERIC.has(w));
8008
+ return kept[kept.length - 1] ?? words[words.length - 1] ?? cls;
8009
+ }
8010
+ var ASPECT = { color: "tone", radius: "shape", spacing: "size", size: "size", "font-size": "size", "line-height": "size" };
8011
+ function aspectOf(values) {
8012
+ for (const a of ["tone", "shape", "size"]) if (values.some((v) => ASPECT[v.category] === a)) return a;
8013
+ return "style";
8014
+ }
8015
+ var norm2 = (f) => f.category === "color" ? f.value.toLowerCase() : f.value.replace(/\s+/g, "");
8016
+ function nearestOf(s) {
8017
+ if (!s) return void 0;
8018
+ return { token: s.token, cssVar: s.cssVar, value: s.value, level: s.level, label: tokenLabel(s.token) };
8019
+ }
8020
+ var TOKEN_TYPE = { color: "color", duration: "duration", "font-weight": "fontWeight", "line-height": "number", "z-index": "number", "font-family": "fontFamily", shadow: "shadow", easing: "cubicBezier" };
8021
+ var valueTitle = (category, value) => {
8022
+ const v = category === "color" && /^#[0-9a-f]+$/i.test(value) ? value.toUpperCase() : value;
8023
+ const word = { color: "color", radius: "radius", spacing: "spacing", size: "size", "font-size": "text size", "font-weight": "font weight", "line-height": "line height", shadow: "shadow", duration: "duration", easing: "easing", "z-index": "layer", "border-width": "border width", "font-family": "font" };
8024
+ return `New ${word[category] ?? category} ${v}`;
8025
+ };
8026
+ function missingPieces(input) {
8027
+ const { ds } = input;
8028
+ const files = input.files.filter((f) => f.after !== null && isAuditable(f.path));
8029
+ const fixes = [];
8030
+ const variants = /* @__PURE__ */ new Map();
8031
+ const tokens = /* @__PURE__ */ new Map();
8032
+ const foreign = /* @__PURE__ */ new Map();
8033
+ const classUse = (cls) => {
8034
+ const re = new RegExp(`<([A-Z][\\w.]*)\\b[^<]*?styles(?:\\.${cls}\\b|\\[['"]${cls}['"]\\])`);
8035
+ for (const f of files) {
8036
+ const m = re.exec(f.after);
8037
+ if (m && ds.components.has(m[1].split(".")[0])) return { component: m[1].split(".")[0], text: childText(f.after, m.index) };
8038
+ }
8039
+ return null;
8040
+ };
8041
+ for (const f of files) {
8042
+ const after = f.after;
8043
+ const report = auditFile(f.path, after, ds);
8044
+ const before = lineBag(f.before);
8045
+ const afterLines = lines(after);
8046
+ const added = (line) => {
8047
+ const k = afterLines[line - 1]?.trim() ?? "";
8048
+ const n = before.get(k) ?? 0;
8049
+ if (n > 0) {
8050
+ before.set(k, n - 1);
8051
+ return false;
8052
+ }
8053
+ return true;
8054
+ };
8055
+ const addedLines = /* @__PURE__ */ new Map();
8056
+ const isNew = (line) => {
8057
+ if (!addedLines.has(line)) addedLines.set(line, added(line));
8058
+ return addedLines.get(line);
8059
+ };
8060
+ const css = report.lang === "css" || report.lang === "scss" || report.lang === "less";
8061
+ for (const finding of report.findings) {
8062
+ if (!isNew(finding.line)) continue;
8063
+ const s = finding.suggestion;
8064
+ const at = { file: f.path, line: finding.line };
8065
+ if (s?.level === "exact" || s?.level === "high" && finding.category !== "color") {
8066
+ fixes.push({ ...at, what: finding.value, use: s.token });
8067
+ continue;
8068
+ }
8069
+ const offset = offsetOf(after, finding.line, finding.column);
8070
+ let component = null;
8071
+ let name = null;
8072
+ if (css) {
8073
+ const classes = ruleClasses(after, offset);
8074
+ for (const cls of [...classes].reverse()) {
8075
+ const use = classUse(cls);
8076
+ if (use) {
8077
+ component = use.component;
8078
+ name = nameFromClass(cls, use.component);
8079
+ break;
8080
+ }
8081
+ }
8082
+ if (!component && classes.length) name = nameFromClass(classes[classes.length - 1]);
8083
+ } else {
8084
+ const tag = enclosingTag(after, offset);
8085
+ const root = tag?.name.split(".")[0];
8086
+ if (tag && root && ds.components.has(root)) {
8087
+ component = root;
8088
+ name = childText(after, tag.start);
8089
+ const cls = /className=\{?["'`]?([\w-]+)/.exec(after.slice(tag.start, offset))?.[1];
8090
+ if (!name && cls) name = nameFromClass(cls, root);
8091
+ }
8092
+ }
8093
+ const hit = { ...at, finding };
8094
+ if (component) {
8095
+ const label = (name ?? "custom").toLowerCase();
8096
+ const key2 = `${component}|${label}`;
8097
+ const v = variants.get(key2) ?? { component, name: label, hits: [] };
8098
+ v.hits.push(hit);
8099
+ variants.set(key2, v);
8100
+ continue;
8101
+ }
8102
+ const key = `${finding.category}|${norm2(finding)}`;
8103
+ const t = tokens.get(key) ?? { category: finding.category, value: finding.value, name, hits: [] };
8104
+ if (!t.name && name) t.name = name;
8105
+ t.hits.push(hit);
8106
+ tokens.set(key, t);
8107
+ }
8108
+ for (const c of report.components) {
8109
+ if (!isNew(c.line)) continue;
8110
+ if (c.suggestion) {
8111
+ fixes.push({ file: f.path, line: c.line, what: c.name, use: c.suggestion });
8112
+ continue;
8113
+ }
8114
+ if (c.kind !== "foreign") continue;
8115
+ const e = foreign.get(c.name) ?? { finding: c, hits: [] };
8116
+ e.hits.push({ file: f.path, line: c.line });
8117
+ foreign.set(c.name, e);
8118
+ }
8119
+ }
8120
+ const where = (hits) => hits.slice(0, 5).map((h) => ({ file: h.file, line: h.line }));
8121
+ const pieces = [];
8122
+ for (const v of variants.values()) {
8123
+ const values = [];
8124
+ for (const h of v.hits) {
8125
+ const pv = { category: h.finding.category, ...h.finding.property ? { property: h.finding.property } : {}, value: h.finding.value };
8126
+ if (!values.some((x) => x.category === pv.category && x.property === pv.property && norm2(x) === norm2(pv))) values.push(pv);
8127
+ }
8128
+ const aspect = aspectOf(values);
8129
+ const bg = v.hits.find((h) => /background/.test(h.finding.property ?? ""))?.finding.suggestion;
8130
+ const nearest = nearestOf(bg ?? v.hits.find((h) => h.finding.suggestion)?.finding.suggestion);
8131
+ const shown = values.map((x) => `${x.property ?? x.category} ${x.value}`).join(", ");
8132
+ pieces.push({
8133
+ id: pieceId([v.component, v.name, aspect]),
8134
+ kind: "variant",
8135
+ title: `${v.component}: ${v.name} ${aspect}`,
8136
+ summary: `${v.component} restyled by hand (${shown}).${nearest ? ` ${nearest.level === "high" ? "It looks a lot like" : "The closest in the library is"} ${nearest.label}.` : ""}`,
8137
+ name: v.name,
8138
+ component: v.component,
8139
+ aspect,
8140
+ values,
8141
+ ...nearest ? { nearest } : {},
8142
+ where: where(v.hits)
8143
+ });
8144
+ }
8145
+ for (const [name, e] of foreign) {
8146
+ pieces.push({
8147
+ id: pieceId(["component", name]),
8148
+ kind: "component",
8149
+ title: `New component: ${name}`,
8150
+ summary: `${name} comes from ${e.finding.library}, and the library has nothing like it.`,
8151
+ name,
8152
+ component: name.split(".").pop(),
8153
+ values: [],
8154
+ where: where(e.hits)
8155
+ });
8156
+ }
8157
+ for (const t of tokens.values()) {
8158
+ const nearest = nearestOf(t.hits[0].finding.suggestion);
8159
+ const pname = t.name ?? `${t.category === "color" ? "custom" : t.category}`;
8160
+ const path = proposedTokenPath(nearest?.token, pname, t.category);
8161
+ const tokenFiles = nearest && input.set ? [...new Set(input.set.defs.filter((d) => d.path === nearest.token).map((d) => d.file))] : [];
8162
+ const n = t.hits.length;
8163
+ pieces.push({
8164
+ id: pieceId([t.category, t.name ?? t.value.replace(/^#/, "")]),
8165
+ kind: "token",
8166
+ title: valueTitle(t.category, t.value),
8167
+ summary: `Written by hand in ${n === 1 ? "one place" : `${n} places`}.${nearest ? ` ${nearest.level === "high" ? "It looks a lot like" : "The closest in the library is"} ${nearest.label} (${nearest.value}).` : " The library has nothing close."}`,
8168
+ name: pname,
8169
+ values: [{ category: t.category, ...t.hits[0].finding.property ? { property: t.hits[0].finding.property } : {}, value: t.value }],
8170
+ ...nearest ? { nearest } : {},
8171
+ where: where(t.hits),
8172
+ token: { path, type: TOKEN_TYPE[t.category] ?? "dimension", value: t.value, files: tokenFiles }
8173
+ });
8174
+ }
8175
+ const order = { variant: 0, component: 1, token: 2 };
8176
+ const seen = /* @__PURE__ */ new Set();
8177
+ const out = pieces.sort((a, b) => order[a.kind] - order[b.kind]).map((p) => {
8178
+ let id = p.id;
8179
+ for (let i = 2; seen.has(id); i++) id = `${p.id}-${i}`;
8180
+ seen.add(id);
8181
+ return id === p.id ? p : { ...p, id };
8182
+ });
8183
+ return { pieces: out, fixes, files: files.length };
8184
+ }
8185
+
7506
8186
  // src/audit.ts
7507
8187
  var PROPERTY_HINTS = [
7508
8188
  [/^(color|fill|stroke)$/, /(^|\.)(text|fg|icon|foreground|link)(\.|-|$)/],
@@ -7523,8 +8203,8 @@ function auditSnippet(set, code2, max = 3) {
7523
8203
  });
7524
8204
  const rank2 = (tier) => tier === "semantic" ? 0 : tier === "component" ? 1 : tier === "density" ? 2 : tier === "brand" ? 3 : 4;
7525
8205
  const findings = [];
7526
- const lines = code2.split("\n");
7527
- lines.forEach((line, i) => {
8206
+ const lines2 = code2.split("\n");
8207
+ lines2.forEach((line, i) => {
7528
8208
  if (/var\(--/.test(line) && !/#[0-9a-f]{3,8}\b|\d(px|rem|ms)\b/i.test(line.replace(/var\([^)]*\)/g, ""))) return;
7529
8209
  const stripped = line.replace(/var\([^)]*\)/g, "");
7530
8210
  const propAt = (idx) => /([a-zA-Z-]+)['"]?\s*:[^;:,{]*$/.exec(stripped.slice(0, idx))?.[1]?.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`).toLowerCase();
@@ -7592,17 +8272,17 @@ function auditSnippet(set, code2, max = 3) {
7592
8272
  // src/markdown.ts
7593
8273
  var slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7594
8274
  function sections(md) {
7595
- const lines = md.split("\n");
8275
+ const lines2 = md.split("\n");
7596
8276
  const heads = [];
7597
8277
  let fence = false;
7598
- lines.forEach((l, i) => {
8278
+ lines2.forEach((l, i) => {
7599
8279
  if (/^\s*(```|~~~)/.test(l)) fence = !fence;
7600
8280
  const m = !fence && /^(#{1,4})\s+(.+?)\s*#*\s*$/.exec(l);
7601
8281
  if (m) heads.push({ level: m[1].length, title: m[2], line: i });
7602
8282
  });
7603
8283
  return heads.map((h, i) => {
7604
8284
  const next = heads.slice(i + 1).find((x) => x.level <= h.level);
7605
- return { level: h.level, title: h.title, slug: slug(h.title), body: lines.slice(h.line, next ? next.line : lines.length).join("\n").trim() };
8285
+ return { level: h.level, title: h.title, slug: slug(h.title), body: lines2.slice(h.line, next ? next.line : lines2.length).join("\n").trim() };
7606
8286
  });
7607
8287
  }
7608
8288
  function toc(md) {
@@ -7634,6 +8314,7 @@ __export(explorations_exports, {
7634
8314
  installOf: () => installOf,
7635
8315
  listExplorations: () => listExplorations,
7636
8316
  promoteExploration: () => promoteExploration,
8317
+ settingsOf: () => settingsOf,
7637
8318
  startExploration: () => startExploration,
7638
8319
  toExplorationBranch: () => toExplorationBranch,
7639
8320
  writeToExploration: () => writeToExploration
@@ -7658,13 +8339,16 @@ __export(branches_exports, {
7658
8339
  listProducts: () => listProducts,
7659
8340
  mergeBranch: () => mergeBranch,
7660
8341
  productMarks: () => productMarks,
8342
+ reader: () => reader,
7661
8343
  reopenBranch: () => reopenBranch,
7662
8344
  resolveProduct: () => resolveProduct,
7663
8345
  reviewBranch: () => reviewBranch,
7664
8346
  sendForReview: () => sendForReview,
7665
8347
  tryBranch: () => tryBranch,
7666
8348
  updateFromMain: () => updateFromMain,
7667
- whatNeedsMe: () => whatNeedsMe
8349
+ viewerOf: () => viewerOf,
8350
+ whatNeedsMe: () => whatNeedsMe,
8351
+ writer: () => writer
7668
8352
  });
7669
8353
  import { relative as relative2 } from "node:path";
7670
8354
  var RIGHTS = {
@@ -7976,11 +8660,11 @@ async function commentOnBranch(ctx, a) {
7976
8660
  async function listComments(ctx, a) {
7977
8661
  const n = branchNumber(a.pr);
7978
8662
  const gh = await reader(ctx);
7979
- const [conversation, lines] = await Promise.all([gh.listIssueComments(n), gh.listReviewComments(n).catch(() => [])]);
8663
+ const [conversation, lines2] = await Promise.all([gh.listIssueComments(n), gh.listReviewComments(n).catch(() => [])]);
7980
8664
  return {
7981
8665
  pr: n,
7982
8666
  conversation: conversation.filter((c) => !c.body.includes("<!-- bridge-branch-preview -->")).map((c) => ({ id: c.id, author: c.author, body: c.body, at: c.updatedAt })),
7983
- threads: lines.filter((c) => !c.inReplyTo).map((c) => ({ id: c.id, author: c.author, file: c.path, line: c.line, body: c.body, url: c.url, replies: lines.filter((r) => r.inReplyTo === c.id).map((r) => ({ id: r.id, author: r.author, body: r.body })) })),
8667
+ threads: lines2.filter((c) => !c.inReplyTo).map((c) => ({ id: c.id, author: c.author, file: c.path, line: c.line, body: c.body, url: c.url, replies: lines2.filter((r) => r.inReplyTo === c.id).map((r) => ({ id: r.id, author: r.author, body: r.body })) })),
7984
8668
  howToReply: "bridge_comment with replyTo: the thread id."
7985
8669
  };
7986
8670
  }
@@ -8432,7 +9116,7 @@ async function checkContrast(ctx, a) {
8432
9116
  const delta2 = core.contrastDelta(beforeRes, afterRes);
8433
9117
  return { ...base, edits: a.edits.length, notes, changed: delta2, newFailures: delta2.filter((d) => d.passBefore && !d.passAfter) };
8434
9118
  }
8435
- var issueKey2 = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
9119
+ var issueKey3 = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
8436
9120
  async function validateTokens(ctx, a) {
8437
9121
  const { before, after, notes } = await withEdits(ctx, a.edits);
8438
9122
  const issues = core.validate(after);
@@ -8448,8 +9132,8 @@ async function validateTokens(ctx, a) {
8448
9132
  ...a.includeWarnings ? { warnings } : warnings.length ? { warningSample: warnings.slice(0, 5) } : {}
8449
9133
  };
8450
9134
  if (!a.edits?.length) return res;
8451
- const prev = new Set(core.validate(before).map(issueKey2));
8452
- return { ...res, notes, newErrors: errors.filter((i) => !prev.has(issueKey2(i))) };
9135
+ const prev = new Set(core.validate(before).map(issueKey3));
9136
+ return { ...res, notes, newErrors: errors.filter((i) => !prev.has(issueKey3(i))) };
8453
9137
  }
8454
9138
  async function listComponentsOp(ctx, a) {
8455
9139
  const { config } = await ctx.config();
@@ -8602,25 +9286,13 @@ async function proposeTokenChange(ctx, a) {
8602
9286
  const target = a.branch ? await explorationContext(ctx, a.branch, { requireCheckout: !a.dryRun }) : null;
8603
9287
  if (target) ctx = target.ctx;
8604
9288
  const { config } = await ctx.config();
8605
- const { before, after, touched, notes } = await withEdits(ctx, a.edits);
8606
- const changes = core.diff(before, after);
8607
- if (!changes.length) return { status: "no-change", message: "The edits leave every token as it was." };
8608
- const prevErrors = new Set(core.validate(before).map(issueKey2));
8609
- const newErrors = core.validate(after).filter((i) => i.level === "error" && !prevErrors.has(issueKey2(i)));
9289
+ const before = await ctx.tokens();
8610
9290
  const pairs = await core.readContrastPairs(ctx.source, config).catch(() => []);
8611
- const delta2 = pairs.length ? core.contrastDelta(core.runContrast(before, pairs, config.contrast?.modes), core.runContrast(after, pairs, config.contrast?.modes)) : [];
8612
- const newFailures = delta2.filter((d) => d.passBefore && !d.passAfter);
8613
- const imp = core.impact(before, after, changes);
8614
- const tier = core.classify(changes, [], config);
8615
- const summary2 = a.summary?.trim() || a.title;
8616
9291
  const marks = productMarks(productsOf(config), product);
8617
- const body = [core.prBody({ summary: summary2, changes, contrast: delta2, impact: imp, tier, validationErrors: newErrors.length, tool: "Bridge MCP" }), marks.line].filter(Boolean).join("\n\n");
8618
- const slugTitle = core.slugify(a.title);
8619
- const changesetPath = `${config.changesets?.dir ?? ".changeset"}/bridge-${slugTitle}.md`;
8620
- const files = [
8621
- ...touched.map((f) => ({ path: prefixPath(after, f), content: after.files.get(f) })),
8622
- ...config.changesets ? [{ path: changesetPath, content: core.changeset(config.changesets.package, tier.bump, summary2) }] : []
8623
- ];
9292
+ const plan = planTokenProposal({ before, pairs, edits: a.edits, title: a.title, summary: a.summary, config, tool: "Bridge MCP", bodyExtra: [marks.line] });
9293
+ if (!plan) return { status: "no-change", message: "The edits leave every token as it was." };
9294
+ const { after, changes, tier, contrast: delta2, newFailures, newErrors, impact: imp, notes, files, body, slug: slugTitle } = plan;
9295
+ const summary2 = plan.summary;
8624
9296
  const report = {
8625
9297
  title: a.title,
8626
9298
  tier: tier.tier,
@@ -8669,15 +9341,12 @@ Tier ${tier.tier}. Proposed through Bridge MCP (source: mcp).`;
8669
9341
  }
8670
9342
  const gh = await ctx.github();
8671
9343
  if (!ctx.canWriteRemote) return { status: "preview", message: "Read-only: set GITHUB_TOKEN to open this as a pull request.", ...preview, fileContents: files };
8672
- const commitMsg = `feat(tokens): ${a.title}
8673
-
8674
- Proposed through Bridge MCP (source: mcp). Tier ${tier.tier}.`;
8675
9344
  const base = ctx.ref ?? baseBranchOf(config);
8676
- for (let i = 2; await gh.getBranchSha(branch).then(Boolean, () => false); i++) branch = branchWithJiraKey(`${config.governance.branchPrefix}${slugTitle}-${i}`, jira);
8677
- const commit = await gh.commitToNewBranch({ base, branch, message: commitMsg, files });
8678
- const labels = [...config.governance.labels, `tier: ${tier.tier}`, ...tier.tier === 3 ? ["tokens"] : [], ...tier.bump === "major" ? ["breaking"] : [], ...marks.labels];
8679
- const pr = await gh.createPullRequest({ base, head: branch, title: titleWithJiraKey(`feat(tokens): ${a.title}`, jira), body, draft: tier.tier >= 2 });
8680
- await gh.addLabels(pr.number, labels);
9345
+ const opened = await writeTokenProposalOnGitHub(gh, plan, { base, branchPrefix: config.governance.branchPrefix, jira, labels: config.governance.labels, labelsAfter: marks.labels, tool: "Bridge MCP (source: mcp)" });
9346
+ branch = opened.branch;
9347
+ const commit = { sha: opened.commit };
9348
+ const pr = { number: opened.pr, url: opened.url };
9349
+ const labels = opened.labels;
8681
9350
  return { status: "pr-opened", ...preview, branch, commit: commit.sha, pr: pr.number, url: pr.url, labels, draft: tier.tier >= 2, product: product.name };
8682
9351
  }
8683
9352
  async function addToBranch(ctx, a) {
@@ -8688,8 +9357,8 @@ async function addToBranch(ctx, a) {
8688
9357
  const { before, after, touched, notes } = await withEdits(at, a.edits);
8689
9358
  const changes = core.diff(before, after);
8690
9359
  if (!changes.length) return { status: "no-change", pr: n, message: `#${n} already has these values.` };
8691
- const prevErrors = new Set(core.validate(before).map(issueKey2));
8692
- const newErrors = core.validate(after).filter((i) => i.level === "error" && !prevErrors.has(issueKey2(i)));
9360
+ const prevErrors = new Set(core.validate(before).map(issueKey3));
9361
+ const newErrors = core.validate(after).filter((i) => i.level === "error" && !prevErrors.has(issueKey3(i)));
8693
9362
  const pairs = await core.readContrastPairs(at.source, config).catch(() => []);
8694
9363
  const delta2 = pairs.length ? core.contrastDelta(core.runContrast(before, pairs, config.contrast?.modes), core.runContrast(after, pairs, config.contrast?.modes)) : [];
8695
9364
  const newFailures = delta2.filter((d) => d.passBefore && !d.passAfter);
@@ -8786,7 +9455,7 @@ async function getReview(ctx, a) {
8786
9455
  const changes = core.diff(before, after);
8787
9456
  const pairs = await core.readContrastPairs(headSrc, config).catch(() => []);
8788
9457
  const delta2 = pairs.length ? core.contrastDelta(core.runContrast(before, pairs, config.contrast?.modes), core.runContrast(after, pairs, config.contrast?.modes)) : [];
8789
- const prevErrors = new Set(core.validate(before).map(issueKey2));
9458
+ const prevErrors = new Set(core.validate(before).map(issueKey3));
8790
9459
  const errors = core.validate(after).filter((i) => i.level === "error");
8791
9460
  const tokenRoot = config.tokens.root.replace(/\/$/, "");
8792
9461
  const otherFiles = changedFiles.filter((f) => !f.startsWith(`${tokenRoot}/`) && !f.startsWith(`${config.changesets?.dir ?? ".changeset"}/`));
@@ -8804,7 +9473,7 @@ async function getReview(ctx, a) {
8804
9473
  diffTable: core.tokenTable(changes.map((c) => ({ ...c, file: prefixPath(after, c.file) }))),
8805
9474
  contrastTable: core.contrastTable(delta2),
8806
9475
  contrastNewFailures: delta2.filter((d) => d.passBefore && !d.passAfter).length,
8807
- validation: { errors: errors.length, newErrors: errors.filter((i) => !prevErrors.has(issueKey2(i))) },
9476
+ validation: { errors: errors.length, newErrors: errors.filter((i) => !prevErrors.has(issueKey3(i))) },
8808
9477
  impact: imp
8809
9478
  };
8810
9479
  }
@@ -9632,8 +10301,142 @@ async function figmaCoverage(ctx, a = {}) {
9632
10301
  };
9633
10302
  }
9634
10303
 
10304
+ // src/pieces.ts
10305
+ var pieces_exports = {};
10306
+ __export(pieces_exports, {
10307
+ missingPiecesOp: () => missingPiecesOp,
10308
+ openLibraryBranchOp: () => openLibraryBranchOp,
10309
+ productBranchOp: () => productBranchOp,
10310
+ switchToPublishedOp: () => switchToPublishedOp,
10311
+ useLibraryPreviewOp: () => useLibraryPreviewOp
10312
+ });
10313
+ var NEED = "Contents, Pull requests and Issues: read and write";
10314
+ var short = (p) => ({
10315
+ id: p.id,
10316
+ kind: p.kind,
10317
+ title: p.title,
10318
+ summary: p.summary,
10319
+ ...p.component ? { component: p.component } : {},
10320
+ ...p.nearest ? { nearest: `${p.nearest.label} (${p.nearest.token}, ${p.nearest.value})` } : {},
10321
+ ...p.token ? { token: p.token.path } : {},
10322
+ where: p.where.map((w) => `${w.file}:${w.line}`),
10323
+ opens: p.kind === "token" ? "a token change" : p.kind === "variant" ? "a library exploration" : "a component request"
10324
+ });
10325
+ async function missingPiecesOp(ctx, a) {
10326
+ const n = branchNumber(a.pr);
10327
+ const gh = a.record ? await writer(ctx, "Recording the missing pieces", NEED) : await reader(ctx);
10328
+ const { config } = await ctx.config();
10329
+ const read = await readProductBranchOnGitHub(gh, n, { products: productsOf(config) });
10330
+ const [ds, set] = await Promise.all([designSystemIndex(ctx), ctx.tokens()]);
10331
+ const r = missingPieces({ files: read.files, ds, set });
10332
+ const recorded = a.record ? await recordPiecesOnGitHub(gh, n, { pieces: r.pieces, fixes: r.fixes.length, headSha: read.pull.headSha }) : null;
10333
+ const linked = recorded?.links ?? read.record?.links ?? [];
10334
+ return {
10335
+ pr: n,
10336
+ branch: read.pull.headRef,
10337
+ product: read.product.name,
10338
+ files: r.files,
10339
+ ...read.skipped ? { skipped: `${read.skipped} more product files were not read (limit ${r.files}).` } : {},
10340
+ pieces: r.pieces.map((p) => ({ ...short(p), ...linked.some((l) => l.piece === p.id) ? { linked: linked.find((l) => l.piece === p.id).number } : {} })),
10341
+ fixes: { count: r.fixes.length, sample: r.fixes.slice(0, 10).map((f) => `${f.file}:${f.line} ${f.what} -> use ${f.use}`) },
10342
+ recorded: Boolean(recorded),
10343
+ next: r.pieces.length ? `${a.record ? "" : "Record them (record: true) so Studio shows them, then "}open a library branch for each with bridge_open_library_branch { pr: ${n}, piece: "<id>" }. Fixes are values and components the library already has: use them in the product code instead.` : "Nothing is missing from the library. Fix the values that equal a token, if any."
10344
+ };
10345
+ }
10346
+ function tokenWriter(ctx, gh, source) {
10347
+ return async (plan) => {
10348
+ const { config } = await ctx.config();
10349
+ const [before, pairs] = await Promise.all([ctx.tokens(), core.readContrastPairs(ctx.source, config).catch(() => [])]);
10350
+ const p = planTokenProposal({ before, pairs, edits: plan.edits, title: plan.title, summary: plan.summary, config, tool: source === "mcp" ? "Bridge MCP" : "the bridge CLI" });
10351
+ if (!p) throw new Error("The token already exists with that value: nothing to add.");
10352
+ if (p.blocked) throw new Error(`The library branch was not opened: ${p.blocked}. Change the value (bridge_propose_token_change with dryRun shows why).`);
10353
+ const opened = await writeTokenProposalOnGitHub(gh, p, { base: baseBranchOf(config), branchPrefix: config.governance.branchPrefix, labels: config.governance.labels, labelsAfter: [NEEDED_BY_LABEL], tool: source === "mcp" ? "Bridge MCP (source: mcp)" : "the bridge CLI" });
10354
+ return { number: opened.pr, url: opened.url, branch: opened.branch };
10355
+ };
10356
+ }
10357
+ async function openLibraryBranchOp(ctx, a) {
10358
+ const n = branchNumber(a.pr);
10359
+ const source = a.source ?? "mcp";
10360
+ const gh = a.dryRun ? await reader(ctx) : await writer(ctx, "Opening a library branch", NEED);
10361
+ const { config } = await ctx.config();
10362
+ const viewer = await viewerOf(ctx, gh);
10363
+ const s = settingsOf(config);
10364
+ const r = await openLibraryBranchOnGitHub(gh, {
10365
+ pr: n,
10366
+ piece: a.piece,
10367
+ base: s.base,
10368
+ products: productsOf(config),
10369
+ exploration: { packages: s.packages, apps: s.apps },
10370
+ writeTokens: tokenWriter(ctx, gh, source),
10371
+ ...viewer ? { openedBy: viewer } : {},
10372
+ ...source === "mcp" ? { agent: "claude" } : {},
10373
+ source,
10374
+ usePreview: a.usePreview,
10375
+ dryRun: a.dryRun
10376
+ });
10377
+ const plan = r.plan;
10378
+ const what = plan.kind === "token-change" ? "token change" : plan.kind === "exploration" ? "library exploration" : "component request";
10379
+ const next = r.status === "dry-run" ? `Run it without dryRun to open the ${what}.` : plan.kind === "exploration" ? `Build it on ${r.link?.branch}: ${plan.brief} Then bridge_use_library_preview { pr: ${n}, piece: "${a.piece}" } so the product branch shows it.` : plan.kind === "request" ? "A person or Claude builds the component from the request (bridge_get_request). The product branch keeps its own version until it ships." : r.preview?.status === "conflict" ? r.preview.said : "Design and tech owners review the token change. Once it ships, bridge_switch_to_published moves the product branch to the real version.";
10380
+ return {
10381
+ status: r.status,
10382
+ pr: n,
10383
+ piece: a.piece,
10384
+ opens: what,
10385
+ title: plan.title,
10386
+ ...plan.kind === "token-change" ? { edits: plan.edits } : {},
10387
+ ...plan.kind === "exploration" ? { exploration: plan.exploration.name, brief: plan.brief } : {},
10388
+ ...plan.kind === "request" ? { request: plan.request.title } : {},
10389
+ neededBy: `${plan.neededBy.title} (#${plan.neededBy.pr}) in ${plan.neededBy.productName}`,
10390
+ ...r.link ? { library: { kind: r.link.kind, number: r.link.number, url: r.link.url, ...r.link.branch ? { branch: r.link.branch } : {} } } : {},
10391
+ ...r.preview ? { preview: r.preview } : {},
10392
+ ...r.warnings.length ? { warnings: r.warnings } : {},
10393
+ next
10394
+ };
10395
+ }
10396
+ async function productBranchOp(ctx, a) {
10397
+ const n = branchNumber(a.pr);
10398
+ const gh = await reader(ctx);
10399
+ const { config } = await ctx.config();
10400
+ const viewer = await viewerOf(ctx, gh);
10401
+ const v = await productBranchViewOnGitHub(gh, n, { products: productsOf(config), viewer });
10402
+ return {
10403
+ pr: n,
10404
+ title: v.pull.title,
10405
+ product: v.product.name,
10406
+ said: `${v.sentence.lead}${v.sentence.strong}${v.sentence.rest}`,
10407
+ checked: v.record ? v.record.checkedAt : null,
10408
+ linked: v.linked.map((l) => ({
10409
+ piece: l.piece?.title ?? l.link.piece,
10410
+ library: { kind: l.link.kind, number: l.link.number, url: l.link.url },
10411
+ status: l.status.line,
10412
+ preview: l.preview.sentence,
10413
+ ...l.preview.action ? { action: l.preview.action.label, tool: l.preview.action.kind === "switch" ? `bridge_switch_to_published { pr: ${n} }` : `bridge_use_library_preview { pr: ${n}, piece: "${l.link.piece}" }` } : {}
10414
+ })),
10415
+ notLinked: v.open.map(short),
10416
+ ...v.record ? {} : { next: `Find its missing pieces: bridge_missing_pieces { pr: ${n}, record: true }.` }
10417
+ };
10418
+ }
10419
+ async function useLibraryPreviewOp(ctx, a) {
10420
+ const n = branchNumber(a.pr);
10421
+ const gh = await writer(ctx, "Using a library preview", "Contents: read and write");
10422
+ const { config } = await ctx.config();
10423
+ const v = await productBranchViewOnGitHub(gh, n, { products: productsOf(config) });
10424
+ const l = v.linked.find((x) => x.link.piece === a.piece);
10425
+ if (!l) throw new Error(`No library branch for "${a.piece}" on #${n}. Open one first: bridge_open_library_branch.`);
10426
+ if (l.link.kind === "request" || !l.link.branch) throw new Error(`"${l.link.title}" is a request: there is nothing to preview until it is built.`);
10427
+ if (l.status.key === "shipped") throw new Error("The library already shipped it: use bridge_switch_to_published instead.");
10428
+ if (l.status.key === "closed") throw new Error("The library branch is closed: there is nothing to preview.");
10429
+ const r = await useLibraryPreviewOnGitHub(gh, { productBranch: v.pull.headRef, libraryBranch: l.link.branch, libraryNumber: l.link.number });
10430
+ return { pr: n, piece: a.piece, library: l.link.number, ...r };
10431
+ }
10432
+ async function switchToPublishedOp(ctx, a) {
10433
+ const n = branchNumber(a.pr);
10434
+ const gh = await writer(ctx, "Switching to the published version", "Contents and Pull requests: read and write");
10435
+ return { pr: n, ...await switchToPublishedOnGitHub(gh, n) };
10436
+ }
10437
+
9635
10438
  // src/server.ts
9636
- var VERSION = true ? "0.4.1" : "dev";
10439
+ var VERSION = true ? "0.5.0" : "dev";
9637
10440
  var ok = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
9638
10441
  var fail = (e) => ({ content: [{ type: "text", text: `Error: ${e.message ?? String(e)}` }], isError: true });
9639
10442
  var run = (fn) => async (a) => {
@@ -9673,6 +10476,7 @@ function createServer(ctx) {
9673
10476
  `Change texts from Figma and docs from Bridge Studio: when the user says "${DRAFT_PHRASE}" (or asks you to draft their Bridge changes or docs), call bridge_pending_drafts. For change items, look at the before and after images and write each title, summary and change line in the Bridge voice. For docs items (scope "docs"), follow the brief: write only the sections it asks for, grounded in the props, CSS module classes and source it quotes, never an invented prop or behavior, never anatomy. Then call bridge_write_drafts. The designer reviews them in the DS Bridge Figma plugin or on the Library page.`,
9674
10477
  "Studio parity: a designer can do in Claude what they do in Bridge Studio. Branches in review: bridge_what_needs_me (the queue), bridge_branch_status, bridge_send_for_review, bridge_back_to_draft, bridge_review_branch, bridge_ask_someone_else, bridge_comment, bridge_update_from_main (token conflicts too), bridge_try_branch (try it in a product, share it), bridge_combine_branches, bridge_close_branch, bridge_reopen_branch, bridge_merge_branch (same gate as Studio, owner override with a reason). Docs: bridge_save_docs. Writes act under the person's GitHub token: only do them when the person asked, and merge only when they explicitly asked to merge.",
9675
10478
  '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
+ "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.",
9676
10480
  'Explorations try experimental variables and components in a product staging branch without touching main: an explore/<slug> branch with a draft pull request labeled exploration, previews and a snapshot package. bridge_start_exploration starts one, bridge_list_explorations lists them. To work on one, pass "branch": "explore/<slug>" to bridge_propose_token_change, bridge_draft_docs and bridge_get_request, and never open a pull request to main from it: a person promotes it to review or closes it.'
9677
10481
  ].join(" ")
9678
10482
  }
@@ -10170,6 +10974,57 @@ function createServer(ctx) {
10170
10974
  const Pr = z.union([z.number().int().positive(), z.string().min(1)]).describe('The branch number Studio shows (12 or "#12"), or its GitHub link.');
10171
10975
  const WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
10172
10976
  const READ = { readOnlyHint: true, openWorldHint: true };
10977
+ const Piece = z.string().min(1).describe('The piece id bridge_missing_pieces returned ("badge-overdue-tone").');
10978
+ server.registerTool(
10979
+ "bridge_missing_pieces",
10980
+ {
10981
+ title: "Find what a product branch adds outside the library",
10982
+ description: 'Check a product branch (an exploration started for a product) against the library: the adoption audit on each product file it changes, at its start and at its head, keeps what the branch added. New hardcoded values become token pieces beside the closest token, values written on a library component become a variant of it ("Badge: overdue tone"), components from another UI library with no library equivalent become new components. Values equal to a token and components the library has are fixes, not pieces. With record: true, saves the result in the branch description, where Bridge Studio shows it (needs GITHUB_TOKEN, pull requests: write). Run it when a designer finishes exploring with you, then offer to open a library branch for each piece.',
10983
+ inputSchema: { pr: Pr, record: z.boolean().optional().describe("Save the pieces in the product branch description (Studio reads them there). Keeps the library branches already opened.") },
10984
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
10985
+ },
10986
+ run((a) => missingPiecesOp(ctx, a))
10987
+ );
10988
+ server.registerTool(
10989
+ "bridge_open_library_branch",
10990
+ {
10991
+ title: "Open a library branch for a missing piece",
10992
+ description: 'Turn one recorded missing piece into a library branch linked to the product branch, as Bridge Studio does: a token piece opens a token change (same gate and body as bridge_propose_token_change, a draft for design and tech review) and the product branch takes its preview; a variant opens a library exploration with the brief to build it (build it there, then bridge_use_library_preview); a new component opens a component request. Both sides link: the library side says "Needed by" the product branch (label needed-by-product), the product branch records it. Needs GITHUB_TOKEN (contents, pull requests and issues: write). dryRun shows what it would open. Only when the person asked.',
10993
+ inputSchema: { pr: Pr, piece: Piece, usePreview: z.boolean().optional().describe("Merge the new library branch into the product branch when it already changes something (default true)."), dryRun: z.boolean().optional() },
10994
+ annotations: WRITE
10995
+ },
10996
+ run((a) => openLibraryBranchOp(ctx, { ...a, source: "mcp" }))
10997
+ );
10998
+ server.registerTool(
10999
+ "bridge_product_branch",
11000
+ {
11001
+ title: "Product branch and its library branches",
11002
+ description: "A product branch as Studio shows it: the summary sentence, each linked library branch with its status and reviewers, what its preview means for the product branch and the next action, and the pieces with no library branch yet.",
11003
+ inputSchema: { pr: Pr },
11004
+ annotations: READ
11005
+ },
11006
+ run((a) => productBranchOp(ctx, a))
11007
+ );
11008
+ server.registerTool(
11009
+ "bridge_use_library_preview",
11010
+ {
11011
+ title: "Use a library branch preview in a product branch",
11012
+ description: "The product branch takes the latest commits of the library branch of a piece (Bridge merges it in), so the product preview shows the real library change before it ships. A conflict is left to a developer. Needs GITHUB_TOKEN (contents: write). Only when the person asked.",
11013
+ inputSchema: { pr: Pr, piece: Piece },
11014
+ annotations: WRITE
11015
+ },
11016
+ run((a) => useLibraryPreviewOp(ctx, a))
11017
+ );
11018
+ server.registerTool(
11019
+ "bridge_switch_to_published",
11020
+ {
11021
+ title: "Switch a product branch to the published version",
11022
+ description: "Once its library branches shipped, bring main into the product branch (GitHub update branch): it stops carrying the library change and uses the shipped one. Token conflicts go through bridge_update_from_main. Needs GITHUB_TOKEN (contents and pull requests: write). Only when the person asked.",
11023
+ inputSchema: { pr: Pr },
11024
+ annotations: WRITE
11025
+ },
11026
+ run((a) => switchToPublishedOp(ctx, a))
11027
+ );
10173
11028
  server.registerTool(
10174
11029
  "bridge_list_products",
10175
11030
  {
@@ -10406,8 +11261,8 @@ function createServer(ctx) {
10406
11261
  new ResourceTemplate("bridge://components/{name}", {
10407
11262
  list: async () => {
10408
11263
  const { config } = await ctx.config();
10409
- const names2 = await listComponentNames(ctx.source, config);
10410
- return { resources: names2.map((n) => ({ uri: `bridge://components/${n.name}`, name: n.name, mimeType: "application/json" })) };
11264
+ const names3 = await listComponentNames(ctx.source, config);
11265
+ return { resources: names3.map((n) => ({ uri: `bridge://components/${n.name}`, name: n.name, mimeType: "application/json" })) };
10411
11266
  },
10412
11267
  complete: {
10413
11268
  name: async (value) => {
@@ -10595,6 +11450,7 @@ export {
10595
11450
  requests_exports,
10596
11451
  drafts_exports,
10597
11452
  studio_exports,
11453
+ pieces_exports,
10598
11454
  VERSION,
10599
11455
  createServer,
10600
11456
  serveStdio,