brandrail 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Keep AI-generated UI on-brand. `brandrail` checks code and live pages against yo
4
4
  (Brand Kit), and writes your rules into the files AI coding tools read.
5
5
 
6
6
  ```bash
7
- npx brandrail init # brandrail.config.json + AGENTS.md, CLAUDE.md, Cursor/Copilot/Windsurf/Gemini rules
7
+ npx brandrail init # brandrail.config.json, src/brandrail.tokens.css + AGENTS.md, CLAUDE.md, Cursor/Copilot/Windsurf/Gemini rules
8
8
  npx brandrail check # violations + brand score for this repo
9
9
  npx brandrail scan <url> # check a running page in a real browser
10
10
  ```
@@ -23,7 +23,7 @@ npx brandrail check --changed --upload # only changed lines, results in the d
23
23
  | --- | --- |
24
24
  | `check [paths]` | `--format pretty\|json\|markdown\|github`, `--fail-below 80`, `--changed [base]`, `--upload` |
25
25
  | `scan <url>` | `--pages 3`, `--storage-state auth.json` (pages behind a login), `--wait 2000`, `--upload`. Needs `npm i -D playwright && npx playwright install chromium` |
26
- | `init` | `--from-api`, `--mcp`, `--no-rules`, `--force` |
26
+ | `init` | `--from-api`, `--mcp`, `--tokens-css <file>`, `--no-rules`, `--force`. Writes the Brand Kit as CSS variables; import that file once at your app entry |
27
27
  | `import <tokens.json>` | DTCG, Tokens Studio or Figma variables JSON into the config. `--replace`, `--primitives` |
28
28
  | `rules` | List the rules |
29
29
 
@@ -175,10 +175,10 @@ function resolveConfig(config) {
175
175
  if (!l.type && !l.path.startsWith("color")) continue;
176
176
  const rgb = parseColor(l.value);
177
177
  if (!rgb) continue;
178
- const cssVar = `--${l.path.replace(/\./g, "-")}`;
179
- colors.push({ path: l.path, cssVar, hex: toHex(rgb), lab: rgbToLab(rgb), alpha: rgb.a });
178
+ const cssVar2 = `--${l.path.replace(/\./g, "-")}`;
179
+ colors.push({ path: l.path, cssVar: cssVar2, hex: toHex(rgb), lab: rgbToLab(rgb), alpha: rgb.a });
180
180
  const dark = typeof l.dark === "string" ? parseColor(l.dark) : null;
181
- if (dark) colors.push({ path: l.path, cssVar, hex: toHex(dark), lab: rgbToLab(dark), alpha: dark.a, mode: "dark" });
181
+ if (dark) colors.push({ path: l.path, cssVar: cssVar2, hex: toHex(dark), lab: rgbToLab(dark), alpha: dark.a, mode: "dark" });
182
182
  }
183
183
  const spaceLeaves = all.filter((l) => /^(space|spacing)\./.test(l.path));
184
184
  const spacing = [0, 1, 2];
@@ -604,7 +604,7 @@ function makeIgnore(globs) {
604
604
  }
605
605
 
606
606
  // ../engine/src/check.ts
607
- var ENGINE_VERSION = "0.1.0";
607
+ var ENGINE_VERSION = "0.2.0";
608
608
  var WEIGHT = { error: 4, warning: 1.5, info: 0.25 };
