@bridge-ds/cli 0.5.0 → 0.7.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.
Files changed (2) hide show
  1. package/dist/index.js +465 -257
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/index.ts
4
4
  import { existsSync as existsSync6 } from "node:fs";
5
5
  import { readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
6
- import { isAbsolute, join as join6, relative as relative4, resolve as resolve4 } from "node:path";
6
+ import { isAbsolute, join as join6, relative as relative4, resolve as resolve5 } from "node:path";
7
7
 
8
8
  // ../core/src/audit/types.ts
9
9
  var AUDIT_SCHEMA_VERSION = 1;
@@ -2158,57 +2158,6 @@ function planCssFixes(text, lang, ds) {
2158
2158
  return { edits: edits.map(({ start: _s, end: _e, ...e }) => e), output };
2159
2159
  }
2160
2160
 
2161
- // ../core/src/node/git-local.ts
2162
- import { execFile } from "node:child_process";
2163
-
2164
- // ../core/src/governance.ts
2165
- var DEFAULT_PROTECTED_BRANCHES = ["main", "master"];
2166
- var ProtectedBranchError = class extends Error {
2167
- constructor(message) {
2168
- super(message);
2169
- this.name = "ProtectedBranchError";
2170
- }
2171
- };
2172
- function assertSafeBranch(branch, rules = {}) {
2173
- const name = branch.replace(/^refs\/heads\//, "").trim();
2174
- const blocked = /* @__PURE__ */ new Set([...rules.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES, ...rules.base ? [rules.base] : []]);
2175
- if (!name || blocked.has(name)) {
2176
- throw new ProtectedBranchError(`Refusing to write to protected branch "${name}". Bridge only creates new branches and pull requests.`);
2177
- }
2178
- if (rules.branchPrefix && !name.startsWith(rules.branchPrefix)) {
2179
- throw new ProtectedBranchError(`Branch "${name}" must start with "${rules.branchPrefix}".`);
2180
- }
2181
- }
2182
-
2183
- // ../core/src/node/git-local.ts
2184
- function git(root, args, opts = {}) {
2185
- return new Promise((resolve5, reject) => {
2186
- const child = execFile("git", args, { cwd: root, env: { ...process.env, ...opts.env }, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
2187
- if (err) reject(new Error(`git ${args.join(" ")}: ${stderr || err.message}`.trim()));
2188
- else resolve5(stdout);
2189
- });
2190
- if (opts.input !== void 0) child.stdin.end(opts.input);
2191
- });
2192
- }
2193
- async function showFile(root, ref, path) {
2194
- try {
2195
- return await git(root, ["show", `${ref}:${path}`]);
2196
- } catch {
2197
- return null;
2198
- }
2199
- }
2200
- async function listFilesAt(root, ref) {
2201
- return (await git(root, ["ls-tree", "-r", "--name-only", ref])).split("\n").filter(Boolean);
2202
- }
2203
- async function originRepo(root) {
2204
- try {
2205
- const url = (await git(root, ["remote", "get-url", "origin"])).trim();
2206
- return /github\.com[:/]([\w.-]+\/[\w.-]+?)(\.git)?$/.exec(url)?.[1];
2207
- } catch {
2208
- return void 0;
2209
- }
2210
- }
2211
-
2212
2161
  // ../core/src/previews.ts
2213
2162
  var PREVIEW_APP_LABELS = { docs: "Docs", storybook: "Storybook", admin: "Admin demo" };
2214
2163
 
@@ -2475,6 +2424,8 @@ function productProblems(raw) {
2475
2424
 
2476
2425
  // ../core/src/config.ts
2477
2426
  var CONFIG_FILE = "bridge.config.json";
2427
+ var CHANGES_MODES = ["suggest", "direct"];
2428
+ var DEFAULT_CHANGES_MODE = "suggest";
2478
2429
  var DEFAULT_OVERRIDE = { allowed: [], requireReason: true, minReasonLength: 20 };
2479
2430
  var DEFAULT_TOKEN_PATHS = {
2480
2431
  root: "tokens",
@@ -2614,6 +2565,8 @@ function defineConfig(input) {
2614
2565
  ...g.rights ? { rights: normalizeRights(g.rights) } : {}
2615
2566
  },
2616
2567
  products: input.products?.length ? input.products.map((p) => ({ ...p })) : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
2568
+ changes: input.changes === "direct" ? "direct" : DEFAULT_CHANGES_MODE,
2569
+ ...input.storybook?.url ? { storybook: { url: input.storybook.url.trim().replace(/\/+$/, "") } } : {},
2617
2570
  ...input.audit ? { audit: { ...input.audit } } : {},
2618
2571
  ...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
2619
2572
  ...input.integrations ? { integrations: { ...input.integrations } } : {},
@@ -2736,6 +2689,13 @@ function loadConfig(source) {
2736
2689
  }
2737
2690
  }
2738
2691
  problems.push(...visualImpactProblems(d.visualImpact));
2692
+ if (d.changes !== void 0 && !CHANGES_MODES.includes(d.changes)) problems.push(`"changes" must be "suggest" or "direct" (got ${JSON.stringify(d.changes)})`);
2693
+ const sb = d.storybook;
2694
+ if (sb !== void 0) {
2695
+ if (!isObj(sb) || !isStr(sb.url)) problems.push('"storybook" must be { "url" } with the deployed Storybook address');
2696
+ else if (!/^https:\/\/[^\s/]+/.test(sb.url.trim())) problems.push('"storybook.url" must start with https://, for example https://storybook.example.com');
2697
+ else for (const k of Object.keys(sb)) if (k !== "url") problems.push(`"storybook.${k}" is not a known key`);
2698
+ }
2739
2699
  problems.push(...productProblems(d.products));
2740
2700
  const audit2 = d.audit;
2741
2701
  if (audit2 !== void 0) {
@@ -2766,6 +2726,57 @@ function loadConfig(source) {
2766
2726
  return defineConfig(d);
2767
2727
  }
2768
2728
 
2729
+ // ../core/src/node/git-local.ts
2730
+ import { execFile } from "node:child_process";
2731
+
2732
+ // ../core/src/governance.ts
2733
+ var DEFAULT_PROTECTED_BRANCHES = ["main", "master"];
2734
+ var ProtectedBranchError = class extends Error {
2735
+ constructor(message) {
2736
+ super(message);
2737
+ this.name = "ProtectedBranchError";
2738
+ }
2739
+ };
2740
+ function assertSafeBranch(branch, rules = {}) {
2741
+ const name = branch.replace(/^refs\/heads\//, "").trim();
2742
+ const blocked = /* @__PURE__ */ new Set([...rules.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES, ...rules.base ? [rules.base] : []]);
2743
+ if (!name || blocked.has(name)) {
2744
+ throw new ProtectedBranchError(`Refusing to write to protected branch "${name}". Bridge only creates new branches and pull requests.`);
2745
+ }
2746
+ if (rules.branchPrefix && !name.startsWith(rules.branchPrefix)) {
2747
+ throw new ProtectedBranchError(`Branch "${name}" must start with "${rules.branchPrefix}".`);
2748
+ }
2749
+ }
2750
+
2751
+ // ../core/src/node/git-local.ts
2752
+ function git(root, args, opts = {}) {
2753
+ return new Promise((resolve6, reject) => {
2754
+ const child = execFile("git", args, { cwd: root, env: { ...process.env, ...opts.env }, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
2755
+ if (err) reject(new Error(`git ${args.join(" ")}: ${stderr || err.message}`.trim()));
2756
+ else resolve6(stdout);
2757
+ });
2758
+ if (opts.input !== void 0) child.stdin.end(opts.input);
2759
+ });
2760
+ }
2761
+ async function showFile(root, ref, path) {
2762
+ try {
2763
+ return await git(root, ["show", `${ref}:${path}`]);
2764
+ } catch {
2765
+ return null;
2766
+ }
2767
+ }
2768
+ async function listFilesAt(root, ref) {
2769
+ return (await git(root, ["ls-tree", "-r", "--name-only", ref])).split("\n").filter(Boolean);
2770
+ }
2771
+ async function originRepo(root) {
2772
+ try {
2773
+ const url = (await git(root, ["remote", "get-url", "origin"])).trim();
2774
+ return /github\.com[:/]([\w.-]+\/[\w.-]+?)(\.git)?$/.exec(url)?.[1];
2775
+ } catch {
2776
+ return void 0;
2777
+ }
2778
+ }
2779
+
2769
2780
  // ../core/src/layered-review.ts
2770
2781
  var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
2771
2782
  function diffLayered(before, after) {
@@ -2958,93 +2969,6 @@ function parseComponentSpecs(text) {
2958
2969
  return f;
2959
2970
  }
2960
2971
 
2961
- // src/index.ts
2962
- import { fileURLToPath } from "node:url";
2963
-
2964
- // ../visual/src/run.ts
2965
- import { existsSync as existsSync4 } from "node:fs";
2966
- import { mkdir as mkdir3, readFile as readFile3, rm as rm2, unlink, writeFile as writeFile3 } from "node:fs/promises";
2967
- import { tmpdir } from "node:os";
2968
- import { join as join4, relative as relative3, resolve as resolve2 } from "node:path";
2969
-
2970
- // ../core/src/protection.ts
2971
- var PROTECTION_PARTS = ["review", "checks", "history", "deletion"];
2972
- var protectionRulesetName = (defaultBranch) => `Protect the library (${defaultBranch})`;
2973
- function refPattern(pattern) {
2974
- const src = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, ".*");
2975
- return new RegExp(`^${src}$`);
2976
- }
2977
- function rulesetTargetsDefault(ruleset, defaultBranch) {
2978
- if (ruleset.target && ruleset.target !== "branch") return false;
2979
- const ref = `refs/heads/${defaultBranch}`;
2980
- const hit = (p) => p === "~ALL" || p === "~DEFAULT_BRANCH" || p === ref || p === defaultBranch || refPattern(p.startsWith("refs/") ? p : `refs/heads/${p}`).test(ref);
2981
- const cond = ruleset.conditions?.ref_name;
2982
- if (!cond) return false;
2983
- if ((cond.exclude ?? []).some(hit)) return false;
2984
- return (cond.include ?? []).some(hit);
2985
- }
2986
- var count = (v) => typeof v === "number" ? v : 0;
2987
- function rulesetParts(ruleset) {
2988
- const parts = /* @__PURE__ */ new Set();
2989
- if (ruleset.enforcement !== "active") return parts;
2990
- for (const rule of ruleset.rules ?? []) {
2991
- if (rule.type === "deletion") parts.add("deletion");
2992
- if (rule.type === "non_fast_forward") parts.add("history");
2993
- if (rule.type === "pull_request" && count(rule.parameters?.required_approving_review_count) >= 1) parts.add("review");
2994
- if (rule.type === "required_status_checks" && Array.isArray(rule.parameters?.required_status_checks) && rule.parameters.required_status_checks.length > 0) parts.add("checks");
2995
- }
2996
- return parts;
2997
- }
2998
- function classicParts(p) {
2999
- const parts = /* @__PURE__ */ new Set();
3000
- if (count(p.required_pull_request_reviews?.required_approving_review_count) >= 1) parts.add("review");
3001
- if (((p.required_status_checks?.contexts?.length ?? 0) || (p.required_status_checks?.checks?.length ?? 0)) > 0) parts.add("checks");
3002
- if (!p.allow_force_pushes?.enabled) parts.add("history");
3003
- if (!p.allow_deletions?.enabled) parts.add("deletion");
3004
- return parts;
3005
- }
3006
- var rulesetUrl = (r) => r.html_url ?? r._links?.html?.href;
3007
- function protectionState(rulesets, branchProtection, defaultBranch) {
3008
- const bridge = rulesets?.find((r) => r.name === protectionRulesetName(defaultBranch));
3009
- const extra = bridge?.id !== void 0 ? { bridgeRulesetId: bridge.id } : {};
3010
- const covered = /* @__PURE__ */ new Set();
3011
- let url;
3012
- for (const r of rulesets ?? []) {
3013
- if (!rulesetTargetsDefault(r, defaultBranch)) continue;
3014
- const parts = rulesetParts(r);
3015
- if (parts.size && !url) url = rulesetUrl(r);
3016
- parts.forEach((p) => covered.add(p));
3017
- }
3018
- if (branchProtection) classicParts(branchProtection).forEach((p) => covered.add(p));
3019
- const missing = PROTECTION_PARTS.filter((p) => !covered.has(p));
3020
- const withUrl = url ? { url } : {};
3021
- if (!missing.length) return { kind: "protected", missing, ...extra, ...withUrl };
3022
- const unreadable = rulesets == null || branchProtection === void 0;
3023
- if (covered.size === 0) return { kind: unreadable ? "unknown" : "unprotected", missing, ...extra };
3024
- return { kind: "partial", missing, ...extra, ...withUrl };
3025
- }
3026
- var PROTECTION_PART_PHRASE = {
3027
- review: "changes can skip review",
3028
- checks: "checks do not have to pass",
3029
- history: "past changes can be rewritten",
3030
- deletion: "the library can be deleted"
3031
- };
3032
- var joinAnd = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
3033
- function protectionSentence(state) {
3034
- switch (state.kind) {
3035
- case "protected":
3036
- return "Protected: every change goes through review.";
3037
- case "unprotected":
3038
- return "Your library can be changed without review.";
3039
- case "partial": {
3040
- const s = joinAnd(state.missing.map((p) => PROTECTION_PART_PHRASE[p]));
3041
- return `Partly protected: ${s}.`;
3042
- }
3043
- default:
3044
- return "Protection status unknown: GitHub did not let this token read it.";
3045
- }
3046
- }
3047
-
3048
2972
  // ../core/src/github-client.ts
3049
2973
  var GitHubError = class extends Error {
3050
2974
  constructor(status, message, detail, info = {}) {
@@ -3423,10 +3347,16 @@ var GitHubClient = class _GitHubClient {
3423
3347
  async addComment(issueNumber2, body) {
3424
3348
  await this.request("POST", `${this.repoPath}/issues/${issueNumber2}/comments`, { body });
3425
3349
  }
3426
- /** Comments on an issue or pull request, oldest first (first 100: the sticky preview comment is posted early). */
3350
+ /**
3351
+ * Comments on an issue or pull request, oldest first (first 100: the sticky preview comment is posted early).
3352
+ * `authorAssociation` is GitHub's OWNER, MEMBER, COLLABORATOR, CONTRIBUTOR, NONE... (suggestion approvals read it).
3353
+ */
3427
3354
  async listIssueComments(issueNumber2) {
3428
- const r = await this.request("GET", `${this.repoPath}/issues/${issueNumber2}/comments?per_page=100`);
3429
- return r.map((c) => ({ id: c.id, body: c.body ?? "", author: c.user?.login ?? "unknown", updatedAt: c.updated_at }));
3355
+ const r = await this.request(
3356
+ "GET",
3357
+ `${this.repoPath}/issues/${issueNumber2}/comments?per_page=100`
3358
+ );
3359
+ return r.map((c) => ({ id: c.id, body: c.body ?? "", author: c.user?.login ?? "unknown", updatedAt: c.updated_at, createdAt: c.created_at ?? c.updated_at, authorAssociation: c.author_association ?? "NONE" }));
3430
3360
  }
3431
3361
  // --- Issues (component change requests) ---------------------------------------------------
3432
3362
  async createIssue(input) {
@@ -3773,6 +3703,17 @@ var GitHubClient = class _GitHubClient {
3773
3703
  this.assertToken("Updating a branch from main");
3774
3704
  await this.request("PUT", `${this.repoPath}/pulls/${n}/update-branch`, { expected_head_sha: expectedHeadSha });
3775
3705
  }
3706
+ /**
3707
+ * Merges the branch `head` into the branch `base` (GitHub's merges API): a merge commit on `base`, or nothing when
3708
+ * `base` already has every commit of `head`. A conflict throws a GitHubError with status 409. Refuses protected
3709
+ * branches and, with a branch prefix, a `base` without it.
3710
+ */
3711
+ async mergeIntoBranch(base, head, message) {
3712
+ this.assertToken("Merging a branch into another");
3713
+ this.assertWritableBranch(base);
3714
+ const r = await this.request("POST", `${this.repoPath}/merges`, { base, head, commit_message: message });
3715
+ return r?.sha ? { status: "merged", sha: r.sha } : { status: "up-to-date", sha: null };
3716
+ }
3776
3717
  /** Posts a comment on an issue or pull request and returns its id and link. */
3777
3718
  async createIssueComment(n, body) {
3778
3719
  this.assertToken("Commenting");
@@ -3905,6 +3846,148 @@ function toIssue(r) {
3905
3846
  };
3906
3847
  }
3907
3848
 
3849
+ // ../core/src/explorations.ts
3850
+ var EXPLORATION_USE_CASES = [
3851
+ { value: "user-test", label: "Prototype for a user test", hint: "A coded prototype on a product staging branch, with the real components and tokens." },
3852
+ { value: "stakeholder-demo", label: "Stakeholder demo", hint: "Show an idea on real screens before anyone commits to it." },
3853
+ { value: "ab-idea", label: "A/B idea", hint: "Ship a variant to a staging or experiment branch and compare it with what is live." },
3854
+ { value: "terminal", label: "Exploring with Claude Code", hint: "A designer works from the terminal with Claude and the Bridge MCP server on this branch, never on main." },
3855
+ { value: "other", label: "Other", hint: "Anything else worth trying before it becomes a change." }
3856
+ ];
3857
+ var EXPLORATION_STATUS_LABEL = { active: "Running", promoted: "Promoted to review", closed: "Closed" };
3858
+
3859
+ // ../core/src/requests.ts
3860
+ var REQUEST_KINDS = [
3861
+ { value: "new-component", label: "New component", hint: "A component the design system does not have yet." },
3862
+ { value: "new-variant", label: "New variant", hint: "A size, tone or state the component does not have yet." },
3863
+ { value: "prop-change", label: "Option change (prop)", hint: "Add, rename or change an option (a prop in code) or its default." },
3864
+ { value: "structure", label: "Structure change", hint: "A part was added, removed or moved." },
3865
+ { value: "visual", label: "Visual change", hint: "Spacing, radius, layout or icon placement." },
3866
+ { value: "token-binding", label: "Token change", hint: "The component should paint with another token." },
3867
+ { value: "bug", label: "Bug", hint: "It looks or behaves differently from the guidance." },
3868
+ { value: "other", label: "Other", hint: "Anything else." }
3869
+ ];
3870
+ var REQUEST_LABEL = "component-change";
3871
+ function requestIssueTitle(r) {
3872
+ const t = r.title.trim().replace(/\.$/, "");
3873
+ return `${r.component}: ${t.charAt(0).toLowerCase()}${t.slice(1)}`;
3874
+ }
3875
+ var REQUEST_STATUS_LABEL = { requested: "Requested", "in-progress": "In progress", done: "Done", declined: "Declined" };
3876
+ function validateIntegrations(i) {
3877
+ const out = [];
3878
+ if (!i?.jira) return out;
3879
+ if (!/^https:\/\/[^/]+/.test(i.jira.baseUrl ?? "")) out.push("Jira site must start with https://, for example https://acme.atlassian.net.");
3880
+ if (!/^[A-Z][A-Z0-9_]{1,9}$/.test(i.jira.project ?? "")) out.push("Jira project key is 2 to 10 capital letters or digits, like DS.");
3881
+ return out;
3882
+ }
3883
+ function jiraIssueFields(r, jira, issueUrl) {
3884
+ const text = (t) => ({ type: "text", text: t });
3885
+ const para = (...c) => ({ type: "paragraph", content: c });
3886
+ return {
3887
+ project: { key: jira.project },
3888
+ issuetype: { name: jira.issueType ?? "Task" },
3889
+ summary: requestIssueTitle(r),
3890
+ labels: [.../* @__PURE__ */ new Set(["bridge", REQUEST_LABEL, ...jira.labels ?? []])].map((l) => l.replace(/\s+/g, "-")),
3891
+ description: {
3892
+ type: "doc",
3893
+ version: 1,
3894
+ content: [
3895
+ para(text(r.summary.trim() || r.title)),
3896
+ ...r.line?.trim() ? [para(text(r.line.trim()))] : [],
3897
+ ...r.changes.map((c) => para(text(`${c.field}: ${c.before ?? "(none)"} -> ${c.after ?? "(removed)"}`))),
3898
+ para({ type: "text", text: "GitHub request", marks: [{ type: "link", attrs: { href: issueUrl } }] })
3899
+ ]
3900
+ }
3901
+ };
3902
+ }
3903
+
3904
+ // src/index.ts
3905
+ import { fileURLToPath } from "node:url";
3906
+
3907
+ // ../visual/src/run.ts
3908
+ import { existsSync as existsSync4 } from "node:fs";
3909
+ import { mkdir as mkdir3, readFile as readFile3, rm as rm2, unlink, writeFile as writeFile3 } from "node:fs/promises";
3910
+ import { tmpdir } from "node:os";
3911
+ import { join as join4, relative as relative3, resolve as resolve2 } from "node:path";
3912
+
3913
+ // ../core/src/protection.ts
3914
+ var PROTECTION_PARTS = ["review", "checks", "history", "deletion"];
3915
+ var protectionRulesetName = (defaultBranch) => `Protect the library (${defaultBranch})`;
3916
+ function refPattern(pattern) {
3917
+ const src = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, ".*");
3918
+ return new RegExp(`^${src}$`);
3919
+ }
3920
+ function rulesetTargetsDefault(ruleset, defaultBranch) {
3921
+ if (ruleset.target && ruleset.target !== "branch") return false;
3922
+ const ref = `refs/heads/${defaultBranch}`;
3923
+ const hit = (p) => p === "~ALL" || p === "~DEFAULT_BRANCH" || p === ref || p === defaultBranch || refPattern(p.startsWith("refs/") ? p : `refs/heads/${p}`).test(ref);
3924
+ const cond = ruleset.conditions?.ref_name;
3925
+ if (!cond) return false;
3926
+ if ((cond.exclude ?? []).some(hit)) return false;
3927
+ return (cond.include ?? []).some(hit);
3928
+ }
3929
+ var count = (v) => typeof v === "number" ? v : 0;
3930
+ function rulesetParts(ruleset) {
3931
+ const parts = /* @__PURE__ */ new Set();
3932
+ if (ruleset.enforcement !== "active") return parts;
3933
+ for (const rule of ruleset.rules ?? []) {
3934
+ if (rule.type === "deletion") parts.add("deletion");
3935
+ if (rule.type === "non_fast_forward") parts.add("history");
3936
+ if (rule.type === "pull_request" && count(rule.parameters?.required_approving_review_count) >= 1) parts.add("review");
3937
+ if (rule.type === "required_status_checks" && Array.isArray(rule.parameters?.required_status_checks) && rule.parameters.required_status_checks.length > 0) parts.add("checks");
3938
+ }
3939
+ return parts;
3940
+ }
3941
+ function classicParts(p) {
3942
+ const parts = /* @__PURE__ */ new Set();
3943
+ if (count(p.required_pull_request_reviews?.required_approving_review_count) >= 1) parts.add("review");
3944
+ if (((p.required_status_checks?.contexts?.length ?? 0) || (p.required_status_checks?.checks?.length ?? 0)) > 0) parts.add("checks");
3945
+ if (!p.allow_force_pushes?.enabled) parts.add("history");
3946
+ if (!p.allow_deletions?.enabled) parts.add("deletion");
3947
+ return parts;
3948
+ }
3949
+ var rulesetUrl = (r) => r.html_url ?? r._links?.html?.href;
3950
+ function protectionState(rulesets, branchProtection, defaultBranch) {
3951
+ const bridge = rulesets?.find((r) => r.name === protectionRulesetName(defaultBranch));
3952
+ const extra = bridge?.id !== void 0 ? { bridgeRulesetId: bridge.id } : {};
3953
+ const covered = /* @__PURE__ */ new Set();
3954
+ let url;
3955
+ for (const r of rulesets ?? []) {
3956
+ if (!rulesetTargetsDefault(r, defaultBranch)) continue;
3957
+ const parts = rulesetParts(r);
3958
+ if (parts.size && !url) url = rulesetUrl(r);
3959
+ parts.forEach((p) => covered.add(p));
3960
+ }
3961
+ if (branchProtection) classicParts(branchProtection).forEach((p) => covered.add(p));
3962
+ const missing = PROTECTION_PARTS.filter((p) => !covered.has(p));
3963
+ const withUrl = url ? { url } : {};
3964
+ if (!missing.length) return { kind: "protected", missing, ...extra, ...withUrl };
3965
+ const unreadable = rulesets == null || branchProtection === void 0;
3966
+ if (covered.size === 0) return { kind: unreadable ? "unknown" : "unprotected", missing, ...extra };
3967
+ return { kind: "partial", missing, ...extra, ...withUrl };
3968
+ }
3969
+ var PROTECTION_PART_PHRASE = {
3970
+ review: "changes can skip review",
3971
+ checks: "checks do not have to pass",
3972
+ history: "past changes can be rewritten",
3973
+ deletion: "the library can be deleted"
3974
+ };
3975
+ var joinAnd = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
3976
+ function protectionSentence(state) {
3977
+ switch (state.kind) {
3978
+ case "protected":
3979
+ return "Protected: every change goes through review.";
3980
+ case "unprotected":
3981
+ return "Your library can be changed without review.";
3982
+ case "partial": {
3983
+ const s = joinAnd(state.missing.map((p) => PROTECTION_PART_PHRASE[p]));
3984
+ return `Partly protected: ${s}.`;
3985
+ }
3986
+ default:
3987
+ return "Protection status unknown: GitHub did not let this token read it.";
3988
+ }
3989
+ }
3990
+
3908
3991
  // ../core/src/protection-github.ts
3909
3992
  async function loadProtection(client, defaultBranch) {
3910
3993
  const rulesets = await client.listRulesets().then((list2) => Promise.all(list2.map((r) => r.enforcement === "disabled" || r.id === void 0 ? r : client.getRuleset(r.id)))).catch(() => null);
@@ -4021,61 +4104,6 @@ function impactedModules(index, input) {
4021
4104
  return { modules, globalHits };
4022
4105
  }
4023
4106
 
4024
- // ../core/src/explorations.ts
4025
- var EXPLORATION_USE_CASES = [
4026
- { value: "user-test", label: "Prototype for a user test", hint: "A coded prototype on a product staging branch, with the real components and tokens." },
4027
- { value: "stakeholder-demo", label: "Stakeholder demo", hint: "Show an idea on real screens before anyone commits to it." },
4028
- { value: "ab-idea", label: "A/B idea", hint: "Ship a variant to a staging or experiment branch and compare it with what is live." },
4029
- { value: "terminal", label: "Exploring with Claude Code", hint: "A designer works from the terminal with Claude and the Bridge MCP server on this branch, never on main." },
4030
- { value: "other", label: "Other", hint: "Anything else worth trying before it becomes a change." }
4031
- ];
4032
- var EXPLORATION_STATUS_LABEL = { active: "Running", promoted: "Promoted to review", closed: "Closed" };
4033
-
4034
- // ../core/src/requests.ts
4035
- var REQUEST_KINDS = [
4036
- { value: "new-component", label: "New component", hint: "A component the design system does not have yet." },
4037
- { value: "new-variant", label: "New variant", hint: "A size, tone or state the component does not have yet." },
4038
- { value: "prop-change", label: "Option change (prop)", hint: "Add, rename or change an option (a prop in code) or its default." },
4039
- { value: "structure", label: "Structure change", hint: "A part was added, removed or moved." },
4040
- { value: "visual", label: "Visual change", hint: "Spacing, radius, layout or icon placement." },
4041
- { value: "token-binding", label: "Token change", hint: "The component should paint with another token." },
4042
- { value: "bug", label: "Bug", hint: "It looks or behaves differently from the guidance." },
4043
- { value: "other", label: "Other", hint: "Anything else." }
4044
- ];
4045
- var REQUEST_LABEL = "component-change";
4046
- function requestIssueTitle(r) {
4047
- const t = r.title.trim().replace(/\.$/, "");
4048
- return `${r.component}: ${t.charAt(0).toLowerCase()}${t.slice(1)}`;
4049
- }
4050
- var REQUEST_STATUS_LABEL = { requested: "Requested", "in-progress": "In progress", done: "Done", declined: "Declined" };
4051
- function validateIntegrations(i) {
4052
- const out = [];
4053
- if (!i?.jira) return out;
4054
- if (!/^https:\/\/[^/]+/.test(i.jira.baseUrl ?? "")) out.push("Jira site must start with https://, for example https://acme.atlassian.net.");
4055
- if (!/^[A-Z][A-Z0-9_]{1,9}$/.test(i.jira.project ?? "")) out.push("Jira project key is 2 to 10 capital letters or digits, like DS.");
4056
- return out;
4057
- }
4058
- function jiraIssueFields(r, jira, issueUrl) {
4059
- const text = (t) => ({ type: "text", text: t });
4060
- const para = (...c) => ({ type: "paragraph", content: c });
4061
- return {
4062
- project: { key: jira.project },
4063
- issuetype: { name: jira.issueType ?? "Task" },
4064
- summary: requestIssueTitle(r),
4065
- labels: [.../* @__PURE__ */ new Set(["bridge", REQUEST_LABEL, ...jira.labels ?? []])].map((l) => l.replace(/\s+/g, "-")),
4066
- description: {
4067
- type: "doc",
4068
- version: 1,
4069
- content: [
4070
- para(text(r.summary.trim() || r.title)),
4071
- ...r.line?.trim() ? [para(text(r.line.trim()))] : [],
4072
- ...r.changes.map((c) => para(text(`${c.field}: ${c.before ?? "(none)"} -> ${c.after ?? "(removed)"}`))),
4073
- para({ type: "text", text: "GitHub request", marks: [{ type: "link", attrs: { href: issueUrl } }] })
4074
- ]
4075
- }
4076
- };
4077
- }
4078
-
4079
4107
  // ../core/src/fidelity-references.ts
4080
4108
  var FIGMA_REFERENCES_DIR = "figma-references";
4081
4109
  var FIGMA_REFERENCES_MANIFEST = `${FIGMA_REFERENCES_DIR}/manifest.json`;
@@ -4299,15 +4327,15 @@ async function shoot(page, r) {
4299
4327
  }
4300
4328
  async function switchStory(page, storyId2) {
4301
4329
  const ok = await page.evaluate(
4302
- (id) => new Promise((resolve5) => {
4330
+ (id) => new Promise((resolve6) => {
4303
4331
  const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
4304
- if (!ch) return resolve5(false);
4332
+ if (!ch) return resolve6(false);
4305
4333
  const events = ["storyRendered", "storyErrored", "storyThrewException", "storyMissing", "playFunctionThrewException"];
4306
4334
  const timer = setTimeout(() => done(), 15e3);
4307
4335
  function done() {
4308
4336
  clearTimeout(timer);
4309
4337
  for (const e of events) ch.off(e, done);
4310
- resolve5(true);
4338
+ resolve6(true);
4311
4339
  }
4312
4340
  for (const e of events) ch.on(e, done);
4313
4341
  window.scrollTo(0, 0);
@@ -4720,12 +4748,12 @@ function serveDir(dir) {
4720
4748
  res.writeHead(200, { "content-type": TYPES[extname(file)] ?? "application/octet-stream", "cache-control": "no-store" });
4721
4749
  createReadStream(file).pipe(res);
4722
4750
  });
4723
- return new Promise((resolve5, reject) => {
4751
+ return new Promise((resolve6, reject) => {
4724
4752
  server.once("error", reject);
4725
4753
  server.listen(0, "127.0.0.1", () => {
4726
4754
  const addr = server.address();
4727
4755
  const port = typeof addr === "object" && addr ? addr.port : 0;
4728
- resolve5({ url: `http://127.0.0.1:${port}`, close: () => new Promise((r) => server.close(() => r())) });
4756
+ resolve6({ url: `http://127.0.0.1:${port}`, close: () => new Promise((r) => server.close(() => r())) });
4729
4757
  });
4730
4758
  });
4731
4759
  }
@@ -4797,7 +4825,7 @@ async function removeWorktree(repo, dir) {
4797
4825
  await git(repo, ["worktree", "prune"]).catch(() => void 0);
4798
4826
  }
4799
4827
  function run(cmd, args, cwd, log, root = cwd, env = {}) {
4800
- return new Promise((resolve5, reject) => {
4828
+ return new Promise((resolve6, reject) => {
4801
4829
  const bin = [.../* @__PURE__ */ new Set([join3(cwd, "node_modules", ".bin"), join3(root, "node_modules", ".bin")])].join(":");
4802
4830
  const child = spawn(cmd, args, {
4803
4831
  cwd,
@@ -4810,7 +4838,7 @@ function run(cmd, args, cwd, log, root = cwd, env = {}) {
4810
4838
  child.stderr.on("data", keep);
4811
4839
  child.on("error", reject);
4812
4840
  child.on("close", (code) => {
4813
- if (code === 0) resolve5();
4841
+ if (code === 0) resolve6();
4814
4842
  else {
4815
4843
  log(tail);
4816
4844
  reject(new Error(`${cmd} ${args.join(" ")} failed in ${cwd} (exit ${code})`));
@@ -5221,12 +5249,12 @@ async function captureFigmaReferences(o) {
5221
5249
  }
5222
5250
  const plan = JSON.stringify(j.plan.render);
5223
5251
  await page.evaluate(
5224
- ({ id, plan: plan2 }) => new Promise((resolve5) => {
5252
+ ({ id, plan: plan2 }) => new Promise((resolve6) => {
5225
5253
  const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
5226
5254
  const done = () => {
5227
5255
  clearTimeout(timer);
5228
5256
  ch.off("storyRendered", done);
5229
- resolve5();
5257
+ resolve6();
5230
5258
  };
5231
5259
  const timer = setTimeout(done, 1e4);
5232
5260
  ch.on("storyRendered", done);
@@ -5515,7 +5543,7 @@ Claude Code is not installed (no \`claude\` on PATH). Install it (npm install -g
5515
5543
  if (!REQUEST_KINDS.some((k) => k.value === kind)) throw new RequestUsageError(`--kind takes ${REQUEST_KINDS.map((k) => k.value).join(", ")}, got "${kind}"`);
5516
5544
  const dryRun = has(a, "dry-run");
5517
5545
  const ctx = await context(root, io, !dryRun);
5518
- const figma = flag(a, "figma");
5546
+ const figma2 = flag(a, "figma");
5519
5547
  const exploration = flag(a, "exploration");
5520
5548
  const r = await requests2.createRequest(ctx, {
5521
5549
  component,
@@ -5523,7 +5551,7 @@ Claude Code is not installed (no \`claude\` on PATH). Install it (npm install -g
5523
5551
  title,
5524
5552
  summary,
5525
5553
  changes: (a.flags.change ?? []).map(parseChange),
5526
- ...figma ? { figmaUrl: figma } : {},
5554
+ ...figma2 ? { figmaUrl: figma2 } : {},
5527
5555
  ...exploration ? { exploration } : {},
5528
5556
  ...flag(a, "product") ? { product: flag(a, "product") } : {},
5529
5557
  source: "cli",
@@ -5904,6 +5932,177 @@ ${r.content}` : String(r.said));
5904
5932
  return 0;
5905
5933
  }
5906
5934
 
5935
+ // src/pieces.ts
5936
+ import { BridgeContext as BridgeContext5, branches as branches2, pieces } from "@bridge-ds/mcp";
5937
+ var PIECES_HELP = `bridge pieces: a product branch and the library
5938
+
5939
+ Usage:
5940
+ bridge pieces <n> [--record] [--json] what the product branch adds that the library lacks (the audit
5941
+ on its diff against main); --record saves it in the branch,
5942
+ where Bridge Studio shows it
5943
+ bridge pieces status <n> [--json] each linked library branch: status, reviewers, preview, next step
5944
+ bridge pieces open <n> <piece> [--no-preview] [--dry-run] [--json]
5945
+ open the library side of a piece, linked both ways: a token
5946
+ change, a library exploration (variant) or a request (component)
5947
+ bridge pieces preview <n> <piece> the product branch takes the latest library branch (merged in)
5948
+ bridge pieces switch <n> once the library shipped: main into the product branch
5949
+
5950
+ GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
5951
+ var has4 = (a, k) => Boolean(a.flags[k]);
5952
+ async function context4(root, io) {
5953
+ const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
5954
+ return BridgeContext5.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
5955
+ }
5956
+ function number2(a, at, usage) {
5957
+ const raw = a._[at];
5958
+ if (!raw) throw new RequestUsageError(`Usage: ${usage}`);
5959
+ return branches2.branchNumber(raw);
5960
+ }
5961
+ function piece(a, at, usage) {
5962
+ const raw = a._[at];
5963
+ if (!raw) throw new RequestUsageError(`Usage: ${usage}. \`bridge pieces <n>\` lists the piece ids.`);
5964
+ return raw;
5965
+ }
5966
+ async function runPieces(a, root, io = defaultIO()) {
5967
+ const sub = a._[1];
5968
+ if (!sub || sub === "help" || has4(a, "help")) {
5969
+ io.print(PIECES_HELP);
5970
+ return sub || has4(a, "help") ? 0 : 2;
5971
+ }
5972
+ const json = has4(a, "json");
5973
+ const ctx = await context4(root, io);
5974
+ const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
5975
+ switch (sub) {
5976
+ case "status": {
5977
+ const r = await pieces.productBranchOp(ctx, { pr: number2(a, 2, "bridge pieces status <n>") });
5978
+ return out(r, () => {
5979
+ io.print(`#${r.pr} ${r.title} (${r.product})`);
5980
+ io.print(r.said);
5981
+ for (const l of r.linked) io.print(` ${l.piece}: #${l.library.number} ${l.status}. ${l.preview}${l.action ? ` Next: ${l.action}.` : ""}`);
5982
+ for (const p of r.notLinked) io.print(` ${p.title} (${p.id}): no library branch yet. bridge pieces open ${r.pr} ${p.id}`);
5983
+ if (r.next) io.print(r.next);
5984
+ });
5985
+ }
5986
+ case "open": {
5987
+ const usage = "bridge pieces open <n> <piece>";
5988
+ const r = await pieces.openLibraryBranchOp(ctx, { pr: number2(a, 2, usage), piece: piece(a, 3, usage), usePreview: !has4(a, "no-preview"), dryRun: has4(a, "dry-run"), source: "cli" });
5989
+ return out(r, () => {
5990
+ io.print(`${r.status === "dry-run" ? "Would open" : r.status === "already-linked" ? "Already linked" : "Opened"} a ${r.opens}: ${r.title}, needed by ${r.neededBy}.`);
5991
+ if (r.library) io.print(` #${r.library.number} ${r.library.url}`);
5992
+ if (r.preview) io.print(` ${r.preview.said}`);
5993
+ for (const w of r.warnings ?? []) io.print(` ${w}`);
5994
+ io.print(`Next: ${r.next}`);
5995
+ });
5996
+ }
5997
+ case "preview": {
5998
+ const usage = "bridge pieces preview <n> <piece>";
5999
+ const r = await pieces.useLibraryPreviewOp(ctx, { pr: number2(a, 2, usage), piece: piece(a, 3, usage) });
6000
+ return out(r, () => io.print(r.said));
6001
+ }
6002
+ case "switch": {
6003
+ const r = await pieces.switchToPublishedOp(ctx, { pr: number2(a, 2, "bridge pieces switch <n>") });
6004
+ return out(r, () => io.print(r.said));
6005
+ }
6006
+ default: {
6007
+ const r = await pieces.missingPiecesOp(ctx, { pr: number2(a, 1, "bridge pieces <n> [--record]"), record: has4(a, "record") });
6008
+ return out(r, () => {
6009
+ io.print(`#${r.pr} ${r.branch} (${r.product}): ${r.pieces.length ? `${r.pieces.length} ${r.pieces.length === 1 ? "piece is" : "pieces are"} missing from the library` : "nothing is missing from the library"}, ${r.files} files read.`);
6010
+ for (const p of r.pieces) io.print(` ${p.id} ${p.title}: ${p.summary} Opens ${p.opens}.${"linked" in p ? ` Linked: #${p.linked}.` : ""}`);
6011
+ if (r.fixes.count) io.print(`${r.fixes.count} value${r.fixes.count === 1 ? "" : "s"} the library already has (use them instead):
6012
+ ${r.fixes.sample.map((s) => ` ${s}`).join("\n")}`);
6013
+ if (r.skipped) io.print(r.skipped);
6014
+ io.print(`Next: ${r.next}`);
6015
+ });
6016
+ }
6017
+ }
6018
+ }
6019
+
6020
+ // src/figma.ts
6021
+ import { readFileSync as readFileSync2 } from "node:fs";
6022
+ import { resolve as resolve4 } from "node:path";
6023
+ import { BridgeContext as BridgeContext6, branches as branches3, figma } from "@bridge-ds/mcp";
6024
+ var FIGMA_HELP = `bridge figma: component changes sent from Figma
6025
+
6026
+ Usage:
6027
+ bridge figma [--all] [--json] the library branches the DS Bridge plugin opened for components
6028
+ changed in Figma, and where each stands
6029
+ bridge figma show <n> [--draft] [--json] one of them: its spec and the brief to draft the code; --draft
6030
+ runs Claude Code on the brief (\`claude\` on PATH)
6031
+ bridge figma drafted <n> [--json] the code is pushed: label it drafted by Claude AI for review
6032
+ bridge figma push <diff.json> [--dry-run] [--json]
6033
+ send a structured component diff like the plugin does: an exact
6034
+ token change, or a branch with the spec for Claude AI
6035
+
6036
+ GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
6037
+ var has5 = (a, k) => Boolean(a.flags[k]);
6038
+ async function context5(root, io) {
6039
+ const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
6040
+ return BridgeContext6.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
6041
+ }
6042
+ function number3(a, usage) {
6043
+ const raw = a._[2];
6044
+ if (!raw) throw new RequestUsageError(`Usage: ${usage}`);
6045
+ return branches3.branchNumber(raw);
6046
+ }
6047
+ async function runFigma(a, root, io = defaultIO()) {
6048
+ const sub = a._[1];
6049
+ if (sub === "help" || has5(a, "help")) {
6050
+ io.print(FIGMA_HELP);
6051
+ return 0;
6052
+ }
6053
+ const json = has5(a, "json");
6054
+ const ctx = await context5(root, io);
6055
+ const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
6056
+ switch (sub) {
6057
+ case void 0:
6058
+ case "list": {
6059
+ const r = await figma.figmaChangesOp(ctx, { state: has5(a, "all") ? "all" : "open" });
6060
+ return out(r, () => {
6061
+ for (const c of r.changes) io.print(`#${c.pr} ${c.title} (${c.route}): ${c.status}. ${c.said}`);
6062
+ io.print(`Next: ${r.next}`);
6063
+ });
6064
+ }
6065
+ case "show": {
6066
+ const r = await figma.getFigmaChangeOp(ctx, { pr: number3(a, "bridge figma show <n>") });
6067
+ if (has5(a, "draft")) {
6068
+ if (!r.brief) throw new RequestUsageError(`#${r.pr} is an exact token change: there is no code to draft.`);
6069
+ if (!io.hasClaude()) throw new RequestUsageError("`claude` is not on PATH: install Claude Code, or paste the brief from `bridge figma show <n>` into Claude.");
6070
+ return io.runClaude(r.brief, root);
6071
+ }
6072
+ return out(r, () => {
6073
+ io.print(`#${r.pr} ${r.title} (${r.route}): ${r.status}. ${r.said}`);
6074
+ io.print(` ${r.url}`);
6075
+ if (r.brief) io.print(`
6076
+ ${r.brief}`);
6077
+ });
6078
+ }
6079
+ case "drafted": {
6080
+ const r = await figma.markFigmaDraftedOp(ctx, { pr: number3(a, "bridge figma drafted <n>") });
6081
+ return out(r, () => io.print(`#${r.pr}: ${r.status}. ${r.said}`));
6082
+ }
6083
+ case "push": {
6084
+ const file = a._[2];
6085
+ if (!file) throw new RequestUsageError("Usage: bridge figma push <diff.json> [--dry-run]");
6086
+ let diff2;
6087
+ try {
6088
+ diff2 = JSON.parse(readFileSync2(resolve4(root, file), "utf8"));
6089
+ } catch (err) {
6090
+ throw new RequestUsageError(`${file} could not be read as JSON: ${err.message}`);
6091
+ }
6092
+ const r = await figma.pushFigmaComponentOp(ctx, { diff: diff2, dryRun: has5(a, "dry-run"), source: "cli" });
6093
+ return out(r, () => {
6094
+ io.print(`${r.component}: ${r.route}. ${r.said}`);
6095
+ for (const x of r.reasons ?? []) io.print(` ${x}`);
6096
+ for (const x of r.fixInFigma ?? []) io.print(` Fix in Figma: ${x.what} ${x.todo}`);
6097
+ if (r.library) io.print(` #${r.library.number} ${r.library.url}`);
6098
+ io.print(`Next: ${r.next}`);
6099
+ });
6100
+ }
6101
+ default:
6102
+ throw new RequestUsageError(`Unknown subcommand "${sub}". ${FIGMA_HELP}`);
6103
+ }
6104
+ }
6105
+
5907
6106
  // src/protection.ts
5908
6107
  var UNKNOWN = { kind: "unknown", missing: ["review", "checks", "history", "deletion"] };
5909
6108
  async function protectionReport(opts) {
@@ -5936,7 +6135,7 @@ async function rolesReport(opts) {
5936
6135
  }
5937
6136
 
5938
6137
  // src/index.ts
5939
- import { brand, BridgeContext as BridgeContext5, CONFIG_FILE as CONFIG_FILE2, core, detectConfig, GitRefSource, history as hist, LocalSource, ops, optionsFromArgv, serveStdio, toConfigJson, VERSION } from "@bridge-ds/mcp";
6138
+ import { brand, BridgeContext as BridgeContext7, CONFIG_FILE as CONFIG_FILE2, core, detectConfig, GitRefSource, history as hist, LocalSource, ops, optionsFromArgv, serveStdio, toConfigJson, VERSION } from "@bridge-ds/mcp";
5940
6139
  var MCP_PACKAGE = "@bridge-ds/mcp";
5941
6140
  var HELP = `bridge ${VERSION}: design system as code, for you and your Claude
5942
6141
 
@@ -6018,6 +6217,13 @@ Usage:
6018
6217
  (\`bridge branch help\`): what needs me, send for review, review,
6019
6218
  merge through the same gate (owner override with a reason), update
6020
6219
  from main with token conflicts, try it in a product, try together
6220
+ bridge pieces <n> [--record] | status <n> | open <n> <piece> | preview <n> <piece> | switch <n>
6221
+ a product branch and the library (\`bridge pieces help\`): what it adds
6222
+ that the library lacks, the library branch each piece opens (linked
6223
+ both ways), its preview in the product branch, the published version
6224
+ bridge figma [show <n> | drafted <n> | push <diff.json>]
6225
+ component changes sent from Figma (\`bridge figma help\`): exact token
6226
+ changes and branches Claude AI drafts from the spec
6021
6227
  bridge products | bridge settings | bridge coverage [Component]
6022
6228
  products (and this folder's), Studio settings safe to read, which
6023
6229
  components are in Figma
@@ -6046,16 +6252,16 @@ function parse(argv) {
6046
6252
  return out;
6047
6253
  }
6048
6254
  var flag4 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
6049
- var has4 = (a, k) => Boolean(a.flags[k]);
6255
+ var has6 = (a, k) => Boolean(a.flags[k]);
6050
6256
  var print = (s) => process.stdout.write(`${s}
6051
6257
  `);
6052
6258
  var modeArg = (s) => s ? Object.fromEntries(s.split(",").map((kv) => kv.split("="))) : void 0;
6053
6259
  async function init(a, root) {
6054
6260
  const cfgPath = join6(root, CONFIG_FILE2);
6055
6261
  let config;
6056
- if (existsSync6(cfgPath) && !has4(a, "force")) {
6262
+ if (existsSync6(cfgPath) && !has6(a, "force")) {
6057
6263
  print(`${CONFIG_FILE2} exists, keeping it (--force to regenerate).`);
6058
- config = (await (await BridgeContext5.create({ root })).config()).config;
6264
+ config = (await (await BridgeContext7.create({ root })).config()).config;
6059
6265
  } else {
6060
6266
  const src = new LocalSource(root);
6061
6267
  const files = await src.list("");
@@ -6083,14 +6289,14 @@ async function init(a, root) {
6083
6289
  print(` claude mcp add bridge -- ${desktop.command} ${desktop.args.join(" ")}`);
6084
6290
  }
6085
6291
  async function check(a, root) {
6086
- const ctx = await BridgeContext5.create({ root });
6292
+ const ctx = await BridgeContext7.create({ root });
6087
6293
  const v = await ops.validateTokens(ctx, { includeWarnings: true });
6088
6294
  const c = await ops.checkContrast(ctx, {});
6089
6295
  const warnings = "warnings" in v ? v.warnings ?? [] : [];
6090
- const failed = !v.valid || "failing" in c && (c.failing ?? 0) > 0 || has4(a, "strict") && warnings.length > 0;
6296
+ const failed = !v.valid || "failing" in c && (c.failing ?? 0) > 0 || has6(a, "strict") && warnings.length > 0;
6091
6297
  const protection = await protectionOf(a, root, ctx);
6092
6298
  const roles = await rolesOf(a, root, ctx);
6093
- if (has4(a, "json")) {
6299
+ if (has6(a, "json")) {
6094
6300
  print(JSON.stringify({ ok: !failed, validation: v, contrast: c, ...protection ? { protection: protection.state } : {}, ...roles?.teams.length ? { teams: roles.teams } : {} }, null, 2));
6095
6301
  return failed ? 1 : 0;
6096
6302
  }
@@ -6109,7 +6315,7 @@ async function check(a, root) {
6109
6315
  return failed ? 1 : 0;
6110
6316
  }
6111
6317
  async function protectionOf(a, root, ctx) {
6112
- if (has4(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
6318
+ if (has6(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
6113
6319
  const { config } = await ctx.config();
6114
6320
  const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
6115
6321
  if (!repo) return void 0;
@@ -6117,7 +6323,7 @@ async function protectionOf(a, root, ctx) {
6117
6323
  return protectionReport({ repo, defaultBranch: config.repo?.defaultBranch ?? "main", token });
6118
6324
  }
6119
6325
  async function rolesOf(a, root, ctx) {
6120
- if (has4(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
6326
+ if (has6(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
6121
6327
  const { config } = await ctx.config();
6122
6328
  const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
6123
6329
  if (!repo) return void 0;
@@ -6127,9 +6333,9 @@ async function rolesOf(a, root, ctx) {
6127
6333
  async function tokens(a, root) {
6128
6334
  const q = a._.slice(1).join(" ");
6129
6335
  if (!q) throw new UsageError("bridge tokens <query>");
6130
- const ctx = await BridgeContext5.create({ root });
6336
+ const ctx = await BridgeContext7.create({ root });
6131
6337
  const r = await ops.searchTokens(ctx, { query: q, limit: Number(flag4(a, "limit") ?? 20) });
6132
- if (has4(a, "json")) return print(JSON.stringify(r, null, 2));
6338
+ if (has6(a, "json")) return print(JSON.stringify(r, null, 2));
6133
6339
  if (!r.results.length) return print(`No token matches "${q}".`);
6134
6340
  const w = Math.max(...r.results.map((x) => x.path.length));
6135
6341
  for (const t of r.results) print(`${t.path.padEnd(w)} ${String(t.value).padEnd(10)} ${(t.tier ?? "").padEnd(9)} ${t.description ?? ""}`);
@@ -6137,7 +6343,7 @@ async function tokens(a, root) {
6137
6343
  async function diff(a, root) {
6138
6344
  const base = a._[1];
6139
6345
  if (!base) throw new UsageError("bridge diff <base> (a git ref: main, HEAD~1, a sha)");
6140
- const ctx = await BridgeContext5.create({ root });
6346
+ const ctx = await BridgeContext7.create({ root });
6141
6347
  const { config } = await ctx.config();
6142
6348
  const before = await core.loadTokenSet(new GitRefSource(root, base), config);
6143
6349
  const after = await ctx.tokens();
@@ -6146,11 +6352,11 @@ async function diff(a, root) {
6146
6352
  const delta2 = pairs2.length ? core.contrastDelta(core.runContrast(before, pairs2, config.contrast?.modes), core.runContrast(after, pairs2, config.contrast?.modes)) : [];
6147
6353
  const tier = core.classify(changes, [], config);
6148
6354
  const imp = core.impact(before, after, changes, 30);
6149
- if (has4(a, "json")) {
6355
+ if (has6(a, "json")) {
6150
6356
  print(JSON.stringify({ base, changes, contrast: delta2, tier, impact: imp }, null, 2));
6151
6357
  return 0;
6152
6358
  }
6153
- if (has4(a, "md")) {
6359
+ if (has6(a, "md")) {
6154
6360
  print(`## Token changes since \`${base}\`
6155
6361
 
6156
6362
  ${core.tokenTable(changes)}
@@ -6196,11 +6402,11 @@ async function propose(a, root) {
6196
6402
  edits.push({ op: "set", path: s.slice(0, i), value: s.slice(i + 1), ...file ? { file } : {}, ...mode2 ? { mode: mode2 } : {} });
6197
6403
  }
6198
6404
  const editsFile = flag4(a, "edits");
6199
- if (editsFile) edits.push(...JSON.parse(await readFile7(resolve4(editsFile), "utf8")));
6405
+ if (editsFile) edits.push(...JSON.parse(await readFile7(resolve5(editsFile), "utf8")));
6200
6406
  if (!edits.length) throw new UsageError("Give at least one --set path=value or --edits file.json");
6201
6407
  const pr = flag4(a, "pr");
6202
6408
  const token = pr ? process.env.BRIDGE_GITHUB_TOKEN || process.env.GITHUB_TOKEN || defaultIO().ghToken() : void 0;
6203
- const ctx = await BridgeContext5.create({ root, ...token ? { token } : {} });
6409
+ const ctx = await BridgeContext7.create({ root, ...token ? { token } : {} });
6204
6410
  const summary = flag4(a, "summary");
6205
6411
  const branch = flag4(a, "branch");
6206
6412
  const jira = flag4(a, "jira");
@@ -6213,9 +6419,9 @@ async function propose(a, root) {
6213
6419
  ...pr ? { pr } : {},
6214
6420
  ...jira ? { jira } : {},
6215
6421
  ...product ? { product } : {},
6216
- dryRun: has4(a, "dry-run")
6422
+ dryRun: has6(a, "dry-run")
6217
6423
  });
6218
- if (has4(a, "json")) print(JSON.stringify(r, null, 2));
6424
+ if (has6(a, "json")) print(JSON.stringify(r, null, 2));
6219
6425
  else {
6220
6426
  print(`status: ${r.status}${r.why ? ` (${r.why})` : ""}`);
6221
6427
  if (r.tier) print(`tier ${r.tier}, ${r.bump}`);
@@ -6230,10 +6436,10 @@ ${r.prBody}`);
6230
6436
  }
6231
6437
  async function brandCmd(a, root) {
6232
6438
  const sub = a._[1];
6233
- const ctx = await BridgeContext5.create({ root });
6439
+ const ctx = await BridgeContext7.create({ root });
6234
6440
  if (sub === "list") {
6235
6441
  const r2 = await brand.listBrands(ctx);
6236
- if (has4(a, "json")) print(JSON.stringify(r2.brands, null, 2));
6442
+ if (has6(a, "json")) print(JSON.stringify(r2.brands, null, 2));
6237
6443
  else for (const b of r2.brands) print(`${b.default ? "*" : " "} ${b.slug.padEnd(14)} ${b.name.padEnd(18)} ${b.primary ?? ""}${b.overrides.length ? ` overrides: ${b.overrides.join(", ")}` : ""}`);
6238
6444
  return 0;
6239
6445
  }
@@ -6253,9 +6459,9 @@ async function brandCmd(a, root) {
6253
6459
  ...opt("radius") ? { radius: opt("radius") } : {},
6254
6460
  ...Object.keys(fonts).length ? { fonts } : {},
6255
6461
  ...opt("notes") ? { notes: opt("notes") } : {},
6256
- dryRun: has4(a, "dry-run")
6462
+ dryRun: has6(a, "dry-run")
6257
6463
  });
6258
- if (has4(a, "json")) {
6464
+ if (has6(a, "json")) {
6259
6465
  const { fileContents: _f, ...rest } = r;
6260
6466
  print(JSON.stringify(rest, null, 2));
6261
6467
  return r.status === "blocked" ? 1 : 0;
@@ -6286,14 +6492,14 @@ async function figmaReferences(a, root) {
6286
6492
  }
6287
6493
  const only = (flag4(a, "only") ?? "").split(",").filter(Boolean);
6288
6494
  const r = await captureFigmaReferences({
6289
- storybook: resolve4(root, flag4(a, "storybook") ?? "storybook-static"),
6290
- specs: resolve4(root, flag4(a, "specs") ?? "packages/react/component-specs.json"),
6291
- out: resolve4(root, flag4(a, "out") ?? "figma-references"),
6495
+ storybook: resolve5(root, flag4(a, "storybook") ?? "storybook-static"),
6496
+ specs: resolve5(root, flag4(a, "specs") ?? "packages/react/component-specs.json"),
6497
+ out: resolve5(root, flag4(a, "out") ?? "figma-references"),
6292
6498
  sha,
6293
6499
  ...only.length ? { only } : {},
6294
- log: has4(a, "json") ? void 0 : (line) => print(line)
6500
+ log: has6(a, "json") ? void 0 : (line) => print(line)
6295
6501
  });
6296
- if (has4(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
6502
+ if (has6(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
6297
6503
  return 0;
6298
6504
  }
6299
6505
  async function impact(a, root) {
@@ -6311,22 +6517,22 @@ async function impact(a, root) {
6311
6517
  root,
6312
6518
  base: flag4(a, "base") ?? "main",
6313
6519
  head: flag4(a, "head") ?? "HEAD",
6314
- ...out ? { out: resolve4(out) } : {},
6520
+ ...out ? { out: resolve5(out) } : {},
6315
6521
  ...only.length ? { only } : {},
6316
6522
  ...threshold !== void 0 ? { threshold } : {},
6317
- ...cacheDir ? { cacheDir: resolve4(cacheDir) } : {},
6523
+ ...cacheDir ? { cacheDir: resolve5(cacheDir) } : {},
6318
6524
  ...maxStories ? { maxStoriesPerComponent: maxStories } : {},
6319
6525
  ...imageBase !== void 0 ? { imageBase } : {},
6320
6526
  ...artifactUrl ? { artifactUrl } : {},
6321
- plan: has4(a, "plan"),
6322
- keepUnchanged: has4(a, "keep-unchanged")
6527
+ plan: has6(a, "plan"),
6528
+ keepUnchanged: has6(a, "keep-unchanged")
6323
6529
  });
6324
- if (has4(a, "json")) {
6530
+ if (has6(a, "json")) {
6325
6531
  print(JSON.stringify({ summary: r.summary, manifest: r.manifestPath, comment: r.commentPath ?? null, ...r.manifest.summary, selection: r.manifest.selection }, null, 2));
6326
6532
  return 0;
6327
6533
  }
6328
6534
  const sel = r.manifest.selection;
6329
- if (has4(a, "plan")) {
6535
+ if (has6(a, "plan")) {
6330
6536
  print(`Plan ${r.manifest.base.ref} (${r.manifest.base.sha.slice(0, 7)}) -> ${r.manifest.head.ref} (${r.manifest.head.sha.slice(0, 7)})`);
6331
6537
  print(` tokens changing value: ${sel.tokens.length}${sel.tokens.length ? ` (${sel.tokens.filter((t) => t.kind === "direct").map((t) => t.path).slice(0, 6).join(", ")})` : ""}`);
6332
6538
  print(` components: ${sel.components.map((c) => c.id).join(", ") || "none"}`);
@@ -6342,12 +6548,12 @@ async function impact(a, root) {
6342
6548
  }
6343
6549
  var day = (iso) => iso.slice(0, 10);
6344
6550
  async function historyCmd(a, root) {
6345
- const ctx = await BridgeContext5.create({ root });
6551
+ const ctx = await BridgeContext7.create({ root });
6346
6552
  const token = a._[1];
6347
6553
  const limit = Number(flag4(a, "limit") ?? 20);
6348
6554
  if (token) {
6349
6555
  const r2 = await hist.getTokenHistory(ctx, { path: token, limit });
6350
- if (has4(a, "json")) return print(JSON.stringify(r2, null, 2)), 0;
6556
+ if (has6(a, "json")) return print(JSON.stringify(r2, null, 2)), 0;
6351
6557
  if (!r2.total) return print(`No recorded change of ${r2.path} in ${r2.source}.`), 0;
6352
6558
  print(`${r2.path}: ${r2.total} change(s) (${r2.source})
6353
6559
  `);
@@ -6359,8 +6565,8 @@ async function historyCmd(a, root) {
6359
6565
  return 0;
6360
6566
  }
6361
6567
  const tier = flag4(a, "tier");
6362
- const r = await hist.getHistory(ctx, { limit, ...flag4(a, "author") ? { author: flag4(a, "author") } : {}, ...tier ? { tier: Number(tier) } : {}, ...flag4(a, "version") ? { version: flag4(a, "version") } : {}, ...has4(a, "breaking") ? { breaking: true } : {} });
6363
- if (has4(a, "json")) return print(JSON.stringify(r, null, 2)), 0;
6568
+ const r = await hist.getHistory(ctx, { limit, ...flag4(a, "author") ? { author: flag4(a, "author") } : {}, ...tier ? { tier: Number(tier) } : {}, ...flag4(a, "version") ? { version: flag4(a, "version") } : {}, ...has6(a, "breaking") ? { breaking: true } : {} });
6569
+ if (has6(a, "json")) return print(JSON.stringify(r, null, 2)), 0;
6364
6570
  print(`${r.source}${r.currentVersion ? `, package version ${r.currentVersion}` : ""}`);
6365
6571
  if (!r.total) return print(r.note ?? "No token change matches."), 0;
6366
6572
  for (const v of r.versions) print(` ${v.version.padEnd(11)} ${day(v.date)} ${v.entries} change(s), ${v.tokenChanges} token edit(s)${v.breaking ? ", breaking" : ""}`);
@@ -6378,10 +6584,10 @@ ${r.truncated}`);
6378
6584
  async function revertCmd(a, root) {
6379
6585
  const target = a._[1];
6380
6586
  if (!target) throw new UsageError("bridge revert <pr|commit|id> [--dry-run] (ids from `bridge history`)");
6381
- const ctx = await BridgeContext5.create({ root });
6587
+ const ctx = await BridgeContext7.create({ root });
6382
6588
  const resolutions = Object.fromEntries((a.flags["use-before"] ?? []).map((k) => [k, "use-before"]));
6383
- const r = await hist.proposeRevert(ctx, { target, resolutions, restoreConflicts: has4(a, "restore-conflicts"), dryRun: has4(a, "dry-run") });
6384
- if (has4(a, "json")) {
6589
+ const r = await hist.proposeRevert(ctx, { target, resolutions, restoreConflicts: has6(a, "restore-conflicts"), dryRun: has6(a, "dry-run") });
6590
+ if (has6(a, "json")) {
6385
6591
  print(JSON.stringify(r, null, 2));
6386
6592
  return r.status === "blocked" ? 1 : 0;
6387
6593
  }
@@ -6406,11 +6612,11 @@ async function audit(a, root) {
6406
6612
  const target = a._[1];
6407
6613
  if (!target) throw new UsageError("bridge audit <path> (a product folder or file that uses the design system)");
6408
6614
  if (!existsSync6(target)) throw new UsageError(`No such file or folder: ${target}`);
6409
- const dsRoot = flag4(a, "ds") ? resolve4(flag4(a, "ds")) : existsSync6(join6(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
6615
+ const dsRoot = flag4(a, "ds") ? resolve5(flag4(a, "ds")) : existsSync6(join6(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
6410
6616
  if (!dsRoot) throw new UsageError(`No ${CONFIG_FILE2} found: run bridge audit from the design system repo or pass --ds <dir>.`);
6411
6617
  const ds = loadDesignSystem(dsRoot);
6412
- if (has4(a, "fix")) {
6413
- const write = has4(a, "write");
6618
+ if (has6(a, "fix")) {
6619
+ const write = has6(a, "write");
6414
6620
  const changes = fixCss(target, ds.index, write);
6415
6621
  const n = changes.reduce((acc, c) => acc + c.edits.length, 0);
6416
6622
  for (const c of changes) for (const e of c.edits) print(`${c.file}:${e.line}:${e.column} ${e.from} -> ${e.to}`);
@@ -6486,7 +6692,7 @@ async function audit(a, root) {
6486
6692
  code = 1;
6487
6693
  }
6488
6694
  }
6489
- if (has4(a, "no-regress") && report.baseline) {
6695
+ if (has6(a, "no-regress") && report.baseline) {
6490
6696
  const b = report.baseline;
6491
6697
  if ((b.tokenAdoption.delta ?? 0) < 0 || (b.componentAdoption.delta ?? 0) < 0 || b.hardcoded.delta > 0) {
6492
6698
  process.stderr.write("bridge audit: adoption regressed since the baseline\n");
@@ -6498,9 +6704,9 @@ async function audit(a, root) {
6498
6704
  async function docsCmd(a, root) {
6499
6705
  const [, sub, name] = a._;
6500
6706
  if (sub !== "draft" || !name) throw new UsageError("Usage: bridge docs draft <Component> [--json]");
6501
- const ctx = await BridgeContext5.create({ root });
6707
+ const ctx = await BridgeContext7.create({ root });
6502
6708
  const d = await ops.draftDocsOp(ctx, { name });
6503
- if (has4(a, "json")) {
6709
+ if (has6(a, "json")) {
6504
6710
  print(JSON.stringify({ component: d.component, path: d.path, exists: d.exists, prefill: d.prefill, prompt: d.prompt }, null, 2));
6505
6711
  return 0;
6506
6712
  }
@@ -6531,15 +6737,17 @@ async function main() {
6531
6737
  const argv = process.argv.slice(2);
6532
6738
  const a = parse(argv);
6533
6739
  const cmd = a._[0];
6534
- const root = resolve4(flag4(a, "root") ?? process.cwd());
6740
+ const root = resolve5(flag4(a, "root") ?? process.cwd());
6535
6741
  if (cmd === "request") return runRequest(a, root);
6536
6742
  if (cmd === "explore") return runExplore(a, root);
6537
6743
  if (cmd === "branch") return runBranch(a, root);
6744
+ if (cmd === "pieces") return runPieces(a, root);
6745
+ if (cmd === "figma") return runFigma(a, root);
6538
6746
  if (cmd === "products" || cmd === "settings" || cmd === "coverage") return runStudioRead(cmd, a, root);
6539
6747
  if (cmd === "docs" && a._[1] === "save") return runDocsSave(a, root);
6540
- if (!cmd || has4(a, "help") || cmd === "help") {
6748
+ if (!cmd || has6(a, "help") || cmd === "help") {
6541
6749
  print(HELP);
6542
- return cmd || has4(a, "help") ? 0 : 2;
6750
+ return cmd || has6(a, "help") ? 0 : 2;
6543
6751
  }
6544
6752
  if (cmd === "--version" || cmd === "version") return print(VERSION), 0;
6545
6753
  switch (cmd) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge-ds/cli",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "bridge CLI: set up Bridge in a design system repo, check tokens in CI, search and diff tokens, propose changes, start the MCP server.",
6
6
  "license": "MIT",
@@ -43,8 +43,8 @@
43
43
  "build:package": "node ../../scripts/build-packages.mjs cli"
44
44
  },
45
45
  "dependencies": {
46
- "@bridge-ds/mcp": "^0.4.0",
47
- "@bridge-ds/visual": "^0.3.1",
46
+ "@bridge-ds/mcp": "^0.6.0",
47
+ "@bridge-ds/visual": "^0.3.4",
48
48
  "typescript": "5.9.3"
49
49
  },
50
50
  "devDependencies": {