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.
- package/dist/brandrail.mjs +188 -78
- package/package.json +1 -1
package/dist/brandrail.mjs
CHANGED
|
@@ -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,
|
|
3914
|
+
function resolveComponent(base, relative4, options, skipNormalization) {
|
|
3915
3915
|
const target = {};
|
|
3916
3916
|
if (!skipNormalization) {
|
|
3917
3917
|
base = parse3(serialize(base, options), options);
|
|
3918
|
-
|
|
3918
|
+
relative4 = parse3(serialize(relative4, options), options);
|
|
3919
3919
|
}
|
|
3920
3920
|
options = options || {};
|
|
3921
|
-
if (!options.tolerant &&
|
|
3922
|
-
target.scheme =
|
|
3923
|
-
target.userinfo =
|
|
3924
|
-
target.host =
|
|
3925
|
-
target.port =
|
|
3926
|
-
target.path = removeDotSegments(
|
|
3927
|
-
target.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 (
|
|
3930
|
-
target.userinfo =
|
|
3931
|
-
target.host =
|
|
3932
|
-
target.port =
|
|
3933
|
-
target.path = removeDotSegments(
|
|
3934
|
-
target.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 (!
|
|
3936
|
+
if (!relative4.path) {
|
|
3937
3937
|
target.path = base.path;
|
|
3938
|
-
if (
|
|
3939
|
-
target.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 (
|
|
3945
|
-
target.path = removeDotSegments(
|
|
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 = "/" +
|
|
3949
|
+
target.path = "/" + relative4.path;
|
|
3950
3950
|
} else if (!base.path) {
|
|
3951
|
-
target.path =
|
|
3951
|
+
target.path = relative4.path;
|
|
3952
3952
|
} else {
|
|
3953
|
-
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) +
|
|
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 =
|
|
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 =
|
|
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
|
|
7260
|
-
import { basename as basename2, dirname as dirname4, join as
|
|
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.
|
|
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
|
|
40964
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
40891
40965
|
import { createRequire as createRequire2 } from "node:module";
|
|
40892
|
-
import { join as
|
|
40966
|
+
import { join as join6, resolve as resolve2 } from "node:path";
|
|
40893
40967
|
function loadPlaywright(cwd) {
|
|
40894
|
-
for (const base of [
|
|
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 && !
|
|
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 (!
|
|
41112
|
+
if (!existsSync7(path)) return void 0;
|
|
41036
41113
|
try {
|
|
41037
|
-
return JSON.parse(
|
|
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:
|
|
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) ??
|
|
41057
|
-
const
|
|
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
|
-
|
|
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
|
|
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 =
|
|
41110
|
-
|
|
41217
|
+
const abs = join7(root, rel);
|
|
41218
|
+
mkdirSync4(dirname4(abs), { recursive: true });
|
|
41111
41219
|
const { css, usesKit } = tokensCssContent(config2, org);
|
|
41112
|
-
|
|
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 =
|
|
41120
|
-
|
|
41121
|
-
const before =
|
|
41122
|
-
|
|
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(
|
|
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 =
|
|
41131
|
-
|
|
41132
|
-
|
|
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 =
|
|
41143
|
-
|
|
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 (
|
|
41254
|
+
if (existsSync7(abs)) {
|
|
41147
41255
|
try {
|
|
41148
|
-
const existing = JSON.parse(
|
|
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
|
-
|
|
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 =
|
|
41185
|
-
if (!
|
|
41292
|
+
const pkg = join7(cwd, "package.json");
|
|
41293
|
+
if (!existsSync7(pkg)) return detectFramework(null);
|
|
41186
41294
|
try {
|
|
41187
|
-
return detectFramework(JSON.parse(
|
|
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 =
|
|
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 (
|
|
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 (
|
|
41316
|
+
if (existsSync7(target)) {
|
|
41209
41317
|
const prev = readConfig(target);
|
|
41210
41318
|
config2 = mergeProjectConfig(prev, config2);
|
|
41211
41319
|
}
|
|
41212
|
-
|
|
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
|
-
|
|
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(
|
|
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) ??
|
|
41267
|
-
const files = collectFiles(root, positional.map((p) =>
|
|
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"${
|
|
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) ??
|
|
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
|
-
|
|
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 ${
|
|
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(
|
|
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(
|
|
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),
|
|
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)) ??
|
|
41430
|
-
const config2 =
|
|
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
|
-
|
|
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 ${
|
|
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.
|
|
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",
|