brandrail 0.5.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.
@@ -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";
@@ -8090,6 +8090,396 @@ function detectFramework(pkg) {
8090
8090
  return "html";
8091
8091
  }
8092
8092
 
8093
+ // ../engine/src/components.ts
8094
+ var SYNONYMS = {
8095
+ button: ["btn", "cta", "action", "submit", "click"],
8096
+ input: ["textinput", "textfield", "field", "text", "inputtext", "entry", "form"],
8097
+ select: ["dropdown", "picker", "combobox", "menu", "options", "choose"],
8098
+ textarea: ["multiline", "comment", "notes", "message", "inputtextarea"],
8099
+ checkbox: ["check", "tick", "toggle", "agree"],
8100
+ radio: ["radiogroup", "option", "choice"],
8101
+ toggle: ["switch", "onoff"],
8102
+ dialog: ["modal", "popup", "overlay", "lightbox"],
8103
+ tooltip: ["hint", "hover", "popover"],
8104
+ notification: ["toast", "alert", "flash", "banner", "snackbar", "message"],
8105
+ avatar: ["profile", "user", "picture", "initials"],
8106
+ datepicker: ["date", "calendar", "day", "schedule"],
8107
+ progressbar: ["progress", "loading", "percent", "meter"],
8108
+ loader: ["spinner", "loading", "busy"],
8109
+ breadcrumb: ["path", "trail", "navigation", "nav"],
8110
+ divider: ["separator", "rule", "hr", "line"],
8111
+ table: ["datatable", "grid", "rows", "list"],
8112
+ card: ["box", "panel", "tile", "container"],
8113
+ label: ["badge", "tag", "chip", "pill", "status"],
8114
+ tabs: ["tab", "segmented"]
8115
+ };
8116
+ var norm = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
8117
+ var squash = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
8118
+ function expand(words) {
8119
+ const out = new Set(words);
8120
+ for (const w of words) {
8121
+ for (const [canon, syns] of Object.entries(SYNONYMS)) {
8122
+ if (w === canon || syns.includes(w)) {
8123
+ out.add(canon);
8124
+ for (const s of syns) out.add(s);
8125
+ }
8126
+ }
8127
+ }
8128
+ return out;
8129
+ }
8130
+ function findComponents(config2, intent, limit = 5) {
8131
+ const words = norm(intent).split(" ").filter((w) => w.length > 1);
8132
+ const query = expand(words);
8133
+ const q = squash(intent);
8134
+ const out = [];
8135
+ for (const c of config2.components ?? []) {
8136
+ if (c.status === "deprecated") continue;
8137
+ const name = squash(c.name);
8138
+ let score = 0;
8139
+ if (name === q) score += 100;
8140
+ else if (q.includes(name) || name.includes(q)) score += 40;
8141
+ const nameTerms = expand([name, ...(c.aliases ?? []).map(squash), ...c.replaces ?? []]);
8142
+ for (const w of query) {
8143
+ if (nameTerms.has(w)) score += 20;
8144
+ else if (name.includes(w) && w.length > 2) score += 10;
8145
+ }
8146
+ const text2 = norm([c.description ?? "", ...c.usageRules ?? []].join(" "));
8147
+ for (const w of words) if (w.length > 2 && text2.includes(w)) score += 3;
8148
+ if (score >= 3) out.push({ component: c, score: score + (c.status === "stable" ? 1 : 0) });
8149
+ }
8150
+ return out.sort((a, b) => b.score - a.score || a.component.name.localeCompare(b.component.name)).slice(0, limit);
8151
+ }
8152
+ function findComponentByName(config2, name) {
8153
+ const n = squash(name);
8154
+ return (config2.components ?? []).find((c) => squash(c.name) === n || (c.aliases ?? []).some((a) => squash(a) === n));
8155
+ }
8156
+
8157
+ // ../engine/src/migrate.ts
8158
+ var LIBRARY_LABEL = {
8159
+ mui: "MUI (Material UI)",
8160
+ "angular-material": "Angular Material",
8161
+ bootstrap: "Bootstrap",
8162
+ antd: "Ant Design",
8163
+ chakra: "Chakra UI",
8164
+ "react-bootstrap": "React Bootstrap",
8165
+ primeng: "PrimeNG"
8166
+ };
8167
+ var CONCEPT_TAG = {
8168
+ button: "button",
8169
+ input: "input",
8170
+ textarea: "textarea",
8171
+ select: "select",
8172
+ checkbox: "input[type=checkbox]",
8173
+ radio: "input[type=radio]",
8174
+ table: "table"
8175
+ };
8176
+ var REACT_LIBS = [
8177
+ {
8178
+ id: "mui",
8179
+ packages: /^(@mui\/material|@mui\/joy|@material-ui\/core|@mui\/x-date-pickers)(\/|$)/,
8180
+ names: {
8181
+ Button: "button",
8182
+ IconButton: "button",
8183
+ Fab: "button",
8184
+ LoadingButton: "button",
8185
+ TextField: "input",
8186
+ Input: "input",
8187
+ OutlinedInput: "input",
8188
+ Select: "select",
8189
+ Autocomplete: "select",
8190
+ NativeSelect: "select",
8191
+ Checkbox: "checkbox",
8192
+ Radio: "radio",
8193
+ RadioGroup: "radio",
8194
+ Switch: "toggle",
8195
+ Dialog: "dialog",
8196
+ Modal: "dialog",
8197
+ Drawer: "dialog",
8198
+ Tooltip: "tooltip",
8199
+ Avatar: "avatar",
8200
+ Chip: "label",
8201
+ Badge: "label",
8202
+ CircularProgress: "loader",
8203
+ Skeleton: "loader",
8204
+ LinearProgress: "progressbar",
8205
+ Breadcrumbs: "breadcrumb",
8206
+ Divider: "divider",
8207
+ Snackbar: "notification",
8208
+ Alert: "notification",
8209
+ DatePicker: "datepicker",
8210
+ DateField: "datepicker",
8211
+ Tabs: "tabs",
8212
+ Tab: "tabs",
8213
+ Card: "card",
8214
+ Paper: "card",
8215
+ Table: "table",
8216
+ TextareaAutosize: "textarea"
8217
+ }
8218
+ },
8219
+ {
8220
+ id: "antd",
8221
+ packages: /^antd(\/|$)/,
8222
+ names: {
8223
+ Button: "button",
8224
+ Input: "input",
8225
+ InputNumber: "input",
8226
+ Select: "select",
8227
+ Checkbox: "checkbox",
8228
+ Radio: "radio",
8229
+ Switch: "toggle",
8230
+ Modal: "dialog",
8231
+ Drawer: "dialog",
8232
+ Tooltip: "tooltip",
8233
+ Popover: "tooltip",
8234
+ Avatar: "avatar",
8235
+ Tag: "label",
8236
+ Badge: "label",
8237
+ Spin: "loader",
8238
+ Progress: "progressbar",
8239
+ Breadcrumb: "breadcrumb",
8240
+ Divider: "divider",
8241
+ Alert: "notification",
8242
+ DatePicker: "datepicker",
8243
+ Tabs: "tabs",
8244
+ Card: "card",
8245
+ Table: "table"
8246
+ }
8247
+ },
8248
+ {
8249
+ id: "chakra",
8250
+ packages: /^@chakra-ui\/react(\/|$)/,
8251
+ names: {
8252
+ Button: "button",
8253
+ IconButton: "button",
8254
+ Input: "input",
8255
+ Textarea: "textarea",
8256
+ Select: "select",
8257
+ Checkbox: "checkbox",
8258
+ Radio: "radio",
8259
+ Switch: "toggle",
8260
+ Modal: "dialog",
8261
+ Drawer: "dialog",
8262
+ Tooltip: "tooltip",
8263
+ Avatar: "avatar",
8264
+ Tag: "label",
8265
+ Badge: "label",
8266
+ Spinner: "loader",
8267
+ Progress: "progressbar",
8268
+ Breadcrumb: "breadcrumb",
8269
+ Divider: "divider",
8270
+ Alert: "notification",
8271
+ Tabs: "tabs",
8272
+ Card: "card",
8273
+ Table: "table"
8274
+ }
8275
+ },
8276
+ {
8277
+ id: "react-bootstrap",
8278
+ packages: /^react-bootstrap(\/|$)/,
8279
+ names: {
8280
+ Button: "button",
8281
+ Form: "input",
8282
+ FormControl: "input",
8283
+ FormSelect: "select",
8284
+ FormCheck: "checkbox",
8285
+ Modal: "dialog",
8286
+ Offcanvas: "dialog",
8287
+ Tooltip: "tooltip",
8288
+ OverlayTrigger: "tooltip",
8289
+ Badge: "label",
8290
+ Spinner: "loader",
8291
+ ProgressBar: "progressbar",
8292
+ Breadcrumb: "breadcrumb",
8293
+ Alert: "notification",
8294
+ Toast: "notification",
8295
+ Tabs: "tabs",
8296
+ Tab: "tabs",
8297
+ Card: "card",
8298
+ Table: "table"
8299
+ }
8300
+ }
8301
+ ];
8302
+ var MAT_TAGS = {
8303
+ "mat-form-field": "input",
8304
+ "mat-select": "select",
8305
+ "mat-autocomplete": "select",
8306
+ "mat-checkbox": "checkbox",
8307
+ "mat-radio-group": "radio",
8308
+ "mat-radio-button": "radio",
8309
+ "mat-slide-toggle": "toggle",
8310
+ "mat-progress-bar": "progressbar",
8311
+ "mat-spinner": "loader",
8312
+ "mat-progress-spinner": "loader",
8313
+ "mat-datepicker": "datepicker",
8314
+ "mat-date-range-picker": "datepicker",
8315
+ "mat-divider": "divider",
8316
+ "mat-chip": "label",
8317
+ "mat-chip-listbox": "label",
8318
+ "mat-chip-option": "label",
8319
+ "mat-tab-group": "tabs",
8320
+ "mat-card": "card",
8321
+ "mat-table": "table",
8322
+ "mat-dialog-content": "dialog",
8323
+ "mat-button-toggle-group": "button"
8324
+ };
8325
+ var MAT_ATTRS = {
8326
+ "mat-button": "button",
8327
+ "mat-raised-button": "button",
8328
+ "mat-flat-button": "button",
8329
+ "mat-stroked-button": "button",
8330
+ "mat-icon-button": "button",
8331
+ "mat-fab": "button",
8332
+ "mat-mini-fab": "button",
8333
+ matButton: "button",
8334
+ matInput: "input",
8335
+ matTooltip: "tooltip",
8336
+ matBadge: "label",
8337
+ "mat-table": "table"
8338
+ };
8339
+ var PRIME_TAGS = {
8340
+ "p-button": "button",
8341
+ "p-dropdown": "select",
8342
+ "p-select": "select",
8343
+ "p-multiselect": "select",
8344
+ "p-checkbox": "checkbox",
8345
+ "p-radiobutton": "radio",
8346
+ "p-inputswitch": "toggle",
8347
+ "p-toggleswitch": "toggle",
8348
+ "p-dialog": "dialog",
8349
+ "p-avatar": "avatar",
8350
+ "p-tag": "label",
8351
+ "p-badge": "label",
8352
+ "p-progressspinner": "loader",
8353
+ "p-progressbar": "progressbar",
8354
+ "p-breadcrumb": "breadcrumb",
8355
+ "p-divider": "divider",
8356
+ "p-toast": "notification",
8357
+ "p-message": "notification",
8358
+ "p-calendar": "datepicker",
8359
+ "p-datepicker": "datepicker",
8360
+ "p-tabview": "tabs",
8361
+ "p-tabs": "tabs",
8362
+ "p-card": "card",
8363
+ "p-table": "table"
8364
+ };
8365
+ var BOOTSTRAP_CLASSES = [
8366
+ [/^btn$/, "button", "btn"],
8367
+ [/^form-control$/, "input", "form-control"],
8368
+ [/^(form-select|custom-select)$/, "select", "form-select"],
8369
+ [/^form-switch$/, "toggle", "form-switch"],
8370
+ [/^(form-check-input|custom-control-input)$/, "checkbox", "form-check-input"],
8371
+ [/^modal$/, "dialog", "modal"],
8372
+ [/^alert$/, "notification", "alert"],
8373
+ [/^toast$/, "notification", "toast"],
8374
+ [/^badge$/, "label", "badge"],
8375
+ [/^spinner-(border|grow)$/, "loader", "spinner"],
8376
+ [/^progress$/, "progressbar", "progress"],
8377
+ [/^breadcrumb$/, "breadcrumb", "breadcrumb"],
8378
+ [/^nav-tabs$/, "tabs", "nav-tabs"],
8379
+ [/^card$/, "card", "card"]
8380
+ ];
8381
+ var BOOTSTRAP_EVIDENCE = /\b(?:btn-(?:primary|secondary|success|danger|warning|info|light|dark|link|outline-[a-z]+)|form-control|form-select|form-check-input|custom-select|custom-control-input|spinner-border|spinner-grow|col-(?:xs|sm|md|lg|xl|xxl)-\d{1,2}|data-bs-[a-z]+|data-toggle)\b/;
8382
+ function* matchAll(re, text2) {
8383
+ let m;
8384
+ const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
8385
+ while (m = g.exec(text2)) {
8386
+ yield m;
8387
+ if (m[0].length === 0) g.lastIndex++;
8388
+ }
8389
+ }
8390
+ function findLegacyUsages(code, kind) {
8391
+ const out = [];
8392
+ if (kind === "css") return out;
8393
+ for (const lib of REACT_LIBS) {
8394
+ const local = /* @__PURE__ */ new Map();
8395
+ for (const m of matchAll(/\bimport\s+(?:type\s+)?(?:(\w+)\s*,?\s*)?(?:\{([^}]*)\})?\s*from\s*(['"])([^'"]+)\3/g, code)) {
8396
+ if (!lib.packages.test(m[4])) continue;
8397
+ const sub = /\/([A-Z]\w*)$/.exec(m[4])?.[1];
8398
+ if (m[1] && sub && lib.names[sub]) local.set(m[1], sub);
8399
+ for (const part of (m[2] ?? "").split(",")) {
8400
+ const [orig, alias] = part.trim().replace(/^type\s+/, "").split(/\s+as\s+/).map((s) => s.trim());
8401
+ if (orig && lib.names[orig]) local.set(alias || orig, orig);
8402
+ }
8403
+ }
8404
+ if (!local.size) continue;
8405
+ const names = [...local.keys()].map((n) => n.replace(/\$/g, "\\$")).join("|");
8406
+ for (const m of matchAll(new RegExp(`<(${names})(?=[\\s/>])`, "g"), code)) {
8407
+ const orig = local.get(m[1]);
8408
+ out.push({ offset: m.index, library: lib.id, what: `${LIBRARY_LABEL[lib.id].split(" ")[0]} <${orig}>`, concept: lib.names[orig] });
8409
+ }
8410
+ }
8411
+ for (const m of matchAll(/<((?:mat|p)-[a-z-]+)(?=[\s/>])([^>]*)>/g, code)) {
8412
+ const tag = m[1];
8413
+ const concept = MAT_TAGS[tag] ?? PRIME_TAGS[tag];
8414
+ if (concept) out.push({ offset: m.index, library: tag.startsWith("mat-") ? "angular-material" : "primeng", what: `<${tag}>`, concept });
8415
+ }
8416
+ for (const m of matchAll(/<([a-z][\w-]*)(?=[\s/>])([^>]*)>/g, code)) {
8417
+ for (const a of matchAll(/(?<=\s)\[?(mat-raised-button|mat-flat-button|mat-stroked-button|mat-icon-button|mat-button|mat-mini-fab|mat-fab|mat-table|matButton|matInput|matTooltip|matBadge)\]?(?=[\s=>/]|$)/g, m[2])) {
8418
+ const name = a[1];
8419
+ const concept = MAT_ATTRS[name];
8420
+ if (concept) out.push({ offset: m.index, library: "angular-material", what: `<${m[1]} ${name}>`, concept });
8421
+ }
8422
+ }
8423
+ if (!BOOTSTRAP_EVIDENCE.test(code)) return out.sort((a, b) => a.offset - b.offset);
8424
+ for (const m of matchAll(/\b(?:class|className)\s*=\s*(["'])([^"']*)\1/g, code)) {
8425
+ const seen = /* @__PURE__ */ new Set();
8426
+ for (const cls of m[2].split(/\s+/)) {
8427
+ for (const [re, concept, label] of BOOTSTRAP_CLASSES) {
8428
+ if (re.test(cls) && !seen.has(concept)) {
8429
+ seen.add(concept);
8430
+ out.push({ offset: m.index, library: "bootstrap", what: `class "${label}"`, concept });
8431
+ }
8432
+ }
8433
+ }
8434
+ }
8435
+ for (const m of matchAll(/data-(?:bs-)?toggle\s*=\s*["'](tooltip|modal|popover)["']/g, code)) {
8436
+ out.push({ offset: m.index, library: "bootstrap", what: `data-bs-toggle="${m[1]}"`, concept: m[1] === "modal" ? "dialog" : "tooltip" });
8437
+ }
8438
+ return out.sort((a, b) => a.offset - b.offset);
8439
+ }
8440
+ function componentForConcept(config2, concept) {
8441
+ const tag = CONCEPT_TAG[concept];
8442
+ if (tag && config2.replaces.get(tag)) return config2.replaces.get(tag);
8443
+ return findComponents({ components: config2.components }, concept, 1)[0]?.component;
8444
+ }
8445
+ function migrationMap(config2, framework, libraries) {
8446
+ const rows = [];
8447
+ const add = (library, from, concept) => {
8448
+ if (libraries && !libraries.includes(library)) return;
8449
+ const c = componentForConcept(config2, concept);
8450
+ rows.push({ library, from, concept, to: c ? { name: c.name, usage: resolveUsage(c, framework) } : void 0 });
8451
+ };
8452
+ for (const lib of REACT_LIBS) for (const [name, concept] of Object.entries(lib.names)) add(lib.id, `<${name}>`, concept);
8453
+ for (const [tag, concept] of Object.entries(MAT_TAGS)) add("angular-material", `<${tag}>`, concept);
8454
+ for (const [attr, concept] of Object.entries(MAT_ATTRS)) add("angular-material", attr, concept);
8455
+ for (const [tag, concept] of Object.entries(PRIME_TAGS)) add("primeng", `<${tag}>`, concept);
8456
+ for (const [, concept, label] of BOOTSTRAP_CLASSES) add("bootstrap", `.${label}`, concept);
8457
+ return rows;
8458
+ }
8459
+ function migrationMapMarkdown(config2, framework, libraries) {
8460
+ const rows = migrationMap(config2, framework, libraries);
8461
+ const lines = ["| From | Library | Use instead |", "| --- | --- | --- |"];
8462
+ for (const r of rows) {
8463
+ const to = !r.to ? `_no ${r.concept} component yet: tokens + semantic HTML_` : r.to.usage ? `\`<${r.to.usage.tag}>\`${r.to.usage.importLine ? ` (\`${r.to.usage.importLine}\`)` : ""}` : `_${r.to.name} has no ${framework} version yet_`;
8464
+ lines.push(`| \`${r.from}\` | ${LIBRARY_LABEL[r.library]} | ${to} |`);
8465
+ }
8466
+ return lines.join("\n");
8467
+ }
8468
+ function detectLibraries(pkg) {
8469
+ if (!pkg) return [];
8470
+ const deps = { ...pkg.devDependencies, ...pkg.dependencies };
8471
+ const has = (re) => Object.keys(deps).some((d) => re.test(d));
8472
+ const out = [];
8473
+ if (has(/^(@mui\/material|@mui\/joy|@material-ui\/core)$/)) out.push("mui");
8474
+ if (has(/^@angular\/material$/)) out.push("angular-material");
8475
+ if (has(/^(bootstrap|ngx-bootstrap|@ng-bootstrap\/ng-bootstrap|bootstrap-vue(-next)?)$/)) out.push("bootstrap");
8476
+ if (has(/^react-bootstrap$/)) out.push("react-bootstrap", "bootstrap");
8477
+ if (has(/^antd$/)) out.push("antd");
8478
+ if (has(/^@chakra-ui\/react$/)) out.push("chakra");
8479
+ if (has(/^primeng$/)) out.push("primeng");
8480
+ return [...new Set(out)];
8481
+ }
8482
+
8093
8483
  // ../engine/src/rules.ts
8094
8484
  var MARKUP = ["jsx", "vue", "svelte", "html", "template"];
8095
8485
  var ALL = ["jsx", "vue", "svelte", "html", "template", "css"];
@@ -8623,6 +9013,26 @@ var componentProps = {
8623
9013
  return out;
8624
9014
  }
8625
9015
  };
9016
+ var legacyLibrary = {
9017
+ id: "legacy-library",
9018
+ title: "Other UI library in use",
9019
+ description: "Pieces of Material, MUI, Bootstrap, Ant Design, Chakra or PrimeNG that a design system component replaces. The count is the migration still to do.",
9020
+ defaultSeverity: "warning",
9021
+ kinds: ["jsx", "vue", "svelte", "html", "template"],
9022
+ check({ code, kind, config: config2, framework }) {
9023
+ const out = [];
9024
+ for (const u of findLegacyUsages(code, kind)) {
9025
+ const comp = componentForConcept(config2, u.concept);
9026
+ const usage = comp ? resolveUsage(comp, framework) : null;
9027
+ out.push({
9028
+ offset: u.offset,
9029
+ message: `${u.what} (${LIBRARY_LABEL[u.library]}) should be ${comp ? comp.name : `a design system ${u.concept}`}`,
9030
+ suggestion: comp && usage ? useSuggestion(comp, usage) : comp ? `${comp.name} has no ${FRAMEWORK_LABEL[framework]} version yet: add one in the registry, or build it with tokens` : `No ${u.concept} component in the registry yet: add one, or build it with tokens and semantic HTML`
9031
+ });
9032
+ }
9033
+ return out;
9034
+ }
9035
+ };
8626
9036
  var RULES = [
8627
9037
  noHardcodedColor,
8628
9038
  noRawPaletteClass,
@@ -8632,7 +9042,8 @@ var RULES = [
8632
9042
  fontFamily,
8633
9043
  radiusScale,
8634
9044
  imgAlt,
8635
- componentProps
9045
+ componentProps,
9046
+ legacyLibrary
8636
9047
  ];
8637
9048
  var RULE_IDS = RULES.map((r) => r.id);
8638
9049
 
@@ -8774,7 +9185,7 @@ function makeIgnore(globs) {
8774
9185
  }
8775
9186
 
8776
9187
  // ../engine/src/check.ts
8777
- var ENGINE_VERSION = "0.5.0";
9188
+ var ENGINE_VERSION = "0.7.0";
8778
9189
  var WEIGHT = { error: 4, warning: 1.5, info: 0.25 };
8779
9190
  function computeScore(counts, lines) {
8780
9191
  const penalty = counts.error * WEIGHT.error + counts.warning * WEIGHT.warning + counts.info * WEIGHT.info;
@@ -9636,6 +10047,9 @@ function generateInstructions(config2, opts = {}) {
9636
10047
  if (sizeVars.length) lines.push(`- Font sizes come from the type scale: ${sizeVars.join(", ")}. Matching line heights are \`--font-line-height-*\`.`);
9637
10048
  if (r.fonts.length) lines.push(`- Fonts: use ${r.fontVars.map((v) => `\`var(${v})\``).join(" or ")}; never set another font-family.`);
9638
10049
  lines.push("");
10050
+ lines.push("## Migrating existing UI");
10051
+ lines.push("- Code that still uses Material, MUI, Bootstrap, Ant Design, Chakra, PrimeNG or raw HTML controls is migration work (`legacy-library` findings). `npx brandrail migrate` lists what's left per file. In Claude Code, use the `brandrail-migrate` skill (or the `brandrail-migrator` agent) and work in small batches; keep behaviour identical.");
10052
+ lines.push("");
9639
10053
  lines.push("## Before you finish");
9640
10054
  lines.push("- If the Brandrail MCP server is connected, call `validate_snippet` on the UI you wrote and fix every violation.");
9641
10055
  lines.push("- Otherwise run `npx brandrail check` and fix errors before committing.");
@@ -9664,70 +10078,6 @@ ${block}
9664
10078
  `;
9665
10079
  }
9666
10080
 
9667
- // ../engine/src/components.ts
9668
- var SYNONYMS = {
9669
- button: ["btn", "cta", "action", "submit", "click"],
9670
- input: ["textinput", "textfield", "field", "text", "inputtext", "entry", "form"],
9671
- select: ["dropdown", "picker", "combobox", "menu", "options", "choose"],
9672
- textarea: ["multiline", "comment", "notes", "message", "inputtextarea"],
9673
- checkbox: ["check", "tick", "toggle", "agree"],
9674
- radio: ["radiogroup", "option", "choice"],
9675
- toggle: ["switch", "onoff"],
9676
- dialog: ["modal", "popup", "overlay", "lightbox"],
9677
- tooltip: ["hint", "hover", "popover"],
9678
- notification: ["toast", "alert", "flash", "banner", "snackbar", "message"],
9679
- avatar: ["profile", "user", "picture", "initials"],
9680
- datepicker: ["date", "calendar", "day", "schedule"],
9681
- progressbar: ["progress", "loading", "percent", "meter"],
9682
- loader: ["spinner", "loading", "busy"],
9683
- breadcrumb: ["path", "trail", "navigation", "nav"],
9684
- divider: ["separator", "rule", "hr", "line"],
9685
- table: ["datatable", "grid", "rows", "list"],
9686
- card: ["box", "panel", "tile", "container"],
9687
- label: ["badge", "tag", "chip", "pill", "status"],
9688
- tabs: ["tab", "segmented"]
9689
- };
9690
- var norm = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
9691
- var squash = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
9692
- function expand(words) {
9693
- const out = new Set(words);
9694
- for (const w of words) {
9695
- for (const [canon, syns] of Object.entries(SYNONYMS)) {
9696
- if (w === canon || syns.includes(w)) {
9697
- out.add(canon);
9698
- for (const s of syns) out.add(s);
9699
- }
9700
- }
9701
- }
9702
- return out;
9703
- }
9704
- function findComponents(config2, intent, limit = 5) {
9705
- const words = norm(intent).split(" ").filter((w) => w.length > 1);
9706
- const query = expand(words);
9707
- const q = squash(intent);
9708
- const out = [];
9709
- for (const c of config2.components ?? []) {
9710
- if (c.status === "deprecated") continue;
9711
- const name = squash(c.name);
9712
- let score = 0;
9713
- if (name === q) score += 100;
9714
- else if (q.includes(name) || name.includes(q)) score += 40;
9715
- const nameTerms = expand([name, ...(c.aliases ?? []).map(squash), ...c.replaces ?? []]);
9716
- for (const w of query) {
9717
- if (nameTerms.has(w)) score += 20;
9718
- else if (name.includes(w) && w.length > 2) score += 10;
9719
- }
9720
- const text2 = norm([c.description ?? "", ...c.usageRules ?? []].join(" "));
9721
- for (const w of words) if (w.length > 2 && text2.includes(w)) score += 3;
9722
- if (score >= 3) out.push({ component: c, score: score + (c.status === "stable" ? 1 : 0) });
9723
- }
9724
- return out.sort((a, b) => b.score - a.score || a.component.name.localeCompare(b.component.name)).slice(0, limit);
9725
- }
9726
- function findComponentByName(config2, name) {
9727
- const n = squash(name);
9728
- return (config2.components ?? []).find((c) => squash(c.name) === n || (c.aliases ?? []).some((a) => squash(a) === n));
9729
- }
9730
-
9731
10081
  // ../engine/src/mcp-config.ts
9732
10082
  var json = (o) => `${JSON.stringify(o, null, 2)}
9733
10083
  `;
@@ -10371,6 +10721,227 @@ function auditBrandKit(tokens) {
10371
10721
  }).sort((a, b) => a.severity === b.severity ? 0 : a.severity === "error" ? -1 : 1);
10372
10722
  }
10373
10723
 
10724
+ // ../engine/src/migrate-skill.ts
10725
+ var MIGRATE_SKILL_FILE = ".claude/skills/brandrail-migrate/SKILL.md";
10726
+ var MIGRATE_AGENT_FILE = ".claude/agents/brandrail-migrator.md";
10727
+ function librariesFor(pkg) {
10728
+ if (!pkg) return void 0;
10729
+ return [.../* @__PURE__ */ new Set([...detectLibraries(pkg), "bootstrap"])];
10730
+ }
10731
+ function migrationSkill(config2, opts = {}) {
10732
+ const fw = config2.framework ?? "react";
10733
+ const libs = librariesFor(opts.pkg);
10734
+ const ds = opts.org ? `the ${opts.org} design system` : "the design system";
10735
+ return `---
10736
+ name: brandrail-migrate
10737
+ description: Migrate existing UI in this repo to ${ds}. Replaces Material, MUI, Bootstrap, Ant Design, Chakra, PrimeNG and raw HTML controls with design system components, and hard-coded colours, spacing and fonts with Brand Kit tokens. Use when asked to migrate, reskin, adopt the design system, or clear Brandrail findings across files.
10738
+ ---
10739
+
10740
+ # Migrate UI to ${ds}
10741
+
10742
+ This is ${fw === "angular" ? "an" : "a"} ${FRAMEWORK_LABEL[fw]} project. The design system rules are in CLAUDE.md / AGENTS.md (section "Design system rules"); follow them.
10743
+ If the Brandrail MCP server is connected, use \`get_component_docs\` for each component's props and events, and \`validate_snippet\` on every file you change.
10744
+
10745
+ ## Steps
10746
+
10747
+ 1. **See what's left**: \`npx brandrail migrate\` lists findings per file, worst first (\`--json\` for a machine-readable list).
10748
+ 2. **Pick a small batch**: the files the user named, or the top 1-5 files from the list. Never the whole repo at once.
10749
+ 3. **Migrate each file**, finding by finding:
10750
+ - \`legacy-library\` and \`forbidden-import\`: replace the other library's piece with the component in the table below. Map its props and events
10751
+ to the component's (check \`get_component_docs\`), keep every binding, handler, validation and accessibility attribute, then remove the old import.
10752
+ - \`prefer-official-component\`: raw \`<button>\`, \`<select>\`, \`<input>\` \u2192 the design system component.
10753
+ - \`no-hardcoded-color\`, \`spacing-scale\`, \`radius-scale\`, \`font-family\`: use the Brand Kit CSS variables, never new literal values.
10754
+ - \`component-props\`: only the props / inputs / events the component lists.
10755
+ 4. **Prove it**: \`npx brandrail check <the files>\` shows none of these findings for them, and the project's own build, lint and tests still pass.
10756
+ Look at the screen if you can: the layout should look the same apart from the brand.
10757
+ 5. **Commit the batch** ("Migrate <area> to the design system"), then go back to step 1.
10758
+ 6. **Remove a library** from package.json only when \`npx brandrail migrate\` shows 0 usages of it in the whole repo, and say so in the commit.
10759
+
10760
+ ## Rules
10761
+
10762
+ - Change the look, not the behaviour: no logic, routing, data or API changes in a migration commit.
10763
+ - If a piece has no design system component (the table says so), build it with semantic HTML and the Brand Kit tokens, and list it in your summary
10764
+ so the design system team can add a component.
10765
+ - Ask before deleting files or changing shared code (layouts, base components) that many screens use.
10766
+ ${fw === "angular" ? "- Angular: every NgModule or standalone component that uses a custom element needs `CUSTOM_ELEMENTS_SCHEMA`; read event values with `$any($event).detail`; bind properties in camelCase (`[selectedDate]`).\n" : ""}
10767
+ ## What replaces what (${FRAMEWORK_LABEL[fw]})
10768
+
10769
+ ${libs ? `Libraries in this project: ${libs.map((l) => LIBRARY_LABEL[l]).join(", ")}.
10770
+
10771
+ ` : ""}${migrationMapMarkdown(resolveConfig(config2), fw, libs)}
10772
+ `;
10773
+ }
10774
+ function migrationAgent(opts = {}) {
10775
+ return `---
10776
+ name: brandrail-migrator
10777
+ description: Migrates a batch of UI files to ${opts.org ? `the ${opts.org}` : "the"} design system using the brandrail-migrate skill. Use for "migrate <files or folder> to the design system", one batch per call.
10778
+ tools: Read, Edit, Write, Grep, Glob, Bash
10779
+ ---
10780
+
10781
+ You migrate UI code to the design system. Follow the \`brandrail-migrate\` skill exactly (.claude/skills/brandrail-migrate/SKILL.md).
10782
+
10783
+ For the files or folder you're given:
10784
+ 1. Run \`npx brandrail check <paths>\` to list the findings.
10785
+ 2. Migrate every finding in those files, keeping behaviour identical.
10786
+ 3. Run \`npx brandrail check <paths>\` again until it's clean, then the project's build or tests if they're quick.
10787
+ 4. Reply with: files changed, findings fixed, anything that had no design system component, and anything you weren't sure about.
10788
+
10789
+ Don't touch files outside the batch. Don't commit unless you were asked to.
10790
+ `;
10791
+ }
10792
+
10793
+ // ../engine/src/project.ts
10794
+ var KIT = "@brandrail/kit";
10795
+ function mergeProjectConfig(prev, kit) {
10796
+ if (!prev) return kit;
10797
+ return {
10798
+ ...prev,
10799
+ ...kit,
10800
+ rules: { ...prev.rules, ...kit.rules },
10801
+ ignore: prev.ignore ?? kit.ignore,
10802
+ tokensCss: prev.tokensCss,
10803
+ framework: prev.framework ?? kit.framework
10804
+ };
10805
+ }
10806
+ function tokensCssContent(config2, org) {
10807
+ let css = generateTokensCss(config2.tokens, { version: config2.tokenVersion, org });
10808
+ const usesKit = (config2.components ?? []).some((c) => c.importPath === KIT);
10809
+ if (usesKit) css += `
10810
+ /* ${KIT} follows the Brand Kit: colours, font, spacing, radius and type scale. */
10811
+ ${kitBridgeCss(config2.tokens)}
10812
+ `;
10813
+ return { css, usesKit };
10814
+ }
10815
+ var CURSOR_GLOBS = "**/*.{tsx,jsx,ts,vue,svelte,html,css,scss,php,twig,hbs,mustache}";
10816
+ function ruleFileContent(file2, md, before) {
10817
+ if (file2.endsWith(".mdc")) return `---
10818
+ description: Design system rules from Brandrail
10819
+ globs: ["${CURSOR_GLOBS}"]
10820
+ alwaysApply: true
10821
+ ---
10822
+
10823
+ ${md}
10824
+ `;
10825
+ if (file2.startsWith(".windsurf/")) return `---
10826
+ trigger: always_on
10827
+ ---
10828
+
10829
+ ${md}
10830
+ `;
10831
+ return upsertSection(before, md);
10832
+ }
10833
+
10834
+ // ../engine/src/migrate-status.ts
10835
+ var MIGRATION_RULES = new Set(RULES.map((r) => r.id).filter((id) => id !== "img-alt"));
10836
+ var LABEL_TO_ID = new Map(Object.entries(LIBRARY_LABEL).map(([id, label]) => [label, id]));
10837
+ function migrationStatus(files, configInput) {
10838
+ const config2 = "replaces" in configInput ? configInput : resolveConfig(configInput);
10839
+ const report = check(files, config2, { snippets: false });
10840
+ const per = /* @__PURE__ */ new Map();
10841
+ const libraries = {};
10842
+ const byRule = {};
10843
+ for (const v of report.violations) {
10844
+ if (!MIGRATION_RULES.has(v.ruleId)) continue;
10845
+ byRule[v.ruleId] = (byRule[v.ruleId] ?? 0) + 1;
10846
+ const f = per.get(v.file) ?? { path: v.file, findings: 0, byRule: {}, libraries: [] };
10847
+ f.findings++;
10848
+ f.byRule[v.ruleId] = (f.byRule[v.ruleId] ?? 0) + 1;
10849
+ if (v.ruleId === "legacy-library") {
10850
+ const label = /\(([^()]*(?:\([^()]*\))?[^()]*)\) should be/.exec(v.message)?.[1];
10851
+ const id = label ? LABEL_TO_ID.get(label) : void 0;
10852
+ if (id) {
10853
+ libraries[id] = (libraries[id] ?? 0) + 1;
10854
+ if (!f.libraries.includes(id)) f.libraries.push(id);
10855
+ }
10856
+ }
10857
+ per.set(v.file, f);
10858
+ }
10859
+ const list = [...per.values()].sort((a, b) => b.findings - a.findings || a.path.localeCompare(b.path));
10860
+ const filesClean = Math.max(0, report.filesScanned - list.length);
10861
+ return {
10862
+ report,
10863
+ files: list,
10864
+ filesScanned: report.filesScanned,
10865
+ filesClean,
10866
+ progress: report.filesScanned ? Math.round(filesClean / report.filesScanned * 100) : 100,
10867
+ libraries,
10868
+ byRule
10869
+ };
10870
+ }
10871
+
10872
+ // ../engine/src/report-html.ts
10873
+ var esc2 = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
10874
+ var ruleTitle = (id) => RULES.find((r) => r.id === id)?.title ?? id;
10875
+ function formatHandoverHtml(s, meta3) {
10876
+ const when = meta3.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
10877
+ const date5 = new Date(when).toLocaleDateString("en-AU", { day: "numeric", month: "long", year: "numeric" });
10878
+ const libs = Object.entries(s.libraries);
10879
+ const rules = Object.entries(s.byRule).sort((a, b) => b[1] - a[1]);
10880
+ const total = rules.reduce((a, [, n]) => a + n, 0);
10881
+ const tone = (n) => n >= 90 ? "good" : n >= 70 ? "warn" : "bad";
10882
+ const trend = meta3.history?.length ? `<section><h2>Progress over time</h2><table><thead><tr><th>Date</th><th class="n">Files done</th><th class="n">Brand score</th></tr></thead><tbody>${[...meta3.history, { at: when, progress: s.progress, score: s.report.score }].map((h) => `<tr><td>${esc2(new Date(h.at).toLocaleDateString("en-AU"))}</td><td class="n">${h.progress}%</td><td class="n">${h.score}</td></tr>`).join("")}</tbody></table></section>` : "";
10883
+ return `<!doctype html>
10884
+ <html lang="en">
10885
+ <head>
10886
+ <meta charset="utf-8">
10887
+ <meta name="viewport" content="width=device-width, initial-scale=1">
10888
+ <title>Design system handover \u2014 ${esc2(meta3.project)}</title>
10889
+ <style>
10890
+ :root { --fg:#1f2328; --muted:#59636e; --line:#d1d9e0; --bg:#fff; --subtle:#f6f8fa; --good:#1a7f37; --warn:#9a6700; --bad:#cf222e; --accent:#0969da; }
10891
+ @media (prefers-color-scheme: dark) { :root { --fg:#e6edf3; --muted:#9198a1; --line:#3d444d; --bg:#0d1117; --subtle:#151b23; --good:#3fb950; --warn:#d29922; --bad:#f85149; --accent:#4493f8; } }
10892
+ * { box-sizing: border-box; }
10893
+ body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif; }
10894
+ main { max-width: 900px; margin: 0 auto; padding: 40px 16px 64px; }
10895
+ h1 { font-size: 24px; margin: 0 0 4px; } h2 { font-size: 16px; margin: 32px 0 8px; }
10896
+ .meta { color: var(--muted); margin: 0 0 24px; }
10897
+ .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
10898
+ .card { border: 1px solid var(--line); border-radius: 8px; padding: 14px 16px; background: var(--subtle); }
10899
+ .card p { margin: 0; color: var(--muted); font-size: 12px; } .card b { display: block; font-size: 26px; margin-top: 2px; }
10900
+ .good { color: var(--good); } .warn { color: var(--warn); } .bad { color: var(--bad); }
10901
+ .bar { height: 10px; border-radius: 5px; background: var(--line); overflow: hidden; margin-top: 8px; }
10902
+ .bar span { display: block; height: 100%; background: var(--good); }
10903
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
10904
+ th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
10905
+ th { color: var(--muted); font-weight: 500; } .n { text-align: right; font-variant-numeric: tabular-nums; }
10906
+ code { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; background: var(--subtle); padding: 1px 4px; border-radius: 4px; }
10907
+ ol li { margin: 4px 0; } footer { margin-top: 40px; color: var(--muted); font-size: 12px; }
10908
+ @media print { body { font-size: 12px; } main { padding: 0; } .card { break-inside: avoid; } }
10909
+ </style>
10910
+ </head>
10911
+ <body>
10912
+ <main>
10913
+ <h1>Design system handover: ${esc2(meta3.project)}</h1>
10914
+ <p class="meta">${esc2(date5)}${meta3.org ? ` \xB7 ${esc2(meta3.org)} design system` : ""}${meta3.kitVersion ? ` \xB7 Brand Kit v${esc2(meta3.kitVersion)}` : ""}${meta3.framework ? ` \xB7 ${esc2(meta3.framework)}` : ""}${meta3.branch ? ` \xB7 branch <code>${esc2(meta3.branch)}</code>` : ""}${meta3.commit ? ` @ <code>${esc2(meta3.commit.slice(0, 7))}</code>` : ""}</p>
10915
+
10916
+ <div class="cards">
10917
+ <div class="card"><p>Files on the design system</p><b class="${tone(s.progress)}">${s.progress}%</b><div class="bar"><span style="width:${s.progress}%"></span></div></div>
10918
+ <div class="card"><p>Brand score</p><b class="${tone(s.report.score)}">${s.report.score}/100</b></div>
10919
+ <div class="card"><p>UI files done</p><b>${s.filesClean} / ${s.filesScanned}</b></div>
10920
+ <div class="card"><p>Findings left</p><b class="${total ? "warn" : "good"}">${total}</b></div>
10921
+ </div>
10922
+
10923
+ ${libs.length ? `<section><h2>Other UI libraries still in use</h2><table><thead><tr><th>Library</th><th class="n">Usages left</th></tr></thead><tbody>${libs.map(([id, n]) => `<tr><td>${esc2(LIBRARY_LABEL[id])}</td><td class="n">${n}</td></tr>`).join("")}</tbody></table></section>` : `<section><h2>Other UI libraries</h2><p class="good">None left in the code.</p></section>`}
10924
+
10925
+ <section><h2>What's left, by kind</h2>${rules.length ? `<table><thead><tr><th>Finding</th><th>Rule</th><th class="n">Count</th></tr></thead><tbody>${rules.map(([id, n]) => `<tr><td>${esc2(ruleTitle(id))}</td><td><code>${esc2(id)}</code></td><td class="n">${n}</td></tr>`).join("")}</tbody></table>` : `<p class="good">Nothing: every UI file follows the design system.</p>`}</section>
10926
+
10927
+ ${s.files.length ? `<section><h2>Files with the most work left</h2><table><thead><tr><th>File</th><th>Libraries</th><th class="n">Findings</th></tr></thead><tbody>${s.files.slice(0, 40).map((f) => `<tr><td><code>${esc2(f.path)}</code></td><td>${esc2(f.libraries.map((l) => LIBRARY_LABEL[l]).join(", "))}</td><td class="n">${f.findings}</td></tr>`).join("")}</tbody></table>${s.files.length > 40 ? `<p class="meta">\u2026and ${s.files.length - 40} more files.</p>` : ""}</section>` : ""}
10928
+
10929
+ ${trend}
10930
+
10931
+ <section><h2>How to finish</h2><ol>
10932
+ <li>Run <code>npx brandrail connect</code> in the repo once (rule files, MCP server and the migration skill for AI tools).</li>
10933
+ <li>Ask the AI tool: <em>"migrate ${esc2(s.files[0]?.path ?? "the next files")} to the design system"</em>. In Claude Code the <code>brandrail-migrate</code> skill and <code>brandrail-migrator</code> agent do it in small, checked batches.</li>
10934
+ <li>Track progress with <code>npx brandrail migrate</code>; make the GitHub check required once the score holds above the team's threshold.</li>
10935
+ <li>Remove each old library from <code>package.json</code> when it shows 0 usages here.</li>
10936
+ </ol></section>
10937
+
10938
+ <footer>Generated by Brandrail (<code>npx brandrail report</code>). Only file paths and counts are included: no source code.</footer>
10939
+ </main>
10940
+ </body>
10941
+ </html>
10942
+ `;
10943
+ }
10944
+
10374
10945
  // src/api.ts
10375
10946
  var DEFAULT_API = "https://www.brandrail.com.au";
10376
10947
  function apiBase() {
@@ -11159,7 +11730,7 @@ __export(util_exports, {
11159
11730
  defineLazy: () => defineLazy,
11160
11731
  defineLazyInternal: () => defineLazyInternal,
11161
11732
  derived: () => derived,
11162
- esc: () => esc2,
11733
+ esc: () => esc3,
11163
11734
  escapeRegex: () => escapeRegex,
11164
11735
  explicitlyAborted: () => explicitlyAborted,
11165
11736
  extend: () => extend,
@@ -11390,7 +11961,7 @@ function randomString(length = 10) {
11390
11961
  }
11391
11962
  return str;
11392
11963
  }
11393
- function esc2(str) {
11964
+ function esc3(str) {
11394
11965
  return JSON.stringify(str);
11395
11966
  }
11396
11967
  function slugify(input2) {
@@ -14190,7 +14761,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
14190
14761
  if (key === "__proto__")
14191
14762
  continue;
14192
14763
  const id = ids[key];
14193
- const k = typeof key === "symbol" ? `syms[${syms.indexOf(key)}]` : esc2(key);
14764
+ const k = typeof key === "symbol" ? `syms[${syms.indexOf(key)}]` : esc3(key);
14194
14765
  const isPresent = `${k} in input`;
14195
14766
  const schema = shape[key];
14196
14767
  const optin = schema?._zod?.optin;
@@ -23865,7 +24436,7 @@ function generatePropertiesChecks(doc, ctx, def, accessor) {
23865
24436
  doc.write(`if (${accessor} == null) return INVALID;`);
23866
24437
  const shape = def.shape;
23867
24438
  for (const key of Reflect.ownKeys(shape)) {
23868
- const keyExpr = typeof key === "symbol" ? addConstant(ctx, key) : esc2(key);
24439
+ const keyExpr = typeof key === "symbol" ? addConstant(ctx, key) : esc3(key);
23869
24440
  const inputVar = newVar(ctx);
23870
24441
  doc.write(`const ${inputVar} = ${accessor}[${keyExpr}];`);
23871
24442
  compileChild(doc, ctx, shape[key], inputVar, false);
@@ -24039,16 +24610,16 @@ function generateStringFormatCheck(doc, ctx, def, accessor, needsValue = true) {
24039
24610
  doc.write(`if (${accessor} !== ${accessor}.toUpperCase()) return INVALID;`);
24040
24611
  break;
24041
24612
  case "includes":
24042
- doc.write(`if (!${accessor}.includes(${esc2(def.includes)})) return INVALID;`);
24613
+ doc.write(`if (!${accessor}.includes(${esc3(def.includes)})) return INVALID;`);
24043
24614
  break;
24044
24615
  case "starts_with": {
24045
24616
  const prefix = def.prefix;
24046
- doc.write(`if (${accessor}.slice(0, ${prefix.length}) !== ${esc2(prefix)}) return INVALID;`);
24617
+ doc.write(`if (${accessor}.slice(0, ${prefix.length}) !== ${esc3(prefix)}) return INVALID;`);
24047
24618
  break;
24048
24619
  }
24049
24620
  case "ends_with": {
24050
24621
  const suffix = def.suffix;
24051
- doc.write(`if (${accessor}.slice(-${suffix.length}) !== ${esc2(suffix)}) return INVALID;`);
24622
+ doc.write(`if (${accessor}.slice(-${suffix.length}) !== ${esc3(suffix)}) return INVALID;`);
24052
24623
  break;
24053
24624
  }
24054
24625
  default: {
@@ -24254,8 +24825,8 @@ function generateObjectCheck(doc, ctx, schema, accessor, buildsValue = true) {
24254
24825
  const keys = Object.keys(shape);
24255
24826
  const symbolKeys = Object.getOwnPropertySymbols(shape);
24256
24827
  const allKeys = symbolKeys.length ? [...keys, ...symbolKeys] : keys;
24257
- const keyExpr = (k) => typeof k === "symbol" ? addConstant(ctx, k) : esc2(k);
24258
- const propKey = (k) => typeof k === "symbol" ? `[${keyExpr(k)}]` : esc2(k);
24828
+ const keyExpr = (k) => typeof k === "symbol" ? addConstant(ctx, k) : esc3(k);
24829
+ const propKey = (k) => typeof k === "symbol" ? `[${keyExpr(k)}]` : esc3(k);
24259
24830
  const propShape = shape;
24260
24831
  if (keys.includes("__proto__")) {
24261
24832
  throw new ZodCompileUnsupportedError('object shape key "__proto__"');
@@ -24299,7 +24870,7 @@ function generateObjectCheck(doc, ctx, schema, accessor, buildsValue = true) {
24299
24870
  if (catchall) {
24300
24871
  const catchallType = catchall._zod.def.type;
24301
24872
  if (catchallType === "never") {
24302
- const condition = keys.map((k) => `k !== ${esc2(k)}`).join(" && ") || "true";
24873
+ const condition = keys.map((k) => `k !== ${esc3(k)}`).join(" && ") || "true";
24303
24874
  doc.write(`for (const k in ${accessor}) {`);
24304
24875
  doc.indented((d) => {
24305
24876
  d.write(`if (${condition}) return INVALID;`);
@@ -24570,7 +25141,7 @@ function generateLiteralCheck(doc, ctx, schema, accessor) {
24570
25141
  return accessor;
24571
25142
  }
24572
25143
  if (typeof value === "string") {
24573
- doc.write(`if (${accessor} !== ${esc2(value)}) return INVALID;`);
25144
+ doc.write(`if (${accessor} !== ${esc3(value)}) return INVALID;`);
24574
25145
  } else if (typeof value === "number" || typeof value === "boolean") {
24575
25146
  doc.write(`if (${accessor} !== ${value}) return INVALID;`);
24576
25147
  } else if (value === null) {
@@ -24771,7 +25342,7 @@ function generateDiscriminatedUnionCheck(doc, ctx, def, accessor) {
24771
25342
  }
24772
25343
  const discVar = newVar(ctx);
24773
25344
  const outputVar = newVar(ctx);
24774
- doc.write(`const ${discVar} = ${accessor}?.[${esc2(def.discriminator)}];`);
25345
+ doc.write(`const ${discVar} = ${accessor}?.[${esc3(def.discriminator)}];`);
24775
25346
  doc.write(`let ${outputVar};`);
24776
25347
  let firstBranch = true;
24777
25348
  const claimed = /* @__PURE__ */ new Set();
@@ -24801,7 +25372,7 @@ function generateDiscriminatedUnionCheck(doc, ctx, def, accessor) {
24801
25372
  }
24802
25373
  function literalEquality(ctx, accessor, value) {
24803
25374
  if (typeof value === "string")
24804
- return `${accessor} === ${esc2(value)}`;
25375
+ return `${accessor} === ${esc3(value)}`;
24805
25376
  if (typeof value === "number") {
24806
25377
  if (Number.isNaN(value))
24807
25378
  return `Number.isNaN(${accessor})`;
@@ -24933,7 +25504,7 @@ function emitOwnKeys(doc, ctx, accessor, kVar, body, onSymbol) {
24933
25504
  }
24934
25505
  function literalPropertyKey(ctx, key) {
24935
25506
  if (typeof key === "string")
24936
- return esc2(key);
25507
+ return esc3(key);
24937
25508
  return addConstant(ctx, key);
24938
25509
  }
24939
25510
  function generateMapCheck(doc, ctx, schema, accessor) {
@@ -40084,6 +40655,21 @@ function createBrandrailMcp(ctx) {
40084
40655
  return text(componentDoc(c, fwOf(framework)));
40085
40656
  }
40086
40657
  );
40658
+ server.registerTool(
40659
+ "get_migration_map",
40660
+ {
40661
+ title: "Map another UI library to the design system",
40662
+ description: "What replaces each piece of Angular Material, MUI, Bootstrap, Ant Design, Chakra, React Bootstrap or PrimeNG in this design system, in the project's framework. Call before migrating code that uses one of those libraries; keep behaviour identical and use get_component_docs for props and events.",
40663
+ inputSchema: {
40664
+ libraries: external_exports.array(external_exports.enum(Object.keys(LIBRARY_LABEL))).optional().describe("Only these libraries (default: all)"),
40665
+ framework: frameworkInput
40666
+ },
40667
+ annotations: { readOnlyHint: true, openWorldHint: false }
40668
+ },
40669
+ async ({ libraries, framework }) => text(`# Migration map (${FRAMEWORK_LABEL[fwOf(framework)]})
40670
+
40671
+ ${migrationMapMarkdown(resolved, fwOf(framework), libraries)}`)
40672
+ );
40087
40673
  server.registerTool(
40088
40674
  "validate_snippet",
40089
40675
  {
@@ -40129,7 +40715,7 @@ function createBrandrailMcp(ctx) {
40129
40715
  // src/files.ts
40130
40716
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
40131
40717
  import { dirname as dirname2, join as join3, relative, resolve, sep } from "node:path";
40132
- var CONFIG_FILE = "brandrail.config.json";
40718
+ var CONFIG_FILE2 = "brandrail.config.json";
40133
40719
  var MAX_FILE_BYTES = 512 * 1024;
40134
40720
  function collectFiles(root, paths, ignoreGlobs) {
40135
40721
  const ignored = makeIgnore(ignoreGlobs);
@@ -40159,7 +40745,7 @@ function collectFiles(root, paths, ignoreGlobs) {
40159
40745
  function findConfig(start) {
40160
40746
  let dir = resolve(start);
40161
40747
  for (; ; ) {
40162
- const candidate = join3(dir, CONFIG_FILE);
40748
+ const candidate = join3(dir, CONFIG_FILE2);
40163
40749
  if (existsSync3(candidate)) return candidate;
40164
40750
  const parent = dirname2(dir);
40165
40751
  if (parent === dir) return null;
@@ -40215,7 +40801,11 @@ function parseChangedLines(diff) {
40215
40801
  }
40216
40802
  function changedLines(cwd, base) {
40217
40803
  const mergeBase = git(["merge-base", base, "HEAD"], cwd) ?? base;
40218
- 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);
40219
40809
  return diff === null ? null : parseChangedLines(diff);
40220
40810
  }
40221
40811
 
@@ -40300,12 +40890,82 @@ async function runLocalMcp(cwd, opts) {
40300
40890
  });
40301
40891
  }
40302
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
+
40303
40963
  // src/scan.ts
40304
- import { existsSync as existsSync5 } from "node:fs";
40964
+ import { existsSync as existsSync6 } from "node:fs";
40305
40965
  import { createRequire as createRequire2 } from "node:module";
40306
- import { join as join5, resolve as resolve2 } from "node:path";
40966
+ import { join as join6, resolve as resolve2 } from "node:path";
40307
40967
  function loadPlaywright(cwd) {
40308
- for (const base of [join5(cwd, "package.json"), import.meta.url]) {
40968
+ for (const base of [join6(cwd, "package.json"), import.meta.url]) {
40309
40969
  for (const name of ["playwright", "playwright-core"]) {
40310
40970
  try {
40311
40971
  return createRequire2(base)(name);
@@ -40319,7 +40979,7 @@ async function scanPages(cwd, url2, config2, opts, log3) {
40319
40979
  const pw = loadPlaywright(cwd);
40320
40980
  const start = /^https?:\/\//i.test(url2) ? url2 : `http://${url2}`;
40321
40981
  const state = opts.storageState ? resolve2(cwd, opts.storageState) : void 0;
40322
- 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}`);
40323
40983
  const browser = await pw.chromium.launch({ headless: true });
40324
40984
  try {
40325
40985
  const context = await browser.newContext({ viewport: { width: opts.width, height: 900 }, storageState: state });
@@ -40364,7 +41024,10 @@ Usage
40364
41024
  brandrail connect Sign in through the browser and set this project up: Brand Kit, AI rule files,
40365
41025
  MCP server for Claude Code / Cursor / Copilot, and the UI kit
40366
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
40367
41028
  brandrail init Create brandrail.config.json and AI rule files
41029
+ brandrail migrate [paths...] What's left on the way to the design system: per library and per file
41030
+ brandrail report Write a shareable HTML handover report (paths and counts, no code)
40368
41031
  brandrail mcp Run the MCP server on this machine (AI tools start it; code never leaves it)
40369
41032
  brandrail whoami | logout Show or remove the saved sign-in
40370
41033
  brandrail scan <url> Check a running page (localhost, staging, behind a login) in a real browser
@@ -40376,6 +41039,8 @@ Options for check
40376
41039
  --format <fmt> pretty | json | markdown | github (default: pretty)
40377
41040
  --fail-below <score> Exit 1 when the brand score is lower (default: off)
40378
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)
40379
41044
  --upload Send the result to Brandrail (needs \`brandrail connect\` or BRANDRAIL_KEY)
40380
41045
  --no-color Plain output
40381
41046
 
@@ -40394,6 +41059,14 @@ Options for connect
40394
41059
  --remote Point AI tools at the hosted MCP server instead of the local one
40395
41060
  --framework, --tokens-css, --no-rules As for init
40396
41061
 
41062
+ Options for migrate
41063
+ --json Machine-readable output
41064
+ --top <n> How many files to list (default 15)
41065
+ --upload Store the result in the dashboard (scores and counts, never code)
41066
+
41067
+ Options for report
41068
+ --out <file> Where to write it (default brandrail-report.html)
41069
+
40397
41070
  Options for mcp
40398
41071
  --framework <name> Override the detected framework
40399
41072
  --report Also send brand scores (never code) to the dashboard
@@ -40425,7 +41098,7 @@ function parseArgs(argv) {
40425
41098
  const [k, v] = a.slice(2).split("=", 2);
40426
41099
  const next = rest[i2 + 1];
40427
41100
  if (v !== void 0) flags[k] = v;
40428
- else if (next !== void 0 && !next.startsWith("-") && ["config", "format", "fail-below", "changed", "pages", "storage-state", "wait", "width", "tokens-css", "framework"].includes(k)) {
41101
+ else if (next !== void 0 && !next.startsWith("-") && ["config", "format", "fail-below", "changed", "pages", "storage-state", "wait", "width", "tokens-css", "framework", "top", "out"].includes(k)) {
40429
41102
  flags[k] = next;
40430
41103
  i2++;
40431
41104
  } else flags[k] = true;
@@ -40435,24 +41108,41 @@ function parseArgs(argv) {
40435
41108
  }
40436
41109
  var log2 = (s = "") => process.stderr.write(`${s}
40437
41110
  `);
41111
+ function readJson(path) {
41112
+ if (!existsSync7(path)) return void 0;
41113
+ try {
41114
+ return JSON.parse(readFileSync6(path, "utf8"));
41115
+ } catch {
41116
+ return void 0;
41117
+ }
41118
+ }
40438
41119
  async function loadConfig(cwd, flags) {
40439
41120
  const explicit = typeof flags.config === "string" ? resolve3(cwd, flags.config) : null;
40440
41121
  const path = explicit ?? findConfig(cwd);
40441
- if (path) return { config: readConfig(path), source: relative2(cwd, path) || CONFIG_FILE };
41122
+ if (path) return { config: readConfig(path), source: relative3(cwd, path) || CONFIG_FILE2 };
40442
41123
  const key = resolveKey();
40443
41124
  if (key) {
40444
41125
  const { config: config2 } = await fetchBrandKit(key);
40445
41126
  return { config: config2, source: `${apiBase()} (published Brand Kit)` };
40446
41127
  }
40447
- throw new Error(`No ${CONFIG_FILE} found. Run \`npx brandrail init\` first, or run \`npx brandrail connect\` to use your published Brand Kit.`);
41128
+ throw new Error(`No ${CONFIG_FILE2} found. Run \`npx brandrail init\` first, or run \`npx brandrail connect\` to use your published Brand Kit.`);
40448
41129
  }
40449
41130
  async function cmdCheck(cwd, positional, flags) {
40450
41131
  const { config: config2, source } = await loadConfig(cwd, flags);
40451
41132
  const resolved = resolveConfig(config2);
40452
- const root = dirname4(typeof flags.config === "string" ? resolve3(cwd, flags.config) : findConfig(cwd) ?? join6(cwd, CONFIG_FILE));
40453
- 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);
40454
41135
  let onlyLines;
40455
- 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) {
40456
41146
  const base = typeof flags.changed === "string" ? flags.changed : "origin/main";
40457
41147
  onlyLines = changedLines(root, base) ?? void 0;
40458
41148
  if (!onlyLines) log2(`warning: couldn't read git diff against ${base}; checking everything`);
@@ -40489,6 +41179,11 @@ async function cmdCheck(cwd, positional, flags) {
40489
41179
  log2(`Uploaded \u2192 ${res.url ?? res.scanId}`);
40490
41180
  }
40491
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
+ }
40492
41187
  if (threshold !== void 0 && report.score < threshold) {
40493
41188
  log2(`
40494
41189
  Brand score ${report.score} is below ${threshold}.`);
@@ -40496,54 +41191,54 @@ Brand score ${report.score} is below ${threshold}.`);
40496
41191
  }
40497
41192
  return 0;
40498
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
+ }
40499
41211
  function tokensCssPath(root, config2, flags) {
40500
41212
  if (typeof flags["tokens-css"] === "string") return flags["tokens-css"];
40501
41213
  if (config2.tokensCss) return config2.tokensCss;
40502
- 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";
40503
41215
  }
40504
41216
  function writeTokensCss(root, rel, config2, org) {
40505
- const abs = join6(root, rel);
40506
- mkdirSync3(dirname4(abs), { recursive: true });
40507
- let css = generateTokensCss(config2.tokens, { version: config2.tokenVersion, org });
40508
- const usesKit = (config2.components ?? []).some((c) => c.importPath === "@brandrail/kit");
40509
- if (usesKit) css += `
40510
- /* @brandrail/kit follows the Brand Kit: colours, font, spacing, radius and type scale. */
40511
- ${kitBridgeCss(config2.tokens)}
40512
- `;
40513
- writeFileSync3(abs, css);
41217
+ const abs = join7(root, rel);
41218
+ mkdirSync4(dirname4(abs), { recursive: true });
41219
+ const { css, usesKit } = tokensCssContent(config2, org);
41220
+ writeFileSync4(abs, css);
40514
41221
  return usesKit;
40515
41222
  }
40516
41223
  function writeRuleFiles(root, config2, org, tokensCss) {
40517
41224
  const md = generateInstructions(config2, { org, tokensCss });
40518
41225
  const written = [];
40519
- const write2 = (rel, content) => {
40520
- const abs = join6(root, rel);
40521
- mkdirSync3(dirname4(abs), { recursive: true });
40522
- const before = existsSync6(abs) ? readFileSync5(abs, "utf8") : "";
40523
- writeFileSync3(abs, content(before));
40524
- written.push(rel);
40525
- };
40526
41226
  for (const { file: file2, readBy } of RULE_FILES) {
40527
- if (file2.endsWith(".mdc")) {
40528
- write2(file2, () => `---
40529
- description: Design system rules from Brandrail
40530
- globs: ["**/*.{tsx,jsx,ts,vue,svelte,html,css,scss,php,twig,hbs,mustache}"]
40531
- alwaysApply: true
40532
- ---
40533
-
40534
- ${md}
40535
- `);
40536
- } else if (file2.startsWith(".windsurf/")) {
40537
- write2(file2, () => `---
40538
- trigger: always_on
40539
- ---
40540
-
40541
- ${md}
40542
- `);
40543
- } else {
40544
- write2(file2, (before) => upsertSection(before, md));
40545
- }
40546
- written[written.length - 1] = `${file2} (${readBy})`;
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));
41231
+ written.push(`${file2} (${readBy})`);
41232
+ }
41233
+ const pkg = readJson(join7(root, "package.json"));
41234
+ for (const [file2, content, readBy] of [
41235
+ [MIGRATE_SKILL_FILE, migrationSkill(config2, { org, pkg }), "Claude Code skill: migrate to the design system"],
41236
+ [MIGRATE_AGENT_FILE, migrationAgent({ org }), "Claude Code agent: brandrail-migrator"]
41237
+ ]) {
41238
+ const abs = join7(root, file2);
41239
+ mkdirSync4(dirname4(abs), { recursive: true });
41240
+ writeFileSync4(abs, content);
41241
+ written.push(`${file2} (${readBy})`);
40547
41242
  }
40548
41243
  return written;
40549
41244
  }
@@ -40552,13 +41247,13 @@ function writeMcpConfigs(root, framework, mode = "remote") {
40552
41247
  const configs = mode === "local" ? mcpLocalConfigs(framework) : mcpClientConfigs(url2);
40553
41248
  for (const c of configs) {
40554
41249
  if (!c.projectFile) continue;
40555
- const abs = join6(root, c.file);
40556
- mkdirSync3(dirname4(abs), { recursive: true });
41250
+ const abs = join7(root, c.file);
41251
+ mkdirSync4(dirname4(abs), { recursive: true });
40557
41252
  const ours = JSON.parse(c.content);
40558
41253
  let merged = ours;
40559
- if (existsSync6(abs)) {
41254
+ if (existsSync7(abs)) {
40560
41255
  try {
40561
- const existing = JSON.parse(readFileSync5(abs, "utf8"));
41256
+ const existing = JSON.parse(readFileSync6(abs, "utf8"));
40562
41257
  merged = { ...existing };
40563
41258
  for (const [k, v] of Object.entries(ours)) {
40564
41259
  if (k === "mcpServers" || k === "servers") {
@@ -40573,7 +41268,7 @@ function writeMcpConfigs(root, framework, mode = "remote") {
40573
41268
  continue;
40574
41269
  }
40575
41270
  }
40576
- writeFileSync3(abs, `${JSON.stringify(merged, null, 2)}
41271
+ writeFileSync4(abs, `${JSON.stringify(merged, null, 2)}
40577
41272
  `);
40578
41273
  log2(`\u2714 added brandrail to ${c.file} (${c.name})`);
40579
41274
  }
@@ -40594,16 +41289,16 @@ function projectFramework(cwd, config2, flags) {
40594
41289
  return asked;
40595
41290
  }
40596
41291
  if (config2.framework && isFramework(config2.framework)) return config2.framework;
40597
- const pkg = join6(cwd, "package.json");
40598
- if (!existsSync6(pkg)) return detectFramework(null);
41292
+ const pkg = join7(cwd, "package.json");
41293
+ if (!existsSync7(pkg)) return detectFramework(null);
40599
41294
  try {
40600
- return detectFramework(JSON.parse(readFileSync5(pkg, "utf8")));
41295
+ return detectFramework(JSON.parse(readFileSync6(pkg, "utf8")));
40601
41296
  } catch {
40602
41297
  return "react";
40603
41298
  }
40604
41299
  }
40605
41300
  async function cmdInit(cwd, flags) {
40606
- const target = join6(cwd, CONFIG_FILE);
41301
+ const target = join7(cwd, CONFIG_FILE2);
40607
41302
  let config2 = starterConfig;
40608
41303
  let org;
40609
41304
  if (flags["from-api"]) {
@@ -40613,23 +41308,23 @@ async function cmdInit(cwd, flags) {
40613
41308
  config2 = { $schema: starterConfig.$schema, ...res.config };
40614
41309
  org = res.org;
40615
41310
  }
40616
- if (existsSync6(target) && !flags.force && !flags["from-api"]) {
40617
- log2(`${CONFIG_FILE} already exists (use --force to overwrite). Updating rule files from it.`);
41311
+ if (existsSync7(target) && !flags.force && !flags["from-api"]) {
41312
+ log2(`${CONFIG_FILE2} already exists (use --force to overwrite). Updating rule files from it.`);
40618
41313
  config2 = readConfig(target);
40619
41314
  org ??= config2.org;
40620
41315
  } else {
40621
- if (existsSync6(target)) {
41316
+ if (existsSync7(target)) {
40622
41317
  const prev = readConfig(target);
40623
- config2 = { ...prev, ...config2, rules: { ...prev.rules, ...config2.rules }, ignore: prev.ignore ?? config2.ignore, tokensCss: prev.tokensCss };
41318
+ config2 = mergeProjectConfig(prev, config2);
40624
41319
  }
40625
- writeFileSync3(target, `${JSON.stringify(config2, null, 2)}
41320
+ writeFileSync4(target, `${JSON.stringify(config2, null, 2)}
40626
41321
  `);
40627
- log2(`\u2714 wrote ${CONFIG_FILE}${org ? ` from ${org}'s Brand Kit` : " (starter tokens \u2014 edit them or use --from-api)"}`);
41322
+ log2(`\u2714 wrote ${CONFIG_FILE2}${org ? ` from ${org}'s Brand Kit` : " (starter tokens \u2014 edit them or use --from-api)"}`);
40628
41323
  }
40629
41324
  const framework = projectFramework(cwd, config2, flags);
40630
41325
  if (config2.framework !== framework) {
40631
41326
  config2 = { ...config2, framework };
40632
- writeFileSync3(target, `${JSON.stringify(config2, null, 2)}
41327
+ writeFileSync4(target, `${JSON.stringify(config2, null, 2)}
40633
41328
  `);
40634
41329
  }
40635
41330
  log2(`\u2714 framework: ${FRAMEWORK_LABEL[framework]}${flags.framework ? "" : " (detected; change with --framework)"}`);
@@ -40663,7 +41358,7 @@ async function cmdConnect(cwd, flags) {
40663
41358
  \u2714 connected to ${org}. The key is saved in ${credentialsPath()} (only you can read it), never in this repo.`);
40664
41359
  }
40665
41360
  await cmdInit(cwd, { ...flags, "from-api": true, mcp: false });
40666
- const config2 = readConfig(join6(cwd, CONFIG_FILE));
41361
+ const config2 = readConfig(join7(cwd, CONFIG_FILE2));
40667
41362
  const framework = projectFramework(cwd, config2, flags);
40668
41363
  writeMcpConfigs(cwd, framework, flags.remote ? "remote" : "local");
40669
41364
  const usesKit = (config2.components ?? []).some((c) => c.importPath === "@brandrail/kit");
@@ -40673,6 +41368,75 @@ Done. Restart your AI tool so it picks up the MCP server, then ask it to build s
40673
41368
  if (framework !== "react" && usesKit) log2(`(${framework}: import "@brandrail/kit/elements" once at your app entry \u2014 the rule files tell the AI the rest.)`);
40674
41369
  return 0;
40675
41370
  }
41371
+ async function cmdMigrate(cwd, positional, flags) {
41372
+ const { config: config2, source } = await loadConfig(cwd, flags);
41373
+ const resolved = resolveConfig(config2);
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);
41376
+ const s = migrationStatus(files, resolved);
41377
+ if (flags.format === "json" || flags.json) {
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)}
41379
+ `);
41380
+ } else {
41381
+ const color = flags.color !== false && process.stdout.isTTY !== false && !process.env.NO_COLOR;
41382
+ const c = (code, t) => color ? `\x1B[${code}m${t}\x1B[0m` : t;
41383
+ const bar = (p) => `${"\u2588".repeat(Math.round(p / 5))}${"\u2591".repeat(20 - Math.round(p / 5))}`;
41384
+ const out = [];
41385
+ out.push(c("2", `Brand Kit: ${source}${config2.tokenVersion ? ` v${config2.tokenVersion}` : ""}`));
41386
+ out.push("");
41387
+ out.push(`${c("1", "Design system migration")} ${bar(s.progress)} ${s.progress}% (${s.filesClean} of ${s.filesScanned} UI files done \xB7 brand score ${s.report.score})`);
41388
+ const libs = Object.entries(s.libraries);
41389
+ if (libs.length) out.push("", c("1", "Other UI libraries still in use"), ...libs.map(([id, n]) => ` ${LIBRARY_LABEL[id].padEnd(22)} ${String(n).padStart(5)} usages`));
41390
+ const top = typeof flags.top === "string" ? Number(flags.top) || 15 : 15;
41391
+ if (s.files.length) {
41392
+ out.push("", c("1", `Next files (${Math.min(top, s.files.length)} of ${s.files.length} with work left)`));
41393
+ for (const f of s.files.slice(0, top)) {
41394
+ const what = Object.entries(f.byRule).sort((a, b) => b[1] - a[1]).map(([r, n]) => `${r} ${n}`).join(", ");
41395
+ out.push(` ${String(f.findings).padStart(4)} ${f.path} ${c("2", what)}`);
41396
+ }
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"}.`);
41398
+ } else out.push("", c("32", "\u2714 Every UI file follows the design system."));
41399
+ process.stdout.write(`${out.join("\n")}
41400
+ `);
41401
+ }
41402
+ if (flags.upload) {
41403
+ const key = resolveKey();
41404
+ if (!key) log2(`warning: --upload skipped. ${NO_KEY}`);
41405
+ else {
41406
+ const repo = repoInfo(root);
41407
+ const res = await uploadReport(key, {
41408
+ source: process.env.CI ? "ci" : "cli",
41409
+ target: { kind: "repo", name: repo.slug.split("/").pop() ?? repo.slug, location: repo.slug },
41410
+ ref: repo.branch ?? repo.sha?.slice(0, 7),
41411
+ title: `Migration status: ${s.progress}% of UI files done`,
41412
+ report: s.report
41413
+ });
41414
+ log2(`Uploaded \u2192 ${res.url ?? res.scanId}`);
41415
+ }
41416
+ }
41417
+ return 0;
41418
+ }
41419
+ async function cmdReport(cwd, flags) {
41420
+ const { config: config2 } = await loadConfig(cwd, flags);
41421
+ const resolved = resolveConfig(config2);
41422
+ const root = dirname4(findConfig(cwd) ?? join7(cwd, CONFIG_FILE2));
41423
+ const s = migrationStatus(collectFiles(root, [], resolved.ignore), resolved);
41424
+ const repo = repoInfo(root);
41425
+ const out = resolve3(cwd, typeof flags.out === "string" ? flags.out : "brandrail-report.html");
41426
+ writeFileSync4(
41427
+ out,
41428
+ formatHandoverHtml(s, {
41429
+ project: repo.slug,
41430
+ org: config2.org,
41431
+ branch: repo.branch,
41432
+ commit: repo.sha,
41433
+ kitVersion: config2.tokenVersion,
41434
+ framework: config2.framework ? FRAMEWORK_LABEL[config2.framework] : void 0
41435
+ })
41436
+ );
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.`);
41438
+ return 0;
41439
+ }
40676
41440
  function cmdWhoami() {
40677
41441
  if (process.env.BRANDRAIL_KEY) {
40678
41442
  log2(`Using BRANDRAIL_KEY from the environment (${process.env.BRANDRAIL_KEY.slice(0, 12)}\u2026) against ${apiBase()}.`);
@@ -40750,12 +41514,12 @@ function readTokenInputs(cwd, inputs) {
40750
41514
  const add = (name, text2) => files.push({ name, data: JSON.parse(text2) });
40751
41515
  const walk = (abs, rel) => {
40752
41516
  if (statSync2(abs).isDirectory()) {
40753
- 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);
40754
41518
  } else if (/\.zip$/i.test(abs)) {
40755
- 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) });
40756
41520
  for (const [name, bytes] of Object.entries(entries)) add(name, strFromU8(bytes));
40757
41521
  } else if (/\.json$/i.test(abs)) {
40758
- add(rel || basename2(abs), readFileSync5(abs, "utf8"));
41522
+ add(rel || basename2(abs), readFileSync6(abs, "utf8"));
40759
41523
  }
40760
41524
  };
40761
41525
  for (const input2 of inputs) {
@@ -40770,16 +41534,16 @@ function cmdImport(cwd, positional, flags) {
40770
41534
  if (!files.length) throw new Error("No .json token files found.");
40771
41535
  const res = importTokenFiles(files, { includePrimitives: !!flags.primitives });
40772
41536
  const file2 = files.length === 1 ? positional[0] : `${files.length} files`;
40773
- const path = (typeof flags.config === "string" ? resolve3(cwd, flags.config) : findConfig(cwd)) ?? join6(cwd, CONFIG_FILE);
40774
- 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: {} };
40775
41539
  const next = flags.replace ? res.tokens : mergeTokens(config2.tokens, res.tokens);
40776
41540
  const d = diffTokens(config2.tokens, next);
40777
- writeFileSync3(path, `${JSON.stringify({ ...config2, tokens: next }, null, 2)}
41541
+ writeFileSync4(path, `${JSON.stringify({ ...config2, tokens: next }, null, 2)}
40778
41542
  `);
40779
41543
  log2(`\u2714 ${res.count} tokens from ${file2} (${res.format})${res.primitives ? `, ${res.primitives} palette primitives left out (--primitives to keep)` : ""}`);
40780
41544
  for (const n of res.notes) log2(` \xB7 ${n}`);
40781
41545
  for (const i2 of auditBrandKit(next)) log2(` ${i2.severity === "error" ? "\u2716" : "!"} ${i2.message}`);
40782
- log2(`\u2714 ${relative2(cwd, path) || CONFIG_FILE}: ${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`);
40783
41547
  log2("\nNext: npx brandrail init (to refresh AI rule files), then npx brandrail check");
40784
41548
  return 0;
40785
41549
  }
@@ -40811,6 +41575,12 @@ async function main(argv = process.argv.slice(2)) {
40811
41575
  return cmdConnect(cwd, flags);
40812
41576
  case "mcp":
40813
41577
  return runLocalMcp(cwd, { framework: typeof flags.framework === "string" ? flags.framework : void 0, report: flags.report === true });
41578
+ case "migrate":
41579
+ return cmdMigrate(cwd, positional, flags);
41580
+ case "report":
41581
+ return cmdReport(cwd, flags);
41582
+ case "hook":
41583
+ return cmdHook(cwd, positional, flags);
40814
41584
  case "whoami":
40815
41585
  return cmdWhoami();
40816
41586
  case "logout":