brandrail 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,6 +32,8 @@ Brandrail only ever receives its hash, and the files it writes into the repo hol
32
32
  | `rules` | List the rules |
33
33
  | `connect` | `--no-browser`, `--no-install`, `--remote` (hosted MCP server), `--framework`, `--force` (sign in again) |
34
34
  | `mcp` | The MCP server over stdio, for AI tools. `--framework`, `--report` (send scores, never code) |
35
+ | `migrate [paths]` | What's left on the way to the design system: other UI libraries (Material, MUI, Bootstrap, Ant Design, Chakra, PrimeNG) per library and per file. `--json`, `--top 30`, `--upload` |
36
+ | `report` | A shareable HTML handover report (paths and counts, never code). `--out file.html` |
35
37
  | `whoami`, `logout` | Show or remove the saved sign-in |
36
38
 
37
39
  Nothing is uploaded unless you pass `--upload`, and code snippets are never uploaded.
@@ -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.6.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;
@@ -40365,6 +40951,8 @@ Usage
40365
40951
  MCP server for Claude Code / Cursor / Copilot, and the UI kit
40366
40952
  brandrail check [paths...] Scan files and print violations
40367
40953
  brandrail init Create brandrail.config.json and AI rule files
40954
+ brandrail migrate [paths...] What's left on the way to the design system: per library and per file
40955
+ brandrail report Write a shareable HTML handover report (paths and counts, no code)
40368
40956
  brandrail mcp Run the MCP server on this machine (AI tools start it; code never leaves it)
40369
40957
  brandrail whoami | logout Show or remove the saved sign-in
40370
40958
  brandrail scan <url> Check a running page (localhost, staging, behind a login) in a real browser
@@ -40394,6 +40982,14 @@ Options for connect
40394
40982
  --remote Point AI tools at the hosted MCP server instead of the local one
40395
40983
  --framework, --tokens-css, --no-rules As for init
40396
40984
 
40985
+ Options for migrate
40986
+ --json Machine-readable output
40987
+ --top <n> How many files to list (default 15)
40988
+ --upload Store the result in the dashboard (scores and counts, never code)
40989
+
40990
+ Options for report
40991
+ --out <file> Where to write it (default brandrail-report.html)
40992
+
40397
40993
  Options for mcp
40398
40994
  --framework <name> Override the detected framework
40399
40995
  --report Also send brand scores (never code) to the dashboard
@@ -40425,7 +41021,7 @@ function parseArgs(argv) {
40425
41021
  const [k, v] = a.slice(2).split("=", 2);
40426
41022
  const next = rest[i2 + 1];
40427
41023
  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)) {
41024
+ 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
41025
  flags[k] = next;
40430
41026
  i2++;
40431
41027
  } else flags[k] = true;
@@ -40435,21 +41031,29 @@ function parseArgs(argv) {
40435
41031
  }
40436
41032
  var log2 = (s = "") => process.stderr.write(`${s}
40437
41033
  `);
41034
+ function readJson(path) {
41035
+ if (!existsSync6(path)) return void 0;
41036
+ try {
41037
+ return JSON.parse(readFileSync5(path, "utf8"));
41038
+ } catch {
41039
+ return void 0;
41040
+ }
41041
+ }
40438
41042
  async function loadConfig(cwd, flags) {
40439
41043
  const explicit = typeof flags.config === "string" ? resolve3(cwd, flags.config) : null;
40440
41044
  const path = explicit ?? findConfig(cwd);
40441
- if (path) return { config: readConfig(path), source: relative2(cwd, path) || CONFIG_FILE };
41045
+ if (path) return { config: readConfig(path), source: relative2(cwd, path) || CONFIG_FILE2 };
40442
41046
  const key = resolveKey();
40443
41047
  if (key) {
40444
41048
  const { config: config2 } = await fetchBrandKit(key);
40445
41049
  return { config: config2, source: `${apiBase()} (published Brand Kit)` };
40446
41050
  }
40447
- throw new Error(`No ${CONFIG_FILE} found. Run \`npx brandrail init\` first, or run \`npx brandrail connect\` to use your published Brand Kit.`);
41051
+ throw new Error(`No ${CONFIG_FILE2} found. Run \`npx brandrail init\` first, or run \`npx brandrail connect\` to use your published Brand Kit.`);
40448
41052
  }