609
609
  function computeScore(counts, lines) {
610
610
  const penalty = counts.error * WEIGHT.error + counts.warning * WEIGHT.warning + counts.info * WEIGHT.info;
@@ -673,7 +673,7 @@ function check(files, configInput, opts = {}) {
673
673
  }
674
674
 
675
675
  // ../engine/src/report.ts
676
- var RENDERED_ONLY = { "text-contrast": "Low text contrast", "font-size-scale": "Off-scale font size" };
676
+ var RENDERED_ONLY = { "text-contrast": "Low text contrast", "font-size-scale": "Off-scale font size", "page-rendered": "Blank page" };
677
677
  var title = (id) => RULES.find((r) => r.id === id)?.title ?? RENDERED_ONLY[id] ?? id;
678
678
  var ansi = (on) => {
679
679
  const w = (code) => (s) => on ? `\x1B[${code}m${s}\x1B[0m` : s;
@@ -784,129 +784,6 @@ var starterConfig = {
784
784
  ignore: []
785
785
  };
786
786
 
787
- // ../engine/src/instructions.ts
788
- var RULES_START = "<!-- brandrail:start -->";
789
- var RULES_END = "<!-- brandrail:end -->";
790
- function generateInstructions(config, opts = {}) {
791
- const r = resolveConfig(config);
792
- const lines = [];
793
- lines.push(`# Design system rules${opts.org ? ` \u2014 ${opts.org}` : ""}`);
794
- lines.push("");
795
- lines.push(
796
- `All UI in this repo must follow the Brand Kit${r.tokenVersion ? ` (v${r.tokenVersion})` : ""}. These rules are checked on every pull request by Brandrail.`
797
- );
798
- lines.push("");
799
- if (r.components.length) {
800
- lines.push("## Components");
801
- lines.push("Always use these components. Never re-create them with raw HTML elements:");
802
- for (const c of r.components) {
803
- const tags = [...r.replaces].filter(([, v]) => v === c).map(([t]) => `<${t}>`);
804
- lines.push(`- \`${c.name}\` from \`${c.importPath}\`${tags.length ? ` (instead of ${tags.join(", ")})` : ""}`);
805
- }
806
- lines.push("");
807
- }
808
- if (r.colors.length) {
809
- lines.push("## Colours");
810
- lines.push("Use CSS variables only. Never write hex, rgb() or hsl() values, and never use Tailwind palette classes like `bg-blue-500`.");
811
- for (const c of r.colors) {
812
- if (c.mode) continue;
813
- const dark = r.colors.find((d) => d.mode === "dark" && d.path === c.path);
814
- lines.push(`- \`var(${c.cssVar})\` \u2014 ${c.path} (${c.hex}${dark ? `, dark ${dark.hex}` : ""})`);
815
- }
816
- lines.push("");
817
- }
818
- lines.push("## Spacing, radius and type");
819
- lines.push(`- Spacing (padding, margin, gap) must be one of: ${r.spacing.filter((s) => s > 2).map((s) => `${s}px`).join(", ")}.`);
820
- lines.push(`- Corner radius must be one of: ${r.radii.filter((x) => x > 0 && x < 999).map((x) => `${x}px`).join(", ")} (or fully rounded).`);
821
- if (r.fonts.length) lines.push(`- Fonts: use ${r.fontVars.map((v) => `\`var(${v})\``).join(" or ")}; never set another font-family.`);
822
- lines.push("");
823
- lines.push("## Before you finish");
824
- lines.push("- If the Brandrail MCP server is connected, call `validate_snippet` on the UI you wrote and fix every violation.");
825
- lines.push("- Otherwise run `npx brandrail check` and fix errors before committing.");
826
- lines.push("");
827
- lines.push("<details><summary>Checked rules</summary>");
828
- lines.push("");
829
- for (const rule of RULES) {
830
- if (r.rules[rule.id] === "off") continue;
831
- lines.push(`- **${rule.title}** (\`${rule.id}\`): ${rule.description}`);
832
- }
833
- lines.push("");
834
- lines.push("</details>");
835
- return lines.join("\n");
836
- }
837
- function upsertSection(existing, section) {
838
- const block = `${RULES_START}
839
- ${section}
840
- ${RULES_END}`;
841
- const start = existing.indexOf(RULES_START);
842
- const end = existing.indexOf(RULES_END);
843
- if (start !== -1 && end > start) return existing.slice(0, start) + block + existing.slice(end + RULES_END.length);
844
- return existing.trim() ? `${existing.trimEnd()}
845
-
846
- ${block}
847
- ` : `${block}
848
- `;
849
- }
850
-
851
- // ../engine/src/mcp-config.ts
852
- var json = (o) => `${JSON.stringify(o, null, 2)}
853
- `;
854
- function mcpClientConfigs(url) {
855
- return [
856
- {
857
- id: "claude-code",
858
- name: "Claude Code",
859
- file: ".mcp.json",
860
- projectFile: true,
861
- content: json({ mcpServers: { brandrail: { type: "http", url, headers: { Authorization: "Bearer ${BRANDRAIL_KEY}" } } } }),
862
- command: `claude mcp add --transport http brandrail ${url} --header "Authorization: Bearer $BRANDRAIL_KEY"`
863
- },
864
- {
865
- id: "codex",
866
- name: "Codex",
867
- file: "~/.codex/config.toml",
868
- projectFile: false,
869
- content: `[mcp_servers.brandrail]
870
- url = "${url}"
871
- bearer_token_env_var = "BRANDRAIL_KEY"
872
- `,
873
- command: `codex mcp add brandrail --url ${url} --bearer-token-env-var BRANDRAIL_KEY`
874
- },
875
- {
876
- id: "cursor",
877
- name: "Cursor",
878
- file: ".cursor/mcp.json",
879
- projectFile: true,
880
- content: json({ mcpServers: { brandrail: { url, headers: { Authorization: "Bearer ${env:BRANDRAIL_KEY}" } } } })
881
- },
882
- {
883
- id: "vscode",
884
- name: "GitHub Copilot (VS Code)",
885
- file: ".vscode/mcp.json",
886
- projectFile: true,
887
- content: json({
888
- inputs: [{ type: "promptString", id: "brandrail-key", description: "Brandrail API key", password: true }],
889
- servers: { brandrail: { type: "http", url, headers: { Authorization: "Bearer ${input:brandrail-key}" } } }
890
- })
891
- },
892
- {
893
- id: "windsurf",
894
- name: "Windsurf",
895
- file: "~/.codeium/windsurf/mcp_config.json",
896
- projectFile: false,
897
- content: json({ mcpServers: { brandrail: { serverUrl: url, headers: { Authorization: "Bearer ${env:BRANDRAIL_KEY}" } } } })
898
- }
899
- ];
900
- }
901
- var RULE_FILES = [
902
- { file: "AGENTS.md", readBy: "Codex, Jules, Amp, Cursor and other agents" },
903
- { file: "CLAUDE.md", readBy: "Claude Code" },
904
- { file: ".cursor/rules/brandrail.mdc", readBy: "Cursor" },
905
- { file: ".github/copilot-instructions.md", readBy: "GitHub Copilot" },
906
- { file: ".windsurf/rules/brandrail.md", readBy: "Windsurf" },
907
- { file: "GEMINI.md", readBy: "Gemini CLI" }
908
- ];
909
-
910
787
  // ../engine/src/tokens.ts
