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 +2 -2
- package/dist/brandrail.mjs +189 -132
- package/package.json +3 -3
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
|
|
package/dist/brandrail.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
1938
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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
|
},
|