40449
41053
  async function cmdCheck(cwd, positional, flags) {
40450
41054
  const { config: config2, source } = await loadConfig(cwd, flags);
40451
41055
  const resolved = resolveConfig(config2);
40452
- const root = dirname4(typeof flags.config === "string" ? resolve3(cwd, flags.config) : findConfig(cwd) ?? join6(cwd, CONFIG_FILE));
41056
+ const root = dirname4(typeof flags.config === "string" ? resolve3(cwd, flags.config) : findConfig(cwd) ?? join6(cwd, CONFIG_FILE2));
40453
41057
  const files = collectFiles(root, positional.map((p) => relative2(root, resolve3(cwd, p))), resolved.ignore);
40454
41058
  let onlyLines;
40455
41059
  if (flags.changed) {
@@ -40504,46 +41108,29 @@ function tokensCssPath(root, config2, flags) {
40504
41108
  function writeTokensCss(root, rel, config2, org) {
40505
41109
  const abs = join6(root, rel);
40506
41110
  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
- `;
41111
+ const { css, usesKit } = tokensCssContent(config2, org);
40513
41112
  writeFileSync3(abs, css);
40514
41113
  return usesKit;
40515
41114
  }
40516
41115
  function writeRuleFiles(root, config2, org, tokensCss) {
40517
41116
  const md = generateInstructions(config2, { org, tokensCss });
40518
41117
  const written = [];
40519
- const write2 = (rel, content) => {
40520
- const abs = join6(root, rel);
41118
+ for (const { file: file2, readBy } of RULE_FILES) {
41119
+ const abs = join6(root, file2);
40521
41120
  mkdirSync3(dirname4(abs), { recursive: true });
40522
41121
  const before = existsSync6(abs) ? readFileSync5(abs, "utf8") : "";
40523
- writeFileSync3(abs, content(before));
40524
- written.push(rel);
40525
- };
40526
- 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})`;
41122
+ writeFileSync3(abs, ruleFileContent(file2, md, before));
41123
+ written.push(`${file2} (${readBy})`);
41124
+ }
41125
+ const pkg = readJson(join6(root, "package.json"));
41126
+ for (const [file2, content, readBy] of [
41127
+ [MIGRATE_SKILL_FILE, migrationSkill(config2, { org, pkg }), "Claude Code skill: migrate to the design system"],
41128
+ [MIGRATE_AGENT_FILE, migrationAgent({ org }), "Claude Code agent: brandrail-migrator"]
41129
+ ]) {
41130
+ const abs = join6(root, file2);
41131
+ mkdirSync3(dirname4(abs), { recursive: true });
41132
+ writeFileSync3(abs, content);
41133
+ written.push(`${file2} (${readBy})`);
40547
41134
  }
40548
41135
  return written;
40549
41136
  }
@@ -40603,7 +41190,7 @@ function projectFramework(cwd, config2, flags) {
40603
41190
  }
40604
41191
  }
40605
41192
  async function cmdInit(cwd, flags) {
40606
- const target = join6(cwd, CONFIG_FILE);
41193
+ const target = join6(cwd, CONFIG_FILE2);
40607
41194
  let config2 = starterConfig;
40608
41195
  let org;
40609
41196
  if (flags["from-api"]) {
@@ -40614,17 +41201,17 @@ async function cmdInit(cwd, flags) {
40614
41201
  org = res.org;
40615
41202
  }
40616
41203
  if (existsSync6(target) && !flags.force && !flags["from-api"]) {
40617
- log2(`${CONFIG_FILE} already exists (use --force to overwrite). Updating rule files from it.`);
41204
+ log2(`${CONFIG_FILE2} already exists (use --force to overwrite). Updating rule files from it.`);
40618
41205
  config2 = readConfig(target);
40619
41206
  org ??= config2.org;
40620
41207
  } else {
40621
41208
  if (existsSync6(target)) {
40622
41209
  const prev = readConfig(target);
40623
- config2 = { ...prev, ...config2, rules: { ...prev.rules, ...config2.rules }, ignore: prev.ignore ?? config2.ignore, tokensCss: prev.tokensCss };
41210
+ config2 = mergeProjectConfig(prev, config2);
40624
41211
  }
40625
41212
  writeFileSync3(target, `${JSON.stringify(config2, null, 2)}
40626
41213
  `);
40627
- log2(`\u2714 wrote ${CONFIG_FILE}${org ? ` from ${org}'s Brand Kit` : " (starter tokens \u2014 edit them or use --from-api)"}`);
41214
+ log2(`\u2714 wrote ${CONFIG_FILE2}${org ? ` from ${org}'s Brand Kit` : " (starter tokens \u2014 edit them or use --from-api)"}`);
40628
41215
  }
40629
41216
  const framework = projectFramework(cwd, config2, flags);
40630
41217
  if (config2.framework !== framework) {
@@ -40663,7 +41250,7 @@ async function cmdConnect(cwd, flags) {
40663
41250
  \u2714 connected to ${org}. The key is saved in ${credentialsPath()} (only you can read it), never in this repo.`);
40664
41251
  }
40665
41252
  await cmdInit(cwd, { ...flags, "from-api": true, mcp: false });
40666
- const config2 = readConfig(join6(cwd, CONFIG_FILE));
41253
+ const config2 = readConfig(join6(cwd, CONFIG_FILE2));
40667
41254
  const framework = projectFramework(cwd, config2, flags);
40668
41255
  writeMcpConfigs(cwd, framework, flags.remote ? "remote" : "local");
40669
41256
  const usesKit = (config2.components ?? []).some((c) => c.importPath === "@brandrail/kit");
@@ -40673,6 +41260,75 @@ Done. Restart your AI tool so it picks up the MCP server, then ask it to build s
40673
41260
  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
41261
  return 0;
40675
41262
  }
41263
+ async function cmdMigrate(cwd, positional, flags) {
41264
+ const { config: config2, source } = await loadConfig(cwd, flags);
41265
+ const resolved = resolveConfig(config2);
41266
+ const root = dirname4(findConfig(cwd) ?? join6(cwd, CONFIG_FILE2));
41267
+ const files = collectFiles(root, positional.map((p) => relative2(root, resolve3(cwd, p))), resolved.ignore);
41268
+ const s = migrationStatus(files, resolved);
41269
+ if (flags.format === "json" || flags.json) {
41270
+ 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)}
41271
+ `);
41272
+ } else {
41273
+ const color = flags.color !== false && process.stdout.isTTY !== false && !process.env.NO_COLOR;
41274
+ const c = (code, t) => color ? `\x1B[${code}m${t}\x1B[0m` : t;
41275
+ const bar = (p) => `${"\u2588".repeat(Math.round(p / 5))}${"\u2591".repeat(20 - Math.round(p / 5))}`;
41276
+ const out = [];
41277
+ out.push(c("2", `Brand Kit: ${source}${config2.tokenVersion ? ` v${config2.tokenVersion}` : ""}`));
41278
+ out.push("");
41279
+ 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})`);
41280
+ const libs = Object.entries(s.libraries);
41281
+ 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`));
41282
+ const top = typeof flags.top === "string" ? Number(flags.top) || 15 : 15;
41283
+ if (s.files.length) {
41284
+ out.push("", c("1", `Next files (${Math.min(top, s.files.length)} of ${s.files.length} with work left)`));
41285
+ for (const f of s.files.slice(0, top)) {
41286
+ const what = Object.entries(f.byRule).sort((a, b) => b[1] - a[1]).map(([r, n]) => `${r} ${n}`).join(", ");
41287
+ out.push(` ${String(f.findings).padStart(4)} ${f.path} ${c("2", what)}`);
41288
+ }
41289
+ out.push("", `Migrate a batch with your AI tool, e.g. "migrate ${s.files[0].path} to the design system"${existsSync6(join6(root, MIGRATE_SKILL_FILE)) ? " (Claude Code uses the brandrail-migrate skill)" : " \u2014 run `npx brandrail connect` first to add the migration skill"}.`);
41290
+ } else out.push("", c("32", "\u2714 Every UI file follows the design system."));
41291
+ process.stdout.write(`${out.join("\n")}
41292
+ `);
41293
+ }
41294
+ if (flags.upload) {
41295
+ const key = resolveKey();
41296
+ if (!key) log2(`warning: --upload skipped. ${NO_KEY}`);
41297
+ else {
41298
+ const repo = repoInfo(root);
41299
+ const res = await uploadReport(key, {
41300
+ source: process.env.CI ? "ci" : "cli",
41301
+ target: { kind: "repo", name: repo.slug.split("/").pop() ?? repo.slug, location: repo.slug },
41302
+ ref: repo.branch ?? repo.sha?.slice(0, 7),
41303
+ title: `Migration status: ${s.progress}% of UI files done`,
41304
+ report: s.report
41305
+ });
41306
+ log2(`Uploaded \u2192 ${res.url ?? res.scanId}`);
41307
+ }
41308
+ }
41309
+ return 0;
41310
+ }
41311
+ async function cmdReport(cwd, flags) {
41312
+ const { config: config2 } = await loadConfig(cwd, flags);
41313
+ const resolved = resolveConfig(config2);
41314
+ const root = dirname4(findConfig(cwd) ?? join6(cwd, CONFIG_FILE2));
41315
+ const s = migrationStatus(collectFiles(root, [], resolved.ignore), resolved);
41316
+ const repo = repoInfo(root);
41317
+ const out = resolve3(cwd, typeof flags.out === "string" ? flags.out : "brandrail-report.html");
41318
+ writeFileSync3(
41319
+ out,
41320
+ formatHandoverHtml(s, {
41321
+ project: repo.slug,
41322
+ org: config2.org,
41323
+ branch: repo.branch,
41324
+ commit: repo.sha,
41325
+ kitVersion: config2.tokenVersion,
41326
+ framework: config2.framework ? FRAMEWORK_LABEL[config2.framework] : void 0
41327
+ })
41328
+ );
41329
+ log2(`\u2714 wrote ${relative2(cwd, out)}: ${s.progress}% of UI files on the design system, brand score ${s.report.score}. Open it in a browser, or print it to PDF.`);
41330
+ return 0;
41331
+ }
40676
41332
  function cmdWhoami() {
40677
41333
  if (process.env.BRANDRAIL_KEY) {
40678
41334
  log2(`Using BRANDRAIL_KEY from the environment (${process.env.BRANDRAIL_KEY.slice(0, 12)}\u2026) against ${apiBase()}.`);
@@ -40770,7 +41426,7 @@ function cmdImport(cwd, positional, flags) {
40770
41426
  if (!files.length) throw new Error("No .json token files found.");
40771
41427
  const res = importTokenFiles(files, { includePrimitives: !!flags.primitives });
40772
41428
  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);
41429
+ const path = (typeof flags.config === "string" ? resolve3(cwd, flags.config) : findConfig(cwd)) ?? join6(cwd, CONFIG_FILE2);
40774
41430
  const config2 = existsSync6(path) ? readConfig(path) : { ...starterConfig, tokens: {} };
40775
41431
  const next = flags.replace ? res.tokens : mergeTokens(config2.tokens, res.tokens);
40776
41432
  const d = diffTokens(config2.tokens, next);
@@ -40779,7 +41435,7 @@ function cmdImport(cwd, positional, flags) {
40779
41435
  log2(`\u2714 ${res.count} tokens from ${file2} (${res.format})${res.primitives ? `, ${res.primitives} palette primitives left out (--primitives to keep)` : ""}`);
40780
41436
  for (const n of res.notes) log2(` \xB7 ${n}`);
40781
41437
  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`);
41438
+ log2(`\u2714 ${relative2(cwd, path) || CONFIG_FILE2}: ${d.changed.length} changed, ${d.added.length} added, ${d.removed.length} removed`);
40783
41439
  log2("\nNext: npx brandrail init (to refresh AI rule files), then npx brandrail check");
40784
41440
  return 0;
40785
41441
  }
@@ -40811,6 +41467,10 @@ async function main(argv = process.argv.slice(2)) {
40811
41467
  return cmdConnect(cwd, flags);
40812
41468
  case "mcp":
40813
41469
  return runLocalMcp(cwd, { framework: typeof flags.framework === "string" ? flags.framework : void 0, report: flags.report === true });
41470
+ case "migrate":
41471
+ return cmdMigrate(cwd, positional, flags);
41472
+ case "report":
41473
+ return cmdReport(cwd, flags);
40814
41474
  case "whoami":
40815
41475
  return cmdWhoami();
40816
41476
  case "logout":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brandrail",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",