@bridge-ds/cli 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +493 -100
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { existsSync as existsSync6 } from "node:fs";
5
- import { readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
6
- import { isAbsolute, join as join6, relative as relative4, resolve as resolve3 } from "node:path";
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;
@@ -2182,10 +2182,10 @@ function assertSafeBranch(branch, rules = {}) {
2182
2182
 
2183
2183
  // ../core/src/node/git-local.ts
2184
2184
  function git(root, args, opts = {}) {
2185
- return new Promise((resolve4, reject) => {
2185
+ return new Promise((resolve5, reject) => {
2186
2186
  const child = execFile("git", args, { cwd: root, env: { ...process.env, ...opts.env }, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
2187
2187
  if (err) reject(new Error(`git ${args.join(" ")}: ${stderr || err.message}`.trim()));
2188
- else resolve4(stdout);
2188
+ else resolve5(stdout);
2189
2189
  });
2190
2190
  if (opts.input !== void 0) child.stdin.end(opts.input);
2191
2191
  });
@@ -2432,6 +2432,47 @@ function visualImpactProblems(raw) {
2432
2432
  return out;
2433
2433
  }
2434
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
+
2435
2476
  // ../core/src/config.ts
2436
2477
  var CONFIG_FILE = "bridge.config.json";
2437
2478
  var DEFAULT_OVERRIDE = { allowed: [], requireReason: true, minReasonLength: 20 };
@@ -2572,6 +2613,7 @@ function defineConfig(input) {
2572
2613
  } : {},
2573
2614
  ...g.rights ? { rights: normalizeRights(g.rights) } : {}
2574
2615
  },
2616
+ products: input.products?.length ? input.products.map((p) => ({ ...p })) : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
2575
2617
  ...input.audit ? { audit: { ...input.audit } } : {},
2576
2618
  ...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
2577
2619
  ...input.integrations ? { integrations: { ...input.integrations } } : {},
@@ -2694,6 +2736,7 @@ function loadConfig(source) {
2694
2736
  }
2695
2737
  }
2696
2738
  problems.push(...visualImpactProblems(d.visualImpact));
2739
+ problems.push(...productProblems(d.products));
2697
2740
  const audit2 = d.audit;
2698
2741
  if (audit2 !== void 0) {
2699
2742
  if (!isObj(audit2)) problems.push('"audit" must be an object');
@@ -3624,7 +3667,7 @@ var GitHubClient = class _GitHubClient {
3624
3667
  }
3625
3668
  async getPullRequest(n) {
3626
3669
  const r = await this.request("GET", `${this.repoPath}/pulls/${n}`);
3627
- 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) };
3628
3671
  }
3629
3672
  async listPullRequestFiles(n) {
3630
3673
  const r = await this.request("GET", `${this.repoPath}/pulls/${n}/files?per_page=100`);
@@ -3701,6 +3744,100 @@ var GitHubClient = class _GitHubClient {
3701
3744
  async removeLabel(issueNumber2, label) {
3702
3745
  await this.request("DELETE", `${this.repoPath}/issues/${issueNumber2}/labels/${encodeURIComponent(label)}`);
3703
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
+ }
3704
3841
  };
3705
3842
  function toCommitSummary(c) {
3706
3843
  return {
@@ -4162,15 +4299,15 @@ async function shoot(page, r) {
4162
4299
  }
4163
4300
  async function switchStory(page, storyId2) {
4164
4301
  const ok = await page.evaluate(
4165
- (id) => new Promise((resolve4) => {
4302
+ (id) => new Promise((resolve5) => {
4166
4303
  const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
4167
- if (!ch) return resolve4(false);
4304
+ if (!ch) return resolve5(false);
4168
4305
  const events = ["storyRendered", "storyErrored", "storyThrewException", "storyMissing", "playFunctionThrewException"];
4169
4306
  const timer = setTimeout(() => done(), 15e3);
4170
4307
  function done() {
4171
4308
  clearTimeout(timer);
4172
4309
  for (const e of events) ch.off(e, done);
4173
- resolve4(true);
4310
+ resolve5(true);
4174
4311
  }
4175
4312
  for (const e of events) ch.on(e, done);
4176
4313
  window.scrollTo(0, 0);
@@ -4583,12 +4720,12 @@ function serveDir(dir) {
4583
4720
  res.writeHead(200, { "content-type": TYPES[extname(file)] ?? "application/octet-stream", "cache-control": "no-store" });
4584
4721
  createReadStream(file).pipe(res);
4585
4722
  });
4586
- return new Promise((resolve4, reject) => {
4723
+ return new Promise((resolve5, reject) => {
4587
4724
  server.once("error", reject);
4588
4725
  server.listen(0, "127.0.0.1", () => {
4589
4726
  const addr = server.address();
4590
4727
  const port = typeof addr === "object" && addr ? addr.port : 0;
4591
- 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())) });
4592
4729
  });
4593
4730
  });
4594
4731
  }
@@ -4660,7 +4797,7 @@ async function removeWorktree(repo, dir) {
4660
4797
  await git(repo, ["worktree", "prune"]).catch(() => void 0);
4661
4798
  }
4662
4799
  function run(cmd, args, cwd, log, root = cwd, env = {}) {
4663
- return new Promise((resolve4, reject) => {
4800
+ return new Promise((resolve5, reject) => {
4664
4801
  const bin = [.../* @__PURE__ */ new Set([join3(cwd, "node_modules", ".bin"), join3(root, "node_modules", ".bin")])].join(":");
4665
4802
  const child = spawn(cmd, args, {
4666
4803
  cwd,
@@ -4673,7 +4810,7 @@ function run(cmd, args, cwd, log, root = cwd, env = {}) {
4673
4810
  child.stderr.on("data", keep);
4674
4811
  child.on("error", reject);
4675
4812
  child.on("close", (code) => {
4676
- if (code === 0) resolve4();
4813
+ if (code === 0) resolve5();
4677
4814
  else {
4678
4815
  log(tail);
4679
4816
  reject(new Error(`${cmd} ${args.join(" ")} failed in ${cwd} (exit ${code})`));
@@ -5084,12 +5221,12 @@ async function captureFigmaReferences(o) {
5084
5221
  }
5085
5222
  const plan = JSON.stringify(j.plan.render);
5086
5223
  await page.evaluate(
5087
- ({ id, plan: plan2 }) => new Promise((resolve4) => {
5224
+ ({ id, plan: plan2 }) => new Promise((resolve5) => {
5088
5225
  const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
5089
5226
  const done = () => {
5090
5227
  clearTimeout(timer);
5091
5228
  ch.off("storyRendered", done);
5092
- resolve4();
5229
+ resolve5();
5093
5230
  };
5094
5231
  const timer = setTimeout(done, 1e4);
5095
5232
  ch.on("storyRendered", done);
@@ -5233,7 +5370,7 @@ Usage:
5233
5370
  --branch: implement it on that exploration, never in a pull request to main
5234
5371
  bridge request create --component Button --kind new-variant --title "Add a danger ghost variant"
5235
5372
  --summary "..." [--change field=before:after ...] [--figma url]
5236
- [--exploration explore/<name>] [--dry-run] [--json]
5373
+ [--exploration explore/<name>] [--product <key>] [--dry-run] [--json]
5237
5374
  bridge request jira-sync
5238
5375
  GitHub Action step: mirror a request issue to Jira (see docs/REQUESTS.md)
5239
5376
 
@@ -5388,6 +5525,7 @@ Claude Code is not installed (no \`claude\` on PATH). Install it (npm install -g
5388
5525
  changes: (a.flags.change ?? []).map(parseChange),
5389
5526
  ...figma ? { figmaUrl: figma } : {},
5390
5527
  ...exploration ? { exploration } : {},
5528
+ ...flag(a, "product") ? { product: flag(a, "product") } : {},
5391
5529
  source: "cli",
5392
5530
  dryRun
5393
5531
  });
@@ -5433,7 +5571,7 @@ staging branch without touching main (an explore/<name> branch with a draft pull
5433
5571
 
5434
5572
  Usage:
5435
5573
  bridge explore start <name> --purpose "..." [--use-case ${EXPLORATION_USE_CASES.map((u) => u.value).join("|")}]
5436
- [--experimental "..." ...] [--dry-run] [--json]
5574
+ [--experimental "..." ...] [--product <key>] [--dry-run] [--json]
5437
5575
  GitHub token: the branch and the draft pull request on GitHub. Without one, in a git repo:
5438
5576
  a local branch only, nothing pushed
5439
5577
  bridge explore list [--all] [--json]
@@ -5471,7 +5609,7 @@ async function runExplore(a, root, io = defaultIO()) {
5471
5609
  if (!name || !purpose) throw new RequestUsageError('Usage: bridge explore start <name> --purpose "What you want to learn"');
5472
5610
  const useCase = flag2(a, "use-case");
5473
5611
  const ctx = await context2(root, io);
5474
- 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") });
5475
5613
  if (json) return io.print(JSON.stringify(r, null, 2)), 0;
5476
5614
  if (r.status === "dry-run") {
5477
5615
  io.print(`Dry run, nothing written.
@@ -5541,6 +5679,231 @@ ${EXPLORE_HELP}`);
5541
5679
  }
5542
5680
  }
5543
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
+
5544
5907
  // src/protection.ts
5545
5908
  var UNKNOWN = { kind: "unknown", missing: ["review", "checks", "history", "deletion"] };
5546
5909
  async function protectionReport(opts) {
@@ -5573,7 +5936,7 @@ async function rolesReport(opts) {
5573
5936
  }
5574
5937
 
5575
5938
  // src/index.ts
5576
- 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";
5577
5940
  var MCP_PACKAGE = "@bridge-ds/mcp";
5578
5941
  var HELP = `bridge ${VERSION}: design system as code, for you and your Claude
5579
5942
 
@@ -5588,9 +5951,13 @@ Usage:
5588
5951
  bridge tokens <query> [--json] search tokens by name, description or value
5589
5952
  bridge diff <base> [--json|--md] designer-readable token diff: working tree vs <base> (git ref)
5590
5953
  bridge propose --title "..." --set path=value [--set ...] [--file f | --mode theme=dark]
5591
- [--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]
5592
5956
  validate + contrast + new local bridge/* branch (never pushes).
5593
- --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
5594
5961
  bridge brand list [--json] brands: slug, name, default, primary action color, theme overrides
5595
5962
  bridge brand create <slug> --from bridge --primary <hex|hue> --neutral <hex|hue>
5596
5963
  [--name "Acme"] [--accent x] [--link x] [--radius soft|balanced|crisp|i,c,co,o]
@@ -5646,6 +6013,16 @@ Usage:
5646
6013
  explorations: an explore/<name> branch with a draft pull request
5647
6014
  labeled exploration, previews and a snapshot package for a product
5648
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)
5649
6026
  bridge mcp [--root dir | --repo owner/name] start the MCP server on stdio
5650
6027
 
5651
6028
  Options:
@@ -5662,23 +6039,23 @@ function parse(argv) {
5662
6039
  const [k, inline] = a.slice(2).split(/=(.*)/s, 2);
5663
6040
  const next = argv[i + 1];
5664
6041
  const fileArg = k === "json" && /\.json$/i.test(next ?? "") || k === "md" && /\.(md|markdown)$/i.test(next ?? "");
5665
- 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));
5666
6043
  (out.flags[k] ??= []).push(inline ?? (takesValue ? argv[++i] : "true"));
5667
6044
  } else out._.push(a);
5668
6045
  }
5669
6046
  return out;
5670
6047
  }
5671
- var flag3 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
5672
- 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]);
5673
6050
  var print = (s) => process.stdout.write(`${s}
5674
6051
  `);
5675
6052
  var modeArg = (s) => s ? Object.fromEntries(s.split(",").map((kv) => kv.split("="))) : void 0;
5676
6053
  async function init(a, root) {
5677
6054
  const cfgPath = join6(root, CONFIG_FILE2);
5678
6055
  let config;
5679
- if (existsSync6(cfgPath) && !has3(a, "force")) {
6056
+ if (existsSync6(cfgPath) && !has4(a, "force")) {
5680
6057
  print(`${CONFIG_FILE2} exists, keeping it (--force to regenerate).`);
5681
- config = (await (await BridgeContext4.create({ root })).config()).config;
6058
+ config = (await (await BridgeContext5.create({ root })).config()).config;
5682
6059
  } else {
5683
6060
  const src = new LocalSource(root);
5684
6061
  const files = await src.list("");
@@ -5694,7 +6071,7 @@ async function init(a, root) {
5694
6071
  const inRepo = !relServer.startsWith("..") && !isAbsolute(relServer);
5695
6072
  const server = (dir) => inRepo ? { command: "node", args: [relServer, "--root", dir] } : { command: "npx", args: ["-y", MCP_PACKAGE, "--root", dir] };
5696
6073
  const mcpPath = join6(root, ".mcp.json");
5697
- const existing = existsSync6(mcpPath) ? JSON.parse(await readFile6(mcpPath, "utf8")) : {};
6074
+ const existing = existsSync6(mcpPath) ? JSON.parse(await readFile7(mcpPath, "utf8")) : {};
5698
6075
  existing.mcpServers = { ...existing.mcpServers ?? {}, bridge: server(".") };
5699
6076
  await writeFile5(mcpPath, `${JSON.stringify(existing, null, 2)}
5700
6077
  `);
@@ -5706,14 +6083,14 @@ async function init(a, root) {
5706
6083
  print(` claude mcp add bridge -- ${desktop.command} ${desktop.args.join(" ")}`);
5707
6084
  }
5708
6085
  async function check(a, root) {
5709
- const ctx = await BridgeContext4.create({ root });
6086
+ const ctx = await BridgeContext5.create({ root });
5710
6087
  const v = await ops.validateTokens(ctx, { includeWarnings: true });
5711
6088
  const c = await ops.checkContrast(ctx, {});
5712
6089
  const warnings = "warnings" in v ? v.warnings ?? [] : [];
5713
- 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;
5714
6091
  const protection = await protectionOf(a, root, ctx);
5715
6092
  const roles = await rolesOf(a, root, ctx);
5716
- if (has3(a, "json")) {
6093
+ if (has4(a, "json")) {
5717
6094
  print(JSON.stringify({ ok: !failed, validation: v, contrast: c, ...protection ? { protection: protection.state } : {}, ...roles?.teams.length ? { teams: roles.teams } : {} }, null, 2));
5718
6095
  return failed ? 1 : 0;
5719
6096
  }
@@ -5732,7 +6109,7 @@ async function check(a, root) {
5732
6109
  return failed ? 1 : 0;
5733
6110
  }
5734
6111
  async function protectionOf(a, root, ctx) {
5735
- 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;
5736
6113
  const { config } = await ctx.config();
5737
6114
  const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
5738
6115
  if (!repo) return void 0;
@@ -5740,7 +6117,7 @@ async function protectionOf(a, root, ctx) {
5740
6117
  return protectionReport({ repo, defaultBranch: config.repo?.defaultBranch ?? "main", token });
5741
6118
  }
5742
6119
  async function rolesOf(a, root, ctx) {
5743
- 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;
5744
6121
  const { config } = await ctx.config();
5745
6122
  const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
5746
6123
  if (!repo) return void 0;
@@ -5750,9 +6127,9 @@ async function rolesOf(a, root, ctx) {
5750
6127
  async function tokens(a, root) {
5751
6128
  const q = a._.slice(1).join(" ");
5752
6129
  if (!q) throw new UsageError("bridge tokens <query>");
5753
- const ctx = await BridgeContext4.create({ root });
5754
- const r = await ops.searchTokens(ctx, { query: q, limit: Number(flag3(a, "limit") ?? 20) });
5755
- 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));
5756
6133
  if (!r.results.length) return print(`No token matches "${q}".`);
5757
6134
  const w = Math.max(...r.results.map((x) => x.path.length));
5758
6135
  for (const t of r.results) print(`${t.path.padEnd(w)} ${String(t.value).padEnd(10)} ${(t.tier ?? "").padEnd(9)} ${t.description ?? ""}`);
@@ -5760,20 +6137,20 @@ async function tokens(a, root) {
5760
6137
  async function diff(a, root) {
5761
6138
  const base = a._[1];
5762
6139
  if (!base) throw new UsageError("bridge diff <base> (a git ref: main, HEAD~1, a sha)");
5763
- const ctx = await BridgeContext4.create({ root });
6140
+ const ctx = await BridgeContext5.create({ root });
5764
6141
  const { config } = await ctx.config();
5765
6142
  const before = await core.loadTokenSet(new GitRefSource(root, base), config);
5766
6143
  const after = await ctx.tokens();
5767
6144
  const changes = core.diff(before, after);
5768
- const pairs = await core.readContrastPairs(ctx.source, config).catch(() => []);
5769
- 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)) : [];
5770
6147
  const tier = core.classify(changes, [], config);
5771
6148
  const imp = core.impact(before, after, changes, 30);
5772
- if (has3(a, "json")) {
6149
+ if (has4(a, "json")) {
5773
6150
  print(JSON.stringify({ base, changes, contrast: delta2, tier, impact: imp }, null, 2));
5774
6151
  return 0;
5775
6152
  }
5776
- if (has3(a, "md")) {
6153
+ if (has4(a, "md")) {
5777
6154
  print(`## Token changes since \`${base}\`
5778
6155
 
5779
6156
  ${core.tokenTable(changes)}
@@ -5808,24 +6185,37 @@ ${imp.total} other token(s) change value through aliases:`);
5808
6185
  return delta2.some((d) => d.passBefore && !d.passAfter) ? 1 : 0;
5809
6186
  }
5810
6187
  async function propose(a, root) {
5811
- const title = flag3(a, "title");
6188
+ const title = flag4(a, "title");
5812
6189
  if (!title) throw new UsageError('bridge propose --title "Darken muted text" --set color.text.muted={palette.neutral.700} --mode theme=light');
5813
6190
  const edits = [];
5814
- const file = flag3(a, "file");
5815
- const mode2 = modeArg(flag3(a, "mode"));
6191
+ const file = flag4(a, "file");
6192
+ const mode2 = modeArg(flag4(a, "mode"));
5816
6193
  for (const s of a.flags.set ?? []) {
5817
6194
  const i = s.indexOf("=");
5818
6195
  if (i < 0) throw new UsageError(`--set needs path=value, got "${s}"`);
5819
6196
  edits.push({ op: "set", path: s.slice(0, i), value: s.slice(i + 1), ...file ? { file } : {}, ...mode2 ? { mode: mode2 } : {} });
5820
6197
  }
5821
- const editsFile = flag3(a, "edits");
5822
- if (editsFile) edits.push(...JSON.parse(await readFile6(resolve3(editsFile), "utf8")));
6198
+ const editsFile = flag4(a, "edits");
6199
+ if (editsFile) edits.push(...JSON.parse(await readFile7(resolve4(editsFile), "utf8")));
5823
6200
  if (!edits.length) throw new UsageError("Give at least one --set path=value or --edits file.json");
5824
- const ctx = await BridgeContext4.create({ root });
5825
- const summary = flag3(a, "summary");
5826
- const branch = flag3(a, "branch");
5827
- const r = await ops.proposeTokenChange(ctx, { title, edits, ...summary ? { summary } : {}, ...branch ? { branch } : {}, dryRun: has3(a, "dry-run") });
5828
- 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));
5829
6219
  else {
5830
6220
  print(`status: ${r.status}${r.why ? ` (${r.why})` : ""}`);
5831
6221
  if (r.tier) print(`tier ${r.tier}, ${r.bump}`);
@@ -5840,17 +6230,17 @@ ${r.prBody}`);
5840
6230
  }
5841
6231
  async function brandCmd(a, root) {
5842
6232
  const sub = a._[1];
5843
- const ctx = await BridgeContext4.create({ root });
6233
+ const ctx = await BridgeContext5.create({ root });
5844
6234
  if (sub === "list") {
5845
6235
  const r2 = await brand.listBrands(ctx);
5846
- if (has3(a, "json")) print(JSON.stringify(r2.brands, null, 2));
6236
+ if (has4(a, "json")) print(JSON.stringify(r2.brands, null, 2));
5847
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(", ")}` : ""}`);
5848
6238
  return 0;
5849
6239
  }
5850
6240
  if (sub !== "create") throw new UsageError('bridge brand list | bridge brand create <slug> --from bridge --primary "#1F5F3A" --neutral "#2A2F2C"');
5851
6241
  const slug = a._[2];
5852
6242
  if (!slug) throw new UsageError('bridge brand create <slug> --from bridge --primary "#1F5F3A" --neutral "#2A2F2C"');
5853
- const opt = (k) => flag3(a, k);
6243
+ const opt = (k) => flag4(a, k);
5854
6244
  const fonts = { ...opt("heading") ? { heading: opt("heading") } : {}, ...opt("text") ? { text: opt("text") } : {} };
5855
6245
  const r = await brand.proposeBrand(ctx, {
5856
6246
  slug,
@@ -5863,9 +6253,9 @@ async function brandCmd(a, root) {
5863
6253
  ...opt("radius") ? { radius: opt("radius") } : {},
5864
6254
  ...Object.keys(fonts).length ? { fonts } : {},
5865
6255
  ...opt("notes") ? { notes: opt("notes") } : {},
5866
- dryRun: has3(a, "dry-run")
6256
+ dryRun: has4(a, "dry-run")
5867
6257
  });
5868
- if (has3(a, "json")) {
6258
+ if (has4(a, "json")) {
5869
6259
  const { fileContents: _f, ...rest } = r;
5870
6260
  print(JSON.stringify(rest, null, 2));
5871
6261
  return r.status === "blocked" ? 1 : 0;
@@ -5886,7 +6276,7 @@ async function brandCmd(a, root) {
5886
6276
  return r.status === "blocked" ? 1 : 0;
5887
6277
  }
5888
6278
  async function figmaReferences(a, root) {
5889
- let sha = flag3(a, "sha");
6279
+ let sha = flag4(a, "sha");
5890
6280
  if (!sha) {
5891
6281
  try {
5892
6282
  sha = execFileSync2("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
@@ -5894,49 +6284,49 @@ async function figmaReferences(a, root) {
5894
6284
  sha = "unknown";
5895
6285
  }
5896
6286
  }
5897
- const only = (flag3(a, "only") ?? "").split(",").filter(Boolean);
6287
+ const only = (flag4(a, "only") ?? "").split(",").filter(Boolean);
5898
6288
  const r = await captureFigmaReferences({
5899
- storybook: resolve3(root, flag3(a, "storybook") ?? "storybook-static"),
5900
- specs: resolve3(root, flag3(a, "specs") ?? "packages/react/component-specs.json"),
5901
- out: resolve3(root, flag3(a, "out") ?? "figma-references"),
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"),
5902
6292
  sha,
5903
6293
  ...only.length ? { only } : {},
5904
- log: has3(a, "json") ? void 0 : (line) => print(line)
6294
+ log: has4(a, "json") ? void 0 : (line) => print(line)
5905
6295
  });
5906
- if (has3(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
6296
+ if (has4(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
5907
6297
  return 0;
5908
6298
  }
5909
6299
  async function impact(a, root) {
5910
- const only = (flag3(a, "only") ?? "").split(",").filter(Boolean);
6300
+ const only = (flag4(a, "only") ?? "").split(",").filter(Boolean);
5911
6301
  for (const x of only) if (x !== "stories" && x !== "app") throw new UsageError(`--only takes stories or app, got "${x}"`);
5912
- 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;
5913
6303
  const threshold = num2("threshold");
5914
6304
  if (threshold !== void 0 && !(threshold >= 0 && threshold <= 1)) throw new UsageError("--threshold is between 0 and 1 (pixelmatch color distance)");
5915
- const imageBase = flag3(a, "image-base");
5916
- const artifactUrl = flag3(a, "artifact-url");
5917
- const out = flag3(a, "out");
5918
- 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");
5919
6309
  const maxStories = num2("max-stories");
5920
6310
  const r = await runImpact({
5921
6311
  root,
5922
- base: flag3(a, "base") ?? "main",
5923
- head: flag3(a, "head") ?? "HEAD",
5924
- ...out ? { out: resolve3(out) } : {},
6312
+ base: flag4(a, "base") ?? "main",
6313
+ head: flag4(a, "head") ?? "HEAD",
6314
+ ...out ? { out: resolve4(out) } : {},
5925
6315
  ...only.length ? { only } : {},
5926
6316
  ...threshold !== void 0 ? { threshold } : {},
5927
- ...cacheDir ? { cacheDir: resolve3(cacheDir) } : {},
6317
+ ...cacheDir ? { cacheDir: resolve4(cacheDir) } : {},
5928
6318
  ...maxStories ? { maxStoriesPerComponent: maxStories } : {},
5929
6319
  ...imageBase !== void 0 ? { imageBase } : {},
5930
6320
  ...artifactUrl ? { artifactUrl } : {},
5931
- plan: has3(a, "plan"),
5932
- keepUnchanged: has3(a, "keep-unchanged")
6321
+ plan: has4(a, "plan"),
6322
+ keepUnchanged: has4(a, "keep-unchanged")
5933
6323
  });
5934
- if (has3(a, "json")) {
6324
+ if (has4(a, "json")) {
5935
6325
  print(JSON.stringify({ summary: r.summary, manifest: r.manifestPath, comment: r.commentPath ?? null, ...r.manifest.summary, selection: r.manifest.selection }, null, 2));
5936
6326
  return 0;
5937
6327
  }
5938
6328
  const sel = r.manifest.selection;
5939
- if (has3(a, "plan")) {
6329
+ if (has4(a, "plan")) {
5940
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)})`);
5941
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(", ")})` : ""}`);
5942
6332
  print(` components: ${sel.components.map((c) => c.id).join(", ") || "none"}`);
@@ -5952,12 +6342,12 @@ async function impact(a, root) {
5952
6342
  }
5953
6343
  var day = (iso) => iso.slice(0, 10);
5954
6344
  async function historyCmd(a, root) {
5955
- const ctx = await BridgeContext4.create({ root });
6345
+ const ctx = await BridgeContext5.create({ root });
5956
6346
  const token = a._[1];
5957
- const limit = Number(flag3(a, "limit") ?? 20);
6347
+ const limit = Number(flag4(a, "limit") ?? 20);
5958
6348
  if (token) {
5959
6349
  const r2 = await hist.getTokenHistory(ctx, { path: token, limit });
5960
- 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;
5961
6351
  if (!r2.total) return print(`No recorded change of ${r2.path} in ${r2.source}.`), 0;
5962
6352
  print(`${r2.path}: ${r2.total} change(s) (${r2.source})
5963
6353
  `);
@@ -5968,9 +6358,9 @@ async function historyCmd(a, root) {
5968
6358
  }
5969
6359
  return 0;
5970
6360
  }
5971
- const tier = flag3(a, "tier");
5972
- 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 } : {} });
5973
- 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;
5974
6364
  print(`${r.source}${r.currentVersion ? `, package version ${r.currentVersion}` : ""}`);
5975
6365
  if (!r.total) return print(r.note ?? "No token change matches."), 0;
5976
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" : ""}`);
@@ -5988,10 +6378,10 @@ ${r.truncated}`);
5988
6378
  async function revertCmd(a, root) {
5989
6379
  const target = a._[1];
5990
6380
  if (!target) throw new UsageError("bridge revert <pr|commit|id> [--dry-run] (ids from `bridge history`)");
5991
- const ctx = await BridgeContext4.create({ root });
6381
+ const ctx = await BridgeContext5.create({ root });
5992
6382
  const resolutions = Object.fromEntries((a.flags["use-before"] ?? []).map((k) => [k, "use-before"]));
5993
- const r = await hist.proposeRevert(ctx, { target, resolutions, restoreConflicts: has3(a, "restore-conflicts"), dryRun: has3(a, "dry-run") });
5994
- 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")) {
5995
6385
  print(JSON.stringify(r, null, 2));
5996
6386
  return r.status === "blocked" ? 1 : 0;
5997
6387
  }
@@ -6016,25 +6406,25 @@ async function audit(a, root) {
6016
6406
  const target = a._[1];
6017
6407
  if (!target) throw new UsageError("bridge audit <path> (a product folder or file that uses the design system)");
6018
6408
  if (!existsSync6(target)) throw new UsageError(`No such file or folder: ${target}`);
6019
- const dsRoot = flag3(a, "ds") ? resolve3(flag3(a, "ds")) : existsSync6(join6(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
6409
+ const dsRoot = flag4(a, "ds") ? resolve4(flag4(a, "ds")) : existsSync6(join6(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
6020
6410
  if (!dsRoot) throw new UsageError(`No ${CONFIG_FILE2} found: run bridge audit from the design system repo or pass --ds <dir>.`);
6021
6411
  const ds = loadDesignSystem(dsRoot);
6022
- if (has3(a, "fix")) {
6023
- const write = has3(a, "write");
6412
+ if (has4(a, "fix")) {
6413
+ const write = has4(a, "write");
6024
6414
  const changes = fixCss(target, ds.index, write);
6025
6415
  const n = changes.reduce((acc, c) => acc + c.edits.length, 0);
6026
6416
  for (const c of changes) for (const e of c.edits) print(`${c.file}:${e.line}:${e.column} ${e.from} -> ${e.to}`);
6027
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.");
6028
6418
  return 0;
6029
6419
  }
6030
- const baselinePath = flag3(a, "baseline");
6420
+ const baselinePath = flag4(a, "baseline");
6031
6421
  let baseline;
6032
6422
  if (baselinePath) {
6033
6423
  if (!existsSync6(baselinePath)) throw new UsageError(`Baseline not found: ${baselinePath}`);
6034
- baseline = JSON.parse(await readFile6(baselinePath, "utf8"));
6424
+ baseline = JSON.parse(await readFile7(baselinePath, "utf8"));
6035
6425
  }
6036
- const label = flag3(a, "label");
6037
- const allowlistPath = flag3(a, "allowlist");
6426
+ const label = flag4(a, "label");
6427
+ const allowlistPath = flag4(a, "allowlist");
6038
6428
  if (allowlistPath && !existsSync6(allowlistPath)) throw new UsageError(`Allowlist not found: ${allowlistPath}`);
6039
6429
  let allowlist;
6040
6430
  try {
@@ -6044,8 +6434,8 @@ async function audit(a, root) {
6044
6434
  }
6045
6435
  const report = runAudit(target, { ds: dsRoot, version: VERSION, ...label ? { label } : {}, ...baseline ? { baseline } : {}, ...allowlist ? { allowlist } : {} });
6046
6436
  const md = auditMarkdown(report);
6047
- const jsonOut = flag3(a, "json");
6048
- const mdOut = flag3(a, "md");
6437
+ const jsonOut = flag4(a, "json");
6438
+ const mdOut = flag4(a, "md");
6049
6439
  if (jsonOut && jsonOut !== "true") await writeFile5(jsonOut, `${JSON.stringify(report, null, 2)}
6050
6440
  `);
6051
6441
  if (mdOut && mdOut !== "true") await writeFile5(mdOut, `${md}
@@ -6085,7 +6475,7 @@ async function audit(a, root) {
6085
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}
6086
6476
  `);
6087
6477
  let code = 0;
6088
- const failUnder = flag3(a, "fail-under");
6478
+ const failUnder = flag4(a, "fail-under");
6089
6479
  if (failUnder !== void 0) {
6090
6480
  const min = Number(failUnder);
6091
6481
  if (!Number.isFinite(min)) throw new UsageError("--fail-under needs a number, e.g. --fail-under 80");
@@ -6096,7 +6486,7 @@ async function audit(a, root) {
6096
6486
  code = 1;
6097
6487
  }
6098
6488
  }
6099
- if (has3(a, "no-regress") && report.baseline) {
6489
+ if (has4(a, "no-regress") && report.baseline) {
6100
6490
  const b = report.baseline;
6101
6491
  if ((b.tokenAdoption.delta ?? 0) < 0 || (b.componentAdoption.delta ?? 0) < 0 || b.hardcoded.delta > 0) {
6102
6492
  process.stderr.write("bridge audit: adoption regressed since the baseline\n");
@@ -6108,9 +6498,9 @@ async function audit(a, root) {
6108
6498
  async function docsCmd(a, root) {
6109
6499
  const [, sub, name] = a._;
6110
6500
  if (sub !== "draft" || !name) throw new UsageError("Usage: bridge docs draft <Component> [--json]");
6111
- const ctx = await BridgeContext4.create({ root });
6501
+ const ctx = await BridgeContext5.create({ root });
6112
6502
  const d = await ops.draftDocsOp(ctx, { name });
6113
- if (has3(a, "json")) {
6503
+ if (has4(a, "json")) {
6114
6504
  print(JSON.stringify({ component: d.component, path: d.path, exists: d.exists, prefill: d.prefill, prompt: d.prompt }, null, 2));
6115
6505
  return 0;
6116
6506
  }
@@ -6141,12 +6531,15 @@ async function main() {
6141
6531
  const argv = process.argv.slice(2);
6142
6532
  const a = parse(argv);
6143
6533
  const cmd = a._[0];
6144
- const root = resolve3(flag3(a, "root") ?? process.cwd());
6534
+ const root = resolve4(flag4(a, "root") ?? process.cwd());
6145
6535
  if (cmd === "request") return runRequest(a, root);
6146
6536
  if (cmd === "explore") return runExplore(a, root);
6147
- 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") {
6148
6541
  print(HELP);
6149
- return cmd || has3(a, "help") ? 0 : 2;
6542
+ return cmd || has4(a, "help") ? 0 : 2;
6150
6543
  }
6151
6544
  if (cmd === "--version" || cmd === "version") return print(VERSION), 0;
6152
6545
  switch (cmd) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge-ds/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.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.3.3",
47
- "@bridge-ds/visual": "^0.3.0",
46
+ "@bridge-ds/mcp": "^0.4.0",
47
+ "@bridge-ds/visual": "^0.3.1",
48
48
  "typescript": "5.9.3"
49
49
  },
50
50
  "devDependencies": {