@bridge-ds/cli 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +768 -108
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { existsSync as existsSync5 } from "node:fs";
5
- import { readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
6
- import { isAbsolute, join as join5, relative as relative4, resolve as resolve3 } from "node:path";
4
+ import { existsSync as existsSync6 } from "node:fs";
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";
7
7
 
8
8
  // ../core/src/audit/types.ts
9
9
  var AUDIT_SCHEMA_VERSION = 1;
@@ -1212,7 +1212,8 @@ function classifyTag(t, imports, ds) {
1212
1212
  if (name === "a" && !t.hasHref) return {};
1213
1213
  if (t.inRenderProp) return {};
1214
1214
  const suggestion = nativeEquivalent(name, t.type, ds.components);
1215
- return { component: { ...at, name, kind: "native", library: "native", ...suggestion ? { suggestion } : {} } };
1215
+ if (!suggestion) return {};
1216
+ return { component: { ...at, name, kind: "native", library: "native", suggestion } };
1216
1217
  }
1217
1218
  const root = name.split(".")[0];
1218
1219
  const imp = imports.get(root);
@@ -2181,10 +2182,10 @@ function assertSafeBranch(branch, rules = {}) {
2181
2182
 
2182
2183
  // ../core/src/node/git-local.ts
2183
2184
  function git(root, args, opts = {}) {
2184
- return new Promise((resolve4, reject) => {
2185
+ return new Promise((resolve5, reject) => {
2185
2186
  const child = execFile("git", args, { cwd: root, env: { ...process.env, ...opts.env }, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
2186
2187
  if (err) reject(new Error(`git ${args.join(" ")}: ${stderr || err.message}`.trim()));
2187
- else resolve4(stdout);
2188
+ else resolve5(stdout);
2188
2189
  });
2189
2190
  if (opts.input !== void 0) child.stdin.end(opts.input);
2190
2191
  });
@@ -2431,6 +2432,47 @@ function visualImpactProblems(raw) {
2431
2432
  return out;
2432
2433
  }
2433
2434
 
2435
+ // ../core/src/products.ts
2436
+ var DEFAULT_PRODUCTS = [
2437
+ { key: "admin", name: "Admin demo", path: "apps/admin", framework: "react" },
2438
+ { key: "library", name: "Library", default: true }
2439
+ ];
2440
+ var KEY = /^[a-z0-9][a-z0-9-]*$/;
2441
+ var REPO = /^[\w.-]+\/[\w.-]+$/;
2442
+ var FIELDS = ["key", "name", "repo", "path", "preview", "framework", "default"];
2443
+ function productProblems(raw) {
2444
+ if (raw === void 0) return [];
2445
+ if (!Array.isArray(raw)) return ['"products" must be a list of { "key", "name" } objects'];
2446
+ if (!raw.length) return ['"products" must list at least one product, or be left out to use the defaults'];
2447
+ const problems = [];
2448
+ const seen = /* @__PURE__ */ new Set();
2449
+ let defaults = 0;
2450
+ raw.forEach((p, i) => {
2451
+ const at = `"products[${i}]`;
2452
+ if (!p || typeof p !== "object" || Array.isArray(p)) {
2453
+ problems.push(`${at}" must be an object with a "key" and a "name"`);
2454
+ return;
2455
+ }
2456
+ const o = p;
2457
+ for (const k of Object.keys(o)) if (!FIELDS.includes(k)) problems.push(`${at}.${k}" is not a known field (${FIELDS.join(", ")})`);
2458
+ if (typeof o.key !== "string" || !KEY.test(o.key)) problems.push(`${at}.key" must be lowercase letters, digits and dashes, such as "admin"`);
2459
+ else if (seen.has(o.key)) problems.push(`${at}.key" ("${o.key}") is used twice: give each product its own key`);
2460
+ else seen.add(o.key);
2461
+ if (typeof o.name !== "string" || !o.name.trim()) problems.push(`${at}.name" must be the name people read, such as "Admin demo"`);
2462
+ if (o.repo !== void 0 && (typeof o.repo !== "string" || !REPO.test(o.repo))) problems.push(`${at}.repo" must be "owner/name"`);
2463
+ if (o.path !== void 0) {
2464
+ const bad = typeof o.path !== "string" || !o.path.trim() || o.path.startsWith("/") || o.path.split("/").includes("..");
2465
+ if (bad) problems.push(`${at}.path" must be a folder relative to the repository root, without a leading "/" or "..", such as "apps/admin"`);
2466
+ }
2467
+ if (o.preview !== void 0 && (typeof o.preview !== "string" || !/^https?:\/\//.test(o.preview))) problems.push(`${at}.preview" must be an http(s) URL template, such as "https://app-git-{branch}.vercel.app"`);
2468
+ if (o.framework !== void 0 && (typeof o.framework !== "string" || !o.framework.trim())) problems.push(`${at}.framework" must be a name such as "react", "vue" or "other"`);
2469
+ if (o.default !== void 0 && typeof o.default !== "boolean") problems.push(`${at}.default" must be true or false`);
2470
+ if (o.default === true) defaults++;
2471
+ });
2472
+ if (defaults > 1) problems.push('"products" marks more than one product as default: keep "default": true on the library only');
2473
+ return problems;
2474
+ }
2475
+
2434
2476
  // ../core/src/config.ts
2435
2477
  var CONFIG_FILE = "bridge.config.json";
2436
2478
  var DEFAULT_OVERRIDE = { allowed: [], requireReason: true, minReasonLength: 20 };
@@ -2571,6 +2613,7 @@ function defineConfig(input) {
2571
2613
  } : {},
2572
2614
  ...g.rights ? { rights: normalizeRights(g.rights) } : {}
2573
2615
  },
2616
+ products: input.products?.length ? input.products.map((p) => ({ ...p })) : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
2574
2617
  ...input.audit ? { audit: { ...input.audit } } : {},
2575
2618
  ...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
2576
2619
  ...input.integrations ? { integrations: { ...input.integrations } } : {},
@@ -2693,6 +2736,7 @@ function loadConfig(source) {
2693
2736
  }
2694
2737
  }
2695
2738
  problems.push(...visualImpactProblems(d.visualImpact));
2739
+ problems.push(...productProblems(d.products));
2696
2740
  const audit2 = d.audit;
2697
2741
  if (audit2 !== void 0) {
2698
2742
  if (!isObj(audit2)) problems.push('"audit" must be an object');
@@ -2898,6 +2942,22 @@ function fixCss(target, ds, write) {
2898
2942
  return out;
2899
2943
  }
2900
2944
 
2945
+ // ../core/src/component-spec.ts
2946
+ var COMPONENT_SPECS_VERSION = 1;
2947
+ var variantKeyOf = (props) => Object.keys(props).sort().map((k) => `${k}=${props[k]}`).join(", ") || "Default";
2948
+ function parseComponentSpecs(text) {
2949
+ let data;
2950
+ try {
2951
+ data = JSON.parse(text);
2952
+ } catch {
2953
+ throw new Error("component-specs.json is not valid JSON.");
2954
+ }
2955
+ const f = data;
2956
+ if (!f || typeof f !== "object" || !Array.isArray(f.components)) throw new Error('component-specs.json has no "components" list.');
2957
+ if (f.version !== COMPONENT_SPECS_VERSION) throw new Error(`component-specs.json is version ${String(f.version)}; this tool reads version ${COMPONENT_SPECS_VERSION}.`);
2958
+ return f;
2959
+ }
2960
+
2901
2961
  // src/index.ts
2902
2962
  import { fileURLToPath } from "node:url";
2903
2963
 
@@ -3607,7 +3667,7 @@ var GitHubClient = class _GitHubClient {
3607
3667
  }
3608
3668
  async getPullRequest(n) {
3609
3669
  const r = await this.request("GET", `${this.repoPath}/pulls/${n}`);
3610
- return { ...toSummary(r), body: r.body ?? "", mergeable: r.mergeable ?? null };
3670
+ return { ...toSummary(r), body: r.body ?? "", mergeable: r.mergeable ?? null, requestedReviewers: (r.requested_reviewers ?? []).map((x) => x.login) };
3611
3671
  }
3612
3672
  async listPullRequestFiles(n) {
3613
3673
  const r = await this.request("GET", `${this.repoPath}/pulls/${n}/files?per_page=100`);
@@ -3684,6 +3744,100 @@ var GitHubClient = class _GitHubClient {
3684
3744
  async removeLabel(issueNumber2, label) {
3685
3745
  await this.request("DELETE", `${this.repoPath}/issues/${issueNumber2}/labels/${encodeURIComponent(label)}`);
3686
3746
  }
3747
+ // --- branch actions shared by Studio, the MCP server and the CLI (branch-actions.ts) ---
3748
+ /** The account the token belongs to (GET /user). Needs a token. */
3749
+ async getViewer() {
3750
+ this.assertToken("Reading who you are on GitHub");
3751
+ const r = await this.request("GET", "/user");
3752
+ return { login: r.login };
3753
+ }
3754
+ /** The repository's merge settings and the viewer's permissions (unknown settings count as allowed). */
3755
+ async getMergeSettings() {
3756
+ return this.request("GET", this.repoPath);
3757
+ }
3758
+ /**
3759
+ * Merges a pull request, only at `sha` (GitHub refuses when the branch moved meanwhile). The repository's branch
3760
+ * rules still apply: GitHub refuses a merge they block, whatever Bridge says.
3761
+ */
3762
+ async mergePullRequest(n, input) {
3763
+ this.assertToken("Merging a branch");
3764
+ const r = await this.request("PUT", `${this.repoPath}/pulls/${n}/merge`, {
3765
+ merge_method: input.method,
3766
+ sha: input.sha,
3767
+ ...input.commitMessage !== void 0 && input.method !== "rebase" ? { commit_message: input.commitMessage } : {}
3768
+ });
3769
+ return { sha: r?.sha ?? "", merged: r?.merged ?? true };
3770
+ }
3771
+ /** GitHub's "Update branch": merges the base into the pull request, only when its head is still `expectedHeadSha`. */
3772
+ async updatePullRequestBranch(n, expectedHeadSha) {
3773
+ this.assertToken("Updating a branch from main");
3774
+ await this.request("PUT", `${this.repoPath}/pulls/${n}/update-branch`, { expected_head_sha: expectedHeadSha });
3775
+ }
3776
+ /** Posts a comment on an issue or pull request and returns its id and link. */
3777
+ async createIssueComment(n, body) {
3778
+ this.assertToken("Commenting");
3779
+ const r = await this.request("POST", `${this.repoPath}/issues/${n}/comments`, { body });
3780
+ return { id: r.id, url: r.html_url };
3781
+ }
3782
+ /** Replaces the text of a comment. */
3783
+ async updateIssueComment(id, body) {
3784
+ this.assertToken("Editing a comment");
3785
+ await this.request("PATCH", `${this.repoPath}/issues/comments/${id}`, { body });
3786
+ }
3787
+ /** Submits a review on a given commit (the head the reviewer saw); the body may be empty for an approval. */
3788
+ async submitReview(n, input) {
3789
+ this.assertToken("Reviewing a branch");
3790
+ const r = await this.request("POST", `${this.repoPath}/pulls/${n}/reviews`, {
3791
+ event: input.event,
3792
+ ...input.body ? { body: input.body } : {},
3793
+ ...input.commitId ? { commit_id: input.commitId } : {}
3794
+ });
3795
+ return { id: r.id, user: r.user?.login, state: r.state, body: r.body ?? "" };
3796
+ }
3797
+ /** Line comments of a pull request (review threads), oldest first. */
3798
+ async listReviewComments(n) {
3799
+ const r = await this.request(
3800
+ "GET",
3801
+ `${this.repoPath}/pulls/${n}/comments?per_page=100`
3802
+ );
3803
+ return r.map((c) => ({ id: c.id, body: c.body ?? "", author: c.user?.login ?? "unknown", path: c.path, line: c.line ?? null, inReplyTo: c.in_reply_to_id ?? null, url: c.html_url, createdAt: c.created_at }));
3804
+ }
3805
+ /** Asks these people to review a pull request. */
3806
+ async requestReviewers(n, logins) {
3807
+ this.assertToken("Asking for a review");
3808
+ await this.request("POST", `${this.repoPath}/pulls/${n}/requested_reviewers`, { reviewers: logins });
3809
+ }
3810
+ /** Takes these people off the requested reviewers. */
3811
+ async removeRequestedReviewers(n, logins) {
3812
+ this.assertToken("Changing the requested reviewers");
3813
+ await this.request("DELETE", `${this.repoPath}/pulls/${n}/requested_reviewers`, { reviewers: logins });
3814
+ }
3815
+ /** Replies in the thread of a line comment. */
3816
+ async replyToReviewComment(n, commentId, body) {
3817
+ this.assertToken("Replying to a comment");
3818
+ const r = await this.request("POST", `${this.repoPath}/pulls/${n}/comments/${commentId}/replies`, { body });
3819
+ return { id: r.id, url: r.html_url };
3820
+ }
3821
+ /**
3822
+ * What `head` adds on top of `base`, with what library sync needs: the shared start (merge base), the base
3823
+ * commit and its tree, how far behind, and the files with their previous names.
3824
+ */
3825
+ async compareDetailed(base, head) {
3826
+ const r = await this.request("GET", `${this.repoPath}/compare/${encodeRef(base)}...${encodeRef(head)}`);
3827
+ return {
3828
+ behindBy: r.behind_by,
3829
+ aheadBy: r.ahead_by,
3830
+ mergeBase: r.merge_base_commit?.sha ?? null,
3831
+ baseSha: r.base_commit?.sha ?? null,
3832
+ baseTree: r.base_commit?.commit?.tree?.sha ?? null,
3833
+ files: (r.files ?? []).map((f) => ({ filename: f.filename, status: f.status, ...f.previous_filename ? { previous_filename: f.previous_filename } : {} }))
3834
+ };
3835
+ }
3836
+ /** A tree on top of `baseTree` from entries with their own file mode (blob sha, inline content, or sha null to delete). */
3837
+ async createTreeEntries(baseTree, entries) {
3838
+ const r = await this.request("POST", `${this.repoPath}/git/trees`, { base_tree: baseTree, tree: entries.map((e) => ({ ...e, type: "blob" })) }, true);
3839
+ return r.sha;
3840
+ }
3687
3841
  };
3688
3842
  function toCommitSummary(c) {
3689
3843
  return {
@@ -3922,6 +4076,132 @@ function jiraIssueFields(r, jira, issueUrl) {
3922
4076
  };
3923
4077
  }
3924
4078
 
4079
+ // ../core/src/fidelity-references.ts
4080
+ var FIGMA_REFERENCES_DIR = "figma-references";
4081
+ var FIGMA_REFERENCES_MANIFEST = `${FIGMA_REFERENCES_DIR}/manifest.json`;
4082
+ var FIGMA_REFERENCE_STORY = "internal-figma-reference--variant";
4083
+ var FIGMA_FILL_WIDTH_TOKEN = "size.80";
4084
+ var STATE_PSEUDO = {
4085
+ default: "rest",
4086
+ rest: "rest",
4087
+ enabled: "rest",
4088
+ hover: "hover",
4089
+ hovered: "hover",
4090
+ pressed: "active",
4091
+ active: "active",
4092
+ focus: "focus-visible",
4093
+ focused: "focus-visible",
4094
+ "focus visible": "focus-visible",
4095
+ disabled: "disabled"
4096
+ };
4097
+ var iconName = (component) => component.replace(/^Icon\//, "");
4098
+ var exportName = (name) => name.replace(/[^A-Za-z0-9]+/g, "");
4099
+ function planReference(spec, variant, file) {
4100
+ if (spec.view === "popup") return { ok: false, reason: "drawn open in Figma; the code image shows it closed" };
4101
+ const props = { ...spec.fixed ?? {} };
4102
+ let pseudo;
4103
+ for (const axis of spec.axes) {
4104
+ const value = variant.props[axis.name];
4105
+ if (value === void 0) return { ok: false, reason: `the variant has no ${axis.name} value` };
4106
+ if (axis.prop) {
4107
+ props[axis.prop] = value === "true" ? true : value === "false" ? false : value;
4108
+ continue;
4109
+ }
4110
+ if (axis.name === "Checked") {
4111
+ props.checked = value === "true";
4112
+ if (value === "mixed") props.indeterminate = true;
4113
+ continue;
4114
+ }
4115
+ if (axis.name.toLowerCase() !== "state") return { ok: false, reason: `${axis.name} has no matching option in code` };
4116
+ const s = STATE_PSEUDO[value.toLowerCase()];
4117
+ if (!s) return { ok: false, reason: `the ${value} state has no code equivalent to render` };
4118
+ if (s === "disabled") props.disabled = true;
4119
+ else if (s !== "rest") pseudo = s;
4120
+ }
4121
+ const root = spec.parts.find((p) => !p.parent);
4122
+ if (!root) return { ok: false, reason: "the spec has no root layer" };
4123
+ const children = (id) => spec.parts.filter((p) => p.parent === id);
4124
+ const textOf = (part) => {
4125
+ const swapTo = variant.slots?.[part.id] ?? part.swap?.default;
4126
+ if (part.swap && swapTo) {
4127
+ const slot = file?.slots?.find((s) => s.name === swapTo);
4128
+ if (!slot) return { reason: `${swapTo} is not in the specs` };
4129
+ if (slot.kind !== "text" || !slot.text) return { reason: `Figma shows a placeholder (${swapTo}) that code has no example of` };
4130
+ return slot.text.default;
4131
+ }
4132
+ const prop = spec.properties.find((p) => p.name === part.text && p.type === "TEXT");
4133
+ return typeof prop?.default === "string" ? prop.default : part.name;
4134
+ };
4135
+ let text;
4136
+ const icons = {};
4137
+ for (const prop of spec.properties) {
4138
+ if (prop.type === "TEXT") {
4139
+ const hidden = spec.properties.some((b) => b.type === "BOOLEAN" && b.prop === prop.prop && b.default === false);
4140
+ if (hidden && prop.prop) continue;
4141
+ if (prop.prop === "children") text = String(prop.default);
4142
+ else if (prop.prop) props[prop.prop] = String(prop.default);
4143
+ continue;
4144
+ }
4145
+ const iconPart = spec.parts.find((p) => p.kind === "icon" && p.visible === prop.name);
4146
+ if (prop.default === true && !iconPart && prop.prop && props[prop.prop] === void 0 && spec.example?.p?.[prop.prop] === void 0) props[prop.prop] = true;
4147
+ }
4148
+ for (const part of spec.parts) {
4149
+ if (part.kind !== "icon" || !part.swap) continue;
4150
+ const shown = part.visible ? spec.properties.find((p) => p.name === part.visible)?.default === true : true;
4151
+ if (!shown) continue;
4152
+ if (!part.swap.prop) return { ok: false, reason: `the ${part.name} icon has no matching option in code` };
4153
+ icons[part.swap.prop] = iconName(variant.slots?.[part.id] ?? part.swap.default);
4154
+ }
4155
+ let parts;
4156
+ const loose = spec.properties.filter((p) => p.type === "TEXT" && !p.prop);
4157
+ const frames = children(root.id).filter((p) => p.kind === "frame");
4158
+ const compound = text === void 0 && frames.length > 0 && frames.every((f) => spec.parts.some((p) => p.parent === f.id && p.kind === "text" && (p.swap || p.text === f.name)));
4159
+ if (compound) {
4160
+ parts = [];
4161
+ for (const f of frames) {
4162
+ const t = spec.parts.find((p) => p.parent === f.id && p.kind === "text");
4163
+ const value = textOf(t);
4164
+ if (typeof value !== "string") return { ok: false, reason: value.reason };
4165
+ parts.push({ component: `${exportName(spec.name)}${exportName(f.name)}`, text: value });
4166
+ }
4167
+ }
4168
+ for (const p of loose) {
4169
+ if (p.name === "Value") continue;
4170
+ const part = spec.parts.find((x) => x.text === p.name);
4171
+ if (compound && part && part.parent !== root.id) continue;
4172
+ if (part && /placeholder$/.test(part.id)) {
4173
+ props.placeholder = String(p.default);
4174
+ continue;
4175
+ }
4176
+ return { ok: false, reason: `the ${p.name} text has no matching option in code` };
4177
+ }
4178
+ const example = spec.example;
4179
+ const exampleChildren = example?.children?.length ? example.children : void 0;
4180
+ return {
4181
+ ok: true,
4182
+ render: {
4183
+ component: exportName(spec.view || spec.fixed || example ? example?.c ?? spec.component ?? spec.name : spec.name),
4184
+ props: { ...example?.p ?? {}, ...props },
4185
+ ...exampleChildren ? { children: exampleChildren } : {},
4186
+ ...text !== void 0 && !exampleChildren ? { text } : {},
4187
+ ...parts && !exampleChildren ? { parts } : {},
4188
+ ...Object.keys(icons).length ? { icons } : {},
4189
+ ...pseudo ? { pseudo } : {},
4190
+ ...root.fill ? { width: FIGMA_FILL_WIDTH_TOKEN } : {}
4191
+ }
4192
+ };
4193
+ }
4194
+ function referenceImagePath(component, key) {
4195
+ const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4196
+ const file = key.split(",").map((kv) => slug(kv)).filter(Boolean).join("__");
4197
+ return `${slug(component)}/${file || "default"}.png`;
4198
+ }
4199
+ function planReferences(file) {
4200
+ return file.components.flatMap(
4201
+ (spec) => spec.variants.map((v) => ({ component: spec.name, specHash: spec.hash, key: variantKeyOf(v.props), plan: planReference(spec, v, file) }))
4202
+ );
4203
+ }
4204
+
3925
4205
  // ../visual/src/capture.ts
3926
4206
  import { mkdir, writeFile } from "node:fs/promises";
3927
4207
  import { dirname as dirname2 } from "node:path";
@@ -3945,8 +4225,8 @@ async function launch(o = {}) {
3945
4225
  args: ["--lang=en-US", "--disable-gpu", "--font-render-hinting=none", "--disable-lcd-text", "--force-color-profile=srgb", "--hide-scrollbars", "--disable-skia-runtime-opts", "--disable-renderer-backgrounding"]
3946
4226
  });
3947
4227
  }
3948
- async function newContext(browser, viewport) {
3949
- const ctx = await browser.newContext({ viewport, deviceScaleFactor: 1, reducedMotion: "reduce", colorScheme: "light", locale: "en-US", timezoneId: "UTC", serviceWorkers: "block" });
4228
+ async function newContext(browser, viewport, deviceScaleFactor = 1) {
4229
+ const ctx = await browser.newContext({ viewport, deviceScaleFactor, reducedMotion: "reduce", colorScheme: "light", locale: "en-US", timezoneId: "UTC", serviceWorkers: "block" });
3950
4230
  await ctx.addInitScript(INIT_SCRIPT);
3951
4231
  await ctx.route(
3952
4232
  /^https?:\/\/(?!127\.0\.0\.1[:/]|localhost[:/])/,
@@ -4019,15 +4299,15 @@ async function shoot(page, r) {
4019
4299
  }
4020
4300
  async function switchStory(page, storyId2) {
4021
4301
  const ok = await page.evaluate(
4022
- (id) => new Promise((resolve4) => {
4302
+ (id) => new Promise((resolve5) => {
4023
4303
  const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
4024
- if (!ch) return resolve4(false);
4304
+ if (!ch) return resolve5(false);
4025
4305
  const events = ["storyRendered", "storyErrored", "storyThrewException", "storyMissing", "playFunctionThrewException"];
4026
4306
  const timer = setTimeout(() => done(), 15e3);
4027
4307
  function done() {
4028
4308
  clearTimeout(timer);
4029
4309
  for (const e of events) ch.off(e, done);
4030
- resolve4(true);
4310
+ resolve5(true);
4031
4311
  }
4032
4312
  for (const e of events) ch.on(e, done);
4033
4313
  window.scrollTo(0, 0);
@@ -4379,9 +4659,9 @@ function selectImpact(input) {
4379
4659
  app
4380
4660
  };
4381
4661
  }
4382
- function storyId(title, exportName) {
4662
+ function storyId(title, exportName2) {
4383
4663
  const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
4384
- const startCase = exportName.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
4664
+ const startCase = exportName2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
4385
4665
  return `${sanitize(title)}--${sanitize(startCase)}`;
4386
4666
  }
4387
4667
  function storiesFromSource(files) {
@@ -4440,12 +4720,12 @@ function serveDir(dir) {
4440
4720
  res.writeHead(200, { "content-type": TYPES[extname(file)] ?? "application/octet-stream", "cache-control": "no-store" });
4441
4721
  createReadStream(file).pipe(res);
4442
4722
  });
4443
- return new Promise((resolve4, reject) => {
4723
+ return new Promise((resolve5, reject) => {
4444
4724
  server.once("error", reject);
4445
4725
  server.listen(0, "127.0.0.1", () => {
4446
4726
  const addr = server.address();
4447
4727
  const port = typeof addr === "object" && addr ? addr.port : 0;
4448
- resolve4({ url: `http://127.0.0.1:${port}`, close: () => new Promise((r) => server.close(() => r())) });
4728
+ resolve5({ url: `http://127.0.0.1:${port}`, close: () => new Promise((r) => server.close(() => r())) });
4449
4729
  });
4450
4730
  });
4451
4731
  }
@@ -4517,7 +4797,7 @@ async function removeWorktree(repo, dir) {
4517
4797
  await git(repo, ["worktree", "prune"]).catch(() => void 0);
4518
4798
  }
4519
4799
  function run(cmd, args, cwd, log, root = cwd, env = {}) {
4520
- return new Promise((resolve4, reject) => {
4800
+ return new Promise((resolve5, reject) => {
4521
4801
  const bin = [.../* @__PURE__ */ new Set([join3(cwd, "node_modules", ".bin"), join3(root, "node_modules", ".bin")])].join(":");
4522
4802
  const child = spawn(cmd, args, {
4523
4803
  cwd,
@@ -4530,7 +4810,7 @@ function run(cmd, args, cwd, log, root = cwd, env = {}) {
4530
4810
  child.stderr.on("data", keep);
4531
4811
  child.on("error", reject);
4532
4812
  child.on("close", (code) => {
4533
- if (code === 0) resolve4();
4813
+ if (code === 0) resolve5();
4534
4814
  else {
4535
4815
  log(tail);
4536
4816
  reject(new Error(`${cmd} ${args.join(" ")} failed in ${cwd} (exit ${code})`));
@@ -4896,9 +5176,105 @@ async function runImpact(o) {
4896
5176
  return result;
4897
5177
  }
4898
5178
 
5179
+ // ../visual/src/references.ts
5180
+ import { existsSync as existsSync5 } from "node:fs";
5181
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
5182
+ import { dirname as dirname4, join as join5 } from "node:path";
5183
+ async function captureFigmaReferences(o) {
5184
+ const log = o.log ?? (() => void 0);
5185
+ const scale = o.scale ?? 2;
5186
+ const brand2 = o.brand ?? "bridge";
5187
+ const theme = o.theme ?? "light";
5188
+ if (!existsSync5(join5(o.storybook, "index.json"))) throw new Error(`No built Storybook in ${o.storybook}: run npm run build-storybook first.`);
5189
+ const index = JSON.parse(await readFile4(join5(o.storybook, "index.json"), "utf8"));
5190
+ if (!index.entries[FIGMA_REFERENCE_STORY]) throw new Error(`The Storybook build has no ${FIGMA_REFERENCE_STORY} story (packages/react/src/stories/FigmaReference.stories.tsx).`);
5191
+ const specs = parseComponentSpecs(await readFile4(o.specs, "utf8"));
5192
+ const jobs = planReferences(specs).filter((j) => !o.only?.length || o.only.includes(j.component));
5193
+ const manifest = {
5194
+ version: 1,
5195
+ tool: "@bridge-ds/visual",
5196
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5197
+ sha: o.sha,
5198
+ scale,
5199
+ mode: { brand: brand2, theme, density: "comfortable" },
5200
+ storyId: FIGMA_REFERENCE_STORY,
5201
+ components: {}
5202
+ };
5203
+ for (const j of jobs) manifest.components[j.component] ??= { specHash: j.specHash, variants: {} };
5204
+ let captured = 0;
5205
+ let skipped = 0;
5206
+ const served = await serveDir(o.storybook);
5207
+ const browser = await launch();
5208
+ try {
5209
+ const ctx = await newContext(browser, { width: 1200, height: 800 }, scale);
5210
+ const page = await newPage(ctx);
5211
+ await page.goto(storyUrl(served.url, FIGMA_REFERENCE_STORY, brand2, theme), { waitUntil: "load", timeout: 3e4 });
5212
+ await page.waitForSelector("[data-fidelity-root], [data-fidelity-error]", { state: "attached", timeout: 2e4 });
5213
+ await settle(page, { busy: false });
5214
+ log(`Capturing ${jobs.length} variants at ${scale}x (${brand2} ${theme})`);
5215
+ for (const j of jobs) {
5216
+ const variants = manifest.components[j.component].variants;
5217
+ if (!j.plan.ok) {
5218
+ variants[j.key] = { skipped: j.plan.reason };
5219
+ skipped++;
5220
+ continue;
5221
+ }
5222
+ const plan = JSON.stringify(j.plan.render);
5223
+ await page.evaluate(
5224
+ ({ id, plan: plan2 }) => new Promise((resolve5) => {
5225
+ const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
5226
+ const done = () => {
5227
+ clearTimeout(timer);
5228
+ ch.off("storyRendered", done);
5229
+ resolve5();
5230
+ };
5231
+ const timer = setTimeout(done, 1e4);
5232
+ ch.on("storyRendered", done);
5233
+ ch.emit("updateStoryArgs", { storyId: id, updatedArgs: { plan: plan2 } });
5234
+ }),
5235
+ { id: FIGMA_REFERENCE_STORY, plan }
5236
+ );
5237
+ await settle(page, { network: false, quiet: 2, interval: 40, busy: false });
5238
+ const error = await page.locator("[data-fidelity-error]").first().textContent({ timeout: 200 }).catch(() => null);
5239
+ let target = page.locator("[data-fidelity-root] > *").first();
5240
+ if (!error && !await target.count()) {
5241
+ const marked = page.locator("[data-fidelity-target]").first();
5242
+ await marked.waitFor({ state: "visible", timeout: 3e3 }).catch(() => void 0);
5243
+ await settle(page, { network: false, quiet: 2, interval: 40, busy: false });
5244
+ target = marked;
5245
+ }
5246
+ if (error || !await target.count()) {
5247
+ variants[j.key] = { skipped: error ?? "the code example did not render" };
5248
+ skipped++;
5249
+ continue;
5250
+ }
5251
+ const file = referenceImagePath(j.component, j.key);
5252
+ const path = join5(o.out, file);
5253
+ await mkdir4(dirname4(path), { recursive: true });
5254
+ await target.screenshot({ path, omitBackground: true, animations: "disabled", caret: "hide", scale: "device" });
5255
+ const box = await target.boundingBox();
5256
+ variants[j.key] = { file, width: Math.round(box?.width ?? 0), height: Math.round(box?.height ?? 0) };
5257
+ captured++;
5258
+ }
5259
+ await ctx.close();
5260
+ } finally {
5261
+ await browser.close().catch(() => void 0);
5262
+ await served.close();
5263
+ }
5264
+ await mkdir4(o.out, { recursive: true });
5265
+ const manifestPath = join5(o.out, "manifest.json");
5266
+ await writeFile4(manifestPath, `${JSON.stringify(manifest, null, 2)}
5267
+ `);
5268
+ log(`${captured} images, ${skipped} variants skipped. ${manifestPath}`);
5269
+ return { manifest, manifestPath, captured, skipped };
5270
+ }
5271
+
5272
+ // src/index.ts
5273
+ import { execFileSync as execFileSync2 } from "node:child_process";
5274
+
4899
5275
  // src/request.ts
4900
5276
  import { execFileSync, spawnSync } from "node:child_process";
4901
- import { readFile as readFile4 } from "node:fs/promises";
5277
+ import { readFile as readFile5 } from "node:fs/promises";
4902
5278
  import { BridgeContext as BridgeContext2, requests as requests2 } from "@bridge-ds/mcp";
4903
5279
 
4904
5280
  // src/jira-sync.ts
@@ -4994,7 +5370,7 @@ Usage:
4994
5370
  --branch: implement it on that exploration, never in a pull request to main
4995
5371
  bridge request create --component Button --kind new-variant --title "Add a danger ghost variant"
4996
5372
  --summary "..." [--change field=before:after ...] [--figma url]
4997
- [--exploration explore/<name>] [--dry-run] [--json]
5373
+ [--exploration explore/<name>] [--product <key>] [--dry-run] [--json]
4998
5374
  bridge request jira-sync
4999
5375
  GitHub Action step: mirror a request issue to Jira (see docs/REQUESTS.md)
5000
5376
 
@@ -5149,6 +5525,7 @@ Claude Code is not installed (no \`claude\` on PATH). Install it (npm install -g
5149
5525
  changes: (a.flags.change ?? []).map(parseChange),
5150
5526
  ...figma ? { figmaUrl: figma } : {},
5151
5527
  ...exploration ? { exploration } : {},
5528
+ ...flag(a, "product") ? { product: flag(a, "product") } : {},
5152
5529
  source: "cli",
5153
5530
  dryRun
5154
5531
  });
@@ -5172,7 +5549,7 @@ ${r.issue.body}`);
5172
5549
  let event = {};
5173
5550
  if (eventPath) {
5174
5551
  try {
5175
- event = JSON.parse(await readFile4(eventPath, "utf8"));
5552
+ event = JSON.parse(await readFile5(eventPath, "utf8"));
5176
5553
  } catch {
5177
5554
  throw new RequestUsageError(`Could not read the GitHub event at ${eventPath}.`);
5178
5555
  }
@@ -5194,7 +5571,7 @@ staging branch without touching main (an explore/<name> branch with a draft pull
5194
5571
 
5195
5572
  Usage:
5196
5573
  bridge explore start <name> --purpose "..." [--use-case ${EXPLORATION_USE_CASES.map((u) => u.value).join("|")}]
5197
- [--experimental "..." ...] [--dry-run] [--json]
5574
+ [--experimental "..." ...] [--product <key>] [--dry-run] [--json]
5198
5575
  GitHub token: the branch and the draft pull request on GitHub. Without one, in a git repo:
5199
5576
  a local branch only, nothing pushed
5200
5577
  bridge explore list [--all] [--json]
@@ -5232,7 +5609,7 @@ async function runExplore(a, root, io = defaultIO()) {
5232
5609
  if (!name || !purpose) throw new RequestUsageError('Usage: bridge explore start <name> --purpose "What you want to learn"');
5233
5610
  const useCase = flag2(a, "use-case");
5234
5611
  const ctx = await context2(root, io);
5235
- const r = await explorations.startExploration(ctx, { name, purpose, ...exploreSource(io.env), ...useCase ? { useCase } : {}, ...a.flags.experimental ? { experimental: a.flags.experimental } : {}, dryRun: has2(a, "dry-run") });
5612
+ const r = await explorations.startExploration(ctx, { name, purpose, ...exploreSource(io.env), ...useCase ? { useCase } : {}, ...a.flags.experimental ? { experimental: a.flags.experimental } : {}, ...flag2(a, "product") ? { product: flag2(a, "product") } : {}, dryRun: has2(a, "dry-run") });
5236
5613
  if (json) return io.print(JSON.stringify(r, null, 2)), 0;
5237
5614
  if (r.status === "dry-run") {
5238
5615
  io.print(`Dry run, nothing written.
@@ -5302,6 +5679,231 @@ ${EXPLORE_HELP}`);
5302
5679
  }
5303
5680
  }
5304
5681
 
5682
+ // src/branch.ts
5683
+ import { readFile as readFile6 } from "node:fs/promises";
5684
+ import { resolve as resolve3 } from "node:path";
5685
+ import { BridgeContext as BridgeContext4, branches, studio } from "@bridge-ds/mcp";
5686
+ var BRANCH_HELP = `bridge branch: branches in review, the same actions as Bridge Studio
5687
+
5688
+ Usage:
5689
+ bridge branch list [--limit 20] [--json] what needs me: the review queue, mine first
5690
+ bridge branch status <n> [--json] status, approvals, tests, main, may I merge, next step
5691
+ bridge branch comments <n> [--json] the conversation and line threads (ids to reply to)
5692
+ bridge branch ready <n> send a draft for review (its author)
5693
+ bridge branch draft <n> back to draft (its author)
5694
+ bridge branch review <n> --approve [--body "..."] | --request-changes "..." | --comment "..."
5695
+ bridge branch ask <n> [--dry-run] ask someone else to review (who can give the missing approvals)
5696
+ bridge branch comment <n> "text" [--reply-to <id>]
5697
+ bridge branch merge <n> [--override "reason"] [--dry-run]
5698
+ same gate as the Merge button; --override: owner override,
5699
+ the reason is posted on the branch first
5700
+ bridge branch close <n> | bridge branch reopen <n>
5701
+ bridge branch update <n> [--choice <key>=mine|theirs ...] [--dry-run]
5702
+ update from main; token conflicts: pick per key
5703
+ bridge branch try <n> [--json] try it in a product, share it with a product team
5704
+ bridge branch combine <n> <n> [<n> <n>] [--pick <key>=<n> ...] [--json]
5705
+ try 2 to 4 token branches together (writes nothing)
5706
+ bridge products [--json] the products and the one this folder belongs to
5707
+ bridge settings [--json] roles, who ships, owner override, protection, products
5708
+ bridge coverage [Component] [--json] which components are in Figma
5709
+ bridge docs save <Component> --file docs.json [--pr <n>] [--title "..."] [--jira DS-42]
5710
+ [--product admin] [--dry-run] [--json]
5711
+ save usage guidance as in the Library edit mode
5712
+
5713
+ Start work for a product: bridge propose|explore start|request create ... --product <key> [--jira DS-42].
5714
+ GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
5715
+ var flag3 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
5716
+ var has3 = (a, k) => Boolean(a.flags[k]);
5717
+ async function context3(root, io) {
5718
+ const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
5719
+ return BridgeContext4.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
5720
+ }
5721
+ function number(a, at, usage) {
5722
+ const raw = a._[at];
5723
+ if (!raw) throw new RequestUsageError(`Usage: ${usage}`);
5724
+ return branches.branchNumber(raw);
5725
+ }
5726
+ function pairs(list2, what) {
5727
+ const out = {};
5728
+ for (const s of list2 ?? []) {
5729
+ const i = s.lastIndexOf("=");
5730
+ if (i <= 0) throw new RequestUsageError(`--${what} needs key=value, got "${s}".`);
5731
+ out[s.slice(0, i)] = s.slice(i + 1);
5732
+ }
5733
+ return out;
5734
+ }
5735
+ var said = (r) => String(r.said ?? r.summary ?? r.status ?? "Done.");
5736
+ async function runBranch(a, root, io = defaultIO()) {
5737
+ const sub = a._[1];
5738
+ if (!sub || sub === "help" || has3(a, "help")) {
5739
+ io.print(BRANCH_HELP);
5740
+ return sub || has3(a, "help") ? 0 : 2;
5741
+ }
5742
+ const json = has3(a, "json");
5743
+ const ctx = await context3(root, io);
5744
+ const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
5745
+ switch (sub) {
5746
+ case "list": {
5747
+ const limit = flag3(a, "limit");
5748
+ const r = await branches.whatNeedsMe(ctx, limit ? { limit: Number(limit) } : {});
5749
+ return out(r, () => {
5750
+ io.print(r.summary);
5751
+ for (const b of r.branches) io.print(`#${b.pr} ${b.statusLabel.padEnd(22)} ${b.title}${b.needsYou ? ` <- ${b.needsYou}` : ""}`);
5752
+ });
5753
+ }
5754
+ case "status": {
5755
+ const r = await branches.branchStatus(ctx, { pr: number(a, 2, "bridge branch status <n>") });
5756
+ return out(r, () => {
5757
+ io.print(`#${r.pr} ${r.title} (${r.product.name})`);
5758
+ io.print(`${r.status.label}. ${r.approvalNeeded} needed; approved by ${r.approvals.approvedBy.map((x) => `@${x}`).join(", ") || "nobody yet"}.`);
5759
+ if (r.merge.blockedBy?.length) for (const x of r.merge.blockedBy) io.print(` ${x}`);
5760
+ io.print(`Next: ${r.next}`);
5761
+ io.print(r.url);
5762
+ });
5763
+ }
5764
+ case "comments": {
5765
+ const r = await branches.listComments(ctx, { pr: number(a, 2, "bridge branch comments <n>") });
5766
+ return out(r, () => {
5767
+ for (const c of r.conversation) io.print(`@${c.author}: ${c.body}`);
5768
+ for (const t of r.threads) io.print(`[thread ${t.id}] ${t.file}:${t.line ?? ""} @${t.author}: ${t.body}${t.replies.length ? ` (${t.replies.length} replies)` : ""}`);
5769
+ });
5770
+ }
5771
+ case "ready": {
5772
+ const r = await branches.sendForReview(ctx, { pr: number(a, 2, "bridge branch ready <n>") });
5773
+ return out(r, () => io.print(r.said));
5774
+ }
5775
+ case "draft": {
5776
+ const r = await branches.backToDraft(ctx, { pr: number(a, 2, "bridge branch draft <n>") });
5777
+ return out(r, () => io.print(r.said));
5778
+ }
5779
+ case "review": {
5780
+ const n = number(a, 2, 'bridge branch review <n> --approve | --request-changes "..." | --comment "..."');
5781
+ const decision = has3(a, "approve") ? "approve" : has3(a, "request-changes") ? "request_changes" : has3(a, "comment") ? "comment" : null;
5782
+ if (!decision) throw new RequestUsageError('Say what you decide: --approve, --request-changes "what to change" or --comment "text".');
5783
+ const body = decision === "approve" ? flag3(a, "body") : flag3(a, decision === "request_changes" ? "request-changes" : "comment");
5784
+ const r = await branches.reviewBranch(ctx, { pr: n, decision, ...body && body !== "true" ? { body } : {} });
5785
+ return out(r, () => io.print(r.said));
5786
+ }
5787
+ case "ask": {
5788
+ const r = await branches.askSomeoneElse(ctx, { pr: number(a, 2, "bridge branch ask <n>"), dryRun: has3(a, "dry-run") });
5789
+ return out(r, () => io.print(r.said));
5790
+ }
5791
+ case "comment": {
5792
+ const n = number(a, 2, 'bridge branch comment <n> "text"');
5793
+ const body = a._.slice(3).join(" ") || flag3(a, "body");
5794
+ if (!body) throw new RequestUsageError('Usage: bridge branch comment <n> "text" [--reply-to <id>]');
5795
+ const reply = flag3(a, "reply-to");
5796
+ const r = await branches.commentOnBranch(ctx, { pr: n, body, ...reply ? { replyTo: Number(reply) } : {} });
5797
+ return out(r, () => io.print(r.said));
5798
+ }
5799
+ case "merge": {
5800
+ const n = number(a, 2, 'bridge branch merge <n> [--override "reason"] [--dry-run]');
5801
+ const reason = flag3(a, "override");
5802
+ const r = await branches.mergeBranch(ctx, { pr: n, ...reason ? { overrideReason: reason } : {}, dryRun: has3(a, "dry-run") });
5803
+ return out(r, () => io.print(r.said));
5804
+ }
5805
+ case "close": {
5806
+ const r = await branches.closeBranch(ctx, { pr: number(a, 2, "bridge branch close <n>") });
5807
+ return out(r, () => io.print(r.said));
5808
+ }
5809
+ case "reopen": {
5810
+ const r = await branches.reopenBranch(ctx, { pr: number(a, 2, "bridge branch reopen <n>") });
5811
+ return out(r, () => io.print(r.said));
5812
+ }
5813
+ case "update": {
5814
+ const n = number(a, 2, "bridge branch update <n> [--choice key=mine|theirs] [--dry-run]");
5815
+ const choices = pairs(a.flags.choice, "choice");
5816
+ for (const [k, v] of Object.entries(choices)) if (v !== "mine" && v !== "theirs") throw new RequestUsageError(`--choice ${k}= takes mine or theirs, got "${v}".`);
5817
+ const r = await branches.updateFromMain(ctx, { pr: n, ...Object.keys(choices).length ? { choices } : {}, dryRun: has3(a, "dry-run") });
5818
+ return out(r, () => {
5819
+ io.print(said(r));
5820
+ for (const c of r.conflicts ?? []) io.print(` ${c.key} (${c.token}, default ${c.default ?? "none: pick one"})`);
5821
+ });
5822
+ }
5823
+ case "try": {
5824
+ const r = await branches.tryBranch(ctx, { pr: number(a, 2, "bridge branch try <n>") });
5825
+ return out(r, () => {
5826
+ for (const p of r.tryInProduct) io.print(`${p.name}: ${p.url ?? p.how}`);
5827
+ io.print(`
5828
+ Share with a product team: ${r.share.install ? r.share.install : r.share.sentence}`);
5829
+ });
5830
+ }
5831
+ case "combine": {
5832
+ const prs = a._.slice(2);
5833
+ const picks = Object.fromEntries(Object.entries(pairs(a.flags.pick, "pick")).map(([k, v]) => [k, branches.branchNumber(v)]));
5834
+ const r = await branches.combineBranches(ctx, { prs, ...Object.keys(picks).length ? { picks } : {} });
5835
+ return out(r, () => {
5836
+ io.print(`${r.tokens} tokens change together.`);
5837
+ for (const c of r.conflicts) io.print(` ${c.key}: pick one of ${c.options.map((o) => `#${o.use}`).join(", ")}${c.picked ? ` (picked #${c.picked})` : ""}`);
5838
+ for (const [k, v] of Object.entries(r.open)) io.print(`${k}: ${v}`);
5839
+ io.print(r.next);
5840
+ });
5841
+ }
5842
+ default:
5843
+ throw new RequestUsageError(`Unknown branch command "${sub}".
5844
+
5845
+ ${BRANCH_HELP}`);
5846
+ }
5847
+ }
5848
+ async function runStudioRead(cmd, a, root, io = defaultIO()) {
5849
+ const ctx = await context3(root, io);
5850
+ const json = has3(a, "json");
5851
+ if (cmd === "products") {
5852
+ const r2 = await branches.listProducts(ctx);
5853
+ if (json) return io.print(JSON.stringify(r2, null, 2)), 0;
5854
+ for (const p of r2.products) io.print(`${p.key === r2.current.key ? "*" : " "} ${p.key.padEnd(12)} ${p.name}${p.default ? " (default)" : ""}${p.path ? ` ${p.path}` : ""}${p.repo ? ` ${p.repo}` : ""}`);
5855
+ io.print(`
5856
+ This folder: ${r2.current.name}. Pick another with --product <key>.`);
5857
+ return 0;
5858
+ }
5859
+ if (cmd === "settings") {
5860
+ const r2 = await studio.getSettings(ctx);
5861
+ if (json) return io.print(JSON.stringify(r2, null, 2)), 0;
5862
+ io.print(`Design approval: ${r2.roles.designApproval.join(", ") || "any reviewer"}`);
5863
+ io.print(`Tech approval: ${r2.roles.techApproval.join(", ") || "any reviewer"}`);
5864
+ io.print(`Merge: ${r2.roles.ship}`);
5865
+ io.print(`Owner override: ${r2.ownerOverride.on ? `${r2.ownerOverride.who.join(", ")}. ${r2.ownerOverride.reason}` : r2.ownerOverride.reason}`);
5866
+ io.print(`Protection: ${r2.protection}`);
5867
+ io.print(`Products: ${r2.products.map((p) => p.name).join(", ")}`);
5868
+ return 0;
5869
+ }
5870
+ const r = await studio.figmaCoverage(ctx, a._[1] ? { component: a._[1] } : {});
5871
+ if (json) return io.print(JSON.stringify(r, null, 2)), 0;
5872
+ io.print(r.summary);
5873
+ for (const c of r.components) io.print(` ${c.name.padEnd(20)} ${c.inFigma}${c.why ? ` (${c.why})` : ""}`);
5874
+ if (r.fidelity) io.print(r.fidelity);
5875
+ return 0;
5876
+ }
5877
+ async function runDocsSave(a, root, io = defaultIO()) {
5878
+ const component = a._[2];
5879
+ const file = flag3(a, "file");
5880
+ if (!component || !file) throw new RequestUsageError('Usage: bridge docs save <Component> --file docs.json [--pr <n>] [--title "..."] [--jira DS-42] [--product admin] [--dry-run]');
5881
+ let docs;
5882
+ try {
5883
+ docs = JSON.parse(await readFile6(resolve3(root, file), "utf8"));
5884
+ } catch (e) {
5885
+ throw new RequestUsageError(`${file} is not a readable JSON file: ${e.message}`);
5886
+ }
5887
+ const ctx = await context3(root, io);
5888
+ const pr = flag3(a, "pr");
5889
+ const r = await studio.saveDocs(ctx, {
5890
+ component,
5891
+ docs,
5892
+ ...pr ? { pr } : {},
5893
+ ...flag3(a, "title") ? { title: flag3(a, "title") } : {},
5894
+ ...flag3(a, "notes") ? { notes: flag3(a, "notes") } : {},
5895
+ ...flag3(a, "jira") ? { jira: flag3(a, "jira") } : {},
5896
+ ...flag3(a, "product") ? { product: flag3(a, "product") } : {},
5897
+ dryRun: has3(a, "dry-run")
5898
+ });
5899
+ if (has3(a, "json")) io.print(JSON.stringify(r, null, 2));
5900
+ else io.print(r.status === "dry-run" || r.status === "preview" ? `${r.said ?? "Dry run, nothing written."}
5901
+
5902
+ ${r.path}:
5903
+ ${r.content}` : String(r.said));
5904
+ return 0;
5905
+ }
5906
+
5305
5907
  // src/protection.ts
5306
5908
  var UNKNOWN = { kind: "unknown", missing: ["review", "checks", "history", "deletion"] };
5307
5909
  async function protectionReport(opts) {
@@ -5334,7 +5936,7 @@ async function rolesReport(opts) {
5334
5936
  }
5335
5937
 
5336
5938
  // src/index.ts
5337
- import { brand, BridgeContext as BridgeContext4, CONFIG_FILE as CONFIG_FILE2, core, detectConfig, GitRefSource, history as hist, LocalSource, ops, optionsFromArgv, serveStdio, toConfigJson, VERSION } from "@bridge-ds/mcp";
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";
5338
5940
  var MCP_PACKAGE = "@bridge-ds/mcp";
5339
5941
  var HELP = `bridge ${VERSION}: design system as code, for you and your Claude
5340
5942
 
@@ -5349,9 +5951,13 @@ Usage:
5349
5951
  bridge tokens <query> [--json] search tokens by name, description or value
5350
5952
  bridge diff <base> [--json|--md] designer-readable token diff: working tree vs <base> (git ref)
5351
5953
  bridge propose --title "..." --set path=value [--set ...] [--file f | --mode theme=dark]
5352
- [--edits edits.json] [--summary "..."] [--branch explore/<name>] [--dry-run]
5954
+ [--edits edits.json] [--summary "..."] [--branch explore/<name>] [--pr <n>]
5955
+ [--product <key>] [--jira DS-42] [--dry-run]
5353
5956
  validate + contrast + new local bridge/* branch (never pushes).
5354
- --branch: write the edits to the checked-out exploration instead
5957
+ --branch: write the edits to the checked-out exploration instead.
5958
+ --pr: add them to that branch in review (GitHub token).
5959
+ --product: the product it is for (bridge products); --jira: the
5960
+ ticket key, first in the branch name and title
5355
5961
  bridge brand list [--json] brands: slug, name, default, primary action color, theme overrides
5356
5962
  bridge brand create <slug> --from bridge --primary <hex|hue> --neutral <hex|hue>
5357
5963
  [--name "Acme"] [--accent x] [--link x] [--radius soft|balanced|crisp|i,c,co,o]
@@ -5364,6 +5970,11 @@ Usage:
5364
5970
  visual impact: build Storybook and the app at both refs, screenshot
5365
5971
  what the change touches, pixel-diff, write manifest.json + images.
5366
5972
  Exit 0 even with diffs (2 on setup errors). Needs Chrome installed.
5973
+ bridge figma-references [--storybook storybook-static] [--specs packages/react/component-specs.json]
5974
+ [--out figma-references] [--sha <sha>] [--only Button,Card] [--json]
5975
+ code images for the Figma fidelity check: one PNG per generated
5976
+ Figma variant from a built Storybook, plus manifest.json.
5977
+ Needs Chrome installed
5367
5978
  bridge history [token] [--json] [--limit n] [--author a] [--tier n] [--version v]
5368
5979
  token history: versions, merged changes, who, when, why
5369
5980
  bridge revert <pr|commit|id> [--dry-run] [--restore-conflicts] [--use-before path@file] [--json]
@@ -5402,6 +6013,16 @@ Usage:
5402
6013
  explorations: an explore/<name> branch with a draft pull request
5403
6014
  labeled exploration, previews and a snapshot package for a product
5404
6015
  staging branch. Never merged as is: promote it to review or close it
6016
+ bridge branch list|status|comments|ready|draft|review|ask|comment|merge|close|reopen|update|try|combine
6017
+ branches in review, the same actions as Bridge Studio
6018
+ (\`bridge branch help\`): what needs me, send for review, review,
6019
+ merge through the same gate (owner override with a reason), update
6020
+ from main with token conflicts, try it in a product, try together
6021
+ bridge products | bridge settings | bridge coverage [Component]
6022
+ products (and this folder's), Studio settings safe to read, which
6023
+ components are in Figma
6024
+ bridge docs save <Component> --file docs.json [--pr n] [--jira DS-42] [--product key] [--dry-run]
6025
+ save usage guidance as in the Library edit mode (GitHub token)
5405
6026
  bridge mcp [--root dir | --repo owner/name] start the MCP server on stdio
5406
6027
 
5407
6028
  Options:
@@ -5418,29 +6039,29 @@ function parse(argv) {
5418
6039
  const [k, inline] = a.slice(2).split(/=(.*)/s, 2);
5419
6040
  const next = argv[i + 1];
5420
6041
  const fileArg = k === "json" && /\.json$/i.test(next ?? "") || k === "md" && /\.(md|markdown)$/i.test(next ?? "");
5421
- const takesValue = inline === void 0 && next !== void 0 && !next.startsWith("--") && (fileArg || !["force", "json", "md", "strict", "dry-run", "help", "fix", "write", "no-regress", "plan", "keep-unchanged", "restore-conflicts", "breaking", "claude", "all", "offline"].includes(k));
6042
+ const takesValue = inline === void 0 && next !== void 0 && !next.startsWith("--") && (fileArg || !["force", "json", "md", "strict", "dry-run", "help", "fix", "write", "no-regress", "plan", "keep-unchanged", "restore-conflicts", "breaking", "claude", "all", "offline", "approve"].includes(k));
5422
6043
  (out.flags[k] ??= []).push(inline ?? (takesValue ? argv[++i] : "true"));
5423
6044
  } else out._.push(a);
5424
6045
  }
5425
6046
  return out;
5426
6047
  }
5427
- var flag3 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
5428
- var has3 = (a, k) => Boolean(a.flags[k]);
6048
+ var flag4 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
6049
+ var has4 = (a, k) => Boolean(a.flags[k]);
5429
6050
  var print = (s) => process.stdout.write(`${s}
5430
6051
  `);
5431
6052
  var modeArg = (s) => s ? Object.fromEntries(s.split(",").map((kv) => kv.split("="))) : void 0;
5432
6053
  async function init(a, root) {
5433
- const cfgPath = join5(root, CONFIG_FILE2);
6054
+ const cfgPath = join6(root, CONFIG_FILE2);
5434
6055
  let config;
5435
- if (existsSync5(cfgPath) && !has3(a, "force")) {
6056
+ if (existsSync6(cfgPath) && !has4(a, "force")) {
5436
6057
  print(`${CONFIG_FILE2} exists, keeping it (--force to regenerate).`);
5437
- config = (await (await BridgeContext4.create({ root })).config()).config;
6058
+ config = (await (await BridgeContext5.create({ root })).config()).config;
5438
6059
  } else {
5439
6060
  const src = new LocalSource(root);
5440
6061
  const files = await src.list("");
5441
6062
  const repo = await originRepo(root);
5442
6063
  config = await detectConfig(files, (p) => src.read(p), repo ? { repo } : {});
5443
- await writeFile4(cfgPath, `${JSON.stringify(toConfigJson(config, "https://raw.githubusercontent.com/peterbono/ai-design-system-demo/main/packages/core/bridge.config.schema.json"), null, 2)}
6064
+ await writeFile5(cfgPath, `${JSON.stringify(toConfigJson(config, "https://raw.githubusercontent.com/peterbono/ai-design-system-demo/main/packages/core/bridge.config.schema.json"), null, 2)}
5444
6065
  `);
5445
6066
  print(`Wrote ${CONFIG_FILE2}: tokens in ${config.tokens.root}, ${config.tokens.layers.length} layers, modes ${Object.keys(config.axes).join(", ") || "none"}.`);
5446
6067
  print("Check the layers order (lowest first) and the tiers; detection is a starting point.");
@@ -5449,10 +6070,10 @@ async function init(a, root) {
5449
6070
  const relServer = relative4(root, serverJs);
5450
6071
  const inRepo = !relServer.startsWith("..") && !isAbsolute(relServer);
5451
6072
  const server = (dir) => inRepo ? { command: "node", args: [relServer, "--root", dir] } : { command: "npx", args: ["-y", MCP_PACKAGE, "--root", dir] };
5452
- const mcpPath = join5(root, ".mcp.json");
5453
- const existing = existsSync5(mcpPath) ? JSON.parse(await readFile5(mcpPath, "utf8")) : {};
6073
+ const mcpPath = join6(root, ".mcp.json");
6074
+ const existing = existsSync6(mcpPath) ? JSON.parse(await readFile7(mcpPath, "utf8")) : {};
5454
6075
  existing.mcpServers = { ...existing.mcpServers ?? {}, bridge: server(".") };
5455
- await writeFile4(mcpPath, `${JSON.stringify(existing, null, 2)}
6076
+ await writeFile5(mcpPath, `${JSON.stringify(existing, null, 2)}
5456
6077
  `);
5457
6078
  print(`Wrote .mcp.json: Claude Code picks up the "bridge" server next time it starts in ${root}.`);
5458
6079
  const desktop = inRepo ? { command: "node", args: [serverJs, "--root", root] } : server(root);
@@ -5462,14 +6083,14 @@ async function init(a, root) {
5462
6083
  print(` claude mcp add bridge -- ${desktop.command} ${desktop.args.join(" ")}`);
5463
6084
  }
5464
6085
  async function check(a, root) {
5465
- const ctx = await BridgeContext4.create({ root });
6086
+ const ctx = await BridgeContext5.create({ root });
5466
6087
  const v = await ops.validateTokens(ctx, { includeWarnings: true });
5467
6088
  const c = await ops.checkContrast(ctx, {});
5468
6089
  const warnings = "warnings" in v ? v.warnings ?? [] : [];
5469
- const failed = !v.valid || "failing" in c && (c.failing ?? 0) > 0 || has3(a, "strict") && warnings.length > 0;
6090
+ const failed = !v.valid || "failing" in c && (c.failing ?? 0) > 0 || has4(a, "strict") && warnings.length > 0;
5470
6091
  const protection = await protectionOf(a, root, ctx);
5471
6092
  const roles = await rolesOf(a, root, ctx);
5472
- if (has3(a, "json")) {
6093
+ if (has4(a, "json")) {
5473
6094
  print(JSON.stringify({ ok: !failed, validation: v, contrast: c, ...protection ? { protection: protection.state } : {}, ...roles?.teams.length ? { teams: roles.teams } : {} }, null, 2));
5474
6095
  return failed ? 1 : 0;
5475
6096
  }
@@ -5488,7 +6109,7 @@ async function check(a, root) {
5488
6109
  return failed ? 1 : 0;
5489
6110
  }
5490
6111
  async function protectionOf(a, root, ctx) {
5491
- if (has3(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
6112
+ if (has4(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
5492
6113
  const { config } = await ctx.config();
5493
6114
  const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
5494
6115
  if (!repo) return void 0;
@@ -5496,7 +6117,7 @@ async function protectionOf(a, root, ctx) {
5496
6117
  return protectionReport({ repo, defaultBranch: config.repo?.defaultBranch ?? "main", token });
5497
6118
  }
5498
6119
  async function rolesOf(a, root, ctx) {
5499
- if (has3(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
6120
+ if (has4(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
5500
6121
  const { config } = await ctx.config();
5501
6122
  const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
5502
6123
  if (!repo) return void 0;
@@ -5506,9 +6127,9 @@ async function rolesOf(a, root, ctx) {
5506
6127
  async function tokens(a, root) {
5507
6128
  const q = a._.slice(1).join(" ");
5508
6129
  if (!q) throw new UsageError("bridge tokens <query>");
5509
- const ctx = await BridgeContext4.create({ root });
5510
- const r = await ops.searchTokens(ctx, { query: q, limit: Number(flag3(a, "limit") ?? 20) });
5511
- if (has3(a, "json")) return print(JSON.stringify(r, null, 2));
6130
+ const ctx = await BridgeContext5.create({ root });
6131
+ 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));
5512
6133
  if (!r.results.length) return print(`No token matches "${q}".`);
5513
6134
  const w = Math.max(...r.results.map((x) => x.path.length));
5514
6135
  for (const t of r.results) print(`${t.path.padEnd(w)} ${String(t.value).padEnd(10)} ${(t.tier ?? "").padEnd(9)} ${t.description ?? ""}`);
@@ -5516,20 +6137,20 @@ async function tokens(a, root) {
5516
6137
  async function diff(a, root) {
5517
6138
  const base = a._[1];
5518
6139
  if (!base) throw new UsageError("bridge diff <base> (a git ref: main, HEAD~1, a sha)");
5519
- const ctx = await BridgeContext4.create({ root });
6140
+ const ctx = await BridgeContext5.create({ root });
5520
6141
  const { config } = await ctx.config();
5521
6142
  const before = await core.loadTokenSet(new GitRefSource(root, base), config);
5522
6143
  const after = await ctx.tokens();
5523
6144
  const changes = core.diff(before, after);
5524
- const pairs = await core.readContrastPairs(ctx.source, config).catch(() => []);
5525
- const delta2 = pairs.length ? core.contrastDelta(core.runContrast(before, pairs, config.contrast?.modes), core.runContrast(after, pairs, config.contrast?.modes)) : [];
6145
+ const pairs2 = await core.readContrastPairs(ctx.source, config).catch(() => []);
6146
+ const delta2 = pairs2.length ? core.contrastDelta(core.runContrast(before, pairs2, config.contrast?.modes), core.runContrast(after, pairs2, config.contrast?.modes)) : [];
5526
6147
  const tier = core.classify(changes, [], config);
5527
6148
  const imp = core.impact(before, after, changes, 30);
5528
- if (has3(a, "json")) {
6149
+ if (has4(a, "json")) {
5529
6150
  print(JSON.stringify({ base, changes, contrast: delta2, tier, impact: imp }, null, 2));
5530
6151
  return 0;
5531
6152
  }
5532
- if (has3(a, "md")) {
6153
+ if (has4(a, "md")) {
5533
6154
  print(`## Token changes since \`${base}\`
5534
6155
 
5535
6156
  ${core.tokenTable(changes)}
@@ -5564,24 +6185,37 @@ ${imp.total} other token(s) change value through aliases:`);
5564
6185
  return delta2.some((d) => d.passBefore && !d.passAfter) ? 1 : 0;
5565
6186
  }
5566
6187
  async function propose(a, root) {
5567
- const title = flag3(a, "title");
6188
+ const title = flag4(a, "title");
5568
6189
  if (!title) throw new UsageError('bridge propose --title "Darken muted text" --set color.text.muted={palette.neutral.700} --mode theme=light');
5569
6190
  const edits = [];
5570
- const file = flag3(a, "file");
5571
- const mode2 = modeArg(flag3(a, "mode"));
6191
+ const file = flag4(a, "file");
6192
+ const mode2 = modeArg(flag4(a, "mode"));
5572
6193
  for (const s of a.flags.set ?? []) {
5573
6194
  const i = s.indexOf("=");
5574
6195
  if (i < 0) throw new UsageError(`--set needs path=value, got "${s}"`);
5575
6196
  edits.push({ op: "set", path: s.slice(0, i), value: s.slice(i + 1), ...file ? { file } : {}, ...mode2 ? { mode: mode2 } : {} });
5576
6197
  }
5577
- const editsFile = flag3(a, "edits");
5578
- if (editsFile) edits.push(...JSON.parse(await readFile5(resolve3(editsFile), "utf8")));
6198
+ const editsFile = flag4(a, "edits");
6199
+ if (editsFile) edits.push(...JSON.parse(await readFile7(resolve4(editsFile), "utf8")));
5579
6200
  if (!edits.length) throw new UsageError("Give at least one --set path=value or --edits file.json");
5580
- const ctx = await BridgeContext4.create({ root });
5581
- const summary = flag3(a, "summary");
5582
- const branch = flag3(a, "branch");
5583
- const r = await ops.proposeTokenChange(ctx, { title, edits, ...summary ? { summary } : {}, ...branch ? { branch } : {}, dryRun: has3(a, "dry-run") });
5584
- if (has3(a, "json")) print(JSON.stringify(r, null, 2));
6201
+ const pr = flag4(a, "pr");
6202
+ 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 } : {} });
6204
+ const summary = flag4(a, "summary");
6205
+ const branch = flag4(a, "branch");
6206
+ const jira = flag4(a, "jira");
6207
+ const product = flag4(a, "product");
6208
+ const r = await ops.proposeTokenChange(ctx, {
6209
+ title,
6210
+ edits,
6211
+ ...summary ? { summary } : {},
6212
+ ...branch ? { branch } : {},
6213
+ ...pr ? { pr } : {},
6214
+ ...jira ? { jira } : {},
6215
+ ...product ? { product } : {},
6216
+ dryRun: has4(a, "dry-run")
6217
+ });
6218
+ if (has4(a, "json")) print(JSON.stringify(r, null, 2));
5585
6219
  else {
5586
6220
  print(`status: ${r.status}${r.why ? ` (${r.why})` : ""}`);
5587
6221
  if (r.tier) print(`tier ${r.tier}, ${r.bump}`);
@@ -5596,17 +6230,17 @@ ${r.prBody}`);
5596
6230
  }
5597
6231
  async function brandCmd(a, root) {
5598
6232
  const sub = a._[1];
5599
- const ctx = await BridgeContext4.create({ root });
6233
+ const ctx = await BridgeContext5.create({ root });
5600
6234
  if (sub === "list") {
5601
6235
  const r2 = await brand.listBrands(ctx);
5602
- if (has3(a, "json")) print(JSON.stringify(r2.brands, null, 2));
6236
+ if (has4(a, "json")) print(JSON.stringify(r2.brands, null, 2));
5603
6237
  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(", ")}` : ""}`);
5604
6238
  return 0;
5605
6239
  }
5606
6240
  if (sub !== "create") throw new UsageError('bridge brand list | bridge brand create <slug> --from bridge --primary "#1F5F3A" --neutral "#2A2F2C"');
5607
6241
  const slug = a._[2];
5608
6242
  if (!slug) throw new UsageError('bridge brand create <slug> --from bridge --primary "#1F5F3A" --neutral "#2A2F2C"');
5609
- const opt = (k) => flag3(a, k);
6243
+ const opt = (k) => flag4(a, k);
5610
6244
  const fonts = { ...opt("heading") ? { heading: opt("heading") } : {}, ...opt("text") ? { text: opt("text") } : {} };
5611
6245
  const r = await brand.proposeBrand(ctx, {
5612
6246
  slug,
@@ -5619,9 +6253,9 @@ async function brandCmd(a, root) {
5619
6253
  ...opt("radius") ? { radius: opt("radius") } : {},
5620
6254
  ...Object.keys(fonts).length ? { fonts } : {},
5621
6255
  ...opt("notes") ? { notes: opt("notes") } : {},
5622
- dryRun: has3(a, "dry-run")
6256
+ dryRun: has4(a, "dry-run")
5623
6257
  });
5624
- if (has3(a, "json")) {
6258
+ if (has4(a, "json")) {
5625
6259
  const { fileContents: _f, ...rest } = r;
5626
6260
  print(JSON.stringify(rest, null, 2));
5627
6261
  return r.status === "blocked" ? 1 : 0;
@@ -5641,37 +6275,58 @@ async function brandCmd(a, root) {
5641
6275
  if (r.hint) print(String(r.hint));
5642
6276
  return r.status === "blocked" ? 1 : 0;
5643
6277
  }
6278
+ async function figmaReferences(a, root) {
6279
+ let sha = flag4(a, "sha");
6280
+ if (!sha) {
6281
+ try {
6282
+ sha = execFileSync2("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
6283
+ } catch {
6284
+ sha = "unknown";
6285
+ }
6286
+ }
6287
+ const only = (flag4(a, "only") ?? "").split(",").filter(Boolean);
6288
+ 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"),
6292
+ sha,
6293
+ ...only.length ? { only } : {},
6294
+ log: has4(a, "json") ? void 0 : (line) => print(line)
6295
+ });
6296
+ if (has4(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
6297
+ return 0;
6298
+ }
5644
6299
  async function impact(a, root) {
5645
- const only = (flag3(a, "only") ?? "").split(",").filter(Boolean);
6300
+ const only = (flag4(a, "only") ?? "").split(",").filter(Boolean);
5646
6301
  for (const x of only) if (x !== "stories" && x !== "app") throw new UsageError(`--only takes stories or app, got "${x}"`);
5647
- const num2 = (k) => flag3(a, k) !== void 0 ? Number(flag3(a, k)) : void 0;
6302
+ const num2 = (k) => flag4(a, k) !== void 0 ? Number(flag4(a, k)) : void 0;
5648
6303
  const threshold = num2("threshold");
5649
6304
  if (threshold !== void 0 && !(threshold >= 0 && threshold <= 1)) throw new UsageError("--threshold is between 0 and 1 (pixelmatch color distance)");
5650
- const imageBase = flag3(a, "image-base");
5651
- const artifactUrl = flag3(a, "artifact-url");
5652
- const out = flag3(a, "out");
5653
- const cacheDir = flag3(a, "cache");
6305
+ const imageBase = flag4(a, "image-base");
6306
+ const artifactUrl = flag4(a, "artifact-url");
6307
+ const out = flag4(a, "out");
6308
+ const cacheDir = flag4(a, "cache");
5654
6309
  const maxStories = num2("max-stories");
5655
6310
  const r = await runImpact({
5656
6311
  root,
5657
- base: flag3(a, "base") ?? "main",
5658
- head: flag3(a, "head") ?? "HEAD",
5659
- ...out ? { out: resolve3(out) } : {},
6312
+ base: flag4(a, "base") ?? "main",
6313
+ head: flag4(a, "head") ?? "HEAD",
6314
+ ...out ? { out: resolve4(out) } : {},
5660
6315
  ...only.length ? { only } : {},
5661
6316
  ...threshold !== void 0 ? { threshold } : {},
5662
- ...cacheDir ? { cacheDir: resolve3(cacheDir) } : {},
6317
+ ...cacheDir ? { cacheDir: resolve4(cacheDir) } : {},
5663
6318
  ...maxStories ? { maxStoriesPerComponent: maxStories } : {},
5664
6319
  ...imageBase !== void 0 ? { imageBase } : {},
5665
6320
  ...artifactUrl ? { artifactUrl } : {},
5666
- plan: has3(a, "plan"),
5667
- keepUnchanged: has3(a, "keep-unchanged")
6321
+ plan: has4(a, "plan"),
6322
+ keepUnchanged: has4(a, "keep-unchanged")
5668
6323
  });
5669
- if (has3(a, "json")) {
6324
+ if (has4(a, "json")) {
5670
6325
  print(JSON.stringify({ summary: r.summary, manifest: r.manifestPath, comment: r.commentPath ?? null, ...r.manifest.summary, selection: r.manifest.selection }, null, 2));
5671
6326
  return 0;
5672
6327
  }
5673
6328
  const sel = r.manifest.selection;
5674
- if (has3(a, "plan")) {
6329
+ if (has4(a, "plan")) {
5675
6330
  print(`Plan ${r.manifest.base.ref} (${r.manifest.base.sha.slice(0, 7)}) -> ${r.manifest.head.ref} (${r.manifest.head.sha.slice(0, 7)})`);
5676
6331
  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(", ")})` : ""}`);
5677
6332
  print(` components: ${sel.components.map((c) => c.id).join(", ") || "none"}`);
@@ -5687,12 +6342,12 @@ async function impact(a, root) {
5687
6342
  }
5688
6343
  var day = (iso) => iso.slice(0, 10);
5689
6344
  async function historyCmd(a, root) {
5690
- const ctx = await BridgeContext4.create({ root });
6345
+ const ctx = await BridgeContext5.create({ root });
5691
6346
  const token = a._[1];
5692
- const limit = Number(flag3(a, "limit") ?? 20);
6347
+ const limit = Number(flag4(a, "limit") ?? 20);
5693
6348
  if (token) {
5694
6349
  const r2 = await hist.getTokenHistory(ctx, { path: token, limit });
5695
- if (has3(a, "json")) return print(JSON.stringify(r2, null, 2)), 0;
6350
+ if (has4(a, "json")) return print(JSON.stringify(r2, null, 2)), 0;
5696
6351
  if (!r2.total) return print(`No recorded change of ${r2.path} in ${r2.source}.`), 0;
5697
6352
  print(`${r2.path}: ${r2.total} change(s) (${r2.source})
5698
6353
  `);
@@ -5703,9 +6358,9 @@ async function historyCmd(a, root) {
5703
6358
  }
5704
6359
  return 0;
5705
6360
  }
5706
- const tier = flag3(a, "tier");
5707
- const r = await hist.getHistory(ctx, { limit, ...flag3(a, "author") ? { author: flag3(a, "author") } : {}, ...tier ? { tier: Number(tier) } : {}, ...flag3(a, "version") ? { version: flag3(a, "version") } : {}, ...has3(a, "breaking") ? { breaking: true } : {} });
5708
- if (has3(a, "json")) return print(JSON.stringify(r, null, 2)), 0;
6361
+ 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;
5709
6364
  print(`${r.source}${r.currentVersion ? `, package version ${r.currentVersion}` : ""}`);
5710
6365
  if (!r.total) return print(r.note ?? "No token change matches."), 0;
5711
6366
  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" : ""}`);
@@ -5723,10 +6378,10 @@ ${r.truncated}`);
5723
6378
  async function revertCmd(a, root) {
5724
6379
  const target = a._[1];
5725
6380
  if (!target) throw new UsageError("bridge revert <pr|commit|id> [--dry-run] (ids from `bridge history`)");
5726
- const ctx = await BridgeContext4.create({ root });
6381
+ const ctx = await BridgeContext5.create({ root });
5727
6382
  const resolutions = Object.fromEntries((a.flags["use-before"] ?? []).map((k) => [k, "use-before"]));
5728
- const r = await hist.proposeRevert(ctx, { target, resolutions, restoreConflicts: has3(a, "restore-conflicts"), dryRun: has3(a, "dry-run") });
5729
- if (has3(a, "json")) {
6383
+ const r = await hist.proposeRevert(ctx, { target, resolutions, restoreConflicts: has4(a, "restore-conflicts"), dryRun: has4(a, "dry-run") });
6384
+ if (has4(a, "json")) {
5730
6385
  print(JSON.stringify(r, null, 2));
5731
6386
  return r.status === "blocked" ? 1 : 0;
5732
6387
  }
@@ -5750,27 +6405,27 @@ ${r.prBody}`);
5750
6405
  async function audit(a, root) {
5751
6406
  const target = a._[1];
5752
6407
  if (!target) throw new UsageError("bridge audit <path> (a product folder or file that uses the design system)");
5753
- if (!existsSync5(target)) throw new UsageError(`No such file or folder: ${target}`);
5754
- const dsRoot = flag3(a, "ds") ? resolve3(flag3(a, "ds")) : existsSync5(join5(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
6408
+ 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);
5755
6410
  if (!dsRoot) throw new UsageError(`No ${CONFIG_FILE2} found: run bridge audit from the design system repo or pass --ds <dir>.`);
5756
6411
  const ds = loadDesignSystem(dsRoot);
5757
- if (has3(a, "fix")) {
5758
- const write = has3(a, "write");
6412
+ if (has4(a, "fix")) {
6413
+ const write = has4(a, "write");
5759
6414
  const changes = fixCss(target, ds.index, write);
5760
6415
  const n = changes.reduce((acc, c) => acc + c.edits.length, 0);
5761
6416
  for (const c of changes) for (const e of c.edits) print(`${c.file}:${e.line}:${e.column} ${e.from} -> ${e.to}`);
5762
6417
  print(n ? `${write ? "Replaced" : "Would replace"} ${n} value(s) in ${changes.length} file(s) with exact token matches.${write ? "" : " Run again with --write to apply."}` : "Nothing to fix: no CSS value equals a token exactly.");
5763
6418
  return 0;
5764
6419
  }
5765
- const baselinePath = flag3(a, "baseline");
6420
+ const baselinePath = flag4(a, "baseline");
5766
6421
  let baseline;
5767
6422
  if (baselinePath) {
5768
- if (!existsSync5(baselinePath)) throw new UsageError(`Baseline not found: ${baselinePath}`);
5769
- baseline = JSON.parse(await readFile5(baselinePath, "utf8"));
6423
+ if (!existsSync6(baselinePath)) throw new UsageError(`Baseline not found: ${baselinePath}`);
6424
+ baseline = JSON.parse(await readFile7(baselinePath, "utf8"));
5770
6425
  }
5771
- const label = flag3(a, "label");
5772
- const allowlistPath = flag3(a, "allowlist");
5773
- if (allowlistPath && !existsSync5(allowlistPath)) throw new UsageError(`Allowlist not found: ${allowlistPath}`);
6426
+ const label = flag4(a, "label");
6427
+ const allowlistPath = flag4(a, "allowlist");
6428
+ if (allowlistPath && !existsSync6(allowlistPath)) throw new UsageError(`Allowlist not found: ${allowlistPath}`);
5774
6429
  let allowlist;
5775
6430
  try {
5776
6431
  allowlist = allowlistPath ? loadAllowlist(allowlistPath) : void 0;
@@ -5779,11 +6434,11 @@ async function audit(a, root) {
5779
6434
  }
5780
6435
  const report = runAudit(target, { ds: dsRoot, version: VERSION, ...label ? { label } : {}, ...baseline ? { baseline } : {}, ...allowlist ? { allowlist } : {} });
5781
6436
  const md = auditMarkdown(report);
5782
- const jsonOut = flag3(a, "json");
5783
- const mdOut = flag3(a, "md");
5784
- if (jsonOut && jsonOut !== "true") await writeFile4(jsonOut, `${JSON.stringify(report, null, 2)}
6437
+ const jsonOut = flag4(a, "json");
6438
+ const mdOut = flag4(a, "md");
6439
+ if (jsonOut && jsonOut !== "true") await writeFile5(jsonOut, `${JSON.stringify(report, null, 2)}
5785
6440
  `);
5786
- if (mdOut && mdOut !== "true") await writeFile4(mdOut, `${md}
6441
+ if (mdOut && mdOut !== "true") await writeFile5(mdOut, `${md}
5787
6442
  `);
5788
6443
  if (jsonOut === "true") print(JSON.stringify(report, null, 2));
5789
6444
  else if (mdOut === "true") print(md);
@@ -5820,7 +6475,7 @@ async function audit(a, root) {
5820
6475
  for (const f of report.files) for (const e of f.ignoreErrors ?? []) process.stderr.write(`${report.target.path}/${f.path}:${e.line}: ${e.message}
5821
6476
  `);
5822
6477
  let code = 0;
5823
- const failUnder = flag3(a, "fail-under");
6478
+ const failUnder = flag4(a, "fail-under");
5824
6479
  if (failUnder !== void 0) {
5825
6480
  const min = Number(failUnder);
5826
6481
  if (!Number.isFinite(min)) throw new UsageError("--fail-under needs a number, e.g. --fail-under 80");
@@ -5831,7 +6486,7 @@ async function audit(a, root) {
5831
6486
  code = 1;
5832
6487
  }
5833
6488
  }
5834
- if (has3(a, "no-regress") && report.baseline) {
6489
+ if (has4(a, "no-regress") && report.baseline) {
5835
6490
  const b = report.baseline;
5836
6491
  if ((b.tokenAdoption.delta ?? 0) < 0 || (b.componentAdoption.delta ?? 0) < 0 || b.hardcoded.delta > 0) {
5837
6492
  process.stderr.write("bridge audit: adoption regressed since the baseline\n");
@@ -5843,9 +6498,9 @@ async function audit(a, root) {
5843
6498
  async function docsCmd(a, root) {
5844
6499
  const [, sub, name] = a._;
5845
6500
  if (sub !== "draft" || !name) throw new UsageError("Usage: bridge docs draft <Component> [--json]");
5846
- const ctx = await BridgeContext4.create({ root });
6501
+ const ctx = await BridgeContext5.create({ root });
5847
6502
  const d = await ops.draftDocsOp(ctx, { name });
5848
- if (has3(a, "json")) {
6503
+ if (has4(a, "json")) {
5849
6504
  print(JSON.stringify({ component: d.component, path: d.path, exists: d.exists, prefill: d.prefill, prompt: d.prompt }, null, 2));
5850
6505
  return 0;
5851
6506
  }
@@ -5876,12 +6531,15 @@ async function main() {
5876
6531
  const argv = process.argv.slice(2);
5877
6532
  const a = parse(argv);
5878
6533
  const cmd = a._[0];
5879
- const root = resolve3(flag3(a, "root") ?? process.cwd());
6534
+ const root = resolve4(flag4(a, "root") ?? process.cwd());
5880
6535
  if (cmd === "request") return runRequest(a, root);
5881
6536
  if (cmd === "explore") return runExplore(a, root);
5882
- if (!cmd || has3(a, "help") || cmd === "help") {
6537
+ if (cmd === "branch") return runBranch(a, root);
6538
+ if (cmd === "products" || cmd === "settings" || cmd === "coverage") return runStudioRead(cmd, a, root);
6539
+ if (cmd === "docs" && a._[1] === "save") return runDocsSave(a, root);
6540
+ if (!cmd || has4(a, "help") || cmd === "help") {
5883
6541
  print(HELP);
5884
- return cmd || has3(a, "help") ? 0 : 2;
6542
+ return cmd || has4(a, "help") ? 0 : 2;
5885
6543
  }
5886
6544
  if (cmd === "--version" || cmd === "version") return print(VERSION), 0;
5887
6545
  switch (cmd) {
@@ -5901,6 +6559,8 @@ async function main() {
5901
6559
  return brandCmd(a, root);
5902
6560
  case "impact":
5903
6561
  return impact(a, root);
6562
+ case "figma-references":
6563
+ return figmaReferences(a, root);
5904
6564
  case "history":
5905
6565
  return historyCmd(a, root);
5906
6566
  case "revert":