@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.
- package/dist/branches.d.ts +8 -0
- package/dist/{chunk-MXMMPO7F.js → chunk-MHCXMHCL.js} +952 -96
- package/dist/explorations.d.ts +6 -1
- package/dist/index.js +1 -1
- package/dist/lib.d.ts +1 -0
- package/dist/lib.js +3 -1
- package/dist/ops.d.ts +3 -0
- package/dist/pieces.d.ts +122 -0
- package/package.json +3 -3
|
@@ -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
|
|
2088
|
-
const s =
|
|
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
|
|
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) =>
|
|
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)
|
|
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
|
|
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
|
-
|
|
2425
|
-
for (const k of conflicts)
|
|
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)
|
|
2428
|
-
return finishPlan({ ...input, kind: "revert", title, prTitle: title, summary:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3631
|
+
lines2.push("", "### Previews", "");
|
|
3621
3632
|
const links = previewLinks(ctx.apps, { branch, ...ctx.pr ? { pr: ctx.pr } : {} });
|
|
3622
|
-
if (links.length)
|
|
3623
|
-
else
|
|
3624
|
-
|
|
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
|
|
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
|
|
3959
|
-
if (!clean.title && !clean.summary && !
|
|
3960
|
-
next.set(r.id, { id: r.id, title: clean.title, summary: clean.summary, ...
|
|
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
|
|
3979
|
-
if (i.component)
|
|
3980
|
-
if (kind)
|
|
3981
|
-
if (i.components?.length)
|
|
3982
|
-
if (i.changes.length)
|
|
3983
|
-
if (i.variables?.length)
|
|
3984
|
-
if (!i.changes.length && !i.variables?.length && !i.lines?.length)
|
|
3985
|
-
if (i.draft.title || i.draft.summary)
|
|
3986
|
-
if (i.notes?.trim())
|
|
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
|
-
|
|
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
|
-
|
|
3991
|
-
for (const c of l.changes)
|
|
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)
|
|
3995
|
-
return
|
|
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
|
|
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}${
|
|
4399
|
+
return `${title}${lines2.length ? `
|
|
4389
4400
|
|
|
4390
|
-
${
|
|
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
|
|
4580
|
-
if (paths.length > 20)
|
|
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
|
-
${
|
|
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
|
|
4691
|
-
const list =
|
|
4692
|
-
const title = opts.title?.trim() || `docs(${
|
|
4693
|
-
const slug2 = docsSlug(
|
|
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
|
|
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(
|
|
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
|
|
5748
|
-
findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...
|
|
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
|
|
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) =>
|
|
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
|
|
7185
|
-
if (!withSummaries) return
|
|
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
|
-
|
|
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
|
|
7527
|
-
|
|
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
|
|
8275
|
+
const lines2 = md.split("\n");
|
|
7596
8276
|
const heads = [];
|
|
7597
8277
|
let fence = false;
|
|
7598
|
-
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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
|
|
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(
|
|
8452
|
-
return { ...res, notes, newErrors: errors.filter((i) => !prev.has(
|
|
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
|
|
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
|
|
8618
|
-
|
|
8619
|
-
const
|
|
8620
|
-
const
|
|
8621
|
-
...touched.map((f) => ({ path: prefixPath(after, f), content: after.files.get(f) })),
|
|
8622
|
-
...config.changesets ? [{ path: changesetPath, content: core.changeset(config.changesets.package, tier.bump, summary2) }] : []
|
|
8623
|
-
];
|
|
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
|
-
|
|
8677
|
-
|
|
8678
|
-
const
|
|
8679
|
-
const pr =
|
|
8680
|
-
|
|
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(
|
|
8692
|
-
const newErrors = core.validate(after).filter((i) => i.level === "error" && !prevErrors.has(
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
|
10410
|
-
return { resources:
|
|
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,
|