@bridge-ds/mcp 0.2.0 → 0.3.1
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/README.md +15 -12
- package/dist/{chunk-6JO2AG6X.js → chunk-I3TL76GK.js} +657 -78
- package/dist/{chunk-FJL6UTZG.js → chunk-X455XY4E.js} +418 -17
- package/dist/drafts.d.ts +4 -6
- package/dist/explorations.d.ts +87 -5
- package/dist/index.js +2 -2
- package/dist/lib.d.ts +1 -0
- package/dist/lib.js +16 -2
- package/dist/ops.d.ts +19 -1
- package/dist/server.d.ts +2 -1
- package/dist/{src-ABJPOTKE.js → src-XSBCZV4W.js} +1 -1
- package/package.json +3 -3
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CONFIG_FILE,
|
|
3
3
|
DEFAULT_PROTECTED_BRANCHES,
|
|
4
|
+
DEFAULT_RIGHTS,
|
|
4
5
|
LayeredTokenSet,
|
|
5
6
|
ProtectedBranchError,
|
|
6
7
|
RISK_LABELS,
|
|
8
|
+
TeamDirectory,
|
|
7
9
|
__export,
|
|
8
10
|
aliasOf,
|
|
11
|
+
approverEntries,
|
|
9
12
|
assertSafeBranch,
|
|
10
13
|
baseBranchOf,
|
|
11
14
|
branchExists,
|
|
@@ -21,10 +24,12 @@ import {
|
|
|
21
24
|
contrastPair,
|
|
22
25
|
currentBranch,
|
|
23
26
|
diffLayered,
|
|
27
|
+
entryLabel,
|
|
28
|
+
expandOwners,
|
|
29
|
+
findPreviewComment,
|
|
24
30
|
git,
|
|
25
31
|
globToRegExp,
|
|
26
32
|
hexToRgba,
|
|
27
|
-
installCommand,
|
|
28
33
|
isGitRepo,
|
|
29
34
|
layeredImpact,
|
|
30
35
|
layeredParityIssues,
|
|
@@ -33,19 +38,23 @@ import {
|
|
|
33
38
|
originRepo,
|
|
34
39
|
parseContrastChecks,
|
|
35
40
|
previewLinks,
|
|
41
|
+
previewsSettled,
|
|
36
42
|
protectedBranchesOf,
|
|
43
|
+
readPreviewComment,
|
|
37
44
|
renderedColor,
|
|
45
|
+
resolveBranchPreviews,
|
|
38
46
|
rgbaToDtcg,
|
|
39
47
|
rgbaToHex,
|
|
40
48
|
runLayeredContrast,
|
|
41
49
|
selectTokenFiles,
|
|
50
|
+
shipPolicy,
|
|
42
51
|
showFile,
|
|
43
|
-
snapshotUrl,
|
|
44
|
-
tarballUrl,
|
|
45
52
|
toConfigJson,
|
|
46
53
|
tokensPath,
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
unreadableTeams,
|
|
55
|
+
validateLayered,
|
|
56
|
+
whoCanShipSentence
|
|
57
|
+
} from "./chunk-X455XY4E.js";
|
|
49
58
|
|
|
50
59
|
// src/lib.ts
|
|
51
60
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -704,15 +713,67 @@ function applyTokenEdits(base, edits) {
|
|
|
704
713
|
|
|
705
714
|
// ../core/src/github-client.ts
|
|
706
715
|
var GitHubError = class extends Error {
|
|
707
|
-
constructor(status, message, detail) {
|
|
716
|
+
constructor(status, message, detail, info = {}) {
|
|
708
717
|
super(message);
|
|
709
718
|
this.status = status;
|
|
710
719
|
this.detail = detail;
|
|
711
720
|
this.name = "GitHubError";
|
|
721
|
+
this.auth = info.auth;
|
|
722
|
+
this.permission = info.permission;
|
|
712
723
|
}
|
|
713
724
|
status;
|
|
714
725
|
detail;
|
|
726
|
+
/** Set when the failure is about the token: rejected (401) or missing a permission (403). */
|
|
727
|
+
auth;
|
|
728
|
+
/** With auth "missing-permission": the permission the call needed, when known. */
|
|
729
|
+
permission;
|
|
715
730
|
};
|
|
731
|
+
var PERMISSION_LABELS = {
|
|
732
|
+
issues: "Issues",
|
|
733
|
+
pull_requests: "Pull requests",
|
|
734
|
+
contents: "Contents",
|
|
735
|
+
checks: "Checks",
|
|
736
|
+
statuses: "Commit statuses",
|
|
737
|
+
metadata: "Metadata",
|
|
738
|
+
actions: "Actions",
|
|
739
|
+
deployments: "Deployments",
|
|
740
|
+
workflows: "Workflows",
|
|
741
|
+
administration: "Administration"
|
|
742
|
+
};
|
|
743
|
+
function permissionLabel(permission) {
|
|
744
|
+
return PERMISSION_LABELS[permission] ?? permission.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
745
|
+
}
|
|
746
|
+
var TOKEN_REJECTED_MESSAGE = "Your GitHub token stopped working. It was revoked, regenerated or has expired: replace it with a new one.";
|
|
747
|
+
function missingPermissionMessage(permission, action) {
|
|
748
|
+
const what = action ? `This token cannot ${action}.` : "This token is missing a permission for this.";
|
|
749
|
+
if (!permission) return `${what} Check its repository permissions on GitHub (Contents and Pull requests need Read and write).`;
|
|
750
|
+
return `${what} Add ${permissionLabel(permission)}: Read and write on GitHub.`;
|
|
751
|
+
}
|
|
752
|
+
function requiredPermission(path, acceptedHeader) {
|
|
753
|
+
if (acceptedHeader) {
|
|
754
|
+
const first = acceptedHeader.split(";")[0].split(",").map((p2) => p2.trim().split("=")[0].trim()).filter((p2) => p2 && p2 !== "metadata");
|
|
755
|
+
if (first.length) return first[0];
|
|
756
|
+
}
|
|
757
|
+
const p = path.replace(/[?#].*$/, "");
|
|
758
|
+
if (/\/repos\/[^/]+\/[^/]+\/(issues|labels|milestones)(\/|$)/.test(p)) return "issues";
|
|
759
|
+
if (/\/repos\/[^/]+\/[^/]+\/pulls(\/|$)/.test(p)) return "pull_requests";
|
|
760
|
+
if (/\/repos\/[^/]+\/[^/]+\/(rulesets(\/|$)|branches\/.+\/protection$)/.test(p)) return "administration";
|
|
761
|
+
if (/\/repos\/[^/]+\/[^/]+\/(contents|git|commits|branches|merges)(\/|$)/.test(p)) return "contents";
|
|
762
|
+
if (/\/repos\/[^/]+\/[^/]+\/(check-runs|check-suites)(\/|$)/.test(p)) return "checks";
|
|
763
|
+
if (/\/repos\/[^/]+\/[^/]+\/statuses(\/|$)/.test(p)) return "statuses";
|
|
764
|
+
if (/\/repos\/[^/]+\/[^/]+\/deployments(\/|$)/.test(p)) return "deployments";
|
|
765
|
+
if (/\/repos\/[^/]+\/[^/]+\/actions(\/|$)/.test(p)) return "actions";
|
|
766
|
+
return void 0;
|
|
767
|
+
}
|
|
768
|
+
var PERMISSION_DENIED = /not accessible by (personal access token|integration)|must have (admin|push|write) (rights|access)|write access to repository not granted|permission to .* denied/i;
|
|
769
|
+
function classifyAuth(status, detail, opts = {}) {
|
|
770
|
+
if (status === 401 && opts.withToken !== false) return { auth: "rejected" };
|
|
771
|
+
if (status !== 403 || /rate limit|archived/i.test(detail)) return {};
|
|
772
|
+
if (opts.acceptedPermissions || PERMISSION_DENIED.test(detail)) {
|
|
773
|
+
return { auth: "missing-permission", permission: requiredPermission(opts.path ?? "", opts.acceptedPermissions) };
|
|
774
|
+
}
|
|
775
|
+
return {};
|
|
776
|
+
}
|
|
716
777
|
var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
|
|
717
778
|
var isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
|
|
718
779
|
var API = "https://api.github.com";
|
|
@@ -774,7 +835,7 @@ var GitHubClient = class _GitHubClient {
|
|
|
774
835
|
}
|
|
775
836
|
}
|
|
776
837
|
assertToken(what) {
|
|
777
|
-
if (!this.token) throw new GitHubError(401, `${what} needs a GitHub token (contents and pull requests, read and write, on this repository only). Without one Bridge is read-only
|
|
838
|
+
if (!this.token) throw new GitHubError(401, `${what} needs a GitHub token (contents and pull requests, read and write, on this repository only). Without one Bridge is read-only.`, "no-token");
|
|
778
839
|
}
|
|
779
840
|
get repoPath() {
|
|
780
841
|
return `/repos/${this.owner}/${this.name}`;
|
|
@@ -823,7 +884,12 @@ var GitHubClient = class _GitHubClient {
|
|
|
823
884
|
detail
|
|
824
885
|
);
|
|
825
886
|
}
|
|
826
|
-
|
|
887
|
+
const info = classifyAuth(res.status, detail, {
|
|
888
|
+
path,
|
|
889
|
+
acceptedPermissions: res.headers?.get("x-accepted-github-permissions"),
|
|
890
|
+
withToken: Boolean(this.token)
|
|
891
|
+
});
|
|
892
|
+
throw new GitHubError(res.status, friendlyMessage(res.status, detail, info), detail, info);
|
|
827
893
|
}
|
|
828
894
|
return await res.json();
|
|
829
895
|
}
|
|
@@ -973,6 +1039,25 @@ var GitHubClient = class _GitHubClient {
|
|
|
973
1039
|
throw new GitHubError(422, `GitHub refused to mark #${n} ready for review${detail ? `: ${detail}` : "."} Use "Ready for review" on GitHub instead.`, detail);
|
|
974
1040
|
}
|
|
975
1041
|
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Turns an open pull request back into a draft: the GraphQL mutation convertPullRequestToDraft (REST cannot set
|
|
1044
|
+
* `draft` either), same token. No-op when it is already a draft.
|
|
1045
|
+
*/
|
|
1046
|
+
async convertToDraft(n) {
|
|
1047
|
+
this.assertToken("Turning a pull request back into a draft");
|
|
1048
|
+
const pr = await this.request("GET", `${this.repoPath}/pulls/${n}`);
|
|
1049
|
+
if (pr.draft === true) return;
|
|
1050
|
+
if (pr.state && pr.state !== "open") throw new GitHubError(422, `Pull request #${n} is ${pr.state}: only an open one can go back to draft.`);
|
|
1051
|
+
if (!pr.node_id) throw new GitHubError(422, `Pull request #${n} has no node id: convert it to draft on GitHub.`);
|
|
1052
|
+
const r = await this.request("POST", "/graphql", {
|
|
1053
|
+
query: "mutation($id: ID!) { convertPullRequestToDraft(input: { pullRequestId: $id }) { pullRequest { number isDraft } } }",
|
|
1054
|
+
variables: { id: pr.node_id }
|
|
1055
|
+
});
|
|
1056
|
+
if (r?.errors?.length) {
|
|
1057
|
+
const detail = r.errors.map((e) => e.message).filter(Boolean).join(" ");
|
|
1058
|
+
throw new GitHubError(422, `GitHub refused to turn #${n} back into a draft${detail ? `: ${detail}` : "."} Use "Convert to draft" on GitHub instead.`, detail);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
976
1061
|
async createPullRequest(input) {
|
|
977
1062
|
this.assertWritableBranch(input.head, input.base);
|
|
978
1063
|
const r = await this.request(
|
|
@@ -1002,6 +1087,11 @@ var GitHubClient = class _GitHubClient {
|
|
|
1002
1087
|
async addComment(issueNumber, body) {
|
|
1003
1088
|
await this.request("POST", `${this.repoPath}/issues/${issueNumber}/comments`, { body });
|
|
1004
1089
|
}
|
|
1090
|
+
/** Comments on an issue or pull request, oldest first (first 100: the sticky preview comment is posted early). */
|
|
1091
|
+
async listIssueComments(issueNumber) {
|
|
1092
|
+
const r = await this.request("GET", `${this.repoPath}/issues/${issueNumber}/comments?per_page=100`);
|
|
1093
|
+
return r.map((c) => ({ id: c.id, body: c.body ?? "", author: c.user?.login ?? "unknown", updatedAt: c.updated_at }));
|
|
1094
|
+
}
|
|
1005
1095
|
// --- Issues (component change requests) ---------------------------------------------------
|
|
1006
1096
|
async createIssue(input) {
|
|
1007
1097
|
const r = await this.request("POST", `${this.repoPath}/issues`, {
|
|
@@ -1022,6 +1112,67 @@ var GitHubClient = class _GitHubClient {
|
|
|
1022
1112
|
async getIssue(n) {
|
|
1023
1113
|
return toIssue(await this.request("GET", `${this.repoPath}/issues/${n}`));
|
|
1024
1114
|
}
|
|
1115
|
+
/** Edits an issue: its body, or closes it with a reason ("not_planned" for a declined request). */
|
|
1116
|
+
async updateIssue(n, patch) {
|
|
1117
|
+
this.assertToken("Editing an issue");
|
|
1118
|
+
return toIssue(await this.request("PATCH", `${this.repoPath}/issues/${n}`, patch));
|
|
1119
|
+
}
|
|
1120
|
+
/** Adds people to an issue's assignees (GitHub ignores logins that cannot be assigned: the result says who is). */
|
|
1121
|
+
async addAssignees(n, logins) {
|
|
1122
|
+
this.assertToken("Assigning an issue");
|
|
1123
|
+
const r = await this.request("POST", `${this.repoPath}/issues/${n}/assignees`, { assignees: logins });
|
|
1124
|
+
return (r.assignees ?? []).map((a) => a.login);
|
|
1125
|
+
}
|
|
1126
|
+
/** People with at least `permission` on the repository (reading the list needs push access). First 100. */
|
|
1127
|
+
async listCollaborators(permission = "push") {
|
|
1128
|
+
const r = await this.request("GET", `${this.repoPath}/collaborators?permission=${permission}&per_page=100`);
|
|
1129
|
+
return r.map((c) => ({ login: c.login, avatarUrl: c.avatar_url ?? "" }));
|
|
1130
|
+
}
|
|
1131
|
+
/** Whether a token is sent with each request (TeamReader). */
|
|
1132
|
+
get hasToken() {
|
|
1133
|
+
return Boolean(this.token);
|
|
1134
|
+
}
|
|
1135
|
+
/** Every page of a list endpoint (per_page 100), up to `maxPages`. */
|
|
1136
|
+
async paginate(path, maxPages = 20) {
|
|
1137
|
+
const out = [];
|
|
1138
|
+
const sep2 = path.includes("?") ? "&" : "?";
|
|
1139
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
1140
|
+
const r = await this.request("GET", `${path}${sep2}per_page=100&page=${page}`);
|
|
1141
|
+
out.push(...r);
|
|
1142
|
+
if (r.length < 100) break;
|
|
1143
|
+
}
|
|
1144
|
+
return out;
|
|
1145
|
+
}
|
|
1146
|
+
/** Every member login of an organization team (needs read:org, or Members: Read on a fine-grained token). */
|
|
1147
|
+
async listTeamMembers(org, slug2) {
|
|
1148
|
+
const r = await this.paginate(`/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug2)}/members`);
|
|
1149
|
+
return r.map((m) => m.login);
|
|
1150
|
+
}
|
|
1151
|
+
/** A user's membership of a team: "active", "pending", or null when GitHub says they are not a member (404). */
|
|
1152
|
+
async getTeamMembership(org, slug2, login) {
|
|
1153
|
+
try {
|
|
1154
|
+
const r = await this.request("GET", `/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug2)}/memberships/${encodeURIComponent(login)}`);
|
|
1155
|
+
return r.state === "active" ? "active" : "pending";
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
if (err instanceof GitHubError && err.status === 404) return null;
|
|
1158
|
+
throw err;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
/** "User" or "Organization" for a login, null when GitHub does not know it. */
|
|
1162
|
+
async getAccountType(login) {
|
|
1163
|
+
try {
|
|
1164
|
+
const r = await this.request("GET", `/users/${encodeURIComponent(login)}`);
|
|
1165
|
+
return r.type === "Organization" ? "Organization" : r.type === "User" ? "User" : null;
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
if (err instanceof GitHubError && err.status === 404) return null;
|
|
1168
|
+
throw err;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
/** The organization's teams this token can see. */
|
|
1172
|
+
async listOrgTeams(org) {
|
|
1173
|
+
const r = await this.paginate(`/orgs/${encodeURIComponent(org)}/teams`);
|
|
1174
|
+
return r.map((t) => ({ slug: t.slug, name: t.name }));
|
|
1175
|
+
}
|
|
1025
1176
|
/** Create or update a binary file (base64 content) on a branch, e.g. a Figma export attached to a request. */
|
|
1026
1177
|
async putBase64File(input) {
|
|
1027
1178
|
this.assertWritableBranch(input.branch);
|
|
@@ -1157,6 +1308,27 @@ var GitHubClient = class _GitHubClient {
|
|
|
1157
1308
|
const pending = checks.some((c) => c.status !== "completed");
|
|
1158
1309
|
return { state: !checks.length ? "none" : failing ? "failure" : pending ? "pending" : "success", checks };
|
|
1159
1310
|
}
|
|
1311
|
+
/** Commit statuses of a commit, newest first (Vercel reports each project as "Vercel – <project>"). */
|
|
1312
|
+
async listCommitStatuses(sha) {
|
|
1313
|
+
const r = await this.request("GET", `${this.repoPath}/commits/${encodeRef(sha)}/statuses?per_page=100`);
|
|
1314
|
+
return r.map((s) => ({ context: s.context, state: s.state, target_url: s.target_url ?? null, description: s.description ?? null }));
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* GitHub deployments of a commit with their latest status (environment "Preview – <project>", environment_url the
|
|
1318
|
+
* preview). Latest per environment only, so it costs one read plus one per environment.
|
|
1319
|
+
*/
|
|
1320
|
+
async listDeployments(sha) {
|
|
1321
|
+
const list = await this.request("GET", `${this.repoPath}/deployments?sha=${encodeURIComponent(sha)}&per_page=30`);
|
|
1322
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1323
|
+
for (const d of list) if (!latest.has(d.environment)) latest.set(d.environment, d);
|
|
1324
|
+
return Promise.all(
|
|
1325
|
+
[...latest.values()].map(async (d) => {
|
|
1326
|
+
const st = await this.request("GET", `${this.repoPath}/deployments/${d.id}/statuses?per_page=1`).catch(() => []);
|
|
1327
|
+
const s = st[0];
|
|
1328
|
+
return { id: d.id, environment: d.environment, state: s?.state ?? null, environmentUrl: s?.environment_url || null };
|
|
1329
|
+
})
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1160
1332
|
async getPullRequest(n) {
|
|
1161
1333
|
const r = await this.request("GET", `${this.repoPath}/pulls/${n}`);
|
|
1162
1334
|
return { ...toSummary(r), body: r.body ?? "", mergeable: r.mergeable ?? null };
|
|
@@ -1183,7 +1355,44 @@ var GitHubClient = class _GitHubClient {
|
|
|
1183
1355
|
"GET",
|
|
1184
1356
|
`${this.repoPath}/commits/${encodeRef(ref)}/check-runs`
|
|
1185
1357
|
);
|
|
1186
|
-
return r.check_runs.map((c) => ({ id: c.id, name: c.name, status: c.status, conclusion: c.conclusion, url: c.html_url }));
|
|
1358
|
+
return r.check_runs.map((c) => ({ id: c.id, name: c.name, status: c.status, conclusion: c.conclusion, url: c.html_url, ...c.app?.slug ? { app: c.app.slug } : {} }));
|
|
1359
|
+
}
|
|
1360
|
+
/** Rulesets that apply to the repository, without their rules (GET /rulesets; read access is enough). */
|
|
1361
|
+
async listRulesets() {
|
|
1362
|
+
const r = await this.request("GET", `${this.repoPath}/rulesets?includes_parents=true&per_page=100`);
|
|
1363
|
+
return r.map(withRulesetUrl);
|
|
1364
|
+
}
|
|
1365
|
+
/** One ruleset with its rules. */
|
|
1366
|
+
async getRuleset(id) {
|
|
1367
|
+
return withRulesetUrl(await this.request("GET", `${this.repoPath}/rulesets/${id}`));
|
|
1368
|
+
}
|
|
1369
|
+
/** Creates a repository ruleset. Needs admin on the repo (fine-grained token: Administration read and write). */
|
|
1370
|
+
async createRuleset(payload) {
|
|
1371
|
+
this.assertToken("Protecting the library");
|
|
1372
|
+
return withRulesetUrl(await this.request("POST", `${this.repoPath}/rulesets`, payload));
|
|
1373
|
+
}
|
|
1374
|
+
/** Replaces a repository ruleset (same permissions as createRuleset). */
|
|
1375
|
+
async updateRuleset(id, payload) {
|
|
1376
|
+
this.assertToken("Protecting the library");
|
|
1377
|
+
return withRulesetUrl(await this.request("PUT", `${this.repoPath}/rulesets/${id}`, payload));
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Classic branch protection of `branch`: the settings, null when the branch has none (404), undefined when this
|
|
1381
|
+
* token cannot read them (403: the endpoint needs admin). On a 403 the public branch summary still tells
|
|
1382
|
+
* whether the branch is protected at all: `protected: false` means there is nothing to read, so null.
|
|
1383
|
+
*/
|
|
1384
|
+
async getBranchProtection(branch) {
|
|
1385
|
+
try {
|
|
1386
|
+
return await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}/protection`);
|
|
1387
|
+
} catch (err) {
|
|
1388
|
+
if (!(err instanceof GitHubError)) throw err;
|
|
1389
|
+
if (err.status === 404) return null;
|
|
1390
|
+
if (err.status === 403 || err.status === 401) {
|
|
1391
|
+
const b = await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}`).catch(() => void 0);
|
|
1392
|
+
return b?.protected === false ? null : void 0;
|
|
1393
|
+
}
|
|
1394
|
+
throw err;
|
|
1395
|
+
}
|
|
1187
1396
|
}
|
|
1188
1397
|
/** Create a check run (needs a GitHub App token; personal tokens are refused by GitHub). */
|
|
1189
1398
|
async createCheckRun(input) {
|
|
@@ -1225,7 +1434,9 @@ function toSummary(r) {
|
|
|
1225
1434
|
...typeof r.body === "string" ? { body: r.body } : {}
|
|
1226
1435
|
};
|
|
1227
1436
|
}
|
|
1228
|
-
function friendlyMessage(status, detail) {
|
|
1437
|
+
function friendlyMessage(status, detail, info = {}) {
|
|
1438
|
+
if (info.auth === "rejected") return TOKEN_REJECTED_MESSAGE;
|
|
1439
|
+
if (info.auth === "missing-permission") return missingPermissionMessage(info.permission);
|
|
1229
1440
|
switch (status) {
|
|
1230
1441
|
case 401:
|
|
1231
1442
|
return "GitHub rejected the token. It may be expired or mistyped: create a new one and save it in Settings.";
|
|
@@ -1245,6 +1456,10 @@ function friendlyMessage(status, detail) {
|
|
|
1245
1456
|
return `GitHub error ${status}${detail ? `: ${detail}` : ""}`;
|
|
1246
1457
|
}
|
|
1247
1458
|
}
|
|
1459
|
+
function withRulesetUrl(r) {
|
|
1460
|
+
const href = r._links?.html?.href;
|
|
1461
|
+
return href && !r.html_url ? { ...r, html_url: href } : r;
|
|
1462
|
+
}
|
|
1248
1463
|
function toIssue(r) {
|
|
1249
1464
|
return {
|
|
1250
1465
|
number: r.number,
|
|
@@ -1850,6 +2065,38 @@ async function loadGitHubHistory(client2, config, opts = {}) {
|
|
|
1850
2065
|
return buildReport({ source: "github", label: `${client2.fullName}@${ref}`, ref, entries, releases, ...currentVersion ? { currentVersion } : {}, truncated: listed >= limit });
|
|
1851
2066
|
}
|
|
1852
2067
|
|
|
2068
|
+
// ../core/src/previews-github.ts
|
|
2069
|
+
async function branchPreviewUrls(gh, ref, config, opts = {}) {
|
|
2070
|
+
const apps = config?.previews?.apps ?? {};
|
|
2071
|
+
if (!Object.values(apps).some(Boolean)) return [];
|
|
2072
|
+
let pr;
|
|
2073
|
+
let branch = opts.branch;
|
|
2074
|
+
let sha = opts.sha;
|
|
2075
|
+
if (typeof ref === "number") {
|
|
2076
|
+
pr = ref;
|
|
2077
|
+
if (!branch || !sha) {
|
|
2078
|
+
const p = await gh.getPullRequest(ref);
|
|
2079
|
+
branch ??= p.headRef;
|
|
2080
|
+
sha ??= p.headSha;
|
|
2081
|
+
}
|
|
2082
|
+
} else {
|
|
2083
|
+
sha = ref;
|
|
2084
|
+
if (!branch || !opts.comments) {
|
|
2085
|
+
const open = (await gh.listPullRequestsForCommit(ref).catch(() => [])).find((p) => p.state === "open");
|
|
2086
|
+
if (open) {
|
|
2087
|
+
pr = open.number;
|
|
2088
|
+
branch ??= open.headRef;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
const [comments, statuses, deployments] = await Promise.all([
|
|
2093
|
+
opts.comments ? Promise.resolve(opts.comments) : pr !== void 0 ? gh.listIssueComments(pr).catch(() => []) : Promise.resolve([]),
|
|
2094
|
+
sha ? gh.listCommitStatuses(sha).catch(() => []) : Promise.resolve([]),
|
|
2095
|
+
sha ? gh.listDeployments(sha).catch(() => []) : Promise.resolve([])
|
|
2096
|
+
]);
|
|
2097
|
+
return resolveBranchPreviews(apps, { branch: branch ?? "", ...pr !== void 0 ? { pr } : {}, ...sha ? { sha } : {} }, { comments, statuses, deployments });
|
|
2098
|
+
}
|
|
2099
|
+
|
|
1853
2100
|
// ../core/src/ramps.js
|
|
1854
2101
|
var STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
|
|
1855
2102
|
var LIGHTNESS = { 50: 0.975, 100: 0.952, 200: 0.912, 300: 0.85, 400: 0.735, 500: 0.63, 600: 0.53, 700: 0.445, 800: 0.36, 900: 0.27, 950: 0.205 };
|
|
@@ -2378,6 +2625,7 @@ function composeNewBrandBody(plan, opts) {
|
|
|
2378
2625
|
}
|
|
2379
2626
|
|
|
2380
2627
|
// ../core/src/docs-draft.ts
|
|
2628
|
+
var DOCS_KEYS = ["summary", "preview", "whenToUse", "whenNotToUse", "dos", "donts", "accessibility", "contentGuidelines", "anatomy"];
|
|
2381
2629
|
var PROSE_KEYS = ["summary", "whenToUse", "whenNotToUse", "dos", "donts", "accessibility", "contentGuidelines"];
|
|
2382
2630
|
var ACRONYMS = /* @__PURE__ */ new Set(["sm", "md", "lg", "xl", "xs"]);
|
|
2383
2631
|
function anatomyLabel(className) {
|
|
@@ -2430,6 +2678,7 @@ function prefillDocs(input) {
|
|
|
2430
2678
|
contentGuidelines: existing?.contentGuidelines ?? [],
|
|
2431
2679
|
anatomy,
|
|
2432
2680
|
...existing?.draftedBy ? { draftedBy: existing.draftedBy } : {},
|
|
2681
|
+
...existing?.draftedSections?.length ? { draftedSections: existing.draftedSections } : {},
|
|
2433
2682
|
...existing?.needsReview !== void 0 ? { needsReview: existing.needsReview } : {}
|
|
2434
2683
|
};
|
|
2435
2684
|
return {
|
|
@@ -2444,7 +2693,17 @@ function prefillDocs(input) {
|
|
|
2444
2693
|
draft
|
|
2445
2694
|
};
|
|
2446
2695
|
}
|
|
2447
|
-
|
|
2696
|
+
var SECTION_SHAPE = {
|
|
2697
|
+
summary: '"summary": "one or two sentences"',
|
|
2698
|
+
whenToUse: '"whenToUse": ["..."]',
|
|
2699
|
+
whenNotToUse: '"whenNotToUse": ["..."]',
|
|
2700
|
+
dos: '"dos": [{ "title": "...", "text": "..." }]',
|
|
2701
|
+
donts: '"donts": [{ "title": "...", "text": "..." }]',
|
|
2702
|
+
accessibility: '"accessibility": ["..."]',
|
|
2703
|
+
contentGuidelines: '"contentGuidelines": ["..."]'
|
|
2704
|
+
};
|
|
2705
|
+
var docsSectionShapes = (keys) => `{ ${keys.map((k) => SECTION_SHAPE[k]).join(", ")} }`;
|
|
2706
|
+
function docsPrompt(prefill, conventions = {}, options = {}) {
|
|
2448
2707
|
const facts = [
|
|
2449
2708
|
`Component: ${prefill.component}`,
|
|
2450
2709
|
prefill.propsTable ? `Props (parsed from TypeScript):
|
|
@@ -2459,15 +2718,33 @@ ${prefill.propsTable}` : "Props: none of its own.",
|
|
|
2459
2718
|
const block = (title, text) => text?.trim() ? `<${title}>
|
|
2460
2719
|
${text.trim()}
|
|
2461
2720
|
</${title}>` : "";
|
|
2721
|
+
const keys = options.keys;
|
|
2722
|
+
const check = keys ? (options.check ?? []).filter((k) => keys.includes(k)) : [];
|
|
2723
|
+
const write = keys ? keys.filter((k) => !check.includes(k)) : [];
|
|
2724
|
+
const task = keys ? [
|
|
2725
|
+
...write.length ? [`${options.rewrite ? "Rewrite" : "Write"} these sections of ${prefill.component}.docs.json for the Bridge design system: ${write.join(", ")}.`] : [],
|
|
2726
|
+
...check.length ? [
|
|
2727
|
+
`Check these written sections of ${prefill.component}.docs.json against the current code: ${check.join(", ")}. The code changed after they were written.`,
|
|
2728
|
+
"Return a checked section ONLY when something in it is no longer true for the code (a wrong or removed prop, a removed state or variant, changed behavior). Then return the whole section with only the untrue parts fixed, keeping the writer's wording everywhere else.",
|
|
2729
|
+
"Leave a checked section out of the answer when it is still accurate: a missing key means unchanged. Never return a checked section just to improve its style."
|
|
2730
|
+
] : [],
|
|
2731
|
+
...options.rewrite ? ["Rewrite each one from the facts and the source: keep what is true in the current text, drop what the code does not support."] : []
|
|
2732
|
+
] : [`Write the missing prose of ${prefill.component}.docs.json for the Bridge design system.`];
|
|
2733
|
+
const shape = keys ? [
|
|
2734
|
+
`- ${check.length ? "Return" : "Write"} only these keys: ${keys.join(", ")}${check.length ? ` (the checked ones, ${check.join(", ")}, only when they need a fix)` : ""}. Never return anatomy, preview or any other key: code decides them.`,
|
|
2735
|
+
`- Answer with one JSON object holding only those keys, nothing else: ${docsSectionShapes(keys)}.`
|
|
2736
|
+
] : [
|
|
2737
|
+
`- Fill only these keys: ${prefill.missing.join(", ") || "none (everything is written: improve nothing, return the draft as is)"}. Keep every other key exactly as given.`,
|
|
2738
|
+
"- Answer with the full docs.json as one JSON object, nothing else."
|
|
2739
|
+
];
|
|
2462
2740
|
return [
|
|
2463
|
-
|
|
2741
|
+
...task,
|
|
2464
2742
|
"",
|
|
2465
2743
|
"Rules:",
|
|
2466
2744
|
"- Ground every sentence in the facts and the source below. Never invent props, variants, keyboard behavior or tokens that are not there.",
|
|
2467
2745
|
"- Bridge voice: sentence case, plain words, verbs first, no em dashes, no marketing words.",
|
|
2468
2746
|
'- Keep the keys and shapes of the example docs.json. dos and donts are { "title", "text" } objects; add an "example" only with components and props that exist.',
|
|
2469
|
-
|
|
2470
|
-
"- Answer with the full docs.json as one JSON object, nothing else.",
|
|
2747
|
+
...shape,
|
|
2471
2748
|
"",
|
|
2472
2749
|
"Facts computed from the code:",
|
|
2473
2750
|
facts.join("\n\n"),
|
|
@@ -2476,11 +2753,179 @@ ${text.trim()}
|
|
|
2476
2753
|
${JSON.stringify(prefill.draft, null, 2)}`,
|
|
2477
2754
|
"",
|
|
2478
2755
|
block("source", conventions.source),
|
|
2756
|
+
block("styles", conventions.styles),
|
|
2479
2757
|
block("agents-md", conventions.agents),
|
|
2480
2758
|
block("voice", conventions.voice),
|
|
2481
2759
|
block("example-docs-json", conventions.example)
|
|
2482
2760
|
].filter((x, i, all) => x !== "" || all[i - 1] !== "").join("\n");
|
|
2483
2761
|
}
|
|
2762
|
+
function markClaudeDraft(d, sections2) {
|
|
2763
|
+
if (!sections2) {
|
|
2764
|
+
const { draftedSections: _all, ...rest } = d;
|
|
2765
|
+
return { ...rest, draftedBy: "claude", needsReview: true };
|
|
2766
|
+
}
|
|
2767
|
+
const waiting = d.needsReview === true ? d.draftedSections ?? (d.draftedBy ? [...PROSE_KEYS] : []) : [];
|
|
2768
|
+
const union = PROSE_KEYS.filter((k) => waiting.includes(k) || sections2.includes(k));
|
|
2769
|
+
return { ...d, draftedBy: "claude", needsReview: true, draftedSections: union };
|
|
2770
|
+
}
|
|
2771
|
+
function formatDocsJson(d) {
|
|
2772
|
+
const order = ["summary", "category", "status", ...DOCS_KEYS.filter((k) => k !== "summary"), "draftedBy", "draftedSections", "needsReview"];
|
|
2773
|
+
const out = {};
|
|
2774
|
+
for (const k of order) if (d[k] !== void 0) out[k] = d[k];
|
|
2775
|
+
for (const [k, v] of Object.entries(d)) if (!(k in out) && v !== void 0) out[k] = v;
|
|
2776
|
+
return `${JSON.stringify(out, null, 2)}
|
|
2777
|
+
`;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
// ../core/src/change-drafts.ts
|
|
2781
|
+
function cleanDraftText(text) {
|
|
2782
|
+
return text.replace(/\s*\u2014\s*/g, ", ").replace(/,\s*,/g, ",").replace(/^,\s*/, "").replace(/[ \t]+/g, " ").trim();
|
|
2783
|
+
}
|
|
2784
|
+
var capital = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
2785
|
+
var DRAFT_LINE_MAX = 120;
|
|
2786
|
+
function clipText(text, max) {
|
|
2787
|
+
const t = text.replace(/\s+/g, " ").trim().replace(/\.$/, "");
|
|
2788
|
+
if (t.length <= max) return t;
|
|
2789
|
+
const cut = t.slice(0, max + 1);
|
|
2790
|
+
const at = cut.lastIndexOf(" ");
|
|
2791
|
+
return (at > max * 0.5 ? cut.slice(0, at) : t.slice(0, max)).replace(/[\s,;:(]+$/, "");
|
|
2792
|
+
}
|
|
2793
|
+
function sanitizeDraftLine(raw) {
|
|
2794
|
+
if (typeof raw !== "string") return "";
|
|
2795
|
+
return capital(clipText(cleanDraftText(raw.replace(/[\r\n]+/g, " ")), DRAFT_LINE_MAX));
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
// ../core/src/docs-ai.ts
|
|
2799
|
+
var DOCS_DRAFT_MODEL = "claude-sonnet-5-5";
|
|
2800
|
+
var ANTHROPIC_MESSAGES_URL = "https://api.anthropic.com/v1/messages";
|
|
2801
|
+
var DOCS_SELECTION_MODES = {
|
|
2802
|
+
clearer: "Make it clearer: plain words, one idea per sentence, the most useful fact first.",
|
|
2803
|
+
shorter: "Make it shorter: keep every fact, drop filler, aim for half the words or fewer.",
|
|
2804
|
+
voice: "Put it in the Bridge voice: sentence case, verbs first, calm and precise, no marketing words, no em dash."
|
|
2805
|
+
};
|
|
2806
|
+
var isDocsSelectionMode = (m) => typeof m === "string" && m in DOCS_SELECTION_MODES;
|
|
2807
|
+
var isProseKey = (k) => typeof k === "string" && PROSE_KEYS.includes(k);
|
|
2808
|
+
function validateDocsTask(x) {
|
|
2809
|
+
if (!x || typeof x !== "object") return ["The docs task is missing."];
|
|
2810
|
+
const t = x;
|
|
2811
|
+
if (t.kind === "sections") {
|
|
2812
|
+
const keys = t.keys;
|
|
2813
|
+
if (!Array.isArray(keys) || !keys.length || !keys.every(isProseKey)) return [`The docs task lists no sections: use ${PROSE_KEYS.join(", ")}.`];
|
|
2814
|
+
if (t.check !== void 0 && (!Array.isArray(t.check) || !t.check.every(isProseKey))) return ["check must list prose sections."];
|
|
2815
|
+
return [];
|
|
2816
|
+
}
|
|
2817
|
+
if (t.kind === "selection") {
|
|
2818
|
+
const out = [];
|
|
2819
|
+
if (!isProseKey(t.key)) out.push(`The selection needs a section: ${PROSE_KEYS.join(", ")}.`);
|
|
2820
|
+
if (typeof t.text !== "string" || !t.text.trim()) out.push("The selection has no text.");
|
|
2821
|
+
if (!isDocsSelectionMode(t.mode)) out.push("The selection mode must be clearer, shorter or voice.");
|
|
2822
|
+
return out;
|
|
2823
|
+
}
|
|
2824
|
+
return ['The docs task kind must be "sections" or "selection".'];
|
|
2825
|
+
}
|
|
2826
|
+
var docsTaskKeys = (t) => t.kind === "sections" ? t.keys : [t.key];
|
|
2827
|
+
function parseDocsReply(text) {
|
|
2828
|
+
const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
|
|
2829
|
+
const body = fenced ? fenced[1] : text;
|
|
2830
|
+
const start = body.indexOf("{");
|
|
2831
|
+
const end = body.lastIndexOf("}");
|
|
2832
|
+
if (start < 0 || end <= start) throw new Error("The reply holds no JSON object.");
|
|
2833
|
+
const json = JSON.parse(body.slice(start, end + 1));
|
|
2834
|
+
if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("The reply is not a docs.json object.");
|
|
2835
|
+
return json;
|
|
2836
|
+
}
|
|
2837
|
+
var cleanDocsText = (s) => cleanDraftText(s.replace(/[\r\n]+/g, " "));
|
|
2838
|
+
var isExample = (x) => {
|
|
2839
|
+
if (typeof x === "string") return true;
|
|
2840
|
+
if (!x || typeof x !== "object" || Array.isArray(x)) return false;
|
|
2841
|
+
const n = x;
|
|
2842
|
+
if (typeof n.c !== "string" || !n.c) return false;
|
|
2843
|
+
if (n.p !== void 0 && (!n.p || typeof n.p !== "object" || Array.isArray(n.p))) return false;
|
|
2844
|
+
return n.children === void 0 || Array.isArray(n.children) && n.children.every(isExample);
|
|
2845
|
+
};
|
|
2846
|
+
var textList = (x) => {
|
|
2847
|
+
if (typeof x === "string") x = [x];
|
|
2848
|
+
if (!Array.isArray(x)) return void 0;
|
|
2849
|
+
const out = x.filter((s) => typeof s === "string").map(cleanDocsText).filter(Boolean);
|
|
2850
|
+
return out.length ? out : void 0;
|
|
2851
|
+
};
|
|
2852
|
+
var guidanceList = (x, examples) => {
|
|
2853
|
+
if (!Array.isArray(x)) return void 0;
|
|
2854
|
+
const out = [];
|
|
2855
|
+
for (const g of x) {
|
|
2856
|
+
if (!g || typeof g !== "object") continue;
|
|
2857
|
+
const r = g;
|
|
2858
|
+
const title = typeof r.title === "string" ? cleanDocsText(r.title) : "";
|
|
2859
|
+
const text = typeof r.text === "string" ? cleanDocsText(r.text) : "";
|
|
2860
|
+
if (!title || !text) continue;
|
|
2861
|
+
out.push({ title, text, ...examples && r.example !== void 0 && isExample(r.example) ? { example: r.example } : {} });
|
|
2862
|
+
}
|
|
2863
|
+
return out.length ? out : void 0;
|
|
2864
|
+
};
|
|
2865
|
+
function sanitizeDocsSections(raw, keys = PROSE_KEYS, opts = {}) {
|
|
2866
|
+
const out = {};
|
|
2867
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out;
|
|
2868
|
+
const r = raw;
|
|
2869
|
+
for (const k of keys) {
|
|
2870
|
+
const v = r[k];
|
|
2871
|
+
if (k === "summary") {
|
|
2872
|
+
const s = typeof v === "string" ? cleanDocsText(v) : "";
|
|
2873
|
+
if (s) out.summary = s;
|
|
2874
|
+
} else if (k === "dos" || k === "donts") {
|
|
2875
|
+
const g = guidanceList(v, opts.examples ?? false);
|
|
2876
|
+
if (g) out[k] = g;
|
|
2877
|
+
} else {
|
|
2878
|
+
const l = textList(v);
|
|
2879
|
+
if (l) out[k] = l;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
return out;
|
|
2883
|
+
}
|
|
2884
|
+
function sanitizeDocsReplacement(raw) {
|
|
2885
|
+
if (typeof raw !== "string") return "";
|
|
2886
|
+
return cleanDocsText(raw.trim().replace(/^["'“]+|["'”]+$/g, ""));
|
|
2887
|
+
}
|
|
2888
|
+
function docsSectionsAnswer(task, raw) {
|
|
2889
|
+
const check = (task.check ?? []).filter((k) => task.keys.includes(k));
|
|
2890
|
+
const sections2 = check.length ? sanitizeDocsSections(raw, task.keys) : docsSectionsResult(task, raw);
|
|
2891
|
+
return { sections: sections2, checked: check.filter((k) => sections2[k] === void 0) };
|
|
2892
|
+
}
|
|
2893
|
+
function docsSectionsResult(task, raw) {
|
|
2894
|
+
const sections2 = sanitizeDocsSections(raw, task.keys);
|
|
2895
|
+
if (!Object.keys(sections2).length) throw new Error(`Claude's answer holds none of the sections asked for (${task.keys.join(", ")}). Try again.`);
|
|
2896
|
+
return sections2;
|
|
2897
|
+
}
|
|
2898
|
+
function mergeDocsDraft(prefillDraft, reply) {
|
|
2899
|
+
const empty = PROSE_KEYS.filter((k) => {
|
|
2900
|
+
const v = prefillDraft[k];
|
|
2901
|
+
return v === void 0 || (typeof v === "string" ? !v.trim() : Array.isArray(v) && !v.length);
|
|
2902
|
+
});
|
|
2903
|
+
const sections2 = sanitizeDocsSections(reply, empty, { examples: true });
|
|
2904
|
+
const merged = { ...prefillDraft, ...sections2 };
|
|
2905
|
+
if (reply.preview && !prefillDraft.preview && isExample(reply.preview)) merged.preview = reply.preview;
|
|
2906
|
+
merged.anatomy = prefillDraft.anatomy;
|
|
2907
|
+
const drafted = PROSE_KEYS.filter((k) => sections2[k] !== void 0);
|
|
2908
|
+
return markClaudeDraft(merged, drafted.length && drafted.length < PROSE_KEYS.length ? drafted : void 0);
|
|
2909
|
+
}
|
|
2910
|
+
function anthropicRequest(input) {
|
|
2911
|
+
const headers = { "x-api-key": input.apiKey, "anthropic-version": "2023-06-01", "content-type": "application/json" };
|
|
2912
|
+
if (input.fallback) headers["anthropic-beta"] = "server-side-fallback-2026-07-01";
|
|
2913
|
+
if (input.browser) headers["anthropic-dangerous-direct-browser-access"] = "true";
|
|
2914
|
+
const body = {
|
|
2915
|
+
model: input.model || DOCS_DRAFT_MODEL,
|
|
2916
|
+
max_tokens: input.maxTokens ?? 16e3,
|
|
2917
|
+
...input.fallback ? { fallbacks: "default" } : {},
|
|
2918
|
+
...input.system ? { system: input.system } : {},
|
|
2919
|
+
messages: [{ role: "user", content: input.prompt }]
|
|
2920
|
+
};
|
|
2921
|
+
return { url: ANTHROPIC_MESSAGES_URL, init: { method: "POST", headers, body: JSON.stringify(body) } };
|
|
2922
|
+
}
|
|
2923
|
+
function anthropicReplyText(msg) {
|
|
2924
|
+
const m = msg ?? {};
|
|
2925
|
+
if (m.stop_reason === "refusal") throw new Error("Claude declined to draft this one.");
|
|
2926
|
+
if (m.stop_reason === "max_tokens") throw new Error("The reply was cut off (max_tokens).");
|
|
2927
|
+
return (m.content ?? []).filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
|
|
2928
|
+
}
|
|
2484
2929
|
|
|
2485
2930
|
// ../core/src/explorations.ts
|
|
2486
2931
|
var EXPLORATION_LABEL = "exploration";
|
|
@@ -2535,16 +2980,6 @@ function explorationNote(e) {
|
|
|
2535
2980
|
""
|
|
2536
2981
|
].join("\n");
|
|
2537
2982
|
}
|
|
2538
|
-
function explorationInstall(input) {
|
|
2539
|
-
const packages = input.packages.map((name) => ({ name, url: snapshotUrl(input.owner, input.repo, name, input.pr) }));
|
|
2540
|
-
const named = (list) => installCommand(list.map((p) => `${p.name}@${p.url}`));
|
|
2541
|
-
const out = { command: named(packages), packages };
|
|
2542
|
-
if (input.sha) {
|
|
2543
|
-
const tarballs = input.packages.map((name) => ({ name, url: tarballUrl(input.owner, input.repo, input.pr, name, input.sha) }));
|
|
2544
|
-
out.fallback = { command: named(tarballs), packages: tarballs, release: `https://github.com/${input.owner}/${input.repo}/releases/tag/preview-pr-${input.pr}` };
|
|
2545
|
-
}
|
|
2546
|
-
return out;
|
|
2547
|
-
}
|
|
2548
2983
|
function explorationPrTitle(e) {
|
|
2549
2984
|
const n = e.name.trim().replace(/\.$/, "");
|
|
2550
2985
|
return `Exploration: ${n.charAt(0).toLowerCase()}${n.slice(1)}`;
|
|
@@ -2570,20 +3005,9 @@ function explorationPrBody(e, ctx) {
|
|
|
2570
3005
|
"### Try it in a product staging branch",
|
|
2571
3006
|
""
|
|
2572
3007
|
];
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
"In the product repo, on a branch that deploys to staging (production keeps the released version):",
|
|
2577
|
-
"",
|
|
2578
|
-
"```sh",
|
|
2579
|
-
install.command,
|
|
2580
|
-
"```",
|
|
2581
|
-
"",
|
|
2582
|
-
`The snapshots follow the latest commit of this pull request. The "Try this DS branch in your product" comment lists the exact commit, and the tarball URLs from the [\`preview-pr-${ctx.pr}\` prerelease](https://github.com/${ctx.owner}/${ctx.repo}/releases/tag/preview-pr-${ctx.pr}) when pkg.pr.new is not set up.`
|
|
2583
|
-
);
|
|
2584
|
-
} else {
|
|
2585
|
-
lines.push('The install command appears here and in the "Try this DS branch in your product" comment once the first snapshot is published.');
|
|
2586
|
-
}
|
|
3008
|
+
lines.push(
|
|
3009
|
+
'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.'
|
|
3010
|
+
);
|
|
2587
3011
|
lines.push("", "### Previews", "");
|
|
2588
3012
|
const links = previewLinks(ctx.apps, { branch, ...ctx.pr ? { pr: ctx.pr } : {} });
|
|
2589
3013
|
if (links.length) lines.push("| App | Preview |", "| --- | --- |", ...links.map((l) => `| ${l.label} | ${l.url} |`));
|
|
@@ -2595,7 +3019,7 @@ function explorationPrBody(e, ctx) {
|
|
|
2595
3019
|
`- **Promote:** in Bridge Studio (Changes, Explorations, Promote to review) or \`bridge explore promote ${slug2}\`. Bridge marks the pull request ready for review, removes the \`${EXPLORATION_LABEL}\` label and the exploration note, and adds the tier labels. Then it goes through the usual review.`,
|
|
2596
3020
|
"- **Close:** close this pull request. The branch stays for reference; the preview prerelease is removed.",
|
|
2597
3021
|
"",
|
|
2598
|
-
`<!-- ${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 } : {} }).replace(/-->/g, "--\\u003e")} -->`
|
|
3022
|
+
`<!-- ${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")} -->`
|
|
2599
3023
|
);
|
|
2600
3024
|
return lines.join("\n");
|
|
2601
3025
|
}
|
|
@@ -2652,6 +3076,13 @@ function promotionPlan(input) {
|
|
|
2652
3076
|
markReady: true
|
|
2653
3077
|
};
|
|
2654
3078
|
}
|
|
3079
|
+
function explorationTryIt(input) {
|
|
3080
|
+
const c = readPreviewComment(input.comment);
|
|
3081
|
+
if (!c) return { state: "building" };
|
|
3082
|
+
if (input.headSha && c.sha7 && !input.headSha.startsWith(c.sha7)) return { state: "building" };
|
|
3083
|
+
if (!c.command || c.source === "none") return { state: "failed" };
|
|
3084
|
+
return { state: "ready", command: c.command, source: c.source, sha7: c.sha7 };
|
|
3085
|
+
}
|
|
2655
3086
|
function explorationRules(branch) {
|
|
2656
3087
|
return [
|
|
2657
3088
|
`- Work on the existing branch ${branch} (git fetch origin && git switch ${branch}). Commit there.`,
|
|
@@ -2664,7 +3095,8 @@ function explorationRules(branch) {
|
|
|
2664
3095
|
var REQUEST_KINDS = [
|
|
2665
3096
|
{ value: "new-component", label: "New component", hint: "A component the design system does not have yet." },
|
|
2666
3097
|
{ value: "new-variant", label: "New variant", hint: "A size, tone or state the component does not have yet." },
|
|
2667
|
-
{ value: "prop-change", label: "
|
|
3098
|
+
{ value: "prop-change", label: "Option change (prop)", hint: "Add, rename or change an option (a prop in code) or its default." },
|
|
3099
|
+
{ value: "structure", label: "Structure change", hint: "A part was added, removed or moved." },
|
|
2668
3100
|
{ value: "visual", label: "Visual change", hint: "Spacing, radius, layout or icon placement." },
|
|
2669
3101
|
{ value: "token-binding", label: "Token change", hint: "The component should paint with another token." },
|
|
2670
3102
|
{ value: "bug", label: "Bug", hint: "It looks or behaves differently from the guidance." },
|
|
@@ -2686,6 +3118,7 @@ function composeRequestIssue(r, opts = {}) {
|
|
|
2686
3118
|
"",
|
|
2687
3119
|
r.summary.trim(),
|
|
2688
3120
|
"",
|
|
3121
|
+
...r.line?.trim() ? [`**What changes:** ${r.line.trim()}`, ""] : [],
|
|
2689
3122
|
...rows.length ? [...rows, ""] : [],
|
|
2690
3123
|
...r.images?.length ? [...r.images.map((u, i) => ``), ""] : [],
|
|
2691
3124
|
...figma ? [`**Design:** ${figma}`, ""] : [],
|
|
@@ -2733,6 +3166,7 @@ function requestBrief(r, ctx) {
|
|
|
2733
3166
|
`Component: ${r.component}`,
|
|
2734
3167
|
`Kind: ${kindLabel(r.kind)}`,
|
|
2735
3168
|
`Request: ${r.title}`,
|
|
3169
|
+
...r.line?.trim() ? [`What changes: ${r.line.trim()}`] : [],
|
|
2736
3170
|
"",
|
|
2737
3171
|
r.summary.trim(),
|
|
2738
3172
|
"",
|
|
@@ -2750,11 +3184,6 @@ function requestBrief(r, ctx) {
|
|
|
2750
3184
|
var cell2 = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
2751
3185
|
var sourceLabel = (s) => s === "figma" ? "Figma" : s === "studio" ? "Bridge Studio" : s === "cli" ? "the CLI" : "Claude (MCP)";
|
|
2752
3186
|
|
|
2753
|
-
// ../core/src/change-drafts.ts
|
|
2754
|
-
function cleanDraftText(text) {
|
|
2755
|
-
return text.replace(/\s*\u2014\s*/g, ", ").replace(/,\s*,/g, ",").replace(/^,\s*/, "").replace(/[ \t]+/g, " ").trim();
|
|
2756
|
-
}
|
|
2757
|
-
|
|
2758
3187
|
// ../core/src/draft-exchange.ts
|
|
2759
3188
|
var DRAFT_EXCHANGE_VERSION = 1;
|
|
2760
3189
|
var DRAFTS_DIR = ".bridge/drafts";
|
|
@@ -2768,12 +3197,16 @@ var DRAFT_VOICE_RULES = [
|
|
|
2768
3197
|
"Use only facts from the changes, the images and the designer's notes. Never invent numbers, names, usage or reasons. If the reason is unknown, say what changes and leave the reason out.",
|
|
2769
3198
|
"Keep the designer's intent from their notes.",
|
|
2770
3199
|
"Never use an em dash: use a colon, a comma or parentheses. No exclamation marks, no emoji, no Markdown.",
|
|
2771
|
-
"A request title never repeats the component name: the issue title already starts with it."
|
|
3200
|
+
"A request title never repeats the component name: the issue title already starts with it.",
|
|
3201
|
+
`For each change line, write one short sentence (at most ${DRAFT_LINE_MAX} characters, no trailing period) of what changed for developers, read from the before and after images: the part, the variant and the values when you can see them, for example "Icon added after the label in primary sm: Pencil, 16px". Never guess a value you cannot read.`,
|
|
3202
|
+
`For each change line, also write the title of the request it becomes: short, sentence case, starts with a verb, at most ${DRAFT_TITLE_MAX} characters, no trailing period, never the component name, for example "Add an icon after the label".`
|
|
2772
3203
|
];
|
|
2773
3204
|
var DRAFT_OUTPUT_CONTRACT = [
|
|
2774
|
-
`Call bridge_write_drafts once per session with {"sessionId": "...", "results": [{"id": "<item id>", "title": "...", "summary": "..."}]}, one result per item.`,
|
|
3205
|
+
`Call bridge_write_drafts once per session with {"sessionId": "...", "results": [{"id": "<item id>", "title": "...", "summary": "...", "lines": [{"id": "<line id>", "line": "...", "title": "..."}]}]}, one result per item.`,
|
|
2775
3206
|
`title: one line, at most ${DRAFT_TITLE_MAX} characters, no trailing period.`,
|
|
2776
3207
|
`summary: at most ${DRAFT_SUMMARY_SENTENCES} sentences, plain text.`,
|
|
3208
|
+
`lines: one per change line the item lists (its "lines"), by line id: line is one sentence, at most ${DRAFT_LINE_MAX} characters, no trailing period; title is the request title for that component change, at most ${DRAFT_TITLE_MAX} characters, no trailing period. Leave lines out when the item lists none.`,
|
|
3209
|
+
`Docs items (scope "docs", from Bridge Studio): follow the item's prompt and send {"id": "<item id>", "docs": {<only the sections it asks for>}} for a sections task (a section to check goes in only when something in it is no longer true for the code: leave accurate ones out, {"docs": {}} when all are), or {"id": "<item id>", "replacement": "..."} for a selection task. No title or summary. The designer reviews them on the Library page.`,
|
|
2777
3210
|
"The designer reviews the text in Figma before anything is sent."
|
|
2778
3211
|
];
|
|
2779
3212
|
var SESSION_RE = /^[a-z0-9-]{6,64}$/;
|
|
@@ -2793,7 +3226,7 @@ function validatePendingDrafts(x) {
|
|
|
2793
3226
|
if (!isDraftSessionId(p.sessionId)) out.push("The session id is missing or invalid.");
|
|
2794
3227
|
if (!str(p.createdAt) || Number.isNaN(Date.parse(p.createdAt))) out.push("createdAt must be an ISO date.");
|
|
2795
3228
|
if (p.status !== "pending" && p.status !== "drafted") out.push('status must be "pending" or "drafted".');
|
|
2796
|
-
if (p.source !== "figma") out.push('source must be "figma".');
|
|
3229
|
+
if (p.source !== "figma" && p.source !== "studio") out.push('source must be "figma" or "studio".');
|
|
2797
3230
|
if (!Array.isArray(p.items) || !p.items.length) out.push("A draft session needs at least one item.");
|
|
2798
3231
|
const ids = /* @__PURE__ */ new Set();
|
|
2799
3232
|
for (const [n, i] of (Array.isArray(p.items) ? p.items : []).entries()) {
|
|
@@ -2805,7 +3238,12 @@ function validatePendingDrafts(x) {
|
|
|
2805
3238
|
if (!str(i.id) || !i.id) out.push(`${at} has no id.`);
|
|
2806
3239
|
else if (ids.has(i.id)) out.push(`${at} repeats the id "${i.id}".`);
|
|
2807
3240
|
else ids.add(i.id);
|
|
2808
|
-
if (i.scope !== "request" && i.scope !== "push") out.push(`${at}: scope must be "request" or "
|
|
3241
|
+
if (i.scope !== "request" && i.scope !== "push" && i.scope !== "docs") out.push(`${at}: scope must be "request", "push" or "docs".`);
|
|
3242
|
+
if (i.scope === "docs") {
|
|
3243
|
+
const d = i.docs;
|
|
3244
|
+
if (!d || typeof d !== "object" || !str(d.slug) || !d.slug || !str(d.prompt) || !d.prompt || !optStr(d.path)) out.push(`${at}: a docs item needs docs {slug, task, prompt}.`);
|
|
3245
|
+
else out.push(...validateDocsTask(d.task).map((m) => `${at}: ${m}`));
|
|
3246
|
+
} else if (i.docs !== void 0) out.push(`${at}: only a docs item has docs.`);
|
|
2809
3247
|
if (!optStr(i.component)) out.push(`${at}: component must be text.`);
|
|
2810
3248
|
if (i.kind !== void 0 && !KINDS.has(i.kind)) out.push(`${at}: unknown kind "${String(i.kind)}".`);
|
|
2811
3249
|
if (!Array.isArray(i.changes) || i.changes.some((c) => !c || !str(c.field) || !optStr(c.before) || !optStr(c.after))) out.push(`${at}: changes must be a list of {field, before, after}.`);
|
|
@@ -2813,10 +3251,17 @@ function validatePendingDrafts(x) {
|
|
|
2813
3251
|
if (i.variables !== void 0 && i.scope !== "push") out.push(`${at}: only a push item has variables.`);
|
|
2814
3252
|
if (i.images !== void 0 && (!Array.isArray(i.images) || i.images.some((m) => !m || m.label !== "before" && m.label !== "after" || !str(m.url)))) out.push(`${at}: images must be a list of {label: before or after, url}.`);
|
|
2815
3253
|
if (!i.draft || !str(i.draft.title) || !str(i.draft.summary)) out.push(`${at}: draft must hold a title and a summary.`);
|
|
3254
|
+
if (i.lines !== void 0 && (!Array.isArray(i.lines) || i.lines.some((l) => !l || !str(l.id) || !l.id || !str(l.line) || !optStr(l.title) || !Array.isArray(l.changes)))) out.push(`${at}: lines must be a list of {id, changes, line, title}.`);
|
|
2816
3255
|
}
|
|
2817
3256
|
if (p.results !== void 0) {
|
|
2818
3257
|
if (!Array.isArray(p.results)) out.push("results must be a list.");
|
|
2819
|
-
else
|
|
3258
|
+
else
|
|
3259
|
+
for (const r of p.results) {
|
|
3260
|
+
if (!r || !str(r.id) || !str(r.title) || !str(r.summary)) out.push("Each result needs an id, a title and a summary.");
|
|
3261
|
+
else if (r.docs !== void 0 && (!r.docs || typeof r.docs !== "object" || Array.isArray(r.docs))) out.push("Result docs must be an object of sections.");
|
|
3262
|
+
else if (!optStr(r.replacement)) out.push("A result replacement must be text.");
|
|
3263
|
+
else if (r.lines !== void 0 && (!Array.isArray(r.lines) || r.lines.some((l) => !l || !str(l.id) || !str(l.line) || !optStr(l.title)))) out.push("Result lines must be a list of {id, line, title}.");
|
|
3264
|
+
}
|
|
2820
3265
|
}
|
|
2821
3266
|
return out;
|
|
2822
3267
|
}
|
|
@@ -2853,19 +3298,69 @@ function sanitizeDraftText(raw) {
|
|
|
2853
3298
|
const summary2 = typeof raw.summary === "string" ? sentences(cleanDraftText(raw.summary)) : "";
|
|
2854
3299
|
return { title, summary: summary2 };
|
|
2855
3300
|
}
|
|
3301
|
+
function sanitizeDocsResult(item, r) {
|
|
3302
|
+
const task = item.docs.task;
|
|
3303
|
+
if (task.kind === "selection") {
|
|
3304
|
+
const raw = r.replacement ?? (r.docs && typeof r.docs === "object" ? r.docs[task.key] : void 0);
|
|
3305
|
+
const replacement = sanitizeDocsReplacement(raw);
|
|
3306
|
+
if (!replacement) throw new Error(`Item "${item.id}" asks to rewrite a passage of ${task.key}: send {"id": "${item.id}", "replacement": "..."}.`);
|
|
3307
|
+
return { replacement };
|
|
3308
|
+
}
|
|
3309
|
+
try {
|
|
3310
|
+
const { sections: sections2, checked } = docsSectionsAnswer(task, r.docs ?? {});
|
|
3311
|
+
return { docs: sections2, checked };
|
|
3312
|
+
} catch {
|
|
3313
|
+
throw new Error(`Item "${item.id}" asks for the sections ${docsTaskKeys(task).join(", ")}: send {"id": "${item.id}", "docs": {${task.keys.map((k) => `"${k}": ...`).join(", ")}}} in the shapes of its prompt.`);
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
function sanitizeResultLines(item, raw) {
|
|
3317
|
+
if (raw === void 0 || raw === null) return [];
|
|
3318
|
+
if (!Array.isArray(raw)) throw new Error(`lines for "${item.id}" must be a list of {"id", "line", "title"}.`);
|
|
3319
|
+
const known = new Set((item.lines ?? []).map((l) => l.id));
|
|
3320
|
+
const out = /* @__PURE__ */ new Map();
|
|
3321
|
+
for (const l of raw) {
|
|
3322
|
+
const id = typeof l?.id === "string" ? l.id : "";
|
|
3323
|
+
if (!known.has(id)) throw new Error(`Item "${item.id}" has no change line "${id}".${known.size ? ` Use the line ids from bridge_pending_drafts: ${[...known].join(", ")}.` : " It lists no lines: leave lines out."}`);
|
|
3324
|
+
const line = sanitizeDraftLine(l.line);
|
|
3325
|
+
const title = sanitizeDraftTitle(l.title);
|
|
3326
|
+
if (line || title) out.set(id, { id, line, ...title ? { title } : {} });
|
|
3327
|
+
}
|
|
3328
|
+
return [...out.values()];
|
|
3329
|
+
}
|
|
3330
|
+
function sanitizeDraftTitle(raw) {
|
|
3331
|
+
if (typeof raw !== "string") return "";
|
|
3332
|
+
const t = clip(cleanDraftText(raw.replace(/[\r\n]+/g, " ")), DRAFT_TITLE_MAX);
|
|
3333
|
+
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
3334
|
+
}
|
|
2856
3335
|
function mergeDraftResults(p, results, at = /* @__PURE__ */ new Date()) {
|
|
2857
3336
|
const ids = new Set(p.items.map((i) => i.id));
|
|
2858
3337
|
const next = new Map((p.results ?? []).map((r) => [r.id, r]));
|
|
2859
3338
|
for (const r of results) {
|
|
2860
3339
|
if (!ids.has(r.id)) throw new Error(`Session ${p.sessionId} has no item "${r.id}". Use the ids from bridge_pending_drafts: ${[...ids].join(", ")}.`);
|
|
3340
|
+
const item = p.items.find((i) => i.id === r.id);
|
|
3341
|
+
if (item.scope === "docs") {
|
|
3342
|
+
next.set(r.id, { id: r.id, title: "", summary: "", ...sanitizeDocsResult(item, r), by: "claude", at: at.toISOString() });
|
|
3343
|
+
continue;
|
|
3344
|
+
}
|
|
2861
3345
|
const clean = sanitizeDraftText(r);
|
|
2862
|
-
|
|
2863
|
-
|
|
3346
|
+
const lines = sanitizeResultLines(item, r.lines);
|
|
3347
|
+
if (!clean.title && !clean.summary && !lines.length) throw new Error(`The result for "${r.id}" has no title and no summary, and no line.`);
|
|
3348
|
+
next.set(r.id, { id: r.id, title: clean.title, summary: clean.summary, ...lines.length ? { lines } : {}, by: "claude", at: at.toISOString() });
|
|
2864
3349
|
}
|
|
2865
3350
|
const merged = p.items.map((i) => next.get(i.id)).filter((r) => !!r);
|
|
2866
3351
|
return { ...p, results: merged, status: merged.length === p.items.length ? "drafted" : "pending" };
|
|
2867
3352
|
}
|
|
2868
3353
|
function draftItemBrief(i) {
|
|
3354
|
+
if (i.scope === "docs" && i.docs) {
|
|
3355
|
+
const t = i.docs.task;
|
|
3356
|
+
const check = t.kind === "sections" ? (t.check ?? []).filter((k) => t.keys.includes(k)) : [];
|
|
3357
|
+
const write = t.kind === "sections" ? t.keys.filter((k) => !check.includes(k)) : [];
|
|
3358
|
+
const what = t.kind === "selection" ? `rewrite one passage of the ${t.key} section (${t.mode}), answer with "replacement"` : [
|
|
3359
|
+
write.length ? `${t.rewrite ? "rewrite" : "write"} the sections ${write.join(", ")}` : "",
|
|
3360
|
+
check.length ? `check the sections ${check.join(", ")} against the code and return one only when something in it is no longer true (leave accurate ones out)` : ""
|
|
3361
|
+
].filter(Boolean).join("; ") + ', answer with "docs"';
|
|
3362
|
+
return [`Item ${i.id}: component docs${i.component ? ` of ${i.component}` : ""} from Bridge Studio (${i.docs.path ?? i.docs.slug}): ${what}.`, "", i.docs.prompt].join("\n");
|
|
3363
|
+
}
|
|
2869
3364
|
const kind = i.kind ? REQUEST_KINDS.find((k) => k.value === i.kind)?.label : void 0;
|
|
2870
3365
|
const cell3 = (s) => (s ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
2871
3366
|
const lines = [`Item ${i.id}: ${i.scope === "request" ? "a component change request (a GitHub issue developers implement)" : "the description of a pull request with design token (variable) edits from Figma"}.`];
|
|
@@ -2874,10 +3369,17 @@ function draftItemBrief(i) {
|
|
|
2874
3369
|
if (i.components?.length) lines.push(`Components with a change request: ${i.components.join(", ")}`);
|
|
2875
3370
|
if (i.changes.length) lines.push("", "| What | Now | In Figma |", "| --- | --- | --- |", ...i.changes.map((c) => `| ${cell3(c.field)} | ${cell3(c.before) || "none"} | ${cell3(c.after) || "removed"} |`));
|
|
2876
3371
|
if (i.variables?.length) lines.push("", "| Token | Now | In Figma |", "| --- | --- | --- |", ...i.variables.map((v) => `| ${cell3(v.path)} | ${cell3(v.before) || "none"} | ${cell3(v.after) || "removed"} |`));
|
|
2877
|
-
if (!i.changes.length && !i.variables?.length) lines.push("", "No change detected: rely on the notes and the images.");
|
|
3372
|
+
if (!i.changes.length && !i.variables?.length && !i.lines?.length) lines.push("", "No change detected: rely on the notes and the images.");
|
|
2878
3373
|
if (i.draft.title || i.draft.summary) lines.push("", "Starting draft, written from the changes only:", `Title: ${i.draft.title}`, `Summary: ${i.draft.summary}`);
|
|
2879
3374
|
if (i.notes?.trim()) lines.push("", "The designer's notes (keep their intent):", i.notes.trim());
|
|
2880
|
-
if (i.
|
|
3375
|
+
if (i.lines?.length) {
|
|
3376
|
+
lines.push("", `Change lines to write (one sentence each, at most ${DRAFT_LINE_MAX} characters, from the before and after images), each with the title of its request (at most ${DRAFT_TITLE_MAX} characters):`);
|
|
3377
|
+
for (const l of i.lines) {
|
|
3378
|
+
lines.push(`- Line ${l.id}${l.component ? `, ${l.component}` : ""}${l.kind ? ` (${REQUEST_KINDS.find((k) => k.value === l.kind)?.label ?? l.kind})` : ""}. Starting line: ${l.line || "(none)"}${l.title ? `. Starting title: ${l.title}` : ""}`);
|
|
3379
|
+
for (const c of l.changes) lines.push(` - ${c.field}: ${c.before || "none"} -> ${c.after || "removed"}`);
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
if (i.images?.length) lines.push("", `Images: ${i.images.map((m) => `${m.label}${m.line ? ` (line ${m.line})` : ""} ${m.url}`).join(", ")}`);
|
|
2881
3383
|
return lines.join("\n");
|
|
2882
3384
|
}
|
|
2883
3385
|
var isConflict = (err) => err instanceof GitHubError ? err.status === 409 || err.status === 422 && /sha/i.test(`${err.detail ?? ""} ${err.message}`) : err?.status === 409;
|
|
@@ -2920,9 +3422,10 @@ function writeDraftResults(client2, sessionId, results, opts = {}) {
|
|
|
2920
3422
|
message: `Bridge drafts: ${results.length === 1 ? "1 text" : `${results.length} texts`} drafted by Claude`
|
|
2921
3423
|
});
|
|
2922
3424
|
}
|
|
3425
|
+
var DRAFT_POLL_TIMEOUT_MS = 10 * 6e4;
|
|
2923
3426
|
|
|
2924
3427
|
// ../core/src/explorations-github.ts
|
|
2925
|
-
function explorationSummary(p,
|
|
3428
|
+
function explorationSummary(p, _owner, _repo, s) {
|
|
2926
3429
|
const e = parseExplorationBody(p.body);
|
|
2927
3430
|
const slug2 = p.headRef?.startsWith(EXPLORATION_PREFIX) ? p.headRef.slice(EXPLORATION_PREFIX.length) : e?.slug ?? explorationSlug(p.title);
|
|
2928
3431
|
return {
|
|
@@ -2936,9 +3439,11 @@ function explorationSummary(p, owner, repo, s) {
|
|
|
2936
3439
|
status: explorationStatus({ state: p.state, labels: p.labels, headRef: p.headRef, body: p.body }),
|
|
2937
3440
|
draft: p.draft,
|
|
2938
3441
|
author: e?.author ?? p.author,
|
|
3442
|
+
...e?.source ? { source: e.source } : {},
|
|
3443
|
+
...e?.agent ? { agent: e.agent } : {},
|
|
2939
3444
|
updatedAt: p.updatedAt,
|
|
2940
|
-
|
|
2941
|
-
|
|
3445
|
+
headSha: p.headSha,
|
|
3446
|
+
previews: previewLinks(s.apps, { branch: p.headRef, pr: p.number, sha: p.headSha })
|
|
2942
3447
|
};
|
|
2943
3448
|
}
|
|
2944
3449
|
async function listExplorationsOnGitHub(gh, s, opts = {}) {
|
|
@@ -2964,7 +3469,7 @@ ${full.purpose.trim()}
|
|
|
2964
3469
|
Exploration: never merged as is. Promote it to review or close it.`,
|
|
2965
3470
|
files: [{ path: explorationNotePath(slug2), content: explorationNote(full) }]
|
|
2966
3471
|
});
|
|
2967
|
-
const ctx = { owner: gh.owner, repo: gh.name, packages: s.packages, apps: s.apps };
|
|
3472
|
+
const ctx = { owner: gh.owner, repo: gh.name, packages: s.packages, apps: s.apps, ...s.requests?.length ? { requests: s.requests } : {} };
|
|
2968
3473
|
const pr = await x.createPullRequest({ title: explorationPrTitle(full), head: branch, base: s.base, body: explorationPrBody(full, ctx), draft: true });
|
|
2969
3474
|
await x.addLabels(pr.number, [EXPLORATION_LABEL]);
|
|
2970
3475
|
await x.updatePullRequest(pr.number, { body: explorationPrBody(full, { ...ctx, pr: pr.number }) }).catch(() => void 0);
|
|
@@ -2974,10 +3479,16 @@ Exploration: never merged as is. Promote it to review or close it.`,
|
|
|
2974
3479
|
pr: pr.number,
|
|
2975
3480
|
url: pr.url,
|
|
2976
3481
|
commit: commit.sha,
|
|
2977
|
-
|
|
3482
|
+
tryIt: { state: "building" },
|
|
2978
3483
|
previews: previewLinks(s.apps, { branch, pr: pr.number, sha: commit.sha })
|
|
2979
3484
|
};
|
|
2980
3485
|
}
|
|
3486
|
+
async function explorationTestBuildOnGitHub(gh, e, config) {
|
|
3487
|
+
const [comments, head] = await Promise.all([gh.listIssueComments(e.number), gh.getBranchSha(e.branch).catch(() => void 0)]);
|
|
3488
|
+
const tryIt = explorationTryIt({ comment: findPreviewComment(comments)?.body, headSha: head });
|
|
3489
|
+
const previews = await branchPreviewUrls(gh, e.number, config, { branch: e.branch, sha: head, comments }).catch(() => []);
|
|
3490
|
+
return { tryIt, previews, settled: tryIt.state !== "building" && previewsSettled(previews) };
|
|
3491
|
+
}
|
|
2981
3492
|
async function findExplorationOnGitHub(gh, ref) {
|
|
2982
3493
|
if (typeof ref === "number" || /^#?\d+$/.test(String(ref))) {
|
|
2983
3494
|
const p = await gh.getPullRequest(Number(String(ref).replace(/^#/, "")));
|
|
@@ -3001,6 +3512,7 @@ async function promoteExplorationOnGitHub(gh, ref, opts = {}) {
|
|
|
3001
3512
|
const x = gh.withBranchPrefix(EXPLORATION_PREFIX);
|
|
3002
3513
|
if (plan.removeFiles.length) {
|
|
3003
3514
|
await x.commitToBranch({ branch: p.headRef, message: "chore(explore): promote to review, remove the exploration note", files: plan.removeFiles.map((path) => ({ path, content: null })) }).catch((e) => {
|
|
3515
|
+
if (e instanceof GitHubError && e.auth === "rejected") throw e;
|
|
3004
3516
|
out.warnings.push(`The exploration note stays on the branch (${e.message}). Delete ${plan.removeFiles.join(", ")} before merging.`);
|
|
3005
3517
|
});
|
|
3006
3518
|
}
|
|
@@ -4632,7 +5144,9 @@ async function detectConfig(files, read, extra = {}) {
|
|
|
4632
5144
|
branchPrefix: "bridge/",
|
|
4633
5145
|
labels: ["source: mcp"],
|
|
4634
5146
|
guardrails: [guidelines, "CLAUDE.md", ".github/", governance, contrast, CONFIG_FILE].filter(Boolean),
|
|
4635
|
-
owners: { design: [], tech: [] }
|
|
5147
|
+
owners: { design: [], tech: [] },
|
|
5148
|
+
// New configs start from the default roles: design owners approve design, tech owners approve tech and ship.
|
|
5149
|
+
rights: DEFAULT_RIGHTS
|
|
4636
5150
|
}
|
|
4637
5151
|
});
|
|
4638
5152
|
}
|
|
@@ -5187,6 +5701,7 @@ __export(ops_exports, {
|
|
|
5187
5701
|
normalizeTokenPath: () => normalizeTokenPath,
|
|
5188
5702
|
proposeTokenChange: () => proposeTokenChange,
|
|
5189
5703
|
reviewPr: () => reviewPr,
|
|
5704
|
+
reviewRoles: () => reviewRoles,
|
|
5190
5705
|
scaffoldComponentOp: () => scaffoldComponentOp,
|
|
5191
5706
|
searchTokens: () => searchTokens,
|
|
5192
5707
|
validateTokens: () => validateTokens,
|
|
@@ -5324,6 +5839,7 @@ __export(explorations_exports, {
|
|
|
5324
5839
|
closeExploration: () => closeExploration,
|
|
5325
5840
|
explorationContext: () => explorationContext,
|
|
5326
5841
|
explorationRules: () => explorationRules,
|
|
5842
|
+
installOf: () => installOf,
|
|
5327
5843
|
listExplorations: () => listExplorations,
|
|
5328
5844
|
promoteExploration: () => promoteExploration,
|
|
5329
5845
|
startExploration: () => startExploration,
|
|
@@ -5361,6 +5877,8 @@ async function startExploration(ctx, a) {
|
|
|
5361
5877
|
...a.useCase ? { useCase: a.useCase } : {},
|
|
5362
5878
|
...a.experimental?.length ? { experimental: a.experimental } : {},
|
|
5363
5879
|
...a.author ? { author: a.author } : {},
|
|
5880
|
+
...a.source ? { source: a.source } : {},
|
|
5881
|
+
...a.agent ? { agent: a.agent } : {},
|
|
5364
5882
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5365
5883
|
};
|
|
5366
5884
|
const gh = await ctx.github().catch(() => null);
|
|
@@ -5377,7 +5895,16 @@ async function startExploration(ctx, a) {
|
|
|
5377
5895
|
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.`;
|
|
5378
5896
|
if (gh && ctx.canWriteRemote) {
|
|
5379
5897
|
const r = await startExplorationOnGitHub(gh, e, s);
|
|
5380
|
-
return {
|
|
5898
|
+
return {
|
|
5899
|
+
status: "started",
|
|
5900
|
+
...preview,
|
|
5901
|
+
pr: r.pr,
|
|
5902
|
+
url: r.url,
|
|
5903
|
+
commit: r.commit,
|
|
5904
|
+
install: installOf(r.tryIt),
|
|
5905
|
+
previews: r.previews,
|
|
5906
|
+
next: `${work} The install command and the preview links appear on the pull request a few minutes after each push: \`bridge explore list\` reads them.`
|
|
5907
|
+
};
|
|
5381
5908
|
}
|
|
5382
5909
|
if (ctx.mode === "local" && ctx.isGit) {
|
|
5383
5910
|
if (await branchExists(ctx.root, branch)) throw new Error(`Branch ${branch} already exists here. Pick another name, or continue it with \`git switch ${branch}\`.`);
|
|
@@ -5404,6 +5931,11 @@ Exploration: never merged as is. Promote it to review or close it.`,
|
|
|
5404
5931
|
}
|
|
5405
5932
|
throw new Error("Starting an exploration needs GITHUB_TOKEN (contents and pull requests: write), or a local git repo. Without them Bridge is read-only. Use dryRun to preview it.");
|
|
5406
5933
|
}
|
|
5934
|
+
function installOf(t) {
|
|
5935
|
+
if (t.state === "ready") return { state: "ready", command: t.command, note: `Snapshot of ${t.sha7 ?? "the latest commit"}, ${t.source === "release" ? "tarballs from the preview prerelease" : t.source === "pkg.pr.new" ? "pkg.pr.new" : "from the preview comment"}. Install it in a product branch that deploys to staging.` };
|
|
5936
|
+
if (t.state === "failed") return { state: "failed", command: null, note: "The test build did not finish for the latest commit. The Preview packages run on the pull request says why." };
|
|
5937
|
+
return { state: "building", command: null, note: 'The test build is being prepared, a few minutes after each push. The command then appears in the "Try this exploration in your product" comment.' };
|
|
5938
|
+
}
|
|
5407
5939
|
async function requestsByExploration(gh) {
|
|
5408
5940
|
const out = /* @__PURE__ */ new Map();
|
|
5409
5941
|
try {
|
|
@@ -5420,7 +5952,15 @@ async function listExplorations(ctx, a = {}) {
|
|
|
5420
5952
|
const { config } = await ctx.config();
|
|
5421
5953
|
const gh = await requireGitHub(ctx);
|
|
5422
5954
|
const [list, linked] = await Promise.all([listExplorationsOnGitHub(gh, settingsOf(config), { state: a.state ?? "open" }), requestsByExploration(gh)]);
|
|
5423
|
-
const explorations =
|
|
5955
|
+
const explorations = await Promise.all(
|
|
5956
|
+
list.map(async (x) => {
|
|
5957
|
+
const requests = linked.get(x.branch) ?? [];
|
|
5958
|
+
if (x.status !== "active") return { ...x, install: null, requests };
|
|
5959
|
+
const t = await explorationTestBuildOnGitHub(gh, { number: x.number, branch: x.branch }, config).catch(() => null);
|
|
5960
|
+
if (!t) return { ...x, install: installOf({ state: "building" }), requests };
|
|
5961
|
+
return { ...x, previews: t.previews.map((p) => ({ app: p.app, label: p.label, url: p.url, state: p.state })), install: installOf(t.tryIt), requests };
|
|
5962
|
+
})
|
|
5963
|
+
);
|
|
5424
5964
|
let localBranches = [];
|
|
5425
5965
|
if (ctx.mode === "local" && ctx.isGit) {
|
|
5426
5966
|
const refs = (await git(ctx.root, ["for-each-ref", "--format=%(refname:short)", `refs/heads/${EXPLORATION_PREFIX}`]).catch(() => "")).split("\n").filter(Boolean);
|
|
@@ -5962,6 +6502,7 @@ async function getReview(ctx, a) {
|
|
|
5962
6502
|
approvals: [...latest.entries()].filter(([, s]) => s === "APPROVED").map(([u]) => u),
|
|
5963
6503
|
changesRequested: [...latest.entries()].filter(([, s]) => s === "CHANGES_REQUESTED").map(([u]) => u)
|
|
5964
6504
|
};
|
|
6505
|
+
meta.roles = await reviewRoles(gh, config, meta.approvals);
|
|
5965
6506
|
}
|
|
5966
6507
|
const [before, after] = await Promise.all([setAt2(baseSrc, config), setAt2(headSrc, config)]);
|
|
5967
6508
|
const changes = core.diff(before, after);
|
|
@@ -5989,6 +6530,25 @@ async function getReview(ctx, a) {
|
|
|
5989
6530
|
impact: imp
|
|
5990
6531
|
};
|
|
5991
6532
|
}
|
|
6533
|
+
var sameLogin = (a, b) => a.trim().toLowerCase().replace(/^@/, "") === b.trim().toLowerCase().replace(/^@/, "");
|
|
6534
|
+
async function reviewRoles(gh, config, approvers) {
|
|
6535
|
+
const { owners, rights } = config.governance;
|
|
6536
|
+
const teams = await new TeamDirectory(gh).resolve(owners);
|
|
6537
|
+
const entries = approverEntries(owners, rights);
|
|
6538
|
+
const lists = expandOwners(entries, teams);
|
|
6539
|
+
const role = (k) => ({
|
|
6540
|
+
owners: entries[k].map((e) => entryLabel(e, teams)),
|
|
6541
|
+
approvedBy: approvers.filter((a) => lists[k].some((x) => sameLogin(x, a)))
|
|
6542
|
+
});
|
|
6543
|
+
const ship = shipPolicy(owners, rights, teams);
|
|
6544
|
+
const unreadable = unreadableTeams(teams);
|
|
6545
|
+
return {
|
|
6546
|
+
design: role("design"),
|
|
6547
|
+
tech: role("tech"),
|
|
6548
|
+
ship: ship ? whoCanShipSentence(ship) : "Anyone with write access can ship.",
|
|
6549
|
+
...unreadable.length ? { unreadableTeams: unreadable.map((t) => t.message) } : {}
|
|
6550
|
+
};
|
|
6551
|
+
}
|
|
5992
6552
|
async function reviewPr(ctx, a) {
|
|
5993
6553
|
const gh = await ctx.github();
|
|
5994
6554
|
if (!gh) throw new Error('No GitHub repo known: set "repo" in bridge.config.json.');
|
|
@@ -6025,7 +6585,7 @@ async function visualImpact(ctx, a, run2) {
|
|
|
6025
6585
|
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.");
|
|
6026
6586
|
if (!ctx.isGit) throw new Error(`${ctx.root} is not a git repository.`);
|
|
6027
6587
|
const out = a.out ? resolve3(ctx.root, a.out) : join4(ctx.root, "visual-impact");
|
|
6028
|
-
const runImpact = run2 ?? (await import("./src-
|
|
6588
|
+
const runImpact = run2 ?? (await import("./src-XSBCZV4W.js")).runImpact;
|
|
6029
6589
|
const r = await runImpact({
|
|
6030
6590
|
root: ctx.root,
|
|
6031
6591
|
base: a.base ?? "main",
|
|
@@ -6620,7 +7180,7 @@ async function pendingDrafts(ctx, a) {
|
|
|
6620
7180
|
for (const img of item.images ?? []) {
|
|
6621
7181
|
if (images.length >= IMAGES_PER_CALL || !img.path) continue;
|
|
6622
7182
|
const f = await gh.getFileBase64(img.path, REQUESTS_BRANCH).catch(() => void 0);
|
|
6623
|
-
if (f && f.size <= IMAGE_INLINE_MAX) images.push({ sessionId: s.sessionId, itemId: item.id, label: img.label, base64: f.base64 });
|
|
7183
|
+
if (f && f.size <= IMAGE_INLINE_MAX) images.push({ sessionId: s.sessionId, itemId: item.id, label: img.label, ...img.line ? { line: img.line } : {}, base64: f.base64 });
|
|
6624
7184
|
}
|
|
6625
7185
|
}
|
|
6626
7186
|
}
|
|
@@ -6632,12 +7192,12 @@ async function pendingDrafts(ctx, a) {
|
|
|
6632
7192
|
sessions: sessions.map((s) => ({ sessionId: s.sessionId, createdAt: s.createdAt, status: s.status, items: s.items, briefs: s.items.map(draftItemBrief) })),
|
|
6633
7193
|
voice: DRAFT_VOICE_RULES,
|
|
6634
7194
|
contract: DRAFT_OUTPUT_CONTRACT,
|
|
6635
|
-
next: sessions.length ? `Write a title and a summary for each item, then call bridge_write_drafts with the sessionId and one result per item id. The designer reviews them in Figma.` : `Nothing is waiting. In the DS Bridge Figma plugin, click "Draft with my Claude" on a request or on the Push description, then say "${DRAFT_PHRASE}" again.`
|
|
7195
|
+
next: sessions.length ? sessions.some((s) => s.items.some((i) => i.scope === "docs")) ? `For each change item write a title, a summary and one line per change line; for each docs item follow its brief and send "docs" (the sections it asks for) or "replacement" (a selected passage). Then call bridge_write_drafts with the sessionId and one result per item id. The designer reviews them in Figma or on the Library page.` : `Write a title and a summary for each item, and one line per change line it lists, then call bridge_write_drafts with the sessionId and one result per item id. The designer reviews them in Figma.` : `Nothing is waiting. In the DS Bridge Figma plugin, click "Draft with my Claude" on a request or on the Push description, or pick "My Claude" when drafting docs on a Library page, then say "${DRAFT_PHRASE}" again.`
|
|
6636
7196
|
};
|
|
6637
7197
|
return { answer, images };
|
|
6638
7198
|
}
|
|
6639
7199
|
async function writeDrafts(ctx, a) {
|
|
6640
|
-
if (!a.results.length) throw new Error('Send at least one result: {"id", "title", "summary"} per item.');
|
|
7200
|
+
if (!a.results.length) throw new Error('Send at least one result: {"id", "title", "summary"} per change item, {"id", "docs"} or {"id", "replacement"} per docs item.');
|
|
6641
7201
|
const gh = await requireGitHub3(ctx);
|
|
6642
7202
|
if (!ctx.canWriteRemote) throw new Error("Writing drafts needs GITHUB_TOKEN (contents: read and write on this repository), the same token as the Figma plugin. Without it Bridge is read-only.");
|
|
6643
7203
|
const out = await writeDraftResults(gh, a.sessionId, a.results);
|
|
@@ -6647,12 +7207,12 @@ async function writeDrafts(ctx, a) {
|
|
|
6647
7207
|
status: out.status,
|
|
6648
7208
|
results: out.results ?? [],
|
|
6649
7209
|
...waiting.length ? { waiting } : {},
|
|
6650
|
-
next: waiting.length ? `Still waiting for ${waiting.join(", ")}: call bridge_write_drafts again with those ids.` : 'Done. The text shows up in the Figma plugin within a few seconds, marked "Drafted by Claude, review before sending". Tell the designer to review it there.'
|
|
7210
|
+
next: waiting.length ? `Still waiting for ${waiting.join(", ")}: call bridge_write_drafts again with those ids.` : out.source === "studio" ? 'Done. The docs show up on the Library page within a few seconds, marked "Drafted by Claude". Tell the designer to review them there before proposing.' : 'Done. The text shows up in the Figma plugin within a few seconds, marked "Drafted by Claude, review before sending". Tell the designer to review it there.'
|
|
6651
7211
|
};
|
|
6652
7212
|
}
|
|
6653
7213
|
|
|
6654
7214
|
// src/server.ts
|
|
6655
|
-
var VERSION = "0.1
|
|
7215
|
+
var VERSION = true ? "0.3.1" : "dev";
|
|
6656
7216
|
var ok = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
|
|
6657
7217
|
var fail = (e) => ({ content: [{ type: "text", text: `Error: ${e.message ?? String(e)}` }], isError: true });
|
|
6658
7218
|
var run = (fn) => async (a) => {
|
|
@@ -6680,6 +7240,7 @@ function createServer(ctx) {
|
|
|
6680
7240
|
{
|
|
6681
7241
|
instructions: [
|
|
6682
7242
|
"Bridge exposes a design system repo (DTCG tokens, components, docs) to you.",
|
|
7243
|
+
"Every write goes through a branch and a pull request, never straight to the default branch: the library only changes through reviewed pull requests, and on a protected repo GitHub refuses direct pushes anyway.",
|
|
6683
7244
|
"Read before you write: bridge_get_guidelines for the repo rules, bridge_search_tokens / bridge_get_token before using or proposing a token.",
|
|
6684
7245
|
"Never hardcode a value in UI code when a token exists; never invent a token name (check it with bridge_get_token).",
|
|
6685
7246
|
"bridge_audit measures how a product codebase adopts the design system (hardcoded values, foreign UI components) and suggests the tokens and components to use.",
|
|
@@ -6688,7 +7249,7 @@ function createServer(ctx) {
|
|
|
6688
7249
|
"New brands: bridge_plan_brand first (dry run with ramps and the contrast gate for every theme), then bridge_propose_brand. A new brand is Tier 3.",
|
|
6689
7250
|
"To undo a merged token change use bridge_plan_revert first (it shows conflicts with later changes), then bridge_propose_revert; never revert by hand-editing files.",
|
|
6690
7251
|
'Component changes (new component, variant, prop, structure, visual, token binding) are requests: GitHub issues labeled component-change. bridge_list_requests finds them, bridge_get_request returns a brief to implement one in a pull request that says "Closes #n", bridge_create_request files a new one.',
|
|
6691
|
-
`Change texts from Figma: when the user says "${DRAFT_PHRASE}" (or asks you to draft their Bridge changes), call bridge_pending_drafts, look at the before and after images
|
|
7252
|
+
`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.`,
|
|
6692
7253
|
'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.'
|
|
6693
7254
|
].join(" ")
|
|
6694
7255
|
}
|
|
@@ -6881,7 +7442,7 @@ function createServer(ctx) {
|
|
|
6881
7442
|
"bridge_get_review",
|
|
6882
7443
|
{
|
|
6883
7444
|
title: "Get review",
|
|
6884
|
-
description: "Review a token change: designer-readable diff table, contrast before -> after, new validation errors, downstream impact, computed risk tier (and a warning if labelled lower), and for PRs the CI status and
|
|
7445
|
+
description: "Review a token change: designer-readable diff table, contrast before -> after, new validation errors, downstream impact, computed risk tier (and a warning if labelled lower), and for PRs the CI status, approvals and roles: who may approve design, approve tech and ship, with GitHub teams named and their size, and any team that cannot be read with the fix. Pass `pr` (GitHub) or `branch` (local proposal branch).",
|
|
6885
7446
|
inputSchema: { pr: z.number().int().positive().optional(), branch: z.string().optional() },
|
|
6886
7447
|
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
6887
7448
|
},
|
|
@@ -7074,9 +7635,11 @@ function createServer(ctx) {
|
|
|
7074
7635
|
title: "Pending change texts from Figma",
|
|
7075
7636
|
description: [
|
|
7076
7637
|
`Change texts a designer asked their own Claude to write from the DS Bridge Figma plugin ("Draft with my Claude"), waiting on the bridge-requests branch. Call it when the user says "${DRAFT_PHRASE}".`,
|
|
7077
|
-
"Returns the pending sessions (newest first) with each item: scope (request: a component change request; push: a pull request description of token edits), component, kind, the change table, token edits, the designer's notes, the deterministic starting draft, a brief, and the before and after image links; the images themselves follow as image content when small enough.",
|
|
7638
|
+
"Returns the pending sessions (newest first) with each item: scope (request: a component change request; push: a pull request description of token edits; docs: component docs sections from Bridge Studio, with a brief holding the full prompt), component, kind, the change table, token edits, the designer's notes, the deterministic starting draft, the change lines to write (one per component change, with an id, its changes and a starting line), a brief, and the before and after image links; the images themselves follow as image content when small enough.",
|
|
7078
7639
|
"Look at the before and after images, then write what changes for developers and why it matters for people who use the component. Sentence case, start the title with a verb, no em dash, no invented numbers, names or reasons: if the reason is unknown, leave it out.",
|
|
7079
|
-
`
|
|
7640
|
+
`Write one short sentence per change line from the images: what changed for developers (the part, the variant, the values you can read), for example "Icon added after the label in primary sm: Pencil, 16px", and the title of the request it becomes, for example "Add an icon after the label".`,
|
|
7641
|
+
`Then call bridge_write_drafts with one result per item id: title at most ${DRAFT_TITLE_MAX} characters, summary at most 3 sentences, lines (one per change line id) with a line of at most ${DRAFT_LINE_MAX} characters and a title of at most ${DRAFT_TITLE_MAX}.`,
|
|
7642
|
+
`Docs items: read the brief. Sections to check (the code changed after they were written) go in the answer ONLY when something in them is no longer true for the code (a wrong or removed prop, a removed state, changed behavior), fixed with the writer's wording kept elsewhere; leave accurate ones out (a missing key means unchanged, "docs": {} when all are accurate). Otherwise write only the sections its task names (summary a string; whenToUse, whenNotToUse, accessibility, contentGuidelines lists of strings; dos and donts lists of {"title", "text"}) from the facts it quotes, never a prop, variant or keyboard behavior that is not in the code, never anatomy; answer with "docs": {sections}. A selection task rewrites one passage: answer with "replacement": "the passage". No title or summary for docs items.`
|
|
7080
7643
|
].join(" "),
|
|
7081
7644
|
inputSchema: {
|
|
7082
7645
|
sessionId: z.string().optional().describe("One session only. Default: every pending session."),
|
|
@@ -7091,7 +7654,7 @@ function createServer(ctx) {
|
|
|
7091
7654
|
content: [
|
|
7092
7655
|
{ type: "text", text: JSON.stringify(answer, null, 2) },
|
|
7093
7656
|
...images.flatMap((img) => [
|
|
7094
|
-
{ type: "text", text: `${img.label === "before" ? "Before (baseline)" : "After (now in Figma)"}: session ${img.sessionId}, item ${img.itemId}` },
|
|
7657
|
+
{ type: "text", text: `${img.label === "before" ? "Before (baseline)" : "After (now in Figma)"}: session ${img.sessionId}, item ${img.itemId}${img.line ? `, line ${img.line}` : ""}` },
|
|
7095
7658
|
{ type: "image", data: img.base64, mimeType: "image/png" }
|
|
7096
7659
|
])
|
|
7097
7660
|
]
|
|
@@ -7105,14 +7668,23 @@ function createServer(ctx) {
|
|
|
7105
7668
|
"bridge_write_drafts",
|
|
7106
7669
|
{
|
|
7107
7670
|
title: "Write change texts for Figma",
|
|
7108
|
-
description:
|
|
7671
|
+
description: `Write the titles, summaries and change lines (or, for docs items, the docs sections or the rewritten passage) you drafted for a session from bridge_pending_drafts. Docs results show up on the Library page marked "Drafted by Claude" for review, cleaned to the docs.json shapes (no em dash; only the sections the item asks for). The DS Bridge Figma plugin fills change texts in within seconds, marked "Drafted by Claude, review before sending"; it never overwrites text the designer typed meanwhile. Each change line becomes the main line of its component row and the first line of its request; its title becomes that request's title. Texts are cleaned to the contract (no em dash, titles at most ${DRAFT_TITLE_MAX} characters without a trailing period, summary at most 3 sentences, each line at most ${DRAFT_LINE_MAX} characters). Results without lines, or lines without a title, still work. Needs GITHUB_TOKEN with contents write. Writes only the session file on the bridge-requests branch, never main.`,
|
|
7109
7672
|
inputSchema: {
|
|
7110
7673
|
sessionId: z.string().min(1).describe("The sessionId from bridge_pending_drafts."),
|
|
7111
7674
|
results: z.array(
|
|
7112
7675
|
z.object({
|
|
7113
7676
|
id: z.string().min(1).describe("The item id from bridge_pending_drafts."),
|
|
7114
|
-
title: z.string().describe(`
|
|
7115
|
-
summary: z.string().describe("
|
|
7677
|
+
title: z.string().optional().describe(`Change items: one line, sentence case, starts with a verb, at most ${DRAFT_TITLE_MAX} characters, no trailing period. A request title never repeats the component name. Leave it out for docs items.`),
|
|
7678
|
+
summary: z.string().optional().describe("Change items: at most 3 sentences, what changes for developers and why it matters. No em dash, no invented numbers. Leave it out for docs items."),
|
|
7679
|
+
docs: z.record(z.string(), z.unknown()).optional().describe('Docs items with a sections task: only the sections the brief asks for (a section to check only when it is no longer true for the code; leave accurate ones out), in docs.json shapes: summary a string; whenToUse, whenNotToUse, accessibility, contentGuidelines lists of strings; dos and donts lists of {"title", "text"}.'),
|
|
7680
|
+
replacement: z.string().optional().describe("Docs items with a selection task: the rewritten passage only, plain text, Bridge voice, no em dash."),
|
|
7681
|
+
lines: z.array(
|
|
7682
|
+
z.object({
|
|
7683
|
+
id: z.string().min(1).describe(`A change line id from the item's "lines" in bridge_pending_drafts.`),
|
|
7684
|
+
line: z.string().describe(`One sentence, at most ${DRAFT_LINE_MAX} characters, no trailing period: what changed for developers, read from the before and after images ("Icon added after the label in primary sm: Pencil, 16px").`),
|
|
7685
|
+
title: z.string().optional().describe(`The title of the request this component change becomes: short, sentence case, starts with a verb, at most ${DRAFT_TITLE_MAX} characters, no trailing period, never the component name ("Add an icon after the label").`)
|
|
7686
|
+
})
|
|
7687
|
+
).optional().describe("One per change line the item lists. Leave it out when the item lists none.")
|
|
7116
7688
|
})
|
|
7117
7689
|
).min(1).describe("One result per item.")
|
|
7118
7690
|
},
|
|
@@ -7134,13 +7706,13 @@ function createServer(ctx) {
|
|
|
7134
7706
|
},
|
|
7135
7707
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
7136
7708
|
},
|
|
7137
|
-
run((a) => startExploration(ctx, a))
|
|
7709
|
+
run((a) => startExploration(ctx, { ...a, source: "mcp" }))
|
|
7138
7710
|
);
|
|
7139
7711
|
server.registerTool(
|
|
7140
7712
|
"bridge_list_explorations",
|
|
7141
7713
|
{
|
|
7142
7714
|
title: "List explorations",
|
|
7143
|
-
description: "Explorations of the repo (draft pull requests labeled exploration on explore/* branches): name, purpose, status (active: running; promoted: ready for review; closed), author, last update, preview links
|
|
7715
|
+
description: "Explorations of the repo (draft pull requests labeled exploration on explore/* branches): name, purpose, status (active: running; promoted: ready for review; closed), author, last update, for running ones the preview links Vercel reports (url null while it has not reported one yet) and the install command a product staging branch uses, read from the preview comment on the pull request (state building until the snapshot of the latest commit is published, never a guessed command), and the component requests implemented on each. Locally, also explore/* branches without a pull request.",
|
|
7144
7716
|
inputSchema: { state: z.enum(["open", "closed", "all"]).optional().describe("Pull request state. Default open: running and promoted ones.") },
|
|
7145
7717
|
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
7146
7718
|
},
|
|
@@ -7345,6 +7917,13 @@ function optionsFromArgv(argv, env = process.env) {
|
|
|
7345
7917
|
}
|
|
7346
7918
|
|
|
7347
7919
|
export {
|
|
7920
|
+
missingProse,
|
|
7921
|
+
formatDocsJson,
|
|
7922
|
+
DOCS_DRAFT_MODEL,
|
|
7923
|
+
parseDocsReply,
|
|
7924
|
+
mergeDocsDraft,
|
|
7925
|
+
anthropicRequest,
|
|
7926
|
+
anthropicReplyText,
|
|
7348
7927
|
detectConfig,
|
|
7349
7928
|
core,
|
|
7350
7929
|
LocalSource,
|