brandrail 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/brandrail.mjs +951 -181
- package/package.json +1 -1
package/dist/brandrail.mjs
CHANGED
|
@@ -3911,50 +3911,50 @@ var require_fast_uri = __commonJS({
|
|
|
3911
3911
|
schemelessOptions.skipEscape = true;
|
|
3912
3912
|
return serialize(resolved, schemelessOptions);
|
|
3913
3913
|
}
|
|
3914
|
-
function resolveComponent(base,
|
|
3914
|
+
function resolveComponent(base, relative4, options, skipNormalization) {
|
|
3915
3915
|
const target = {};
|
|
3916
3916
|
if (!skipNormalization) {
|
|
3917
3917
|
base = parse3(serialize(base, options), options);
|
|
3918
|
-
|
|
3918
|
+
relative4 = parse3(serialize(relative4, options), options);
|
|
3919
3919
|
}
|
|
3920
3920
|
options = options || {};
|
|
3921
|
-
if (!options.tolerant &&
|
|
3922
|
-
target.scheme =
|
|
3923
|
-
target.userinfo =
|
|
3924
|
-
target.host =
|
|
3925
|
-
target.port =
|
|
3926
|
-
target.path = removeDotSegments(
|
|
3927
|
-
target.query =
|
|
3921
|
+
if (!options.tolerant && relative4.scheme) {
|
|
3922
|
+
target.scheme = relative4.scheme;
|
|
3923
|
+
target.userinfo = relative4.userinfo;
|
|
3924
|
+
target.host = relative4.host;
|
|
3925
|
+
target.port = relative4.port;
|
|
3926
|
+
target.path = removeDotSegments(relative4.path || "");
|
|
3927
|
+
target.query = relative4.query;
|
|
3928
3928
|
} else {
|
|
3929
|
-
if (
|
|
3930
|
-
target.userinfo =
|
|
3931
|
-
target.host =
|
|
3932
|
-
target.port =
|
|
3933
|
-
target.path = removeDotSegments(
|
|
3934
|
-
target.query =
|
|
3929
|
+
if (relative4.userinfo !== void 0 || relative4.host !== void 0 || relative4.port !== void 0) {
|
|
3930
|
+
target.userinfo = relative4.userinfo;
|
|
3931
|
+
target.host = relative4.host;
|
|
3932
|
+
target.port = relative4.port;
|
|
3933
|
+
target.path = removeDotSegments(relative4.path || "");
|
|
3934
|
+
target.query = relative4.query;
|
|
3935
3935
|
} else {
|
|
3936
|
-
if (!
|
|
3936
|
+
if (!relative4.path) {
|
|
3937
3937
|
target.path = base.path;
|
|
3938
|
-
if (
|
|
3939
|
-
target.query =
|
|
3938
|
+
if (relative4.query !== void 0) {
|
|
3939
|
+
target.query = relative4.query;
|
|
3940
3940
|
} else {
|
|
3941
3941
|
target.query = base.query;
|
|
3942
3942
|
}
|
|
3943
3943
|
} else {
|
|
3944
|
-
if (
|
|
3945
|
-
target.path = removeDotSegments(
|
|
3944
|
+
if (relative4.path[0] === "/") {
|
|
3945
|
+
target.path = removeDotSegments(relative4.path);
|
|
3946
3946
|
} else {
|
|
3947
3947
|
const baseHasAuthority = base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0;
|
|
3948
3948
|
if (baseHasAuthority && !base.path) {
|
|
3949
|
-
target.path = "/" +
|
|
3949
|
+
target.path = "/" + relative4.path;
|
|
3950
3950
|
} else if (!base.path) {
|
|
3951
|
-
target.path =
|
|
3951
|
+
target.path = relative4.path;
|
|
3952
3952
|
} else {
|
|
3953
|
-
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) +
|
|
3953
|
+
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative4.path;
|
|
3954
3954
|
}
|
|
3955
3955
|
target.path = base.scheme === void 0 && !baseHasAuthority && target.path[0] !== "/" ? removeDotSegmentsRelative(target.path) : removeDotSegments(target.path);
|
|
3956
3956
|
}
|
|
3957
|
-
target.query =
|
|
3957
|
+
target.query = relative4.query;
|
|
3958
3958
|
}
|
|
3959
3959
|
target.userinfo = base.userinfo;
|
|
3960
3960
|
target.host = base.host;
|
|
@@ -3962,7 +3962,7 @@ var require_fast_uri = __commonJS({
|
|
|
3962
3962
|
}
|
|
3963
3963
|
target.scheme = base.scheme;
|
|
3964
3964
|
}
|
|
3965
|
-
target.fragment =
|
|
3965
|
+
target.fragment = relative4.fragment;
|
|
3966
3966
|
return target;
|
|
3967
3967
|
}
|
|
3968
3968
|
function equal(uriA, uriB, options) {
|
|
@@ -7256,8 +7256,8 @@ var require_dist = __commonJS({
|
|
|
7256
7256
|
});
|
|
7257
7257
|
|
|
7258
7258
|
// src/index.ts
|
|
7259
|
-
import { existsSync as
|
|
7260
|
-
import { basename as basename2, dirname as dirname4, join as
|
|
7259
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7260
|
+
import { basename as basename2, dirname as dirname4, join as join7, relative as relative3, resolve as resolve3 } from "node:path";
|
|
7261
7261
|
|
|
7262
7262
|
// ../../node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs
|
|
7263
7263
|
import { createRequire } from "module";
|
|
@@ -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.7.0";
|
|
8778
9189
|
var WEIGHT = { error: 4, warning: 1.5, info: 0.25 };
|
|
8779
9190
|
function computeScore(counts, lines) {
|
|
8780
9191
|
const penalty = counts.error * WEIGHT.error + counts.warning * WEIGHT.warning + counts.info * WEIGHT.info;
|
|
@@ -9636,6 +10047,9 @@ function generateInstructions(config2, opts = {}) {
|
|
|
9636
10047
|
if (sizeVars.length) lines.push(`- Font sizes come from the type scale: ${sizeVars.join(", ")}. Matching line heights are \`--font-line-height-*\`.`);
|
|
9637
10048
|
if (r.fonts.length) lines.push(`- Fonts: use ${r.fontVars.map((v) => `\`var(${v})\``).join(" or ")}; never set another font-family.`);
|
|
9638
10049
|
lines.push("");
|
|
10050
|
+
lines.push("## Migrating existing UI");
|
|
10051
|
+
lines.push("- Code that still uses Material, MUI, Bootstrap, Ant Design, Chakra, PrimeNG or raw HTML controls is migration work (`legacy-library` findings). `npx brandrail migrate` lists what's left per file. In Claude Code, use the `brandrail-migrate` skill (or the `brandrail-migrator` agent) and work in small batches; keep behaviour identical.");
|
|
10052
|
+
lines.push("");
|
|
9639
10053
|
lines.push("## Before you finish");
|
|
9640
10054
|
lines.push("- If the Brandrail MCP server is connected, call `validate_snippet` on the UI you wrote and fix every violation.");
|
|
9641
10055
|
lines.push("- Otherwise run `npx brandrail check` and fix errors before committing.");
|
|
@@ -9664,70 +10078,6 @@ ${block}
|
|
|
9664
10078
|
`;
|
|
9665
10079
|
}
|
|
9666
10080
|
|
|
9667
|
-
// ../engine/src/components.ts
|
|
9668
|
-
var SYNONYMS = {
|
|
9669
|
-
button: ["btn", "cta", "action", "submit", "click"],
|
|
9670
|
-
input: ["textinput", "textfield", "field", "text", "inputtext", "entry", "form"],
|
|
9671
|
-
select: ["dropdown", "picker", "combobox", "menu", "options", "choose"],
|
|
9672
|
-
textarea: ["multiline", "comment", "notes", "message", "inputtextarea"],
|
|
9673
|
-
checkbox: ["check", "tick", "toggle", "agree"],
|
|
9674
|
-
radio: ["radiogroup", "option", "choice"],
|
|
9675
|
-
toggle: ["switch", "onoff"],
|
|
9676
|
-
dialog: ["modal", "popup", "overlay", "lightbox"],
|
|
9677
|
-
tooltip: ["hint", "hover", "popover"],
|
|
9678
|
-
notification: ["toast", "alert", "flash", "banner", "snackbar", "message"],
|
|
9679
|
-
avatar: ["profile", "user", "picture", "initials"],
|
|
9680
|
-
datepicker: ["date", "calendar", "day", "schedule"],
|
|
9681
|
-
progressbar: ["progress", "loading", "percent", "meter"],
|
|
9682
|
-
loader: ["spinner", "loading", "busy"],
|
|
9683
|
-
breadcrumb: ["path", "trail", "navigation", "nav"],
|
|
9684
|
-
divider: ["separator", "rule", "hr", "line"],
|
|
9685
|
-
table: ["datatable", "grid", "rows", "list"],
|
|
9686
|
-
card: ["box", "panel", "tile", "container"],
|
|
9687
|
-
label: ["badge", "tag", "chip", "pill", "status"],
|
|
9688
|
-
tabs: ["tab", "segmented"]
|
|
9689
|
-
};
|
|
9690
|
-
var norm = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
9691
|
-
var squash = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
9692
|
-
function expand(words) {
|
|
9693
|
-
const out = new Set(words);
|
|
9694
|
-
for (const w of words) {
|
|
9695
|
-
for (const [canon, syns] of Object.entries(SYNONYMS)) {
|
|
9696
|
-
if (w === canon || syns.includes(w)) {
|
|
9697
|
-
out.add(canon);
|
|
9698
|
-
for (const s of syns) out.add(s);
|
|
9699
|
-
}
|
|
9700
|
-
}
|
|
9701
|
-
}
|
|
9702
|
-
return out;
|
|
9703
|
-
}
|
|
9704
|
-
function findComponents(config2, intent, limit = 5) {
|
|
9705
|
-
const words = norm(intent).split(" ").filter((w) => w.length > 1);
|
|
9706
|
-
const query = expand(words);
|
|
9707
|
-
const q = squash(intent);
|
|
9708
|
-
const out = [];
|
|
9709
|
-
for (const c of config2.components ?? []) {
|
|
9710
|
-
if (c.status === "deprecated") continue;
|
|
9711
|
-
const name = squash(c.name);
|
|
9712
|
-
let score = 0;
|
|
9713
|
-
if (name === q) score += 100;
|
|
9714
|
-
else if (q.includes(name) || name.includes(q)) score += 40;
|
|
9715
|
-
const nameTerms = expand([name, ...(c.aliases ?? []).map(squash), ...c.replaces ?? []]);
|
|
9716
|
-
for (const w of query) {
|
|
9717
|
-
if (nameTerms.has(w)) score += 20;
|
|
9718
|
-
else if (name.includes(w) && w.length > 2) score += 10;
|
|
9719
|
-
}
|
|
9720
|
-
const text2 = norm([c.description ?? "", ...c.usageRules ?? []].join(" "));
|
|
9721
|
-
for (const w of words) if (w.length > 2 && text2.includes(w)) score += 3;
|
|
9722
|
-
if (score >= 3) out.push({ component: c, score: score + (c.status === "stable" ? 1 : 0) });
|
|
9723
|
-
}
|
|
9724
|
-
return out.sort((a, b) => b.score - a.score || a.component.name.localeCompare(b.component.name)).slice(0, limit);
|
|
9725
|
-
}
|
|
9726
|
-
function findComponentByName(config2, name) {
|
|
9727
|
-
const n = squash(name);
|
|
9728
|
-
return (config2.components ?? []).find((c) => squash(c.name) === n || (c.aliases ?? []).some((a) => squash(a) === n));
|
|
9729
|
-
}
|
|
9730
|
-
|
|
9731
10081
|
// ../engine/src/mcp-config.ts
|
|
9732
10082
|
var json = (o) => `${JSON.stringify(o, null, 2)}
|
|
9733
10083
|
`;
|
|
@@ -10371,6 +10721,227 @@ function auditBrandKit(tokens) {
|
|
|
10371
10721
|
}).sort((a, b) => a.severity === b.severity ? 0 : a.severity === "error" ? -1 : 1);
|
|
10372
10722
|
}
|
|
10373
10723
|
|
|
10724
|
+
// ../engine/src/migrate-skill.ts
|
|
10725
|
+
var MIGRATE_SKILL_FILE = ".claude/skills/brandrail-migrate/SKILL.md";
|
|
10726
|
+
var MIGRATE_AGENT_FILE = ".claude/agents/brandrail-migrator.md";
|
|
10727
|
+
function librariesFor(pkg) {
|
|
10728
|
+
if (!pkg) return void 0;
|
|
10729
|
+
return [.../* @__PURE__ */ new Set([...detectLibraries(pkg), "bootstrap"])];
|
|
10730
|
+
}
|
|
10731
|
+
function migrationSkill(config2, opts = {}) {
|
|
10732
|
+
const fw = config2.framework ?? "react";
|
|
10733
|
+
const libs = librariesFor(opts.pkg);
|
|
10734
|
+
const ds = opts.org ? `the ${opts.org} design system` : "the design system";
|
|
10735
|
+
return `---
|
|
10736
|
+
name: brandrail-migrate
|
|
10737
|
+
description: Migrate existing UI in this repo to ${ds}. Replaces Material, MUI, Bootstrap, Ant Design, Chakra, PrimeNG and raw HTML controls with design system components, and hard-coded colours, spacing and fonts with Brand Kit tokens. Use when asked to migrate, reskin, adopt the design system, or clear Brandrail findings across files.
|
|
10738
|
+
---
|
|
10739
|
+
|
|
10740
|
+
# Migrate UI to ${ds}
|
|
10741
|
+
|
|
10742
|
+
This is ${fw === "angular" ? "an" : "a"} ${FRAMEWORK_LABEL[fw]} project. The design system rules are in CLAUDE.md / AGENTS.md (section "Design system rules"); follow them.
|
|
10743
|
+
If the Brandrail MCP server is connected, use \`get_component_docs\` for each component's props and events, and \`validate_snippet\` on every file you change.
|
|
10744
|
+
|
|
10745
|
+
## Steps
|
|
10746
|
+
|
|
10747
|
+
1. **See what's left**: \`npx brandrail migrate\` lists findings per file, worst first (\`--json\` for a machine-readable list).
|
|
10748
|
+
2. **Pick a small batch**: the files the user named, or the top 1-5 files from the list. Never the whole repo at once.
|
|
10749
|
+
3. **Migrate each file**, finding by finding:
|
|
10750
|
+
- \`legacy-library\` and \`forbidden-import\`: replace the other library's piece with the component in the table below. Map its props and events
|
|
10751
|
+
to the component's (check \`get_component_docs\`), keep every binding, handler, validation and accessibility attribute, then remove the old import.
|
|
10752
|
+
- \`prefer-official-component\`: raw \`<button>\`, \`<select>\`, \`<input>\` \u2192 the design system component.
|
|
10753
|
+
- \`no-hardcoded-color\`, \`spacing-scale\`, \`radius-scale\`, \`font-family\`: use the Brand Kit CSS variables, never new literal values.
|
|
10754
|
+
- \`component-props\`: only the props / inputs / events the component lists.
|
|
10755
|
+
4. **Prove it**: \`npx brandrail check <the files>\` shows none of these findings for them, and the project's own build, lint and tests still pass.
|
|
10756
|
+
Look at the screen if you can: the layout should look the same apart from the brand.
|
|
10757
|
+
5. **Commit the batch** ("Migrate <area> to the design system"), then go back to step 1.
|
|
10758
|
+
6. **Remove a library** from package.json only when \`npx brandrail migrate\` shows 0 usages of it in the whole repo, and say so in the commit.
|
|
10759
|
+
|
|
10760
|
+
## Rules
|
|
10761
|
+
|
|
10762
|
+
- Change the look, not the behaviour: no logic, routing, data or API changes in a migration commit.
|
|
10763
|
+
- If a piece has no design system component (the table says so), build it with semantic HTML and the Brand Kit tokens, and list it in your summary
|
|
10764
|
+
so the design system team can add a component.
|
|
10765
|
+
- Ask before deleting files or changing shared code (layouts, base components) that many screens use.
|
|
10766
|
+
${fw === "angular" ? "- Angular: every NgModule or standalone component that uses a custom element needs `CUSTOM_ELEMENTS_SCHEMA`; read event values with `$any($event).detail`; bind properties in camelCase (`[selectedDate]`).\n" : ""}
|
|
10767
|
+
## What replaces what (${FRAMEWORK_LABEL[fw]})
|
|
10768
|
+
|
|
10769
|
+
${libs ? `Libraries in this project: ${libs.map((l) => LIBRARY_LABEL[l]).join(", ")}.
|
|
10770
|
+
|
|
10771
|
+
` : ""}${migrationMapMarkdown(resolveConfig(config2), fw, libs)}
|
|
10772
|
+
`;
|
|
10773
|
+
}
|
|
10774
|
+
function migrationAgent(opts = {}) {
|
|
10775
|
+
return `---
|
|
10776
|
+
name: brandrail-migrator
|
|
10777
|
+
description: Migrates a batch of UI files to ${opts.org ? `the ${opts.org}` : "the"} design system using the brandrail-migrate skill. Use for "migrate <files or folder> to the design system", one batch per call.
|
|
10778
|
+
tools: Read, Edit, Write, Grep, Glob, Bash
|
|
10779
|
+
---
|
|
10780
|
+
|
|
10781
|
+
You migrate UI code to the design system. Follow the \`brandrail-migrate\` skill exactly (.claude/skills/brandrail-migrate/SKILL.md).
|
|
10782
|
+
|
|
10783
|
+
For the files or folder you're given:
|
|
10784
|
+
1. Run \`npx brandrail check <paths>\` to list the findings.
|
|
10785
|
+
2. Migrate every finding in those files, keeping behaviour identical.
|
|
10786
|
+
3. Run \`npx brandrail check <paths>\` again until it's clean, then the project's build or tests if they're quick.
|
|
10787
|
+
4. Reply with: files changed, findings fixed, anything that had no design system component, and anything you weren't sure about.
|
|
10788
|
+
|
|
10789
|
+
Don't touch files outside the batch. Don't commit unless you were asked to.
|
|
10790
|
+
`;
|
|
10791
|
+
}
|
|
10792
|
+
|
|
10793
|
+
// ../engine/src/project.ts
|
|
10794
|
+
var KIT = "@brandrail/kit";
|
|
10795
|
+
function mergeProjectConfig(prev, kit) {
|
|
10796
|
+
if (!prev) return kit;
|
|
10797
|
+
return {
|
|
10798
|
+
...prev,
|
|
10799
|
+
...kit,
|
|
10800
|
+
rules: { ...prev.rules, ...kit.rules },
|
|
10801
|
+
ignore: prev.ignore ?? kit.ignore,
|
|
10802
|
+
tokensCss: prev.tokensCss,
|
|
10803
|
+
framework: prev.framework ?? kit.framework
|
|
10804
|
+
};
|
|
10805
|
+
}
|
|
10806
|
+
function tokensCssContent(config2, org) {
|
|
10807
|
+
let css = generateTokensCss(config2.tokens, { version: config2.tokenVersion, org });
|
|
10808
|
+
const usesKit = (config2.components ?? []).some((c) => c.importPath === KIT);
|
|
10809
|
+
if (usesKit) css += `
|
|
10810
|
+
/* ${KIT} follows the Brand Kit: colours, font, spacing, radius and type scale. */
|
|
10811
|
+
${kitBridgeCss(config2.tokens)}
|
|
10812
|
+
`;
|
|
10813
|
+
return { css, usesKit };
|
|
10814
|
+
}
|
|
10815
|
+
var CURSOR_GLOBS = "**/*.{tsx,jsx,ts,vue,svelte,html,css,scss,php,twig,hbs,mustache}";
|
|
10816
|
+
function ruleFileContent(file2, md, before) {
|
|
10817
|
+
if (file2.endsWith(".mdc")) return `---
|
|
10818
|
+
description: Design system rules from Brandrail
|
|
10819
|
+
globs: ["${CURSOR_GLOBS}"]
|
|
10820
|
+
alwaysApply: true
|
|
10821
|
+
---
|
|
10822
|
+
|
|
10823
|
+
${md}
|
|
10824
|
+
`;
|
|
10825
|
+
if (file2.startsWith(".windsurf/")) return `---
|
|
10826
|
+
trigger: always_on
|
|
10827
|
+
---
|
|
10828
|
+
|
|
10829
|
+
${md}
|
|
10830
|
+
`;
|
|
10831
|
+
return upsertSection(before, md);
|
|
10832
|
+
}
|
|
10833
|
+
|
|
10834
|
+
// ../engine/src/migrate-status.ts
|
|
10835
|
+
var MIGRATION_RULES = new Set(RULES.map((r) => r.id).filter((id) => id !== "img-alt"));
|
|
10836
|
+
var LABEL_TO_ID = new Map(Object.entries(LIBRARY_LABEL).map(([id, label]) => [label, id]));
|
|
10837
|
+
function migrationStatus(files, configInput) {
|
|
10838
|
+
const config2 = "replaces" in configInput ? configInput : resolveConfig(configInput);
|
|
10839
|
+
const report = check(files, config2, { snippets: false });
|
|
10840
|
+
const per = /* @__PURE__ */ new Map();
|
|
10841
|
+
const libraries = {};
|
|
10842
|
+
const byRule = {};
|
|
10843
|
+
for (const v of report.violations) {
|
|
10844
|
+
if (!MIGRATION_RULES.has(v.ruleId)) continue;
|
|
10845
|
+
byRule[v.ruleId] = (byRule[v.ruleId] ?? 0) + 1;
|
|
10846
|
+
const f = per.get(v.file) ?? { path: v.file, findings: 0, byRule: {}, libraries: [] };
|
|
10847
|
+
f.findings++;
|
|
10848
|
+
f.byRule[v.ruleId] = (f.byRule[v.ruleId] ?? 0) + 1;
|
|
10849
|
+
if (v.ruleId === "legacy-library") {
|
|
10850
|
+
const label = /\(([^()]*(?:\([^()]*\))?[^()]*)\) should be/.exec(v.message)?.[1];
|
|
10851
|
+
const id = label ? LABEL_TO_ID.get(label) : void 0;
|
|
10852
|
+
if (id) {
|
|
10853
|
+
libraries[id] = (libraries[id] ?? 0) + 1;
|
|
10854
|
+
if (!f.libraries.includes(id)) f.libraries.push(id);
|
|
10855
|
+
}
|
|
10856
|
+
}
|
|
10857
|
+
per.set(v.file, f);
|
|
10858
|
+
}
|
|
10859
|
+
const list = [...per.values()].sort((a, b) => b.findings - a.findings || a.path.localeCompare(b.path));
|
|
10860
|
+
const filesClean = Math.max(0, report.filesScanned - list.length);
|
|
10861
|
+
return {
|
|
10862
|
+
report,
|
|
10863
|
+
files: list,
|
|
10864
|
+
filesScanned: report.filesScanned,
|
|
10865
|
+
filesClean,
|
|
10866
|
+
progress: report.filesScanned ? Math.round(filesClean / report.filesScanned * 100) : 100,
|
|
10867
|
+
libraries,
|
|
10868
|
+
byRule
|
|
10869
|
+
};
|
|
10870
|
+
}
|
|
10871
|
+
|
|
10872
|
+
// ../engine/src/report-html.ts
|
|
10873
|
+
var esc2 = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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;
|
|
@@ -40215,7 +40801,11 @@ function parseChangedLines(diff) {
|
|
|
40215
40801
|
}
|
|
40216
40802
|
function changedLines(cwd, base) {
|
|
40217
40803
|
const mergeBase = git(["merge-base", base, "HEAD"], cwd) ?? base;
|
|
40218
|
-
const diff = git(["diff", "-U0", "--no-color", "--diff-filter=AMR", mergeBase], cwd);
|
|
40804
|
+
const diff = git(["diff", "--relative", "-U0", "--no-color", "--diff-filter=AMR", mergeBase], cwd);
|
|
40805
|
+
return diff === null ? null : parseChangedLines(diff);
|
|
40806
|
+
}
|
|
40807
|
+
function stagedLines(cwd) {
|
|
40808
|
+
const diff = git(["diff", "--cached", "--relative", "-U0", "--no-color", "--diff-filter=AMR"], cwd);
|
|
40219
40809
|
return diff === null ? null : parseChangedLines(diff);
|
|
40220
40810
|
}
|
|
40221
40811
|
|
|
@@ -40300,12 +40890,82 @@ async function runLocalMcp(cwd, opts) {
|
|
|
40300
40890
|
});
|
|
40301
40891
|
}
|
|
40302
40892
|
|
|
40893
|
+
// src/hook.ts
|
|
40894
|
+
import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
40895
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
40896
|
+
import { isAbsolute, join as join5, relative as relative2 } from "node:path";
|
|
40897
|
+
var HOOK_START = "# >>> brandrail (npx brandrail hook uninstall to remove)";
|
|
40898
|
+
var HOOK_END = "# <<< brandrail";
|
|
40899
|
+
var hookBlock = (cmd) => [
|
|
40900
|
+
HOOK_START,
|
|
40901
|
+
`${cmd} || { echo ""; echo "Brandrail: fix the errors above (or commit with --no-verify to skip this once)."; exit 1; }`,
|
|
40902
|
+
HOOK_END
|
|
40903
|
+
].join("\n");
|
|
40904
|
+
var git2 = (args, cwd) => {
|
|
40905
|
+
try {
|
|
40906
|
+
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
40907
|
+
} catch {
|
|
40908
|
+
return null;
|
|
40909
|
+
}
|
|
40910
|
+
};
|
|
40911
|
+
function hookFile(cwd) {
|
|
40912
|
+
const top = git2(["rev-parse", "--show-toplevel"], cwd);
|
|
40913
|
+
if (!top) return null;
|
|
40914
|
+
if (existsSync5(join5(top, ".husky"))) return { path: join5(top, ".husky", "pre-commit"), kind: "husky" };
|
|
40915
|
+
const custom2 = git2(["config", "core.hooksPath"], cwd);
|
|
40916
|
+
if (custom2 && !/\.husky/.test(custom2)) return { path: join5(isAbsolute(custom2) ? custom2 : join5(top, custom2), "pre-commit"), kind: "git" };
|
|
40917
|
+
const hooks = git2(["rev-parse", "--git-path", "hooks"], cwd);
|
|
40918
|
+
if (!hooks) return null;
|
|
40919
|
+
return { path: join5(isAbsolute(hooks) ? hooks : join5(cwd, hooks), "pre-commit"), kind: "git" };
|
|
40920
|
+
}
|
|
40921
|
+
function withoutBlock(text2) {
|
|
40922
|
+
const start = text2.indexOf(HOOK_START);
|
|
40923
|
+
const end = text2.indexOf(HOOK_END);
|
|
40924
|
+
if (start === -1 || end === -1 || end < start) return text2;
|
|
40925
|
+
return (text2.slice(0, start) + text2.slice(end + HOOK_END.length)).replace(/\n{3,}/g, "\n\n");
|
|
40926
|
+
}
|
|
40927
|
+
function withBlock(text2, block, kind) {
|
|
40928
|
+
const base = withoutBlock(text2).trimEnd();
|
|
40929
|
+
if (!base) return kind === "husky" ? `${block}
|
|
40930
|
+
` : `#!/bin/sh
|
|
40931
|
+
${block}
|
|
40932
|
+
`;
|
|
40933
|
+
return `${base}
|
|
40934
|
+
|
|
40935
|
+
${block}
|
|
40936
|
+
`;
|
|
40937
|
+
}
|
|
40938
|
+
function installHook(cwd, opts) {
|
|
40939
|
+
const where = hookFile(cwd);
|
|
40940
|
+
if (!where) throw new Error("This isn't a git repository (no .git found).");
|
|
40941
|
+
const top = git2(["rev-parse", "--show-toplevel"], cwd) ?? cwd;
|
|
40942
|
+
const sub = relative2(top, opts.root);
|
|
40943
|
+
const cd = sub && !sub.startsWith("..") ? `cd "${sub}" && ` : "";
|
|
40944
|
+
const cmd = `${cd}npx --yes brandrail check --staged --strict${opts.failBelow !== void 0 ? ` --fail-below ${opts.failBelow}` : ""}`;
|
|
40945
|
+
const before = existsSync5(where.path) ? readFileSync5(where.path, "utf8") : "";
|
|
40946
|
+
mkdirSync3(join5(where.path, ".."), { recursive: true });
|
|
40947
|
+
writeFileSync3(where.path, withBlock(before, hookBlock(cmd), where.kind));
|
|
40948
|
+
try {
|
|
40949
|
+
chmodSync2(where.path, 493);
|
|
40950
|
+
} catch {
|
|
40951
|
+
}
|
|
40952
|
+
return { path: relative2(cwd, where.path) || where.path, kind: where.kind };
|
|
40953
|
+
}
|
|
40954
|
+
function uninstallHook(cwd) {
|
|
40955
|
+
const where = hookFile(cwd);
|
|
40956
|
+
if (!where || !existsSync5(where.path)) return null;
|
|
40957
|
+
const before = readFileSync5(where.path, "utf8");
|
|
40958
|
+
if (!before.includes(HOOK_START)) return null;
|
|
40959
|
+
writeFileSync3(where.path, withoutBlock(before));
|
|
40960
|
+
return relative2(cwd, where.path) || where.path;
|
|
40961
|
+
}
|
|
40962
|
+
|
|
40303
40963
|
// src/scan.ts
|
|
40304
|
-
import { existsSync as
|
|
40964
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
40305
40965
|
import { createRequire as createRequire2 } from "node:module";
|
|
40306
|
-
import { join as
|
|
40966
|
+
import { join as join6, resolve as resolve2 } from "node:path";
|
|
40307
40967
|
function loadPlaywright(cwd) {
|
|
40308
|
-
for (const base of [
|
|
40968
|
+
for (const base of [join6(cwd, "package.json"), import.meta.url]) {
|
|
40309
40969
|
for (const name of ["playwright", "playwright-core"]) {
|
|
40310
40970
|
try {
|
|
40311
40971
|
return createRequire2(base)(name);
|
|
@@ -40319,7 +40979,7 @@ async function scanPages(cwd, url2, config2, opts, log3) {
|
|
|
40319
40979
|
const pw = loadPlaywright(cwd);
|
|
40320
40980
|
const start = /^https?:\/\//i.test(url2) ? url2 : `http://${url2}`;
|
|
40321
40981
|
const state = opts.storageState ? resolve2(cwd, opts.storageState) : void 0;
|
|
40322
|
-
if (state && !
|
|
40982
|
+
if (state && !existsSync6(state)) throw new Error(`--storage-state file not found: ${state}`);
|
|
40323
40983
|
const browser = await pw.chromium.launch({ headless: true });
|
|
40324
40984
|
try {
|
|
40325
40985
|
const context = await browser.newContext({ viewport: { width: opts.width, height: 900 }, storageState: state });
|
|
@@ -40364,7 +41024,10 @@ Usage
|
|
|
40364
41024
|
brandrail connect Sign in through the browser and set this project up: Brand Kit, AI rule files,
|
|
40365
41025
|
MCP server for Claude Code / Cursor / Copilot, and the UI kit
|
|
40366
41026
|
brandrail check [paths...] Scan files and print violations
|
|
41027
|
+
brandrail hook install Check staged lines before every commit (errors stop the commit); hook uninstall removes it
|
|
40367
41028
|
brandrail init Create brandrail.config.json and AI rule files
|
|
41029
|
+
brandrail migrate [paths...] What's left on the way to the design system: per library and per file
|
|
41030
|
+
brandrail report Write a shareable HTML handover report (paths and counts, no code)
|
|
40368
41031
|
brandrail mcp Run the MCP server on this machine (AI tools start it; code never leaves it)
|
|
40369
41032
|
brandrail whoami | logout Show or remove the saved sign-in
|
|
40370
41033
|
brandrail scan <url> Check a running page (localhost, staging, behind a login) in a real browser
|
|
@@ -40376,6 +41039,8 @@ Options for check
|
|
|
40376
41039
|
--format <fmt> pretty | json | markdown | github (default: pretty)
|
|
40377
41040
|
--fail-below <score> Exit 1 when the brand score is lower (default: off)
|
|
40378
41041
|
--changed [base] Only report lines changed vs base branch (default: origin/main)
|
|
41042
|
+
--staged Only the lines staged for the next commit (what the pre-commit hook runs)
|
|
41043
|
+
--strict Exit 1 on any error (warnings never fail)
|
|
40379
41044
|
--upload Send the result to Brandrail (needs \`brandrail connect\` or BRANDRAIL_KEY)
|
|
40380
41045
|
--no-color Plain output
|
|
40381
41046
|
|
|
@@ -40394,6 +41059,14 @@ Options for connect
|
|
|
40394
41059
|
--remote Point AI tools at the hosted MCP server instead of the local one
|
|
40395
41060
|
--framework, --tokens-css, --no-rules As for init
|
|
40396
41061
|
|
|
41062
|
+
Options for migrate
|
|
41063
|
+
--json Machine-readable output
|
|
41064
|
+
--top <n> How many files to list (default 15)
|
|
41065
|
+
--upload Store the result in the dashboard (scores and counts, never code)
|
|
41066
|
+
|
|
41067
|
+
Options for report
|
|
41068
|
+
--out <file> Where to write it (default brandrail-report.html)
|
|
41069
|
+
|
|
40397
41070
|
Options for mcp
|
|
40398
41071
|
--framework <name> Override the detected framework
|
|
40399
41072
|
--report Also send brand scores (never code) to the dashboard
|
|
@@ -40425,7 +41098,7 @@ function parseArgs(argv) {
|
|
|
40425
41098
|
const [k, v] = a.slice(2).split("=", 2);
|
|
40426
41099
|
const next = rest[i2 + 1];
|
|
40427
41100
|
if (v !== void 0) flags[k] = v;
|
|
40428
|
-
else if (next !== void 0 && !next.startsWith("-") && ["config", "format", "fail-below", "changed", "pages", "storage-state", "wait", "width", "tokens-css", "framework"].includes(k)) {
|
|
41101
|
+
else if (next !== void 0 && !next.startsWith("-") && ["config", "format", "fail-below", "changed", "pages", "storage-state", "wait", "width", "tokens-css", "framework", "top", "out"].includes(k)) {
|
|
40429
41102
|
flags[k] = next;
|
|
40430
41103
|
i2++;
|
|
40431
41104
|
} else flags[k] = true;
|
|
@@ -40435,24 +41108,41 @@ function parseArgs(argv) {
|
|
|
40435
41108
|
}
|
|
40436
41109
|
var log2 = (s = "") => process.stderr.write(`${s}
|
|
40437
41110
|
`);
|
|
41111
|
+
function readJson(path) {
|
|
41112
|
+
if (!existsSync7(path)) return void 0;
|
|
41113
|
+
try {
|
|
41114
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
41115
|
+
} catch {
|
|
41116
|
+
return void 0;
|
|
41117
|
+
}
|
|
41118
|
+
}
|
|
40438
41119
|
async function loadConfig(cwd, flags) {
|
|
40439
41120
|
const explicit = typeof flags.config === "string" ? resolve3(cwd, flags.config) : null;
|
|
40440
41121
|
const path = explicit ?? findConfig(cwd);
|
|
40441
|
-
if (path) return { config: readConfig(path), source:
|
|
41122
|
+
if (path) return { config: readConfig(path), source: relative3(cwd, path) || CONFIG_FILE2 };
|
|
40442
41123
|
const key = resolveKey();
|
|
40443
41124
|
if (key) {
|
|
40444
41125
|
const { config: config2 } = await fetchBrandKit(key);
|
|
40445
41126
|
return { config: config2, source: `${apiBase()} (published Brand Kit)` };
|
|
40446
41127
|
}
|
|
40447
|
-
throw new Error(`No ${
|
|
41128
|
+
throw new Error(`No ${CONFIG_FILE2} found. Run \`npx brandrail init\` first, or run \`npx brandrail connect\` to use your published Brand Kit.`);
|
|
40448
41129
|
}
|
|
40449
41130
|
async function cmdCheck(cwd, positional, flags) {
|
|
40450
41131
|
const { config: config2, source } = await loadConfig(cwd, flags);
|
|
40451
41132
|
const resolved = resolveConfig(config2);
|
|
40452
|
-
const root = dirname4(typeof flags.config === "string" ? resolve3(cwd, flags.config) : findConfig(cwd) ??
|
|
40453
|
-
const
|
|
41133
|
+
const root = dirname4(typeof flags.config === "string" ? resolve3(cwd, flags.config) : findConfig(cwd) ?? join7(cwd, CONFIG_FILE2));
|
|
41134
|
+
const collected = collectFiles(root, positional.map((p) => relative3(root, resolve3(cwd, p))), resolved.ignore);
|
|
40454
41135
|
let onlyLines;
|
|
40455
|
-
|
|
41136
|
+
let files = collected;
|
|
41137
|
+
if (flags.staged) {
|
|
41138
|
+
const staged = stagedLines(root);
|
|
41139
|
+
if (!staged) log2("warning: couldn't read the staged changes; checking everything");
|
|
41140
|
+
else {
|
|
41141
|
+
onlyLines = staged;
|
|
41142
|
+
files = collected.filter((f) => staged.has(f.path));
|
|
41143
|
+
if (!files.length) return 0;
|
|
41144
|
+
}
|
|
41145
|
+
} else if (flags.changed) {
|
|
40456
41146
|
const base = typeof flags.changed === "string" ? flags.changed : "origin/main";
|
|
40457
41147
|
onlyLines = changedLines(root, base) ?? void 0;
|
|
40458
41148
|
if (!onlyLines) log2(`warning: couldn't read git diff against ${base}; checking everything`);
|
|
@@ -40489,6 +41179,11 @@ async function cmdCheck(cwd, positional, flags) {
|
|
|
40489
41179
|
log2(`Uploaded \u2192 ${res.url ?? res.scanId}`);
|
|
40490
41180
|
}
|
|
40491
41181
|
}
|
|
41182
|
+
if (flags.strict && report.counts.error > 0) {
|
|
41183
|
+
log2(`
|
|
41184
|
+
${report.counts.error} error${report.counts.error === 1 ? "" : "s"} (--strict fails on any error; warnings don't block).`);
|
|
41185
|
+
return 1;
|
|
41186
|
+
}
|
|
40492
41187
|
if (threshold !== void 0 && report.score < threshold) {
|
|
40493
41188
|
log2(`
|
|
40494
41189
|
Brand score ${report.score} is below ${threshold}.`);
|
|
@@ -40496,54 +41191,54 @@ Brand score ${report.score} is below ${threshold}.`);
|
|
|
40496
41191
|
}
|
|
40497
41192
|
return 0;
|
|
40498
41193
|
}
|
|
41194
|
+
function cmdHook(cwd, positional, flags) {
|
|
41195
|
+
const action = positional[0] ?? "install";
|
|
41196
|
+
if (action === "uninstall" || action === "remove") {
|
|
41197
|
+
const removed = uninstallHook(cwd);
|
|
41198
|
+
log2(removed ? `\u2714 removed the Brandrail check from ${removed}` : "No Brandrail pre-commit hook found.");
|
|
41199
|
+
return 0;
|
|
41200
|
+
}
|
|
41201
|
+
if (action !== "install") throw new Error("Usage: brandrail hook install [--fail-below 80] | brandrail hook uninstall");
|
|
41202
|
+
const config2 = findConfig(cwd);
|
|
41203
|
+
if (!config2) throw new Error(`No ${CONFIG_FILE2} here or above. Run \`brandrail connect\` first.`);
|
|
41204
|
+
const failBelow = flags["fail-below"] !== void 0 ? Number(flags["fail-below"]) : void 0;
|
|
41205
|
+
const r = installHook(cwd, { root: dirname4(config2), failBelow });
|
|
41206
|
+
log2(`\u2714 ${r.kind === "husky" ? "added to Husky's" : "installed the"} pre-commit hook (${r.path})`);
|
|
41207
|
+
log2(" Each commit now checks the staged lines; errors stop the commit, warnings don't.");
|
|
41208
|
+
log2(" Skip it once with git commit --no-verify. Remove it with brandrail hook uninstall.");
|
|
41209
|
+
return 0;
|
|
41210
|
+
}
|
|
40499
41211
|
function tokensCssPath(root, config2, flags) {
|
|
40500
41212
|
if (typeof flags["tokens-css"] === "string") return flags["tokens-css"];
|
|
40501
41213
|
if (config2.tokensCss) return config2.tokensCss;
|
|
40502
|
-
return
|
|
41214
|
+
return existsSync7(join7(root, "src")) ? "src/brandrail.tokens.css" : "brandrail.tokens.css";
|
|
40503
41215
|
}
|
|
40504
41216
|
function writeTokensCss(root, rel, config2, org) {
|
|
40505
|
-
const abs =
|
|
40506
|
-
|
|
40507
|
-
|
|
40508
|
-
|
|
40509
|
-
if (usesKit) css += `
|
|
40510
|
-
/* @brandrail/kit follows the Brand Kit: colours, font, spacing, radius and type scale. */
|
|
40511
|
-
${kitBridgeCss(config2.tokens)}
|
|
40512
|
-
`;
|
|
40513
|
-
writeFileSync3(abs, css);
|
|
41217
|
+
const abs = join7(root, rel);
|
|
41218
|
+
mkdirSync4(dirname4(abs), { recursive: true });
|
|
41219
|
+
const { css, usesKit } = tokensCssContent(config2, org);
|
|
41220
|
+
writeFileSync4(abs, css);
|
|
40514
41221
|
return usesKit;
|
|
40515
41222
|
}
|
|
40516
41223
|
function writeRuleFiles(root, config2, org, tokensCss) {
|
|
40517
41224
|
const md = generateInstructions(config2, { org, tokensCss });
|
|
40518
41225
|
const written = [];
|
|
40519
|
-
const write2 = (rel, content) => {
|
|
40520
|
-
const abs = join6(root, rel);
|
|
40521
|
-
mkdirSync3(dirname4(abs), { recursive: true });
|
|
40522
|
-
const before = existsSync6(abs) ? readFileSync5(abs, "utf8") : "";
|
|
40523
|
-
writeFileSync3(abs, content(before));
|
|
40524
|
-
written.push(rel);
|
|
40525
|
-
};
|
|
40526
41226
|
for (const { file: file2, readBy } of RULE_FILES) {
|
|
40527
|
-
|
|
40528
|
-
|
|
40529
|
-
|
|
40530
|
-
|
|
40531
|
-
|
|
40532
|
-
|
|
40533
|
-
|
|
40534
|
-
|
|
40535
|
-
|
|
40536
|
-
}
|
|
40537
|
-
|
|
40538
|
-
|
|
40539
|
-
|
|
40540
|
-
|
|
40541
|
-
${
|
|
40542
|
-
`);
|
|
40543
|
-
} else {
|
|
40544
|
-
write2(file2, (before) => upsertSection(before, md));
|
|
40545
|
-
}
|
|
40546
|
-
written[written.length - 1] = `${file2} (${readBy})`;
|
|
41227
|
+
const abs = join7(root, file2);
|
|
41228
|
+
mkdirSync4(dirname4(abs), { recursive: true });
|
|
41229
|
+
const before = existsSync7(abs) ? readFileSync6(abs, "utf8") : "";
|
|
41230
|
+
writeFileSync4(abs, ruleFileContent(file2, md, before));
|
|
41231
|
+
written.push(`${file2} (${readBy})`);
|
|
41232
|
+
}
|
|
41233
|
+
const pkg = readJson(join7(root, "package.json"));
|
|
41234
|
+
for (const [file2, content, readBy] of [
|
|
41235
|
+
[MIGRATE_SKILL_FILE, migrationSkill(config2, { org, pkg }), "Claude Code skill: migrate to the design system"],
|
|
41236
|
+
[MIGRATE_AGENT_FILE, migrationAgent({ org }), "Claude Code agent: brandrail-migrator"]
|
|
41237
|
+
]) {
|
|
41238
|
+
const abs = join7(root, file2);
|
|
41239
|
+
mkdirSync4(dirname4(abs), { recursive: true });
|
|
41240
|
+
writeFileSync4(abs, content);
|
|
41241
|
+
written.push(`${file2} (${readBy})`);
|
|
40547
41242
|
}
|
|
40548
41243
|
return written;
|
|
40549
41244
|
}
|
|
@@ -40552,13 +41247,13 @@ function writeMcpConfigs(root, framework, mode = "remote") {
|
|
|
40552
41247
|
const configs = mode === "local" ? mcpLocalConfigs(framework) : mcpClientConfigs(url2);
|
|
40553
41248
|
for (const c of configs) {
|
|
40554
41249
|
if (!c.projectFile) continue;
|
|
40555
|
-
const abs =
|
|
40556
|
-
|
|
41250
|
+
const abs = join7(root, c.file);
|
|
41251
|
+
mkdirSync4(dirname4(abs), { recursive: true });
|
|
40557
41252
|
const ours = JSON.parse(c.content);
|
|
40558
41253
|
let merged = ours;
|
|
40559
|
-
if (
|
|
41254
|
+
if (existsSync7(abs)) {
|
|
40560
41255
|
try {
|
|
40561
|
-
const existing = JSON.parse(
|
|
41256
|
+
const existing = JSON.parse(readFileSync6(abs, "utf8"));
|
|
40562
41257
|
merged = { ...existing };
|
|
40563
41258
|
for (const [k, v] of Object.entries(ours)) {
|
|
40564
41259
|
if (k === "mcpServers" || k === "servers") {
|
|
@@ -40573,7 +41268,7 @@ function writeMcpConfigs(root, framework, mode = "remote") {
|
|
|
40573
41268
|
continue;
|
|
40574
41269
|
}
|
|
40575
41270
|
}
|
|
40576
|
-
|
|
41271
|
+
writeFileSync4(abs, `${JSON.stringify(merged, null, 2)}
|
|
40577
41272
|
`);
|
|
40578
41273
|
log2(`\u2714 added brandrail to ${c.file} (${c.name})`);
|
|
40579
41274
|
}
|
|
@@ -40594,16 +41289,16 @@ function projectFramework(cwd, config2, flags) {
|
|
|
40594
41289
|
return asked;
|
|
40595
41290
|
}
|
|
40596
41291
|
if (config2.framework && isFramework(config2.framework)) return config2.framework;
|
|
40597
|
-
const pkg =
|
|
40598
|
-
if (!
|
|
41292
|
+
const pkg = join7(cwd, "package.json");
|
|
41293
|
+
if (!existsSync7(pkg)) return detectFramework(null);
|
|
40599
41294
|
try {
|
|
40600
|
-
return detectFramework(JSON.parse(
|
|
41295
|
+
return detectFramework(JSON.parse(readFileSync6(pkg, "utf8")));
|
|
40601
41296
|
} catch {
|
|
40602
41297
|
return "react";
|
|
40603
41298
|
}
|
|
40604
41299
|
}
|
|
40605
41300
|
async function cmdInit(cwd, flags) {
|
|
40606
|
-
const target =
|
|
41301
|
+
const target = join7(cwd, CONFIG_FILE2);
|
|
40607
41302
|
let config2 = starterConfig;
|
|
40608
41303
|
let org;
|
|
40609
41304
|
if (flags["from-api"]) {
|
|
@@ -40613,23 +41308,23 @@ async function cmdInit(cwd, flags) {
|
|
|
40613
41308
|
config2 = { $schema: starterConfig.$schema, ...res.config };
|
|
40614
41309
|
org = res.org;
|
|
40615
41310
|
}
|
|
40616
|
-
if (
|
|
40617
|
-
log2(`${
|
|
41311
|
+
if (existsSync7(target) && !flags.force && !flags["from-api"]) {
|
|
41312
|
+
log2(`${CONFIG_FILE2} already exists (use --force to overwrite). Updating rule files from it.`);
|
|
40618
41313
|
config2 = readConfig(target);
|
|
40619
41314
|
org ??= config2.org;
|
|
40620
41315
|
} else {
|
|
40621
|
-
if (
|
|
41316
|
+
if (existsSync7(target)) {
|
|
40622
41317
|
const prev = readConfig(target);
|
|
40623
|
-
config2 =
|
|
41318
|
+
config2 = mergeProjectConfig(prev, config2);
|
|
40624
41319
|
}
|
|
40625
|
-
|
|
41320
|
+
writeFileSync4(target, `${JSON.stringify(config2, null, 2)}
|
|
40626
41321
|
`);
|
|
40627
|
-
log2(`\u2714 wrote ${
|
|
41322
|
+
log2(`\u2714 wrote ${CONFIG_FILE2}${org ? ` from ${org}'s Brand Kit` : " (starter tokens \u2014 edit them or use --from-api)"}`);
|
|
40628
41323
|
}
|
|
40629
41324
|
const framework = projectFramework(cwd, config2, flags);
|
|
40630
41325
|
if (config2.framework !== framework) {
|
|
40631
41326
|
config2 = { ...config2, framework };
|
|
40632
|
-
|
|
41327
|
+
writeFileSync4(target, `${JSON.stringify(config2, null, 2)}
|
|
40633
41328
|
`);
|
|
40634
41329
|
}
|
|
40635
41330
|
log2(`\u2714 framework: ${FRAMEWORK_LABEL[framework]}${flags.framework ? "" : " (detected; change with --framework)"}`);
|
|
@@ -40663,7 +41358,7 @@ async function cmdConnect(cwd, flags) {
|
|
|
40663
41358
|
\u2714 connected to ${org}. The key is saved in ${credentialsPath()} (only you can read it), never in this repo.`);
|
|
40664
41359
|
}
|
|
40665
41360
|
await cmdInit(cwd, { ...flags, "from-api": true, mcp: false });
|
|
40666
|
-
const config2 = readConfig(
|
|
41361
|
+
const config2 = readConfig(join7(cwd, CONFIG_FILE2));
|
|
40667
41362
|
const framework = projectFramework(cwd, config2, flags);
|
|
40668
41363
|
writeMcpConfigs(cwd, framework, flags.remote ? "remote" : "local");
|
|
40669
41364
|
const usesKit = (config2.components ?? []).some((c) => c.importPath === "@brandrail/kit");
|
|
@@ -40673,6 +41368,75 @@ Done. Restart your AI tool so it picks up the MCP server, then ask it to build s
|
|
|
40673
41368
|
if (framework !== "react" && usesKit) log2(`(${framework}: import "@brandrail/kit/elements" once at your app entry \u2014 the rule files tell the AI the rest.)`);
|
|
40674
41369
|
return 0;
|
|
40675
41370
|
}
|
|
41371
|
+
async function cmdMigrate(cwd, positional, flags) {
|
|
41372
|
+
const { config: config2, source } = await loadConfig(cwd, flags);
|
|
41373
|
+
const resolved = resolveConfig(config2);
|
|
41374
|
+
const root = dirname4(findConfig(cwd) ?? join7(cwd, CONFIG_FILE2));
|
|
41375
|
+
const files = collectFiles(root, positional.map((p) => relative3(root, resolve3(cwd, p))), resolved.ignore);
|
|
41376
|
+
const s = migrationStatus(files, resolved);
|
|
41377
|
+
if (flags.format === "json" || flags.json) {
|
|
41378
|
+
process.stdout.write(`${JSON.stringify({ progress: s.progress, filesScanned: s.filesScanned, filesClean: s.filesClean, score: s.report.score, libraries: s.libraries, byRule: s.byRule, files: s.files }, null, 2)}
|
|
41379
|
+
`);
|
|
41380
|
+
} else {
|
|
41381
|
+
const color = flags.color !== false && process.stdout.isTTY !== false && !process.env.NO_COLOR;
|
|
41382
|
+
const c = (code, t) => color ? `\x1B[${code}m${t}\x1B[0m` : t;
|
|
41383
|
+
const bar = (p) => `${"\u2588".repeat(Math.round(p / 5))}${"\u2591".repeat(20 - Math.round(p / 5))}`;
|
|
41384
|
+
const out = [];
|
|
41385
|
+
out.push(c("2", `Brand Kit: ${source}${config2.tokenVersion ? ` v${config2.tokenVersion}` : ""}`));
|
|
41386
|
+
out.push("");
|
|
41387
|
+
out.push(`${c("1", "Design system migration")} ${bar(s.progress)} ${s.progress}% (${s.filesClean} of ${s.filesScanned} UI files done \xB7 brand score ${s.report.score})`);
|
|
41388
|
+
const libs = Object.entries(s.libraries);
|
|
41389
|
+
if (libs.length) out.push("", c("1", "Other UI libraries still in use"), ...libs.map(([id, n]) => ` ${LIBRARY_LABEL[id].padEnd(22)} ${String(n).padStart(5)} usages`));
|
|
41390
|
+
const top = typeof flags.top === "string" ? Number(flags.top) || 15 : 15;
|
|
41391
|
+
if (s.files.length) {
|
|
41392
|
+
out.push("", c("1", `Next files (${Math.min(top, s.files.length)} of ${s.files.length} with work left)`));
|
|
41393
|
+
for (const f of s.files.slice(0, top)) {
|
|
41394
|
+
const what = Object.entries(f.byRule).sort((a, b) => b[1] - a[1]).map(([r, n]) => `${r} ${n}`).join(", ");
|
|
41395
|
+
out.push(` ${String(f.findings).padStart(4)} ${f.path} ${c("2", what)}`);
|
|
41396
|
+
}
|
|
41397
|
+
out.push("", `Migrate a batch with your AI tool, e.g. "migrate ${s.files[0].path} to the design system"${existsSync7(join7(root, MIGRATE_SKILL_FILE)) ? " (Claude Code uses the brandrail-migrate skill)" : " \u2014 run `npx brandrail connect` first to add the migration skill"}.`);
|
|
41398
|
+
} else out.push("", c("32", "\u2714 Every UI file follows the design system."));
|
|
41399
|
+
process.stdout.write(`${out.join("\n")}
|
|
41400
|
+
`);
|
|
41401
|
+
}
|
|
41402
|
+
if (flags.upload) {
|
|
41403
|
+
const key = resolveKey();
|
|
41404
|
+
if (!key) log2(`warning: --upload skipped. ${NO_KEY}`);
|
|
41405
|
+
else {
|
|
41406
|
+
const repo = repoInfo(root);
|
|
41407
|
+
const res = await uploadReport(key, {
|
|
41408
|
+
source: process.env.CI ? "ci" : "cli",
|
|
41409
|
+
target: { kind: "repo", name: repo.slug.split("/").pop() ?? repo.slug, location: repo.slug },
|
|
41410
|
+
ref: repo.branch ?? repo.sha?.slice(0, 7),
|
|
41411
|
+
title: `Migration status: ${s.progress}% of UI files done`,
|
|
41412
|
+
report: s.report
|
|
41413
|
+
});
|
|
41414
|
+
log2(`Uploaded \u2192 ${res.url ?? res.scanId}`);
|
|
41415
|
+
}
|
|
41416
|
+
}
|
|
41417
|
+
return 0;
|
|
41418
|
+
}
|
|
41419
|
+
async function cmdReport(cwd, flags) {
|
|
41420
|
+
const { config: config2 } = await loadConfig(cwd, flags);
|
|
41421
|
+
const resolved = resolveConfig(config2);
|
|
41422
|
+
const root = dirname4(findConfig(cwd) ?? join7(cwd, CONFIG_FILE2));
|
|
41423
|
+
const s = migrationStatus(collectFiles(root, [], resolved.ignore), resolved);
|
|
41424
|
+
const repo = repoInfo(root);
|
|
41425
|
+
const out = resolve3(cwd, typeof flags.out === "string" ? flags.out : "brandrail-report.html");
|
|
41426
|
+
writeFileSync4(
|
|
41427
|
+
out,
|
|
41428
|
+
formatHandoverHtml(s, {
|
|
41429
|
+
project: repo.slug,
|
|
41430
|
+
org: config2.org,
|
|
41431
|
+
branch: repo.branch,
|
|
41432
|
+
commit: repo.sha,
|
|
41433
|
+
kitVersion: config2.tokenVersion,
|
|
41434
|
+
framework: config2.framework ? FRAMEWORK_LABEL[config2.framework] : void 0
|
|
41435
|
+
})
|
|
41436
|
+
);
|
|
41437
|
+
log2(`\u2714 wrote ${relative3(cwd, out)}: ${s.progress}% of UI files on the design system, brand score ${s.report.score}. Open it in a browser, or print it to PDF.`);
|
|
41438
|
+
return 0;
|
|
41439
|
+
}
|
|
40676
41440
|
function cmdWhoami() {
|
|
40677
41441
|
if (process.env.BRANDRAIL_KEY) {
|
|
40678
41442
|
log2(`Using BRANDRAIL_KEY from the environment (${process.env.BRANDRAIL_KEY.slice(0, 12)}\u2026) against ${apiBase()}.`);
|
|
@@ -40750,12 +41514,12 @@ function readTokenInputs(cwd, inputs) {
|
|
|
40750
41514
|
const add = (name, text2) => files.push({ name, data: JSON.parse(text2) });
|
|
40751
41515
|
const walk = (abs, rel) => {
|
|
40752
41516
|
if (statSync2(abs).isDirectory()) {
|
|
40753
|
-
for (const e of readdirSync2(abs)) if (!e.startsWith(".")) walk(
|
|
41517
|
+
for (const e of readdirSync2(abs)) if (!e.startsWith(".")) walk(join7(abs, e), rel ? `${rel}/${e}` : e);
|
|
40754
41518
|
} else if (/\.zip$/i.test(abs)) {
|
|
40755
|
-
const entries = unzipSync(new Uint8Array(
|
|
41519
|
+
const entries = unzipSync(new Uint8Array(readFileSync6(abs)), { filter: (e) => /\.json$/i.test(e.name) && !/(^|\/)(__MACOSX|\.)/.test(e.name) });
|
|
40756
41520
|
for (const [name, bytes] of Object.entries(entries)) add(name, strFromU8(bytes));
|
|
40757
41521
|
} else if (/\.json$/i.test(abs)) {
|
|
40758
|
-
add(rel || basename2(abs),
|
|
41522
|
+
add(rel || basename2(abs), readFileSync6(abs, "utf8"));
|
|
40759
41523
|
}
|
|
40760
41524
|
};
|
|
40761
41525
|
for (const input2 of inputs) {
|
|
@@ -40770,16 +41534,16 @@ function cmdImport(cwd, positional, flags) {
|
|
|
40770
41534
|
if (!files.length) throw new Error("No .json token files found.");
|
|
40771
41535
|
const res = importTokenFiles(files, { includePrimitives: !!flags.primitives });
|
|
40772
41536
|
const file2 = files.length === 1 ? positional[0] : `${files.length} files`;
|
|
40773
|
-
const path = (typeof flags.config === "string" ? resolve3(cwd, flags.config) : findConfig(cwd)) ??
|
|
40774
|
-
const config2 =
|
|
41537
|
+
const path = (typeof flags.config === "string" ? resolve3(cwd, flags.config) : findConfig(cwd)) ?? join7(cwd, CONFIG_FILE2);
|
|
41538
|
+
const config2 = existsSync7(path) ? readConfig(path) : { ...starterConfig, tokens: {} };
|
|
40775
41539
|
const next = flags.replace ? res.tokens : mergeTokens(config2.tokens, res.tokens);
|
|
40776
41540
|
const d = diffTokens(config2.tokens, next);
|
|
40777
|
-
|
|
41541
|
+
writeFileSync4(path, `${JSON.stringify({ ...config2, tokens: next }, null, 2)}
|
|
40778
41542
|
`);
|
|
40779
41543
|
log2(`\u2714 ${res.count} tokens from ${file2} (${res.format})${res.primitives ? `, ${res.primitives} palette primitives left out (--primitives to keep)` : ""}`);
|
|
40780
41544
|
for (const n of res.notes) log2(` \xB7 ${n}`);
|
|
40781
41545
|
for (const i2 of auditBrandKit(next)) log2(` ${i2.severity === "error" ? "\u2716" : "!"} ${i2.message}`);
|
|
40782
|
-
log2(`\u2714 ${
|
|
41546
|
+
log2(`\u2714 ${relative3(cwd, path) || CONFIG_FILE2}: ${d.changed.length} changed, ${d.added.length} added, ${d.removed.length} removed`);
|
|
40783
41547
|
log2("\nNext: npx brandrail init (to refresh AI rule files), then npx brandrail check");
|
|
40784
41548
|
return 0;
|
|
40785
41549
|
}
|
|
@@ -40811,6 +41575,12 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
40811
41575
|
return cmdConnect(cwd, flags);
|
|
40812
41576
|
case "mcp":
|
|
40813
41577
|
return runLocalMcp(cwd, { framework: typeof flags.framework === "string" ? flags.framework : void 0, report: flags.report === true });
|
|
41578
|
+
case "migrate":
|
|
41579
|
+
return cmdMigrate(cwd, positional, flags);
|
|
41580
|
+
case "report":
|
|
41581
|
+
return cmdReport(cwd, flags);
|
|
41582
|
+
case "hook":
|
|
41583
|
+
return cmdHook(cwd, positional, flags);
|
|
40814
41584
|
case "whoami":
|
|
40815
41585
|
return cmdWhoami();
|
|
40816
41586
|
case "logout":
|