@bridge-ds/mcp 0.4.0 → 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 +10 -5
- package/dist/{chunk-NYFH322K.js → chunk-MHCXMHCL.js} +1026 -114
- package/dist/{chunk-7DBLDJIL.js → chunk-QTO4EP6V.js} +5 -1
- package/dist/explorations.d.ts +6 -1
- package/dist/index.js +2 -2
- package/dist/lib.d.ts +1 -0
- package/dist/lib.js +4 -2
- package/dist/ops.d.ts +3 -0
- package/dist/pieces.d.ts +122 -0
- package/dist/{src-2YRRAUUM.js → src-DHJOM5PQ.js} +1 -1
- package/dist/studio.d.ts +12 -6
- package/package.json +3 -3
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
previewUrl,
|
|
53
53
|
previewsSettled,
|
|
54
54
|
productForPlace,
|
|
55
|
-
|
|
55
|
+
productMarks,
|
|
56
56
|
productOfBranch,
|
|
57
57
|
productsOf,
|
|
58
58
|
protectedBranchesOf,
|
|
@@ -72,7 +72,7 @@ import {
|
|
|
72
72
|
unreadableTeams,
|
|
73
73
|
validateLayered,
|
|
74
74
|
whoCanShipSentence
|
|
75
|
-
} from "./chunk-
|
|
75
|
+
} from "./chunk-QTO4EP6V.js";
|
|
76
76
|
|
|
77
77
|
// src/lib.ts
|
|
78
78
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -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) {
|
|
@@ -4084,7 +4095,7 @@ Exploration: never merged as is. Promote it to review or close it.`,
|
|
|
4084
4095
|
const ctx = { owner: gh.owner, repo: gh.name, packages: s.packages, apps: s.apps, ...s.requests?.length ? { requests: s.requests } : {} };
|
|
4085
4096
|
const title = full.jira ? `${full.jira.trim().toUpperCase()} ${explorationPrTitle(full)}` : explorationPrTitle(full);
|
|
4086
4097
|
const pr = await x.createPullRequest({ title, head: branch, base: s.base, body: explorationPrBody(full, ctx), draft: true });
|
|
4087
|
-
await x.addLabels(pr.number, [EXPLORATION_LABEL]);
|
|
4098
|
+
await x.addLabels(pr.number, [EXPLORATION_LABEL, ...s.labels ?? []]);
|
|
4088
4099
|
await x.updatePullRequest(pr.number, { body: explorationPrBody(full, { ...ctx, pr: pr.number }) }).catch(() => void 0);
|
|
4089
4100
|
return {
|
|
4090
4101
|
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
|
|
@@ -4606,7 +4617,7 @@ function branchWithJiraKey(branch, key) {
|
|
|
4606
4617
|
// ../core/src/docs-proposal.ts
|
|
4607
4618
|
var DOCS_EDITOR_ORIGIN = {
|
|
4608
4619
|
where: "in the Bridge docs editor",
|
|
4609
|
-
footer:
|
|
4620
|
+
footer: "Written by a person in the Bridge docs editor. No AI involved in this text.",
|
|
4610
4621
|
label: "source: bridge-web"
|
|
4611
4622
|
};
|
|
4612
4623
|
var DOCS_MCP_ORIGIN = {
|
|
@@ -4642,13 +4653,55 @@ function cleanDocs(d) {
|
|
|
4642
4653
|
}
|
|
4643
4654
|
var docsFileText = (d, original) => patchDocsJson(original, cleanDocs(d));
|
|
4644
4655
|
var slugDate = (d) => d.toISOString().slice(0, 10).replace(/-/g, "");
|
|
4656
|
+
var docsSlug = (nameOrSlug) => nameOrSlug.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9-]+/g, "-").slice(0, 40);
|
|
4657
|
+
function defaultDocsFooter(edits, origin) {
|
|
4658
|
+
if (origin.label !== DOCS_EDITOR_ORIGIN.label) return origin.footer;
|
|
4659
|
+
const claude = edits.some((e) => {
|
|
4660
|
+
try {
|
|
4661
|
+
return JSON.parse(e.after).draftedBy === "claude";
|
|
4662
|
+
} catch {
|
|
4663
|
+
return false;
|
|
4664
|
+
}
|
|
4665
|
+
});
|
|
4666
|
+
return claude ? `Drafted by Claude AI (the docs say "draftedBy": "claude"), sent by a person ${origin.where}.` : origin.footer;
|
|
4667
|
+
}
|
|
4668
|
+
function withAuthorLine(body, line) {
|
|
4669
|
+
const i = body.lastIndexOf("\n---\n");
|
|
4670
|
+
return i < 0 ? `${body.trimEnd()}
|
|
4671
|
+
|
|
4672
|
+
---
|
|
4673
|
+
${line}` : `${body.slice(0, i + 5)}${line}`;
|
|
4674
|
+
}
|
|
4675
|
+
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4676
|
+
function isDocsProposalBranch(branch, component) {
|
|
4677
|
+
const slugs = /* @__PURE__ */ new Set([docsSlug(component.slug ?? component.name), docsSlug(component.name), docsSlug(component.name.toLowerCase())]);
|
|
4678
|
+
return [...slugs].some((s) => new RegExp(`^docs/(?:[A-Z][A-Z0-9]*-\\d+-)?${escapeRe(s)}-\\d{8}-[a-z0-9]+$`).test(branch));
|
|
4679
|
+
}
|
|
4680
|
+
function findOpenDocsProposal(pulls, component, base) {
|
|
4681
|
+
const hit = pulls.filter((p) => p.state === "open" && isDocsProposalBranch(p.headRef, component) && (!base || !p.baseRef || p.baseRef === base)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
|
|
4682
|
+
return hit ? { number: hit.number, url: hit.url, branch: hit.headRef, author: hit.author, title: hit.title, updatedAt: hit.updatedAt } : null;
|
|
4683
|
+
}
|
|
4684
|
+
async function findOpenDocsProposalOnGitHub(gh, component, base) {
|
|
4685
|
+
return findOpenDocsProposal(await gh.listPullRequests("open"), component, base);
|
|
4686
|
+
}
|
|
4687
|
+
async function continueDocsProposalOnGitHub(gh, open, edit, opts = {}) {
|
|
4688
|
+
const message = opts.message ?? `docs(${docsSlug(edit.component)}): edit usage guidance in the Bridge docs editor`;
|
|
4689
|
+
const cur = await gh.getFile(edit.path, open.branch);
|
|
4690
|
+
if (cur?.text === edit.after) return "";
|
|
4691
|
+
const sha = await gh.putFile({ path: edit.path, content: edit.after, branch: open.branch, message, sha: cur?.sha });
|
|
4692
|
+
if (opts.authorLine) {
|
|
4693
|
+
const line = opts.authorLine;
|
|
4694
|
+
await gh.getPullRequest(open.number).then((pr) => gh.updatePullRequest(open.number, { body: withAuthorLine(pr.body, line) })).catch(() => void 0);
|
|
4695
|
+
}
|
|
4696
|
+
return sha;
|
|
4697
|
+
}
|
|
4645
4698
|
function docsProposal(edits, opts) {
|
|
4646
4699
|
const origin = opts.origin ?? DOCS_EDITOR_ORIGIN;
|
|
4647
4700
|
if (!edits.length) throw new Error("No docs edits in the draft.");
|
|
4648
|
-
const
|
|
4649
|
-
const list =
|
|
4650
|
-
const title = opts.title?.trim() || `docs(${
|
|
4651
|
-
const slug2 =
|
|
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`);
|
|
4652
4705
|
const branch = `docs/${slug2}-${slugDate(opts.date)}-${opts.date.getTime().toString(36).slice(-4)}`;
|
|
4653
4706
|
const csPath = `${opts.changesetDir ?? ".changeset"}/docs-${slug2}-${opts.date.getTime().toString(36)}.md`;
|
|
4654
4707
|
const pkg = opts.packageName ?? "@bridge-ds/react";
|
|
@@ -4685,12 +4738,20 @@ Usage guidance for ${list}.
|
|
|
4685
4738
|
${opts.notes.trim()}
|
|
4686
4739
|
` : "",
|
|
4687
4740
|
"---",
|
|
4688
|
-
origin
|
|
4741
|
+
opts.authorLine?.trim() || defaultDocsFooter(edits, origin)
|
|
4689
4742
|
].filter((x, i, a) => !(x === "" && a[i - 1] === "")).join("\n");
|
|
4690
4743
|
return { branch, title, body, files, risk, bump: "patch", labels: ["docs", origin.label, `tier-${risk.tier}`] };
|
|
4691
4744
|
}
|
|
4692
4745
|
async function proposeDocsOnGitHub(gh, edits, opts) {
|
|
4693
|
-
|
|
4746
|
+
if (opts.continueOpen !== false && edits.length === 1) {
|
|
4747
|
+
const e = edits[0];
|
|
4748
|
+
const open = await findOpenDocsProposalOnGitHub(gh, { name: e.component, slug: e.slug }, opts.base).catch(() => null);
|
|
4749
|
+
if (open) {
|
|
4750
|
+
const sha = await continueDocsProposalOnGitHub(gh, open, e, { authorLine: opts.authorLine?.trim() || defaultDocsFooter(edits, opts.origin ?? DOCS_EDITOR_ORIGIN) });
|
|
4751
|
+
return { number: open.number, url: open.url, branch: open.branch, title: open.title, continued: true, unchanged: !sha };
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
const p = docsProposal(edits, { title: opts.title, notes: opts.notes, date: opts.date ?? /* @__PURE__ */ new Date(), origin: opts.origin, authorLine: opts.authorLine });
|
|
4694
4755
|
const prTitle = titleWithJiraKey(p.title, opts.jira ?? null);
|
|
4695
4756
|
const branch = branchWithJiraKey(p.branch, opts.jira ?? null);
|
|
4696
4757
|
const mainSha = await gh.getBranchSha(opts.base);
|
|
@@ -4702,7 +4763,7 @@ ${opts.commitNote ?? "Proposed with the Bridge docs editor."}`, tree, [mainSha])
|
|
|
4702
4763
|
await gh.createBranch(branch, commit);
|
|
4703
4764
|
const pr = await gh.createPullRequest({ title: prTitle, head: branch, base: opts.base, body: p.body, draft: false });
|
|
4704
4765
|
await gh.addLabels(pr.number, [...p.labels, ...opts.extraLabels ?? []]).catch(() => false);
|
|
4705
|
-
return { number: pr.number, url: pr.url, branch, title: prTitle };
|
|
4766
|
+
return { number: pr.number, url: pr.url, branch, title: prTitle, continued: false, unchanged: false };
|
|
4706
4767
|
}
|
|
4707
4768
|
async function commitFileOnGitHub(gh, input) {
|
|
4708
4769
|
const cur = await gh.getFile(input.path, input.branch);
|
|
@@ -4823,9 +4884,9 @@ var COMBINE_COMPARE_LIMIT = 300;
|
|
|
4823
4884
|
async function readCombineBranch(gh, pull, opts) {
|
|
4824
4885
|
const compare = await gh.compareDetailed(opts.base, pull.headSha);
|
|
4825
4886
|
const files = compare.files;
|
|
4826
|
-
const
|
|
4887
|
+
const names3 = files.flatMap((f) => f.previous_filename ? [f.filename, f.previous_filename] : [f.filename]);
|
|
4827
4888
|
const start = compare.mergeBase;
|
|
4828
|
-
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 };
|
|
4829
4890
|
const root = opts.tokensRoot.replace(/^\/+|\/+$/g, "");
|
|
4830
4891
|
const tokenFiles = files.filter((f) => tokenFilesOf([f.filename], root).length);
|
|
4831
4892
|
const source = { number: pull.number, base: {}, head: {}, files: tokenFiles.map((f) => tokenFilesOf([f.filename], root)[0]) };
|
|
@@ -4839,6 +4900,433 @@ async function readCombineBranch(gh, pull, opts) {
|
|
|
4839
4900
|
return { number: pull.number, title: pull.title, tokens: true, edits: changeEdits(source) };
|
|
4840
4901
|
}
|
|
4841
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
|
+
|
|
4842
5330
|
// ../core/src/node/git-history.ts
|
|
4843
5331
|
var US = "";
|
|
4844
5332
|
var RS = "";
|
|
@@ -5694,8 +6182,8 @@ function tailwindFindings(hits, offset, pos, ds) {
|
|
|
5694
6182
|
else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
|
|
5695
6183
|
if (cat === "color" && !isColor) continue;
|
|
5696
6184
|
const suggestion = ds.suggest(cat, v);
|
|
5697
|
-
const
|
|
5698
|
-
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 } : {} });
|
|
5699
6187
|
}
|
|
5700
6188
|
return { findings, themeRefs };
|
|
5701
6189
|
}
|
|
@@ -6703,9 +7191,9 @@ var DesignSystemIndex = class {
|
|
|
6703
7191
|
return generic ? this.make(generic, 1, 0.3) : void 0;
|
|
6704
7192
|
}
|
|
6705
7193
|
case "shadow": {
|
|
6706
|
-
const
|
|
7194
|
+
const norm3 = (s) => s.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").trim().toLowerCase();
|
|
6707
7195
|
const cands = this.entries(["shadow"]);
|
|
6708
|
-
const exact = cands.find((e) =>
|
|
7196
|
+
const exact = cands.find((e) => norm3(e.css) === norm3(value));
|
|
6709
7197
|
if (exact) return this.make(exact, 0, 1);
|
|
6710
7198
|
const t = shadowNums(value);
|
|
6711
7199
|
const scored = cands.map((e) => {
|
|
@@ -7131,11 +7619,11 @@ async function listComponentNames(source, config) {
|
|
|
7131
7619
|
}
|
|
7132
7620
|
var findFamily = (stories) => stories ? /title:\s*['"]([^'"/]+)\//.exec(stories)?.[1] : void 0;
|
|
7133
7621
|
async function listComponents(source, config, withSummaries) {
|
|
7134
|
-
const
|
|
7135
|
-
if (!withSummaries) return
|
|
7622
|
+
const names3 = await listComponentNames(source, config);
|
|
7623
|
+
if (!withSummaries) return names3.map((n) => ({ name: n.name }));
|
|
7136
7624
|
const dir = config.components.dir;
|
|
7137
7625
|
return Promise.all(
|
|
7138
|
-
|
|
7626
|
+
names3.map(async ({ name, files }) => {
|
|
7139
7627
|
const docsPath = `${dir}/${name}/${name}${config.components.docsSuffix}`;
|
|
7140
7628
|
const storiesPath = files.find((f) => /\.stories\.\w+$/.test(f));
|
|
7141
7629
|
const [docs, stories] = await Promise.all([files.includes(docsPath) ? source.read(docsPath) : null, storiesPath ? source.read(storiesPath) : null]);
|
|
@@ -7430,6 +7918,7 @@ __export(ops_exports, {
|
|
|
7430
7918
|
auditOp: () => auditOp,
|
|
7431
7919
|
auditUi: () => auditUi,
|
|
7432
7920
|
checkContrast: () => checkContrast,
|
|
7921
|
+
designSystemIndex: () => designSystemIndex,
|
|
7433
7922
|
draftDocsOp: () => draftDocsOp,
|
|
7434
7923
|
getComponentOp: () => getComponentOp,
|
|
7435
7924
|
getConfig: () => getConfig,
|
|
@@ -7453,6 +7942,247 @@ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:f
|
|
|
7453
7942
|
import { mkdir as mkdir2, writeFile as writeFile2, access } from "node:fs/promises";
|
|
7454
7943
|
import { dirname as dirname3, isAbsolute, join as join4, resolve as resolve3 } from "node:path";
|
|
7455
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
|
+
|
|
7456
8186
|
// src/audit.ts
|
|
7457
8187
|
var PROPERTY_HINTS = [
|
|
7458
8188
|
[/^(color|fill|stroke)$/, /(^|\.)(text|fg|icon|foreground|link)(\.|-|$)/],
|
|
@@ -7473,8 +8203,8 @@ function auditSnippet(set, code2, max = 3) {
|
|
|
7473
8203
|
});
|
|
7474
8204
|
const rank2 = (tier) => tier === "semantic" ? 0 : tier === "component" ? 1 : tier === "density" ? 2 : tier === "brand" ? 3 : 4;
|
|
7475
8205
|
const findings = [];
|
|
7476
|
-
const
|
|
7477
|
-
|
|
8206
|
+
const lines2 = code2.split("\n");
|
|
8207
|
+
lines2.forEach((line, i) => {
|
|
7478
8208
|
if (/var\(--/.test(line) && !/#[0-9a-f]{3,8}\b|\d(px|rem|ms)\b/i.test(line.replace(/var\([^)]*\)/g, ""))) return;
|
|
7479
8209
|
const stripped = line.replace(/var\([^)]*\)/g, "");
|
|
7480
8210
|
const propAt = (idx) => /([a-zA-Z-]+)['"]?\s*:[^;:,{]*$/.exec(stripped.slice(0, idx))?.[1]?.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`).toLowerCase();
|
|
@@ -7542,17 +8272,17 @@ function auditSnippet(set, code2, max = 3) {
|
|
|
7542
8272
|
// src/markdown.ts
|
|
7543
8273
|
var slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7544
8274
|
function sections(md) {
|
|
7545
|
-
const
|
|
8275
|
+
const lines2 = md.split("\n");
|
|
7546
8276
|
const heads = [];
|
|
7547
8277
|
let fence = false;
|
|
7548
|
-
|
|
8278
|
+
lines2.forEach((l, i) => {
|
|
7549
8279
|
if (/^\s*(```|~~~)/.test(l)) fence = !fence;
|
|
7550
8280
|
const m = !fence && /^(#{1,4})\s+(.+?)\s*#*\s*$/.exec(l);
|
|
7551
8281
|
if (m) heads.push({ level: m[1].length, title: m[2], line: i });
|
|
7552
8282
|
});
|
|
7553
8283
|
return heads.map((h, i) => {
|
|
7554
8284
|
const next = heads.slice(i + 1).find((x) => x.level <= h.level);
|
|
7555
|
-
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() };
|
|
7556
8286
|
});
|
|
7557
8287
|
}
|
|
7558
8288
|
function toc(md) {
|
|
@@ -7584,6 +8314,7 @@ __export(explorations_exports, {
|
|
|
7584
8314
|
installOf: () => installOf,
|
|
7585
8315
|
listExplorations: () => listExplorations,
|
|
7586
8316
|
promoteExploration: () => promoteExploration,
|
|
8317
|
+
settingsOf: () => settingsOf,
|
|
7587
8318
|
startExploration: () => startExploration,
|
|
7588
8319
|
toExplorationBranch: () => toExplorationBranch,
|
|
7589
8320
|
writeToExploration: () => writeToExploration
|
|
@@ -7608,13 +8339,16 @@ __export(branches_exports, {
|
|
|
7608
8339
|
listProducts: () => listProducts,
|
|
7609
8340
|
mergeBranch: () => mergeBranch,
|
|
7610
8341
|
productMarks: () => productMarks,
|
|
8342
|
+
reader: () => reader,
|
|
7611
8343
|
reopenBranch: () => reopenBranch,
|
|
7612
8344
|
resolveProduct: () => resolveProduct,
|
|
7613
8345
|
reviewBranch: () => reviewBranch,
|
|
7614
8346
|
sendForReview: () => sendForReview,
|
|
7615
8347
|
tryBranch: () => tryBranch,
|
|
7616
8348
|
updateFromMain: () => updateFromMain,
|
|
7617
|
-
|
|
8349
|
+
viewerOf: () => viewerOf,
|
|
8350
|
+
whatNeedsMe: () => whatNeedsMe,
|
|
8351
|
+
writer: () => writer
|
|
7618
8352
|
});
|
|
7619
8353
|
import { relative as relative2 } from "node:path";
|
|
7620
8354
|
var RIGHTS = {
|
|
@@ -7803,10 +8537,6 @@ async function resolveProduct(ctx, name) {
|
|
|
7803
8537
|
}
|
|
7804
8538
|
return (await productHere(ctx, config)).product;
|
|
7805
8539
|
}
|
|
7806
|
-
function productMarks(products, p) {
|
|
7807
|
-
if (p.key === defaultProduct(products).key) return { labels: [], line: "" };
|
|
7808
|
-
return { labels: [productLabel(p.key)], line: `**Product:** ${p.name}` };
|
|
7809
|
-
}
|
|
7810
8540
|
async function tryBranch(ctx, a) {
|
|
7811
8541
|
const n = branchNumber(a.pr);
|
|
7812
8542
|
const gh = await reader(ctx);
|
|
@@ -7930,11 +8660,11 @@ async function commentOnBranch(ctx, a) {
|
|
|
7930
8660
|
async function listComments(ctx, a) {
|
|
7931
8661
|
const n = branchNumber(a.pr);
|
|
7932
8662
|
const gh = await reader(ctx);
|
|
7933
|
-
const [conversation,
|
|
8663
|
+
const [conversation, lines2] = await Promise.all([gh.listIssueComments(n), gh.listReviewComments(n).catch(() => [])]);
|
|
7934
8664
|
return {
|
|
7935
8665
|
pr: n,
|
|
7936
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 })),
|
|
7937
|
-
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 })) })),
|
|
7938
8668
|
howToReply: "bridge_comment with replyTo: the thread id."
|
|
7939
8669
|
};
|
|
7940
8670
|
}
|
|
@@ -8087,8 +8817,7 @@ async function startExploration(ctx, a) {
|
|
|
8087
8817
|
if (a.dryRun) return { status: "dry-run", ...preview };
|
|
8088
8818
|
const work = `Work on it: \`git fetch origin && git switch ${branch}\`. Token edits: bridge_propose_token_change with "branch": "${branch}". Requests: bridge_create_request with "exploration": "${branch}", then bridge_get_request with "branch": "${branch}". It is never merged as is: promote it (\`bridge explore promote ${slug2}\`) or close it.`;
|
|
8089
8819
|
if (gh && ctx.canWriteRemote) {
|
|
8090
|
-
const r = await startExplorationOnGitHub(gh, e, s);
|
|
8091
|
-
if (marks.labels.length) await gh.addLabels(r.pr, marks.labels);
|
|
8820
|
+
const r = await startExplorationOnGitHub(gh, e, { ...s, labels: marks.labels });
|
|
8092
8821
|
return {
|
|
8093
8822
|
status: "started",
|
|
8094
8823
|
...preview,
|
|
@@ -8387,7 +9116,7 @@ async function checkContrast(ctx, a) {
|
|
|
8387
9116
|
const delta2 = core.contrastDelta(beforeRes, afterRes);
|
|
8388
9117
|
return { ...base, edits: a.edits.length, notes, changed: delta2, newFailures: delta2.filter((d) => d.passBefore && !d.passAfter) };
|
|
8389
9118
|
}
|
|
8390
|
-
var
|
|
9119
|
+
var issueKey3 = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
|
|
8391
9120
|
async function validateTokens(ctx, a) {
|
|
8392
9121
|
const { before, after, notes } = await withEdits(ctx, a.edits);
|
|
8393
9122
|
const issues = core.validate(after);
|
|
@@ -8403,8 +9132,8 @@ async function validateTokens(ctx, a) {
|
|
|
8403
9132
|
...a.includeWarnings ? { warnings } : warnings.length ? { warningSample: warnings.slice(0, 5) } : {}
|
|
8404
9133
|
};
|
|
8405
9134
|
if (!a.edits?.length) return res;
|
|
8406
|
-
const prev = new Set(core.validate(before).map(
|
|
8407
|
-
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))) };
|
|
8408
9137
|
}
|
|
8409
9138
|
async function listComponentsOp(ctx, a) {
|
|
8410
9139
|
const { config } = await ctx.config();
|
|
@@ -8557,25 +9286,13 @@ async function proposeTokenChange(ctx, a) {
|
|
|
8557
9286
|
const target = a.branch ? await explorationContext(ctx, a.branch, { requireCheckout: !a.dryRun }) : null;
|
|
8558
9287
|
if (target) ctx = target.ctx;
|
|
8559
9288
|
const { config } = await ctx.config();
|
|
8560
|
-
const
|
|
8561
|
-
const changes = core.diff(before, after);
|
|
8562
|
-
if (!changes.length) return { status: "no-change", message: "The edits leave every token as it was." };
|
|
8563
|
-
const prevErrors = new Set(core.validate(before).map(issueKey2));
|
|
8564
|
-
const newErrors = core.validate(after).filter((i) => i.level === "error" && !prevErrors.has(issueKey2(i)));
|
|
9289
|
+
const before = await ctx.tokens();
|
|
8565
9290
|
const pairs = await core.readContrastPairs(ctx.source, config).catch(() => []);
|
|
8566
|
-
const delta2 = pairs.length ? core.contrastDelta(core.runContrast(before, pairs, config.contrast?.modes), core.runContrast(after, pairs, config.contrast?.modes)) : [];
|
|
8567
|
-
const newFailures = delta2.filter((d) => d.passBefore && !d.passAfter);
|
|
8568
|
-
const imp = core.impact(before, after, changes);
|
|
8569
|
-
const tier = core.classify(changes, [], config);
|
|
8570
|
-
const summary2 = a.summary?.trim() || a.title;
|
|
8571
9291
|
const marks = productMarks(productsOf(config), product);
|
|
8572
|
-
const
|
|
8573
|
-
|
|
8574
|
-
const
|
|
8575
|
-
const
|
|
8576
|
-
...touched.map((f) => ({ path: prefixPath(after, f), content: after.files.get(f) })),
|
|
8577
|
-
...config.changesets ? [{ path: changesetPath, content: core.changeset(config.changesets.package, tier.bump, summary2) }] : []
|
|
8578
|
-
];
|
|
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;
|
|
8579
9296
|
const report = {
|
|
8580
9297
|
title: a.title,
|
|
8581
9298
|
tier: tier.tier,
|
|
@@ -8624,15 +9341,12 @@ Tier ${tier.tier}. Proposed through Bridge MCP (source: mcp).`;
|
|
|
8624
9341
|
}
|
|
8625
9342
|
const gh = await ctx.github();
|
|
8626
9343
|
if (!ctx.canWriteRemote) return { status: "preview", message: "Read-only: set GITHUB_TOKEN to open this as a pull request.", ...preview, fileContents: files };
|
|
8627
|
-
const commitMsg = `feat(tokens): ${a.title}
|
|
8628
|
-
|
|
8629
|
-
Proposed through Bridge MCP (source: mcp). Tier ${tier.tier}.`;
|
|
8630
9344
|
const base = ctx.ref ?? baseBranchOf(config);
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
const
|
|
8634
|
-
const pr =
|
|
8635
|
-
|
|
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;
|
|
8636
9350
|
return { status: "pr-opened", ...preview, branch, commit: commit.sha, pr: pr.number, url: pr.url, labels, draft: tier.tier >= 2, product: product.name };
|
|
8637
9351
|
}
|
|
8638
9352
|
async function addToBranch(ctx, a) {
|
|
@@ -8643,8 +9357,8 @@ async function addToBranch(ctx, a) {
|
|
|
8643
9357
|
const { before, after, touched, notes } = await withEdits(at, a.edits);
|
|
8644
9358
|
const changes = core.diff(before, after);
|
|
8645
9359
|
if (!changes.length) return { status: "no-change", pr: n, message: `#${n} already has these values.` };
|
|
8646
|
-
const prevErrors = new Set(core.validate(before).map(
|
|
8647
|
-
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)));
|
|
8648
9362
|
const pairs = await core.readContrastPairs(at.source, config).catch(() => []);
|
|
8649
9363
|
const delta2 = pairs.length ? core.contrastDelta(core.runContrast(before, pairs, config.contrast?.modes), core.runContrast(after, pairs, config.contrast?.modes)) : [];
|
|
8650
9364
|
const newFailures = delta2.filter((d) => d.passBefore && !d.passAfter);
|
|
@@ -8741,7 +9455,7 @@ async function getReview(ctx, a) {
|
|
|
8741
9455
|
const changes = core.diff(before, after);
|
|
8742
9456
|
const pairs = await core.readContrastPairs(headSrc, config).catch(() => []);
|
|
8743
9457
|
const delta2 = pairs.length ? core.contrastDelta(core.runContrast(before, pairs, config.contrast?.modes), core.runContrast(after, pairs, config.contrast?.modes)) : [];
|
|
8744
|
-
const prevErrors = new Set(core.validate(before).map(
|
|
9458
|
+
const prevErrors = new Set(core.validate(before).map(issueKey3));
|
|
8745
9459
|
const errors = core.validate(after).filter((i) => i.level === "error");
|
|
8746
9460
|
const tokenRoot = config.tokens.root.replace(/\/$/, "");
|
|
8747
9461
|
const otherFiles = changedFiles.filter((f) => !f.startsWith(`${tokenRoot}/`) && !f.startsWith(`${config.changesets?.dir ?? ".changeset"}/`));
|
|
@@ -8759,7 +9473,7 @@ async function getReview(ctx, a) {
|
|
|
8759
9473
|
diffTable: core.tokenTable(changes.map((c) => ({ ...c, file: prefixPath(after, c.file) }))),
|
|
8760
9474
|
contrastTable: core.contrastTable(delta2),
|
|
8761
9475
|
contrastNewFailures: delta2.filter((d) => d.passBefore && !d.passAfter).length,
|
|
8762
|
-
validation: { errors: errors.length, newErrors: errors.filter((i) => !prevErrors.has(
|
|
9476
|
+
validation: { errors: errors.length, newErrors: errors.filter((i) => !prevErrors.has(issueKey3(i))) },
|
|
8763
9477
|
impact: imp
|
|
8764
9478
|
};
|
|
8765
9479
|
}
|
|
@@ -8819,7 +9533,7 @@ async function visualImpact(ctx, a, run2) {
|
|
|
8819
9533
|
if (ctx.mode !== "local" || !ctx.root) throw new Error("Visual impact needs a local checkout (start Bridge with --root): it builds Storybook and the app at both refs.");
|
|
8820
9534
|
if (!ctx.isGit) throw new Error(`${ctx.root} is not a git repository.`);
|
|
8821
9535
|
const out = a.out ? resolve3(ctx.root, a.out) : join4(ctx.root, "visual-impact");
|
|
8822
|
-
const runImpact = run2 ?? (await import("./src-
|
|
9536
|
+
const runImpact = run2 ?? (await import("./src-DHJOM5PQ.js")).runImpact;
|
|
8823
9537
|
const r = await runImpact({
|
|
8824
9538
|
root: ctx.root,
|
|
8825
9539
|
base: a.base ?? "main",
|
|
@@ -9486,8 +10200,10 @@ async function saveDocs(ctx, a) {
|
|
|
9486
10200
|
return docsFileText(cleanDocs(merged), current);
|
|
9487
10201
|
};
|
|
9488
10202
|
const gh = await ctx.github();
|
|
9489
|
-
|
|
9490
|
-
|
|
10203
|
+
const open = a.pr === void 0 && gh ? await findOpenDocsProposalOnGitHub(gh, { name, slug: docsSlug(name) }, ctx.ref ?? baseBranchOf(config)).catch(() => null) : null;
|
|
10204
|
+
const target = a.pr ?? open?.number;
|
|
10205
|
+
if (target !== void 0) {
|
|
10206
|
+
const n = branchNumber(target);
|
|
9491
10207
|
if (!gh) throw new Error('No GitHub repo known: set "repo" in bridge.config.json or add an origin remote.');
|
|
9492
10208
|
const pr = await gh.getPullRequest(n);
|
|
9493
10209
|
if (pr.state !== "open") throw new Error(`#${n} is ${pr.state}: save the docs on a new branch instead (leave pr out).`);
|
|
@@ -9495,14 +10211,23 @@ async function saveDocs(ctx, a) {
|
|
|
9495
10211
|
if (current === null) throw new Error(`${path} is not on #${n}. Save the docs on a new branch instead (leave pr out), or check the component name.`);
|
|
9496
10212
|
const text2 = next(current);
|
|
9497
10213
|
if (a.dryRun) return { status: "dry-run", pr: n, path, content: text2 };
|
|
10214
|
+
if (!ctx.canWriteRemote && open) return { status: "preview", pr: n, path, content: text2, said: `Nothing sent: the ${name} docs have an open proposal (#${n}) and adding to it needs a GitHub token with ${RIGHTS.docs}. Set GITHUB_TOKEN.` };
|
|
9498
10215
|
if (!ctx.canWriteRemote) throw new Error(`Saving docs on #${n} needs a GitHub token with ${RIGHTS.docs}. Set GITHUB_TOKEN where the Bridge MCP server runs, or save it in the Library edit mode.`);
|
|
9499
10216
|
const sha = await commitFileOnGitHub(gh.withBranchPrefix(void 0), { path, branch: pr.headRef, message: `docs(${name.toLowerCase()}): edit usage guidance with Claude (Bridge MCP)`, next });
|
|
9500
|
-
|
|
10217
|
+
const joined = open ? ` It continues the open ${name} docs proposal by @${open.author} (one open proposal per component).` : "";
|
|
10218
|
+
return {
|
|
10219
|
+
status: sha ? "committed" : "unchanged",
|
|
10220
|
+
pr: n,
|
|
10221
|
+
path,
|
|
10222
|
+
commit: sha || null,
|
|
10223
|
+
...open ? { continued: true } : {},
|
|
10224
|
+
said: (sha ? `Saved the ${name} docs on #${n}. Reviewers see them now.` : "The docs on that branch already say this.") + joined
|
|
10225
|
+
};
|
|
9501
10226
|
}
|
|
9502
10227
|
const original = await ctx.source.read(path);
|
|
9503
10228
|
const text = next(original);
|
|
9504
10229
|
if (original === text) return { status: "unchanged", path, said: `The ${name} docs on main already say this.` };
|
|
9505
|
-
const edit = { path, component: name, slug: name
|
|
10230
|
+
const edit = { path, component: name, slug: docsSlug(name), before: original, after: text };
|
|
9506
10231
|
const marks = productMarks(productsOf(config), product);
|
|
9507
10232
|
const notes = [a.notes?.trim(), marks.line].filter(Boolean).join("\n\n");
|
|
9508
10233
|
if (a.dryRun || !gh || !ctx.canWriteRemote) {
|
|
@@ -9576,8 +10301,142 @@ async function figmaCoverage(ctx, a = {}) {
|
|
|
9576
10301
|
};
|
|
9577
10302
|
}
|
|
9578
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
|
+
|
|
9579
10438
|
// src/server.ts
|
|
9580
|
-
var VERSION = true ? "0.
|
|
10439
|
+
var VERSION = true ? "0.5.0" : "dev";
|
|
9581
10440
|
var ok = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
|
|
9582
10441
|
var fail = (e) => ({ content: [{ type: "text", text: `Error: ${e.message ?? String(e)}` }], isError: true });
|
|
9583
10442
|
var run = (fn) => async (a) => {
|
|
@@ -9617,6 +10476,7 @@ function createServer(ctx) {
|
|
|
9617
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.`,
|
|
9618
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.",
|
|
9619
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.",
|
|
9620
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.'
|
|
9621
10481
|
].join(" ")
|
|
9622
10482
|
}
|
|
@@ -10114,6 +10974,57 @@ function createServer(ctx) {
|
|
|
10114
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.');
|
|
10115
10975
|
const WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
|
|
10116
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
|
+
);
|
|
10117
11028
|
server.registerTool(
|
|
10118
11029
|
"bridge_list_products",
|
|
10119
11030
|
{
|
|
@@ -10350,8 +11261,8 @@ function createServer(ctx) {
|
|
|
10350
11261
|
new ResourceTemplate("bridge://components/{name}", {
|
|
10351
11262
|
list: async () => {
|
|
10352
11263
|
const { config } = await ctx.config();
|
|
10353
|
-
const
|
|
10354
|
-
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" })) };
|
|
10355
11266
|
},
|
|
10356
11267
|
complete: {
|
|
10357
11268
|
name: async (value) => {
|
|
@@ -10539,6 +11450,7 @@ export {
|
|
|
10539
11450
|
requests_exports,
|
|
10540
11451
|
drafts_exports,
|
|
10541
11452
|
studio_exports,
|
|
11453
|
+
pieces_exports,
|
|
10542
11454
|
VERSION,
|
|
10543
11455
|
createServer,
|
|
10544
11456
|
serveStdio,
|