brandrail 0.6.0 → 0.7.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/brandrail.mjs +188 -78
  2. package/package.json +1 -1
@@ -3911,50 +3911,50 @@ var require_fast_uri = __commonJS({
3911
3911
  schemelessOptions.skipEscape = true;
3912
3912
  return serialize(resolved, schemelessOptions);
3913
3913
  }
3914
- function resolveComponent(base, relative3, options, skipNormalization) {
3914
+ function resolveComponent(base, relative4, options, skipNormalization) {
3915
3915
  const target = {};
3916
3916
  if (!skipNormalization) {
3917
3917
  base = parse3(serialize(base, options), options);
3918
- relative3 = parse3(serialize(relative3, options), options);
3918
+ relative4 = parse3(serialize(relative4, options), options);
3919
3919
  }
3920
3920
  options = options || {};
3921
- if (!options.tolerant && relative3.scheme) {
3922
- target.scheme = relative3.scheme;
3923
- target.userinfo = relative3.userinfo;
3924
- target.host = relative3.host;
3925
- target.port = relative3.port;
3926
- target.path = removeDotSegments(relative3.path || "");
3927
- target.query = relative3.query;
3921
+ if (!options.tolerant && relative4.scheme) {
3922
+ target.scheme = relative4.scheme;
3923
+ target.userinfo = relative4.userinfo;
3924
+ target.host = relative4.host;
3925
+ target.port = relative4.port;
3926
+ target.path = removeDotSegments(relative4.path || "");
3927
+ target.query = relative4.query;
3928
3928
  } else {
3929
- if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
3930
- target.userinfo = relative3.userinfo;
3931
- target.host = relative3.host;
3932
- target.port = relative3.port;
3933
- target.path = removeDotSegments(relative3.path || "");
3934
- target.query = relative3.query;
3929
+ if (relative4.userinfo !== void 0 || relative4.host !== void 0 || relative4.port !== void 0) {
3930
+ target.userinfo = relative4.userinfo;
3931
+ target.host = relative4.host;
3932
+ target.port = relative4.port;
3933
+ target.path = removeDotSegments(relative4.path || "");
3934
+ target.query = relative4.query;
3935
3935
  } else {
3936
- if (!relative3.path) {
3936
+ if (!relative4.path) {
3937
3937
  target.path = base.path;
3938
- if (relative3.query !== void 0) {
3939
- target.query = relative3.query;
3938
+ if (relative4.query !== void 0) {
3939
+ target.query = relative4.query;
3940
3940
  } else {
3941
3941
  target.query = base.query;
3942
3942
  }
3943
3943
  } else {
3944
- if (relative3.path[0] === "/") {
3945
- target.path = removeDotSegments(relative3.path);
3944
+ if (relative4.path[0] === "/") {
3945
+ target.path = removeDotSegments(relative4.path);
3946
3946
  } else {
3947
3947
  const baseHasAuthority = base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0;
3948
3948
  if (baseHasAuthority && !base.path) {
3949
- target.path = "/" + relative3.path;
3949
+ target.path = "/" + relative4.path;
3950
3950
  } else if (!base.path) {
3951
- target.path = relative3.path;
3951
+ target.path = relative4.path;
3952
3952
  } else {
3953
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative3.path;
3953
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative4.path;
3954
3954
  }
3955
3955
  target.path = base.scheme === void 0 && !baseHasAuthority && target.path[0] !== "/" ? removeDotSegmentsRelative(target.path) : removeDotSegments(target.path);
3956
3956
  }
3957
- target.query = relative3.query;
3957
+ target.query = relative4.query;
3958
3958
  }
3959
3959
  target.userinfo = base.userinfo;
3960
3960
  target.host = base.host;
@@ -3962,7 +3962,7 @@ var require_fast_uri = __commonJS({
3962
3962
  }
3963
3963
  target.scheme = base.scheme;
3964
3964
  }
3965
- target.fragment = relative3.fragment;
3965
+ target.fragment = relative4.fragment;
3966
3966
  return target;
3967
3967
  }
3968
3968
  function equal(uriA, uriB, options) {
@@ -7256,8 +7256,8 @@ var require_dist = __commonJS({
7256
7256
  });
7257
7257
 
7258
7258
  // src/index.ts
7259
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
7260
- import { basename as basename2, dirname as dirname4, join as join6, relative as relative2, resolve as resolve3 } from "node:path";
7259
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync4 } from "node:fs";
7260
+ import { basename as basename2, dirname as dirname4, join as join7, relative as relative3, resolve as resolve3 } from "node:path";
7261
7261
 
7262
7262
  // ../../node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs
7263
7263
  import { createRequire } from "module";
@@ -9185,7 +9185,7 @@ function makeIgnore(globs) {
9185
9185
  }
9186
9186
 
9187
9187
  // ../engine/src/check.ts
9188
- var ENGINE_VERSION = "0.6.0";
9188
+ var ENGINE_VERSION = "0.7.0";
9189
9189
  var WEIGHT = { error: 4, warning: 1.5, info: 0.25 };
9190
9190
  function computeScore(counts, lines) {
9191
9191
  const penalty = counts.error * WEIGHT.error + counts.warning * WEIGHT.warning + counts.info * WEIGHT.info;
@@ -40801,7 +40801,11 @@ function parseChangedLines(diff) {
40801
40801
  }
40802
40802
  function changedLines(cwd, base) {
40803
40803
  const mergeBase = git(["merge-base", base, "HEAD"], cwd) ?? base;
40804
- const diff = git(["diff", "-U0", "--no-color", "--diff-filter=AMR", mergeBase], cwd);
40804
+ const diff = git(["diff", "--relative", "-U0", "--no-color", "--diff-filter=AMR", mergeBase], cwd);
40805
+ return diff === null ? null : parseChangedLines(diff);
40806
+ }
40807
+ function stagedLines(cwd) {
40808
+ const diff = git(["diff", "--cached", "--relative", "-U0", "--no-color", "--diff-filter=AMR"], cwd);
40805
40809
  return diff === null ? null : parseChangedLines(diff);
40806
40810
  }
40807
40811
 
@@ -40886,12 +40890,82 @@ async function runLocalMcp(cwd, opts) {
40886
40890
  });
40887
40891
  }
40888
40892
 
40893
+ // src/hook.ts
40894
+ import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
40895
+ import { execFileSync as execFileSync2 } from "node:child_process";
40896
+ import { isAbsolute, join as join5, relative as relative2 } from "node:path";
40897
+ var HOOK_START = "# >>> brandrail (npx brandrail hook uninstall to remove)";
40898
+ var HOOK_END = "# <<< brandrail";
40899
+ var hookBlock = (cmd) => [
40900
+ HOOK_START,
40901
+ `${cmd} || { echo ""; echo "Brandrail: fix the errors above (or commit with --no-verify to skip this once)."; exit 1; }`,
40902
+ HOOK_END
40903
+ ].join("\n");
40904
+ var git2 = (args, cwd) => {
40905
+ try {
40906
+ return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
40907
+ } catch {
40908
+ return null;
40909
+ }
40910
+ };
40911
+ function hookFile(cwd) {
40912
+ const top = git2(["rev-parse", "--show-toplevel"], cwd);
40913
+ if (!top) return null;
40914
+ if (existsSync5(join5(top, ".husky"))) return { path: join5(top, ".husky", "pre-commit"), kind: "husky" };
40915
+ const custom2 = git2(["config", "core.hooksPath"], cwd);
40916
+ if (custom2 && !/\.husky/.test(custom2)) return { path: join5(isAbsolute(custom2) ? custom2 : join5(top, custom2), "pre-commit"), kind: "git" };
40917
+ const hooks = git2(["rev-parse", "--git-path", "hooks"], cwd);
40918
+ if (!hooks) return null;
40919
+ return { path: join5(isAbsolute(hooks) ? hooks : join5(cwd, hooks), "pre-commit"), kind: "git" };
40920
+ }
40921
+ function withoutBlock(text2) {
40922
+ const start = text2.indexOf(HOOK_START);
40923
+ const end = text2.indexOf(HOOK_END);
40924
+ if (start === -1 || end === -1 || end < start) return text2;
40925
+ return (text2.slice(0, start) + text2.slice(end + HOOK_END.length)).replace(/\n{3,}/g, "\n\n");
40926
+ }
40927
+ function withBlock(text2, block, kind) {
40928
+ const base = withoutBlock(text2).trimEnd();
40929
+ if (!base) return kind === "husky" ? `${block}
40930
+ ` : `#!/bin/sh
40931
+ ${block}
40932
+ `;
40933
+ return `${base}
40934
+
40935
+ ${block}
40936
+ `;
40937
+ }
40938
+ function installHook(cwd, opts) {
40939
+ const where = hookFile(cwd);
40940
+ if (!where) throw new Error("This isn't a git repository (no .git found).");
40941
+ const top = git2(["rev-parse", "--show-toplevel"], cwd) ?? cwd;
40942
+ const sub = relative2(top, opts.root);
40943
+ const cd = sub && !sub.startsWith("..") ? `cd "${sub}" && ` : "";
40944
+ const cmd = `${cd}npx --yes brandrail check --staged --strict${opts.failBelow !== void 0 ? ` --fail-below ${opts.failBelow}` : ""}`;
40945
+ const before = existsSync5(where.path) ? readFileSync5(where.path, "utf8") : "";
40946
+ mkdirSync3(join5(where.path, ".."), { recursive: true });
40947
+ writeFileSync3(where.path, withBlock(before, hookBlock(cmd), where.kind));
40948
+ try {
40949
+ chmodSync2(where.path, 493);
40950
+ } catch {
40951
+ }
40952
+ return { path: relative2(cwd, where.path) || where.path, kind: where.kind };
40953
+ }
40954
+ function uninstallHook(cwd) {
40955
+ const where = hookFile(cwd);
40956
+ if (!where || !existsSync5(where.path)) return null;
40957
+ const before = readFileSync5(where.path, "utf8");
40958
+ if (!before.includes(HOOK_START)) return null;
40959
+ writeFileSync3(where.path, withoutBlock(before));
40960
+ return relative2(cwd, where.path) || where.path;
40961
+ }
40962
+
40889
40963
  // src/scan.ts
40890
- import { existsSync as existsSync5 } from "node:fs";
40964
+ import { existsSync as existsSync6 } from "node:fs";
40891
40965
  import { createRequire as createRequire2 } from "node:module";
40892
- import { join as join5, resolve as resolve2 } from "node:path";
40966
+ import { join as join6, resolve as resolve2 } from "node:path";
40893
40967
  function loadPlaywright(cwd) {
40894
- for (const base of [join5(cwd, "package.json"), import.meta.url]) {
40968
+ for (const base of [join6(cwd, "package.json"), import.meta.url]) {
40895
40969
  for (const name of ["playwright", "playwright-core"]) {
40896
40970
  try {
40897
40971
  return createRequire2(base)(name);
@@ -40905,7 +40979,7 @@ async function scanPages(cwd, url2, config2, opts, log3) {
40905
40979
  const pw = loadPlaywright(cwd);
40906
40980
  const start = /^https?:\/\//i.test(url2) ? url2 : `http://${url2}`;
40907
40981
  const state = opts.storageState ? resolve2(cwd, opts.storageState) : void 0;
40908
- if (state && !existsSync5(state)) throw new Error(`--storage-state file not found: ${state}`);
40982
+ if (state && !existsSync6(state)) throw new Error(`--storage-state file not found: ${state}`);
40909
40983
  const browser = await pw.chromium.launch({ headless: true });
40910
40984
  try {
40911
40985
  const context = await browser.newContext({ viewport: { width: opts.width, height: 900 }, storageState: state });
@@ -40950,6 +41024,7 @@ Usage
40950
41024
  brandrail connect Sign in through the browser and set this project up: Brand Kit, AI rule files,
40951
41025
  MCP server for Claude Code / Cursor / Copilot, and the UI kit
40952
41026
  brandrail check [paths...] Scan files and print violations
41027
+ brandrail hook install Check staged lines before every commit (errors stop the commit); hook uninstall removes it
40953
41028
  brandrail init Create brandrail.config.json and AI rule files
40954
41029
  brandrail migrate [paths...] What's left on the way to the design system: per library and per file
40955
41030
  brandrail report Write a shareable HTML handover report (paths and counts, no code)
@@ -40964,6 +41039,8 @@ Options for check
40964
41039
  --format <fmt> pretty | json | markdown | github (default: pretty)
40965
41040
  --fail-below <score> Exit 1 when the brand score is lower (default: off)
40966
41041
  --changed [base] Only report lines changed vs base branch (default: origin/main)
41042
+ --staged Only the lines staged for the next commit (what the pre-commit hook runs)
41043
+ --strict Exit 1 on any error (warnings never fail)
40967
41044
  --upload Send the result to Brandrail (needs \`brandrail connect\` or BRANDRAIL_KEY)
40968
41045
  --no-color Plain output
40969
41046
 
@@ -41032,9 +41109,9 @@ function parseArgs(argv) {
41032
41109
  var log2 = (s = "") => process.stderr.write(`${s}
41033
41110
  `);
41034
41111
  function readJson(path) {
41035
- if (!existsSync6(path)) return void 0;
41112
+ if (!existsSync7(path)) return void 0;
41036
41113
  try {
41037
- return JSON.parse(readFileSync5(path, "utf8"));
41114
+ return JSON.parse(readFileSync6(path, "utf8"));
41038
41115
  } catch {
41039
41116
  return void 0;
41040
41117
  }
@@ -41042,7 +41119,7 @@ function readJson(path) {
41042
41119
  async function loadConfig(cwd, flags) {
41043
41120
  const explicit = typeof flags.config === "string" ? resolve3(cwd, flags.config) : null;
41044
41121
  const path = explicit ?? findConfig(cwd);
41045
- if (path) return { config: readConfig(path), source: relative2(cwd, path) || CONFIG_FILE2 };
41122
+ if (path) return { config: readConfig(path), source: relative3(cwd, path) || CONFIG_FILE2 };
41046
41123
  const key = resolveKey();
41047
41124
  if (key) {
41048
41125
  const { config: config2 } = await fetchBrandKit(key);
@@ -41053,10 +41130,19 @@ async function loadConfig(cwd, flags) {
41053
41130
  async function cmdCheck(cwd, positional, flags) {
41054
41131
  const { config: config2, source } = await loadConfig(cwd, flags);
41055
41132
  const resolved = resolveConfig(config2);
41056
- const root = dirname4(typeof flags.config === "string" ? resolve3(cwd, flags.config) : findConfig(cwd) ?? join6(cwd, CONFIG_FILE2));
41057
- const files = collectFiles(root, positional.map((p) => relative2(root, resolve3(cwd, p))), resolved.ignore);
41133
+ const root = dirname4(typeof flags.config === "string" ? resolve3(cwd, flags.config) : findConfig(cwd) ?? join7(cwd, CONFIG_FILE2));
41134
+ const collected = collectFiles(root, positional.map((p) => relative3(root, resolve3(cwd, p))), resolved.ignore);
41058
41135
  let onlyLines;
41059
- if (flags.changed) {
41136
+ let files = collected;
41137
+ if (flags.staged) {
41138
+ const staged = stagedLines(root);
41139
+ if (!staged) log2("warning: couldn't read the staged changes; checking everything");
41140
+ else {
41141
+ onlyLines = staged;
41142
+ files = collected.filter((f) => staged.has(f.path));
41143
+ if (!files.length) return 0;
41144
+ }
41145
+ } else if (flags.changed) {
41060
41146
  const base = typeof flags.changed === "string" ? flags.changed : "origin/main";
41061
41147
  onlyLines = changedLines(root, base) ?? void 0;
41062
41148
  if (!onlyLines) log2(`warning: couldn't read git diff against ${base}; checking everything`);
@@ -41093,6 +41179,11 @@ async function cmdCheck(cwd, positional, flags) {
41093
41179
  log2(`Uploaded \u2192 ${res.url ?? res.scanId}`);
41094
41180
  }
41095
41181
  }
41182
+ if (flags.strict && report.counts.error > 0) {
41183
+ log2(`
41184
+ ${report.counts.error} error${report.counts.error === 1 ? "" : "s"} (--strict fails on any error; warnings don't block).`);
41185
+ return 1;
41186
+ }
41096
41187
  if (threshold !== void 0 && report.score < threshold) {
41097
41188
  log2(`
41098
41189
  Brand score ${report.score} is below ${threshold}.`);
@@ -41100,36 +41191,53 @@ Brand score ${report.score} is below ${threshold}.`);
41100
41191
  }
41101
41192
  return 0;
41102
41193
  }
41194
+ function cmdHook(cwd, positional, flags) {
41195
+ const action = positional[0] ?? "install";
41196
+ if (action === "uninstall" || action === "remove") {
41197
+ const removed = uninstallHook(cwd);
41198
+ log2(removed ? `\u2714 removed the Brandrail check from ${removed}` : "No Brandrail pre-commit hook found.");
41199
+ return 0;
41200
+ }
41201
+ if (action !== "install") throw new Error("Usage: brandrail hook install [--fail-below 80] | brandrail hook uninstall");
41202
+ const config2 = findConfig(cwd);
41203
+ if (!config2) throw new Error(`No ${CONFIG_FILE2} here or above. Run \`brandrail connect\` first.`);
41204
+ const failBelow = flags["fail-below"] !== void 0 ? Number(flags["fail-below"]) : void 0;
41205
+ const r = installHook(cwd, { root: dirname4(config2), failBelow });
41206
+ log2(`\u2714 ${r.kind === "husky" ? "added to Husky's" : "installed the"} pre-commit hook (${r.path})`);
41207
+ log2(" Each commit now checks the staged lines; errors stop the commit, warnings don't.");
41208
+ log2(" Skip it once with git commit --no-verify. Remove it with brandrail hook uninstall.");
41209
+ return 0;
41210
+ }
41103
41211
  function tokensCssPath(root, config2, flags) {
41104
41212
  if (typeof flags["tokens-css"] === "string") return flags["tokens-css"];
41105
41213
  if (config2.tokensCss) return config2.tokensCss;
41106
- return existsSync6(join6(root, "src")) ? "src/brandrail.tokens.css" : "brandrail.tokens.css";
41214
+ return existsSync7(join7(root, "src")) ? "src/brandrail.tokens.css" : "brandrail.tokens.css";
41107
41215
  }
41108
41216
  function writeTokensCss(root, rel, config2, org) {
41109
- const abs = join6(root, rel);
41110
- mkdirSync3(dirname4(abs), { recursive: true });
41217
+ const abs = join7(root, rel);
41218
+ mkdirSync4(dirname4(abs), { recursive: true });
41111
41219
  const { css, usesKit } = tokensCssContent(config2, org);
41112
- writeFileSync3(abs, css);
41220
+ writeFileSync4(abs, css);
41113
41221
  return usesKit;
41114
41222
  }
41115
41223
  function writeRuleFiles(root, config2, org, tokensCss) {
41116
41224
  const md = generateInstructions(config2, { org, tokensCss });
41117
41225
  const written = [];
41118
41226
  for (const { file: file2, readBy } of RULE_FILES) {
41119
- const abs = join6(root, file2);
41120
- mkdirSync3(dirname4(abs), { recursive: true });
41121
- const before = existsSync6(abs) ? readFileSync5(abs, "utf8") : "";
41122
- writeFileSync3(abs, ruleFileContent(file2, md, before));
41227
+ const abs = join7(root, file2);
41228
+ mkdirSync4(dirname4(abs), { recursive: true });
41229
+ const before = existsSync7(abs) ? readFileSync6(abs, "utf8") : "";
41230
+ writeFileSync4(abs, ruleFileContent(file2, md, before));
41123
41231
  written.push(`${file2} (${readBy})`);
41124
41232
  }
41125
- const pkg = readJson(join6(root, "package.json"));
41233
+ const pkg = readJson(join7(root, "package.json"));
41126
41234
  for (const [file2, content, readBy] of [
41127
41235
  [MIGRATE_SKILL_FILE, migrationSkill(config2, { org, pkg }), "Claude Code skill: migrate to the design system"],
41128
41236
  [MIGRATE_AGENT_FILE, migrationAgent({ org }), "Claude Code agent: brandrail-migrator"]
41129
41237
  ]) {
41130
- const abs = join6(root, file2);
41131
- mkdirSync3(dirname4(abs), { recursive: true });
41132
- writeFileSync3(abs, content);
41238
+ const abs = join7(root, file2);
41239
+ mkdirSync4(dirname4(abs), { recursive: true });
41240
+ writeFileSync4(abs, content);
41133
41241
  written.push(`${file2} (${readBy})`);
41134
41242
  }
41135
41243
  return written;
@@ -41139,13 +41247,13 @@ function writeMcpConfigs(root, framework, mode = "remote") {
41139
41247
  const configs = mode === "local" ? mcpLocalConfigs(framework) : mcpClientConfigs(url2);
41140
41248
  for (const c of configs) {
41141
41249
  if (!c.projectFile) continue;
41142
- const abs = join6(root, c.file);
41143
- mkdirSync3(dirname4(abs), { recursive: true });
41250
+ const abs = join7(root, c.file);
41251
+ mkdirSync4(dirname4(abs), { recursive: true });
41144
41252
  const ours = JSON.parse(c.content);
41145
41253
  let merged = ours;
41146
- if (existsSync6(abs)) {
41254
+ if (existsSync7(abs)) {
41147
41255
  try {
41148
- const existing = JSON.parse(readFileSync5(abs, "utf8"));
41256
+ const existing = JSON.parse(readFileSync6(abs, "utf8"));
41149
41257
  merged = { ...existing };
41150
41258
  for (const [k, v] of Object.entries(ours)) {
41151
41259
  if (k === "mcpServers" || k === "servers") {
@@ -41160,7 +41268,7 @@ function writeMcpConfigs(root, framework, mode = "remote") {
41160
41268
  continue;
41161
41269
  }
41162
41270
  }
41163
- writeFileSync3(abs, `${JSON.stringify(merged, null, 2)}
41271
+ writeFileSync4(abs, `${JSON.stringify(merged, null, 2)}
41164
41272
  `);
41165
41273
  log2(`\u2714 added brandrail to ${c.file} (${c.name})`);
41166
41274
  }
@@ -41181,16 +41289,16 @@ function projectFramework(cwd, config2, flags) {
41181
41289
  return asked;
41182
41290
  }
41183
41291
  if (config2.framework && isFramework(config2.framework)) return config2.framework;
41184
- const pkg = join6(cwd, "package.json");
41185
- if (!existsSync6(pkg)) return detectFramework(null);
41292
+ const pkg = join7(cwd, "package.json");
41293
+ if (!existsSync7(pkg)) return detectFramework(null);
41186
41294
  try {
41187
- return detectFramework(JSON.parse(readFileSync5(pkg, "utf8")));
41295
+ return detectFramework(JSON.parse(readFileSync6(pkg, "utf8")));
41188
41296
  } catch {
41189
41297
  return "react";
41190
41298
  }
41191
41299
  }
41192
41300
  async function cmdInit(cwd, flags) {
41193
- const target = join6(cwd, CONFIG_FILE2);
41301
+ const target = join7(cwd, CONFIG_FILE2);
41194
41302
  let config2 = starterConfig;
41195
41303
  let org;
41196
41304
  if (flags["from-api"]) {
@@ -41200,23 +41308,23 @@ async function cmdInit(cwd, flags) {
41200
41308
  config2 = { $schema: starterConfig.$schema, ...res.config };
41201
41309
  org = res.org;
41202
41310
  }
41203
- if (existsSync6(target) && !flags.force && !flags["from-api"]) {
41311
+ if (existsSync7(target) && !flags.force && !flags["from-api"]) {
41204
41312
  log2(`${CONFIG_FILE2} already exists (use --force to overwrite). Updating rule files from it.`);
41205
41313
  config2 = readConfig(target);
41206
41314
  org ??= config2.org;
41207
41315
  } else {
41208
- if (existsSync6(target)) {
41316
+ if (existsSync7(target)) {
41209
41317
  const prev = readConfig(target);
41210
41318
  config2 = mergeProjectConfig(prev, config2);
41211
41319
  }
41212
- writeFileSync3(target, `${JSON.stringify(config2, null, 2)}
41320
+ writeFileSync4(target, `${JSON.stringify(config2, null, 2)}
41213
41321
  `);
41214
41322
  log2(`\u2714 wrote ${CONFIG_FILE2}${org ? ` from ${org}'s Brand Kit` : " (starter tokens \u2014 edit them or use --from-api)"}`);
41215
41323
  }
41216
41324
  const framework = projectFramework(cwd, config2, flags);
41217
41325
  if (config2.framework !== framework) {
41218
41326
  config2 = { ...config2, framework };
41219
- writeFileSync3(target, `${JSON.stringify(config2, null, 2)}
41327
+ writeFileSync4(target, `${JSON.stringify(config2, null, 2)}
41220
41328
  `);
41221
41329
  }
41222
41330
  log2(`\u2714 framework: ${FRAMEWORK_LABEL[framework]}${flags.framework ? "" : " (detected; change with --framework)"}`);
@@ -41250,7 +41358,7 @@ async function cmdConnect(cwd, flags) {
41250
41358
  \u2714 connected to ${org}. The key is saved in ${credentialsPath()} (only you can read it), never in this repo.`);
41251
41359
  }
41252
41360
  await cmdInit(cwd, { ...flags, "from-api": true, mcp: false });
41253
- const config2 = readConfig(join6(cwd, CONFIG_FILE2));
41361
+ const config2 = readConfig(join7(cwd, CONFIG_FILE2));
41254
41362
  const framework = projectFramework(cwd, config2, flags);
41255
41363
  writeMcpConfigs(cwd, framework, flags.remote ? "remote" : "local");
41256
41364
  const usesKit = (config2.components ?? []).some((c) => c.importPath === "@brandrail/kit");
@@ -41263,8 +41371,8 @@ Done. Restart your AI tool so it picks up the MCP server, then ask it to build s
41263
41371
  async function cmdMigrate(cwd, positional, flags) {
41264
41372
  const { config: config2, source } = await loadConfig(cwd, flags);
41265
41373
  const resolved = resolveConfig(config2);
41266
- const root = dirname4(findConfig(cwd) ?? join6(cwd, CONFIG_FILE2));
41267
- const files = collectFiles(root, positional.map((p) => relative2(root, resolve3(cwd, p))), resolved.ignore);
41374
+ const root = dirname4(findConfig(cwd) ?? join7(cwd, CONFIG_FILE2));
41375
+ const files = collectFiles(root, positional.map((p) => relative3(root, resolve3(cwd, p))), resolved.ignore);
41268
41376
  const s = migrationStatus(files, resolved);
41269
41377
  if (flags.format === "json" || flags.json) {
41270
41378
  process.stdout.write(`${JSON.stringify({ progress: s.progress, filesScanned: s.filesScanned, filesClean: s.filesClean, score: s.report.score, libraries: s.libraries, byRule: s.byRule, files: s.files }, null, 2)}
@@ -41286,7 +41394,7 @@ async function cmdMigrate(cwd, positional, flags) {
41286
41394
  const what = Object.entries(f.byRule).sort((a, b) => b[1] - a[1]).map(([r, n]) => `${r} ${n}`).join(", ");
41287
41395
  out.push(` ${String(f.findings).padStart(4)} ${f.path} ${c("2", what)}`);
41288
41396
  }
41289
- out.push("", `Migrate a batch with your AI tool, e.g. "migrate ${s.files[0].path} to the design system"${existsSync6(join6(root, MIGRATE_SKILL_FILE)) ? " (Claude Code uses the brandrail-migrate skill)" : " \u2014 run `npx brandrail connect` first to add the migration skill"}.`);
41397
+ out.push("", `Migrate a batch with your AI tool, e.g. "migrate ${s.files[0].path} to the design system"${existsSync7(join7(root, MIGRATE_SKILL_FILE)) ? " (Claude Code uses the brandrail-migrate skill)" : " \u2014 run `npx brandrail connect` first to add the migration skill"}.`);
41290
41398
  } else out.push("", c("32", "\u2714 Every UI file follows the design system."));
41291
41399
  process.stdout.write(`${out.join("\n")}
41292
41400
  `);
@@ -41311,11 +41419,11 @@ async function cmdMigrate(cwd, positional, flags) {
41311
41419
  async function cmdReport(cwd, flags) {
41312
41420
  const { config: config2 } = await loadConfig(cwd, flags);
41313
41421
  const resolved = resolveConfig(config2);
41314
- const root = dirname4(findConfig(cwd) ?? join6(cwd, CONFIG_FILE2));
41422
+ const root = dirname4(findConfig(cwd) ?? join7(cwd, CONFIG_FILE2));
41315
41423
  const s = migrationStatus(collectFiles(root, [], resolved.ignore), resolved);
41316
41424
  const repo = repoInfo(root);
41317
41425
  const out = resolve3(cwd, typeof flags.out === "string" ? flags.out : "brandrail-report.html");
41318
- writeFileSync3(
41426
+ writeFileSync4(
41319
41427
  out,
41320
41428
  formatHandoverHtml(s, {
41321
41429
  project: repo.slug,
@@ -41326,7 +41434,7 @@ async function cmdReport(cwd, flags) {
41326
41434
  framework: config2.framework ? FRAMEWORK_LABEL[config2.framework] : void 0
41327
41435
  })
41328
41436
  );
41329
- log2(`\u2714 wrote ${relative2(cwd, out)}: ${s.progress}% of UI files on the design system, brand score ${s.report.score}. Open it in a browser, or print it to PDF.`);
41437
+ log2(`\u2714 wrote ${relative3(cwd, out)}: ${s.progress}% of UI files on the design system, brand score ${s.report.score}. Open it in a browser, or print it to PDF.`);
41330
41438
  return 0;
41331
41439
  }
41332
41440
  function cmdWhoami() {
@@ -41406,12 +41514,12 @@ function readTokenInputs(cwd, inputs) {
41406
41514
  const add = (name, text2) => files.push({ name, data: JSON.parse(text2) });
41407
41515
  const walk = (abs, rel) => {
41408
41516
  if (statSync2(abs).isDirectory()) {
41409
- for (const e of readdirSync2(abs)) if (!e.startsWith(".")) walk(join6(abs, e), rel ? `${rel}/${e}` : e);
41517
+ for (const e of readdirSync2(abs)) if (!e.startsWith(".")) walk(join7(abs, e), rel ? `${rel}/${e}` : e);
41410
41518
  } else if (/\.zip$/i.test(abs)) {
41411
- const entries = unzipSync(new Uint8Array(readFileSync5(abs)), { filter: (e) => /\.json$/i.test(e.name) && !/(^|\/)(__MACOSX|\.)/.test(e.name) });
41519
+ const entries = unzipSync(new Uint8Array(readFileSync6(abs)), { filter: (e) => /\.json$/i.test(e.name) && !/(^|\/)(__MACOSX|\.)/.test(e.name) });
41412
41520
  for (const [name, bytes] of Object.entries(entries)) add(name, strFromU8(bytes));
41413
41521
  } else if (/\.json$/i.test(abs)) {
41414
- add(rel || basename2(abs), readFileSync5(abs, "utf8"));
41522
+ add(rel || basename2(abs), readFileSync6(abs, "utf8"));
41415
41523
  }
41416
41524
  };
41417
41525
  for (const input2 of inputs) {
@@ -41426,16 +41534,16 @@ function cmdImport(cwd, positional, flags) {
41426
41534
  if (!files.length) throw new Error("No .json token files found.");
41427
41535
  const res = importTokenFiles(files, { includePrimitives: !!flags.primitives });
41428
41536
  const file2 = files.length === 1 ? positional[0] : `${files.length} files`;
41429
- const path = (typeof flags.config === "string" ? resolve3(cwd, flags.config) : findConfig(cwd)) ?? join6(cwd, CONFIG_FILE2);
41430
- const config2 = existsSync6(path) ? readConfig(path) : { ...starterConfig, tokens: {} };
41537
+ const path = (typeof flags.config === "string" ? resolve3(cwd, flags.config) : findConfig(cwd)) ?? join7(cwd, CONFIG_FILE2);
41538
+ const config2 = existsSync7(path) ? readConfig(path) : { ...starterConfig, tokens: {} };
41431
41539
  const next = flags.replace ? res.tokens : mergeTokens(config2.tokens, res.tokens);
41432
41540
  const d = diffTokens(config2.tokens, next);
41433
- writeFileSync3(path, `${JSON.stringify({ ...config2, tokens: next }, null, 2)}
41541
+ writeFileSync4(path, `${JSON.stringify({ ...config2, tokens: next }, null, 2)}
41434
41542
  `);
41435
41543
  log2(`\u2714 ${res.count} tokens from ${file2} (${res.format})${res.primitives ? `, ${res.primitives} palette primitives left out (--primitives to keep)` : ""}`);
41436
41544
  for (const n of res.notes) log2(` \xB7 ${n}`);
41437
41545
  for (const i2 of auditBrandKit(next)) log2(` ${i2.severity === "error" ? "\u2716" : "!"} ${i2.message}`);
41438
- log2(`\u2714 ${relative2(cwd, path) || CONFIG_FILE2}: ${d.changed.length} changed, ${d.added.length} added, ${d.removed.length} removed`);
41546
+ log2(`\u2714 ${relative3(cwd, path) || CONFIG_FILE2}: ${d.changed.length} changed, ${d.added.length} added, ${d.removed.length} removed`);
41439
41547
  log2("\nNext: npx brandrail init (to refresh AI rule files), then npx brandrail check");
41440
41548
  return 0;
41441
41549
  }
@@ -41471,6 +41579,8 @@ async function main(argv = process.argv.slice(2)) {
41471
41579
  return cmdMigrate(cwd, positional, flags);
41472
41580
  case "report":
41473
41581
  return cmdReport(cwd, flags);
41582
+ case "hook":
41583
+ return cmdHook(cwd, positional, flags);
41474
41584
  case "whoami":
41475
41585
  return cmdWhoami();
41476
41586
  case "logout":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brandrail",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Keep AI-generated UI on-brand: checks code and live pages against your design system, and wires your Brand Kit into Claude Code, Cursor, Copilot and Codex.",
5
5
  "keywords": [
6
6
  "design-system",