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 +2 -0
- package/dist/brandrail.mjs +785 -125
- package/package.json +1 -1
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.
|
package/dist/brandrail.mjs
CHANGED
|
@@ -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.
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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: () =>
|
|
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
|
|
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)}]` :
|
|
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) :
|
|
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(${
|
|
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}) !== ${
|
|
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}) !== ${
|
|
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) :
|
|
24258
|
-
const propKey = (k) => typeof k === "symbol" ? `[${keyExpr(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 !== ${
|
|
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} !== ${
|
|
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}?.[${
|
|
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} === ${
|
|
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
|
|
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
|
|
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,
|
|
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) ||
|
|
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 ${
|
|
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,
|
|
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
|
-
|
|
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
|
|
40520
|
-
const abs = join6(root,
|
|
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,
|
|
40524
|
-
written.push(
|
|
40525
|
-
}
|
|
40526
|
-
|
|
40527
|
-
|
|
40528
|
-
|
|
40529
|
-
|
|
40530
|
-
|
|
40531
|
-
|
|
40532
|
-
|
|
40533
|
-
|
|
40534
|
-
${
|
|
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,
|
|
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(`${
|
|
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 =
|
|
41210
|
+
config2 = mergeProjectConfig(prev, config2);
|
|
40624
41211
|
}
|
|
40625
41212
|
writeFileSync3(target, `${JSON.stringify(config2, null, 2)}
|
|
40626
41213
|
`);
|
|
40627
|
-
log2(`\u2714 wrote ${
|
|
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,
|
|
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,
|
|
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) ||
|
|
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.
|
|
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",
|