@bridge-ds/cli 0.9.0 → 0.11.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 +1314 -116
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { existsSync as existsSync6 } from "node:fs";
4
+ import { existsSync as existsSync8 } from "node:fs";
5
5
  import { readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
6
- import { isAbsolute, join as join6, relative as relative4, resolve as resolve5 } from "node:path";
6
+ import { isAbsolute, join as join8, relative as relative5, resolve as resolve6 } from "node:path";
7
7
 
8
8
  // ../core/src/audit/types.ts
9
9
  var AUDIT_SCHEMA_VERSION = 1;
@@ -1798,8 +1798,8 @@ function scanMarkup(path, text, lang, ds) {
1798
1798
  out.themeRefs += r.themeRefs;
1799
1799
  }
1800
1800
  const scanExpr = (prefix, expr, offset, suffix, keepLookalikes = false) => {
1801
- const { line, column } = pos(offset);
1802
- const src = line >= 2 ? `${"\n".repeat(line - 2)}${prefix}
1801
+ const { line: line2, column } = pos(offset);
1802
+ const src = line2 >= 2 ? `${"\n".repeat(line2 - 2)}${prefix}
1803
1803
  ${" ".repeat(column - 1)}${expr}${suffix}` : `${prefix}${expr}${suffix}`;
1804
1804
  const c = scanCode(`${path}.tsx`, src, ds);
1805
1805
  merge({ ...c, components: keepLookalikes ? c.components.filter((x) => x.kind === "recreated" && x.name.startsWith(".")) : [], definitions: [], ds: {} });
@@ -2445,7 +2445,7 @@ var DEFAULT_GOVERNANCE = {
2445
2445
  owners: { design: [], tech: [] }
2446
2446
  };
2447
2447
  var FLAT_TOKEN_KEYS = ["primitives", "brands", "semantic", "base", "density", "components", "contrastChecks"];
2448
- var CANONICAL_TOKEN_KEYS = ["root", "include", "exclude", "tiers", "layers"];
2448
+ var CANONICAL_TOKEN_KEYS = ["root", "include", "exclude", "tiers", "layers", "usage"];
2449
2449
  var FLAT_MODE_KEYS = ["brands", "themes", "densities", "defaults"];
2450
2450
  var AXIS_OF = { brands: "brand", themes: "theme", densities: "density" };
2451
2451
  var ConfigError = class extends Error {
@@ -2533,7 +2533,7 @@ function defineConfig(input) {
2533
2533
  const contrastFile = contrastPath ?? [root === "." ? "" : root, paths.contrastChecks].filter(Boolean).join("/");
2534
2534
  return {
2535
2535
  repo: parseRepoInput(input.repo, input.branch),
2536
- tokens: { ...paths, include: input.tokens?.include ?? ["**/*.tokens.json"], exclude: input.tokens?.exclude ?? [], tiers, layers },
2536
+ tokens: { ...paths, include: input.tokens?.include ?? ["**/*.tokens.json"], exclude: input.tokens?.exclude ?? [], tiers, layers, ...t.usage ? { usage: trimSlashes(t.usage) } : {} },
2537
2537
  axes,
2538
2538
  modes,
2539
2539
  contrast: { path: contrastFile, modes: input.contrast?.modes },
@@ -2568,6 +2568,7 @@ function defineConfig(input) {
2568
2568
  products: input.products?.length ? input.products.map((p) => ({ ...p })) : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
2569
2569
  changes: input.changes === "direct" ? "direct" : DEFAULT_CHANGES_MODE,
2570
2570
  ...input.storybook?.url ? { storybook: { url: input.storybook.url.trim().replace(/\/+$/, "") } } : {},
2571
+ ...input.library?.public === true ? { library: { public: true } } : {},
2571
2572
  ...input.audit ? { audit: { ...input.audit } } : {},
2572
2573
  ...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
2573
2574
  ...input.integrations ? { integrations: { ...input.integrations } } : {},
@@ -2612,7 +2613,7 @@ function loadConfig(source) {
2612
2613
  if (!isObj(tokens2)) problems.push('"tokens" must be an object');
2613
2614
  else
2614
2615
  for (const [k, v] of Object.entries(tokens2)) {
2615
- if (FLAT_TOKEN_KEYS.includes(k) || k === "root") {
2616
+ if (FLAT_TOKEN_KEYS.includes(k) || k === "root" || k === "usage") {
2616
2617
  if (!isStr(v)) problems.push(`"tokens.${k}" must be a non-empty string`);
2617
2618
  } else if (k === "include" || k === "exclude") {
2618
2619
  if (!isStrList(v)) problems.push(`"tokens.${k}" must be a list of globs`);
@@ -2698,6 +2699,14 @@ function loadConfig(source) {
2698
2699
  else if (!/^https:\/\/[^\s/]+/.test(sb.url.trim())) problems.push('"storybook.url" must start with https://, for example https://storybook.example.com');
2699
2700
  else for (const k of Object.keys(sb)) if (k !== "url") problems.push(`"storybook.${k}" is not a known key`);
2700
2701
  }
2702
+ const lib = d.library;
2703
+ if (lib !== void 0) {
2704
+ if (!isObj(lib)) problems.push('"library" must be an object, for example { "public": true }');
2705
+ else {
2706
+ if (lib.public !== void 0 && typeof lib.public !== "boolean") problems.push('"library.public" must be true or false');
2707
+ for (const k of Object.keys(lib)) if (k !== "public") problems.push(`"library.${k}" is not a known key`);
2708
+ }
2709
+ }
2701
2710
  problems.push(...productProblems(d.products));
2702
2711
  const audit2 = d.audit;
2703
2712
  if (audit2 !== void 0) {
@@ -2727,6 +2736,7 @@ function loadConfig(source) {
2727
2736
  if (problems.length) throw new ConfigError(problems);
2728
2737
  return defineConfig(d);
2729
2738
  }
2739
+ var tokensPath = (config, rel = "") => [config.tokens.root === "." ? "" : config.tokens.root, rel].filter(Boolean).join("/");
2730
2740
 
2731
2741
  // ../core/src/node/git-local.ts
2732
2742
  import { execFile } from "node:child_process";
@@ -2752,10 +2762,10 @@ function assertSafeBranch(branch, rules = {}) {
2752
2762
 
2753
2763
  // ../core/src/node/git-local.ts
2754
2764
  function git(root, args, opts = {}) {
2755
- return new Promise((resolve6, reject) => {
2765
+ return new Promise((resolve7, reject) => {
2756
2766
  const child = execFile("git", args, { cwd: root, env: { ...process.env, ...opts.env }, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
2757
2767
  if (err) reject(new Error(`git ${args.join(" ")}: ${stderr || err.message}`.trim()));
2758
- else resolve6(stdout);
2768
+ else resolve7(stdout);
2759
2769
  });
2760
2770
  if (opts.input !== void 0) child.stdin.end(opts.input);
2761
2771
  });
@@ -3034,6 +3044,44 @@ function classifyAuth(status, detail, opts = {}) {
3034
3044
  }
3035
3045
  return {};
3036
3046
  }
3047
+ function memoryEtagStore(max = 500) {
3048
+ const map = /* @__PURE__ */ new Map();
3049
+ return {
3050
+ get(key) {
3051
+ const e = map.get(key);
3052
+ if (e) {
3053
+ map.delete(key);
3054
+ map.set(key, e);
3055
+ }
3056
+ return e;
3057
+ },
3058
+ set(key, entry) {
3059
+ map.delete(key);
3060
+ map.set(key, entry);
3061
+ while (map.size > max) map.delete(map.keys().next().value);
3062
+ },
3063
+ clear: () => map.clear()
3064
+ };
3065
+ }
3066
+ var sharedEtags = memoryEtagStore();
3067
+ function tokenFingerprint(token) {
3068
+ if (!token) return "anon";
3069
+ let a = 2166136261;
3070
+ let b = 16777619 ^ token.length;
3071
+ for (let i = 0; i < token.length; i++) {
3072
+ const c = token.charCodeAt(i);
3073
+ a = Math.imul(a ^ c, 16777619) >>> 0;
3074
+ b = Math.imul(b ^ c ^ i & 255, 1540483477) >>> 0;
3075
+ }
3076
+ return a.toString(16).padStart(8, "0") + b.toString(16).padStart(8, "0");
3077
+ }
3078
+ function installationFor(list2, owner) {
3079
+ const inst = list2.find((i) => i.account?.login?.toLowerCase() === owner.toLowerCase());
3080
+ if (!inst) return { reach: "no" };
3081
+ const selection = inst.repositorySelection ?? inst.repository_selection;
3082
+ return selection === "all" ? { reach: "yes", id: inst.id } : { reach: "check", id: inst.id };
3083
+ }
3084
+ var MAX_PAGES = 10;
3037
3085
  function rateLimitMessage(x) {
3038
3086
  const when = x.resetsAt ? ` It resets at ${x.resetsAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : "";
3039
3087
  if (x.withToken) return `GitHub's hourly request limit for this token is used up.${when} Try again after that.`;
@@ -3041,8 +3089,11 @@ function rateLimitMessage(x) {
3041
3089
  return `GitHub's hourly limit for requests without a token (60 an hour) is used up.${when} To keep going now, ${hint}: a token gets 5,000 requests an hour.`;
3042
3090
  }
3043
3091
  var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
3092
+ var hourlyLimitErrors = /* @__PURE__ */ new WeakSet();
3044
3093
  var isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
3045
3094
  var API = "https://api.github.com";
3095
+ var ACCEPT_JSON = "application/vnd.github+json";
3096
+ var ACCEPT_RAW = "application/vnd.github.raw";
3046
3097
  var RAW = "https://raw.githubusercontent.com";
3047
3098
  function parseRepo(repo) {
3048
3099
  const m = /^\s*(?:https?:\/\/github\.com\/|git@github\.com:)?([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?\/?\s*$/.exec(repo);
@@ -3061,6 +3112,7 @@ function base64ToUtf8(b64) {
3061
3112
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
3062
3113
  return new TextDecoder("utf-8").decode(bytes);
3063
3114
  }
3115
+ var copy = (value) => value === null || typeof value !== "object" ? value : typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
3064
3116
  var encodeRef = (ref) => ref.split("/").map(encodeURIComponent).join("/");
3065
3117
  var GitHubClient = class _GitHubClient {
3066
3118
  constructor(repo, token, fetchImpl = (url, init2) => fetch(url, init2), retry = DEFAULT_RETRY, options = {}) {
@@ -3073,6 +3125,8 @@ var GitHubClient = class _GitHubClient {
3073
3125
  this.name = r.name;
3074
3126
  this.api = (options.apiUrl ?? API).replace(/\/$/, "");
3075
3127
  this.protectedBranches = options.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES;
3128
+ this.etags = options.etags === false ? void 0 : options.etags ?? sharedEtags;
3129
+ this.etagScope = options.etagScope ?? tokenFingerprint(token);
3076
3130
  }
3077
3131
  token;
3078
3132
  fetchImpl;
@@ -3083,6 +3137,15 @@ var GitHubClient = class _GitHubClient {
3083
3137
  blobCache = /* @__PURE__ */ new Map();
3084
3138
  api;
3085
3139
  protectedBranches;
3140
+ etags;
3141
+ etagScope;
3142
+ rate;
3143
+ /** What the conditional request cache did for this client. */
3144
+ etagStats = { conditional: 0, notModified: 0, stale: 0 };
3145
+ /** GitHub's rate-limit headers from the last API response, or undefined before any response carried them. */
3146
+ get rateLimit() {
3147
+ return this.rate;
3148
+ }
3086
3149
  /** "owner/name". */
3087
3150
  get fullName() {
3088
3151
  return `${this.owner}/${this.name}`;
@@ -3106,45 +3169,77 @@ var GitHubClient = class _GitHubClient {
3106
3169
  get repoPath() {
3107
3170
  return `/repos/${this.owner}/${this.name}`;
3108
3171
  }
3109
- async request(method, path, body, retryable = method === "GET") {
3172
+ async request(method, path, body, retryable = method === "GET", opts = {}) {
3110
3173
  const attempts = retryable ? Math.max(1, this.retry.attempts) : 1;
3111
3174
  for (let attempt = 1; ; attempt++) {
3112
3175
  try {
3113
- return await this.once(method, path, body);
3176
+ return await this.once(method, path, body, opts);
3114
3177
  } catch (err) {
3115
3178
  const e = err;
3116
- if (attempt >= attempts || !(e instanceof GitHubError) || !isTransient(e.status, e.detail ?? "")) throw err;
3179
+ if (attempt >= attempts || !(e instanceof GitHubError) || hourlyLimitErrors.has(e) || !isTransient(e.status, e.detail ?? "")) throw err;
3117
3180
  await this.retry.sleep(this.retry.baseDelayMs * 2 ** (attempt - 1));
3118
3181
  }
3119
3182
  }
3120
3183
  }
3121
- async once(method, path, body) {
3184
+ noteRateLimit(headers) {
3185
+ const remaining = headers?.get("x-ratelimit-remaining");
3186
+ if (remaining == null) return;
3187
+ const limit = headers?.get("x-ratelimit-limit");
3188
+ const reset = headers?.get("x-ratelimit-reset");
3189
+ this.rate = {
3190
+ limit: limit != null ? Number(limit) : void 0,
3191
+ remaining: Number(remaining),
3192
+ resetsAt: reset ? new Date(Number(reset) * 1e3) : void 0,
3193
+ resource: headers?.get("x-ratelimit-resource") ?? void 0
3194
+ };
3195
+ }
3196
+ async once(method, path, body, opts = {}) {
3197
+ const accept = opts.accept ?? ACCEPT_JSON;
3122
3198
  const headers = {
3123
- Accept: "application/vnd.github+json",
3199
+ Accept: accept,
3124
3200
  "X-GitHub-Api-Version": "2022-11-28"
3125
3201
  };
3126
3202
  if (this.token) headers.Authorization = `Bearer ${this.token}`;
3127
3203
  if (body !== void 0) headers["Content-Type"] = "application/json";
3204
+ const key = method === "GET" && this.etags && opts.etag !== false ? `${this.etagScope} ${accept} ${this.api}${path}` : void 0;
3205
+ const cached = key ? this.etags.get(key) : void 0;
3206
+ if (cached && this.token) {
3207
+ headers["If-None-Match"] = cached.etag;
3208
+ this.etagStats.conditional++;
3209
+ }
3128
3210
  let res;
3129
3211
  try {
3130
3212
  res = await this.fetchImpl(`${this.api}${path}`, { method, headers, ...body !== void 0 ? { body: JSON.stringify(body) } : {} });
3131
3213
  } catch (err) {
3214
+ if (err instanceof GitHubError) throw err;
3132
3215
  throw new GitHubError(0, "Could not reach GitHub. Check your connection, then try again.", err.message);
3133
3216
  }
3217
+ this.noteRateLimit(res.headers);
3218
+ if (res.status === 304 && cached && key && this.token) {
3219
+ this.etagStats.notModified++;
3220
+ this.etags.set(key, { ...cached, at: Date.now() });
3221
+ return copy(cached.body);
3222
+ }
3134
3223
  if (res.status === 204) return void 0;
3135
3224
  if (!res.ok) {
3136
3225
  let detail = "";
3137
3226
  try {
3138
- const data = await res.json();
3139
- detail = [data.message, ...(data.errors ?? []).map((e) => typeof e === "string" ? e : e.message)].filter(Boolean).join(" ");
3227
+ const data2 = await res.json();
3228
+ detail = [data2.message, ...(data2.errors ?? []).map((e) => typeof e === "string" ? e : e.message)].filter(Boolean).join(" ");
3140
3229
  } catch {
3141
3230
  detail = "";
3142
3231
  }
3143
3232
  const remaining = res.headers?.get("x-ratelimit-remaining");
3144
3233
  const reset = res.headers?.get("x-ratelimit-reset");
3145
3234
  if ((res.status === 403 || res.status === 429) && remaining === "0") {
3235
+ if (cached && this.options.serveStaleOnRateLimit) {
3236
+ this.etagStats.stale++;
3237
+ return copy(cached.body);
3238
+ }
3146
3239
  const resetsAt = reset ? new Date(Number(reset) * 1e3) : void 0;
3147
- throw new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail);
3240
+ const limited = new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail);
3241
+ hourlyLimitErrors.add(limited);
3242
+ throw limited;
3148
3243
  }
3149
3244
  const info = classifyAuth(res.status, detail, {
3150
3245
  path,
@@ -3153,7 +3248,24 @@ var GitHubClient = class _GitHubClient {
3153
3248
  });
3154
3249
  throw new GitHubError(res.status, friendlyMessage(res.status, detail, info), detail, info);
3155
3250
  }
3156
- return await res.json();
3251
+ const data = opts.text ? await res.text() : await res.json();
3252
+ const etag = key ? res.headers?.get("etag") : null;
3253
+ if (key && etag) this.etags.set(key, { etag, body: copy(data), at: Date.now() });
3254
+ return data;
3255
+ }
3256
+ /**
3257
+ * Every item of a paged listing (`per_page=100`), at most 10 pages. `pick` takes the items out of one page (the
3258
+ * installation endpoints wrap them in an object with `total_count`).
3259
+ */
3260
+ async paged(path, pick) {
3261
+ const out = [];
3262
+ const sep3 = path.includes("?") ? "&" : "?";
3263
+ for (let page = 1; page <= MAX_PAGES; page++) {
3264
+ const { items, total } = pick(await this.request("GET", `${path}${sep3}per_page=100&page=${page}`));
3265
+ out.push(...items);
3266
+ if (items.length < 100 || total !== void 0 && out.length >= total) break;
3267
+ }
3268
+ return out;
3157
3269
  }
3158
3270
  async getRepo() {
3159
3271
  const r = await this.request(
@@ -3536,6 +3648,14 @@ var GitHubClient = class _GitHubClient {
3536
3648
  */
3537
3649
  async readFile(path, ref) {
3538
3650
  if (this.token) return this.getFileText(path, ref);
3651
+ return this.readRawFile(path, ref);
3652
+ }
3653
+ /**
3654
+ * Text of a file from the raw host (raw.githubusercontent.com), sent without any header so the browser makes a
3655
+ * simple CORS request (no preflight) and nothing counts against the API limit. Works for public repositories only:
3656
+ * a private file answers 404 there, like a missing one. Undefined on 404.
3657
+ */
3658
+ async readRawFile(path, ref) {
3539
3659
  const url = `${(this.options.rawUrl ?? RAW).replace(/\/$/, "")}/${this.owner}/${this.name}/${encodeRef(ref)}/${encodeRef(path)}`;
3540
3660
  let res;
3541
3661
  try {
@@ -3547,6 +3667,76 @@ var GitHubClient = class _GitHubClient {
3547
3667
  if (!res.ok) throw new GitHubError(res.status, friendlyMessage(res.status, ""), `raw ${path}@${ref}`);
3548
3668
  return res.text();
3549
3669
  }
3670
+ /**
3671
+ * Text of a file at a ref through the contents API with the raw media type: the way to read a private file (the raw
3672
+ * host cannot take the token without a preflight). Counts against the API limit; conditional when cached.
3673
+ * Undefined when the file does not exist.
3674
+ */
3675
+ async getRawText(path, ref) {
3676
+ try {
3677
+ return await this.request("GET", `${this.repoPath}/contents/${encodeRef(path)}?ref=${encodeURIComponent(ref)}`, void 0, true, { accept: ACCEPT_RAW, text: true });
3678
+ } catch (err) {
3679
+ if (err instanceof GitHubError && err.status === 404) return void 0;
3680
+ throw err;
3681
+ }
3682
+ }
3683
+ /**
3684
+ * Text of a blob by its sha through the git blobs API with the raw media type: how a signed-in reader loads a file
3685
+ * listed by a tree (the raw host refuses any request that carries a token). A blob sha names its content, so the
3686
+ * answer is cached for the client's life and never revalidated. Undefined on 404.
3687
+ */
3688
+ async getBlobRaw(sha) {
3689
+ const cached = this.blobCache.get(sha);
3690
+ if (cached !== void 0) return cached;
3691
+ try {
3692
+ const text = await this.request("GET", `${this.repoPath}/git/blobs/${encodeURIComponent(sha)}`, void 0, true, { accept: ACCEPT_RAW, text: true, etag: false });
3693
+ this.blobCache.set(sha, text);
3694
+ return text;
3695
+ } catch (err) {
3696
+ if (err instanceof GitHubError && err.status === 404) return void 0;
3697
+ throw err;
3698
+ }
3699
+ }
3700
+ // --- GitHub App installations (user-to-server tokens) ---------------------------------------
3701
+ /**
3702
+ * Installations of the signing-in GitHub App that the signed-in user can reach (GET /user/installations, every
3703
+ * page). Needs a user-to-server token of that same App: a personal or OAuth token is refused (403), and each App
3704
+ * sees only its own installations. GitHub lists only installations on accounts and repositories the user has
3705
+ * explicit permission on: a public repository where the user is not a collaborator is never listed, installed or
3706
+ * not. (GET /repos/{o}/{r}/installation would say, but it needs the App's private key, which Bridge never holds.)
3707
+ */
3708
+ async listUserInstallations() {
3709
+ this.assertToken("Reading where Bridge is installed");
3710
+ const list2 = await this.paged("/user/installations", (p) => ({ items: p.installations ?? [], total: p.total_count }));
3711
+ return list2.map((i) => ({
3712
+ id: i.id,
3713
+ account: i.account?.login ? { login: i.account.login, type: i.account.type ?? "User" } : null,
3714
+ repositorySelection: i.repository_selection ?? "selected",
3715
+ appSlug: i.app_slug
3716
+ }));
3717
+ }
3718
+ /** Repositories of one installation the signed-in user can reach (GET /user/installations/{id}/repositories, every page). */
3719
+ async listInstallationRepos(installationId) {
3720
+ this.assertToken("Reading where Bridge is installed");
3721
+ const list2 = await this.paged(`/user/installations/${encodeURIComponent(String(installationId))}/repositories`, (p) => ({
3722
+ items: p.repositories ?? [],
3723
+ total: p.total_count
3724
+ }));
3725
+ return list2.map((r) => ({ owner: r.owner?.login ?? r.full_name.split("/")[0], name: r.name, fullName: r.full_name, private: Boolean(r.private) }));
3726
+ }
3727
+ /**
3728
+ * The App installation that gives the signed-in user this repository (this client's by default): its id, or null
3729
+ * when the App is not installed there or the user cannot reach it. One call when the installation covers all of
3730
+ * the owner's repositories, two (paged) when it covers selected ones.
3731
+ */
3732
+ async findInstallation(owner = this.owner, repo = this.name) {
3733
+ const found = installationFor(await this.listUserInstallations(), owner);
3734
+ if (found.reach === "no") return null;
3735
+ if (found.reach === "yes") return { id: found.id, selection: "all" };
3736
+ const repos = await this.listInstallationRepos(found.id);
3737
+ const hit = repos.some((r) => r.owner.toLowerCase() === owner.toLowerCase() && r.name.toLowerCase() === repo.toLowerCase());
3738
+ return hit ? { id: found.id, selection: "selected" } : null;
3739
+ }
3550
3740
  /** The repository's default branch. */
3551
3741
  async getDefaultBranch() {
3552
3742
  return (await this.getRepo()).defaultBranch;
@@ -3924,14 +4114,101 @@ var SUGGESTION_APPLY_EXIT = {
3924
4114
  refused: 1
3925
4115
  };
3926
4116
 
4117
+ // ../core/src/node/fs-source.ts
4118
+ import { execFileSync } from "node:child_process";
4119
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
4120
+ import { join as join2 } from "node:path";
4121
+
4122
+ // ../core/src/repo-source.ts
4123
+ function normalizeRepoPath(path) {
4124
+ return path.replace(/\\/g, "/").split("/").filter((s) => s && s !== ".").join("/");
4125
+ }
4126
+ var parentDir = (path) => {
4127
+ const p = normalizeRepoPath(path);
4128
+ const i = p.lastIndexOf("/");
4129
+ return i < 0 ? "" : p.slice(0, i);
4130
+ };
4131
+ async function readRepoTexts(source, paths, concurrency = 4, onError = (_path, err) => {
4132
+ throw err;
4133
+ }, onProgress) {
4134
+ const unique = [...new Set(paths)];
4135
+ const out = /* @__PURE__ */ new Map();
4136
+ onProgress?.(0, unique.length);
4137
+ let next = 0;
4138
+ const worker = async () => {
4139
+ while (next < unique.length) {
4140
+ const path = unique[next++];
4141
+ let body;
4142
+ try {
4143
+ body = await source.text(path);
4144
+ } catch (err) {
4145
+ body = onError(path, err);
4146
+ }
4147
+ out.set(path, body);
4148
+ onProgress?.(out.size, unique.length);
4149
+ }
4150
+ };
4151
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(concurrency, unique.length)) }, worker));
4152
+ return out;
4153
+ }
4154
+
4155
+ // ../core/src/node/fs-source.ts
4156
+ var SKIP = /* @__PURE__ */ new Set([".git", "node_modules"]);
4157
+ function gitOut(root, args) {
4158
+ try {
4159
+ return execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
4160
+ } catch {
4161
+ return "";
4162
+ }
4163
+ }
4164
+ function localRepoRef(root) {
4165
+ const origin = /github\.com[:/]([\w.-]+)\/([\w.-]+?)(\.git)?$/.exec(gitOut(root, ["remote", "get-url", "origin"]));
4166
+ return { owner: origin?.[1] ?? "", repo: origin?.[2] ?? "", sha: gitOut(root, ["rev-parse", "HEAD"]) || "working-tree" };
4167
+ }
4168
+ function fsSource(root, ref = {}) {
4169
+ let resolved;
4170
+ const walk2 = (rel, out) => {
4171
+ for (const entry of readdirSync2(join2(root, rel), { withFileTypes: true })) {
4172
+ if (SKIP.has(entry.name)) continue;
4173
+ const path = rel ? `${rel}/${entry.name}` : entry.name;
4174
+ if (entry.isDirectory()) walk2(path, out);
4175
+ else if (entry.isFile()) out.push(path);
4176
+ else if (entry.isSymbolicLink() && statSync2(join2(root, path), { throwIfNoEntry: false })?.isFile()) out.push(path);
4177
+ }
4178
+ };
4179
+ return {
4180
+ get ref() {
4181
+ resolved ??= { ...localRepoRef(root), ...Object.fromEntries(Object.entries(ref).filter(([, v]) => v !== void 0)) };
4182
+ return resolved;
4183
+ },
4184
+ async list(prefix = "") {
4185
+ const rel = normalizeRepoPath(prefix);
4186
+ const abs = join2(root, rel);
4187
+ if (!existsSync2(abs)) return [];
4188
+ if (!statSync2(abs).isDirectory()) return [rel];
4189
+ const out = [];
4190
+ walk2(rel, out);
4191
+ return out.sort();
4192
+ },
4193
+ async text(path) {
4194
+ const abs = join2(root, normalizeRepoPath(path));
4195
+ try {
4196
+ return statSync2(abs).isFile() ? readFileSync2(abs, "utf8") : null;
4197
+ } catch {
4198
+ return null;
4199
+ }
4200
+ }
4201
+ };
4202
+ }
4203
+
3927
4204
  // src/index.ts
3928
4205
  import { fileURLToPath } from "node:url";
3929
4206
 
3930
4207
  // ../visual/src/run.ts
3931
- import { existsSync as existsSync4 } from "node:fs";
4208
+ import { existsSync as existsSync5 } from "node:fs";
3932
4209
  import { mkdir as mkdir3, readFile as readFile3, rm as rm2, unlink, writeFile as writeFile3 } from "node:fs/promises";
3933
4210
  import { tmpdir } from "node:os";
3934
- import { join as join4, relative as relative3, resolve as resolve2 } from "node:path";
4211
+ import { join as join5, relative as relative3, resolve as resolve2 } from "node:path";
3935
4212
 
3936
4213
  // ../core/src/protection.ts
3937
4214
  var PROTECTION_PARTS = ["review", "checks", "history", "deletion"];
@@ -4328,6 +4605,819 @@ function storybookIframeUrl(base, id, o = {}) {
4328
4605
  return url;
4329
4606
  }
4330
4607
 
4608
+ // ../core/src/token-rows.ts
4609
+ var FALLBACK_BRANDS = ["bridge", "ocean"];
4610
+ var FALLBACK_THEMES = ["light", "dark"];
4611
+ var FALLBACK_DENSITIES = ["comfortable", "compact"];
4612
+ function tokenRowAxes(config, readText = () => void 0) {
4613
+ const brandAxis = config.axes.brand;
4614
+ const brands = brandAxis?.values.length ? [...brandAxis.values] : [...FALLBACK_BRANDS];
4615
+ const defaultBrand = brandAxis?.default ?? brands[0];
4616
+ const themes = config.modes.themes.length ? [...config.modes.themes] : [...FALLBACK_THEMES];
4617
+ const densities = config.modes.densities.length ? [...config.modes.densities] : [...FALLBACK_DENSITIES];
4618
+ const files = tokenRowFiles(config);
4619
+ const brandName = (b) => {
4620
+ try {
4621
+ const text = readText(files.brandFile(b));
4622
+ const name = text ? JSON.parse(text).$extensions?.["dev.bridge.brand"]?.name : void 0;
4623
+ if (typeof name === "string" && name) return name;
4624
+ } catch {
4625
+ }
4626
+ return b.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
4627
+ };
4628
+ return {
4629
+ brands,
4630
+ defaultBrand,
4631
+ brandLabels: Object.fromEntries(brands.map((b) => [b, brandName(b)])),
4632
+ themes,
4633
+ densities,
4634
+ defaultDensity: config.modes.defaults.density ?? densities[0]
4635
+ };
4636
+ }
4637
+ function tokenRowFiles(config) {
4638
+ const t = config.tokens;
4639
+ const at = (rel) => tokensPath(config, rel);
4640
+ return {
4641
+ primitivesDir: at(t.primitives),
4642
+ semanticDir: at(t.semantic),
4643
+ brandsDir: at(t.brands),
4644
+ densityDir: at(t.density),
4645
+ componentsDir: at(t.components),
4646
+ baseFile: at(`${t.semantic}/${t.base}.tokens.json`),
4647
+ semanticFile: (theme) => at(`${t.semantic}/${theme}.tokens.json`),
4648
+ brandFile: (brand2) => at(`${t.brands}/${brand2}.tokens.json`),
4649
+ overrideFile: (brand2, theme) => at(`${t.brands}/${brand2}.${theme}.tokens.json`),
4650
+ densityFile: (density) => at(`${t.density}/${density}.tokens.json`)
4651
+ };
4652
+ }
4653
+ function flattenTokenFile(node, file, path = [], inheritedType = "") {
4654
+ const type = node.$type ?? inheritedType;
4655
+ if ("$value" in node) {
4656
+ return [
4657
+ {
4658
+ path,
4659
+ type,
4660
+ value: node.$value,
4661
+ description: node.$description,
4662
+ deprecated: node.$deprecated,
4663
+ file
4664
+ }
4665
+ ];
4666
+ }
4667
+ const out = [];
4668
+ for (const [key, child] of Object.entries(node)) {
4669
+ if (key.startsWith("$") || typeof child !== "object" || child === null) continue;
4670
+ out.push(...flattenTokenFile(child, file, [...path, key], type));
4671
+ }
4672
+ return out;
4673
+ }
4674
+ var toHex22 = (n) => Math.round(n * 255).toString(16).padStart(2, "0");
4675
+ function colorCss(c) {
4676
+ if (typeof c === "string") return { css: c, hex: /^#[0-9a-f]{6}$/i.test(c) ? c.toLowerCase() : void 0 };
4677
+ const v = c;
4678
+ const [r = 0, g = 0, b = 0] = v.components;
4679
+ const hex = (v.hex ?? `#${[r, g, b].map(toHex22).join("")}`).toLowerCase();
4680
+ const alpha = v.alpha ?? 1;
4681
+ if (alpha < 1) return { css: `rgb(${[r, g, b].map((x) => Math.round(x * 255)).join(" ")} / ${alpha})`, hex };
4682
+ return { css: hex, hex };
4683
+ }
4684
+ var dimCss = (d) => typeof d === "object" && d !== null ? `${d.value}${d.unit}` : String(d);
4685
+ function shadowCss(s) {
4686
+ if (typeof s === "string") return s;
4687
+ const layers = Array.isArray(s) ? s : [s];
4688
+ return layers.map((l) => [l.inset ? "inset" : "", dimCss(l.offsetX), dimCss(l.offsetY), dimCss(l.blur), dimCss(l.spread), colorCss(l.color).css].filter(Boolean).join(" ")).join(", ");
4689
+ }
4690
+ function tokenValueCss(type, value) {
4691
+ switch (type) {
4692
+ case "color":
4693
+ return colorCss(value);
4694
+ case "dimension":
4695
+ case "duration":
4696
+ return { css: dimCss(value) };
4697
+ case "shadow":
4698
+ return { css: shadowCss(value) };
4699
+ case "cubicBezier":
4700
+ return { css: Array.isArray(value) ? `cubic-bezier(${value.join(", ")})` : String(value) };
4701
+ case "fontFamily":
4702
+ return { css: Array.isArray(value) ? value.map((n) => /\s/.test(n) ? `"${n}"` : n).join(", ") : String(value) };
4703
+ default:
4704
+ return { css: String(value) };
4705
+ }
4706
+ }
4707
+ var tokenRowCssVar = (segments) => `--${segments.join("-")}`;
4708
+ var tokenRowFigmaName = (segments) => segments.join("/");
4709
+ var isAlias2 = (v) => typeof v === "string" && /^\{[^}]+\}$/.test(v);
4710
+ function buildTokenRows(config, readText, listFiles) {
4711
+ const axes = tokenRowAxes(config, readText);
4712
+ const { brands: BRANDS2, defaultBrand: DEFAULT_BRAND, themes: MODES, densities: DENSITIES, defaultDensity: DEFAULT_DENSITY } = axes;
4713
+ const files = tokenRowFiles(config);
4714
+ const REF_THEME = MODES[0];
4715
+ const jsonFiles = (dir) => listFiles(dir).filter((f) => f.endsWith(".tokens.json")).sort();
4716
+ const flatCache = /* @__PURE__ */ new Map();
4717
+ const flatFile = (f) => {
4718
+ if (!flatCache.has(f)) {
4719
+ const text = readText(f);
4720
+ flatCache.set(f, text === void 0 ? [] : flattenTokenFile(JSON.parse(text), f));
4721
+ }
4722
+ return flatCache.get(f);
4723
+ };
4724
+ const byPathOf = (tokens2) => new Map(tokens2.map((t) => [t.path.join("."), t]));
4725
+ const primitiveFiles = jsonFiles(files.primitivesDir);
4726
+ const componentFiles = jsonFiles(files.componentsDir);
4727
+ const sources = [
4728
+ ...primitiveFiles,
4729
+ ...jsonFiles(files.brandsDir),
4730
+ ...MODES.map(files.semanticFile),
4731
+ files.baseFile,
4732
+ ...jsonFiles(files.densityDir),
4733
+ ...componentFiles
4734
+ ];
4735
+ const primitives = primitiveFiles.flatMap((f) => flatFile(f)).map((t) => {
4736
+ const { css, hex } = tokenValueCss(t.type, t.value);
4737
+ return {
4738
+ path: t.path.join("."),
4739
+ segments: t.path,
4740
+ type: t.type,
4741
+ css,
4742
+ hex,
4743
+ cssVar: tokenRowCssVar(t.path),
4744
+ figma: tokenRowFigmaName(t.path),
4745
+ description: t.description,
4746
+ deprecated: t.deprecated,
4747
+ raw: t.value,
4748
+ file: t.file
4749
+ };
4750
+ });
4751
+ const primByPath = new Map(primitives.map((p) => [p.path, p]));
4752
+ const brandRaw = Object.fromEntries(BRANDS2.map((b) => [b, flatFile(files.brandFile(b))]));
4753
+ const themeRaw = Object.fromEntries(MODES.map((m) => [m, flatFile(files.semanticFile(m))]));
4754
+ const baseRaw = flatFile(files.baseFile);
4755
+ const densityRaw = Object.fromEntries(DENSITIES.map((d) => [d, flatFile(files.densityFile(d))]));
4756
+ const componentRaw = componentFiles.flatMap((f) => flatFile(f));
4757
+ const layerCache = /* @__PURE__ */ new Map();
4758
+ const layers = (b, m, d) => {
4759
+ const k = `${b}.${m}.${d}`;
4760
+ if (!layerCache.has(k))
4761
+ layerCache.set(k, [
4762
+ byPathOf(flatFile(files.overrideFile(b, m))),
4763
+ byPathOf(themeRaw[m] ?? []),
4764
+ byPathOf(baseRaw),
4765
+ byPathOf(densityRaw[d] ?? []),
4766
+ byPathOf(componentRaw),
4767
+ byPathOf(brandRaw[b] ?? [])
4768
+ ]);
4769
+ return layerCache.get(k);
4770
+ };
4771
+ const resolve7 = (b, m, d, value, type) => {
4772
+ const chain = [];
4773
+ let current = value;
4774
+ let currentType = type;
4775
+ let guard = 0;
4776
+ while (isAlias2(current) && guard++ < 30) {
4777
+ const target = current.slice(1, -1);
4778
+ chain.push(target);
4779
+ const prim = primByPath.get(target);
4780
+ if (prim) return { alias: chain[0] ?? null, chain, css: prim.css, hex: prim.hex };
4781
+ const hit = layers(b, m, d).map((l) => l.get(target)).find(Boolean);
4782
+ if (!hit) throw new Error(`tokens: alias {${target}} does not resolve in ${b} ${m} ${d}`);
4783
+ current = hit.value;
4784
+ currentType = hit.type || currentType;
4785
+ }
4786
+ const { css, hex } = tokenValueCss(currentType, current);
4787
+ return { alias: chain[0] ?? null, chain, css, hex };
4788
+ };
4789
+ const same2 = (a, c) => a.css === c.css && a.chain.join() === c.chain.join();
4790
+ const build = (t, tier, valueFor) => {
4791
+ const at = (b, m, d = DEFAULT_DENSITY) => {
4792
+ const raw = valueFor(b, m, d) ?? t;
4793
+ return resolve7(b, m, d, raw.value, raw.type || t.type);
4794
+ };
4795
+ const brands = Object.fromEntries(BRANDS2.map((b) => [b, Object.fromEntries(MODES.map((m) => [m, at(b, m)]))]));
4796
+ return {
4797
+ path: t.path.join("."),
4798
+ segments: t.path,
4799
+ type: t.type,
4800
+ cssVar: tokenRowCssVar(t.path),
4801
+ figma: tokenRowFigmaName(t.path),
4802
+ description: t.description,
4803
+ group: t.path[0] === "color" ? t.path.slice(0, 2).join(".") : t.path[0],
4804
+ deprecated: t.deprecated,
4805
+ tier,
4806
+ file: t.file,
4807
+ modes: brands[DEFAULT_BRAND],
4808
+ brands,
4809
+ densities: tier === "density" ? Object.fromEntries(DENSITIES.map((d) => [d, at(DEFAULT_BRAND, REF_THEME, d)])) : void 0,
4810
+ brandSpecific: BRANDS2.some((b) => MODES.some((m) => !same2(brands[b][m], brands[DEFAULT_BRAND][m])))
4811
+ };
4812
+ };
4813
+ const brand2 = (brandRaw[DEFAULT_BRAND] ?? []).map((t) => build(t, "brand", (b) => byPathOf(brandRaw[b] ?? []).get(t.path.join("."))));
4814
+ const themedBy = Object.fromEntries(MODES.map((m) => [m, byPathOf(themeRaw[m] ?? [])]));
4815
+ const themed = (themeRaw[REF_THEME] ?? []).map((t) => {
4816
+ const path = t.path.join(".");
4817
+ const others = MODES.slice(1).map((m) => {
4818
+ const hit = themedBy[m].get(path);
4819
+ if (!hit) throw new Error(`tokens: semantic token ${path} is missing in ${files.semanticFile(m)}`);
4820
+ return hit;
4821
+ });
4822
+ const s = build(t, "semantic", (b, m) => byPathOf(flatFile(files.overrideFile(b, m))).get(path) ?? (m === REF_THEME ? t : themedBy[m].get(path)));
4823
+ s.description = [t, ...others].map((x) => x.description).find((x) => x !== void 0);
4824
+ s.deprecated = [t, ...others].map((x) => x.deprecated).find((x) => x !== void 0);
4825
+ s.file = MODES.map(files.semanticFile).join(", ");
4826
+ return s;
4827
+ });
4828
+ const base = baseRaw.map((t) => build(t, "semantic", () => t));
4829
+ const semantic = [...themed, ...base];
4830
+ const density = (densityRaw[DEFAULT_DENSITY] ?? []).map((t) => build(t, "density", (_b, _m, d) => byPathOf(densityRaw[d] ?? []).get(t.path.join("."))));
4831
+ const component = componentRaw.map((t) => build(t, "component", () => t));
4832
+ return { ...axes, primitives, semantic, brand: brand2, density, component, all: [...brand2, ...semantic, ...density, ...component], sources };
4833
+ }
4834
+ function tokenRowIndex(rows) {
4835
+ const map = /* @__PURE__ */ new Map();
4836
+ for (const p of rows.primitives) map.set(p.cssVar, { path: p.path, kind: "primitive", type: p.type });
4837
+ for (const s of rows.all) map.set(s.cssVar, { path: s.path, kind: s.tier, type: s.type });
4838
+ return map;
4839
+ }
4840
+
4841
+ // ../core/src/contrast-model.ts
4842
+ var CONTRAST_THRESHOLDS = { text: { aa: 4.5, aaa: 7 }, ui: { aa: 3, aaa: null } };
4843
+ var channel = (c) => {
4844
+ const s = c / 255;
4845
+ return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
4846
+ };
4847
+ function hexLuminance(hex) {
4848
+ const n = parseInt(hex.slice(1), 16);
4849
+ return 0.2126 * channel(n >> 16 & 255) + 0.7152 * channel(n >> 8 & 255) + 0.0722 * channel(n & 255);
4850
+ }
4851
+ function hexContrastRatio(a, b) {
4852
+ const [hi, lo] = [hexLuminance(a), hexLuminance(b)].sort((x, y) => y - x);
4853
+ return (hi + 0.05) / (lo + 0.05);
4854
+ }
4855
+ function parseContrastSpecs(text) {
4856
+ if (!text) return [];
4857
+ const data = JSON.parse(text);
4858
+ return (data.pairs ?? []).map((p) => {
4859
+ const [fg, bg, min] = Array.isArray(p) ? p : [p.fg, p.bg, p.min];
4860
+ return { fg, bg, min, usage: min >= CONTRAST_THRESHOLDS.text.aa ? "text" : "ui" };
4861
+ });
4862
+ }
4863
+ function evaluateContrastPairs(specs, rows) {
4864
+ const byPath = new Map(rows.all.map((s) => [s.path, s]));
4865
+ return specs.map((spec) => {
4866
+ const fgToken = byPath.get(spec.fg);
4867
+ const bgToken = byPath.get(spec.bg);
4868
+ if (!fgToken || !bgToken) throw new Error(`contrast: pair ${spec.fg} on ${spec.bg} references a token that does not exist`);
4869
+ const t = CONTRAST_THRESHOLDS[spec.usage];
4870
+ const result = (b, m) => {
4871
+ const fgHex = fgToken.brands[b][m].hex;
4872
+ const bgHex = bgToken.brands[b][m].hex;
4873
+ const r = hexContrastRatio(fgHex, bgHex);
4874
+ return { fgHex, bgHex, ratio: Math.floor(r * 100) / 100, aa: r >= spec.min, aaa: t.aaa === null ? null : r >= t.aaa };
4875
+ };
4876
+ const brands = Object.fromEntries(rows.brands.map((b) => [b, Object.fromEntries(rows.themes.map((m) => [m, result(b, m)]))]));
4877
+ return { ...spec, fgToken, bgToken, modes: brands[rows.defaultBrand], brands };
4878
+ });
4879
+ }
4880
+
4881
+ // ../core/src/component-scan.ts
4882
+ var componentSlug = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
4883
+ var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4884
+ function parseComponentFamilies(indexSrc, componentsRel = "components") {
4885
+ const out = [];
4886
+ const exportRe = new RegExp(`export \\* from '\\./${escapeRe(componentsRel)}/(\\w+)'`);
4887
+ for (const line2 of indexSrc.split("\n")) {
4888
+ const head = line2.match(/^\/\/\s*([A-Z][\w ]+)$/);
4889
+ if (head) out.push({ name: head[1].trim(), members: [] });
4890
+ const exp = line2.match(exportRe);
4891
+ if (exp && out.length) out[out.length - 1].members.push(exp[1]);
4892
+ }
4893
+ return out.filter((f) => f.members.length);
4894
+ }
4895
+ var NATIVE = {
4896
+ HTMLButtonElement: "button",
4897
+ HTMLInputElement: "input",
4898
+ HTMLSpanElement: "span",
4899
+ HTMLDivElement: "div",
4900
+ HTMLAnchorElement: "a"
4901
+ };
4902
+ function parseExtends(clause) {
4903
+ const el = clause.match(/<(HTML\w+Element)>/)?.[1];
4904
+ const omitted = [...(clause.match(/Omit<[^,]+,\s*([^>]+)>/)?.[1] ?? "").matchAll(/'([^']+)'/g)].map((m) => m[1]);
4905
+ return { inherits: el ? NATIVE[el] ?? el : void 0, omitted };
4906
+ }
4907
+ function parseComponentTsx(name, src) {
4908
+ const aliases = /* @__PURE__ */ new Map();
4909
+ for (const m of src.matchAll(/export type (\w+)\s*=\s*([^;]+);/g)) aliases.set(m[1], m[2].trim());
4910
+ let props = [];
4911
+ let inherits;
4912
+ let omitted = [];
4913
+ const iface = src.match(new RegExp(`export interface ${name}Props(?:\\s+extends\\s+([^{]+))?\\s*\\{([\\s\\S]*?)\\n\\}`));
4914
+ if (iface) {
4915
+ ({ inherits, omitted } = parseExtends(iface[1] ?? ""));
4916
+ for (const m of iface[2].matchAll(/(?:\/\*\*([\s\S]*?)\*\/\s*)?(\w+)(\?)?:\s*([^;]+);/g)) {
4917
+ const rawType = m[4].trim();
4918
+ props.push({
4919
+ name: m[2],
4920
+ type: aliases.get(rawType) ?? rawType,
4921
+ required: !m[3],
4922
+ description: m[1]?.replace(/^\s*\*\s?/gm, "").replace(/\s+/g, " ").trim() || void 0
4923
+ });
4924
+ }
4925
+ } else {
4926
+ const alias = aliases.get(`${name}Props`);
4927
+ if (alias) ({ inherits, omitted } = parseExtends(alias));
4928
+ }
4929
+ const sig = src.match(new RegExp(`function ${name}\\(\\s*\\{([^}]*)\\}`));
4930
+ if (sig) {
4931
+ for (const m of sig[1].matchAll(/(\w+)\s*=\s*('[^']*'|[\w.]+)/g)) {
4932
+ const p = props.find((x) => x.name === m[1]);
4933
+ if (p) p.defaultValue = m[2];
4934
+ }
4935
+ }
4936
+ const docBefore = (re) => {
4937
+ const idx = src.search(re);
4938
+ if (idx < 0) return void 0;
4939
+ const before = src.slice(0, idx).trimEnd();
4940
+ if (!before.endsWith("*/")) return void 0;
4941
+ const start = before.lastIndexOf("/*", before.length - 2);
4942
+ if (start < 0 || before[start + 2] !== "*") return void 0;
4943
+ return before.slice(start + 3, -2).replace(/^\s*\*\s?/gm, "").replace(/\s+/g, " ").trim();
4944
+ };
4945
+ const description = docBefore(new RegExp(`export (?:function|const) ${name}\\b`));
4946
+ const subcomponents = [...src.matchAll(/export function (\w+)/g)].map((m) => m[1]).filter((n) => n !== name);
4947
+ props = props.sort((a, b) => Number(b.required) - Number(a.required));
4948
+ return { props, inherits, omitted, description, subcomponents };
4949
+ }
4950
+ var stateOf = (selector) => {
4951
+ const states = [
4952
+ [/:focus-visible/, "focus"],
4953
+ [/:hover/, "hover"],
4954
+ [/:disabled(?!,)|^[^(]*:disabled/, "disabled"],
4955
+ [/\[data-loading\]/, "loading"],
4956
+ [/::placeholder/, "placeholder"]
4957
+ ];
4958
+ const hit = states.filter(([re]) => re.test(selector)).map(([, s]) => s);
4959
+ return hit.length ? `, ${hit.join(" + ")}` : "";
4960
+ };
4961
+ var BASE_UI_RUNTIME = /^--(_|accordion-panel-|collapsible-panel-|active-tab-|anchor-|available-|drawer-swipe-|toast-(frontmost-height|height|index|offset-y|swipe-)|transform-origin)/;
4962
+ function parseComponentCss(src, labels, describe) {
4963
+ const clean = src.replace(/\/\*[\s\S]*?\*\//g, "");
4964
+ const rules = [];
4965
+ const walk2 = (text, prefix = "") => {
4966
+ let i = 0;
4967
+ let buf = "";
4968
+ while (i < text.length) {
4969
+ const ch = text[i];
4970
+ if (ch === "{") {
4971
+ const head = buf.trim();
4972
+ buf = "";
4973
+ let depth = 1;
4974
+ const start = ++i;
4975
+ while (i < text.length && depth > 0) {
4976
+ if (text[i] === "{") depth++;
4977
+ else if (text[i] === "}") depth--;
4978
+ i++;
4979
+ }
4980
+ const inner = text.slice(start, i - 1);
4981
+ if (head.startsWith("@keyframes")) continue;
4982
+ if (head.startsWith("@")) {
4983
+ walk2(inner, `${head} `);
4984
+ continue;
4985
+ }
4986
+ const names = [...new Set([...inner.matchAll(/var\((--[\w-]+)/g)].map((m) => m[1]))].filter((n) => !BASE_UI_RUNTIME.test(n));
4987
+ if (!names.length) continue;
4988
+ const className = head.match(/\.([\w-]+)/)?.[1] ?? head;
4989
+ rules.push({
4990
+ selector: prefix + head.replace(/\s+/g, " "),
4991
+ className,
4992
+ label: (labels[className] ?? className) + stateOf(head),
4993
+ vars: names.map((n) => {
4994
+ const d = describe(n);
4995
+ return { name: n, href: d?.href, known: !!d, path: d?.path, kind: d?.kind };
4996
+ })
4997
+ });
4998
+ } else {
4999
+ buf += ch;
5000
+ i++;
5001
+ }
5002
+ }
5003
+ };
5004
+ walk2(clean);
5005
+ return rules;
5006
+ }
5007
+ function parseComponentStories(src) {
5008
+ const title = src.match(/title:\s*'([^']+)'/)?.[1];
5009
+ if (!title) return [];
5010
+ const base = title.split("/").map((s) => componentSlug(s.trim()).replace(/\s+/g, "-")).join("-");
5011
+ return [...src.matchAll(/export const (\w+):\s*Story/g)].map((m) => ({ name: m[1], id: `${base}--${componentSlug(m[1])}` }));
5012
+ }
5013
+ function resolveInheritedTokens(list2) {
5014
+ const byName = new Map(list2.map((c) => [c.name, c]));
5015
+ for (const c of list2) {
5016
+ const seen = /* @__PURE__ */ new Set([c.name]);
5017
+ const queue = [...c.reuses ?? []];
5018
+ const inherited = [];
5019
+ const taken = new Set(c.tokenPaths);
5020
+ while (queue.length) {
5021
+ const from = byName.get(queue.shift());
5022
+ if (!from || seen.has(from.name)) continue;
5023
+ seen.add(from.name);
5024
+ const paths = from.tokenPaths.filter((p) => !taken.has(p));
5025
+ paths.forEach((p) => taken.add(p));
5026
+ inherited.push({ from: from.name, slug: from.slug, paths });
5027
+ queue.push(...from.reuses ?? []);
5028
+ }
5029
+ c.inherited = inherited;
5030
+ c.allTokenPaths = [...taken].sort();
5031
+ }
5032
+ for (const c of list2) delete c.reuses;
5033
+ }
5034
+ function scanComponents(input) {
5035
+ const { dir, readText, describeVar } = input;
5036
+ const docsSuffix = input.docsSuffix ?? ".docs.json";
5037
+ const styleSuffix = input.styleSuffix ?? ".module.css";
5038
+ const fileSet = new Set(input.files);
5039
+ const has8 = (p) => fileSet.has(p);
5040
+ const fam = input.indexText === void 0 ? [] : parseComponentFamilies(input.indexText, input.indexRel ?? "components");
5041
+ const familyOf = (name) => fam.find((f) => f.members.includes(name))?.name;
5042
+ const names = [
5043
+ ...new Set(
5044
+ input.files.map((f) => f.startsWith(`${dir}/`) ? f.slice(dir.length + 1).split("/") : []).filter((parts) => parts.length === 2 && parts[1] === `${parts[0]}.tsx`).map((parts) => parts[0])
5045
+ )
5046
+ ].sort();
5047
+ const reuseRe = new RegExp(`from\\s+'\\.\\.\\/(\\w+)\\/(\\w+)${escapeRe(styleSuffix)}'`, "g");
5048
+ const result = names.map((name) => {
5049
+ const folder = `${dir}/${name}`;
5050
+ const files = { tsx: `${folder}/${name}.tsx`, css: `${folder}/${name}${styleSuffix}`, stories: `${folder}/${name}.stories.tsx`, docs: `${folder}/${name}${docsSuffix}` };
5051
+ let docs = null;
5052
+ if (has8(files.docs)) {
5053
+ try {
5054
+ docs = JSON.parse(readText(files.docs) ?? "");
5055
+ } catch (err) {
5056
+ input.problems?.push(`${files.docs}: ${err.message}`);
5057
+ }
5058
+ }
5059
+ const tsxSrc = readText(files.tsx) ?? "";
5060
+ const tsx = parseComponentTsx(name, tsxSrc);
5061
+ const reuses = [...tsxSrc.matchAll(reuseRe)].filter((m) => m[1] === m[2] && m[1] !== name).map((m) => m[1]);
5062
+ const cssText = has8(files.css) ? readText(files.css) : void 0;
5063
+ const rules = cssText === void 0 ? [] : parseComponentCss(cssText, docs?.anatomy ?? {}, describeVar);
5064
+ const used = [...new Set(rules.flatMap((r) => r.vars.map((v) => v.name)))].sort();
5065
+ const storiesText = has8(files.stories) ? readText(files.stories) : void 0;
5066
+ return {
5067
+ name,
5068
+ slug: componentSlug(name),
5069
+ dir: folder,
5070
+ files,
5071
+ ...tsx,
5072
+ rules,
5073
+ tokensUsed: used.map((n) => {
5074
+ const d = describeVar(n);
5075
+ return { name: n, href: d?.href, known: !!d, kind: d?.kind };
5076
+ }),
5077
+ tokenPaths: [...new Set(used.flatMap((n) => describeVar(n)?.path ?? []))].sort(),
5078
+ inherited: [],
5079
+ allTokenPaths: [],
5080
+ reuses,
5081
+ stories: storiesText === void 0 ? [] : parseComponentStories(storiesText),
5082
+ docs,
5083
+ category: docs?.category ?? familyOf(name) ?? "Core"
5084
+ };
5085
+ });
5086
+ const order = (c) => {
5087
+ const i = fam.findIndex((f) => f.name === c.category);
5088
+ return i < 0 ? fam.length : i;
5089
+ };
5090
+ result.sort((a, b) => order(a) - order(b) || a.name.localeCompare(b.name));
5091
+ resolveInheritedTokens(result);
5092
+ return result;
5093
+ }
5094
+
5095
+ // ../core/src/repo-model.ts
5096
+ var REPO_MODEL_DEFAULTS = {
5097
+ tokensRoot: "packages/tokens/src",
5098
+ componentsDir: "packages/react/src/components",
5099
+ componentSpecs: "packages/react/component-specs.json",
5100
+ /** Optional token usage notes (group -> sentence), relative to tokens.root, when tokens.usage is not set. */
5101
+ tokensUsage: "usage.json",
5102
+ repo: { owner: "peterbono", repo: "ai-design-system-demo", defaultBranch: "main" }
5103
+ };
5104
+ function siteRepoFromConfig(config, raw, fallback = REPO_MODEL_DEFAULTS.repo) {
5105
+ const legacy = raw && typeof raw === "object" ? raw : void 0;
5106
+ const g = config.governance;
5107
+ const legacyOwners = legacy?.governance?.owners === void 0 ? legacy?.owners : void 0;
5108
+ return {
5109
+ owner: typeof legacy?.owner === "string" && legacy.owner ? legacy.owner : config.repo?.owner ?? fallback.owner,
5110
+ repo: config.repo?.name ?? fallback.repo,
5111
+ defaultBranch: typeof legacy?.defaultBranch === "string" && legacy.defaultBranch ? legacy.defaultBranch : config.repo?.defaultBranch ?? fallback.defaultBranch,
5112
+ owners: legacyOwners ? { design: legacyOwners.design ?? [], tech: legacyOwners.tech ?? [] } : { design: [...g.owners.design], tech: [...g.owners.tech] },
5113
+ override: g.override ? { ...g.override } : { ...DEFAULT_OVERRIDE },
5114
+ rights: g.rights ?? null,
5115
+ soloOwnerCanApproveBoth: g.soloOwnerCanApproveBoth === true,
5116
+ visualImpact: { mode: config.visualImpact?.mode === "on-demand" ? "on-demand" : "auto" },
5117
+ tokensRoot: config.tokens.root,
5118
+ integrations: config.integrations ?? {},
5119
+ products: config.products.length ? config.products : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
5120
+ changes: config.changes ?? DEFAULT_CHANGES_MODE
5121
+ };
5122
+ }
5123
+ function withModelDefaults(raw) {
5124
+ const tokens2 = raw.tokens && typeof raw.tokens === "object" ? raw.tokens : {};
5125
+ return {
5126
+ ...raw,
5127
+ tokens: { ...tokens2, root: tokens2.root ?? REPO_MODEL_DEFAULTS.tokensRoot },
5128
+ components: raw.components ?? { dir: REPO_MODEL_DEFAULTS.componentsDir }
5129
+ };
5130
+ }
5131
+ var defaultModelConfig = () => defineConfig(withModelDefaults({}));
5132
+ function parseModelConfig(text) {
5133
+ if (text === null) return { config: defaultModelConfig(), raw: void 0, problems: [] };
5134
+ let raw;
5135
+ try {
5136
+ raw = JSON.parse(text);
5137
+ } catch (err) {
5138
+ return { config: defaultModelConfig(), raw: void 0, problems: [`${CONFIG_FILE} is not valid JSON (${err.message})`] };
5139
+ }
5140
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { config: defaultModelConfig(), raw, problems: [`${CONFIG_FILE}: top level must be an object`] };
5141
+ try {
5142
+ return { config: loadConfig(withModelDefaults(raw)), raw, problems: [] };
5143
+ } catch (err) {
5144
+ const problems = err instanceof ConfigError ? err.problems.map((p) => `${CONFIG_FILE}: ${p}`) : [`${CONFIG_FILE}: ${err.message}`];
5145
+ return { config: defaultModelConfig(), raw, problems };
5146
+ }
5147
+ }
5148
+ var emptyRows = (config) => ({ ...tokenRowAxes(config), primitives: [], semantic: [], brand: [], density: [], component: [], all: [], sources: [] });
5149
+ async function loadRepoModel(source, options = {}) {
5150
+ const concurrency = options.concurrency ?? 8;
5151
+ const problems = [];
5152
+ const tooLarge = (path, err) => {
5153
+ if (err?.status !== 413) throw err;
5154
+ problems.push(`${path}: ${err.message}`);
5155
+ return null;
5156
+ };
5157
+ const configText = await source.text(CONFIG_FILE);
5158
+ const parsed = parseModelConfig(configText);
5159
+ const { config } = parsed;
5160
+ problems.push(...parsed.problems);
5161
+ const fallbackRepo = source.ref.owner && source.ref.repo ? { owner: source.ref.owner, repo: source.ref.repo, defaultBranch: REPO_MODEL_DEFAULTS.repo.defaultBranch } : REPO_MODEL_DEFAULTS.repo;
5162
+ const site = siteRepoFromConfig(config, parsed.raw, fallbackRepo);
5163
+ const root = config.tokens.root;
5164
+ const underRoot = await source.list(root === "." ? "" : root);
5165
+ const tokenRel = selectTokenFiles(config, underRoot);
5166
+ const contrastPath = config.contrast?.path;
5167
+ const usagePath = tokensPath(config, config.tokens.usage ?? REPO_MODEL_DEFAULTS.tokensUsage);
5168
+ const tokenPaths = tokenRel.map((f) => tokensPath(config, f));
5169
+ const tokenRead = [.../* @__PURE__ */ new Set([...tokenPaths, ...contrastPath ? [contrastPath] : [], usagePath])];
5170
+ const comps = config.components ?? { dir: REPO_MODEL_DEFAULTS.componentsDir, docsSuffix: ".docs.json", styleSuffix: ".module.css" };
5171
+ const specsPath = comps.specs ?? REPO_MODEL_DEFAULTS.componentSpecs;
5172
+ const indexPath = comps.index ?? (parentDir(comps.dir) ? `${parentDir(comps.dir)}/index.ts` : "index.ts");
5173
+ let compFiles = [];
5174
+ const codeRead = [specsPath];
5175
+ const docsRead = [];
5176
+ if (options.components !== false) {
5177
+ compFiles = await source.list(comps.dir);
5178
+ for (const f of compFiles) {
5179
+ const parts = f.slice(comps.dir.length + 1).split("/");
5180
+ if (parts.length !== 2) continue;
5181
+ const [n, file] = parts;
5182
+ if (file === `${n}${comps.docsSuffix}`) docsRead.push(f);
5183
+ else if ([`${n}.tsx`, `${n}${comps.styleSuffix}`, `${n}.stories.tsx`].includes(file)) codeRead.push(f);
5184
+ }
5185
+ codeRead.push(indexPath);
5186
+ }
5187
+ const seen = /* @__PURE__ */ new Set();
5188
+ const fresh = (paths) => paths.filter((p) => {
5189
+ if (seen.has(p)) return false;
5190
+ seen.add(p);
5191
+ return true;
5192
+ });
5193
+ const phases = [
5194
+ ["tokens", fresh(tokenRead)],
5195
+ ["components", fresh(codeRead)],
5196
+ ["docs", fresh(docsRead)]
5197
+ ];
5198
+ const filesTotal = phases.reduce((n, [, paths]) => n + paths.length, 0);
5199
+ let before = 0;
5200
+ const readPhase = async (phase, paths) => {
5201
+ const offset = before;
5202
+ const out = await readRepoTexts(source, paths, concurrency, tooLarge, (done, total) => options.onProgress?.({ phase, done, total, filesDone: offset + done, filesTotal }));
5203
+ before += paths.length;
5204
+ return out;
5205
+ };
5206
+ const bodies = await readPhase("tokens", phases[0][1]);
5207
+ const read = (p) => bodies.get(p) ?? void 0;
5208
+ const files = {};
5209
+ tokenRel.forEach((rel, i) => {
5210
+ const body = read(tokenPaths[i]);
5211
+ if (body !== void 0) files[rel] = body;
5212
+ });
5213
+ const listFiles = (dir) => underRoot.filter((p) => parentDir(p) === dir);
5214
+ let rows;
5215
+ try {
5216
+ rows = buildTokenRows(config, read, listFiles);
5217
+ } catch (err) {
5218
+ problems.push(err.message);
5219
+ rows = emptyRows(config);
5220
+ }
5221
+ let usage = null;
5222
+ const usageText = read(usagePath);
5223
+ if (usageText !== void 0) {
5224
+ try {
5225
+ usage = JSON.parse(usageText);
5226
+ } catch (err) {
5227
+ problems.push(`${usagePath}: ${err.message}`);
5228
+ }
5229
+ }
5230
+ let specs = [];
5231
+ let pairs3 = [];
5232
+ try {
5233
+ specs = parseContrastSpecs(contrastPath ? read(contrastPath) : void 0);
5234
+ pairs3 = evaluateContrastPairs(specs, rows);
5235
+ } catch (err) {
5236
+ problems.push(err.message);
5237
+ }
5238
+ const index = tokenRowIndex(rows);
5239
+ const describeVar = (cssVar) => {
5240
+ const hit = index.get(cssVar);
5241
+ return hit ? { path: hit.path, kind: hit.kind, href: options.href?.(cssVar, hit.type, hit.kind, hit.path) } : void 0;
5242
+ };
5243
+ let components = null;
5244
+ const code = await readPhase("components", phases[1][1]);
5245
+ const docs = await readPhase("docs", phases[2][1]);
5246
+ const readMore = (p) => code.get(p) ?? docs.get(p) ?? bodies.get(p) ?? void 0;
5247
+ if (options.components !== false) {
5248
+ const indexText = readMore(indexPath);
5249
+ const indexDir = parentDir(indexPath);
5250
+ const indexRel = indexDir && comps.dir.startsWith(`${indexDir}/`) ? comps.dir.slice(indexDir.length + 1) : comps.dir;
5251
+ const list2 = scanComponents({ dir: comps.dir, files: compFiles, readText: readMore, indexText, indexRel, docsSuffix: comps.docsSuffix, styleSuffix: comps.styleSuffix, describeVar, problems });
5252
+ components = { dir: comps.dir, index: indexText === void 0 ? void 0 : indexPath, families: indexText === void 0 ? [] : parseComponentFamilies(indexText, indexRel), list: list2 };
5253
+ }
5254
+ const specsText = readMore(specsPath);
5255
+ let specsFile = null;
5256
+ let coverage = [];
5257
+ if (specsText !== void 0) {
5258
+ try {
5259
+ specsFile = parseComponentSpecs(specsText);
5260
+ } catch {
5261
+ }
5262
+ try {
5263
+ coverage = JSON.parse(specsText).coverage ?? [];
5264
+ } catch (err) {
5265
+ problems.push(`${specsPath}: ${err.message}`);
5266
+ }
5267
+ }
5268
+ return {
5269
+ ref: source.ref,
5270
+ configText,
5271
+ config,
5272
+ site,
5273
+ problems,
5274
+ tokens: { root, files, rows, usage },
5275
+ contrast: { path: contrastPath, specs, pairs: pairs3 },
5276
+ components,
5277
+ specs: { path: specsPath, file: specsFile, coverage }
5278
+ };
5279
+ }
5280
+
5281
+ // ../core/src/snapshot.ts
5282
+ var SNAPSHOT_SCHEMA_VERSION = 1;
5283
+ var SNAPSHOT_BRANCH = "bridge-data";
5284
+ var SNAPSHOT_FILE = "snapshot.json";
5285
+ var SNAPSHOT_AUDITS_DIR = "audits";
5286
+ var SNAPSHOT_MAX_BYTES = 5e6;
5287
+ function recordingSource(source) {
5288
+ const paths = /* @__PURE__ */ new Set();
5289
+ const files = /* @__PURE__ */ new Map();
5290
+ return {
5291
+ paths,
5292
+ files,
5293
+ source: {
5294
+ get ref() {
5295
+ return source.ref;
5296
+ },
5297
+ async list(prefix) {
5298
+ const out = await source.list(prefix);
5299
+ for (const p of out) paths.add(p);
5300
+ return out;
5301
+ },
5302
+ async text(path) {
5303
+ const body = await source.text(path);
5304
+ if (body !== null) files.set(normalizeRepoPath(path), body);
5305
+ return body;
5306
+ }
5307
+ }
5308
+ };
5309
+ }
5310
+ function snapshotAudit(id, report) {
5311
+ return {
5312
+ id,
5313
+ path: `${SNAPSHOT_AUDITS_DIR}/${id}.json`,
5314
+ ...report.label ? { label: report.label } : {},
5315
+ generatedAt: report.generatedAt,
5316
+ target: { path: report.target.path, files: report.target.files },
5317
+ metrics: report.metrics
5318
+ };
5319
+ }
5320
+ var auditId = (s) => s.toLowerCase().replace(/\.json$/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "audit";
5321
+ var pairResults = (p) => {
5322
+ const brands = Object.values(p.brands);
5323
+ return brands.length ? brands.flatMap((b) => Object.values(b)) : Object.values(p.modes);
5324
+ };
5325
+ function snapshotSummary(model) {
5326
+ const results = model.contrast.pairs.flatMap((p) => pairResults(p).map((r) => r.ratio >= p.min));
5327
+ const coverage = model.specs.coverage;
5328
+ const visual = coverage.filter((c) => c.status !== "layout").length;
5329
+ const inFigma = coverage.filter((c) => c.status === "generated" || c.status === "drafted").length;
5330
+ return {
5331
+ tokens: model.tokens.rows.all.length + model.tokens.rows.primitives.length,
5332
+ components: model.components ? model.components.list.length : null,
5333
+ contrast: { pairs: model.contrast.pairs.length, checks: results.length, failing: results.filter((ok) => !ok).length },
5334
+ figma: coverage.length ? { inFigma, visual } : null
5335
+ };
5336
+ }
5337
+ function buildSnapshot(input) {
5338
+ const { model } = input;
5339
+ const files = {};
5340
+ const entries = input.files instanceof Map ? [...input.files] : Object.entries(input.files);
5341
+ const specsPath = normalizeRepoPath(model.specs.path);
5342
+ for (const [k, v] of entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
5343
+ const p = normalizeRepoPath(k);
5344
+ if (p !== specsPath) files[p] = v;
5345
+ }
5346
+ const hasSpecs = entries.some(([k]) => normalizeRepoPath(k) === specsPath);
5347
+ return {
5348
+ kind: "bridge-snapshot",
5349
+ schemaVersion: SNAPSHOT_SCHEMA_VERSION,
5350
+ tool: { name: "bridge", version: input.version },
5351
+ generatedAt: input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
5352
+ repo: { owner: model.ref.owner || model.site.owner, name: model.ref.repo || model.site.repo, branch: input.branch ?? model.site.defaultBranch },
5353
+ sourceSha: model.ref.sha,
5354
+ ...input.committedAt ? { committedAt: input.committedAt } : {},
5355
+ paths: [...new Set([...input.paths].map(normalizeRepoPath))].filter((p) => p !== specsPath).sort(),
5356
+ files,
5357
+ figma: hasSpecs ? { path: specsPath, ...model.specs.file?.generated ? { generated: model.specs.file.generated } : {}, coverage: model.specs.coverage } : null,
5358
+ summary: snapshotSummary(model),
5359
+ contrast: { ...model.contrast.path ? { path: model.contrast.path } : {}, pairs: model.contrast.pairs.map(({ fgToken: _fg, bgToken: _bg, ...rest }) => rest) },
5360
+ audits: input.audits ?? [],
5361
+ problems: [...model.problems]
5362
+ };
5363
+ }
5364
+ var SNAPSHOT_LLMS_FILE = "llms.txt";
5365
+ var LLMS_MAX_TOKENS = 600;
5366
+ var line = (text) => typeof text === "string" ? text.replace(/\s+/g, " ").trim() : "";
5367
+ var strings = (list2) => Array.isArray(list2) ? list2.map(line).filter(Boolean) : [];
5368
+ function snapshotLlmsTxt(model, snapshot) {
5369
+ const { owner, name, branch } = snapshot.repo;
5370
+ const date2 = (snapshot.committedAt ?? snapshot.generatedAt).slice(0, 10);
5371
+ const rows = model.tokens.rows;
5372
+ const tokens2 = rows.all.filter((t) => !t.deprecated);
5373
+ const shown = tokens2.slice(0, LLMS_MAX_TOKENS);
5374
+ const comps = model.components?.list ?? [];
5375
+ const out = [
5376
+ `# ${owner}/${name} design system`,
5377
+ "",
5378
+ `> Tokens, components and usage guidance of the design system in github.com/${owner}/${name}, read at ${snapshot.sourceSha.slice(0, 7)} on ${branch} (${date2}). Published by Bridge for AI agents and people.`,
5379
+ "",
5380
+ `- Source: https://github.com/${owner}/${name}/tree/${snapshot.sourceSha}`,
5381
+ `- Every file Bridge read, as JSON: ${SNAPSHOT_FILE}, next to this file on the ${SNAPSHOT_BRANCH} branch.`,
5382
+ `- Themes: ${rows.themes.join(", ") || "none"}. Brands: ${rows.brands.join(", ") || "none"}. Densities: ${rows.densities.join(", ") || "none"}.`,
5383
+ "",
5384
+ "## Tokens",
5385
+ "",
5386
+ `${tokens2.length} tokens (brand, semantic, density and component tiers) on top of ${rows.primitives.length} primitives. Use the semantic tokens in product code, never the primitives. Values are for the default brand${rows.defaultBrand ? ` (${rows.defaultBrand})` : ""}.`,
5387
+ ""
5388
+ ];
5389
+ for (const t of shown) {
5390
+ const values = Object.entries(t.modes).map(([theme, v]) => `${theme} ${line(v.css)}`).join(", ");
5391
+ const note = line(t.description);
5392
+ out.push(`- \`${t.path}\` (\`${t.cssVar}\`, ${t.tier} ${t.type}): ${values || "no value"}${note ? `. ${note}` : ""}`);
5393
+ }
5394
+ if (tokens2.length > shown.length) out.push("", `${tokens2.length - shown.length} more tokens are in ${SNAPSHOT_FILE}.`);
5395
+ out.push("", "## Components", "");
5396
+ if (!comps.length) out.push("No components were read (components.dir in bridge.config.json names the folder).");
5397
+ for (const c of comps) {
5398
+ const d = c.docs;
5399
+ out.push(`### ${line(c.name)}`, "");
5400
+ const summary = line(d?.summary) || line(c.description);
5401
+ if (summary) out.push(summary, "");
5402
+ out.push(`- Family: ${line(c.category)}${c.docs?.status && c.docs.status !== "stable" ? `, ${c.docs.status}` : ""}`);
5403
+ if (c.allTokenPaths.length) out.push(`- Tokens: ${c.allTokenPaths.slice(0, 40).join(", ")}${c.allTokenPaths.length > 40 ? ", ..." : ""}`);
5404
+ for (const [label, list2] of [
5405
+ ["Use it for", strings(d?.whenToUse)],
5406
+ ["Not for", strings(d?.whenNotToUse)],
5407
+ ["Accessibility", strings(d?.accessibility)]
5408
+ ]) {
5409
+ if (list2.length) out.push(`- ${label}:`, ...list2.map((x) => ` - ${x}`));
5410
+ }
5411
+ const dos = Array.isArray(d?.dos) ? d.dos : [];
5412
+ const donts = Array.isArray(d?.donts) ? d.donts : [];
5413
+ for (const g of dos) if (line(g?.title)) out.push(`- Do: ${line(g.title)}${line(g.text) ? `. ${line(g.text)}` : ""}`);
5414
+ for (const g of donts) if (line(g?.title)) out.push(`- Don't: ${line(g.title)}${line(g.text) ? `. ${line(g.text)}` : ""}`);
5415
+ out.push("");
5416
+ }
5417
+ return `${out.join("\n").replace(/\n{3,}/g, "\n\n").trim()}
5418
+ `;
5419
+ }
5420
+
4331
5421
  // ../visual/src/capture.ts
4332
5422
  import { mkdir, writeFile } from "node:fs/promises";
4333
5423
  import { dirname as dirname2 } from "node:path";
@@ -4425,15 +5515,15 @@ async function shoot(page, r) {
4425
5515
  }
4426
5516
  async function switchStory(page, storyId2) {
4427
5517
  const ok = await page.evaluate(
4428
- (id) => new Promise((resolve6) => {
5518
+ (id) => new Promise((resolve7) => {
4429
5519
  const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
4430
- if (!ch) return resolve6(false);
5520
+ if (!ch) return resolve7(false);
4431
5521
  const events = ["storyRendered", "storyErrored", "storyThrewException", "storyMissing", "playFunctionThrewException"];
4432
5522
  const timer = setTimeout(() => done(), 15e3);
4433
5523
  function done() {
4434
5524
  clearTimeout(timer);
4435
5525
  for (const e of events) ch.off(e, done);
4436
- resolve6(true);
5526
+ resolve7(true);
4437
5527
  }
4438
5528
  for (const e of events) ch.on(e, done);
4439
5529
  window.scrollTo(0, 0);
@@ -4804,9 +5894,9 @@ function storiesFromSource(files) {
4804
5894
  }
4805
5895
 
4806
5896
  // ../visual/src/serve.ts
4807
- import { createReadStream, existsSync as existsSync2, statSync as statSync2 } from "node:fs";
5897
+ import { createReadStream, existsSync as existsSync3, statSync as statSync3 } from "node:fs";
4808
5898
  import { createServer } from "node:http";
4809
- import { extname, join as join2, normalize, sep as sep2 } from "node:path";
5899
+ import { extname, join as join3, normalize, sep as sep2 } from "node:path";
4810
5900
  var TYPES = {
4811
5901
  ".html": "text/html; charset=utf-8",
4812
5902
  ".js": "text/javascript; charset=utf-8",
@@ -4830,50 +5920,50 @@ function serveDir(dir) {
4830
5920
  const root = normalize(dir);
4831
5921
  const server = createServer((req, res) => {
4832
5922
  const path = decodeURIComponent(new URL(req.url ?? "/", "http://x").pathname);
4833
- let file = normalize(join2(root, path));
5923
+ let file = normalize(join3(root, path));
4834
5924
  if (!file.startsWith(root + sep2) && file !== root) {
4835
5925
  res.writeHead(403).end();
4836
5926
  return;
4837
5927
  }
4838
- if (existsSync2(file) && statSync2(file).isDirectory()) file = join2(file, "index.html");
4839
- if (!existsSync2(file)) {
5928
+ if (existsSync3(file) && statSync3(file).isDirectory()) file = join3(file, "index.html");
5929
+ if (!existsSync3(file)) {
4840
5930
  if (extname(path)) {
4841
5931
  res.writeHead(404).end();
4842
5932
  return;
4843
5933
  }
4844
- file = join2(root, "index.html");
5934
+ file = join3(root, "index.html");
4845
5935
  }
4846
5936
  res.writeHead(200, { "content-type": TYPES[extname(file)] ?? "application/octet-stream", "cache-control": "no-store" });
4847
5937
  createReadStream(file).pipe(res);
4848
5938
  });
4849
- return new Promise((resolve6, reject) => {
5939
+ return new Promise((resolve7, reject) => {
4850
5940
  server.once("error", reject);
4851
5941
  server.listen(0, "127.0.0.1", () => {
4852
5942
  const addr = server.address();
4853
5943
  const port = typeof addr === "object" && addr ? addr.port : 0;
4854
- resolve6({ url: `http://127.0.0.1:${port}`, close: () => new Promise((r) => server.close(() => r())) });
5944
+ resolve7({ url: `http://127.0.0.1:${port}`, close: () => new Promise((r) => server.close(() => r())) });
4855
5945
  });
4856
5946
  });
4857
5947
  }
4858
5948
 
4859
5949
  // ../visual/src/workspace.ts
4860
5950
  import { spawn } from "node:child_process";
4861
- import { existsSync as existsSync3 } from "node:fs";
5951
+ import { existsSync as existsSync4 } from "node:fs";
4862
5952
  import { lstat, mkdir as mkdir2, readdir, readFile as readFile2, realpath, rm, symlink } from "node:fs/promises";
4863
- import { dirname as dirname3, join as join3, relative as relative2 } from "node:path";
5953
+ import { dirname as dirname3, join as join4, relative as relative2 } from "node:path";
4864
5954
  var PRIVATE = /* @__PURE__ */ new Set([".cache", ".vite", ".vite-temp", ".package-lock.json"]);
4865
5955
  async function workspaceDirs(root) {
4866
- const pkg = JSON.parse(await readFile2(join3(root, "package.json"), "utf8"));
5956
+ const pkg = JSON.parse(await readFile2(join4(root, "package.json"), "utf8"));
4867
5957
  const pats = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages ?? [];
4868
5958
  const out = [];
4869
5959
  for (const p of pats) {
4870
5960
  if (!p.endsWith("/*")) {
4871
- if (existsSync3(join3(root, p, "package.json"))) out.push(p);
5961
+ if (existsSync4(join4(root, p, "package.json"))) out.push(p);
4872
5962
  continue;
4873
5963
  }
4874
5964
  const parent = p.slice(0, -2);
4875
- if (!existsSync3(join3(root, parent))) continue;
4876
- for (const d of await readdir(join3(root, parent))) if (existsSync3(join3(root, parent, d, "package.json"))) out.push(`${parent}/${d}`);
5965
+ if (!existsSync4(join4(root, parent))) continue;
5966
+ for (const d of await readdir(join4(root, parent))) if (existsSync4(join4(root, parent, d, "package.json"))) out.push(`${parent}/${d}`);
4877
5967
  }
4878
5968
  return out;
4879
5969
  }
@@ -4882,30 +5972,30 @@ async function link(target, at) {
4882
5972
  await symlink(target, at);
4883
5973
  }
4884
5974
  async function linkNodeModules(src, dst) {
4885
- const srcNm = await realpath(join3(src, "node_modules"));
4886
- const dstNm = join3(dst, "node_modules");
4887
- if (existsSync3(dstNm)) return;
5975
+ const srcNm = await realpath(join4(src, "node_modules"));
5976
+ const dstNm = join4(dst, "node_modules");
5977
+ if (existsSync4(dstNm)) return;
4888
5978
  const dirs = await workspaceDirs(dst);
4889
5979
  const names = /* @__PURE__ */ new Map();
4890
5980
  for (const d of dirs) {
4891
- const { name } = JSON.parse(await readFile2(join3(dst, d, "package.json"), "utf8"));
5981
+ const { name } = JSON.parse(await readFile2(join4(dst, d, "package.json"), "utf8"));
4892
5982
  if (name) names.set(name, d);
4893
5983
  }
4894
5984
  await mkdir2(dstNm, { recursive: true });
4895
5985
  for (const e of await readdir(srcNm)) {
4896
5986
  if (PRIVATE.has(e)) continue;
4897
5987
  if (e.startsWith("@")) {
4898
- for (const c of await readdir(join3(srcNm, e))) if (!names.has(`${e}/${c}`)) await link(join3(srcNm, e, c), join3(dstNm, e, c));
4899
- } else if (!names.has(e)) await link(join3(srcNm, e), join3(dstNm, e));
5988
+ for (const c of await readdir(join4(srcNm, e))) if (!names.has(`${e}/${c}`)) await link(join4(srcNm, e, c), join4(dstNm, e, c));
5989
+ } else if (!names.has(e)) await link(join4(srcNm, e), join4(dstNm, e));
4900
5990
  }
4901
5991
  for (const [name, d] of names) {
4902
- const at = join3(dstNm, name);
5992
+ const at = join4(dstNm, name);
4903
5993
  await rm(at, { force: true, recursive: false }).catch(() => void 0);
4904
- await link(relative2(dirname3(at), join3(dst, d)), at);
5994
+ await link(relative2(dirname3(at), join4(dst, d)), at);
4905
5995
  }
4906
5996
  for (const d of dirs) {
4907
- const nested = join3(src, d, "node_modules");
4908
- if (existsSync3(nested) && !existsSync3(join3(dst, d, "node_modules"))) await link(await realpath(nested), join3(dst, d, "node_modules"));
5997
+ const nested = join4(src, d, "node_modules");
5998
+ if (existsSync4(nested) && !existsSync4(join4(dst, d, "node_modules"))) await link(await realpath(nested), join4(dst, d, "node_modules"));
4909
5999
  }
4910
6000
  }
4911
6001
  async function addWorktree(repo, sha, dir) {
@@ -4915,7 +6005,7 @@ async function addWorktree(repo, sha, dir) {
4915
6005
  }
4916
6006
  async function removeWorktree(repo, dir) {
4917
6007
  try {
4918
- await rm(join3(dir, "node_modules"), { recursive: true, force: true });
6008
+ await rm(join4(dir, "node_modules"), { recursive: true, force: true });
4919
6009
  await git(repo, ["worktree", "remove", "--force", dir]);
4920
6010
  } catch {
4921
6011
  await rm(dir, { recursive: true, force: true });
@@ -4923,8 +6013,8 @@ async function removeWorktree(repo, dir) {
4923
6013
  await git(repo, ["worktree", "prune"]).catch(() => void 0);
4924
6014
  }
4925
6015
  function run(cmd, args, cwd, log, root = cwd, env = {}) {
4926
- return new Promise((resolve6, reject) => {
4927
- const bin = [.../* @__PURE__ */ new Set([join3(cwd, "node_modules", ".bin"), join3(root, "node_modules", ".bin")])].join(":");
6016
+ return new Promise((resolve7, reject) => {
6017
+ const bin = [.../* @__PURE__ */ new Set([join4(cwd, "node_modules", ".bin"), join4(root, "node_modules", ".bin")])].join(":");
4928
6018
  const child = spawn(cmd, args, {
4929
6019
  cwd,
4930
6020
  env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, NODE_OPTIONS: process.env.NODE_OPTIONS ?? "--max-old-space-size=3072", STORYBOOK_DISABLE_TELEMETRY: "1", ...env },
@@ -4936,7 +6026,7 @@ function run(cmd, args, cwd, log, root = cwd, env = {}) {
4936
6026
  child.stderr.on("data", keep);
4937
6027
  child.on("error", reject);
4938
6028
  child.on("close", (code) => {
4939
- if (code === 0) resolve6();
6029
+ if (code === 0) resolve7();
4940
6030
  else {
4941
6031
  log(tail);
4942
6032
  reject(new Error(`${cmd} ${args.join(" ")} failed in ${cwd} (exit ${code})`));
@@ -4952,17 +6042,17 @@ var DEFAULT_BUILD = {
4952
6042
  var isDir = async (p) => (await lstat(p).catch(() => null))?.isDirectory() ?? false;
4953
6043
  async function buildRef(o) {
4954
6044
  const cfg = o.config ?? DEFAULT_BUILD;
4955
- const out = join3(o.cache, o.sha);
4956
- const sbOut = join3(out, "storybook");
4957
- const appOut = join3(out, "app");
4958
- const needSb = o.plan.storybook && !existsSync3(join3(sbOut, "index.json"));
4959
- const needApp = o.plan.app && !existsSync3(join3(appOut, "index.html"));
6045
+ const out = join4(o.cache, o.sha);
6046
+ const sbOut = join4(out, "storybook");
6047
+ const appOut = join4(out, "app");
6048
+ const needSb = o.plan.storybook && !existsSync4(join4(sbOut, "index.json"));
6049
+ const needApp = o.plan.app && !existsSync4(join4(appOut, "index.html"));
4960
6050
  const built = { ...o.plan.storybook ? { storybook: sbOut } : {}, ...o.plan.app ? { app: appOut } : {} };
4961
6051
  if (!needSb && !needApp) {
4962
6052
  o.log(` ${o.sha.slice(0, 7)}: builds cached`);
4963
6053
  return built;
4964
6054
  }
4965
- const dir = join3(o.work, `wt-${o.sha.slice(0, 12)}`);
6055
+ const dir = join4(o.work, `wt-${o.sha.slice(0, 12)}`);
4966
6056
  o.log(` ${o.sha.slice(0, 7)}: worktree ${dir}`);
4967
6057
  await addWorktree(o.repo, o.sha, dir);
4968
6058
  try {
@@ -4974,15 +6064,15 @@ async function buildRef(o) {
4974
6064
  await rm(sbOut, { recursive: true, force: true });
4975
6065
  await run("storybook", ["build", "-c", cfg.storybookConfigDir, "-o", sbOut, "--quiet", "--disable-telemetry"], dir, o.log);
4976
6066
  }
4977
- if (needApp && await isDir(join3(dir, cfg.appDir))) {
6067
+ if (needApp && await isDir(join4(dir, cfg.appDir))) {
4978
6068
  o.log(` ${o.sha.slice(0, 7)}: app build`);
4979
6069
  await rm(appOut, { recursive: true, force: true });
4980
- await run("vite", ["build", "--outDir", appOut, "--emptyOutDir", "--logLevel", "warn"], join3(dir, cfg.appDir), o.log, dir);
6070
+ await run("vite", ["build", "--outDir", appOut, "--emptyOutDir", "--logLevel", "warn"], join4(dir, cfg.appDir), o.log, dir);
4981
6071
  }
4982
6072
  } finally {
4983
6073
  if (!o.keep) await removeWorktree(o.repo, dir);
4984
6074
  }
4985
- if (o.plan.app && !existsSync3(join3(appOut, "index.html"))) delete built.app;
6075
+ if (o.plan.app && !existsSync4(join4(appOut, "index.html"))) delete built.app;
4986
6076
  return built;
4987
6077
  }
4988
6078
 
@@ -4990,7 +6080,7 @@ async function buildRef(o) {
4990
6080
  var STORY_READY = `document.body.classList.contains('sb-show-main') && (document.querySelector('#storybook-root')?.childElementCount > 0 || document.querySelector('[role="dialog"]')) || document.body.classList.contains('sb-show-errordisplay')`;
4991
6081
  var APP_READY = `document.querySelector('#root')?.childElementCount > 0 && !document.querySelector('[aria-busy="true"]')`;
4992
6082
  async function readConfig(root, sha) {
4993
- const text = await showFile(root, sha, "bridge.config.json") ?? (existsSync4(join4(root, "bridge.config.json")) ? await readFile3(join4(root, "bridge.config.json"), "utf8") : null);
6083
+ const text = await showFile(root, sha, "bridge.config.json") ?? (existsSync5(join5(root, "bridge.config.json")) ? await readFile3(join5(root, "bridge.config.json"), "utf8") : null);
4994
6084
  if (!text) throw new Error("bridge.config.json not found: run `bridge init` first.");
4995
6085
  return loadConfig(text);
4996
6086
  }
@@ -5013,7 +6103,7 @@ function changedTokens(before, after) {
5013
6103
  return [...modes.entries()].map(([path, m]) => ({ path, kind: direct.has(path) ? "direct" : "alias", modes: m })).sort((a, b) => Number(a.kind === "alias") - Number(b.kind === "alias") || a.path.localeCompare(b.path));
5014
6104
  }
5015
6105
  async function readIndexJson(dir) {
5016
- const index = parseStorybookIndex(JSON.parse(await readFile3(join4(dir, "index.json"), "utf8")), "index.json");
6106
+ const index = parseStorybookIndex(JSON.parse(await readFile3(join5(dir, "index.json"), "utf8")), "index.json");
5017
6107
  return index.entries.filter((e) => e.type === "story").map(({ id, title, name, importPath }) => ({ id, title, name, importPath }));
5018
6108
  }
5019
6109
  async function planImpact(o) {
@@ -5055,7 +6145,7 @@ async function runImpact(o) {
5055
6145
  const log = o.log ?? ((m) => process.stderr.write(`${m}
5056
6146
  `));
5057
6147
  const root = resolve2(o.root);
5058
- const outDir = resolve2(o.out ?? join4(root, "visual-impact"));
6148
+ const outDir = resolve2(o.out ?? join5(root, "visual-impact"));
5059
6149
  const threshold = o.threshold ?? 0.03;
5060
6150
  const only = o.only?.length ? o.only : ["stories", "app"];
5061
6151
  const vp = { stories: o.viewport?.stories ?? { width: 1024, height: 768 }, app: o.viewport?.app ?? { width: 1280, height: 800 } };
@@ -5082,12 +6172,12 @@ async function runImpact(o) {
5082
6172
  shots: shots2
5083
6173
  };
5084
6174
  await mkdir3(outDir, { recursive: true });
5085
- const manifestPath = join4(outDir, "manifest.json");
6175
+ const manifestPath = join5(outDir, "manifest.json");
5086
6176
  await writeFile3(manifestPath, `${JSON.stringify(manifest, null, 2)}
5087
6177
  `);
5088
6178
  let commentPath;
5089
6179
  if (o.imageBase !== void 0 || o.artifactUrl) {
5090
- commentPath = join4(outDir, "comment.md");
6180
+ commentPath = join5(outDir, "comment.md");
5091
6181
  await writeFile3(commentPath, renderComment(manifest, { ...o.imageBase ? { imageBase: o.imageBase } : {}, ...o.artifactUrl ? { artifactUrl: o.artifactUrl } : {} }));
5092
6182
  }
5093
6183
  return { manifest, outDir, manifestPath, ...commentPath ? { commentPath } : {}, summary: summaryLine(manifest) };
@@ -5099,8 +6189,8 @@ async function runImpact(o) {
5099
6189
  log("Nothing visual to capture.");
5100
6190
  return finish([]);
5101
6191
  }
5102
- const cache2 = resolve2(o.cacheDir ?? join4(root, "node_modules", ".cache", "bridge-visual"));
5103
- const work = resolve2(o.workDir ?? join4(tmpdir(), `bridge-visual-${process.pid}`));
6192
+ const cache2 = resolve2(o.cacheDir ?? join5(root, "node_modules", ".cache", "bridge-visual"));
6193
+ const work = resolve2(o.workDir ?? join5(tmpdir(), `bridge-visual-${process.pid}`));
5104
6194
  await mkdir3(work, { recursive: true });
5105
6195
  const builds = { base: {}, head: {} };
5106
6196
  try {
@@ -5129,7 +6219,7 @@ async function runImpact(o) {
5129
6219
  }
5130
6220
  const byId = /* @__PURE__ */ new Map();
5131
6221
  if (builds.head.storybook) for (const s of await readIndexJson(builds.head.storybook)) byId.set(s.id, s);
5132
- if (builds.base.storybook && existsSync4(join4(builds.base.storybook, "index.json"))) {
6222
+ if (builds.base.storybook && existsSync5(join5(builds.base.storybook, "index.json"))) {
5133
6223
  for (const s of await readIndexJson(builds.base.storybook)) if (!byId.has(s.id)) byId.set(s.id, s);
5134
6224
  }
5135
6225
  const jobs = [];
@@ -5144,18 +6234,18 @@ async function runImpact(o) {
5144
6234
  for (const theme of THEMES)
5145
6235
  for (const r of routes) jobs.push({ key: shotKey("app", r.id, t.id, theme), kind: "route", label: r.label, route: r, tenant: t, brand: t.brand, theme });
5146
6236
  await rm2(outDir, { recursive: true, force: true });
5147
- for (const d of ["before", "after", "diff", "composite"]) await mkdir3(join4(outDir, d), { recursive: true });
6237
+ for (const d of ["before", "after", "diff", "composite"]) await mkdir3(join5(outDir, d), { recursive: true });
5148
6238
  const errors = /* @__PURE__ */ new Map();
5149
6239
  for (const side of ["base", "head"]) {
5150
6240
  const b = builds[side];
5151
6241
  const served = [];
5152
6242
  const browser = await launch();
5153
6243
  try {
5154
- const sb = b.storybook && existsSync4(join4(b.storybook, "index.json")) ? await serveDir(b.storybook) : null;
5155
- const app = b.app && existsSync4(join4(b.app, "index.html")) ? await serveDir(b.app) : null;
6244
+ const sb = b.storybook && existsSync5(join5(b.storybook, "index.json")) ? await serveDir(b.storybook) : null;
6245
+ const app = b.app && existsSync5(join5(b.app, "index.html")) ? await serveDir(b.app) : null;
5156
6246
  if (sb) served.push(sb);
5157
6247
  if (app) served.push(app);
5158
- const dir = join4(outDir, side === "base" ? "before" : "after");
6248
+ const dir = join5(outDir, side === "base" ? "before" : "after");
5159
6249
  const storyJobs = jobs.filter((j) => j.kind === "story");
5160
6250
  if (sb && storyJobs.length) {
5161
6251
  log(`Capturing ${storyJobs.length} story shots at ${side}`);
@@ -5170,7 +6260,7 @@ async function runImpact(o) {
5170
6260
  if (!known.has(j.storyId)) continue;
5171
6261
  const err = await shoot(page, {
5172
6262
  url: storyUrl(sb.url, j.storyId, j.brand, j.theme),
5173
- file: join4(dir, `${j.key}.png`),
6263
+ file: join5(dir, `${j.key}.png`),
5174
6264
  fullPage: true,
5175
6265
  readyFn: STORY_READY,
5176
6266
  // Loading stories (Spinner, Skeleton) stay aria-busy on purpose: do not wait for them.
@@ -5196,7 +6286,7 @@ async function runImpact(o) {
5196
6286
  const ctx = await newContext(browser, vp.app);
5197
6287
  const page = await newPage(ctx);
5198
6288
  for (const j of routeJobs.filter((x) => x.tenant.id === t.id && x.theme === theme)) {
5199
- const err = await shoot(page, { url: routeUrl(app.url, j.route, t.id, theme), file: join4(dir, `${j.key}.png`), fullPage: false, readyFn: APP_READY, ...j.route.waitFor ? { waitFor: j.route.waitFor } : {}, mask: ["[data-visual-mask]", ...j.route.mask ?? []] });
6289
+ const err = await shoot(page, { url: routeUrl(app.url, j.route, t.id, theme), file: join5(dir, `${j.key}.png`), fullPage: false, readyFn: APP_READY, ...j.route.waitFor ? { waitFor: j.route.waitFor } : {}, mask: ["[data-visual-mask]", ...j.route.mask ?? []] });
5200
6290
  if (err) errors.set(`${side}:${j.key}`, err);
5201
6291
  }
5202
6292
  await ctx.close();
@@ -5210,10 +6300,10 @@ async function runImpact(o) {
5210
6300
  log("Diffing");
5211
6301
  const shots = [];
5212
6302
  for (const j of jobs) {
5213
- const bf = join4(outDir, "before", `${j.key}.png`);
5214
- const af = join4(outDir, "after", `${j.key}.png`);
5215
- const hasB = existsSync4(bf);
5216
- const hasA = existsSync4(af);
6303
+ const bf = join5(outDir, "before", `${j.key}.png`);
6304
+ const af = join5(outDir, "after", `${j.key}.png`);
6305
+ const hasB = existsSync5(bf);
6306
+ const hasA = existsSync5(af);
5217
6307
  const base = {
5218
6308
  key: j.key,
5219
6309
  kind: j.kind,
@@ -5253,7 +6343,7 @@ async function runImpact(o) {
5253
6343
  if (changed || o.keepUnchanged) Object.assign(images, { before: `before/${j.key}.png`, after: `after/${j.key}.png` });
5254
6344
  else await Promise.all([unlink(bf), unlink(af)]);
5255
6345
  if (d.diff) {
5256
- await writeFile3(join4(outDir, "diff", `${j.key}.png`), encodePng(d.diff));
6346
+ await writeFile3(join5(outDir, "diff", `${j.key}.png`), encodePng(d.diff));
5257
6347
  images.diff = `diff/${j.key}.png`;
5258
6348
  }
5259
6349
  shots.push({
@@ -5278,13 +6368,13 @@ async function runImpact(o) {
5278
6368
  for (const s of toComposite) {
5279
6369
  const w = Math.max(s.size.before?.width ?? 0, s.size.after?.width ?? 0);
5280
6370
  const h = Math.max(s.size.before?.height ?? 0, s.size.after?.height ?? 0);
5281
- const file = join4(outDir, "composite", `${s.key}.png`);
6371
+ const file = join5(outDir, "composite", `${s.key}.png`);
5282
6372
  await composite(page, {
5283
6373
  title: s.label,
5284
6374
  subtitle: `${s.tenant ? `${DEFAULT_TENANTS.find((t) => t.id === s.tenant)?.name ?? s.tenant}, ` : ""}${s.brand}, ${s.theme} \xB7 ${s.status === "changed" ? `${s.percent.toFixed(2)}% changed` : s.status}`,
5285
- before: s.images.before ? join4(outDir, s.images.before) : null,
5286
- after: s.images.after ? join4(outDir, s.images.after) : null,
5287
- diff: s.images.diff ? join4(outDir, s.images.diff) : null,
6375
+ before: s.images.before ? join5(outDir, s.images.before) : null,
6376
+ after: s.images.after ? join5(outDir, s.images.after) : null,
6377
+ diff: s.images.diff ? join5(outDir, s.images.diff) : null,
5288
6378
  width: w,
5289
6379
  height: h,
5290
6380
  boxes: s.boxes,
@@ -5303,16 +6393,16 @@ async function runImpact(o) {
5303
6393
  }
5304
6394
 
5305
6395
  // ../visual/src/references.ts
5306
- import { existsSync as existsSync5 } from "node:fs";
6396
+ import { existsSync as existsSync6 } from "node:fs";
5307
6397
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
5308
- import { dirname as dirname4, join as join5 } from "node:path";
6398
+ import { dirname as dirname4, join as join6 } from "node:path";
5309
6399
  async function captureFigmaReferences(o) {
5310
6400
  const log = o.log ?? (() => void 0);
5311
6401
  const scale = o.scale ?? 2;
5312
6402
  const brand2 = o.brand ?? "bridge";
5313
6403
  const theme = o.theme ?? "light";
5314
- if (!existsSync5(join5(o.storybook, "index.json"))) throw new Error(`No built Storybook in ${o.storybook}: run npm run build-storybook first.`);
5315
- const index = JSON.parse(await readFile4(join5(o.storybook, "index.json"), "utf8"));
6404
+ if (!existsSync6(join6(o.storybook, "index.json"))) throw new Error(`No built Storybook in ${o.storybook}: run npm run build-storybook first.`);
6405
+ const index = JSON.parse(await readFile4(join6(o.storybook, "index.json"), "utf8"));
5316
6406
  if (!index.entries[FIGMA_REFERENCE_STORY]) throw new Error(`The Storybook build has no ${FIGMA_REFERENCE_STORY} story (packages/react/src/stories/FigmaReference.stories.tsx).`);
5317
6407
  const specs = parseComponentSpecs(await readFile4(o.specs, "utf8"));
5318
6408
  const jobs = planReferences(specs).filter((j) => !o.only?.length || o.only.includes(j.component));
@@ -5347,12 +6437,12 @@ async function captureFigmaReferences(o) {
5347
6437
  }
5348
6438
  const plan = JSON.stringify(j.plan.render);
5349
6439
  await page.evaluate(
5350
- ({ id, plan: plan2 }) => new Promise((resolve6) => {
6440
+ ({ id, plan: plan2 }) => new Promise((resolve7) => {
5351
6441
  const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
5352
6442
  const done = () => {
5353
6443
  clearTimeout(timer);
5354
6444
  ch.off("storyRendered", done);
5355
- resolve6();
6445
+ resolve7();
5356
6446
  };
5357
6447
  const timer = setTimeout(done, 1e4);
5358
6448
  ch.on("storyRendered", done);
@@ -5375,7 +6465,7 @@ async function captureFigmaReferences(o) {
5375
6465
  continue;
5376
6466
  }
5377
6467
  const file = referenceImagePath(j.component, j.key);
5378
- const path = join5(o.out, file);
6468
+ const path = join6(o.out, file);
5379
6469
  await mkdir4(dirname4(path), { recursive: true });
5380
6470
  await target.screenshot({ path, omitBackground: true, animations: "disabled", caret: "hide", scale: "device" });
5381
6471
  const box = await target.boundingBox();
@@ -5388,7 +6478,7 @@ async function captureFigmaReferences(o) {
5388
6478
  await served.close();
5389
6479
  }
5390
6480
  await mkdir4(o.out, { recursive: true });
5391
- const manifestPath = join5(o.out, "manifest.json");
6481
+ const manifestPath = join6(o.out, "manifest.json");
5392
6482
  await writeFile4(manifestPath, `${JSON.stringify(manifest, null, 2)}
5393
6483
  `);
5394
6484
  log(`${captured} images, ${skipped} variants skipped. ${manifestPath}`);
@@ -5396,10 +6486,10 @@ async function captureFigmaReferences(o) {
5396
6486
  }
5397
6487
 
5398
6488
  // src/index.ts
5399
- import { execFileSync as execFileSync2 } from "node:child_process";
6489
+ import { execFileSync as execFileSync4 } from "node:child_process";
5400
6490
 
5401
6491
  // src/request.ts
5402
- import { execFileSync, spawnSync } from "node:child_process";
6492
+ import { execFileSync as execFileSync2, spawnSync } from "node:child_process";
5403
6493
  import { readFile as readFile5 } from "node:fs/promises";
5404
6494
  import { BridgeContext as BridgeContext2, requests as requests2 } from "@bridge-ds/mcp";
5405
6495
 
@@ -5515,7 +6605,7 @@ var defaultIO = () => ({
5515
6605
  env: process.env,
5516
6606
  ghToken() {
5517
6607
  try {
5518
- return execFileSync("gh", ["auth", "token"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 }).trim() || void 0;
6608
+ return execFileSync2("gh", ["auth", "token"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 }).trim() || void 0;
5519
6609
  } catch {
5520
6610
  return void 0;
5521
6611
  }
@@ -6312,7 +7402,7 @@ ${r.fixes.sample.map((s) => ` ${s}`).join("\n")}`);
6312
7402
  }
6313
7403
 
6314
7404
  // src/figma.ts
6315
- import { readFileSync as readFileSync2 } from "node:fs";
7405
+ import { readFileSync as readFileSync3 } from "node:fs";
6316
7406
  import { resolve as resolve4 } from "node:path";
6317
7407
  import { BridgeContext as BridgeContext7, branches as branches3, figma } from "@bridge-ds/mcp";
6318
7408
  var FIGMA_HELP = `bridge figma: component changes sent from Figma
@@ -6379,7 +7469,7 @@ ${r.brief}`);
6379
7469
  if (!file) throw new RequestUsageError("Usage: bridge figma push <diff.json> [--dry-run]");
6380
7470
  let diff2;
6381
7471
  try {
6382
- diff2 = JSON.parse(readFileSync2(resolve4(root, file), "utf8"));
7472
+ diff2 = JSON.parse(readFileSync3(resolve4(root, file), "utf8"));
6383
7473
  } catch (err) {
6384
7474
  throw new RequestUsageError(`${file} could not be read as JSON: ${err.message}`);
6385
7475
  }
@@ -6399,6 +7489,94 @@ ${r.brief}`);
6399
7489
  }
6400
7490
  }
6401
7491
 
7492
+ // src/snapshot.ts
7493
+ import { execFileSync as execFileSync3 } from "node:child_process";
7494
+ import { existsSync as existsSync7, mkdirSync, readdirSync as readdirSync3, readFileSync as readFileSync4, rmSync, statSync as statSync4, writeFileSync as writeFileSync2 } from "node:fs";
7495
+ import { basename, join as join7, relative as relative4, resolve as resolve5 } from "node:path";
7496
+ var git2 = (root, args) => {
7497
+ try {
7498
+ return execFileSync3("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
7499
+ } catch {
7500
+ return "";
7501
+ }
7502
+ };
7503
+ var isReport = (x) => {
7504
+ const r = x;
7505
+ return !!r && typeof r === "object" && r.schemaVersion === 1 && typeof r.generatedAt === "string" && !!r.metrics && Array.isArray(r.files);
7506
+ };
7507
+ function reportFiles(root, entry) {
7508
+ const abs = resolve5(root, entry);
7509
+ if (!existsSync7(abs)) throw new Error(`Audit report not found: ${entry}`);
7510
+ if (!statSync4(abs).isDirectory()) return [abs];
7511
+ return readdirSync3(abs).filter((n) => n.endsWith(".json")).sort().map((n) => join7(abs, n));
7512
+ }
7513
+ async function writeSnapshot(opts) {
7514
+ const rec = recordingSource(fsSource(opts.root));
7515
+ const model = await loadRepoModel(rec.source);
7516
+ const audits = /* @__PURE__ */ new Map();
7517
+ for (const entry of opts.auditReports ?? []) {
7518
+ for (const file of reportFiles(opts.root, entry)) {
7519
+ let data;
7520
+ try {
7521
+ data = JSON.parse(readFileSync4(file, "utf8"));
7522
+ } catch (err) {
7523
+ throw new Error(`${relative4(opts.root, file)} is not valid JSON (${err.message})`);
7524
+ }
7525
+ if (!isReport(data)) continue;
7526
+ const id = auditId(basename(file));
7527
+ audits.set(id, { summary: snapshotAudit(id, data), report: data });
7528
+ }
7529
+ }
7530
+ for (const spec of opts.audit ?? []) {
7531
+ const [path, label] = spec.split(/=(.*)/s, 2);
7532
+ const abs = resolve5(opts.root, path);
7533
+ if (!existsSync7(abs)) throw new Error(`No such file or folder to audit: ${path}`);
7534
+ const report = runAudit(abs, { ds: opts.root, version: opts.version, ...label ? { label } : {} });
7535
+ const id = auditId(label || relative4(opts.root, abs) || basename(abs));
7536
+ audits.set(id, { summary: snapshotAudit(id, report), report });
7537
+ }
7538
+ const snapshot = buildSnapshot({
7539
+ model,
7540
+ paths: rec.paths,
7541
+ files: rec.files,
7542
+ version: opts.version,
7543
+ ...opts.branch ? { branch: opts.branch } : {},
7544
+ ...git2(opts.root, ["log", "-1", "--format=%cI"]) ? { committedAt: git2(opts.root, ["log", "-1", "--format=%cI"]) } : {},
7545
+ ...opts.generatedAt ? { generatedAt: opts.generatedAt } : {},
7546
+ audits: [...audits.values()].map((a) => a.summary)
7547
+ });
7548
+ const text = `${JSON.stringify(snapshot)}
7549
+ `;
7550
+ const bytes = Buffer.byteLength(text);
7551
+ if (bytes > SNAPSHOT_MAX_BYTES) throw new Error(`${SNAPSHOT_FILE} would be ${(bytes / 1e6).toFixed(1)} MB, over the ${SNAPSHOT_MAX_BYTES / 1e6} MB limit: is tokens.root or components.dir too wide in bridge.config.json?`);
7552
+ rmSync(join7(opts.out, SNAPSHOT_AUDITS_DIR), { recursive: true, force: true });
7553
+ mkdirSync(opts.out, { recursive: true });
7554
+ writeFileSync2(join7(opts.out, SNAPSHOT_FILE), text);
7555
+ writeFileSync2(join7(opts.out, SNAPSHOT_LLMS_FILE), snapshotLlmsTxt(model, snapshot));
7556
+ const written = [SNAPSHOT_FILE, SNAPSHOT_LLMS_FILE];
7557
+ if (audits.size) mkdirSync(join7(opts.out, SNAPSHOT_AUDITS_DIR), { recursive: true });
7558
+ for (const { summary, report } of audits.values()) {
7559
+ writeFileSync2(join7(opts.out, summary.path), `${JSON.stringify(report)}
7560
+ `);
7561
+ written.push(summary.path);
7562
+ }
7563
+ return { snapshot, written, bytes };
7564
+ }
7565
+ function snapshotLines(r, out) {
7566
+ const s = r.snapshot;
7567
+ const c = s.summary.contrast;
7568
+ const kb = Math.max(1, Math.round(r.bytes / 1024));
7569
+ return [
7570
+ `Wrote ${join7(out, SNAPSHOT_FILE)} (${kb} KB) for ${s.repo.owner}/${s.repo.name} at ${s.sourceSha.slice(0, 7)}.`,
7571
+ ` tokens ${s.summary.tokens}`,
7572
+ ` components ${s.summary.components ?? "not scanned"}`,
7573
+ ` contrast ${c.checks} checks over ${c.pairs} pairs, ${c.failing} failing`,
7574
+ ` figma ${s.summary.figma ? `${s.summary.figma.inFigma} of ${s.summary.figma.visual} visual components` : "no component-specs.json"}`,
7575
+ ` audits ${s.audits.length ? s.audits.map((a) => a.path).join(", ") : "none (--audit <path> or --audit-report <file|dir>)"}`,
7576
+ ...s.problems.map((p) => ` problem ${p}`)
7577
+ ];
7578
+ }
7579
+
6402
7580
  // src/protection.ts
6403
7581
  var UNKNOWN = { kind: "unknown", missing: ["review", "checks", "history", "deletion"] };
6404
7582
  async function protectionReport(opts) {
@@ -6491,6 +7669,13 @@ Usage:
6491
7669
  Exit 0 ok, 1 under --fail-under or regressed, 2 setup error
6492
7670
  bridge audit <path> --fix [--write] safe autofix: CSS values equal to a token become var(--token).
6493
7671
  Dry run unless --write
7672
+ bridge snapshot [--out bridge-data] [--audit <path>[=label] ...] [--audit-report <file|dir> ...]
7673
+ [--branch main] [--json]
7674
+ the repo model of this checkout for hosted Bridge (Studio and Library):
7675
+ snapshot.json (token, contrast and component files, Figma coverage,
7676
+ contrast results) plus audits/<id>.json. --audit runs bridge audit on a
7677
+ product folder; --audit-report carries reports already written.
7678
+ The bridge-data workflow publishes the folder to the bridge-data branch
6494
7679
  bridge docs draft <Component> [--json]
6495
7680
  docs.json pre-fill from the code (props, tokens, anatomy, contrast,
6496
7681
  changelog line, missing keys) and a prompt for Claude to write the
@@ -6579,9 +7764,9 @@ function printSent(r) {
6579
7764
  if (r.hint) print(String(r.hint));
6580
7765
  }
6581
7766
  async function init(a, root) {
6582
- const cfgPath = join6(root, CONFIG_FILE2);
7767
+ const cfgPath = join8(root, CONFIG_FILE2);
6583
7768
  let config;
6584
- if (existsSync6(cfgPath) && !has7(a, "force")) {
7769
+ if (existsSync8(cfgPath) && !has7(a, "force")) {
6585
7770
  print(`${CONFIG_FILE2} exists, keeping it (--force to regenerate).`);
6586
7771
  config = (await (await BridgeContext8.create({ root })).config()).config;
6587
7772
  } else {
@@ -6595,11 +7780,11 @@ async function init(a, root) {
6595
7780
  print("Check the layers order (lowest first) and the tiers; detection is a starting point.");
6596
7781
  }
6597
7782
  const serverJs = fileURLToPath(new URL("./index.js", import.meta.resolve("@bridge-ds/mcp")));
6598
- const relServer = relative4(root, serverJs);
7783
+ const relServer = relative5(root, serverJs);
6599
7784
  const inRepo = !relServer.startsWith("..") && !isAbsolute(relServer);
6600
7785
  const server = (dir) => inRepo ? { command: "node", args: [relServer, "--root", dir] } : { command: "npx", args: ["-y", MCP_PACKAGE, "--root", dir] };
6601
- const mcpPath = join6(root, ".mcp.json");
6602
- const existing = existsSync6(mcpPath) ? JSON.parse(await readFile7(mcpPath, "utf8")) : {};
7786
+ const mcpPath = join8(root, ".mcp.json");
7787
+ const existing = existsSync8(mcpPath) ? JSON.parse(await readFile7(mcpPath, "utf8")) : {};
6603
7788
  existing.mcpServers = { ...existing.mcpServers ?? {}, bridge: server(".") };
6604
7789
  await writeFile5(mcpPath, `${JSON.stringify(existing, null, 2)}
6605
7790
  `);
@@ -6631,8 +7816,8 @@ async function check(a, root) {
6631
7816
  const tight = c.tightest?.[0];
6632
7817
  if (tight) print(` tightest: ${tight.pair} [${tight.modes}] ${tight.ratio}:1 (min ${tight.min})`);
6633
7818
  } else print(`contrast: ${c.message}`);
6634
- for (const line of protection?.lines ?? []) print(line);
6635
- for (const line of roles?.lines ?? []) print(line);
7819
+ for (const line2 of protection?.lines ?? []) print(line2);
7820
+ for (const line2 of roles?.lines ?? []) print(line2);
6636
7821
  print(failed ? "FAIL" : "OK");
6637
7822
  return failed ? 1 : 0;
6638
7823
  }
@@ -6724,7 +7909,7 @@ async function propose(a, root) {
6724
7909
  edits.push({ op: "set", path: s.slice(0, i), value: s.slice(i + 1), ...file ? { file } : {}, ...mode2 ? { mode: mode2 } : {} });
6725
7910
  }
6726
7911
  const editsFile = flag5(a, "edits");
6727
- if (editsFile) edits.push(...JSON.parse(await readFile7(resolve5(editsFile), "utf8")));
7912
+ if (editsFile) edits.push(...JSON.parse(await readFile7(resolve6(editsFile), "utf8")));
6728
7913
  if (!edits.length) throw new UsageError("Give at least one --set path=value or --edits file.json");
6729
7914
  const pr = flag5(a, "pr");
6730
7915
  const changes = changesFlag(a);
@@ -6810,19 +7995,19 @@ async function figmaReferences(a, root) {
6810
7995
  let sha = flag5(a, "sha");
6811
7996
  if (!sha) {
6812
7997
  try {
6813
- sha = execFileSync2("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
7998
+ sha = execFileSync4("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
6814
7999
  } catch {
6815
8000
  sha = "unknown";
6816
8001
  }
6817
8002
  }
6818
8003
  const only = (flag5(a, "only") ?? "").split(",").filter(Boolean);
6819
8004
  const r = await captureFigmaReferences({
6820
- storybook: resolve5(root, flag5(a, "storybook") ?? "storybook-static"),
6821
- specs: resolve5(root, flag5(a, "specs") ?? "packages/react/component-specs.json"),
6822
- out: resolve5(root, flag5(a, "out") ?? "figma-references"),
8005
+ storybook: resolve6(root, flag5(a, "storybook") ?? "storybook-static"),
8006
+ specs: resolve6(root, flag5(a, "specs") ?? "packages/react/component-specs.json"),
8007
+ out: resolve6(root, flag5(a, "out") ?? "figma-references"),
6823
8008
  sha,
6824
8009
  ...only.length ? { only } : {},
6825
- log: has7(a, "json") ? void 0 : (line) => print(line)
8010
+ log: has7(a, "json") ? void 0 : (line2) => print(line2)
6826
8011
  });
6827
8012
  if (has7(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
6828
8013
  return 0;
@@ -6842,10 +8027,10 @@ async function impact(a, root) {
6842
8027
  root,
6843
8028
  base: flag5(a, "base") ?? "main",
6844
8029
  head: flag5(a, "head") ?? "HEAD",
6845
- ...out ? { out: resolve5(out) } : {},
8030
+ ...out ? { out: resolve6(out) } : {},
6846
8031
  ...only.length ? { only } : {},
6847
8032
  ...threshold !== void 0 ? { threshold } : {},
6848
- ...cacheDir ? { cacheDir: resolve5(cacheDir) } : {},
8033
+ ...cacheDir ? { cacheDir: resolve6(cacheDir) } : {},
6849
8034
  ...maxStories ? { maxStoriesPerComponent: maxStories } : {},
6850
8035
  ...imageBase !== void 0 ? { imageBase } : {},
6851
8036
  ...artifactUrl ? { artifactUrl } : {},
@@ -6937,8 +8122,8 @@ ${r.suggestion?.body ?? r.prBody}`);
6937
8122
  async function audit(a, root) {
6938
8123
  const target = a._[1];
6939
8124
  if (!target) throw new UsageError("bridge audit <path> (a product folder or file that uses the design system)");
6940
- if (!existsSync6(target)) throw new UsageError(`No such file or folder: ${target}`);
6941
- const dsRoot = flag5(a, "ds") ? resolve5(flag5(a, "ds")) : existsSync6(join6(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
8125
+ if (!existsSync8(target)) throw new UsageError(`No such file or folder: ${target}`);
8126
+ const dsRoot = flag5(a, "ds") ? resolve6(flag5(a, "ds")) : existsSync8(join8(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
6942
8127
  if (!dsRoot) throw new UsageError(`No ${CONFIG_FILE2} found: run bridge audit from the design system repo or pass --ds <dir>.`);
6943
8128
  const ds = loadDesignSystem(dsRoot);
6944
8129
  if (has7(a, "fix")) {
@@ -6952,12 +8137,12 @@ async function audit(a, root) {
6952
8137
  const baselinePath = flag5(a, "baseline");
6953
8138
  let baseline;
6954
8139
  if (baselinePath) {
6955
- if (!existsSync6(baselinePath)) throw new UsageError(`Baseline not found: ${baselinePath}`);
8140
+ if (!existsSync8(baselinePath)) throw new UsageError(`Baseline not found: ${baselinePath}`);
6956
8141
  baseline = JSON.parse(await readFile7(baselinePath, "utf8"));
6957
8142
  }
6958
8143
  const label = flag5(a, "label");
6959
8144
  const allowlistPath = flag5(a, "allowlist");
6960
- if (allowlistPath && !existsSync6(allowlistPath)) throw new UsageError(`Allowlist not found: ${allowlistPath}`);
8145
+ if (allowlistPath && !existsSync8(allowlistPath)) throw new UsageError(`Allowlist not found: ${allowlistPath}`);
6961
8146
  let allowlist;
6962
8147
  try {
6963
8148
  allowlist = allowlistPath ? loadAllowlist(allowlistPath) : void 0;
@@ -7027,6 +8212,17 @@ async function audit(a, root) {
7027
8212
  }
7028
8213
  return code;
7029
8214
  }
8215
+ async function snapshotCmd(a, root) {
8216
+ const out = resolve6(root, flag5(a, "out") ?? "bridge-data");
8217
+ const branch = flag5(a, "branch");
8218
+ const r = await writeSnapshot({ root, out, version: VERSION, audit: a.flags.audit ?? [], auditReports: a.flags["audit-report"] ?? [], ...branch ? { branch } : {} });
8219
+ if (has7(a, "json")) print(JSON.stringify({ out, written: r.written, bytes: r.bytes, sourceSha: r.snapshot.sourceSha, summary: r.snapshot.summary, audits: r.snapshot.audits.map((x) => x.path), problems: r.snapshot.problems }, null, 2));
8220
+ else {
8221
+ const shown = relative5(process.cwd(), out);
8222
+ for (const line2 of snapshotLines(r, shown.startsWith("..") || isAbsolute(shown) ? out : shown || ".")) print(line2);
8223
+ }
8224
+ return 0;
8225
+ }
7030
8226
  async function docsCmd(a, root) {
7031
8227
  const [, sub, name] = a._;
7032
8228
  if (sub !== "draft" || !name) throw new UsageError("Usage: bridge docs draft <Component> [--json]");
@@ -7063,7 +8259,7 @@ async function main() {
7063
8259
  const argv = process.argv.slice(2);
7064
8260
  const a = parse(argv);
7065
8261
  const cmd = a._[0];
7066
- const root = resolve5(flag5(a, "root") ?? process.cwd());
8262
+ const root = resolve6(flag5(a, "root") ?? process.cwd());
7067
8263
  if (cmd === "request") return runRequest(a, root);
7068
8264
  if (cmd === "explore") return runExplore(a, root);
7069
8265
  if (cmd === "branch") return runBranch(a, root);
@@ -7106,6 +8302,8 @@ async function main() {
7106
8302
  return audit(a, root);
7107
8303
  case "docs":
7108
8304
  return docsCmd(a, root);
8305
+ case "snapshot":
8306
+ return snapshotCmd(a, root);
7109
8307
  case "mcp":
7110
8308
  await serveStdio(optionsFromArgv(argv.slice(1)));
7111
8309
  return -1;