@bridge-ds/cli 0.7.0 → 0.9.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 +501 -172
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1813,10 +1813,10 @@ ${" ".repeat(column - 1)}${expr}${suffix}` : `${prefix}${expr}${suffix}`;
1813
1813
  out.themeRefs += r.themeRefs;
1814
1814
  return;
1815
1815
  }
1816
- const str = /^(\s*)([`'"])([\s\S]*)\2\s*$/.exec(a.value);
1817
- if (str) {
1818
- const body = str[3].replace(/\$\{[^}]*\}/g, (m) => " ".repeat(m.length));
1819
- const r = auditCssText(body, "css", ds, { pos, offset: a.valueStart + str[1].length + 1, source: "style" });
1816
+ const str2 = /^(\s*)([`'"])([\s\S]*)\2\s*$/.exec(a.value);
1817
+ if (str2) {
1818
+ const body = str2[3].replace(/\$\{[^}]*\}/g, (m) => " ".repeat(m.length));
1819
+ const r = auditCssText(body, "css", ds, { pos, offset: a.valueStart + str2[1].length + 1, source: "style" });
1820
1820
  out.findings.push(...r.findings);
1821
1821
  out.tokenUsages += r.tokenUsages;
1822
1822
  out.themeRefs += r.themeRefs;
@@ -2176,8 +2176,8 @@ function parseOwnerEntry(raw) {
2176
2176
  var teamKey = (org, slug) => `@${org.toLowerCase()}/${slug.toLowerCase()}`;
2177
2177
  function ownerEntryProblems(raw) {
2178
2178
  if (raw === void 0) return [];
2179
- const isObj2 = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
2180
- if (!isObj2(raw)) return ['"governance.owners" must be { "design": [people or teams], "tech": [people or teams] }'];
2179
+ const isObj3 = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
2180
+ if (!isObj3(raw)) return ['"governance.owners" must be { "design": [people or teams], "tech": [people or teams] }'];
2181
2181
  const out = [];
2182
2182
  for (const [k, v] of Object.entries(raw)) {
2183
2183
  if (k !== "design" && k !== "tech") {
@@ -2562,7 +2562,8 @@ function defineConfig(input) {
2562
2562
  minReasonLength: g.override.minReasonLength ?? DEFAULT_OVERRIDE.minReasonLength
2563
2563
  }
2564
2564
  } : {},
2565
- ...g.rights ? { rights: normalizeRights(g.rights) } : {}
2565
+ ...g.rights ? { rights: normalizeRights(g.rights) } : {},
2566
+ ...g.soloOwnerCanApproveBoth === true ? { soloOwnerCanApproveBoth: true } : {}
2566
2567
  },
2567
2568
  products: input.products?.length ? input.products.map((p) => ({ ...p })) : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
2568
2569
  changes: input.changes === "direct" ? "direct" : DEFAULT_CHANGES_MODE,
@@ -2686,6 +2687,7 @@ function loadConfig(source) {
2686
2687
  }
2687
2688
  }
2688
2689
  problems.push(...rightsProblems(gov.rights));
2690
+ if (gov.soloOwnerCanApproveBoth !== void 0 && typeof gov.soloOwnerCanApproveBoth !== "boolean") problems.push('"governance.soloOwnerCanApproveBoth" must be true or false');
2689
2691
  }
2690
2692
  }
2691
2693
  problems.push(...visualImpactProblems(d.visualImpact));
@@ -3336,25 +3338,25 @@ var GitHubClient = class _GitHubClient {
3336
3338
  return pr ? { number: pr.number, url: pr.html_url, title: pr.title, draft: !!pr.draft, headRef: pr.head.ref } : void 0;
3337
3339
  }
3338
3340
  /** Best effort: labels need the repo to allow it; failure is not fatal. */
3339
- async addLabels(issueNumber2, labels) {
3341
+ async addLabels(issueNumber3, labels) {
3340
3342
  try {
3341
- await this.request("POST", `${this.repoPath}/issues/${issueNumber2}/labels`, { labels });
3343
+ await this.request("POST", `${this.repoPath}/issues/${issueNumber3}/labels`, { labels });
3342
3344
  return true;
3343
3345
  } catch {
3344
3346
  return false;
3345
3347
  }
3346
3348
  }
3347
- async addComment(issueNumber2, body) {
3348
- await this.request("POST", `${this.repoPath}/issues/${issueNumber2}/comments`, { body });
3349
+ async addComment(issueNumber3, body) {
3350
+ await this.request("POST", `${this.repoPath}/issues/${issueNumber3}/comments`, { body });
3349
3351
  }
3350
3352
  /**
3351
3353
  * Comments on an issue or pull request, oldest first (first 100: the sticky preview comment is posted early).
3352
3354
  * `authorAssociation` is GitHub's OWNER, MEMBER, COLLABORATOR, CONTRIBUTOR, NONE... (suggestion approvals read it).
3353
3355
  */
3354
- async listIssueComments(issueNumber2) {
3356
+ async listIssueComments(issueNumber3) {
3355
3357
  const r = await this.request(
3356
3358
  "GET",
3357
- `${this.repoPath}/issues/${issueNumber2}/comments?per_page=100`
3359
+ `${this.repoPath}/issues/${issueNumber3}/comments?per_page=100`
3358
3360
  );
3359
3361
  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" }));
3360
3362
  }
@@ -3671,8 +3673,8 @@ var GitHubClient = class _GitHubClient {
3671
3673
  });
3672
3674
  return { id: r.id, name: r.name, status: r.status, conclusion: r.conclusion, url: r.html_url };
3673
3675
  }
3674
- async removeLabel(issueNumber2, label) {
3675
- await this.request("DELETE", `${this.repoPath}/issues/${issueNumber2}/labels/${encodeURIComponent(label)}`);
3676
+ async removeLabel(issueNumber3, label) {
3677
+ await this.request("DELETE", `${this.repoPath}/issues/${issueNumber3}/labels/${encodeURIComponent(label)}`);
3676
3678
  }
3677
3679
  // --- branch actions shared by Studio, the MCP server and the CLI (branch-actions.ts) ---
3678
3680
  /** The account the token belongs to (GET /user). Needs a token. */
@@ -3901,6 +3903,27 @@ function jiraIssueFields(r, jira, issueUrl) {
3901
3903
  };
3902
3904
  }
3903
3905
 
3906
+ // ../core/src/suggestions.ts
3907
+ var SUGGESTION_STATUS_LABEL = {
3908
+ "needs-review": "Needs review",
3909
+ "changes-requested": "Changes requested",
3910
+ approved: "Approved, ready to apply",
3911
+ applying: "Being applied",
3912
+ applied: "Applied",
3913
+ declined: "Declined",
3914
+ outdated: "Outdated"
3915
+ };
3916
+
3917
+ // ../core/src/suggestion-apply.ts
3918
+ var SUGGESTION_APPLY_EXIT = {
3919
+ ready: 0,
3920
+ "already-applied": 0,
3921
+ conflict: 3,
3922
+ "not-approved": 1,
3923
+ blocked: 1,
3924
+ refused: 1
3925
+ };
3926
+
3904
3927
  // src/index.ts
3905
3928
  import { fileURLToPath } from "node:url";
3906
3929
 
@@ -4230,6 +4253,81 @@ function planReferences(file) {
4230
4253
  );
4231
4254
  }
4232
4255
 
4256
+ // ../core/src/storybook.ts
4257
+ var StorybookIndexError = class extends Error {
4258
+ constructor(code, message, status) {
4259
+ super(message);
4260
+ this.code = code;
4261
+ this.status = status;
4262
+ this.name = "StorybookIndexError";
4263
+ }
4264
+ code;
4265
+ status;
4266
+ };
4267
+ var isObj2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4268
+ var str = (v) => typeof v === "string" && v.trim() ? v : void 0;
4269
+ function entryFrom(key, raw, v3) {
4270
+ if (!isObj2(raw)) return null;
4271
+ const id = str(raw.id) ?? key;
4272
+ const title = str(raw.title) ?? str(raw.kind);
4273
+ const name = str(raw.name) ?? str(raw.story);
4274
+ if (!id || !title || !name) return null;
4275
+ const params = isObj2(raw.parameters) ? raw.parameters : {};
4276
+ const type = raw.type === "docs" || raw.type === "story" ? raw.type : v3 && params.docsOnly === true ? "docs" : "story";
4277
+ const e = {
4278
+ id,
4279
+ title,
4280
+ name,
4281
+ type,
4282
+ importPath: str(raw.importPath) ?? str(params.fileName) ?? "",
4283
+ tags: Array.isArray(raw.tags) ? raw.tags.filter((t) => typeof t === "string") : []
4284
+ };
4285
+ const subtype = str(raw.subtype);
4286
+ if (subtype) e.subtype = subtype;
4287
+ const exportName2 = str(raw.exportName);
4288
+ if (exportName2) e.exportName = exportName2;
4289
+ const componentPath = str(raw.componentPath);
4290
+ if (componentPath) e.componentPath = componentPath;
4291
+ return e;
4292
+ }
4293
+ function groupStorybookComponents(entries) {
4294
+ const byTitle = /* @__PURE__ */ new Map();
4295
+ for (const e of entries) {
4296
+ let c = byTitle.get(e.title);
4297
+ if (!c) {
4298
+ const parts = e.title.split("/").map((p) => p.trim()).filter(Boolean);
4299
+ c = { title: e.title, name: parts[parts.length - 1] ?? e.title, group: parts.slice(0, -1), stories: [], docs: [], tags: [] };
4300
+ byTitle.set(e.title, c);
4301
+ }
4302
+ if (e.type === "docs") c.docs.push(e);
4303
+ else if (e.subtype !== "test") c.stories.push(e);
4304
+ for (const t of e.tags) if (!c.tags.includes(t)) c.tags.push(t);
4305
+ }
4306
+ const out = [...byTitle.values()].filter((c) => c.stories.length || c.docs.length);
4307
+ for (const c of out) c.tags.sort();
4308
+ return out.sort((a, b) => a.title.localeCompare(b.title));
4309
+ }
4310
+ function parseStorybookIndex(json, source) {
4311
+ if (!isObj2(json)) throw new StorybookIndexError("not-json", "The Storybook index is not a JSON object.");
4312
+ if (typeof json.loginUrl === "string" && !("entries" in json) && !("stories" in json)) {
4313
+ throw new StorybookIndexError("private", "This Storybook is private: it asks for a sign-in before showing its index.");
4314
+ }
4315
+ const map = isObj2(json.entries) ? json.entries : isObj2(json.stories) ? json.stories : null;
4316
+ if (!map) throw new StorybookIndexError("empty", "This file has no Storybook entries.");
4317
+ const v3 = !isObj2(json.entries);
4318
+ const entries = Object.entries(map).map(([k, raw]) => entryFrom(k, raw, v3)).filter((e) => e !== null);
4319
+ if (!entries.length) throw new StorybookIndexError("empty", "This Storybook index lists no stories or docs.");
4320
+ const version = typeof json.v === "number" ? json.v : v3 ? 3 : 4;
4321
+ return { version, source: source ?? (v3 ? "stories.json" : "index.json"), entries, components: groupStorybookComponents(entries) };
4322
+ }
4323
+ var pairs = (o) => Object.entries(o).map(([k, v]) => `${encodeURIComponent(k)}:${encodeURIComponent(v)}`).join(";");
4324
+ function storybookIframeUrl(base, id, o = {}) {
4325
+ let url = `${base.replace(/\/+$/, "")}/iframe.html?id=${encodeURIComponent(id)}&viewMode=${o.viewMode ?? "story"}`;
4326
+ if (o.globals && Object.keys(o.globals).length) url += `&globals=${pairs(o.globals)}`;
4327
+ if (o.args && Object.keys(o.args).length) url += `&args=${pairs(o.args)}`;
4328
+ return url;
4329
+ }
4330
+
4233
4331
  // ../visual/src/capture.ts
4234
4332
  import { mkdir, writeFile } from "node:fs/promises";
4235
4333
  import { dirname as dirname2 } from "node:path";
@@ -4624,7 +4722,7 @@ function routeUrl(base, route, tenant, theme) {
4624
4722
  return `${base.replace(/\/$/, "")}/?tenant=${encodeURIComponent(tenant)}&theme=${encodeURIComponent(theme)}&density=comfortable&lang=en#${route.pathByTenant?.[tenant] ?? route.path}`;
4625
4723
  }
4626
4724
  function storyUrl(base, id, brand2, theme) {
4627
- return `${base.replace(/\/$/, "")}/iframe.html?id=${encodeURIComponent(id)}&viewMode=story&globals=brand:${brand2};theme:${theme};density:comfortable`;
4725
+ return storybookIframeUrl(base, id, { globals: { brand: brand2, theme, density: "comfortable" } });
4628
4726
  }
4629
4727
  var shotKey = (...parts) => parts.map((p) => p.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-|-$/g, "")).join("__");
