@bridge-ds/cli 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +719 -144
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/index.ts
4
4
  import { existsSync as existsSync5 } from "node:fs";
5
5
  import { readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
6
- import { join as join5, relative as relative4, resolve as resolve3 } from "node:path";
6
+ import { isAbsolute, join as join5, relative as relative4, resolve as resolve3 } from "node:path";
7
7
 
8
8
  // ../core/src/audit/types.ts
9
9
  var AUDIT_SCHEMA_VERSION = 1;
@@ -1115,9 +1115,9 @@ var DesignSystemIndex = class {
1115
1115
  return generic ? this.make(generic, 1, 0.3) : void 0;
1116
1116
  }
1117
1117
  case "shadow": {
1118
- const norm3 = (s) => s.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").trim().toLowerCase();
1118
+ const norm4 = (s) => s.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").trim().toLowerCase();
1119
1119
  const cands = this.entries(["shadow"]);
1120
- const exact = cands.find((e) => norm3(e.css) === norm3(value));
1120
+ const exact = cands.find((e) => norm4(e.css) === norm4(value));
1121
1121
  if (exact) return this.make(exact, 0, 1);
1122
1122
  const t = shadowNums(value);
1123
1123
  const scored = cands.map((e) => {
@@ -1766,6 +1766,226 @@ async function originRepo(root) {
1766
1766
  // ../core/src/previews.ts
1767
1767
  var PREVIEW_APP_LABELS = { docs: "Docs", storybook: "Storybook", admin: "Admin demo" };
1768
1768
 
1769
+ // ../core/src/teams.ts
1770
+ var LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
1771
+ var SLUG = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
1772
+ function parseOwnerEntry(raw) {
1773
+ if (typeof raw !== "string") return null;
1774
+ const s = raw.trim().replace(/^@/, "");
1775
+ const slash = s.indexOf("/");
1776
+ if (slash < 0) return LOGIN.test(s) ? { kind: "person", login: s } : null;
1777
+ const org = s.slice(0, slash);
1778
+ const slug = s.slice(slash + 1);
1779
+ return LOGIN.test(org) && SLUG.test(slug) ? { kind: "team", org, slug } : null;
1780
+ }
1781
+ var teamKey = (org, slug) => `@${org.toLowerCase()}/${slug.toLowerCase()}`;
1782
+ function ownerEntryProblems(raw) {
1783
+ if (raw === void 0) return [];
1784
+ const isObj2 = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
1785
+ if (!isObj2(raw)) return ['"governance.owners" must be { "design": [people or teams], "tech": [people or teams] }'];
1786
+ const out = [];
1787
+ for (const [k, v] of Object.entries(raw)) {
1788
+ if (k !== "design" && k !== "tech") {
1789
+ out.push(`"governance.owners.${k}" is not an owner group (design, tech)`);
1790
+ continue;
1791
+ }
1792
+ if (!Array.isArray(v)) {
1793
+ out.push(`"governance.owners.${k}" must be a list of people or teams`);
1794
+ continue;
1795
+ }
1796
+ v.forEach((x, i) => {
1797
+ if (typeof x !== "string" || !parseOwnerEntry(x))
1798
+ out.push(`"governance.owners.${k}[${i}]" (${JSON.stringify(x)}) is not a GitHub login or team. Write a person as "peterbono" or "@peterbono", a team as "@org/team-slug".`);
1799
+ });
1800
+ }
1801
+ return out;
1802
+ }
1803
+ function teamUnreadableMessage(team, reason) {
1804
+ const name = `@${team.org}/${team.slug}`;
1805
+ switch (reason) {
1806
+ case "personal-account":
1807
+ return `${name} cannot be used: ${team.org} is a personal GitHub account, and teams only exist in organizations. List people instead, or move the repository to an organization.`;
1808
+ case "missing-permission":
1809
+ return `The members of ${name} cannot be read: the GitHub token needs read:org (classic token) or Members: Read on the ${team.org} organization (fine-grained token).`;
1810
+ case "no-token":
1811
+ return `The members of ${name} cannot be read without a working GitHub token. Connect GitHub with a token that has read:org.`;
1812
+ case "not-found":
1813
+ return `${name} was not found. Check the team slug on GitHub, or give the token read:org so it can see the team.`;
1814
+ case "unreachable":
1815
+ return `The members of ${name} cannot be read right now: GitHub did not answer. Try again in a moment.`;
1816
+ }
1817
+ }
1818
+ function teamsIn(owners) {
1819
+ const all = Array.isArray(owners) ? owners : [...owners.design ?? [], ...owners.tech ?? []];
1820
+ const out = /* @__PURE__ */ new Map();
1821
+ for (const raw of all) {
1822
+ const e = parseOwnerEntry(raw);
1823
+ if (e?.kind === "team") out.set(teamKey(e.org, e.slug), { key: teamKey(e.org, e.slug), org: e.org, slug: e.slug });
1824
+ }
1825
+ return [...out.values()];
1826
+ }
1827
+ var norm = (x) => x.trim().toLowerCase().replace(/^@/, "");
1828
+ var people = (n) => `${n} ${n === 1 ? "person" : "people"}`;
1829
+ function entryLabel(raw, teams) {
1830
+ const e = parseOwnerEntry(raw);
1831
+ if (!e) return raw;
1832
+ if (e.kind === "person") return `@${e.login}`;
1833
+ const state = teams?.[teamKey(e.org, e.slug)];
1834
+ const name = `@${e.org}/${e.slug}`;
1835
+ return state?.status === "ok" ? `${name} (${people(state.members.length)})` : name;
1836
+ }
1837
+ var TeamDirectory = class {
1838
+ constructor(reader, opts = {}) {
1839
+ this.reader = reader;
1840
+ this.ttl = opts.ttlMs ?? 5 * 6e4;
1841
+ this.now = opts.now ?? Date.now;
1842
+ }
1843
+ reader;
1844
+ teams = /* @__PURE__ */ new Map();
1845
+ accounts = /* @__PURE__ */ new Map();
1846
+ memberships = /* @__PURE__ */ new Map();
1847
+ ttl;
1848
+ now;
1849
+ accountType(org) {
1850
+ const k = org.toLowerCase();
1851
+ let p = this.accounts.get(k);
1852
+ if (!p) {
1853
+ p = this.reader.getAccountType(org).catch(() => null);
1854
+ this.accounts.set(k, p);
1855
+ }
1856
+ return p;
1857
+ }
1858
+ /** The reason a read of `org` failed, as a state. A 404 on a personal account says so; a 403 is a missing permission. */
1859
+ async failure(ref, err) {
1860
+ const e = err ?? {};
1861
+ let reason;
1862
+ if (!this.reader.hasToken) reason = await this.accountType(ref.org) === "User" ? "personal-account" : "no-token";
1863
+ else if (e.status === 401) reason = "no-token";
1864
+ else if (e.status === 403 || e.auth === "missing-permission") reason = "missing-permission";
1865
+ else if (e.status === 404) reason = await this.accountType(ref.org) === "User" ? "personal-account" : "not-found";
1866
+ else reason = "unreachable";
1867
+ return { ...ref, status: "unreadable", reason, message: teamUnreadableMessage(ref, reason) };
1868
+ }
1869
+ /** Members of one team, or why they cannot be read. Cached. */
1870
+ team(org, slug) {
1871
+ const ref = { key: teamKey(org, slug), org, slug };
1872
+ const hit = this.teams.get(ref.key);
1873
+ if (hit && this.now() - hit.at < this.ttl) return hit.value;
1874
+ const value = (async () => {
1875
+ if (!this.reader.hasToken) return this.failure(ref, void 0);
1876
+ try {
1877
+ const members = await this.reader.listTeamMembers(org, slug);
1878
+ return { ...ref, status: "ok", members };
1879
+ } catch (err) {
1880
+ return this.failure(ref, err);
1881
+ }
1882
+ })();
1883
+ this.teams.set(ref.key, { at: this.now(), value });
1884
+ void value.then((s) => {
1885
+ if (s.status === "unreadable" && s.reason === "unreachable") this.teams.delete(ref.key);
1886
+ });
1887
+ return value;
1888
+ }
1889
+ /** Every team named in the owner lists, read in parallel. Empty object when no team is named (no request). */
1890
+ async resolve(owners) {
1891
+ const refs = teamsIn(owners);
1892
+ const states = await Promise.all(refs.map((r) => this.team(r.org, r.slug)));
1893
+ return Object.fromEntries(states.map((s) => [s.key, s]));
1894
+ }
1895
+ /**
1896
+ * Whether `login` is an active member of one team: from the cached member list when there is one, else one
1897
+ * membership read (GET /orgs/{org}/teams/{slug}/memberships/{user}). An unreadable team returns its state.
1898
+ */
1899
+ async isMember(org, slug, login) {
1900
+ const ref = { key: teamKey(org, slug), org, slug };
1901
+ const cached = this.teams.get(ref.key);
1902
+ if (cached && this.now() - cached.at < this.ttl) {
1903
+ const s = await cached.value;
1904
+ if (s.status === "ok") return s.members.some((m) => norm(m) === norm(login));
1905
+ }
1906
+ const k = `${ref.key}#${norm(login)}`;
1907
+ const hit = this.memberships.get(k);
1908
+ if (hit && this.now() - hit.at < this.ttl) return hit.value;
1909
+ const value = (async () => {
1910
+ if (!this.reader.hasToken) return this.failure(ref, void 0);
1911
+ try {
1912
+ return await this.reader.getTeamMembership(org, slug, login) === "active";
1913
+ } catch (err) {
1914
+ return this.failure(ref, err);
1915
+ }
1916
+ })();
1917
+ this.memberships.set(k, { at: this.now(), value });
1918
+ return value;
1919
+ }
1920
+ /**
1921
+ * Whether `login` holds a right given to `entries` (people and teams): a person entry matches directly, a team
1922
+ * through its membership. Teams are only read when no person entry matches.
1923
+ */
1924
+ async memberOfAny(entries, login) {
1925
+ const unreadable = [];
1926
+ const parsed = entries.map(parseOwnerEntry).filter((e) => Boolean(e));
1927
+ if (parsed.some((e) => e.kind === "person" && norm(e.login) === norm(login))) return { member: true, unreadable };
1928
+ for (const e of parsed) {
1929
+ if (e.kind !== "team") continue;
1930
+ const r = await this.isMember(e.org, e.slug, login);
1931
+ if (r === true) return { member: true, unreadable };
1932
+ if (typeof r === "object" && r.status === "unreadable") unreadable.push(r);
1933
+ }
1934
+ return { member: false, unreadable };
1935
+ }
1936
+ /** The organization's teams for a picker, or why they cannot be listed (a personal account has none). */
1937
+ async orgTeams(org) {
1938
+ if (await this.accountType(org) === "User") return { status: "unreadable", reason: "personal-account" };
1939
+ if (!this.reader.hasToken) return { status: "unreadable", reason: "no-token" };
1940
+ try {
1941
+ return { status: "ok", teams: await this.reader.listOrgTeams(org) };
1942
+ } catch (err) {
1943
+ const s = await this.failure({ key: teamKey(org, "*"), org, slug: "*" }, err);
1944
+ return { status: "unreadable", reason: s.status === "unreadable" ? s.reason : "unreachable" };
1945
+ }
1946
+ }
1947
+ /** Drops every cached read (after the token changes). */
1948
+ clear() {
1949
+ this.teams.clear();
1950
+ this.accounts.clear();
1951
+ this.memberships.clear();
1952
+ }
1953
+ };
1954
+
1955
+ // ../core/src/rights.ts
1956
+ var OWNER_GROUPS = ["design", "tech"];
1957
+ var RIGHTS = ["approveDesign", "approveTech", "ship"];
1958
+ var DEFAULT_RIGHTS = { approveDesign: ["design"], approveTech: ["tech"], ship: ["tech"] };
1959
+ function normalizeRights(input) {
1960
+ const pick = (r) => {
1961
+ const v = input?.[r];
1962
+ if (!Array.isArray(v)) return [...DEFAULT_RIGHTS[r]];
1963
+ return OWNER_GROUPS.filter((g) => v.includes(g));
1964
+ };
1965
+ return { approveDesign: pick("approveDesign"), approveTech: pick("approveTech"), ship: pick("ship") };
1966
+ }
1967
+ function rightsProblems(raw) {
1968
+ if (raw === void 0) return [];
1969
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return ['"governance.rights" must be { "approveDesign": [groups], "approveTech": [groups], "ship": [groups] }'];
1970
+ const out = [];
1971
+ for (const [k, v] of Object.entries(raw)) {
1972
+ if (!RIGHTS.includes(k)) out.push(`"governance.rights.${k}" is not a right (${RIGHTS.join(", ")})`);
1973
+ else if (!Array.isArray(v) || v.length === 0 || !v.every((g) => OWNER_GROUPS.includes(g)))
1974
+ out.push(`"governance.rights.${k}" must list one or more of "design", "tech"`);
1975
+ }
1976
+ return out;
1977
+ }
1978
+ function visualImpactProblems(raw) {
1979
+ if (raw === void 0) return [];
1980
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return ['"visualImpact" must be { "mode": "auto" | "on-demand" }'];
1981
+ const out = [];
1982
+ for (const [k, v] of Object.entries(raw)) {
1983
+ if (k !== "mode") out.push(`"visualImpact.${k}" is not a setting (mode)`);
1984
+ else if (v !== "auto" && v !== "on-demand") out.push('"visualImpact.mode" must be "auto" or "on-demand"');
1985
+ }
1986
+ return out;
1987
+ }
1988
+
1769
1989
  // ../core/src/config.ts
1770
1990
  var CONFIG_FILE = "bridge.config.json";
1771
1991
  var DEFAULT_OVERRIDE = { allowed: [], requireReason: true, minReasonLength: 20 };
@@ -1903,9 +2123,11 @@ function defineConfig(input) {
1903
2123
  requireReason: g.override.requireReason ?? DEFAULT_OVERRIDE.requireReason,
1904
2124
  minReasonLength: g.override.minReasonLength ?? DEFAULT_OVERRIDE.minReasonLength
1905
2125
  }
1906
- } : {}
2126
+ } : {},
2127
+ ...g.rights ? { rights: normalizeRights(g.rights) } : {}
1907
2128
  },
1908
2129
  ...input.audit ? { audit: { ...input.audit } } : {},
2130
+ ...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
1909
2131
  ...input.integrations ? { integrations: { ...input.integrations } } : {},
1910
2132
  ...input.previews ? {
1911
2133
  previews: {
@@ -2012,9 +2234,7 @@ function loadConfig(source) {
2012
2234
  else {
2013
2235
  for (const k of ["protectedBranches", "labels", "guardrails"]) if (gov[k] !== void 0 && !(Array.isArray(gov[k]) && gov[k].every((x) => typeof x === "string"))) problems.push(`"governance.${k}" must be a list of strings`);
2014
2236
  if (gov.branchPrefix !== void 0 && !isStr(gov.branchPrefix)) problems.push('"governance.branchPrefix" must be a non-empty string');
2015
- const o = gov.owners;
2016
- if (o !== void 0 && (!isObj(o) || ["design", "tech"].some((k) => o[k] !== void 0 && !(Array.isArray(o[k]) && o[k].every((x) => typeof x === "string")))))
2017
- problems.push('"governance.owners" must be { "design": [logins], "tech": [logins] }');
2237
+ problems.push(...ownerEntryProblems(gov.owners));
2018
2238
  const ov = gov.override;
2019
2239
  if (ov !== void 0) {
2020
2240
  if (!isObj(ov)) problems.push('"governance.override" must be an object');
@@ -2024,8 +2244,10 @@ function loadConfig(source) {
2024
2244
  if (ov.minReasonLength !== void 0 && !(Number.isInteger(ov.minReasonLength) && ov.minReasonLength >= 0)) problems.push('"governance.override.minReasonLength" must be a whole number, 0 or more');
2025
2245
  }
2026
2246
  }
2247
+ problems.push(...rightsProblems(gov.rights));
2027
2248
  }
2028
2249
  }
2250
+ problems.push(...visualImpactProblems(d.visualImpact));
2029
2251
  const audit2 = d.audit;
2030
2252
  if (audit2 !== void 0) {
2031
2253
  if (!isObj(audit2)) problems.push('"audit" must be an object');
@@ -2237,125 +2459,147 @@ import { mkdir as mkdir3, readFile as readFile3, rm as rm2, unlink, writeFile as
2237
2459
  import { tmpdir } from "node:os";
2238
2460
  import { join as join4, relative as relative3, resolve as resolve2 } from "node:path";
2239
2461
 
2240
- // ../core/src/usage.ts
2241
- var VAR_RE = /var\(\s*(--[A-Za-z0-9_-]+)/g;
2242
- var IMPORT_RE = /(?:import|export)\s[^'"]*?from\s*['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
2243
- function cssVarsIn(text) {
2244
- const out = /* @__PURE__ */ new Set();
2245
- for (const m of text.matchAll(VAR_RE)) out.add(m[1]);
2246
- return [...out];
2247
- }
2248
- function importsIn(text) {
2249
- const out = /* @__PURE__ */ new Set();
2250
- for (const m of text.matchAll(IMPORT_RE)) out.add(m[1] ?? m[2]);
2251
- return [...out];
2252
- }
2253
- var norm = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
2254
- var isStory = (p) => /\.stories\.[cm]?[jt]sx?$/.test(p);
2255
- var isTest = (p) => /\.(test|spec)\.[cm]?[jt]sx?$/.test(p);
2256
- var isCss = (p) => /\.(css|scss|less)$/.test(p);
2257
- var isScript = (p) => /\.[cm]?[jt]sx?$/.test(p);
2258
- function componentOf(from, spec, componentsDir) {
2259
- if (!spec.startsWith(".")) return null;
2260
- const parts = from.split("/").slice(0, -1);
2261
- for (const seg of spec.split("/")) {
2262
- if (seg === "." || seg === "") continue;
2263
- if (seg === "..") parts.pop();
2264
- else parts.push(seg);
2265
- }
2266
- const abs = parts.join("/");
2267
- if (!abs.startsWith(`${componentsDir}/`)) return null;
2268
- return abs.slice(componentsDir.length + 1).split("/")[0] || null;
2269
- }
2270
- function componentForPath(path, componentsDir) {
2271
- const dir = norm(componentsDir);
2272
- const p = norm(path);
2273
- if (!p.startsWith(`${dir}/`)) return null;
2274
- const rest = p.slice(dir.length + 1).split("/");
2275
- return rest.length >= 2 ? rest[0] : null;
2276
- }
2277
- function buildUsageIndex(files, componentsDir) {
2278
- const dir = norm(componentsDir);
2279
- const modules = {};
2280
- const vars = /* @__PURE__ */ new Map();
2281
- const imports = /* @__PURE__ */ new Map();
2282
- const storyImports = /* @__PURE__ */ new Map();
2283
- const globalVars = {};
2284
- const get = (m, k) => m.get(k) ?? m.set(k, /* @__PURE__ */ new Set()).get(k);
2285
- for (const f of files) {
2286
- const path = norm(f.path);
2287
- const id = componentForPath(path, dir);
2288
- if (!id) {
2289
- if (isCss(path)) {
2290
- const v = cssVarsIn(f.text);
2291
- if (v.length) globalVars[path] = v.sort();
2292
- }
2293
- continue;
2294
- }
2295
- const mod = modules[id] ??= { id, dir: `${dir}/${id}`, files: [], vars: [], imports: [], storyImports: [] };
2296
- mod.files.push(path);
2297
- if (isTest(path)) continue;
2298
- for (const v of cssVarsIn(f.text)) get(vars, id).add(v);
2299
- if (!isScript(path)) continue;
2300
- for (const spec of importsIn(f.text)) {
2301
- const dep = componentOf(path, spec, dir);
2302
- if (!dep || dep === id) continue;
2303
- get(isStory(path) ? storyImports : imports, id).add(dep);
2304
- }
2305
- }
2306
- const byVar = {};
2307
- for (const mod of Object.values(modules)) {
2308
- mod.files.sort();
2309
- mod.vars = [...vars.get(mod.id) ?? []].sort();
2310
- mod.imports = [...imports.get(mod.id) ?? []].sort();
2311
- mod.storyImports = [...storyImports.get(mod.id) ?? []].filter((d) => !mod.imports.includes(d)).sort();
2312
- for (const v of mod.vars) (byVar[v] ??= []).push(mod.id);
2313
- }
2314
- for (const k of Object.keys(byVar)) byVar[k].sort();
2315
- return { componentsDir: dir, modules, byVar, globalVars };
2316
- }
2317
- function impactedModules(index, input) {
2318
- const reasons = /* @__PURE__ */ new Map();
2319
- const add = (id, reason, indirect) => {
2320
- const r = reasons.get(id);
2321
- if (r) {
2322
- if (!r.reasons.includes(reason)) r.reasons.push(reason);
2323
- r.indirect &&= indirect;
2324
- } else reasons.set(id, { reasons: [reason], indirect });
2325
- };
2326
- const changedVars = new Set(input.vars ?? []);
2327
- for (const v of changedVars) for (const id of index.byVar[v] ?? []) add(id, `uses ${v}`, false);
2328
- for (const f of input.files ?? []) {
2329
- const id = componentForPath(f, index.componentsDir);
2330
- if (id && index.modules[id]) add(id, `file changed: ${norm(f)}`, false);
2331
- }
2332
- const renderedBy = /* @__PURE__ */ new Map();
2333
- for (const m of Object.values(index.modules)) for (const d of m.imports) (renderedBy.get(d) ?? renderedBy.set(d, []).get(d)).push(m.id);
2334
- const queue = [...reasons.keys()];
2335
- while (queue.length) {
2336
- const id = queue.shift();
2337
- for (const parent of renderedBy.get(id) ?? []) {
2338
- const known = reasons.has(parent);
2339
- add(parent, `renders ${id}`, true);
2340
- if (!known) queue.push(parent);
2462
+ // ../core/src/protection.ts
2463
+ var PROTECTION_PARTS = ["review", "checks", "history", "deletion"];
2464
+ var protectionRulesetName = (defaultBranch) => `Protect the library (${defaultBranch})`;
2465
+ function refPattern(pattern) {
2466
+ const src = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, ".*");
2467
+ return new RegExp(`^${src}$`);
2468
+ }
2469
+ function rulesetTargetsDefault(ruleset, defaultBranch) {
2470
+ if (ruleset.target && ruleset.target !== "branch") return false;
2471
+ const ref = `refs/heads/${defaultBranch}`;
2472
+ const hit = (p) => p === "~ALL" || p === "~DEFAULT_BRANCH" || p === ref || p === defaultBranch || refPattern(p.startsWith("refs/") ? p : `refs/heads/${p}`).test(ref);
2473
+ const cond = ruleset.conditions?.ref_name;
2474
+ if (!cond) return false;
2475
+ if ((cond.exclude ?? []).some(hit)) return false;
2476
+ return (cond.include ?? []).some(hit);
2477
+ }
2478
+ var count = (v) => typeof v === "number" ? v : 0;
2479
+ function rulesetParts(ruleset) {
2480
+ const parts = /* @__PURE__ */ new Set();
2481
+ if (ruleset.enforcement !== "active") return parts;
2482
+ for (const rule of ruleset.rules ?? []) {
2483
+ if (rule.type === "deletion") parts.add("deletion");
2484
+ if (rule.type === "non_fast_forward") parts.add("history");
2485
+ if (rule.type === "pull_request" && count(rule.parameters?.required_approving_review_count) >= 1) parts.add("review");
2486
+ if (rule.type === "required_status_checks" && Array.isArray(rule.parameters?.required_status_checks) && rule.parameters.required_status_checks.length > 0) parts.add("checks");
2487
+ }
2488
+ return parts;
2489
+ }
2490
+ function classicParts(p) {
2491
+ const parts = /* @__PURE__ */ new Set();
2492
+ if (count(p.required_pull_request_reviews?.required_approving_review_count) >= 1) parts.add("review");
2493
+ if (((p.required_status_checks?.contexts?.length ?? 0) || (p.required_status_checks?.checks?.length ?? 0)) > 0) parts.add("checks");
2494
+ if (!p.allow_force_pushes?.enabled) parts.add("history");
2495
+ if (!p.allow_deletions?.enabled) parts.add("deletion");
2496
+ return parts;
2497
+ }
2498
+ var rulesetUrl = (r) => r.html_url ?? r._links?.html?.href;
2499
+ function protectionState(rulesets, branchProtection, defaultBranch) {
2500
+ const bridge = rulesets?.find((r) => r.name === protectionRulesetName(defaultBranch));
2501
+ const extra = bridge?.id !== void 0 ? { bridgeRulesetId: bridge.id } : {};
2502
+ const covered = /* @__PURE__ */ new Set();
2503
+ let url;
2504
+ for (const r of rulesets ?? []) {
2505
+ if (!rulesetTargetsDefault(r, defaultBranch)) continue;
2506
+ const parts = rulesetParts(r);
2507
+ if (parts.size && !url) url = rulesetUrl(r);
2508
+ parts.forEach((p) => covered.add(p));
2509
+ }
2510
+ if (branchProtection) classicParts(branchProtection).forEach((p) => covered.add(p));
2511
+ const missing = PROTECTION_PARTS.filter((p) => !covered.has(p));
2512
+ const withUrl = url ? { url } : {};
2513
+ if (!missing.length) return { kind: "protected", missing, ...extra, ...withUrl };
2514
+ const unreadable = rulesets == null || branchProtection === void 0;
2515
+ if (covered.size === 0) return { kind: unreadable ? "unknown" : "unprotected", missing, ...extra };
2516
+ return { kind: "partial", missing, ...extra, ...withUrl };
2517
+ }
2518
+ var PROTECTION_PART_PHRASE = {
2519
+ review: "changes can skip review",
2520
+ checks: "checks do not have to pass",
2521
+ history: "past changes can be rewritten",
2522
+ deletion: "the library can be deleted"
2523
+ };
2524
+ var joinAnd = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
2525
+ function protectionSentence(state) {
2526
+ switch (state.kind) {
2527
+ case "protected":
2528
+ return "Protected: every change goes through review.";
2529
+ case "unprotected":
2530
+ return "Your library can be changed without review.";
2531
+ case "partial": {
2532
+ const s = joinAnd(state.missing.map((p) => PROTECTION_PART_PHRASE[p]));
2533
+ return `Partly protected: ${s}.`;
2341
2534
  }
2535
+ default:
2536
+ return "Protection status unknown: GitHub did not let this token read it.";
2342
2537
  }
2343
- const globalHits = Object.entries(index.globalVars).map(([file, vs]) => ({ file, vars: vs.filter((v) => changedVars.has(v)) })).filter((g) => g.vars.length > 0);
2344
- const modules = [...reasons.entries()].map(([id, r]) => ({ id, reasons: r.reasons, indirect: r.indirect })).sort((a, b) => Number(a.indirect) - Number(b.indirect) || a.id.localeCompare(b.id));
2345
- return { modules, globalHits };
2346
2538
  }
2347
2539
 
2348
2540
  // ../core/src/github-client.ts
2349
2541
  var GitHubError = class extends Error {
2350
- constructor(status, message, detail) {
2542
+ constructor(status, message, detail, info = {}) {
2351
2543
  super(message);
2352
2544
  this.status = status;
2353
2545
  this.detail = detail;
2354
2546
  this.name = "GitHubError";
2547
+ this.auth = info.auth;
2548
+ this.permission = info.permission;
2355
2549
  }
2356
2550
  status;
2357
2551
  detail;
2552
+ /** Set when the failure is about the token: rejected (401) or missing a permission (403). */
2553
+ auth;
2554
+ /** With auth "missing-permission": the permission the call needed, when known. */
2555
+ permission;
2556
+ };
2557
+ var PERMISSION_LABELS = {
2558
+ issues: "Issues",
2559
+ pull_requests: "Pull requests",
2560
+ contents: "Contents",
2561
+ checks: "Checks",
2562
+ statuses: "Commit statuses",
2563
+ metadata: "Metadata",
2564
+ actions: "Actions",
2565
+ deployments: "Deployments",
2566
+ workflows: "Workflows",
2567
+ administration: "Administration"
2358
2568
  };
2569
+ function permissionLabel(permission) {
2570
+ return PERMISSION_LABELS[permission] ?? permission.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
2571
+ }
2572
+ var TOKEN_REJECTED_MESSAGE = "Your GitHub token stopped working. It was revoked, regenerated or has expired: replace it with a new one.";
2573
+ function missingPermissionMessage(permission, action) {
2574
+ const what = action ? `This token cannot ${action}.` : "This token is missing a permission for this.";
2575
+ if (!permission) return `${what} Check its repository permissions on GitHub (Contents and Pull requests need Read and write).`;
2576
+ return `${what} Add ${permissionLabel(permission)}: Read and write on GitHub.`;
2577
+ }
2578
+ function requiredPermission(path, acceptedHeader) {
2579
+ if (acceptedHeader) {
2580
+ const first = acceptedHeader.split(";")[0].split(",").map((p2) => p2.trim().split("=")[0].trim()).filter((p2) => p2 && p2 !== "metadata");
2581
+ if (first.length) return first[0];
2582
+ }
2583
+ const p = path.replace(/[?#].*$/, "");
2584
+ if (/\/repos\/[^/]+\/[^/]+\/(issues|labels|milestones)(\/|$)/.test(p)) return "issues";
2585
+ if (/\/repos\/[^/]+\/[^/]+\/pulls(\/|$)/.test(p)) return "pull_requests";
2586
+ if (/\/repos\/[^/]+\/[^/]+\/(rulesets(\/|$)|branches\/.+\/protection$)/.test(p)) return "administration";
2587
+ if (/\/repos\/[^/]+\/[^/]+\/(contents|git|commits|branches|merges)(\/|$)/.test(p)) return "contents";
2588
+ if (/\/repos\/[^/]+\/[^/]+\/(check-runs|check-suites)(\/|$)/.test(p)) return "checks";
2589
+ if (/\/repos\/[^/]+\/[^/]+\/statuses(\/|$)/.test(p)) return "statuses";
2590
+ if (/\/repos\/[^/]+\/[^/]+\/deployments(\/|$)/.test(p)) return "deployments";
2591
+ if (/\/repos\/[^/]+\/[^/]+\/actions(\/|$)/.test(p)) return "actions";
2592
+ return void 0;
2593
+ }
2594
+ var PERMISSION_DENIED = /not accessible by (personal access token|integration)|must have (admin|push|write) (rights|access)|write access to repository not granted|permission to .* denied/i;
2595
+ function classifyAuth(status, detail, opts = {}) {
2596
+ if (status === 401 && opts.withToken !== false) return { auth: "rejected" };
2597
+ if (status !== 403 || /rate limit|archived/i.test(detail)) return {};
2598
+ if (opts.acceptedPermissions || PERMISSION_DENIED.test(detail)) {
2599
+ return { auth: "missing-permission", permission: requiredPermission(opts.path ?? "", opts.acceptedPermissions) };
2600
+ }
2601
+ return {};
2602
+ }
2359
2603
  var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
2360
2604
  var isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
2361
2605
  var API = "https://api.github.com";
@@ -2417,7 +2661,7 @@ var GitHubClient = class _GitHubClient {
2417
2661
  }
2418
2662
  }
2419
2663
  assertToken(what) {
2420
- if (!this.token) throw new GitHubError(401, `${what} needs a GitHub token (contents and pull requests, read and write, on this repository only). Without one Bridge is read-only.`);
2664
+ if (!this.token) throw new GitHubError(401, `${what} needs a GitHub token (contents and pull requests, read and write, on this repository only). Without one Bridge is read-only.`, "no-token");
2421
2665
  }
2422
2666
  get repoPath() {
2423
2667
  return `/repos/${this.owner}/${this.name}`;
@@ -2466,7 +2710,12 @@ var GitHubClient = class _GitHubClient {
2466
2710
  detail
2467
2711
  );
2468
2712
  }
2469
- throw new GitHubError(res.status, friendlyMessage(res.status, detail), detail);
2713
+ const info = classifyAuth(res.status, detail, {
2714
+ path,
2715
+ acceptedPermissions: res.headers?.get("x-accepted-github-permissions"),
2716
+ withToken: Boolean(this.token)
2717
+ });
2718
+ throw new GitHubError(res.status, friendlyMessage(res.status, detail, info), detail, info);
2470
2719
  }
2471
2720
  return await res.json();
2472
2721
  }
@@ -2616,6 +2865,25 @@ var GitHubClient = class _GitHubClient {
2616
2865
  throw new GitHubError(422, `GitHub refused to mark #${n} ready for review${detail ? `: ${detail}` : "."} Use "Ready for review" on GitHub instead.`, detail);
2617
2866
  }
2618
2867
  }
2868
+ /**
2869
+ * Turns an open pull request back into a draft: the GraphQL mutation convertPullRequestToDraft (REST cannot set
2870
+ * `draft` either), same token. No-op when it is already a draft.
2871
+ */
2872
+ async convertToDraft(n) {
2873
+ this.assertToken("Turning a pull request back into a draft");
2874
+ const pr = await this.request("GET", `${this.repoPath}/pulls/${n}`);
2875
+ if (pr.draft === true) return;
2876
+ if (pr.state && pr.state !== "open") throw new GitHubError(422, `Pull request #${n} is ${pr.state}: only an open one can go back to draft.`);
2877
+ if (!pr.node_id) throw new GitHubError(422, `Pull request #${n} has no node id: convert it to draft on GitHub.`);
2878
+ const r = await this.request("POST", "/graphql", {
2879
+ query: "mutation($id: ID!) { convertPullRequestToDraft(input: { pullRequestId: $id }) { pullRequest { number isDraft } } }",
2880
+ variables: { id: pr.node_id }
2881
+ });
2882
+ if (r?.errors?.length) {
2883
+ const detail = r.errors.map((e) => e.message).filter(Boolean).join(" ");
2884
+ throw new GitHubError(422, `GitHub refused to turn #${n} back into a draft${detail ? `: ${detail}` : "."} Use "Convert to draft" on GitHub instead.`, detail);
2885
+ }
2886
+ }
2619
2887
  async createPullRequest(input) {
2620
2888
  this.assertWritableBranch(input.head, input.base);
2621
2889
  const r = await this.request(
@@ -2645,6 +2913,11 @@ var GitHubClient = class _GitHubClient {
2645
2913
  async addComment(issueNumber2, body) {
2646
2914
  await this.request("POST", `${this.repoPath}/issues/${issueNumber2}/comments`, { body });
2647
2915
  }
2916
+ /** Comments on an issue or pull request, oldest first (first 100: the sticky preview comment is posted early). */
2917
+ async listIssueComments(issueNumber2) {
2918
+ const r = await this.request("GET", `${this.repoPath}/issues/${issueNumber2}/comments?per_page=100`);
2919
+ return r.map((c) => ({ id: c.id, body: c.body ?? "", author: c.user?.login ?? "unknown", updatedAt: c.updated_at }));
2920
+ }
2648
2921
  // --- Issues (component change requests) ---------------------------------------------------
2649
2922
  async createIssue(input) {
2650
2923
  const r = await this.request("POST", `${this.repoPath}/issues`, {
@@ -2665,6 +2938,67 @@ var GitHubClient = class _GitHubClient {
2665
2938
  async getIssue(n) {
2666
2939
  return toIssue(await this.request("GET", `${this.repoPath}/issues/${n}`));
2667
2940
  }
2941
+ /** Edits an issue: its body, or closes it with a reason ("not_planned" for a declined request). */
2942
+ async updateIssue(n, patch) {
2943
+ this.assertToken("Editing an issue");
2944
+ return toIssue(await this.request("PATCH", `${this.repoPath}/issues/${n}`, patch));
2945
+ }
2946
+ /** Adds people to an issue's assignees (GitHub ignores logins that cannot be assigned: the result says who is). */
2947
+ async addAssignees(n, logins) {
2948
+ this.assertToken("Assigning an issue");
2949
+ const r = await this.request("POST", `${this.repoPath}/issues/${n}/assignees`, { assignees: logins });
2950
+ return (r.assignees ?? []).map((a) => a.login);
2951
+ }
2952
+ /** People with at least `permission` on the repository (reading the list needs push access). First 100. */
2953
+ async listCollaborators(permission = "push") {
2954
+ const r = await this.request("GET", `${this.repoPath}/collaborators?permission=${permission}&per_page=100`);
2955
+ return r.map((c) => ({ login: c.login, avatarUrl: c.avatar_url ?? "" }));
2956
+ }
2957
+ /** Whether a token is sent with each request (TeamReader). */
2958
+ get hasToken() {
2959
+ return Boolean(this.token);
2960
+ }
2961
+ /** Every page of a list endpoint (per_page 100), up to `maxPages`. */
2962
+ async paginate(path, maxPages = 20) {
2963
+ const out = [];
2964
+ const sep3 = path.includes("?") ? "&" : "?";
2965
+ for (let page = 1; page <= maxPages; page++) {
2966
+ const r = await this.request("GET", `${path}${sep3}per_page=100&page=${page}`);
2967
+ out.push(...r);
2968
+ if (r.length < 100) break;
2969
+ }
2970
+ return out;
2971
+ }
2972
+ /** Every member login of an organization team (needs read:org, or Members: Read on a fine-grained token). */
2973
+ async listTeamMembers(org, slug) {
2974
+ const r = await this.paginate(`/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug)}/members`);
2975
+ return r.map((m) => m.login);
2976
+ }
2977
+ /** A user's membership of a team: "active", "pending", or null when GitHub says they are not a member (404). */
2978
+ async getTeamMembership(org, slug, login) {
2979
+ try {
2980
+ const r = await this.request("GET", `/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug)}/memberships/${encodeURIComponent(login)}`);
2981
+ return r.state === "active" ? "active" : "pending";
2982
+ } catch (err) {
2983
+ if (err instanceof GitHubError && err.status === 404) return null;
2984
+ throw err;
2985
+ }
2986
+ }
2987
+ /** "User" or "Organization" for a login, null when GitHub does not know it. */
2988
+ async getAccountType(login) {
2989
+ try {
2990
+ const r = await this.request("GET", `/users/${encodeURIComponent(login)}`);
2991
+ return r.type === "Organization" ? "Organization" : r.type === "User" ? "User" : null;
2992
+ } catch (err) {
2993
+ if (err instanceof GitHubError && err.status === 404) return null;
2994
+ throw err;
2995
+ }
2996
+ }
2997
+ /** The organization's teams this token can see. */
2998
+ async listOrgTeams(org) {
2999
+ const r = await this.paginate(`/orgs/${encodeURIComponent(org)}/teams`);
3000
+ return r.map((t) => ({ slug: t.slug, name: t.name }));
3001
+ }
2668
3002
  /** Create or update a binary file (base64 content) on a branch, e.g. a Figma export attached to a request. */
2669
3003
  async putBase64File(input) {
2670
3004
  this.assertWritableBranch(input.branch);
@@ -2800,6 +3134,27 @@ var GitHubClient = class _GitHubClient {
2800
3134
  const pending = checks.some((c) => c.status !== "completed");
2801
3135
  return { state: !checks.length ? "none" : failing ? "failure" : pending ? "pending" : "success", checks };
2802
3136
  }
3137
+ /** Commit statuses of a commit, newest first (Vercel reports each project as "Vercel – <project>"). */
3138
+ async listCommitStatuses(sha) {
3139
+ const r = await this.request("GET", `${this.repoPath}/commits/${encodeRef(sha)}/statuses?per_page=100`);
3140
+ return r.map((s) => ({ context: s.context, state: s.state, target_url: s.target_url ?? null, description: s.description ?? null }));
3141
+ }
3142
+ /**
3143
+ * GitHub deployments of a commit with their latest status (environment "Preview – <project>", environment_url the
3144
+ * preview). Latest per environment only, so it costs one read plus one per environment.
3145
+ */
3146
+ async listDeployments(sha) {
3147
+ const list2 = await this.request("GET", `${this.repoPath}/deployments?sha=${encodeURIComponent(sha)}&per_page=30`);
3148
+ const latest = /* @__PURE__ */ new Map();
3149
+ for (const d of list2) if (!latest.has(d.environment)) latest.set(d.environment, d);
3150
+ return Promise.all(
3151
+ [...latest.values()].map(async (d) => {
3152
+ const st = await this.request("GET", `${this.repoPath}/deployments/${d.id}/statuses?per_page=1`).catch(() => []);
3153
+ const s = st[0];
3154
+ return { id: d.id, environment: d.environment, state: s?.state ?? null, environmentUrl: s?.environment_url || null };
3155
+ })
3156
+ );
3157
+ }
2803
3158
  async getPullRequest(n) {
2804
3159
  const r = await this.request("GET", `${this.repoPath}/pulls/${n}`);
2805
3160
  return { ...toSummary(r), body: r.body ?? "", mergeable: r.mergeable ?? null };
@@ -2826,7 +3181,44 @@ var GitHubClient = class _GitHubClient {
2826
3181
  "GET",
2827
3182
  `${this.repoPath}/commits/${encodeRef(ref)}/check-runs`
2828
3183
  );
2829
- return r.check_runs.map((c) => ({ id: c.id, name: c.name, status: c.status, conclusion: c.conclusion, url: c.html_url }));
3184
+ return r.check_runs.map((c) => ({ id: c.id, name: c.name, status: c.status, conclusion: c.conclusion, url: c.html_url, ...c.app?.slug ? { app: c.app.slug } : {} }));
3185
+ }
3186
+ /** Rulesets that apply to the repository, without their rules (GET /rulesets; read access is enough). */
3187
+ async listRulesets() {
3188
+ const r = await this.request("GET", `${this.repoPath}/rulesets?includes_parents=true&per_page=100`);
3189
+ return r.map(withRulesetUrl);
3190
+ }
3191
+ /** One ruleset with its rules. */
3192
+ async getRuleset(id) {
3193
+ return withRulesetUrl(await this.request("GET", `${this.repoPath}/rulesets/${id}`));
3194
+ }
3195
+ /** Creates a repository ruleset. Needs admin on the repo (fine-grained token: Administration read and write). */
3196
+ async createRuleset(payload) {
3197
+ this.assertToken("Protecting the library");
3198
+ return withRulesetUrl(await this.request("POST", `${this.repoPath}/rulesets`, payload));
3199
+ }
3200
+ /** Replaces a repository ruleset (same permissions as createRuleset). */
3201
+ async updateRuleset(id, payload) {
3202
+ this.assertToken("Protecting the library");
3203
+ return withRulesetUrl(await this.request("PUT", `${this.repoPath}/rulesets/${id}`, payload));
3204
+ }
3205
+ /**
3206
+ * Classic branch protection of `branch`: the settings, null when the branch has none (404), undefined when this
3207
+ * token cannot read them (403: the endpoint needs admin). On a 403 the public branch summary still tells
3208
+ * whether the branch is protected at all: `protected: false` means there is nothing to read, so null.
3209
+ */
3210
+ async getBranchProtection(branch) {
3211
+ try {
3212
+ return await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}/protection`);
3213
+ } catch (err) {
3214
+ if (!(err instanceof GitHubError)) throw err;
3215
+ if (err.status === 404) return null;
3216
+ if (err.status === 403 || err.status === 401) {
3217
+ const b = await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}`).catch(() => void 0);
3218
+ return b?.protected === false ? null : void 0;
3219
+ }
3220
+ throw err;
3221
+ }
2830
3222
  }
2831
3223
  /** Create a check run (needs a GitHub App token; personal tokens are refused by GitHub). */
2832
3224
  async createCheckRun(input) {
@@ -2868,7 +3260,9 @@ function toSummary(r) {
2868
3260
  ...typeof r.body === "string" ? { body: r.body } : {}
2869
3261
  };
2870
3262
  }
2871
- function friendlyMessage(status, detail) {
3263
+ function friendlyMessage(status, detail, info = {}) {
3264
+ if (info.auth === "rejected") return TOKEN_REJECTED_MESSAGE;
3265
+ if (info.auth === "missing-permission") return missingPermissionMessage(info.permission);
2872
3266
  switch (status) {
2873
3267
  case 401:
2874
3268
  return "GitHub rejected the token. It may be expired or mistyped: create a new one and save it in Settings.";
@@ -2888,6 +3282,10 @@ function friendlyMessage(status, detail) {
2888
3282
  return `GitHub error ${status}${detail ? `: ${detail}` : ""}`;
2889
3283
  }
2890
3284
  }
3285
+ function withRulesetUrl(r) {
3286
+ const href = r._links?.html?.href;
3287
+ return href && !r.html_url ? { ...r, html_url: href } : r;
3288
+ }
2891
3289
  function toIssue(r) {
2892
3290
  return {
2893
3291
  number: r.number,
@@ -2903,6 +3301,122 @@ function toIssue(r) {
2903
3301
  };
2904
3302
  }
2905
3303
 
3304
+ // ../core/src/protection-github.ts
3305
+ async function loadProtection(client, defaultBranch) {
3306
+ const rulesets = await client.listRulesets().then((list2) => Promise.all(list2.map((r) => r.enforcement === "disabled" || r.id === void 0 ? r : client.getRuleset(r.id)))).catch(() => null);
3307
+ const classic = await client.getBranchProtection(defaultBranch).catch(() => void 0);
3308
+ return protectionState(rulesets, classic, defaultBranch);
3309
+ }
3310
+ var rulesSettingsUrl = (owner, repo) => `https://github.com/${owner}/${repo}/settings/rules`;
3311
+
3312
+ // ../core/src/usage.ts
3313
+ var VAR_RE = /var\(\s*(--[A-Za-z0-9_-]+)/g;
3314
+ var IMPORT_RE = /(?:import|export)\s[^'"]*?from\s*['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
3315
+ function cssVarsIn(text) {
3316
+ const out = /* @__PURE__ */ new Set();
3317
+ for (const m of text.matchAll(VAR_RE)) out.add(m[1]);
3318
+ return [...out];
3319
+ }
3320
+ function importsIn(text) {
3321
+ const out = /* @__PURE__ */ new Set();
3322
+ for (const m of text.matchAll(IMPORT_RE)) out.add(m[1] ?? m[2]);
3323
+ return [...out];
3324
+ }
3325
+ var norm2 = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
3326
+ var isStory = (p) => /\.stories\.[cm]?[jt]sx?$/.test(p);
3327
+ var isTest = (p) => /\.(test|spec)\.[cm]?[jt]sx?$/.test(p);
3328
+ var isCss = (p) => /\.(css|scss|less)$/.test(p);
3329
+ var isScript = (p) => /\.[cm]?[jt]sx?$/.test(p);
3330
+ function componentOf(from, spec, componentsDir) {
3331
+ if (!spec.startsWith(".")) return null;
3332
+ const parts = from.split("/").slice(0, -1);
3333
+ for (const seg of spec.split("/")) {
3334
+ if (seg === "." || seg === "") continue;
3335
+ if (seg === "..") parts.pop();
3336
+ else parts.push(seg);
3337
+ }
3338
+ const abs = parts.join("/");
3339
+ if (!abs.startsWith(`${componentsDir}/`)) return null;
3340
+ return abs.slice(componentsDir.length + 1).split("/")[0] || null;
3341
+ }
3342
+ function componentForPath(path, componentsDir) {
3343
+ const dir = norm2(componentsDir);
3344
+ const p = norm2(path);
3345
+ if (!p.startsWith(`${dir}/`)) return null;
3346
+ const rest = p.slice(dir.length + 1).split("/");
3347
+ return rest.length >= 2 ? rest[0] : null;
3348
+ }
3349
+ function buildUsageIndex(files, componentsDir) {
3350
+ const dir = norm2(componentsDir);
3351
+ const modules = {};
3352
+ const vars = /* @__PURE__ */ new Map();
3353
+ const imports = /* @__PURE__ */ new Map();
3354
+ const storyImports = /* @__PURE__ */ new Map();
3355
+ const globalVars = {};
3356
+ const get = (m, k) => m.get(k) ?? m.set(k, /* @__PURE__ */ new Set()).get(k);
3357
+ for (const f of files) {
3358
+ const path = norm2(f.path);
3359
+ const id = componentForPath(path, dir);
3360
+ if (!id) {
3361
+ if (isCss(path)) {
3362
+ const v = cssVarsIn(f.text);
3363
+ if (v.length) globalVars[path] = v.sort();
3364
+ }
3365
+ continue;
3366
+ }
3367
+ const mod = modules[id] ??= { id, dir: `${dir}/${id}`, files: [], vars: [], imports: [], storyImports: [] };
3368
+ mod.files.push(path);
3369
+ if (isTest(path)) continue;
3370
+ for (const v of cssVarsIn(f.text)) get(vars, id).add(v);
3371
+ if (!isScript(path)) continue;
3372
+ for (const spec of importsIn(f.text)) {
3373
+ const dep = componentOf(path, spec, dir);
3374
+ if (!dep || dep === id) continue;
3375
+ get(isStory(path) ? storyImports : imports, id).add(dep);
3376
+ }
3377
+ }
3378
+ const byVar = {};
3379
+ for (const mod of Object.values(modules)) {
3380
+ mod.files.sort();
3381
+ mod.vars = [...vars.get(mod.id) ?? []].sort();
3382
+ mod.imports = [...imports.get(mod.id) ?? []].sort();
3383
+ mod.storyImports = [...storyImports.get(mod.id) ?? []].filter((d) => !mod.imports.includes(d)).sort();
3384
+ for (const v of mod.vars) (byVar[v] ??= []).push(mod.id);
3385
+ }
3386
+ for (const k of Object.keys(byVar)) byVar[k].sort();
3387
+ return { componentsDir: dir, modules, byVar, globalVars };
3388
+ }
3389
+ function impactedModules(index, input) {
3390
+ const reasons = /* @__PURE__ */ new Map();
3391
+ const add = (id, reason, indirect) => {
3392
+ const r = reasons.get(id);
3393
+ if (r) {
3394
+ if (!r.reasons.includes(reason)) r.reasons.push(reason);
3395
+ r.indirect &&= indirect;
3396
+ } else reasons.set(id, { reasons: [reason], indirect });
3397
+ };
3398
+ const changedVars = new Set(input.vars ?? []);
3399
+ for (const v of changedVars) for (const id of index.byVar[v] ?? []) add(id, `uses ${v}`, false);
3400
+ for (const f of input.files ?? []) {
3401
+ const id = componentForPath(f, index.componentsDir);
3402
+ if (id && index.modules[id]) add(id, `file changed: ${norm2(f)}`, false);
3403
+ }
3404
+ const renderedBy = /* @__PURE__ */ new Map();
3405
+ for (const m of Object.values(index.modules)) for (const d of m.imports) (renderedBy.get(d) ?? renderedBy.set(d, []).get(d)).push(m.id);
3406
+ const queue = [...reasons.keys()];
3407
+ while (queue.length) {
3408
+ const id = queue.shift();
3409
+ for (const parent of renderedBy.get(id) ?? []) {
3410
+ const known = reasons.has(parent);
3411
+ add(parent, `renders ${id}`, true);
3412
+ if (!known) queue.push(parent);
3413
+ }
3414
+ }
3415
+ const globalHits = Object.entries(index.globalVars).map(([file, vs]) => ({ file, vars: vs.filter((v) => changedVars.has(v)) })).filter((g) => g.vars.length > 0);
3416
+ const modules = [...reasons.entries()].map(([id, r]) => ({ id, reasons: r.reasons, indirect: r.indirect })).sort((a, b) => Number(a.indirect) - Number(b.indirect) || a.id.localeCompare(b.id));
3417
+ return { modules, globalHits };
3418
+ }
3419
+
2906
3420
  // ../core/src/explorations.ts
2907
3421
  var EXPLORATION_USE_CASES = [
2908
3422
  { value: "user-test", label: "Prototype for a user test", hint: "A coded prototype on a product staging branch, with the real components and tokens." },
@@ -2917,7 +3431,8 @@ var EXPLORATION_STATUS_LABEL = { active: "Running", promoted: "Promoted to revie
2917
3431
  var REQUEST_KINDS = [
2918
3432
  { value: "new-component", label: "New component", hint: "A component the design system does not have yet." },
2919
3433
  { value: "new-variant", label: "New variant", hint: "A size, tone or state the component does not have yet." },
2920
- { value: "prop-change", label: "Prop change", hint: "Add, rename or change a prop or its default." },
3434
+ { value: "prop-change", label: "Option change (prop)", hint: "Add, rename or change an option (a prop in code) or its default." },
3435
+ { value: "structure", label: "Structure change", hint: "A part was added, removed or moved." },
2921
3436
  { value: "visual", label: "Visual change", hint: "Spacing, radius, layout or icon placement." },
2922
3437
  { value: "token-binding", label: "Token change", hint: "The component should paint with another token." },
2923
3438
  { value: "bug", label: "Bug", hint: "It looks or behaves differently from the guidance." },
@@ -2949,6 +3464,7 @@ function jiraIssueFields(r, jira, issueUrl) {
2949
3464
  version: 1,
2950
3465
  content: [
2951
3466
  para(text(r.summary.trim() || r.title)),
3467
+ ...r.line?.trim() ? [para(text(r.line.trim()))] : [],
2952
3468
  ...r.changes.map((c) => para(text(`${c.field}: ${c.before ?? "(none)"} -> ${c.after ?? "(removed)"}`))),
2953
3469
  para({ type: "text", text: "GitHub request", marks: [{ type: "link", attrs: { href: issueUrl } }] })
2954
3470
  ]
@@ -3213,14 +3729,14 @@ function union(a, b) {
3213
3729
  // ../visual/src/manifest.ts
3214
3730
  var COMMENT_MARKER = "<!-- bridge-visual-impact -->";
3215
3731
  function summarize(shots) {
3216
- const count = (s) => shots.filter((x) => x.status === s).length;
3732
+ const count2 = (s) => shots.filter((x) => x.status === s).length;
3217
3733
  return {
3218
3734
  shots: shots.length,
3219
- changed: count("changed"),
3220
- unchanged: count("unchanged"),
3221
- added: count("added"),
3222
- removed: count("removed"),
3223
- errors: count("error"),
3735
+ changed: count2("changed"),
3736
+ unchanged: count2("unchanged"),
3737
+ added: count2("added"),
3738
+ removed: count2("removed"),
3739
+ errors: count2("error"),
3224
3740
  maxPercent: shots.reduce((m, s) => Math.max(m, s.percent), 0)
3225
3741
  };
3226
3742
  }
@@ -3356,17 +3872,17 @@ var shotKey = (...parts) => parts.map((p) => p.toLowerCase().replace(/[^a-z0-9-]
3356
3872
 
3357
3873
  // ../visual/src/select.ts
3358
3874
  var NO_VISUAL = /(\.test\.[cm]?[jt]sx?|\.spec\.[cm]?[jt]sx?|\.docs\.json|\.mdx?|README[^/]*)$/i;
3359
- var norm2 = (p) => p.replace(/^\.\//, "").replace(/\\/g, "/");
3875
+ var norm3 = (p) => p.replace(/^\.\//, "").replace(/\\/g, "/");
3360
3876
  var isAllStates = (s) => /--all-states$/.test(s.id) || /^all ?states$/i.test(s.name);
3361
3877
  function storyComponent(s, componentsDir) {
3362
- return componentForPath(norm2(s.importPath), componentsDir);
3878
+ return componentForPath(norm3(s.importPath), componentsDir);
3363
3879
  }
3364
3880
  function selectImpact(input) {
3365
3881
  const { usage, stories } = input;
3366
3882
  const dir = usage.componentsDir;
3367
3883
  const lib = input.librarySrc.replace(/\/$/, "");
3368
3884
  const cap = input.maxStoriesPerComponent ?? 4;
3369
- const visualFiles = input.files.map(norm2).filter((f) => !NO_VISUAL.test(f));
3885
+ const visualFiles = input.files.map(norm3).filter((f) => !NO_VISUAL.test(f));
3370
3886
  const vars = [...new Set(input.tokens.map((t) => input.cssVar(t.path)))].sort();
3371
3887
  const impact2 = impactedModules(usage, { vars, files: visualFiles });
3372
3888
  let fallback = null;
@@ -3406,7 +3922,7 @@ function selectImpact(input) {
3406
3922
  return {
3407
3923
  tokens: input.tokens,
3408
3924
  vars,
3409
- files: input.files.map(norm2),
3925
+ files: input.files.map(norm3),
3410
3926
  components: components.map((c) => ({ id: c.id, reasons: c.reasons, indirect: c.indirect })),
3411
3927
  stories: [...new Set(picked)],
3412
3928
  fallback,
@@ -3425,7 +3941,7 @@ function storiesFromSource(files) {
3425
3941
  if (!title) continue;
3426
3942
  for (const m of f.text.matchAll(/export\s+const\s+([A-Z]\w*)\s*[:=]/g)) {
3427
3943
  const name = m[1];
3428
- out.push({ id: storyId(title, name), title, name: name.replace(/([a-z0-9])([A-Z])/g, "$1 $2"), importPath: `./${norm2(f.path)}` });
3944
+ out.push({ id: storyId(title, name), title, name: name.replace(/([a-z0-9])([A-Z])/g, "$1 $2"), importPath: `./${norm3(f.path)}` });
3429
3945
  }
3430
3946
  }
3431
3947
  return out;
@@ -4232,8 +4748,9 @@ Usage:
4232
4748
  GitHub token: the branch and the draft pull request on GitHub. Without one, in a git repo:
4233
4749
  a local branch only, nothing pushed
4234
4750
  bridge explore list [--all] [--json]
4235
- running and promoted explorations, with previews, the snapshot install command and the
4236
- requests implemented on each (--all: closed ones too)
4751
+ running and promoted explorations, with the preview links Vercel reports, the install
4752
+ command from the pull request (once the snapshot is built) and the requests implemented
4753
+ on each (--all: closed ones too)
4237
4754
  bridge explore promote <name|#pr> [--dry-run] [--json]
4238
4755
  ready for review, tier labels, exploration label and note removed (never merges)
4239
4756
  bridge explore close <name|#pr> [--json]
@@ -4249,6 +4766,7 @@ async function context2(root, io) {
4249
4766
  const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
4250
4767
  return BridgeContext3.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
4251
4768
  }
4769
+ var exploreSource = (env) => ({ source: "cli", ...env.CLAUDECODE ? { agent: "claude" } : {} });
4252
4770
  var date = (iso) => iso ? iso.slice(0, 10) : "";
4253
4771
  async function runExplore(a, root, io = defaultIO()) {
4254
4772
  const sub = a._[1];
@@ -4264,7 +4782,7 @@ async function runExplore(a, root, io = defaultIO()) {
4264
4782
  if (!name || !purpose) throw new RequestUsageError('Usage: bridge explore start <name> --purpose "What you want to learn"');
4265
4783
  const useCase = flag2(a, "use-case");
4266
4784
  const ctx = await context2(root, io);
4267
- const r = await explorations.startExploration(ctx, { name, purpose, ...useCase ? { useCase } : {}, ...a.flags.experimental ? { experimental: a.flags.experimental } : {}, dryRun: has2(a, "dry-run") });
4785
+ 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") });
4268
4786
  if (json) return io.print(JSON.stringify(r, null, 2)), 0;
4269
4787
  if (r.status === "dry-run") {
4270
4788
  io.print(`Dry run, nothing written.
@@ -4279,8 +4797,7 @@ ${r.prBody}`);
4279
4797
  if (r.status === "started") {
4280
4798
  io.print(`Started ${r.branch}: draft pull request #${r.pr} ${r.url}`);
4281
4799
  io.print(`
4282
- Install it in a product staging branch:
4283
- ${r.install.command}`);
4800
+ ${r.install.note}`);
4284
4801
  for (const p of r.previews ?? []) io.print(`${p.label}: ${p.url}`);
4285
4802
  } else io.print(`Created the local branch ${r.branch} (${String(r.commit).slice(0, 7)}).`);
4286
4803
  io.print(`
@@ -4297,8 +4814,8 @@ Next: ${r.next}`);
4297
4814
  #${x.number} ${x.name} ${EXPLORATION_STATUS_LABEL[x.status]} ${x.branch} @${x.author} ${date(x.updatedAt)}`);
4298
4815
  if (x.purpose) io.print(` ${x.purpose}`);
4299
4816
  io.print(` ${x.url}`);
4300
- for (const p of x.previews) io.print(` ${p.label}: ${p.url}`);
4301
- if (x.install.packages.length) io.print(` Install: ${x.install.command}`);
4817
+ for (const p of x.previews) io.print(` ${p.label}: ${p.url ? `${p.url}${p.state === "building" ? " (building)" : p.state === "failed" ? " (build failed)" : ""}` : "link not reported by Vercel yet, see the pull request"}`);
4818
+ if (x.install) io.print(x.install.command ? ` Install: ${x.install.command}` : ` Install: ${x.install.note}`);
4302
4819
  if (x.requests.length) io.print(` Requests: ${x.requests.map((q) => `#${q.number}`).join(", ")}`);
4303
4820
  }
4304
4821
  for (const b of r.localBranches ?? []) io.print(`
@@ -4335,13 +4852,50 @@ ${EXPLORE_HELP}`);
4335
4852
  }
4336
4853
  }
4337
4854
 
4855
+ // src/protection.ts
4856
+ var UNKNOWN = { kind: "unknown", missing: ["review", "checks", "history", "deletion"] };
4857
+ async function protectionReport(opts) {
4858
+ let state = UNKNOWN;
4859
+ try {
4860
+ const client = new GitHubClient(opts.repo, opts.token ?? "", opts.fetch, { attempts: 1, baseDelayMs: 0, sleep: async () => {
4861
+ } });
4862
+ state = await loadProtection(client, opts.defaultBranch);
4863
+ } catch {
4864
+ state = UNKNOWN;
4865
+ }
4866
+ const [owner, name] = opts.repo.split("/");
4867
+ const lines = [`protection: ${protectionSentence(state)}`];
4868
+ if (state.kind === "protected" && state.url) lines.push(` ${state.url}`);
4869
+ if (state.kind === "unprotected" || state.kind === "partial") lines.push(` Protect it in Studio > Settings, or GitHub settings > Rules: ${rulesSettingsUrl(owner, name)}`);
4870
+ return { state, lines };
4871
+ }
4872
+
4873
+ // src/roles.ts
4874
+ async function rolesReport(opts) {
4875
+ if (!teamsIn(opts.owners).length) return { teams: [], lines: [] };
4876
+ const client = new GitHubClient(opts.repo, opts.token ?? "", opts.fetch, { attempts: 1, baseDelayMs: 0, sleep: async () => {
4877
+ } });
4878
+ const states = await new TeamDirectory(client).resolve(opts.owners);
4879
+ const teams = Object.values(states);
4880
+ const group = (entries) => entries.length ? entries.map((e) => entryLabel(e, states)).join(", ") : "nobody listed";
4881
+ const lines = [`roles: design owners ${group(opts.owners.design)}; tech owners ${group(opts.owners.tech)}`];
4882
+ for (const t of teams) if (t.status === "unreadable") lines.push(` warn team-unreadable ${t.message}`);
4883
+ return { teams, lines };
4884
+ }
4885
+
4338
4886
  // src/index.ts
4339
4887
  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";
4888
+ var MCP_PACKAGE = "@bridge-ds/mcp";
4340
4889
  var HELP = `bridge ${VERSION}: design system as code, for you and your Claude
4341
4890
 
4342
4891
  Usage:
4343
4892
  bridge init [--force] write bridge.config.json (detected) and .mcp.json for Claude Code
4344
- bridge check [--json] [--strict] validate tokens + contrast gate. Exit 0 ok, 1 failures, 2 setup error
4893
+ bridge check [--json] [--strict] [--offline]
4894
+ validate tokens + contrast gate, and say whether the library is
4895
+ protected (reviewed pull requests only) and who is in the GitHub
4896
+ teams named as owners, reporting teams it cannot read (neither
4897
+ changes the exit code; --offline skips both). Exit 0 ok, 1 failures,
4898
+ 2 setup error
4345
4899
  bridge tokens <query> [--json] search tokens by name, description or value
4346
4900
  bridge diff <base> [--json|--md] designer-readable token diff: working tree vs <base> (git ref)
4347
4901
  bridge propose --title "..." --set path=value [--set ...] [--file f | --mode theme=dark]
@@ -4408,7 +4962,7 @@ function parse(argv) {
4408
4962
  const [k, inline] = a.slice(2).split(/=(.*)/s, 2);
4409
4963
  const next = argv[i + 1];
4410
4964
  const fileArg = k === "json" && /\.json$/i.test(next ?? "") || k === "md" && /\.(md|markdown)$/i.test(next ?? "");
4411
- 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"].includes(k));
4965
+ 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));
4412
4966
  (out.flags[k] ??= []).push(inline ?? (takesValue ? argv[++i] : "true"));
4413
4967
  } else out._.push(a);
4414
4968
  }
@@ -4437,18 +4991,19 @@ async function init(a, root) {
4437
4991
  }
4438
4992
  const serverJs = fileURLToPath(new URL("./index.js", import.meta.resolve("@bridge-ds/mcp")));
4439
4993
  const relServer = relative4(root, serverJs);
4440
- const inRepo = !relServer.startsWith("..");
4994
+ const inRepo = !relServer.startsWith("..") && !isAbsolute(relServer);
4995
+ const server = (dir) => inRepo ? { command: "node", args: [relServer, "--root", dir] } : { command: "npx", args: ["-y", MCP_PACKAGE, "--root", dir] };
4441
4996
  const mcpPath = join5(root, ".mcp.json");
4442
4997
  const existing = existsSync5(mcpPath) ? JSON.parse(await readFile5(mcpPath, "utf8")) : {};
4443
- existing.mcpServers = { ...existing.mcpServers ?? {}, bridge: { command: "node", args: [inRepo ? relServer : serverJs, "--root", "."] } };
4998
+ existing.mcpServers = { ...existing.mcpServers ?? {}, bridge: server(".") };
4444
4999
  await writeFile4(mcpPath, `${JSON.stringify(existing, null, 2)}
4445
5000
  `);
4446
5001
  print(`Wrote .mcp.json: Claude Code picks up the "bridge" server next time it starts in ${root}.`);
5002
+ const desktop = inRepo ? { command: "node", args: [serverJs, "--root", root] } : server(root);
4447
5003
  print("\nClaude Desktop: add this to claude_desktop_config.json (Settings > Developer > Edit config):");
4448
- print(JSON.stringify({ mcpServers: { bridge: { command: "node", args: [serverJs, "--root", root] } } }, null, 2));
5004
+ print(JSON.stringify({ mcpServers: { bridge: desktop } }, null, 2));
4449
5005
  print("\nOr with the Claude Code CLI:");
4450
- print(` claude mcp add bridge -- node ${serverJs} --root ${root}`);
4451
- print("(Once @bridge-ds/mcp is published: claude mcp add bridge -- npx -y @bridge-ds/mcp)");
5006
+ print(` claude mcp add bridge -- ${desktop.command} ${desktop.args.join(" ")}`);
4452
5007
  }
4453
5008
  async function check(a, root) {
4454
5009
  const ctx = await BridgeContext4.create({ root });
@@ -4456,8 +5011,10 @@ async function check(a, root) {
4456
5011
  const c = await ops.checkContrast(ctx, {});
4457
5012
  const warnings = "warnings" in v ? v.warnings ?? [] : [];
4458
5013
  const failed = !v.valid || "failing" in c && (c.failing ?? 0) > 0 || has3(a, "strict") && warnings.length > 0;
5014
+ const protection = await protectionOf(a, root, ctx);
5015
+ const roles = await rolesOf(a, root, ctx);
4459
5016
  if (has3(a, "json")) {
4460
- print(JSON.stringify({ ok: !failed, validation: v, contrast: c }, null, 2));
5017
+ print(JSON.stringify({ ok: !failed, validation: v, contrast: c, ...protection ? { protection: protection.state } : {}, ...roles?.teams.length ? { teams: roles.teams } : {} }, null, 2));
4461
5018
  return failed ? 1 : 0;
4462
5019
  }
4463
5020
  print(`tokens: ${v.tokens} in ${v.files} files, ${v.modeCombinations} mode combinations`);
@@ -4469,9 +5026,27 @@ async function check(a, root) {
4469
5026
  const tight = c.tightest?.[0];
4470
5027
  if (tight) print(` tightest: ${tight.pair} [${tight.modes}] ${tight.ratio}:1 (min ${tight.min})`);
4471
5028
  } else print(`contrast: ${c.message}`);
5029
+ for (const line of protection?.lines ?? []) print(line);
5030
+ for (const line of roles?.lines ?? []) print(line);
4472
5031
  print(failed ? "FAIL" : "OK");
4473
5032
  return failed ? 1 : 0;
4474
5033
  }
5034
+ async function protectionOf(a, root, ctx) {
5035
+ if (has3(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
5036
+ const { config } = await ctx.config();
5037
+ const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
5038
+ if (!repo) return void 0;
5039
+ const token = process.env.BRIDGE_GITHUB_TOKEN || process.env.GITHUB_TOKEN || void 0;
5040
+ return protectionReport({ repo, defaultBranch: config.repo?.defaultBranch ?? "main", token });
5041
+ }
5042
+ async function rolesOf(a, root, ctx) {
5043
+ if (has3(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
5044
+ const { config } = await ctx.config();
5045
+ const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
5046
+ if (!repo) return void 0;
5047
+ const token = process.env.BRIDGE_GITHUB_TOKEN || process.env.GITHUB_TOKEN || void 0;
5048
+ return rolesReport({ repo, owners: config.governance.owners, token });
5049
+ }
4475
5050
  async function tokens(a, root) {
4476
5051
  const q = a._.slice(1).join(" ");
4477
5052
  if (!q) throw new UsageError("bridge tokens <query>");
@@ -4834,7 +5409,7 @@ async function main() {
4834
5409
  if (cmd === "explore") return runExplore(a, root);
4835
5410
  if (!cmd || has3(a, "help") || cmd === "help") {
4836
5411
  print(HELP);
4837
- return cmd ? 0 : 2;
5412
+ return cmd || has3(a, "help") ? 0 : 2;
4838
5413
  }
4839
5414
  if (cmd === "--version" || cmd === "version") return print(VERSION), 0;
4840
5415
  switch (cmd) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge-ds/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
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.2.0",
47
- "@bridge-ds/visual": "^0.2.0",
46
+ "@bridge-ds/mcp": "^0.3.1",
47
+ "@bridge-ds/visual": "^0.2.2",
48
48
  "typescript": "5.9.3"
49
49
  },
50
50
  "devDependencies": {