911
788
  var EXT = "dev.brandrail";
912
789
  function slug(s) {
@@ -1374,6 +1251,139 @@ function mergeTokens(base, incoming) {
1374
1251
  return buildDtcg([...m.values()]);
1375
1252
  }
1376
1253
 
1254
+ // ../engine/src/instructions.ts
1255
+ var RULES_START = "<!-- brandrail:start -->";
1256
+ var RULES_END = "<!-- brandrail:end -->";
1257
+ function generateInstructions(config, opts = {}) {
1258
+ const r = resolveConfig(config);
1259
+ const lines = [];
1260
+ lines.push(`# Design system rules${opts.org ? ` \u2014 ${opts.org}` : ""}`);
1261
+ lines.push("");
1262
+ lines.push(
1263
+ `All UI in this repo must follow the Brand Kit${r.tokenVersion ? ` (v${r.tokenVersion})` : ""}. These rules are checked on every pull request by Brandrail.`
1264
+ );
1265
+ lines.push("");
1266
+ if (opts.tokensCss) {
1267
+ lines.push("## Setup");
1268
+ lines.push(`- \`${opts.tokensCss}\` defines every CSS variable below (light and dark). Import it once at the app entry, before other styles. Don't edit it: it's generated from the Brand Kit.`);
1269
+ lines.push("");
1270
+ }
1271
+ if (r.components.length) {
1272
+ lines.push("## Components");
1273
+ lines.push("Always use these components. Never re-create them with raw HTML elements:");
1274
+ for (const c of r.components) {
1275
+ const tags = [...r.replaces].filter(([, v]) => v === c).map(([t]) => `<${t}>`);
1276
+ lines.push(`- \`${c.name}\` from \`${c.importPath}\`${tags.length ? ` (instead of ${tags.join(", ")})` : ""}`);
1277
+ }
1278
+ lines.push("");
1279
+ }
1280
+ if (r.colors.length) {
1281
+ lines.push("## Colours");
1282
+ lines.push("Use CSS variables only. Never write hex, rgb() or hsl() values, and never use Tailwind palette classes like `bg-blue-500`.");
1283
+ for (const c of r.colors) {
1284
+ if (c.mode) continue;
1285
+ const dark = r.colors.find((d) => d.mode === "dark" && d.path === c.path);
1286
+ lines.push(`- \`var(${c.cssVar})\` \u2014 ${c.path} (${c.hex}${dark ? `, dark ${dark.hex}` : ""})`);
1287
+ }
1288
+ lines.push("");
1289
+ }
1290
+ lines.push("## Spacing, radius and type");
1291
+ const named = (prefix) => flattenTokens(config.tokens).filter((t) => prefix.test(t.path)).map((t) => `\`var(--${t.path.replace(/\./g, "-")})\` ${t.value}`);
1292
+ const spaceVars = named(/^(space|spacing)\./);
1293
+ const radiusVars = named(/^radius\./);
1294
+ const sizeVars = named(/^font\.size\./);
1295
+ lines.push(`- Spacing (padding, margin, gap) must be one of: ${r.spacing.filter((s) => s > 2).map((s) => `${s}px`).join(", ")}.${spaceVars.length ? ` Prefer the variables: ${spaceVars.join(", ")}.` : ""}`);
1296
+ lines.push(`- Corner radius must be one of: ${r.radii.filter((x) => x > 0 && x < 999).map((x) => `${x}px`).join(", ")} (or fully rounded).${radiusVars.length ? ` Variables: ${radiusVars.join(", ")}.` : ""}`);
1297
+ if (sizeVars.length) lines.push(`- Font sizes come from the type scale: ${sizeVars.join(", ")}. Matching line heights are \`--font-line-height-*\`.`);
1298
+ if (r.fonts.length) lines.push(`- Fonts: use ${r.fontVars.map((v) => `\`var(${v})\``).join(" or ")}; never set another font-family.`);
1299
+ lines.push("");
1300
+ lines.push("## Before you finish");
1301
+ lines.push("- If the Brandrail MCP server is connected, call `validate_snippet` on the UI you wrote and fix every violation.");
1302
+ lines.push("- Otherwise run `npx brandrail check` and fix errors before committing.");
1303
+ lines.push("");
1304
+ lines.push("<details><summary>Checked rules</summary>");
1305
+ lines.push("");
1306
+ for (const rule of RULES) {
1307
+ if (r.rules[rule.id] === "off") continue;
1308
+ lines.push(`- **${rule.title}** (\`${rule.id}\`): ${rule.description}`);
1309
+ }
1310
+ lines.push("");
1311
+ lines.push("</details>");
1312
+ return lines.join("\n");
1313
+ }
1314
+ function upsertSection(existing, section) {
1315
+ const block = `${RULES_START}
1316
+ ${section}
1317
+ ${RULES_END}`;
1318
+ const start = existing.indexOf(RULES_START);
1319
+ const end = existing.indexOf(RULES_END);
1320
+ if (start !== -1 && end > start) return existing.slice(0, start) + block + existing.slice(end + RULES_END.length);
1321
+ return existing.trim() ? `${existing.trimEnd()}
1322
+
1323
+ ${block}
1324
+ ` : `${block}
1325
+ `;
1326
+ }
1327
+
1328
+ // ../engine/src/mcp-config.ts
1329
+ var json = (o) => `${JSON.stringify(o, null, 2)}
1330
+ `;
1331
+ function mcpClientConfigs(url) {
1332
+ return [
1333
+ {
1334
+ id: "claude-code",
1335
+ name: "Claude Code",
1336
+ file: ".mcp.json",
1337
+ projectFile: true,
1338
+ content: json({ mcpServers: { brandrail: { type: "http", url, headers: { Authorization: "Bearer ${BRANDRAIL_KEY}" } } } }),
1339
+ command: `claude mcp add --transport http brandrail ${url} --header "Authorization: Bearer $BRANDRAIL_KEY"`
1340
+ },
1341
+ {
1342
+ id: "codex",
1343
+ name: "Codex",
1344
+ file: "~/.codex/config.toml",
1345
+ projectFile: false,
1346
+ content: `[mcp_servers.brandrail]
1347
+ url = "${url}"
1348
+ bearer_token_env_var = "BRANDRAIL_KEY"
1349
+ `,
1350
+ command: `codex mcp add brandrail --url ${url} --bearer-token-env-var BRANDRAIL_KEY`
1351
+ },
1352
+ {
1353
+ id: "cursor",
1354
+ name: "Cursor",
1355
+ file: ".cursor/mcp.json",
1356
+ projectFile: true,
1357
+ content: json({ mcpServers: { brandrail: { url, headers: { Authorization: "Bearer ${env:BRANDRAIL_KEY}" } } } })
1358
+ },
1359
+ {
1360
+ id: "vscode",
1361
+ name: "GitHub Copilot (VS Code)",
1362
+ file: ".vscode/mcp.json",
1363
+ projectFile: true,
1364
+ content: json({
1365
+ inputs: [{ type: "promptString", id: "brandrail-key", description: "Brandrail API key", password: true }],
1366
+ servers: { brandrail: { type: "http", url, headers: { Authorization: "Bearer ${input:brandrail-key}" } } }
1367
+ })
1368
+ },
1369
+ {
1370
+ id: "windsurf",
1371
+ name: "Windsurf",
1372
+ file: "~/.codeium/windsurf/mcp_config.json",
1373
+ projectFile: false,
1374
+ content: json({ mcpServers: { brandrail: { serverUrl: url, headers: { Authorization: "Bearer ${env:BRANDRAIL_KEY}" } } } })
1375
+ }
1376
+ ];
1377
+ }
1378
+ var RULE_FILES = [
1379
+ { file: "AGENTS.md", readBy: "Codex, Jules, Amp, Cursor and other agents" },
1380
+ { file: "CLAUDE.md", readBy: "Claude Code" },
1381
+ { file: ".cursor/rules/brandrail.mdc", readBy: "Cursor" },
1382
+ { file: ".github/copilot-instructions.md", readBy: "GitHub Copilot" },
1383
+ { file: ".windsurf/rules/brandrail.md", readBy: "Windsurf" },
1384
+ { file: "GEMINI.md", readBy: "Gemini CLI" }
1385
+ ];
1386
+
1377
1387
  // ../engine/src/rendered.ts