4630
4728
 
@@ -4915,8 +5013,8 @@ function changedTokens(before, after) {
4915
5013
  return [...modes.entries()].map(([path, m]) => ({ path, kind: direct.has(path) ? "direct" : "alias", modes: m })).sort((a, b) => Number(a.kind === "alias") - Number(b.kind === "alias") || a.path.localeCompare(b.path));
4916
5014
  }
4917
5015
  async function readIndexJson(dir) {
4918
- const raw = JSON.parse(await readFile3(join4(dir, "index.json"), "utf8"));
4919
- return Object.values(raw.entries).filter((e) => e.type === "story").map(({ id, title, name, importPath }) => ({ id, title, name, importPath }));
5016
+ const index = parseStorybookIndex(JSON.parse(await readFile3(join4(dir, "index.json"), "utf8")), "index.json");
5017
+ return index.entries.filter((e) => e.type === "story").map(({ id, title, name, importPath }) => ({ id, title, name, importPath }));
4920
5018
  }
4921
5019
  async function planImpact(o) {
4922
5020
  const root = resolve2(o.root);
@@ -5710,7 +5808,196 @@ ${EXPLORE_HELP}`);
5710
5808
  // src/branch.ts
5711
5809
  import { readFile as readFile6 } from "node:fs/promises";
5712
5810
  import { resolve as resolve3 } from "node:path";
5713
- import { BridgeContext as BridgeContext4, branches, studio } from "@bridge-ds/mcp";
5811
+ import { BridgeContext as BridgeContext5, branches, studio } from "@bridge-ds/mcp";
5812
+
5813
+ // src/suggest.ts
5814
+ import { BridgeContext as BridgeContext4, suggestions } from "@bridge-ds/mcp";
5815
+ var SuggestUsageError = class extends Error {
5816
+ };
5817
+ var SUGGESTIONS_HELP = `bridge suggestions: changes filed as GitHub issues (label bridge-suggestion), applied by a developer
5818
+
5819
+ Usage:
5820
+ bridge suggestions [list] [--status needs-review|changes-requested|approved|applying|applied|declined|outdated|all]
5821
+ [--kind token-change|docs|component-spec|brand|revert|config] [--json]
5822
+ bridge suggestions show <n> [--json]
5823
+ the change, the checks, its version, who approved it and what is missing
5824
+ bridge suggestions link <n> --pr <m> | --branch <name>
5825
+ record the pull request that applies it (it must say "Closes #<n>")
5826
+ bridge apply <n> [--write | --patch] [--dry-run] [--accept-edited] [--force-unapproved] [--branch name] [--json]
5827
+ apply it on today's code: a new local branch by default (working tree untouched, nothing
5828
+ pushed), --write into the working tree, --patch prints a patch for git am. --dry-run shows
5829
+ the plan and the diff and writes nothing
5830
+ bridge review <n> --approve | --request-changes --message "..." | --decline --message "..." [--version <sha>]
5831
+ as the owner your GitHub token belongs to; counts only for owners in bridge.config.json
5832
+
5833
+ GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Reading a public repo works without one.
5834
+ Exit codes (apply): 0 applied, already applied or a clean dry run; 1 not approved, blocked by a check or refused;
5835
+ 2 usage or setup error; 3 conflict with today's code.`;
5836
+ var flag3 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
5837
+ var has3 = (a, k) => Boolean(a.flags[k]);
5838
+ function changesFlag(a) {
5839
+ if (has3(a, "suggest") && has3(a, "direct")) throw new SuggestUsageError("Pass --suggest or --direct, not both.");
5840
+ return has3(a, "direct") ? "direct" : has3(a, "suggest") ? "suggest" : void 0;
5841
+ }
5842
+ function tokenFrom2(io) {
5843
+ return io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
5844
+ }
5845
+ async function context3(root, io) {
5846
+ const token = tokenFrom2(io);
5847
+ return BridgeContext4.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
5848
+ }
5849
+ function issueNumber2(raw, usage) {
5850
+ const n = Number((raw ?? "").replace(/^#/, ""));
5851
+ if (!raw || !Number.isInteger(n) || n <= 0) throw new SuggestUsageError(`Usage: ${usage}`);
5852
+ return n;
5853
+ }
5854
+ var STATUSES = ["needs-review", "changes-requested", "approved", "applying", "applied", "declined", "outdated", "all"];
5855
+ var KINDS = ["token-change", "docs", "component-spec", "brand", "revert", "config"];
5856
+ async function runSuggestions(a, root, io = defaultIO()) {
5857
+ const sub = a._[1] ?? "list";
5858
+ if (sub === "help" || has3(a, "help")) return io.print(SUGGESTIONS_HELP), 0;
5859
+ switch (sub) {
5860
+ case "list": {
5861
+ const status = flag3(a, "status") ?? "all";
5862
+ if (!STATUSES.includes(status)) throw new SuggestUsageError(`--status takes ${STATUSES.join(", ")}, got "${status}"`);
5863
+ const kind = flag3(a, "kind");
5864
+ if (kind && !KINDS.includes(kind)) throw new SuggestUsageError(`--kind takes ${KINDS.join(", ")}, got "${kind}"`);
5865
+ const ctx = await context3(root, io);
5866
+ const r = await suggestions.listSuggestionsOp(ctx, { status, ...kind ? { kind } : {} });
5867
+ if (has3(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
5868
+ if (!r.suggestions.length) return io.print(`No ${status === "all" ? "open " : `${status} `}suggestion in ${r.repo}.`), 0;
5869
+ for (const x of r.suggestions) {
5870
+ const prs = x.linkedPrs.length ? ` PR ${x.linkedPrs.map((p) => `#${p.number}${p.merged ? " merged" : ` ${p.state}`}`).join(", ")}` : "";
5871
+ io.print(`#${String(x.number).padEnd(5)} ${x.said.padEnd(25)} ${(x.kind ?? "").padEnd(14)} ${x.tier ? `tier ${x.tier}` : " "} ${x.title}${prs}`);
5872
+ }
5873
+ io.print(`
5874
+ ${r.next}`);
5875
+ return 0;
5876
+ }
5877
+ case "show": {
5878
+ const n = issueNumber2(a._[2], "bridge suggestions show <number>");
5879
+ const ctx = await context3(root, io);
5880
+ const r = await suggestions.getSuggestionOp(ctx, { number: n });
5881
+ if (has3(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
5882
+ io.print(`#${r.number} ${r.title}`);
5883
+ io.print(`${r.said}${r.kind ? `, ${r.kind}` : ""}${r.tier ? `, tier ${r.tier} (${r.bump})` : ""}${r.source ? `, from ${r.source}` : ""}${r.suggestedBy ? ` by @${r.suggestedBy}` : ""}`);
5884
+ io.print(r.url);
5885
+ if (r.problem) {
5886
+ io.print(`
5887
+ ${r.problem}`);
5888
+ return 1;
5889
+ }
5890
+ if (r.summary) io.print(`
5891
+ ${r.summary}`);
5892
+ const c = r.change ?? {};
5893
+ if (c.tokens?.length) {
5894
+ io.print("\nTokens:");
5895
+ for (const t of c.tokens) io.print(` ${t.kind ?? t.op} ${t.path}${t.before !== void 0 || t.after !== void 0 ? `: ${fmt(t.before)} -> ${fmt(t.after)}` : ""}`);
5896
+ }
5897
+ for (const d of c.docs ?? []) io.print(`Docs: ${d.component} (${d.path})`);
5898
+ if (c.componentSpec) io.print(`Component spec: ${c.componentSpec.component} (${c.componentSpec.specPath})`);
5899
+ for (const f of c.files ?? []) io.print(`File: ${f.change} ${f.path}`);
5900
+ io.print(`
5901
+ Version ${r.version}${r.edited ? ` (${r.edited})` : ""}`);
5902
+ io.print(`Approved by: ${r.approvals.approvedBy.map((l) => `@${l}`).join(", ") || "nobody yet"}`);
5903
+ if (r.approvals.missing.length) io.print(`Missing: ${r.approvals.missing.join(" and ")}`);
5904
+ if (r.approvals.soloOwner) io.print(r.approvals.soloOwner);
5905
+ if (r.approvals.changesRequestedBy.length) io.print(`Changes requested by: ${r.approvals.changesRequestedBy.map((l) => `@${l}`).join(", ")}`);
5906
+ for (const p of r.linkedPrs ?? []) io.print(`Pull request: #${p.number} ${p.merged ? "merged" : p.state} ${p.url}`);
5907
+ io.print(`
5908
+ Next: ${r.next}`);
5909
+ return 0;
5910
+ }
5911
+ case "link": {
5912
+ const n = issueNumber2(a._[2], "bridge suggestions link <number> --pr <m>");
5913
+ const prRaw = flag3(a, "pr");
5914
+ const pr = prRaw ? issueNumber2(prRaw, "bridge suggestions link <number> --pr <m>") : void 0;
5915
+ const branch = flag3(a, "branch");
5916
+ if (!pr && !branch) throw new SuggestUsageError("Usage: bridge suggestions link <number> --pr <m> (or --branch <name>)");
5917
+ const ctx = await context3(root, io);
5918
+ const r = await suggestions.linkSuggestionOp(ctx, { number: n, ...pr ? { pr } : {}, ...branch ? { branch } : {} });
5919
+ if (has3(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
5920
+ io.print(`#${n} is being applied${pr ? ` in #${pr}` : branch ? ` on ${branch}` : ""}.`);
5921
+ for (const w of r.warnings ?? []) io.error(`warning: ${w}`);
5922
+ return 0;
5923
+ }
5924
+ default:
5925
+ throw new SuggestUsageError(`Unknown suggestions command "${sub}".
5926
+
5927
+ ${SUGGESTIONS_HELP}`);
5928
+ }
5929
+ }
5930
+ var fmt = (v) => v === void 0 ? "(none)" : typeof v === "string" ? v : JSON.stringify(v);
5931
+ async function runApply(a, root, io = defaultIO()) {
5932
+ if (has3(a, "help") || a._[1] === "help") return io.print(SUGGESTIONS_HELP), 0;
5933
+ const n = issueNumber2(a._[1], "bridge apply <number> [--write | --patch] [--dry-run]");
5934
+ if (has3(a, "write") && has3(a, "patch")) throw new SuggestUsageError("Pass --write or --patch, not both.");
5935
+ const mode2 = has3(a, "write") ? "write" : has3(a, "patch") ? "patch" : "branch";
5936
+ const ctx = await context3(root, io);
5937
+ if (ctx.mode !== "local" || !ctx.isGit) throw new SuggestUsageError("bridge apply runs in a git checkout of the design system: cd into it, or pass --root.");
5938
+ const branch = flag3(a, "branch");
5939
+ const r = await suggestions.applySuggestionOp(ctx, {
5940
+ number: n,
5941
+ mode: has3(a, "dry-run") ? "dry-run" : mode2,
5942
+ acceptEdited: has3(a, "accept-edited"),
5943
+ forceUnapproved: has3(a, "force-unapproved"),
5944
+ ...branch ? { branch } : {}
5945
+ });
5946
+ const code = typeof r.exitCode === "number" ? r.exitCode : SUGGESTION_APPLY_EXIT[r.outcome] ?? 1;
5947
+ if (has3(a, "json")) return io.print(JSON.stringify(r, null, 2)), code;
5948
+ if (mode2 === "patch" && r.patch && !has3(a, "dry-run")) {
5949
+ io.print(r.patch.trimEnd());
5950
+ io.error(r.message);
5951
+ for (const w of r.warnings ?? []) io.error(`warning: ${w}`);
5952
+ return code;
5953
+ }
5954
+ io.print(`#${n}: ${r.message}`);
5955
+ if (r.table) io.print(`
5956
+ ${r.table}`);
5957
+ for (const p of r.problems ?? []) io.print(` ${p}`);
5958
+ for (const note of r.notes ?? []) io.print(` note: ${note}`);
5959
+ if (r.diff) io.print(`
5960
+ ${r.diff.trimEnd()}`);
5961
+ if (r.brief) io.print(`
5962
+ Brief to implement it:
5963
+ ${r.brief}`);
5964
+ if (r.branch) io.print(`
5965
+ branch: ${r.branch}${r.commit ? ` (${String(r.commit).slice(0, 7)})` : ""}`);
5966
+ for (const w of r.warnings ?? []) io.error(`warning: ${w}`);
5967
+ if (r.next && r.next !== r.message) io.print(`
5968
+ Next: ${r.next}`);
5969
+ return code;
5970
+ }
5971
+ async function runReview(a, root, io = defaultIO()) {
5972
+ if (has3(a, "help") || a._[1] === "help") return io.print(SUGGESTIONS_HELP), 0;
5973
+ const usage = 'bridge review <number> --approve | --request-changes --message "..." | --decline --message "..."';
5974
+ const n = issueNumber2(a._[1], usage);
5975
+ const events = ["approve", "request-changes", "decline", "comment"].filter((e) => has3(a, e));
5976
+ if (events.length !== 1) throw new SuggestUsageError(`Usage: ${usage}`);
5977
+ const event = events[0];
5978
+ const inline = flag3(a, event);
5979
+ const message = flag3(a, "message") ?? (inline && inline !== "true" ? inline : void 0);
5980
+ if ((event === "request-changes" || event === "decline" || event === "comment") && !message?.trim()) throw new SuggestUsageError(`--${event} needs --message "..." (the author reads it).`);
5981
+ const role = flag3(a, "role");
5982
+ if (role && !["design", "tech", "approval"].includes(role)) throw new SuggestUsageError(`--role takes design, tech or approval, got "${role}"`);
5983
+ const ctx = await context3(root, io);
5984
+ const version = flag3(a, "version");
5985
+ const r = await suggestions.reviewSuggestionOp(ctx, {
5986
+ number: n,
5987
+ event,
5988
+ ...message ? { message } : {},
5989
+ ...role ? { role } : {},
5990
+ ...version ? { payloadSha: version } : {}
5991
+ });
5992
+ if (has3(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
5993
+ const done = event === "approve" ? "Approved" : event === "request-changes" ? "Changes requested on" : event === "decline" ? "Declined" : "Commented on";
5994
+ io.print(`${done} #${n}${r.version ? ` (version ${r.version})` : ""}. Now: ${SUGGESTION_STATUS_LABEL[r.status] ?? r.status}.`);
5995
+ for (const w of r.warnings ?? []) io.error(`warning: ${w}`);
5996
+ if (r.next) io.print(`Next: ${r.next}`);
5997
+ return r.counts === false ? 1 : 0;
5998
+ }
5999
+
6000
+ // src/branch.ts
5714
6001
  var BRANCH_HELP = `bridge branch: branches in review, the same actions as Bridge Studio
5715
6002
 
5716
6003
  Usage:
@@ -5740,18 +6027,18 @@ Usage:
5740
6027
 
5741
6028
  Start work for a product: bridge propose|explore start|request create ... --product <key> [--jira DS-42].
5742
6029
  GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
5743
- var flag3 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
5744
- var has3 = (a, k) => Boolean(a.flags[k]);
5745
- async function context3(root, io) {
6030
+ var flag4 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
6031
+ var has4 = (a, k) => Boolean(a.flags[k]);
6032
+ async function context4(root, io) {
5746
6033
  const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
5747
- return BridgeContext4.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
6034
+ return BridgeContext5.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
5748
6035
  }
5749
6036
  function number(a, at, usage) {
5750
6037
  const raw = a._[at];
5751
6038
  if (!raw) throw new RequestUsageError(`Usage: ${usage}`);
5752
6039
  return branches.branchNumber(raw);
5753
6040
  }
5754
- function pairs(list2, what) {
6041
+ function pairs2(list2, what) {
5755
6042
  const out = {};
5756
6043
  for (const s of list2 ?? []) {
5757
6044
  const i = s.lastIndexOf("=");
@@ -5763,16 +6050,16 @@ function pairs(list2, what) {
5763
6050
  var said = (r) => String(r.said ?? r.summary ?? r.status ?? "Done.");
5764
6051
  async function runBranch(a, root, io = defaultIO()) {
5765
6052
  const sub = a._[1];
5766
- if (!sub || sub === "help" || has3(a, "help")) {
6053
+ if (!sub || sub === "help" || has4(a, "help")) {
5767
6054
  io.print(BRANCH_HELP);
5768
- return sub || has3(a, "help") ? 0 : 2;
6055
+ return sub || has4(a, "help") ? 0 : 2;
5769
6056
  }
5770
- const json = has3(a, "json");
5771
- const ctx = await context3(root, io);
6057
+ const json = has4(a, "json");
6058
+ const ctx = await context4(root, io);
5772
6059
  const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
5773
6060
  switch (sub) {
5774
6061
  case "list": {
5775
- const limit = flag3(a, "limit");
6062
+ const limit = flag4(a, "limit");
5776
6063
  const r = await branches.whatNeedsMe(ctx, limit ? { limit: Number(limit) } : {});
5777
6064
  return out(r, () => {
5778
6065
  io.print(r.summary);
@@ -5784,6 +6071,7 @@ async function runBranch(a, root, io = defaultIO()) {
5784
6071
  return out(r, () => {
5785
6072
  io.print(`#${r.pr} ${r.title} (${r.product.name})`);
5786
6073
  io.print(`${r.status.label}. ${r.approvalNeeded} needed; approved by ${r.approvals.approvedBy.map((x) => `@${x}`).join(", ") || "nobody yet"}.`);
6074
+ if (r.approvals.soloOwner) io.print(r.approvals.soloOwner);
5787
6075
  if (r.merge.blockedBy?.length) for (const x of r.merge.blockedBy) io.print(` ${x}`);
5788
6076
  io.print(`Next: ${r.next}`);
5789
6077
  io.print(r.url);
@@ -5806,28 +6094,28 @@ async function runBranch(a, root, io = defaultIO()) {
5806
6094
  }
5807
6095
  case "review": {
5808
6096
  const n = number(a, 2, 'bridge branch review <n> --approve | --request-changes "..." | --comment "..."');
5809
- const decision = has3(a, "approve") ? "approve" : has3(a, "request-changes") ? "request_changes" : has3(a, "comment") ? "comment" : null;
6097
+ const decision = has4(a, "approve") ? "approve" : has4(a, "request-changes") ? "request_changes" : has4(a, "comment") ? "comment" : null;
5810
6098
  if (!decision) throw new RequestUsageError('Say what you decide: --approve, --request-changes "what to change" or --comment "text".');
5811
- const body = decision === "approve" ? flag3(a, "body") : flag3(a, decision === "request_changes" ? "request-changes" : "comment");
6099
+ const body = decision === "approve" ? flag4(a, "body") : flag4(a, decision === "request_changes" ? "request-changes" : "comment");
5812
6100
  const r = await branches.reviewBranch(ctx, { pr: n, decision, ...body && body !== "true" ? { body } : {} });
5813
6101
  return out(r, () => io.print(r.said));
5814
6102
  }
5815
6103
  case "ask": {
5816
- const r = await branches.askSomeoneElse(ctx, { pr: number(a, 2, "bridge branch ask <n>"), dryRun: has3(a, "dry-run") });
6104
+ const r = await branches.askSomeoneElse(ctx, { pr: number(a, 2, "bridge branch ask <n>"), dryRun: has4(a, "dry-run") });
5817
6105
  return out(r, () => io.print(r.said));
5818
6106
  }
5819
6107
  case "comment": {
5820
6108
  const n = number(a, 2, 'bridge branch comment <n> "text"');
5821
- const body = a._.slice(3).join(" ") || flag3(a, "body");
6109
+ const body = a._.slice(3).join(" ") || flag4(a, "body");
5822
6110
  if (!body) throw new RequestUsageError('Usage: bridge branch comment <n> "text" [--reply-to <id>]');
5823
- const reply = flag3(a, "reply-to");
6111
+ const reply = flag4(a, "reply-to");
5824
6112
  const r = await branches.commentOnBranch(ctx, { pr: n, body, ...reply ? { replyTo: Number(reply) } : {} });
5825
6113
  return out(r, () => io.print(r.said));
5826
6114
  }
5827
6115
  case "merge": {
5828
6116
  const n = number(a, 2, 'bridge branch merge <n> [--override "reason"] [--dry-run]');
5829
- const reason = flag3(a, "override");
5830
- const r = await branches.mergeBranch(ctx, { pr: n, ...reason ? { overrideReason: reason } : {}, dryRun: has3(a, "dry-run") });
6117
+ const reason = flag4(a, "override");
6118
+ const r = await branches.mergeBranch(ctx, { pr: n, ...reason ? { overrideReason: reason } : {}, dryRun: has4(a, "dry-run") });
5831
6119
  return out(r, () => io.print(r.said));
5832
6120
  }
5833
6121
  case "close": {
@@ -5840,9 +6128,9 @@ async function runBranch(a, root, io = defaultIO()) {
5840
6128
  }
5841
6129
  case "update": {
5842
6130
  const n = number(a, 2, "bridge branch update <n> [--choice key=mine|theirs] [--dry-run]");
5843
- const choices = pairs(a.flags.choice, "choice");
6131
+ const choices = pairs2(a.flags.choice, "choice");
5844
6132
  for (const [k, v] of Object.entries(choices)) if (v !== "mine" && v !== "theirs") throw new RequestUsageError(`--choice ${k}= takes mine or theirs, got "${v}".`);
5845
- const r = await branches.updateFromMain(ctx, { pr: n, ...Object.keys(choices).length ? { choices } : {}, dryRun: has3(a, "dry-run") });
6133
+ const r = await branches.updateFromMain(ctx, { pr: n, ...Object.keys(choices).length ? { choices } : {}, dryRun: has4(a, "dry-run") });
5846
6134
  return out(r, () => {
5847
6135
  io.print(said(r));
5848
6136
  for (const c of r.conflicts ?? []) io.print(` ${c.key} (${c.token}, default ${c.default ?? "none: pick one"})`);
@@ -5858,7 +6146,7 @@ Share with a product team: ${r.share.install ? r.share.install : r.share.sentenc
5858
6146
  }
5859
6147
  case "combine": {
5860
6148
  const prs = a._.slice(2);
5861
- const picks = Object.fromEntries(Object.entries(pairs(a.flags.pick, "pick")).map(([k, v]) => [k, branches.branchNumber(v)]));
6149
+ const picks = Object.fromEntries(Object.entries(pairs2(a.flags.pick, "pick")).map(([k, v]) => [k, branches.branchNumber(v)]));
5862
6150
  const r = await branches.combineBranches(ctx, { prs, ...Object.keys(picks).length ? { picks } : {} });
5863
6151
  return out(r, () => {
5864
6152
  io.print(`${r.tokens} tokens change together.`);
@@ -5874,8 +6162,8 @@ ${BRANCH_HELP}`);
5874
6162
  }
5875
6163
  }
5876
6164
  async function runStudioRead(cmd, a, root, io = defaultIO()) {
5877
- const ctx = await context3(root, io);
5878
- const json = has3(a, "json");
6165
+ const ctx = await context4(root, io);
6166
+ const json = has4(a, "json");
5879
6167
  if (cmd === "products") {
5880
6168
  const r2 = await branches.listProducts(ctx);
5881
6169
  if (json) return io.print(JSON.stringify(r2, null, 2)), 0;
@@ -5889,6 +6177,7 @@ This folder: ${r2.current.name}. Pick another with --product <key>.`);
5889
6177
  if (json) return io.print(JSON.stringify(r2, null, 2)), 0;
5890
6178
  io.print(`Design approval: ${r2.roles.designApproval.join(", ") || "any reviewer"}`);
5891
6179
  io.print(`Tech approval: ${r2.roles.techApproval.join(", ") || "any reviewer"}`);
6180
+ if (r2.roles.soloOwner) io.print(r2.roles.soloOwner);
5892
6181
  io.print(`Merge: ${r2.roles.ship}`);
5893
6182
  io.print(`Owner override: ${r2.ownerOverride.on ? `${r2.ownerOverride.who.join(", ")}. ${r2.ownerOverride.reason}` : r2.ownerOverride.reason}`);
5894
6183
  io.print(`Protection: ${r2.protection}`);
@@ -5904,7 +6193,7 @@ This folder: ${r2.current.name}. Pick another with --product <key>.`);
5904
6193
  }
5905
6194
  async function runDocsSave(a, root, io = defaultIO()) {
5906
6195
  const component = a._[2];
5907
- const file = flag3(a, "file");
6196
+ const file = flag4(a, "file");
5908
6197
  if (!component || !file) throw new RequestUsageError('Usage: bridge docs save <Component> --file docs.json [--pr <n>] [--title "..."] [--jira DS-42] [--product admin] [--dry-run]');
5909
6198
  let docs;
5910
6199
  try {
@@ -5912,28 +6201,32 @@ async function runDocsSave(a, root, io = defaultIO()) {
5912
6201
  } catch (e) {
5913
6202
  throw new RequestUsageError(`${file} is not a readable JSON file: ${e.message}`);
5914
6203
  }
5915
- const ctx = await context3(root, io);
5916
- const pr = flag3(a, "pr");
6204
+ const ctx = await context4(root, io);
6205
+ const pr = flag4(a, "pr");
6206
+ const changes = changesFlag(a);
5917
6207
  const r = await studio.saveDocs(ctx, {
5918
6208
  component,
5919
6209
  docs,
5920
6210
  ...pr ? { pr } : {},
5921
- ...flag3(a, "title") ? { title: flag3(a, "title") } : {},
5922
- ...flag3(a, "notes") ? { notes: flag3(a, "notes") } : {},
5923
- ...flag3(a, "jira") ? { jira: flag3(a, "jira") } : {},
5924
- ...flag3(a, "product") ? { product: flag3(a, "product") } : {},
5925
- dryRun: has3(a, "dry-run")
6211
+ ...flag4(a, "title") ? { title: flag4(a, "title") } : {},
6212
+ ...flag4(a, "notes") ? { notes: flag4(a, "notes") } : {},
6213
+ ...flag4(a, "jira") ? { jira: flag4(a, "jira") } : {},
6214
+ ...flag4(a, "product") ? { product: flag4(a, "product") } : {},
6215
+ ...changes ? { changes } : {},
6216
+ source: "cli",
6217
+ dryRun: has4(a, "dry-run")
5926
6218
  });
5927
- if (has3(a, "json")) io.print(JSON.stringify(r, null, 2));
6219
+ if (has4(a, "json")) io.print(JSON.stringify(r, null, 2));
5928
6220
  else io.print(r.status === "dry-run" || r.status === "preview" ? `${r.said ?? "Dry run, nothing written."}
5929
6221
 
5930
6222
  ${r.path}:
5931
- ${r.content}` : String(r.said));
6223
+ ${r.content}` : `${String(r.said)}${r.next ? `
6224
+ ${String(r.next)}` : ""}`);
5932
6225
  return 0;
5933
6226
  }
5934
6227
 
5935
6228
  // src/pieces.ts
5936
- import { BridgeContext as BridgeContext5, branches as branches2, pieces } from "@bridge-ds/mcp";
6229
+ import { BridgeContext as BridgeContext6, branches as branches2, pieces } from "@bridge-ds/mcp";
5937
6230
  var PIECES_HELP = `bridge pieces: a product branch and the library
5938
6231
 
5939
6232
  Usage:
@@ -5948,10 +6241,10 @@ Usage:
5948
6241
  bridge pieces switch <n> once the library shipped: main into the product branch
5949
6242
 
5950
6243
  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) {
6244
+ var has5 = (a, k) => Boolean(a.flags[k]);
6245
+ async function context5(root, io) {
5953
6246
  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 } : {} });
6247
+ return BridgeContext6.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
5955
6248
  }
5956
6249
  function number2(a, at, usage) {
5957
6250
  const raw = a._[at];
@@ -5965,12 +6258,12 @@ function piece(a, at, usage) {
5965
6258
  }
5966
6259
  async function runPieces(a, root, io = defaultIO()) {
5967
6260
  const sub = a._[1];
5968
- if (!sub || sub === "help" || has4(a, "help")) {
6261
+ if (!sub || sub === "help" || has5(a, "help")) {
5969
6262
  io.print(PIECES_HELP);
5970
- return sub || has4(a, "help") ? 0 : 2;
6263
+ return sub || has5(a, "help") ? 0 : 2;
5971
6264
  }
5972
- const json = has4(a, "json");
5973
- const ctx = await context4(root, io);
6265
+ const json = has5(a, "json");
6266
+ const ctx = await context5(root, io);
5974
6267
  const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
5975
6268
  switch (sub) {
5976
6269
  case "status": {
@@ -5985,9 +6278,10 @@ async function runPieces(a, root, io = defaultIO()) {
5985
6278
  }
5986
6279
  case "open": {
5987
6280
  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" });
6281
+ const changes = changesFlag(a);
6282
+ const r = await pieces.openLibraryBranchOp(ctx, { pr: number2(a, 2, usage), piece: piece(a, 3, usage), usePreview: !has5(a, "no-preview"), dryRun: has5(a, "dry-run"), source: "cli", ...changes ? { changes } : {} });
5989
6283
  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}.`);
6284
+ io.print(`${r.status === "dry-run" ? "Would open" : r.status === "already-linked" ? "Already linked" : r.status === "suggested" ? "Suggested" : "Opened"} a ${r.opens}: ${r.title}, needed by ${r.neededBy}.`);
5991
6285
  if (r.library) io.print(` #${r.library.number} ${r.library.url}`);
5992
6286
  if (r.preview) io.print(` ${r.preview.said}`);
5993
6287
  for (const w of r.warnings ?? []) io.print(` ${w}`);
@@ -6004,7 +6298,7 @@ async function runPieces(a, root, io = defaultIO()) {
6004
6298
  return out(r, () => io.print(r.said));
6005
6299
  }
6006
6300
  default: {
6007
- const r = await pieces.missingPiecesOp(ctx, { pr: number2(a, 1, "bridge pieces <n> [--record]"), record: has4(a, "record") });
6301
+ const r = await pieces.missingPiecesOp(ctx, { pr: number2(a, 1, "bridge pieces <n> [--record]"), record: has5(a, "record") });
6008
6302
  return out(r, () => {
6009
6303
  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
6304
  for (const p of r.pieces) io.print(` ${p.id} ${p.title}: ${p.summary} Opens ${p.opens}.${"linked" in p ? ` Linked: #${p.linked}.` : ""}`);
@@ -6020,7 +6314,7 @@ ${r.fixes.sample.map((s) => ` ${s}`).join("\n")}`);
6020
6314
  // src/figma.ts
6021
6315
  import { readFileSync as readFileSync2 } from "node:fs";
6022
6316
  import { resolve as resolve4 } from "node:path";
6023
- import { BridgeContext as BridgeContext6, branches as branches3, figma } from "@bridge-ds/mcp";
6317
+ import { BridgeContext as BridgeContext7, branches as branches3, figma } from "@bridge-ds/mcp";
6024
6318
  var FIGMA_HELP = `bridge figma: component changes sent from Figma
6025
6319
 
6026
6320
  Usage:
@@ -6034,10 +6328,10 @@ Usage:
6034
6328
  token change, or a branch with the spec for Claude AI
6035
6329
 
6036
6330
  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) {
6331
+ var has6 = (a, k) => Boolean(a.flags[k]);
6332
+ async function context6(root, io) {
6039
6333
  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 } : {} });
6334
+ return BridgeContext7.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
6041
6335
  }
6042
6336
  function number3(a, usage) {
6043
6337
  const raw = a._[2];
@@ -6046,17 +6340,17 @@ function number3(a, usage) {
6046
6340
  }
6047
6341
  async function runFigma(a, root, io = defaultIO()) {
6048
6342
  const sub = a._[1];
6049
- if (sub === "help" || has5(a, "help")) {
6343
+ if (sub === "help" || has6(a, "help")) {
6050
6344
  io.print(FIGMA_HELP);
6051
6345
  return 0;
6052
6346
  }
6053
- const json = has5(a, "json");
6054
- const ctx = await context5(root, io);
6347
+ const json = has6(a, "json");
6348
+ const ctx = await context6(root, io);
6055
6349
  const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
6056
6350
  switch (sub) {
6057
6351
  case void 0:
6058
6352
  case "list": {
6059
- const r = await figma.figmaChangesOp(ctx, { state: has5(a, "all") ? "all" : "open" });
6353
+ const r = await figma.figmaChangesOp(ctx, { state: has6(a, "all") ? "all" : "open" });
6060
6354
  return out(r, () => {
6061
6355
  for (const c of r.changes) io.print(`#${c.pr} ${c.title} (${c.route}): ${c.status}. ${c.said}`);
6062
6356
  io.print(`Next: ${r.next}`);
@@ -6064,7 +6358,7 @@ async function runFigma(a, root, io = defaultIO()) {
6064
6358
  }
6065
6359
  case "show": {
6066
6360
  const r = await figma.getFigmaChangeOp(ctx, { pr: number3(a, "bridge figma show <n>") });
6067
- if (has5(a, "draft")) {
6361
+ if (has6(a, "draft")) {
6068
6362
  if (!r.brief) throw new RequestUsageError(`#${r.pr} is an exact token change: there is no code to draft.`);
6069
6363
  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
6364
  return io.runClaude(r.brief, root);
@@ -6089,12 +6383,14 @@ ${r.brief}`);
6089
6383
  } catch (err) {
6090
6384
  throw new RequestUsageError(`${file} could not be read as JSON: ${err.message}`);
6091
6385
  }
6092
- const r = await figma.pushFigmaComponentOp(ctx, { diff: diff2, dryRun: has5(a, "dry-run"), source: "cli" });
6386
+ const changes = changesFlag(a);
6387
+ const r = await figma.pushFigmaComponentOp(ctx, { diff: diff2, dryRun: has6(a, "dry-run"), source: "cli", ...changes ? { changes } : {} });
6093
6388
  return out(r, () => {
6094
6389
  io.print(`${r.component}: ${r.route}. ${r.said}`);
6095
6390
  for (const x of r.reasons ?? []) io.print(` ${x}`);
6096
6391
  for (const x of r.fixInFigma ?? []) io.print(` Fix in Figma: ${x.what} ${x.todo}`);
6097
6392
  if (r.library) io.print(` #${r.library.number} ${r.library.url}`);
6393
+ if (r.suggestion) io.print(` suggestion #${r.suggestion.number} ${r.suggestion.url}`);
6098
6394
  io.print(`Next: ${r.next}`);
6099
6395
  });
6100
6396
  }
@@ -6135,7 +6431,7 @@ async function rolesReport(opts) {
6135
6431
  }
6136
6432
 
6137
6433
  // src/index.ts
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";
6434
+ import { brand, BridgeContext as BridgeContext8, CONFIG_FILE as CONFIG_FILE2, core, detectConfig, GitRefSource, history as hist, LocalSource, ops, optionsFromArgv, serveStdio, toConfigJson, VERSION } from "@bridge-ds/mcp";
6139
6435
  var MCP_PACKAGE = "@bridge-ds/mcp";
6140
6436
  var HELP = `bridge ${VERSION}: design system as code, for you and your Claude
6141
6437
 
@@ -6151,8 +6447,10 @@ Usage:
6151
6447
  bridge diff <base> [--json|--md] designer-readable token diff: working tree vs <base> (git ref)
6152
6448
  bridge propose --title "..." --set path=value [--set ...] [--file f | --mode theme=dark]
6153
6449
  [--edits edits.json] [--summary "..."] [--branch explore/<name>] [--pr <n>]
6154
- [--product <key>] [--jira DS-42] [--dry-run]
6155
- validate + contrast + new local bridge/* branch (never pushes).
6450
+ [--product <key>] [--jira DS-42] [--suggest | --direct] [--dry-run]
6451
+ validate + contrast, then a suggestion (a GitHub issue a developer
6452
+ applies with bridge apply) or, with "changes": "direct" in
6453
+ bridge.config.json or --direct, a new local bridge/* branch (never pushes).
6156
6454
  --branch: write the edits to the checked-out exploration instead.
6157
6455
  --pr: add them to that branch in review (GitHub token).
6158
6456
  --product: the product it is for (bridge products); --jira: the
@@ -6160,9 +6458,10 @@ Usage:
6160
6458
  bridge brand list [--json] brands: slug, name, default, primary action color, theme overrides
6161
6459
  bridge brand create <slug> --from bridge --primary <hex|hue> --neutral <hex|hue>
6162
6460
  [--name "Acme"] [--accent x] [--link x] [--radius soft|balanced|crisp|i,c,co,o]
6163
- [--heading font] [--text font] [--notes "..."] [--dry-run] [--json]
6164
- new brand: generated ramps, contrast gate for every theme,
6165
- new local bridge/brand-<slug> branch (never pushes, never main).
6461
+ [--heading font] [--text font] [--notes "..."] [--suggest | --direct] [--dry-run] [--json]
6462
+ new brand: generated ramps, contrast gate for every theme, then a
6463
+ suggestion, or in direct mode a new local bridge/brand-<slug> branch
6464
+ (never pushes, never main).
6166
6465
  Exit 1 when blocked
6167
6466
  bridge impact [--base main] [--head HEAD] [--out dir] [--only stories|app] [--threshold 0.03]
6168
6467
  [--plan] [--json] [--image-base url] [--artifact-url url] [--keep-unchanged]
@@ -6176,9 +6475,9 @@ Usage:
6176
6475
  Needs Chrome installed
6177
6476
  bridge history [token] [--json] [--limit n] [--author a] [--tier n] [--version v]
6178
6477
  token history: versions, merged changes, who, when, why
6179
- bridge revert <pr|commit|id> [--dry-run] [--restore-conflicts] [--use-before path@file] [--json]
6180
- undo a merged token change against HEAD: conflicts shown,
6181
- new local bridge/* branch (never pushes, never main)
6478
+ bridge revert <pr|commit|id> [--dry-run] [--restore-conflicts] [--use-before path@file] [--suggest | --direct] [--json]
6479
+ undo a merged token change against HEAD: conflicts shown, then a
6480
+ suggestion, or in direct mode a new local bridge/* branch (never pushes)
6182
6481
  bridge audit <path> [--json [out.json]] [--md [out.md]] [--fail-under 80] [--baseline prev.json]
6183
6482
  [--label "..."] [--ds dir] [--no-regress] [--allowlist known.json]
6184
6483
  design system adoption in a product folder: token and component
@@ -6221,6 +6520,17 @@ Usage:
6221
6520
  a product branch and the library (\`bridge pieces help\`): what it adds
6222
6521
  that the library lacks, the library branch each piece opens (linked
6223
6522
  both ways), its preview in the product branch, the published version
6523
+ bridge suggestions [list | show <n> | link <n> --pr <m>] [--status approved] [--json]
6524
+ suggestions: changes filed as GitHub issues (label bridge-suggestion)
6525
+ that the owners approve and a developer applies (\`bridge suggestions help\`)
6526
+ bridge apply <n> [--write | --patch] [--dry-run] [--accept-edited] [--force-unapproved] [--json]
6527
+ apply suggestion #n on today's code: a new local branch (working tree
6528
+ untouched, nothing pushed), --write into the working tree, --patch for
6529
+ git am. Exit 0 applied or already applied, 1 not approved or blocked,
6530
+ 2 usage, 3 conflict
6531
+ bridge review <n> --approve | --request-changes --message "..." | --decline --message "..."
6532
+ review a suggestion as its owner (counts only for owners in
6533
+ bridge.config.json, on the version you saw)
6224
6534
  bridge figma [show <n> | drafted <n> | push <diff.json>]
6225
6535
  component changes sent from Figma (\`bridge figma help\`): exact token
6226
6536
  changes and branches Claude AI drafts from the spec
@@ -6234,8 +6544,11 @@ Usage:
6234
6544
  Options:
6235
6545
  --root <dir> repo root (default: current directory)
6236
6546
  --ds <dir> audit: design system repo with bridge.config.json (default: --root, or the nearest one above)
6237
- GITHUB_TOKEN env, optional: lets the MCP server open PRs on a remote repo, and bridge request create
6238
- file issues (else \`gh auth token\`). Never needed for local token work.
6547
+ GITHUB_TOKEN env, optional: lets bridge file suggestions and requests (Issues: read and write) and the
6548
+ MCP server open PRs on a remote repo in direct mode (else \`gh auth token\`). Never needed for
6549
+ local token work in direct mode.
6550
+ --suggest | --direct how a change reaches the code for this command (propose, brand create, revert,
6551
+ docs save, pieces open, figma push). Default: "changes" in bridge.config.json, else suggest.
6239
6552
  `;
6240
6553
  function parse(argv) {
6241
6554
  const out = { _: [], flags: {} };
@@ -6245,23 +6558,32 @@ function parse(argv) {
6245
6558
  const [k, inline] = a.slice(2).split(/=(.*)/s, 2);
6246
6559
  const next = argv[i + 1];
6247
6560
  const fileArg = k === "json" && /\.json$/i.test(next ?? "") || k === "md" && /\.(md|markdown)$/i.test(next ?? "");
6248
- 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));
6561
+ 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", "patch", "accept-edited", "force-unapproved", "suggest", "direct"].includes(k));
6249
6562
  (out.flags[k] ??= []).push(inline ?? (takesValue ? argv[++i] : "true"));
6250
6563
  } else out._.push(a);
6251
6564
  }
6252
6565
  return out;
6253
6566
  }
6254
- var flag4 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
6255
- var has6 = (a, k) => Boolean(a.flags[k]);
6567
+ var flag5 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
6568
+ var has7 = (a, k) => Boolean(a.flags[k]);
6256
6569
  var print = (s) => process.stdout.write(`${s}
6257
6570
  `);
6258
6571
  var modeArg = (s) => s ? Object.fromEntries(s.split(",").map((kv) => kv.split("="))) : void 0;
6572
+ var githubToken = () => process.env.BRIDGE_GITHUB_TOKEN || process.env.GITHUB_TOKEN || defaultIO().ghToken();
6573
+ function printSent(r) {
6574
+ if (r.status === "suggested") print(`suggestion: #${r.number} ${r.url}`);
6575
+ if (r.message) print(String(r.message));
6576
+ if (r.branch && r.status !== "suggested") print(`branch: ${r.branch}`);
6577
+ for (const w of r.warnings ?? []) print(`warning: ${w}`);
6578
+ if (r.next) print(String(r.next));
6579
+ if (r.hint) print(String(r.hint));
6580
+ }
6259
6581
  async function init(a, root) {
6260
6582
  const cfgPath = join6(root, CONFIG_FILE2);
6261
6583
  let config;
6262
- if (existsSync6(cfgPath) && !has6(a, "force")) {
6584
+ if (existsSync6(cfgPath) && !has7(a, "force")) {
6263
6585
  print(`${CONFIG_FILE2} exists, keeping it (--force to regenerate).`);
6264
- config = (await (await BridgeContext7.create({ root })).config()).config;
6586
+ config = (await (await BridgeContext8.create({ root })).config()).config;
6265
6587
  } else {
6266
6588
  const src = new LocalSource(root);
6267
6589
  const files = await src.list("");
@@ -6289,14 +6611,14 @@ async function init(a, root) {
6289
6611
  print(` claude mcp add bridge -- ${desktop.command} ${desktop.args.join(" ")}`);
6290
6612
  }
6291
6613
  async function check(a, root) {
6292
- const ctx = await BridgeContext7.create({ root });
6614
+ const ctx = await BridgeContext8.create({ root });
6293
6615
  const v = await ops.validateTokens(ctx, { includeWarnings: true });
6294
6616
  const c = await ops.checkContrast(ctx, {});
6295
6617
  const warnings = "warnings" in v ? v.warnings ?? [] : [];
6296
- const failed = !v.valid || "failing" in c && (c.failing ?? 0) > 0 || has6(a, "strict") && warnings.length > 0;
6618
+ const failed = !v.valid || "failing" in c && (c.failing ?? 0) > 0 || has7(a, "strict") && warnings.length > 0;
6297
6619
  const protection = await protectionOf(a, root, ctx);
6298
6620
  const roles = await rolesOf(a, root, ctx);
6299
- if (has6(a, "json")) {
6621
+ if (has7(a, "json")) {
6300
6622
  print(JSON.stringify({ ok: !failed, validation: v, contrast: c, ...protection ? { protection: protection.state } : {}, ...roles?.teams.length ? { teams: roles.teams } : {} }, null, 2));
6301
6623
  return failed ? 1 : 0;
6302
6624
  }
@@ -6315,7 +6637,7 @@ async function check(a, root) {
6315
6637
  return failed ? 1 : 0;
6316
6638
  }
6317
6639
  async function protectionOf(a, root, ctx) {
6318
- if (has6(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
6640
+ if (has7(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
6319
6641
  const { config } = await ctx.config();
6320
6642
  const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
6321
6643
  if (!repo) return void 0;
@@ -6323,7 +6645,7 @@ async function protectionOf(a, root, ctx) {
6323
6645
  return protectionReport({ repo, defaultBranch: config.repo?.defaultBranch ?? "main", token });
6324
6646
  }
6325
6647
  async function rolesOf(a, root, ctx) {
6326
- if (has6(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
6648
+ if (has7(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
6327
6649
  const { config } = await ctx.config();
6328
6650
  const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
6329
6651
  if (!repo) return void 0;
@@ -6333,9 +6655,9 @@ async function rolesOf(a, root, ctx) {
6333
6655
  async function tokens(a, root) {
6334
6656
  const q = a._.slice(1).join(" ");
6335
6657
  if (!q) throw new UsageError("bridge tokens <query>");
6336
- const ctx = await BridgeContext7.create({ root });
6337
- const r = await ops.searchTokens(ctx, { query: q, limit: Number(flag4(a, "limit") ?? 20) });
6338
- if (has6(a, "json")) return print(JSON.stringify(r, null, 2));
6658
+ const ctx = await BridgeContext8.create({ root });
6659
+ const r = await ops.searchTokens(ctx, { query: q, limit: Number(flag5(a, "limit") ?? 20) });
6660
+ if (has7(a, "json")) return print(JSON.stringify(r, null, 2));
6339
6661
  if (!r.results.length) return print(`No token matches "${q}".`);
6340
6662
  const w = Math.max(...r.results.map((x) => x.path.length));
6341
6663
  for (const t of r.results) print(`${t.path.padEnd(w)} ${String(t.value).padEnd(10)} ${(t.tier ?? "").padEnd(9)} ${t.description ?? ""}`);
@@ -6343,20 +6665,20 @@ async function tokens(a, root) {
6343
6665
  async function diff(a, root) {
6344
6666
  const base = a._[1];
6345
6667
  if (!base) throw new UsageError("bridge diff <base> (a git ref: main, HEAD~1, a sha)");
6346
- const ctx = await BridgeContext7.create({ root });
6668
+ const ctx = await BridgeContext8.create({ root });
6347
6669
  const { config } = await ctx.config();
6348
6670
  const before = await core.loadTokenSet(new GitRefSource(root, base), config);
6349
6671
  const after = await ctx.tokens();
6350
6672
  const changes = core.diff(before, after);
6351
- const pairs2 = await core.readContrastPairs(ctx.source, config).catch(() => []);
6352
- const delta2 = pairs2.length ? core.contrastDelta(core.runContrast(before, pairs2, config.contrast?.modes), core.runContrast(after, pairs2, config.contrast?.modes)) : [];
6673
+ const pairs3 = await core.readContrastPairs(ctx.source, config).catch(() => []);
6674
+ const delta2 = pairs3.length ? core.contrastDelta(core.runContrast(before, pairs3, config.contrast?.modes), core.runContrast(after, pairs3, config.contrast?.modes)) : [];
6353
6675
  const tier = core.classify(changes, [], config);
6354
6676
  const imp = core.impact(before, after, changes, 30);
6355
- if (has6(a, "json")) {
6677
+ if (has7(a, "json")) {
6356
6678
  print(JSON.stringify({ base, changes, contrast: delta2, tier, impact: imp }, null, 2));
6357
6679
  return 0;
6358
6680
  }
6359
- if (has6(a, "md")) {
6681
+ if (has7(a, "md")) {
6360
6682
  print(`## Token changes since \`${base}\`
6361
6683
 
6362
6684
  ${core.tokenTable(changes)}
@@ -6391,26 +6713,27 @@ ${imp.total} other token(s) change value through aliases:`);
6391
6713
  return delta2.some((d) => d.passBefore && !d.passAfter) ? 1 : 0;
6392
6714
  }
6393
6715
  async function propose(a, root) {
6394
- const title = flag4(a, "title");
6716
+ const title = flag5(a, "title");
6395
6717
  if (!title) throw new UsageError('bridge propose --title "Darken muted text" --set color.text.muted={palette.neutral.700} --mode theme=light');
6396
6718
  const edits = [];
6397
- const file = flag4(a, "file");
6398
- const mode2 = modeArg(flag4(a, "mode"));
6719
+ const file = flag5(a, "file");
6720
+ const mode2 = modeArg(flag5(a, "mode"));
6399
6721
  for (const s of a.flags.set ?? []) {
6400
6722
  const i = s.indexOf("=");
6401
6723
  if (i < 0) throw new UsageError(`--set needs path=value, got "${s}"`);
6402
6724
  edits.push({ op: "set", path: s.slice(0, i), value: s.slice(i + 1), ...file ? { file } : {}, ...mode2 ? { mode: mode2 } : {} });
6403
6725
  }
6404
- const editsFile = flag4(a, "edits");
6726
+ const editsFile = flag5(a, "edits");
6405
6727
  if (editsFile) edits.push(...JSON.parse(await readFile7(resolve5(editsFile), "utf8")));
6406
6728
  if (!edits.length) throw new UsageError("Give at least one --set path=value or --edits file.json");
6407
- const pr = flag4(a, "pr");
6408
- const token = pr ? process.env.BRIDGE_GITHUB_TOKEN || process.env.GITHUB_TOKEN || defaultIO().ghToken() : void 0;
6409
- const ctx = await BridgeContext7.create({ root, ...token ? { token } : {} });
6410
- const summary = flag4(a, "summary");
6411
- const branch = flag4(a, "branch");
6412
- const jira = flag4(a, "jira");
6413
- const product = flag4(a, "product");
6729
+ const pr = flag5(a, "pr");
6730
+ const changes = changesFlag(a);
6731
+ const token = pr || changes !== "direct" ? githubToken() : void 0;
6732
+ const ctx = await BridgeContext8.create({ root, ...token ? { token } : {} });
6733
+ const summary = flag5(a, "summary");
6734
+ const branch = flag5(a, "branch");
6735
+ const jira = flag5(a, "jira");
6736
+ const product = flag5(a, "product");
6414
6737
  const r = await ops.proposeTokenChange(ctx, {
6415
6738
  title,
6416
6739
  edits,
@@ -6419,34 +6742,36 @@ async function propose(a, root) {
6419
6742
  ...pr ? { pr } : {},
6420
6743
  ...jira ? { jira } : {},
6421
6744
  ...product ? { product } : {},
6422
- dryRun: has6(a, "dry-run")
6745
+ ...changes ? { changes } : {},
6746
+ source: "cli",
6747
+ dryRun: has7(a, "dry-run")
6423
6748
  });
6424
- if (has6(a, "json")) print(JSON.stringify(r, null, 2));
6749
+ if (has7(a, "json")) print(JSON.stringify(r, null, 2));
6425
6750
  else {
6426
6751
  print(`status: ${r.status}${r.why ? ` (${r.why})` : ""}`);
6427
6752
  if (r.tier) print(`tier ${r.tier}, ${r.bump}`);
6428
6753
  for (const c of r.changes ?? []) print(` ${c.kind} ${c.path}: ${c.before ?? ""} -> ${c.after ?? ""}`);
6429
- if (r.branch) print(`branch: ${r.branch}`);
6430
- if (r.next) print(String(r.next));
6431
- if (r.hint) print(String(r.hint));
6754
+ printSent(r);
6432
6755
  if (r.status === "dry-run") print(`
6433
- ${r.prBody}`);
6756
+ ${r.suggestion?.body ?? r.prBody}`);
6434
6757
  }
6435
6758
  return r.status === "blocked" ? 1 : 0;
6436
6759
  }
6437
6760
  async function brandCmd(a, root) {
6438
6761
  const sub = a._[1];
6439
- const ctx = await BridgeContext7.create({ root });
6762
+ const changes = changesFlag(a);
6763
+ const token = sub === "create" && changes !== "direct" ? githubToken() : void 0;
6764
+ const ctx = await BridgeContext8.create({ root, ...token ? { token } : {} });
6440
6765
  if (sub === "list") {
6441
6766
  const r2 = await brand.listBrands(ctx);
6442
- if (has6(a, "json")) print(JSON.stringify(r2.brands, null, 2));
6767
+ if (has7(a, "json")) print(JSON.stringify(r2.brands, null, 2));
6443
6768
  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(", ")}` : ""}`);
6444
6769
  return 0;
6445
6770
  }
6446
6771
  if (sub !== "create") throw new UsageError('bridge brand list | bridge brand create <slug> --from bridge --primary "#1F5F3A" --neutral "#2A2F2C"');
6447
6772
  const slug = a._[2];
6448
6773
  if (!slug) throw new UsageError('bridge brand create <slug> --from bridge --primary "#1F5F3A" --neutral "#2A2F2C"');
6449
- const opt = (k) => flag4(a, k);
6774
+ const opt = (k) => flag5(a, k);
6450
6775
  const fonts = { ...opt("heading") ? { heading: opt("heading") } : {}, ...opt("text") ? { text: opt("text") } : {} };
6451
6776
  const r = await brand.proposeBrand(ctx, {
6452
6777
  slug,
@@ -6459,9 +6784,11 @@ async function brandCmd(a, root) {
6459
6784
  ...opt("radius") ? { radius: opt("radius") } : {},
6460
6785
  ...Object.keys(fonts).length ? { fonts } : {},
6461
6786
  ...opt("notes") ? { notes: opt("notes") } : {},
6462
- dryRun: has6(a, "dry-run")
6787
+ ...changes ? { changes } : {},
6788
+ source: "cli",
6789
+ dryRun: has7(a, "dry-run")
6463
6790
  });
6464
- if (has6(a, "json")) {
6791
+ if (has7(a, "json")) {
6465
6792
  const { fileContents: _f, ...rest } = r;
6466
6793
  print(JSON.stringify(rest, null, 2));
6467
6794
  return r.status === "blocked" ? 1 : 0;
@@ -6476,13 +6803,11 @@ async function brandCmd(a, root) {
6476
6803
  for (const i of r.issues) print(` ${i.level}: ${i.message}`);
6477
6804
  print("files:");
6478
6805
  for (const f of r.files) print(` ${f.action} ${f.path}`);
6479
- if (r.branch) print(`branch: ${r.branch}`);
6480
- if (r.next) print(String(r.next));
6481
- if (r.hint) print(String(r.hint));
6806
+ printSent(r);
6482
6807
  return r.status === "blocked" ? 1 : 0;
6483
6808
  }
6484
6809
  async function figmaReferences(a, root) {
6485
- let sha = flag4(a, "sha");
6810
+ let sha = flag5(a, "sha");
6486
6811
  if (!sha) {
6487
6812
  try {
6488
6813
  sha = execFileSync2("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
@@ -6490,33 +6815,33 @@ async function figmaReferences(a, root) {
6490
6815
  sha = "unknown";
6491
6816
  }
6492
6817
  }
6493
- const only = (flag4(a, "only") ?? "").split(",").filter(Boolean);
6818
+ const only = (flag5(a, "only") ?? "").split(",").filter(Boolean);
6494
6819
  const r = await captureFigmaReferences({
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"),
6820
+ storybook: resolve5(root, flag5(a, "storybook") ?? "storybook-static"),
6821
+ specs: resolve5(root, flag5(a, "specs") ?? "packages/react/component-specs.json"),
6822
+ out: resolve5(root, flag5(a, "out") ?? "figma-references"),
6498
6823
  sha,
6499
6824
  ...only.length ? { only } : {},
6500
- log: has6(a, "json") ? void 0 : (line) => print(line)
6825
+ log: has7(a, "json") ? void 0 : (line) => print(line)
6501
6826
  });
6502
- if (has6(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
6827
+ if (has7(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
6503
6828
  return 0;
6504
6829
  }
6505
6830
  async function impact(a, root) {
6506
- const only = (flag4(a, "only") ?? "").split(",").filter(Boolean);
6831
+ const only = (flag5(a, "only") ?? "").split(",").filter(Boolean);
6507
6832
  for (const x of only) if (x !== "stories" && x !== "app") throw new UsageError(`--only takes stories or app, got "${x}"`);
6508
- const num2 = (k) => flag4(a, k) !== void 0 ? Number(flag4(a, k)) : void 0;
6833
+ const num2 = (k) => flag5(a, k) !== void 0 ? Number(flag5(a, k)) : void 0;
6509
6834
  const threshold = num2("threshold");
6510
6835
  if (threshold !== void 0 && !(threshold >= 0 && threshold <= 1)) throw new UsageError("--threshold is between 0 and 1 (pixelmatch color distance)");
6511
- const imageBase = flag4(a, "image-base");
6512
- const artifactUrl = flag4(a, "artifact-url");
6513
- const out = flag4(a, "out");
6514
- const cacheDir = flag4(a, "cache");
6836
+ const imageBase = flag5(a, "image-base");
6837
+ const artifactUrl = flag5(a, "artifact-url");
6838
+ const out = flag5(a, "out");
6839
+ const cacheDir = flag5(a, "cache");
6515
6840
  const maxStories = num2("max-stories");
6516
6841
  const r = await runImpact({
6517
6842
  root,
6518
- base: flag4(a, "base") ?? "main",
6519
- head: flag4(a, "head") ?? "HEAD",
6843
+ base: flag5(a, "base") ?? "main",
6844
+ head: flag5(a, "head") ?? "HEAD",
6520
6845
  ...out ? { out: resolve5(out) } : {},
6521
6846
  ...only.length ? { only } : {},
6522
6847
  ...threshold !== void 0 ? { threshold } : {},
@@ -6524,15 +6849,15 @@ async function impact(a, root) {
6524
6849
  ...maxStories ? { maxStoriesPerComponent: maxStories } : {},
6525
6850
  ...imageBase !== void 0 ? { imageBase } : {},
6526
6851
  ...artifactUrl ? { artifactUrl } : {},
6527
- plan: has6(a, "plan"),
6528
- keepUnchanged: has6(a, "keep-unchanged")
6852
+ plan: has7(a, "plan"),
6853
+ keepUnchanged: has7(a, "keep-unchanged")
6529
6854
  });
6530
- if (has6(a, "json")) {
6855
+ if (has7(a, "json")) {
6531
6856
  print(JSON.stringify({ summary: r.summary, manifest: r.manifestPath, comment: r.commentPath ?? null, ...r.manifest.summary, selection: r.manifest.selection }, null, 2));
6532
6857
  return 0;
6533
6858
  }
6534
6859
  const sel = r.manifest.selection;
6535
- if (has6(a, "plan")) {
6860
+ if (has7(a, "plan")) {
6536
6861
  print(`Plan ${r.manifest.base.ref} (${r.manifest.base.sha.slice(0, 7)}) -> ${r.manifest.head.ref} (${r.manifest.head.sha.slice(0, 7)})`);
6537
6862
  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(", ")})` : ""}`);
6538
6863
  print(` components: ${sel.components.map((c) => c.id).join(", ") || "none"}`);
@@ -6548,12 +6873,12 @@ async function impact(a, root) {
6548
6873
  }
6549
6874
  var day = (iso) => iso.slice(0, 10);
6550
6875
  async function historyCmd(a, root) {
6551
- const ctx = await BridgeContext7.create({ root });
6876
+ const ctx = await BridgeContext8.create({ root });
6552
6877
  const token = a._[1];
6553
- const limit = Number(flag4(a, "limit") ?? 20);
6878
+ const limit = Number(flag5(a, "limit") ?? 20);
6554
6879
  if (token) {
6555
6880
  const r2 = await hist.getTokenHistory(ctx, { path: token, limit });
6556
- if (has6(a, "json")) return print(JSON.stringify(r2, null, 2)), 0;
6881
+ if (has7(a, "json")) return print(JSON.stringify(r2, null, 2)), 0;
6557
6882
  if (!r2.total) return print(`No recorded change of ${r2.path} in ${r2.source}.`), 0;
6558
6883
  print(`${r2.path}: ${r2.total} change(s) (${r2.source})
6559
6884
  `);
@@ -6564,9 +6889,9 @@ async function historyCmd(a, root) {
6564
6889
  }
6565
6890
  return 0;
6566
6891
  }
6567
- const tier = flag4(a, "tier");
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;
6892
+ const tier = flag5(a, "tier");
6893
+ const r = await hist.getHistory(ctx, { limit, ...flag5(a, "author") ? { author: flag5(a, "author") } : {}, ...tier ? { tier: Number(tier) } : {}, ...flag5(a, "version") ? { version: flag5(a, "version") } : {}, ...has7(a, "breaking") ? { breaking: true } : {} });
6894
+ if (has7(a, "json")) return print(JSON.stringify(r, null, 2)), 0;
6570
6895
  print(`${r.source}${r.currentVersion ? `, package version ${r.currentVersion}` : ""}`);
6571
6896
  if (!r.total) return print(r.note ?? "No token change matches."), 0;
6572
6897
  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" : ""}`);
@@ -6584,10 +6909,12 @@ ${r.truncated}`);
6584
6909
  async function revertCmd(a, root) {
6585
6910
  const target = a._[1];
6586
6911
  if (!target) throw new UsageError("bridge revert <pr|commit|id> [--dry-run] (ids from `bridge history`)");
6587
- const ctx = await BridgeContext7.create({ root });
6912
+ const changes = changesFlag(a);
6913
+ const token = changes !== "direct" ? githubToken() : void 0;
6914
+ const ctx = await BridgeContext8.create({ root, ...token ? { token } : {} });
6588
6915
  const resolutions = Object.fromEntries((a.flags["use-before"] ?? []).map((k) => [k, "use-before"]));
6589
- const r = await hist.proposeRevert(ctx, { target, resolutions, restoreConflicts: has6(a, "restore-conflicts"), dryRun: has6(a, "dry-run") });
6590
- if (has6(a, "json")) {
6916
+ const r = await hist.proposeRevert(ctx, { target, resolutions, restoreConflicts: has7(a, "restore-conflicts"), dryRun: has7(a, "dry-run"), ...changes ? { changes } : {}, source: "cli" });
6917
+ if (has7(a, "json")) {
6591
6918
  print(JSON.stringify(r, null, 2));
6592
6919
  return r.status === "blocked" ? 1 : 0;
6593
6920
  }
@@ -6602,35 +6929,34 @@ async function revertCmd(a, root) {
6602
6929
  }
6603
6930
  for (const s of r.skipped ?? []) print(` skipped ${s}`);
6604
6931
  if (r.contrast?.moved) print(`contrast: ${r.contrast.moved} pair(s) move, ${r.contrast.newFailures.length} new failure(s)`);
6605
- if (r.branch) print(`branch: ${r.branch}`);
6606
- if (r.next) print(String(r.next));
6932
+ printSent(r);
6607
6933
  if (r.status === "dry-run") print(`
6608
- ${r.prBody}`);
6934
+ ${r.suggestion?.body ?? r.prBody}`);
6609
6935
  return r.status === "blocked" ? 1 : 0;
6610
6936
  }
6611
6937
  async function audit(a, root) {
6612
6938
  const target = a._[1];
6613
6939
  if (!target) throw new UsageError("bridge audit <path> (a product folder or file that uses the design system)");
6614
6940
  if (!existsSync6(target)) throw new UsageError(`No such file or folder: ${target}`);
6615
- const dsRoot = flag4(a, "ds") ? resolve5(flag4(a, "ds")) : existsSync6(join6(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
6941
+ const dsRoot = flag5(a, "ds") ? resolve5(flag5(a, "ds")) : existsSync6(join6(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
6616
6942
  if (!dsRoot) throw new UsageError(`No ${CONFIG_FILE2} found: run bridge audit from the design system repo or pass --ds <dir>.`);
6617
6943
  const ds = loadDesignSystem(dsRoot);
6618
- if (has6(a, "fix")) {
6619
- const write = has6(a, "write");
6944
+ if (has7(a, "fix")) {
6945
+ const write = has7(a, "write");
6620
6946
  const changes = fixCss(target, ds.index, write);
6621
6947
  const n = changes.reduce((acc, c) => acc + c.edits.length, 0);
6622
6948
  for (const c of changes) for (const e of c.edits) print(`${c.file}:${e.line}:${e.column} ${e.from} -> ${e.to}`);
6623
6949
  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.");
6624
6950
  return 0;
6625
6951
  }
6626
- const baselinePath = flag4(a, "baseline");
6952
+ const baselinePath = flag5(a, "baseline");
6627
6953
  let baseline;
6628
6954
  if (baselinePath) {
6629
6955
  if (!existsSync6(baselinePath)) throw new UsageError(`Baseline not found: ${baselinePath}`);
6630
6956
  baseline = JSON.parse(await readFile7(baselinePath, "utf8"));
6631
6957
  }
6632
- const label = flag4(a, "label");
6633
- const allowlistPath = flag4(a, "allowlist");
6958
+ const label = flag5(a, "label");
6959
+ const allowlistPath = flag5(a, "allowlist");
6634
6960
  if (allowlistPath && !existsSync6(allowlistPath)) throw new UsageError(`Allowlist not found: ${allowlistPath}`);
6635
6961
  let allowlist;
6636
6962
  try {
@@ -6640,8 +6966,8 @@ async function audit(a, root) {
6640
6966
  }
6641
6967
  const report = runAudit(target, { ds: dsRoot, version: VERSION, ...label ? { label } : {}, ...baseline ? { baseline } : {}, ...allowlist ? { allowlist } : {} });
6642
6968
  const md = auditMarkdown(report);
6643
- const jsonOut = flag4(a, "json");
6644
- const mdOut = flag4(a, "md");
6969
+ const jsonOut = flag5(a, "json");
6970
+ const mdOut = flag5(a, "md");
6645
6971
  if (jsonOut && jsonOut !== "true") await writeFile5(jsonOut, `${JSON.stringify(report, null, 2)}
6646
6972
  `);
6647
6973
  if (mdOut && mdOut !== "true") await writeFile5(mdOut, `${md}
@@ -6681,7 +7007,7 @@ async function audit(a, root) {
6681
7007
  for (const f of report.files) for (const e of f.ignoreErrors ?? []) process.stderr.write(`${report.target.path}/${f.path}:${e.line}: ${e.message}
6682
7008
  `);
6683
7009
  let code = 0;
6684
- const failUnder = flag4(a, "fail-under");
7010
+ const failUnder = flag5(a, "fail-under");
6685
7011
  if (failUnder !== void 0) {
6686
7012
  const min = Number(failUnder);
6687
7013
  if (!Number.isFinite(min)) throw new UsageError("--fail-under needs a number, e.g. --fail-under 80");
@@ -6692,7 +7018,7 @@ async function audit(a, root) {
6692
7018
  code = 1;
6693
7019
  }
6694
7020
  }
6695
- if (has6(a, "no-regress") && report.baseline) {
7021
+ if (has7(a, "no-regress") && report.baseline) {
6696
7022
  const b = report.baseline;
6697
7023
  if ((b.tokenAdoption.delta ?? 0) < 0 || (b.componentAdoption.delta ?? 0) < 0 || b.hardcoded.delta > 0) {
6698
7024
  process.stderr.write("bridge audit: adoption regressed since the baseline\n");
@@ -6704,9 +7030,9 @@ async function audit(a, root) {
6704
7030
  async function docsCmd(a, root) {
6705
7031
  const [, sub, name] = a._;
6706
7032
  if (sub !== "draft" || !name) throw new UsageError("Usage: bridge docs draft <Component> [--json]");
6707
- const ctx = await BridgeContext7.create({ root });
7033
+ const ctx = await BridgeContext8.create({ root });
6708
7034
  const d = await ops.draftDocsOp(ctx, { name });
6709
- if (has6(a, "json")) {
7035
+ if (has7(a, "json")) {
6710
7036
  print(JSON.stringify({ component: d.component, path: d.path, exists: d.exists, prefill: d.prefill, prompt: d.prompt }, null, 2));
6711
7037
  return 0;
6712
7038
  }
@@ -6737,17 +7063,20 @@ async function main() {
6737
7063
  const argv = process.argv.slice(2);
6738
7064
  const a = parse(argv);
6739
7065
  const cmd = a._[0];
6740
- const root = resolve5(flag4(a, "root") ?? process.cwd());
7066
+ const root = resolve5(flag5(a, "root") ?? process.cwd());
6741
7067
  if (cmd === "request") return runRequest(a, root);
6742
7068
  if (cmd === "explore") return runExplore(a, root);
6743
7069
  if (cmd === "branch") return runBranch(a, root);
6744
7070
  if (cmd === "pieces") return runPieces(a, root);
6745
7071
  if (cmd === "figma") return runFigma(a, root);
7072
+ if (cmd === "suggestions" || cmd === "suggest") return runSuggestions(a, root);
7073
+ if (cmd === "apply") return runApply(a, root);
7074
+ if (cmd === "review") return runReview(a, root);
6746
7075
  if (cmd === "products" || cmd === "settings" || cmd === "coverage") return runStudioRead(cmd, a, root);
6747
7076
  if (cmd === "docs" && a._[1] === "save") return runDocsSave(a, root);
6748
- if (!cmd || has6(a, "help") || cmd === "help") {
7077
+ if (!cmd || has7(a, "help") || cmd === "help") {
6749
7078
  print(HELP);
6750
- return cmd || has6(a, "help") ? 0 : 2;
7079
+ return cmd || has7(a, "help") ? 0 : 2;
6751
7080
  }
6752
7081
  if (cmd === "--version" || cmd === "version") return print(VERSION), 0;
6753
7082
  switch (cmd) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge-ds/cli",
3
- "version": "0.7.0",
3
+ "version": "0.9.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.6.0",
47
- "@bridge-ds/visual": "^0.3.4",
46
+ "@bridge-ds/mcp": "^0.8.0",
47
+ "@bridge-ds/visual": "^0.3.7",
48
48
  "typescript": "5.9.3"
49
49
  },
50
50
  "devDependencies": {