1378
1388
  function collectRenderedNodes(maxNodes = 3e3) {
1379
1389
  const SKIP = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "IFRAME", "HEAD", "META", "LINK", "BR", "WBR", "SOURCE", "TRACK"]);
@@ -1587,6 +1597,17 @@ function checkRendered(pages, configInput, opts = {}) {
1587
1597
  }
1588
1598
  }
1589
1599
  const path = pagePath(page.url);
1600
+ if (page.nodes.length < 5) {
1601
+ violations.push({
1602
+ ruleId: "page-rendered",
1603
+ severity: "error",
1604
+ file: path,
1605
+ line: 0,
1606
+ column: 0,
1607
+ message: `The page rendered ${page.nodes.length} visible element${page.nodes.length === 1 ? "" : "s"}, so there was nothing to check`,
1608
+ suggestion: "Open it in a browser: it may have crashed, need a login (use --storage-state), or need longer to load (--wait)."
1609
+ });
1610
+ }
1590
1611
  for (const g of groups.values()) {
1591
1612
  violations.push({
1592
1613
  ruleId: g.ruleId,
@@ -1631,6 +1652,21 @@ function pagePath(url) {
1631
1652
  }
1632
1653
  }
1633
1654
 
1655
+ // ../engine/src/tokens-css.ts
1656
+ var cssVar = (path) => `--${path.replace(/\./g, "-")}`;
1657
+ function generateTokensCss(tokens, opts = {}) {
1658
+ const flat = flattenTokens(tokens);
1659
+ const head = `/* Generated by Brandrail from the ${opts.org ? `${opts.org} ` : ""}Brand Kit${opts.version ? ` v${opts.version}` : ""}. Do not edit: run \`npx brandrail init --from-api\` to update. */`;
1660
+ const light = flat.map((t) => ` ${cssVar(t.path)}: ${t.type === "fontFamily" ? t.value : t.value};`);
1661
+ const dark = flat.filter((t) => t.dark).map((t) => ` ${cssVar(t.path)}: ${t.dark};`);
1662
+ const out = [head, "", ":root {", ...light, "}"];
1663
+ if (dark.length) {
1664
+ out.push("", '[data-theme="dark"] {', ...dark, "}", "", "@media (prefers-color-scheme: dark) {", ' :root:not([data-theme="light"]) {', ...dark.map((l) => ` ${l}`), " }", "}");
1665
+ }
1666
+ return `${out.join("\n")}
1667
+ `;
1668
+ }
1669
+
1634
1670
  // src/api.ts
1635
1671
  var DEFAULT_API = "https://web-phi-nine-77.vercel.app";
1636
1672
  function apiBase() {
@@ -1834,6 +1870,7 @@ Options for init
1834
1870
  --no-rules Don't write AGENTS.md (Codex), CLAUDE.md, Cursor, Copilot, Windsurf, Gemini rule files
1835
1871
  --mcp Also add the Brandrail MCP server to .mcp.json, .cursor/mcp.json, .vscode/mcp.json
1836
1872
  and print the Codex / Windsurf config
1873
+ --tokens-css <file> Where to write the CSS variables (default src/brandrail.tokens.css); --no-tokens-css to skip
1837
1874
  --force Overwrite an existing brandrail.config.json
1838
1875
 
1839
1876
  Options for scan
@@ -1863,7 +1900,7 @@ function parseArgs(argv) {
1863
1900
  const [k, v] = a.slice(2).split("=", 2);
1864
1901
  const next = rest[i + 1];
1865
1902
  if (v !== void 0) flags[k] = v;
1866
- else if (next !== void 0 && !next.startsWith("-") && ["config", "format", "fail-below", "changed", "pages", "storage-state", "wait", "width"].includes(k)) {
1903
+ else if (next !== void 0 && !next.startsWith("-") && ["config", "format", "fail-below", "changed", "pages", "storage-state", "wait", "width", "tokens-css"].includes(k)) {
1867
1904
  flags[k] = next;
1868
1905
  i++;
1869
1906
  } else flags[k] = true;
@@ -1934,8 +1971,18 @@ Brand score ${report.score} is below ${threshold}.`);
1934
1971
  }
1935
1972
  return 0;
1936
1973
  }
1937
- function writeRuleFiles(root, config, org) {
1938
- const md = generateInstructions(config, { org });
1974
+ function tokensCssPath(root, config, flags) {
1975
+ if (typeof flags["tokens-css"] === "string") return flags["tokens-css"];
1976
+ if (config.tokensCss) return config.tokensCss;
1977
+ return existsSync3(join3(root, "src")) ? "src/brandrail.tokens.css" : "brandrail.tokens.css";
1978
+ }
1979
+ function writeTokensCss(root, rel, config, org) {
1980
+ const abs = join3(root, rel);
1981
+ mkdirSync(dirname2(abs), { recursive: true });
1982
+ writeFileSync(abs, generateTokensCss(config.tokens, { version: config.tokenVersion, org }));
1983
+ }
1984
+ function writeRuleFiles(root, config, org, tokensCss) {
1985
+ const md = generateInstructions(config, { org, tokensCss });
1939
1986
  const written = [];
1940
1987
  const write = (rel, content) => {
1941
1988
  const abs = join3(root, rel);
@@ -2018,12 +2065,22 @@ async function cmdInit(cwd, flags) {
2018
2065
  if (existsSync3(target) && !flags.force && !flags["from-api"]) {
2019
2066
  log(`${CONFIG_FILE} already exists (use --force to overwrite). Updating rule files from it.`);
2020
2067
  config = readConfig(target);
2068
+ org ??= config.org;
2021
2069
  } else {
2070
+ if (existsSync3(target)) {
2071
+ const prev = readConfig(target);
2072
+ config = { ...prev, ...config, rules: { ...prev.rules, ...config.rules }, ignore: prev.ignore ?? config.ignore, tokensCss: prev.tokensCss };
2073
+ }
2022
2074
  writeFileSync(target, `${JSON.stringify(config, null, 2)}
2023
2075
  `);
2024
2076
  log(`\u2714 wrote ${CONFIG_FILE}${org ? ` from ${org}'s Brand Kit` : " (starter tokens \u2014 edit them or use --from-api)"}`);
2025
2077
  }
2026
- if (flags.rules !== false) for (const f of writeRuleFiles(cwd, config, org)) log(`\u2714 updated ${f}`);
2078
+ const css = flags["tokens-css"] === false ? void 0 : tokensCssPath(cwd, config, flags);
2079
+ if (css) {
2080
+ writeTokensCss(cwd, css, config, org);
2081
+ log(`\u2714 wrote ${css} (the Brand Kit as CSS variables \u2014 import it once at your app entry)`);
2082
+ }
2083
+ if (flags.rules !== false) for (const f of writeRuleFiles(cwd, config, org, css)) log(`\u2714 updated ${f}`);
2027
2084
  if (flags.mcp) writeMcpConfigs(cwd);
2028
2085
  log("\nNext: npx brandrail check");
2029
2086
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brandrail",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
@@ -23,7 +23,7 @@
23
23
  "license": "UNLICENSED",
24
24
  "type": "module",
25
25
  "bin": {
26
- "brandrail": "./dist/brandrail.mjs"
26
+ "brandrail": "dist/brandrail.mjs"
27
27
  },
28
28
  "files": [
29
29
  "dist",
@@ -36,7 +36,7 @@
36
36
  "access": "public"
37
37
  },
38
38
  "scripts": {
39
- "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/brandrail.mjs --external:playwright --external:playwright-core --banner:js='#!/usr/bin/env node'",
39
+ "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/brandrail.mjs --external:playwright --external:playwright-core --banner:js='#!/usr/bin/env node' && chmod +x dist/brandrail.mjs",
40
40
  "typecheck": "tsc --noEmit",
41
41
  "prepublishOnly": "pnpm run build"
42
42
  },