design-system-enforcer 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +728 -117
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +728 -117
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +347 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +347 -56
- package/dist/index.js.map +1 -1
- package/package.json +71 -71
package/dist/cli.cjs
CHANGED
|
@@ -24,14 +24,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
|
-
var
|
|
28
|
-
var
|
|
27
|
+
var import_node_fs8 = __toESM(require("fs"), 1);
|
|
28
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
29
29
|
var import_commander = require("commander");
|
|
30
|
-
var
|
|
30
|
+
var import_picocolors3 = __toESM(require("picocolors"), 1);
|
|
31
31
|
|
|
32
32
|
// src/config/loader.ts
|
|
33
|
-
var
|
|
34
|
-
var
|
|
33
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
34
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
35
35
|
var import_node_url = require("url");
|
|
36
36
|
|
|
37
37
|
// src/config/presets/berserk.ts
|
|
@@ -120,6 +120,9 @@ var berserkPreset = {
|
|
|
120
120
|
disallowRawHtml: {
|
|
121
121
|
button: { replaceWith: "Button", importFrom: "@/components/ui/button", checkRawElement: true },
|
|
122
122
|
select: { replaceWith: "Dropdown", importFrom: "@/components/ui/dropdown", checkRawElement: true },
|
|
123
|
+
textarea: { replaceWith: "Textarea", importFrom: "@/components/ui/textarea", checkRawElement: true },
|
|
124
|
+
progress: { replaceWith: "ProgressBar", importFrom: "@/components/ui/progress", checkRawElement: true },
|
|
125
|
+
dialog: { replaceWith: "Modal or Dialog", importFrom: "@/components/ui/modal", checkRawElement: true },
|
|
123
126
|
table: { replaceWith: "Table", importFrom: "@/components/ui/table", checkRawElement: true },
|
|
124
127
|
thead: { replaceWith: "TableHeader", importFrom: "@/components/ui/table", checkRawElement: true },
|
|
125
128
|
tbody: { replaceWith: "TableBody", importFrom: "@/components/ui/table", checkRawElement: true },
|
|
@@ -171,7 +174,8 @@ var berserkPreset = {
|
|
|
171
174
|
"table-no-horizontal-stacking": "error",
|
|
172
175
|
"table-windowed-pagination": "error",
|
|
173
176
|
"tabs-component-required": "error",
|
|
174
|
-
"component-inventory-active": "error"
|
|
177
|
+
"component-inventory-active": "error",
|
|
178
|
+
"no-wrapping-compact-elements": "error"
|
|
175
179
|
}
|
|
176
180
|
};
|
|
177
181
|
|
|
@@ -208,7 +212,10 @@ var defaultPreset = {
|
|
|
208
212
|
components: {
|
|
209
213
|
disallowRawHtml: {
|
|
210
214
|
button: { replaceWith: "Button", importFrom: "@/components/ui/button", checkRawElement: true },
|
|
211
|
-
select: { replaceWith: "Dropdown", importFrom: "@/components/ui/dropdown", checkRawElement: true }
|
|
215
|
+
select: { replaceWith: "Dropdown", importFrom: "@/components/ui/dropdown", checkRawElement: true },
|
|
216
|
+
textarea: { replaceWith: "Textarea", importFrom: "@/components/ui/textarea", checkRawElement: true },
|
|
217
|
+
progress: { replaceWith: "ProgressBar", importFrom: "@/components/ui/progress", checkRawElement: true },
|
|
218
|
+
dialog: { replaceWith: "Modal or Dialog", importFrom: "@/components/ui/modal", checkRawElement: true }
|
|
212
219
|
},
|
|
213
220
|
disallowPseudoChips: true
|
|
214
221
|
},
|
|
@@ -219,7 +226,8 @@ var defaultPreset = {
|
|
|
219
226
|
"no-pseudo-chips": "warn",
|
|
220
227
|
"no-chip-colored-buttons": "error",
|
|
221
228
|
"no-text-arrows": "error",
|
|
222
|
-
"no-backdrop-blur": "warn"
|
|
229
|
+
"no-backdrop-blur": "warn",
|
|
230
|
+
"no-wrapping-compact-elements": "warn"
|
|
223
231
|
}
|
|
224
232
|
};
|
|
225
233
|
|
|
@@ -283,6 +291,221 @@ var pythonPreset = {
|
|
|
283
291
|
}
|
|
284
292
|
};
|
|
285
293
|
|
|
294
|
+
// src/detector/index.ts
|
|
295
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
296
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
297
|
+
|
|
298
|
+
// src/detector/ignores.ts
|
|
299
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
300
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
301
|
+
var DEFAULT_IGNORES = [
|
|
302
|
+
"**/node_modules/**",
|
|
303
|
+
"**/.next/**",
|
|
304
|
+
"**/dist/**",
|
|
305
|
+
"**/build/**",
|
|
306
|
+
"**/out/**",
|
|
307
|
+
"**/.turbo/**",
|
|
308
|
+
"**/.git/**",
|
|
309
|
+
"**/coverage/**",
|
|
310
|
+
"**/.venv/**",
|
|
311
|
+
"**/venv/**",
|
|
312
|
+
"**/env/**",
|
|
313
|
+
"**/__pycache__/**",
|
|
314
|
+
"**/.pytest_cache/**",
|
|
315
|
+
"**/*.pyc",
|
|
316
|
+
// Non-UI code
|
|
317
|
+
"**/api/**",
|
|
318
|
+
"**/*.test.*",
|
|
319
|
+
"**/*.spec.*",
|
|
320
|
+
"**/__tests__/**",
|
|
321
|
+
"**/*.stories.*",
|
|
322
|
+
"**/generated/**"
|
|
323
|
+
];
|
|
324
|
+
function getSmartIgnores(cwd) {
|
|
325
|
+
const result = new Set(DEFAULT_IGNORES);
|
|
326
|
+
const gitignorePath = import_node_path.default.resolve(cwd, ".gitignore");
|
|
327
|
+
if (import_node_fs.default.existsSync(gitignorePath)) {
|
|
328
|
+
try {
|
|
329
|
+
const content = import_node_fs.default.readFileSync(gitignorePath, "utf-8");
|
|
330
|
+
const lines = content.split(/\r?\n/);
|
|
331
|
+
for (let line of lines) {
|
|
332
|
+
line = line.trim();
|
|
333
|
+
if (!line || line.startsWith("#")) continue;
|
|
334
|
+
if (line.startsWith("/")) {
|
|
335
|
+
line = line.substring(1);
|
|
336
|
+
}
|
|
337
|
+
if (line.endsWith("/")) {
|
|
338
|
+
line = line.slice(0, -1);
|
|
339
|
+
}
|
|
340
|
+
if (line) {
|
|
341
|
+
result.add(`**/${line}/**`);
|
|
342
|
+
result.add(`**/${line}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch {
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return Array.from(result);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/detector/index.ts
|
|
352
|
+
function safeReadJson(filePath) {
|
|
353
|
+
try {
|
|
354
|
+
if (import_node_fs2.default.existsSync(filePath)) {
|
|
355
|
+
const raw = import_node_fs2.default.readFileSync(filePath, "utf-8");
|
|
356
|
+
return JSON.parse(raw);
|
|
357
|
+
}
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
function detectProject(cwd = process.cwd()) {
|
|
363
|
+
const pkg = safeReadJson(import_node_path2.default.resolve(cwd, "package.json"));
|
|
364
|
+
const deps = { ...pkg?.dependencies || {}, ...pkg?.devDependencies || {} };
|
|
365
|
+
const hasRequirements = import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "requirements.txt"));
|
|
366
|
+
const hasPyproject = import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "pyproject.toml"));
|
|
367
|
+
const hasPython = hasRequirements || hasPyproject || import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "setup.py"));
|
|
368
|
+
let isStreamlit = false;
|
|
369
|
+
if (hasPython) {
|
|
370
|
+
if (hasRequirements) {
|
|
371
|
+
try {
|
|
372
|
+
const reqContent = import_node_fs2.default.readFileSync(import_node_path2.default.resolve(cwd, "requirements.txt"), "utf-8");
|
|
373
|
+
if (reqContent.toLowerCase().includes("streamlit")) {
|
|
374
|
+
isStreamlit = true;
|
|
375
|
+
}
|
|
376
|
+
} catch {
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const isPnpmMonorepo = import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "pnpm-workspace.yaml"));
|
|
381
|
+
const isTurbo = import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "turbo.json"));
|
|
382
|
+
let monorepoWorkspaces = [];
|
|
383
|
+
if (Array.isArray(pkg?.workspaces)) {
|
|
384
|
+
monorepoWorkspaces = pkg.workspaces;
|
|
385
|
+
} else if (pkg?.workspaces?.packages) {
|
|
386
|
+
monorepoWorkspaces = pkg.workspaces.packages;
|
|
387
|
+
}
|
|
388
|
+
const isMonorepo = isPnpmMonorepo || isTurbo || monorepoWorkspaces.length > 0;
|
|
389
|
+
let framework = "unknown";
|
|
390
|
+
if (deps["next"]) {
|
|
391
|
+
framework = "nextjs";
|
|
392
|
+
} else if (deps["vite"]) {
|
|
393
|
+
framework = "vite";
|
|
394
|
+
} else if (deps["@remix-run/react"]) {
|
|
395
|
+
framework = "remix";
|
|
396
|
+
} else if (deps["astro"]) {
|
|
397
|
+
framework = "astro";
|
|
398
|
+
} else if (deps["react-scripts"]) {
|
|
399
|
+
framework = "react-cra";
|
|
400
|
+
} else if (isStreamlit) {
|
|
401
|
+
framework = "streamlit";
|
|
402
|
+
} else if (hasPython) {
|
|
403
|
+
framework = "python-general";
|
|
404
|
+
}
|
|
405
|
+
const libraries = [];
|
|
406
|
+
const hasShadcnConfig = import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "components.json"));
|
|
407
|
+
if (hasShadcnConfig || deps["@radix-ui/react-slot"]) {
|
|
408
|
+
libraries.push("shadcn");
|
|
409
|
+
}
|
|
410
|
+
if (Object.keys(deps).some((d) => d.startsWith("@radix-ui/"))) {
|
|
411
|
+
if (!libraries.includes("shadcn")) libraries.push("radix");
|
|
412
|
+
}
|
|
413
|
+
if (deps["tailwindcss"]) {
|
|
414
|
+
libraries.push("tailwind");
|
|
415
|
+
}
|
|
416
|
+
if (deps["lucide-react"] || deps["lucide"]) {
|
|
417
|
+
libraries.push("lucide");
|
|
418
|
+
}
|
|
419
|
+
if (deps["@mui/material"] || deps["@material-ui/core"]) {
|
|
420
|
+
libraries.push("mui");
|
|
421
|
+
}
|
|
422
|
+
if (deps["@chakra-ui/react"]) {
|
|
423
|
+
libraries.push("chakra");
|
|
424
|
+
}
|
|
425
|
+
const hasTailwind = Boolean(deps["tailwindcss"] || import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "tailwind.config.js")) || import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "tailwind.config.ts")));
|
|
426
|
+
let tailwindVersion;
|
|
427
|
+
if (hasTailwind) {
|
|
428
|
+
const rawVersion = deps["tailwindcss"] || "";
|
|
429
|
+
if (rawVersion.includes("^4") || rawVersion.includes("~4") || rawVersion.startsWith("4")) {
|
|
430
|
+
tailwindVersion = "v4";
|
|
431
|
+
} else {
|
|
432
|
+
tailwindVersion = "v3";
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const hasTypeScript = Boolean(deps["typescript"] || import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "tsconfig.json")));
|
|
436
|
+
const suggestedIncludes = [];
|
|
437
|
+
const extGroup = hasTypeScript ? "{tsx,jsx,ts,js}" : "{jsx,js}";
|
|
438
|
+
const candidateDirs = [
|
|
439
|
+
"frontend/src",
|
|
440
|
+
"frontend",
|
|
441
|
+
"apps/web/src",
|
|
442
|
+
"apps/web",
|
|
443
|
+
"client/src",
|
|
444
|
+
"client",
|
|
445
|
+
"src",
|
|
446
|
+
"app",
|
|
447
|
+
"pages",
|
|
448
|
+
"components"
|
|
449
|
+
];
|
|
450
|
+
let foundUiDir = false;
|
|
451
|
+
for (const dir of candidateDirs) {
|
|
452
|
+
if (import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, dir))) {
|
|
453
|
+
if (dir === "frontend" && import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "frontend/src"))) continue;
|
|
454
|
+
if (dir === "client" && import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "client/src"))) continue;
|
|
455
|
+
if (dir === "apps/web" && import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "apps/web/src"))) continue;
|
|
456
|
+
suggestedIncludes.push(`${dir}/**/*.${extGroup}`);
|
|
457
|
+
foundUiDir = true;
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (hasPython) {
|
|
462
|
+
suggestedIncludes.push("**/*.py");
|
|
463
|
+
} else if (!foundUiDir) {
|
|
464
|
+
suggestedIncludes.push(`**/*.${extGroup}`);
|
|
465
|
+
}
|
|
466
|
+
const suggestedIgnores = getSmartIgnores(cwd);
|
|
467
|
+
if (hasShadcnConfig || libraries.includes("shadcn")) {
|
|
468
|
+
suggestedIgnores.push("**/components/ui/**");
|
|
469
|
+
}
|
|
470
|
+
const tokenCandidates = [
|
|
471
|
+
"frontend/src/lib/tokens.ts",
|
|
472
|
+
"frontend/src/tokens.ts",
|
|
473
|
+
"src/lib/tokens.ts",
|
|
474
|
+
"src/tokens.ts",
|
|
475
|
+
"src/design/tokens.ts",
|
|
476
|
+
"styles/tokens.css",
|
|
477
|
+
"src/app/globals.css",
|
|
478
|
+
"frontend/src/index.css",
|
|
479
|
+
"src/index.css"
|
|
480
|
+
];
|
|
481
|
+
let tokensFile;
|
|
482
|
+
for (const candidate of tokenCandidates) {
|
|
483
|
+
if (import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, candidate))) {
|
|
484
|
+
tokensFile = candidate;
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
let preset = "berserk";
|
|
489
|
+
if (framework === "streamlit" || framework === "python-general") {
|
|
490
|
+
preset = "python";
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
root: cwd,
|
|
494
|
+
isMonorepo,
|
|
495
|
+
monorepoWorkspaces,
|
|
496
|
+
framework,
|
|
497
|
+
libraries,
|
|
498
|
+
hasTailwind,
|
|
499
|
+
tailwindVersion,
|
|
500
|
+
hasTypeScript,
|
|
501
|
+
hasPython,
|
|
502
|
+
suggestedIncludes,
|
|
503
|
+
suggestedIgnores,
|
|
504
|
+
tokensFile,
|
|
505
|
+
preset
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
286
509
|
// src/config/loader.ts
|
|
287
510
|
var CONFIG_FILES = [
|
|
288
511
|
"dse.config.js",
|
|
@@ -294,17 +517,18 @@ var CONFIG_FILES = [
|
|
|
294
517
|
".dserc"
|
|
295
518
|
];
|
|
296
519
|
async function loadConfig(cwd = process.cwd(), customPath) {
|
|
520
|
+
const detected = detectProject(cwd);
|
|
297
521
|
let userConfig = {};
|
|
298
522
|
let foundConfigPath = null;
|
|
299
523
|
if (customPath) {
|
|
300
|
-
const resolved =
|
|
301
|
-
if (
|
|
524
|
+
const resolved = import_node_path3.default.resolve(cwd, customPath);
|
|
525
|
+
if (import_node_fs3.default.existsSync(resolved)) {
|
|
302
526
|
foundConfigPath = resolved;
|
|
303
527
|
}
|
|
304
528
|
} else {
|
|
305
529
|
for (const filename of CONFIG_FILES) {
|
|
306
|
-
const candidate =
|
|
307
|
-
if (
|
|
530
|
+
const candidate = import_node_path3.default.resolve(cwd, filename);
|
|
531
|
+
if (import_node_fs3.default.existsSync(candidate)) {
|
|
308
532
|
foundConfigPath = candidate;
|
|
309
533
|
break;
|
|
310
534
|
}
|
|
@@ -312,7 +536,7 @@ async function loadConfig(cwd = process.cwd(), customPath) {
|
|
|
312
536
|
}
|
|
313
537
|
if (foundConfigPath) {
|
|
314
538
|
if (foundConfigPath.endsWith(".json") || foundConfigPath.endsWith(".dserc")) {
|
|
315
|
-
const content =
|
|
539
|
+
const content = import_node_fs3.default.readFileSync(foundConfigPath, "utf-8");
|
|
316
540
|
try {
|
|
317
541
|
userConfig = JSON.parse(content);
|
|
318
542
|
} catch (err) {
|
|
@@ -325,7 +549,7 @@ async function loadConfig(cwd = process.cwd(), customPath) {
|
|
|
325
549
|
userConfig = mod.default || mod;
|
|
326
550
|
} catch {
|
|
327
551
|
try {
|
|
328
|
-
const content =
|
|
552
|
+
const content = import_node_fs3.default.readFileSync(foundConfigPath, "utf-8");
|
|
329
553
|
const cjsReq = createCjsEvaluator(foundConfigPath, content);
|
|
330
554
|
userConfig = cjsReq;
|
|
331
555
|
} catch (innerErr) {
|
|
@@ -334,18 +558,19 @@ async function loadConfig(cwd = process.cwd(), customPath) {
|
|
|
334
558
|
}
|
|
335
559
|
}
|
|
336
560
|
}
|
|
337
|
-
let presetName = userConfig.preset;
|
|
338
|
-
if (!presetName) {
|
|
339
|
-
const isPython = import_node_fs.default.existsSync(import_node_path.default.resolve(cwd, "requirements.txt")) || import_node_fs.default.existsSync(import_node_path.default.resolve(cwd, "pyproject.toml")) || import_node_fs.default.existsSync(import_node_path.default.resolve(cwd, ".streamlit")) || import_node_fs.default.existsSync(import_node_path.default.resolve(cwd, "manage.py"));
|
|
340
|
-
presetName = isPython ? "python" : "berserk";
|
|
341
|
-
}
|
|
561
|
+
let presetName = userConfig.preset || detected.preset;
|
|
342
562
|
let basePreset = berserkPreset;
|
|
343
563
|
if (presetName === "python") {
|
|
344
564
|
basePreset = pythonPreset;
|
|
345
565
|
} else if (presetName === "recommended") {
|
|
346
566
|
basePreset = defaultPreset;
|
|
347
567
|
}
|
|
348
|
-
|
|
568
|
+
const autonomousDefaults = {
|
|
569
|
+
include: detected.suggestedIncludes,
|
|
570
|
+
exclude: detected.suggestedIgnores
|
|
571
|
+
};
|
|
572
|
+
const withAutonomous = mergeConfigs(basePreset, autonomousDefaults);
|
|
573
|
+
return mergeConfigs(withAutonomous, userConfig);
|
|
349
574
|
}
|
|
350
575
|
function createCjsEvaluator(filePath, content) {
|
|
351
576
|
if (content.includes("export default")) {
|
|
@@ -395,8 +620,8 @@ function mergeConfigs(base, overrides) {
|
|
|
395
620
|
}
|
|
396
621
|
|
|
397
622
|
// src/rules/runner.ts
|
|
398
|
-
var
|
|
399
|
-
var
|
|
623
|
+
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
624
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
400
625
|
var import_fast_glob = __toESM(require("fast-glob"), 1);
|
|
401
626
|
|
|
402
627
|
// src/parser/ast.ts
|
|
@@ -809,16 +1034,52 @@ function checkRawHtmlElements(tagName, attributes, pos, filePath, addViolation,
|
|
|
809
1034
|
pos.line,
|
|
810
1035
|
pos.col,
|
|
811
1036
|
"no-raw-html-elements",
|
|
812
|
-
`Raw browser default '<input type="checkbox">' detected. Use standard design system '<Checkbox>' or '<Toggle>'.`,
|
|
1037
|
+
`Raw browser default '<input type="checkbox">' detected. Never use unstyled browser checkboxes. Use standard design system '<Checkbox>' or '<Toggle>'.`,
|
|
813
1038
|
lineSnippet
|
|
814
1039
|
);
|
|
815
|
-
} else if (
|
|
1040
|
+
} else if (typeVal === "radio") {
|
|
816
1041
|
addViolation(
|
|
817
1042
|
filePath,
|
|
818
1043
|
pos.line,
|
|
819
1044
|
pos.col,
|
|
820
1045
|
"no-raw-html-elements",
|
|
821
|
-
`Raw '<input type="
|
|
1046
|
+
`Raw browser default '<input type="radio">' detected. Never use unstyled browser radio buttons. Use standard design system '<RadioGroup>' or styled toggle.`,
|
|
1047
|
+
lineSnippet
|
|
1048
|
+
);
|
|
1049
|
+
} else if (typeVal === "range") {
|
|
1050
|
+
addViolation(
|
|
1051
|
+
filePath,
|
|
1052
|
+
pos.line,
|
|
1053
|
+
pos.col,
|
|
1054
|
+
"no-raw-html-elements",
|
|
1055
|
+
`Raw browser default '<input type="range">' detected. Never use unstyled browser sliders. Use standard design system '<Slider>'.`,
|
|
1056
|
+
lineSnippet
|
|
1057
|
+
);
|
|
1058
|
+
} else if (typeVal === "file") {
|
|
1059
|
+
addViolation(
|
|
1060
|
+
filePath,
|
|
1061
|
+
pos.line,
|
|
1062
|
+
pos.col,
|
|
1063
|
+
"no-raw-html-elements",
|
|
1064
|
+
`Raw browser default '<input type="file">' detected. Never display native unstyled file picker inputs. Use a custom styled dropzone or trigger.`,
|
|
1065
|
+
lineSnippet
|
|
1066
|
+
);
|
|
1067
|
+
} else if (typeVal === "color") {
|
|
1068
|
+
addViolation(
|
|
1069
|
+
filePath,
|
|
1070
|
+
pos.line,
|
|
1071
|
+
pos.col,
|
|
1072
|
+
"no-raw-html-elements",
|
|
1073
|
+
`Raw browser default '<input type="color">' detected. Never use native browser color pickers. Use a custom color picker component.`,
|
|
1074
|
+
lineSnippet
|
|
1075
|
+
);
|
|
1076
|
+
} else if (["text", "email", "search", "password", "number", "tel", "url"].includes(typeVal)) {
|
|
1077
|
+
addViolation(
|
|
1078
|
+
filePath,
|
|
1079
|
+
pos.line,
|
|
1080
|
+
pos.col,
|
|
1081
|
+
"no-raw-html-elements",
|
|
1082
|
+
`Raw browser default '<input type="${typeVal}">' element detected. Use standard design system '<Input>' component.`,
|
|
822
1083
|
lineSnippet
|
|
823
1084
|
);
|
|
824
1085
|
}
|
|
@@ -1076,6 +1337,9 @@ function checkPseudoChips(tagName, attributes, pos, filePath, addViolation, line
|
|
|
1076
1337
|
function checkElementClasses(node, tagName, attributes, pos, filePath, addViolation, sourceFile, config) {
|
|
1077
1338
|
if (!attributes.className) return;
|
|
1078
1339
|
const isButton = ["Button", "IconButton", "ButtonyIcon", "FloatingActionButton"].includes(tagName);
|
|
1340
|
+
const isChip = ["Chip", "Badge", "Tag", "StatusBadge"].includes(tagName);
|
|
1341
|
+
const isTab = ["Tab", "TabTrigger", "TabsTrigger"].includes(tagName);
|
|
1342
|
+
const isCompactElement = isButton || isChip || isTab;
|
|
1079
1343
|
const isCheckbox = tagName === "Checkbox" || attributes.type?.initializer?.getText(sourceFile)?.includes("checkbox") || attributes.role?.initializer?.getText(sourceFile)?.includes("checkbox") || Boolean(attributes["aria-checked"]) || attributes.className && /\b(?:size-5|w-5\s+h-5)\b/.test(attributes.className.rawText);
|
|
1080
1344
|
const lineSnippet = sourceFile.getFullText().split("\n")[pos.line - 1] || "";
|
|
1081
1345
|
const classStrings = extractClassNames(attributes.className.initializer, sourceFile);
|
|
@@ -1115,8 +1379,33 @@ function checkElementClasses(node, tagName, attributes, pos, filePath, addViolat
|
|
|
1115
1379
|
);
|
|
1116
1380
|
}
|
|
1117
1381
|
}
|
|
1382
|
+
if (isCompactElement && config.rules?.["no-wrapping-compact-elements"] !== "off") {
|
|
1383
|
+
if (dec.core === "flex-wrap" || dec.core === "whitespace-normal" || dec.core === "whitespace-pre-wrap") {
|
|
1384
|
+
addViolation(
|
|
1385
|
+
filePath,
|
|
1386
|
+
nodePos.line,
|
|
1387
|
+
nodePos.col,
|
|
1388
|
+
"no-wrapping-compact-elements",
|
|
1389
|
+
`Disallowed text wrapping class '${dec.core}' on <${tagName}>. Compact interactive elements (buttons, chips, badges, tabs) must never wrap text across multiple lines.`,
|
|
1390
|
+
lineSnippet
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1118
1394
|
});
|
|
1119
1395
|
});
|
|
1396
|
+
if (tagName === "TableHead" && config.rules?.["no-wrapping-compact-elements"] !== "off") {
|
|
1397
|
+
const rawClass = attributes.className?.rawText || "";
|
|
1398
|
+
if (/\b(?:flex-wrap|whitespace-normal|whitespace-pre-wrap)\b/.test(rawClass)) {
|
|
1399
|
+
addViolation(
|
|
1400
|
+
filePath,
|
|
1401
|
+
pos.line,
|
|
1402
|
+
pos.col,
|
|
1403
|
+
"no-wrapping-compact-elements",
|
|
1404
|
+
`Disallowed text wrapping on <TableHead>. Column headers must remain single-line ('whitespace-nowrap') to preserve table column alignment.`,
|
|
1405
|
+
lineSnippet
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1120
1409
|
}
|
|
1121
1410
|
|
|
1122
1411
|
// src/rules/regex-rules.ts
|
|
@@ -1337,8 +1626,8 @@ function runPythonRules(content, filePath, addViolation, config) {
|
|
|
1337
1626
|
|
|
1338
1627
|
// src/rules/runner.ts
|
|
1339
1628
|
async function runDseScan(config, cwd = process.cwd()) {
|
|
1340
|
-
let targetDir =
|
|
1341
|
-
if (!
|
|
1629
|
+
let targetDir = import_node_path4.default.resolve(cwd, config.targetDir || "src");
|
|
1630
|
+
if (!import_node_fs4.default.existsSync(targetDir)) {
|
|
1342
1631
|
targetDir = cwd;
|
|
1343
1632
|
}
|
|
1344
1633
|
const includePatterns = config.include || ["**/*.{tsx,jsx,ts,js}"];
|
|
@@ -1370,7 +1659,7 @@ async function runDseScan(config, cwd = process.cwd()) {
|
|
|
1370
1659
|
const violations = [];
|
|
1371
1660
|
const seenViolationKeys = /* @__PURE__ */ new Set();
|
|
1372
1661
|
const addViolation = (file, line, col, rule, message, snippet) => {
|
|
1373
|
-
const normFile =
|
|
1662
|
+
const normFile = import_node_path4.default.relative(cwd, file).replace(/\\/g, "/");
|
|
1374
1663
|
const key = `${normFile}:${line}:${rule}`;
|
|
1375
1664
|
if (seenViolationKeys.has(key)) return;
|
|
1376
1665
|
seenViolationKeys.add(key);
|
|
@@ -1388,7 +1677,7 @@ async function runDseScan(config, cwd = process.cwd()) {
|
|
|
1388
1677
|
};
|
|
1389
1678
|
let scannedJsxFiles = false;
|
|
1390
1679
|
for (const filePath of files) {
|
|
1391
|
-
const content =
|
|
1680
|
+
const content = import_node_fs4.default.readFileSync(filePath, "utf-8");
|
|
1392
1681
|
const isJsx = filePath.endsWith(".tsx") || filePath.endsWith(".jsx") || filePath.endsWith(".ts") || filePath.endsWith(".js");
|
|
1393
1682
|
const isPythonOrTemplate = filePath.endsWith(".py") || filePath.endsWith(".html") || filePath.endsWith(".jinja") || filePath.endsWith(".jinja2") || filePath.endsWith(".css");
|
|
1394
1683
|
if (isJsx) {
|
|
@@ -1509,8 +1798,8 @@ function formatJson(result) {
|
|
|
1509
1798
|
}
|
|
1510
1799
|
|
|
1511
1800
|
// src/scaffold/index.ts
|
|
1512
|
-
var
|
|
1513
|
-
var
|
|
1801
|
+
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
1802
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
1514
1803
|
|
|
1515
1804
|
// src/scaffold/templates/tokens.ts
|
|
1516
1805
|
var tokensTemplate = `/**
|
|
@@ -1653,7 +1942,8 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
1653
1942
|
({ className = "", variant = "primary", size = "default", children, ...props }, ref) => {
|
|
1654
1943
|
// Standard 36px height, 8px radius, font-medium, strictly neutral palette
|
|
1655
1944
|
const baseClasses =
|
|
1656
|
-
"inline-flex items-center justify-center font-medium text-sm rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 disabled:pointer-events-none disabled:opacity-50 select-none";
|
|
1945
|
+
"inline-flex items-center justify-center font-medium text-sm rounded-md whitespace-nowrap shrink-0 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 disabled:pointer-events-none disabled:opacity-50 select-none cursor-pointer";
|
|
1946
|
+
|
|
1657
1947
|
|
|
1658
1948
|
const variantClasses = {
|
|
1659
1949
|
primary: "bg-neutral-900 text-white hover:bg-neutral-800 shadow-2xs",
|
|
@@ -1694,7 +1984,8 @@ export const Chip: React.FC<ChipProps> = ({
|
|
|
1694
1984
|
}) => {
|
|
1695
1985
|
// Standard 36px height, 8px radius, 14px typography
|
|
1696
1986
|
const baseClasses =
|
|
1697
|
-
"inline-flex items-center h-[36px] min-h-[36px] px-3 rounded-md text-sm font-medium gap-1.5 select-none";
|
|
1987
|
+
"inline-flex items-center h-[36px] min-h-[36px] px-3 rounded-md text-sm font-medium whitespace-nowrap shrink-0 gap-1.5 select-none";
|
|
1988
|
+
|
|
1698
1989
|
|
|
1699
1990
|
const variantClasses = {
|
|
1700
1991
|
neutral: "bg-neutral-100 text-neutral-800 border border-neutral-200",
|
|
@@ -1864,12 +2155,12 @@ export const TableHead = React.forwardRef<HTMLTableCellElement, TableHeadProps>(
|
|
|
1864
2155
|
({ className = "", icon, resizable = true, children, ...props }, ref) => (
|
|
1865
2156
|
<th
|
|
1866
2157
|
ref={ref}
|
|
1867
|
-
className={\`h-[36px] px-4 text-left font-medium text-sm text-neutral-900 select-none align-middle relative \${className}\`}
|
|
2158
|
+
className={\`h-[36px] px-4 text-left font-medium text-sm text-neutral-900 select-none align-middle relative whitespace-nowrap \${className}\`}
|
|
1868
2159
|
{...props}
|
|
1869
2160
|
>
|
|
1870
2161
|
<div className="flex items-center gap-2">
|
|
1871
2162
|
{icon && <span className="text-neutral-500 shrink-0">{icon}</span>}
|
|
1872
|
-
<span>{children}</span>
|
|
2163
|
+
<span className="whitespace-nowrap">{children}</span>
|
|
1873
2164
|
</div>
|
|
1874
2165
|
</th>
|
|
1875
2166
|
)
|
|
@@ -1915,7 +2206,7 @@ export const Tabs: React.FC<TabsProps> = ({ items, activeId, onChange, className
|
|
|
1915
2206
|
type="button"
|
|
1916
2207
|
disabled={tab.disabled}
|
|
1917
2208
|
onClick={() => onChange(tab.id)}
|
|
1918
|
-
className={\`inline-flex h-[28px] items-center justify-center gap-1.5 rounded-sm px-3 text-sm font-medium transition-all select-none \${
|
|
2209
|
+
className={\`inline-flex h-[28px] items-center justify-center gap-1.5 rounded-sm px-3 text-sm font-medium whitespace-nowrap shrink-0 transition-all select-none \${
|
|
1919
2210
|
isActive
|
|
1920
2211
|
? "bg-white text-neutral-900 shadow-2xs"
|
|
1921
2212
|
: "text-neutral-600 hover:text-neutral-900 hover:bg-neutral-200/50"
|
|
@@ -2007,8 +2298,8 @@ var COMPONENT_TEMPLATES = {
|
|
|
2007
2298
|
};
|
|
2008
2299
|
function scaffoldConfig(cwd = process.cwd(), preset = "berserk") {
|
|
2009
2300
|
if (preset === "python") {
|
|
2010
|
-
const jsonPath =
|
|
2011
|
-
if (
|
|
2301
|
+
const jsonPath = import_node_path5.default.resolve(cwd, "dse.config.json");
|
|
2302
|
+
if (import_node_fs5.default.existsSync(jsonPath)) return jsonPath;
|
|
2012
2303
|
const content2 = JSON.stringify(
|
|
2013
2304
|
{
|
|
2014
2305
|
preset: "python",
|
|
@@ -2025,11 +2316,11 @@ function scaffoldConfig(cwd = process.cwd(), preset = "berserk") {
|
|
|
2025
2316
|
null,
|
|
2026
2317
|
2
|
|
2027
2318
|
) + "\n";
|
|
2028
|
-
|
|
2319
|
+
import_node_fs5.default.writeFileSync(jsonPath, content2, "utf-8");
|
|
2029
2320
|
return jsonPath;
|
|
2030
2321
|
}
|
|
2031
|
-
const configPath =
|
|
2032
|
-
if (
|
|
2322
|
+
const configPath = import_node_path5.default.resolve(cwd, "dse.config.js");
|
|
2323
|
+
if (import_node_fs5.default.existsSync(configPath)) {
|
|
2033
2324
|
return configPath;
|
|
2034
2325
|
}
|
|
2035
2326
|
const content = `import { defineConfig } from "design-system-enforcer";
|
|
@@ -2057,21 +2348,21 @@ export default defineConfig({
|
|
|
2057
2348
|
],
|
|
2058
2349
|
});
|
|
2059
2350
|
`;
|
|
2060
|
-
|
|
2351
|
+
import_node_fs5.default.writeFileSync(configPath, content, "utf-8");
|
|
2061
2352
|
return configPath;
|
|
2062
2353
|
}
|
|
2063
2354
|
function scaffoldTokens(cwd = process.cwd()) {
|
|
2064
|
-
const libDir =
|
|
2065
|
-
const appDir =
|
|
2066
|
-
if (!
|
|
2067
|
-
if (!
|
|
2068
|
-
const tokensPath =
|
|
2069
|
-
if (!
|
|
2070
|
-
|
|
2071
|
-
}
|
|
2072
|
-
const cssPath =
|
|
2073
|
-
if (!
|
|
2074
|
-
|
|
2355
|
+
const libDir = import_node_path5.default.resolve(cwd, "src/lib");
|
|
2356
|
+
const appDir = import_node_path5.default.resolve(cwd, "src/app");
|
|
2357
|
+
if (!import_node_fs5.default.existsSync(libDir)) import_node_fs5.default.mkdirSync(libDir, { recursive: true });
|
|
2358
|
+
if (!import_node_fs5.default.existsSync(appDir)) import_node_fs5.default.mkdirSync(appDir, { recursive: true });
|
|
2359
|
+
const tokensPath = import_node_path5.default.join(libDir, "tokens.ts");
|
|
2360
|
+
if (!import_node_fs5.default.existsSync(tokensPath)) {
|
|
2361
|
+
import_node_fs5.default.writeFileSync(tokensPath, tokensTemplate, "utf-8");
|
|
2362
|
+
}
|
|
2363
|
+
const cssPath = import_node_path5.default.join(appDir, "globals.css");
|
|
2364
|
+
if (!import_node_fs5.default.existsSync(cssPath)) {
|
|
2365
|
+
import_node_fs5.default.writeFileSync(cssPath, cssTemplate, "utf-8");
|
|
2075
2366
|
}
|
|
2076
2367
|
return { tokensPath, cssPath };
|
|
2077
2368
|
}
|
|
@@ -2081,13 +2372,13 @@ function scaffoldComponent(componentName, cwd = process.cwd()) {
|
|
|
2081
2372
|
if (!template) {
|
|
2082
2373
|
return null;
|
|
2083
2374
|
}
|
|
2084
|
-
const uiDir =
|
|
2085
|
-
if (!
|
|
2086
|
-
|
|
2375
|
+
const uiDir = import_node_path5.default.resolve(cwd, "src/components/ui");
|
|
2376
|
+
if (!import_node_fs5.default.existsSync(uiDir)) {
|
|
2377
|
+
import_node_fs5.default.mkdirSync(uiDir, { recursive: true });
|
|
2087
2378
|
}
|
|
2088
|
-
const targetPath =
|
|
2089
|
-
if (!
|
|
2090
|
-
|
|
2379
|
+
const targetPath = import_node_path5.default.join(uiDir, template.filename);
|
|
2380
|
+
if (!import_node_fs5.default.existsSync(targetPath)) {
|
|
2381
|
+
import_node_fs5.default.writeFileSync(targetPath, template.content, "utf-8");
|
|
2091
2382
|
}
|
|
2092
2383
|
return targetPath;
|
|
2093
2384
|
}
|
|
@@ -2152,12 +2443,283 @@ function generateMarkdownRules(config) {
|
|
|
2152
2443
|
`;
|
|
2153
2444
|
}
|
|
2154
2445
|
|
|
2446
|
+
// src/doctor/index.ts
|
|
2447
|
+
function computeDoctorScore(result) {
|
|
2448
|
+
const categoryViolations = {
|
|
2449
|
+
typography: 0,
|
|
2450
|
+
geometry: 0,
|
|
2451
|
+
components: 0,
|
|
2452
|
+
colors: 0,
|
|
2453
|
+
wrapping: 0,
|
|
2454
|
+
tables: 0
|
|
2455
|
+
};
|
|
2456
|
+
const fileCountMap = {};
|
|
2457
|
+
result.violations.forEach((v) => {
|
|
2458
|
+
fileCountMap[v.file] = (fileCountMap[v.file] || 0) + 1;
|
|
2459
|
+
if (v.rule.startsWith("font-") || v.rule.includes("text-size") || v.rule.includes("uppercase")) {
|
|
2460
|
+
categoryViolations.typography++;
|
|
2461
|
+
} else if (v.rule.includes("wrapping")) {
|
|
2462
|
+
categoryViolations.wrapping++;
|
|
2463
|
+
} else if (v.rule.startsWith("control-") || v.rule.startsWith("corner-radius")) {
|
|
2464
|
+
categoryViolations.geometry++;
|
|
2465
|
+
} else if (v.rule.includes("color") || v.rule.includes("gradient") || v.rule.includes("blur")) {
|
|
2466
|
+
categoryViolations.colors++;
|
|
2467
|
+
} else if (v.rule.startsWith("table-")) {
|
|
2468
|
+
categoryViolations.tables++;
|
|
2469
|
+
} else {
|
|
2470
|
+
categoryViolations.components++;
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
const filesCount = Math.max(1, result.scannedFilesCount || 1);
|
|
2474
|
+
function calcScore(violations) {
|
|
2475
|
+
const penaltyPerViolation = 100 / (filesCount * 2);
|
|
2476
|
+
return Math.max(0, Math.round(100 - violations * penaltyPerViolation));
|
|
2477
|
+
}
|
|
2478
|
+
const categories = [
|
|
2479
|
+
{ name: "Typography Scale", category: "typography", violations: categoryViolations.typography, score: calcScore(categoryViolations.typography) },
|
|
2480
|
+
{ name: "Geometry & Radii", category: "geometry", violations: categoryViolations.geometry, score: calcScore(categoryViolations.geometry) },
|
|
2481
|
+
{ name: "Wrap Integrity", category: "wrapping", violations: categoryViolations.wrapping, score: calcScore(categoryViolations.wrapping) },
|
|
2482
|
+
{ name: "Component Inventory", category: "components", violations: categoryViolations.components, score: calcScore(categoryViolations.components) },
|
|
2483
|
+
{ name: "Color Purity", category: "colors", violations: categoryViolations.colors, score: calcScore(categoryViolations.colors) },
|
|
2484
|
+
{ name: "Table Invariants", category: "tables", violations: categoryViolations.tables, score: calcScore(categoryViolations.tables) }
|
|
2485
|
+
];
|
|
2486
|
+
const totalScore = Math.round(categories.reduce((acc, c) => acc + c.score, 0) / categories.length);
|
|
2487
|
+
let grade = "F";
|
|
2488
|
+
if (totalScore >= 98) grade = "A+";
|
|
2489
|
+
else if (totalScore >= 90) grade = "A";
|
|
2490
|
+
else if (totalScore >= 80) grade = "B";
|
|
2491
|
+
else if (totalScore >= 70) grade = "C";
|
|
2492
|
+
else if (totalScore >= 60) grade = "D";
|
|
2493
|
+
const fileBreakdown = Object.entries(fileCountMap).map(([file, violations]) => ({ file, violations })).sort((a, b) => b.violations - a.violations).slice(0, 5);
|
|
2494
|
+
return {
|
|
2495
|
+
overallScore: totalScore,
|
|
2496
|
+
grade,
|
|
2497
|
+
scannedFiles: filesCount,
|
|
2498
|
+
totalViolations: result.violations.length,
|
|
2499
|
+
categories,
|
|
2500
|
+
fileBreakdown
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
// src/doctor/formatter.ts
|
|
2505
|
+
var import_picocolors2 = __toESM(require("picocolors"), 1);
|
|
2506
|
+
function renderProgressBar(percentage, width = 20) {
|
|
2507
|
+
const filledChars = Math.round(percentage / 100 * width);
|
|
2508
|
+
const emptyChars = width - filledChars;
|
|
2509
|
+
let colorFn = import_picocolors2.default.green;
|
|
2510
|
+
if (percentage < 70) colorFn = import_picocolors2.default.red;
|
|
2511
|
+
else if (percentage < 90) colorFn = import_picocolors2.default.yellow;
|
|
2512
|
+
const bar = colorFn("\u2588".repeat(filledChars)) + import_picocolors2.default.dim("\u2591".repeat(emptyChars));
|
|
2513
|
+
return `${bar} ${colorFn(percentage.toString().padStart(3) + "%")}`;
|
|
2514
|
+
}
|
|
2515
|
+
function formatDoctorOutput(report) {
|
|
2516
|
+
const lines = [];
|
|
2517
|
+
lines.push("");
|
|
2518
|
+
lines.push(import_picocolors2.default.bold(import_picocolors2.default.cyan("========================================================")));
|
|
2519
|
+
lines.push(import_picocolors2.default.bold(import_picocolors2.default.cyan(" DESIGN SYSTEM ENFORCER - HEALTH REPORT ")));
|
|
2520
|
+
lines.push(import_picocolors2.default.bold(import_picocolors2.default.cyan("========================================================")));
|
|
2521
|
+
lines.push("");
|
|
2522
|
+
let gradeColor = import_picocolors2.default.green;
|
|
2523
|
+
if (report.grade === "B") gradeColor = import_picocolors2.default.cyan;
|
|
2524
|
+
else if (report.grade === "C") gradeColor = import_picocolors2.default.yellow;
|
|
2525
|
+
else if (report.grade === "D" || report.grade === "F") gradeColor = import_picocolors2.default.red;
|
|
2526
|
+
lines.push(
|
|
2527
|
+
` ${import_picocolors2.default.bold("Overall Score:")} ${gradeColor(import_picocolors2.default.bold(report.overallScore + "/100"))} [${gradeColor(import_picocolors2.default.bold(report.grade))}]`
|
|
2528
|
+
);
|
|
2529
|
+
lines.push(` ${import_picocolors2.default.bold("Scanned Files:")} ${import_picocolors2.default.white(report.scannedFiles)}`);
|
|
2530
|
+
lines.push(` ${import_picocolors2.default.bold("Total Issues:")} ${report.totalViolations === 0 ? import_picocolors2.default.green("0") : import_picocolors2.default.red(report.totalViolations)}`);
|
|
2531
|
+
lines.push("");
|
|
2532
|
+
lines.push(import_picocolors2.default.bold(" Category Compliance:"));
|
|
2533
|
+
report.categories.forEach((cat) => {
|
|
2534
|
+
const paddedName = cat.name.padEnd(22, " ");
|
|
2535
|
+
const issueCount = cat.violations > 0 ? import_picocolors2.default.dim(`(${cat.violations} issues)`) : "";
|
|
2536
|
+
lines.push(` ${import_picocolors2.default.dim(paddedName)} ${renderProgressBar(cat.score)} ${issueCount}`);
|
|
2537
|
+
});
|
|
2538
|
+
lines.push("");
|
|
2539
|
+
if (report.fileBreakdown.length > 0) {
|
|
2540
|
+
lines.push(import_picocolors2.default.bold(import_picocolors2.default.yellow(" Top Offending Files:")));
|
|
2541
|
+
report.fileBreakdown.forEach((item, idx) => {
|
|
2542
|
+
lines.push(` ${idx + 1}. ${import_picocolors2.default.cyan(item.file)}: ${import_picocolors2.default.red(item.violations + " violations")}`);
|
|
2543
|
+
});
|
|
2544
|
+
lines.push("");
|
|
2545
|
+
lines.push(import_picocolors2.default.dim(" Run 'dse check' for detailed line-by-line violation reports."));
|
|
2546
|
+
lines.push(import_picocolors2.default.dim(" Run 'dse fix' to automatically remediate common violations."));
|
|
2547
|
+
} else {
|
|
2548
|
+
lines.push(import_picocolors2.default.bold(import_picocolors2.default.green(" \u2714 All design tokens and component invariants are 100% compliant!")));
|
|
2549
|
+
}
|
|
2550
|
+
lines.push("");
|
|
2551
|
+
return lines.join("\n");
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// src/fixer/index.ts
|
|
2555
|
+
var import_node_fs6 = __toESM(require("fs"), 1);
|
|
2556
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
2557
|
+
async function runDseFix(config, cwd = process.cwd()) {
|
|
2558
|
+
const scanResult = await runDseScan(config, cwd);
|
|
2559
|
+
const violationsByFile = {};
|
|
2560
|
+
scanResult.violations.forEach((v) => {
|
|
2561
|
+
if (!violationsByFile[v.file]) {
|
|
2562
|
+
violationsByFile[v.file] = [];
|
|
2563
|
+
}
|
|
2564
|
+
violationsByFile[v.file].push(v);
|
|
2565
|
+
});
|
|
2566
|
+
const details = [];
|
|
2567
|
+
let totalModifications = 0;
|
|
2568
|
+
for (const [relPath, violations] of Object.entries(violationsByFile)) {
|
|
2569
|
+
const fullPath = import_node_path6.default.resolve(cwd, relPath);
|
|
2570
|
+
if (!import_node_fs6.default.existsSync(fullPath)) continue;
|
|
2571
|
+
let content = import_node_fs6.default.readFileSync(fullPath, "utf-8");
|
|
2572
|
+
const originalContent = content;
|
|
2573
|
+
const fileFixes = [];
|
|
2574
|
+
if (violations.some((v) => v.rule === "no-wrapping-compact-elements")) {
|
|
2575
|
+
const before = content;
|
|
2576
|
+
content = content.replace(/(<(?:Button|IconButton|Chip|Badge)\b[^>]*className=["'`][^"'`]*?)\b(?:whitespace-normal|whitespace-pre-wrap|flex-wrap)\b([^"'`]*?["'`])/g, (match, prefix, suffix) => {
|
|
2577
|
+
return `${prefix}whitespace-nowrap shrink-0${suffix}`;
|
|
2578
|
+
});
|
|
2579
|
+
if (before !== content) {
|
|
2580
|
+
fileFixes.push("Replaced wrapping classes with 'whitespace-nowrap shrink-0' on compact controls");
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
if (violations.some((v) => v.rule === "no-raw-html-elements" && v.message.includes("<button>"))) {
|
|
2584
|
+
const before = content;
|
|
2585
|
+
content = content.replace(/<button(\s|>)/g, "<Button$1");
|
|
2586
|
+
content = content.replace(/<\/button>/g, "</Button>");
|
|
2587
|
+
if (before !== content) {
|
|
2588
|
+
if (!content.includes("import { Button }") && !content.includes("import Button") && !relPath.includes("components/ui/button")) {
|
|
2589
|
+
content = `import { Button } from "@/components/ui/button";
|
|
2590
|
+
` + content;
|
|
2591
|
+
}
|
|
2592
|
+
fileFixes.push("Upgraded raw <button> to <Button>");
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
if (violations.some((v) => v.rule === "no-raw-html-elements" && v.message.includes("<select>"))) {
|
|
2596
|
+
const before = content;
|
|
2597
|
+
content = content.replace(/<select(\s|>)/g, "<Dropdown$1");
|
|
2598
|
+
content = content.replace(/<\/select>/g, "</Dropdown>");
|
|
2599
|
+
if (before !== content) {
|
|
2600
|
+
if (!content.includes("import { Dropdown }") && !content.includes("import Dropdown") && !relPath.includes("components/ui/dropdown")) {
|
|
2601
|
+
content = `import { Dropdown } from "@/components/ui/dropdown";
|
|
2602
|
+
` + content;
|
|
2603
|
+
}
|
|
2604
|
+
fileFixes.push("Upgraded raw <select> to <Dropdown>");
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
if (violations.some((v) => v.rule === "no-raw-html-elements" && v.message.includes("<textarea>"))) {
|
|
2608
|
+
const before = content;
|
|
2609
|
+
content = content.replace(/<textarea(\s|>)/g, "<Textarea$1");
|
|
2610
|
+
content = content.replace(/<\/textarea>/g, "</Textarea>");
|
|
2611
|
+
if (before !== content) {
|
|
2612
|
+
if (!content.includes("import { Textarea }") && !content.includes("import Textarea") && !relPath.includes("components/ui/textarea")) {
|
|
2613
|
+
content = `import { Textarea } from "@/components/ui/textarea";
|
|
2614
|
+
` + content;
|
|
2615
|
+
}
|
|
2616
|
+
fileFixes.push("Upgraded raw <textarea> to <Textarea>");
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
if (violations.some((v) => v.rule === "no-arbitrary-text-size")) {
|
|
2620
|
+
const before = content;
|
|
2621
|
+
content = content.replace(/\btext-\[(?:13|13\.5|14|14\.5|15|15\.5)px\]\b/g, "text-sm");
|
|
2622
|
+
content = content.replace(/\btext-\[(?:11|11\.5|12|12\.5)px\]\b/g, "text-xs");
|
|
2623
|
+
content = content.replace(/\btext-\[(?:16|17)px\]\b/g, "text-base");
|
|
2624
|
+
if (before !== content) {
|
|
2625
|
+
fileFixes.push("Standardized arbitrary font sizes to compliant token scales");
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
if (violations.some((v) => v.rule === "no-backdrop-blur")) {
|
|
2629
|
+
const before = content;
|
|
2630
|
+
content = content.replace(/\bbackdrop-blur(?:-(?:none|sm|md|lg|xl|2xl|3xl))?\b/g, "");
|
|
2631
|
+
if (before !== content) {
|
|
2632
|
+
fileFixes.push("Removed non-compliant backdrop-blur classes");
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
if (content !== originalContent) {
|
|
2636
|
+
import_node_fs6.default.writeFileSync(fullPath, content, "utf-8");
|
|
2637
|
+
totalModifications += fileFixes.length;
|
|
2638
|
+
details.push({
|
|
2639
|
+
file: relPath,
|
|
2640
|
+
fixes: fileFixes
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
return {
|
|
2645
|
+
filesFixed: details.length,
|
|
2646
|
+
totalModifications,
|
|
2647
|
+
details
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// src/guardrails/index.ts
|
|
2652
|
+
var import_node_fs7 = __toESM(require("fs"), 1);
|
|
2653
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
2654
|
+
function generateAgentInstructions(config) {
|
|
2655
|
+
const allowedRadii = config.tokens?.geometry?.allowedRadii?.join(", ") || "0px, 8px, 12px, 16px, 9999px";
|
|
2656
|
+
const maxControlHeight = config.tokens?.geometry?.maxControlHeight || 36;
|
|
2657
|
+
const bannedColors = config.tokens?.colors?.bannedColorFamilies?.join(", ") || "slate, gray, zinc, stone, etc.";
|
|
2658
|
+
return `<!-- BEGIN:DSE-AGENT-RULES -->
|
|
2659
|
+
# DESIGN SYSTEM ENFORCER (DSE) - MANDATORY RULES
|
|
2660
|
+
|
|
2661
|
+
You MUST adhere to these design system invariants for all UI generation:
|
|
2662
|
+
|
|
2663
|
+
1. COMPONENT INVARIANTS:
|
|
2664
|
+
- NEVER use raw HTML: <button>, <select>, <textarea>, <table>, <input type="checkbox">, <input type="radio">.
|
|
2665
|
+
- ALWAYS use standard primitives: <Button>, <Dropdown>, <Textarea>, <Table>, <Checkbox>, <Chip>.
|
|
2666
|
+
- Never create pseudo-chips using raw <div> or <span> with background fills and borders. Use <Chip>.
|
|
2667
|
+
|
|
2668
|
+
2. WRAP INTEGRITY (COMPACT ELEMENTS):
|
|
2669
|
+
- Interactive compact elements (<Button>, <Chip>, <Badge>, <TableHead>, tabs) MUST NOT wrap text.
|
|
2670
|
+
- ALWAYS include 'whitespace-nowrap shrink-0' on compact interactive components.
|
|
2671
|
+
|
|
2672
|
+
3. GEOMETRY & RADII:
|
|
2673
|
+
- Standard control height for buttons and inputs is strictly max ${maxControlHeight}px ('h-[36px]' or 'h-9').
|
|
2674
|
+
- Allowed corner radii tokens: ${allowedRadii}. Never use random arbitrary radii.
|
|
2675
|
+
|
|
2676
|
+
4. COLOR SCALE & PURITY:
|
|
2677
|
+
- Strictly neutral palette for base surfaces and borders ('neutral-100' through 'neutral-900').
|
|
2678
|
+
- Banned arbitrary color families: ${bannedColors}.
|
|
2679
|
+
- Never use arbitrary inline hex colors ('#1a2b3c') in utility classes. Use design system CSS variables ('var(--primary)').
|
|
2680
|
+
|
|
2681
|
+
5. BEFORE FINISHING UI EDITS:
|
|
2682
|
+
- Run 'npx dse check' to verify zero design system violations.
|
|
2683
|
+
<!-- END:DSE-AGENT-RULES -->
|
|
2684
|
+
`;
|
|
2685
|
+
}
|
|
2686
|
+
function installPreCommitHook(cwd = process.cwd()) {
|
|
2687
|
+
const gitDir = import_node_path7.default.resolve(cwd, ".git");
|
|
2688
|
+
if (!import_node_fs7.default.existsSync(gitDir)) {
|
|
2689
|
+
throw new Error("No .git repository found in this directory.");
|
|
2690
|
+
}
|
|
2691
|
+
const hooksDir = import_node_path7.default.resolve(gitDir, "hooks");
|
|
2692
|
+
if (!import_node_fs7.default.existsSync(hooksDir)) {
|
|
2693
|
+
import_node_fs7.default.mkdirSync(hooksDir, { recursive: true });
|
|
2694
|
+
}
|
|
2695
|
+
const hookPath = import_node_path7.default.resolve(hooksDir, "pre-commit");
|
|
2696
|
+
const hookScript = `#!/usr/bin/env sh
|
|
2697
|
+
# Design System Enforcer (DSE) Pre-Commit Hook
|
|
2698
|
+
echo "
|
|
2699
|
+
\u{1F50D} Running Design System Enforcer pre-commit check..."
|
|
2700
|
+
|
|
2701
|
+
if command -v npx >/dev/null 2>&1; then
|
|
2702
|
+
npx dse check --strict
|
|
2703
|
+
RESULT=$?
|
|
2704
|
+
if [ $RESULT -ne 0 ]; then
|
|
2705
|
+
echo "
|
|
2706
|
+
\u274C Commit rejected: Design System violations found."
|
|
2707
|
+
echo "\u{1F4A1} Run 'npx dse fix' or 'npx dse check' to review issues.
|
|
2708
|
+
"
|
|
2709
|
+
exit 1
|
|
2710
|
+
fi
|
|
2711
|
+
fi
|
|
2712
|
+
`;
|
|
2713
|
+
import_node_fs7.default.writeFileSync(hookPath, hookScript, { mode: 493 });
|
|
2714
|
+
return true;
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2155
2717
|
// src/cli.ts
|
|
2156
2718
|
var program = new import_commander.Command();
|
|
2157
|
-
program.name("dse").description("Design System Enforcer (DSE) - AST-based Linter and Scaffold Engine").version("1.0.
|
|
2719
|
+
program.name("dse").description("Design System Enforcer (DSE) - AST-based Linter and Scaffold Engine").version("1.0.4");
|
|
2158
2720
|
program.command("check [targetDir]", { isDefault: true }).description("Scan codebase and enforce Design System rules").option("-c, --config <path>", "Path to custom dse.config.js / dse.config.json").option("-f, --format <format>", "Output format: 'stylish' | 'json'", "stylish").option("--strict", "Treat warnings as errors", false).action(async (targetDirArg, options) => {
|
|
2159
2721
|
try {
|
|
2160
|
-
const cwd = targetDirArg ?
|
|
2722
|
+
const cwd = targetDirArg ? import_node_path8.default.resolve(process.cwd(), targetDirArg) : process.cwd();
|
|
2161
2723
|
const config = await loadConfig(cwd, options.config);
|
|
2162
2724
|
const result = await runDseScan(config, cwd);
|
|
2163
2725
|
if (options.format === "json") {
|
|
@@ -2170,76 +2732,125 @@ program.command("check [targetDir]", { isDefault: true }).description("Scan code
|
|
|
2170
2732
|
process.exit(1);
|
|
2171
2733
|
}
|
|
2172
2734
|
} catch (err) {
|
|
2173
|
-
console.error(
|
|
2735
|
+
console.error(import_picocolors3.default.red(`
|
|
2736
|
+
\u2716 Error running scan: ${err.message}`));
|
|
2174
2737
|
process.exit(1);
|
|
2175
2738
|
}
|
|
2176
2739
|
});
|
|
2177
|
-
program.command("
|
|
2740
|
+
program.command("doctor [targetDir]").description("Perform a visual Design System health audit and compute compliance score").option("-c, --config <path>", "Path to custom dse.config.js / dse.config.json").action(async (targetDirArg, options) => {
|
|
2741
|
+
try {
|
|
2742
|
+
const cwd = targetDirArg ? import_node_path8.default.resolve(process.cwd(), targetDirArg) : process.cwd();
|
|
2743
|
+
const config = await loadConfig(cwd, options.config);
|
|
2744
|
+
const result = await runDseScan(config, cwd);
|
|
2745
|
+
const report = computeDoctorScore(result);
|
|
2746
|
+
console.log(formatDoctorOutput(report));
|
|
2747
|
+
} catch (err) {
|
|
2748
|
+
console.error(import_picocolors3.default.red(`
|
|
2749
|
+
\u2716 Error generating health report: ${err.message}`));
|
|
2750
|
+
process.exit(1);
|
|
2751
|
+
}
|
|
2752
|
+
});
|
|
2753
|
+
program.command("fix [targetDir]").description("Automatically remediate common Design System violations in place").option("-c, --config <path>", "Path to custom dse.config.js / dse.config.json").action(async (targetDirArg, options) => {
|
|
2754
|
+
try {
|
|
2755
|
+
const cwd = targetDirArg ? import_node_path8.default.resolve(process.cwd(), targetDirArg) : process.cwd();
|
|
2756
|
+
const config = await loadConfig(cwd, options.config);
|
|
2757
|
+
console.log(import_picocolors3.default.cyan("\n\u{1F527} Running Design System Enforcer Auto-Fixer...\n"));
|
|
2758
|
+
const fixReport = await runDseFix(config, cwd);
|
|
2759
|
+
if (fixReport.filesFixed === 0) {
|
|
2760
|
+
console.log(import_picocolors3.default.green("\u2714 No auto-fixable violations found. Your codebase is clean!"));
|
|
2761
|
+
} else {
|
|
2762
|
+
console.log(import_picocolors3.default.bold(import_picocolors3.default.green(`\u2714 Successfully fixed ${fixReport.totalModifications} issues across ${fixReport.filesFixed} files:
|
|
2763
|
+
`)));
|
|
2764
|
+
fixReport.details.forEach((item) => {
|
|
2765
|
+
console.log(` ${import_picocolors3.default.bold(import_picocolors3.default.cyan(item.file))}:`);
|
|
2766
|
+
item.fixes.forEach((f) => console.log(` - ${import_picocolors3.default.dim(f)}`));
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
console.log("");
|
|
2770
|
+
} catch (err) {
|
|
2771
|
+
console.error(import_picocolors3.default.red(`
|
|
2772
|
+
\u2716 Error during auto-fix: ${err.message}`));
|
|
2773
|
+
process.exit(1);
|
|
2774
|
+
}
|
|
2775
|
+
});
|
|
2776
|
+
program.command("detect [targetDir]").description("Inspect project architecture, framework, libraries, and design tokens autonomously").action((targetDirArg) => {
|
|
2777
|
+
const cwd = targetDirArg ? import_node_path8.default.resolve(process.cwd(), targetDirArg) : process.cwd();
|
|
2778
|
+
const info = detectProject(cwd);
|
|
2779
|
+
console.log(import_picocolors3.default.bold(import_picocolors3.default.cyan("\n\u{1F50D} DSE Autonomous Project Diagnostics\n")));
|
|
2780
|
+
console.log(` ${import_picocolors3.default.bold("Target Directory:")} ${info.root}`);
|
|
2781
|
+
console.log(` ${import_picocolors3.default.bold("Framework:")} ${import_picocolors3.default.green(info.framework)}`);
|
|
2782
|
+
console.log(` ${import_picocolors3.default.bold("Monorepo:")} ${info.isMonorepo ? import_picocolors3.default.yellow("Yes") : "No"}`);
|
|
2783
|
+
if (info.isMonorepo && info.monorepoWorkspaces.length > 0) {
|
|
2784
|
+
console.log(` ${import_picocolors3.default.bold("Workspaces:")} ${info.monorepoWorkspaces.join(", ")}`);
|
|
2785
|
+
}
|
|
2786
|
+
console.log(` ${import_picocolors3.default.bold("Libraries:")} ${info.libraries.length ? import_picocolors3.default.magenta(info.libraries.join(", ")) : "None"}`);
|
|
2787
|
+
console.log(` ${import_picocolors3.default.bold("Tailwind CSS:")} ${info.hasTailwind ? import_picocolors3.default.green(info.tailwindVersion || "Yes") : "No"}`);
|
|
2788
|
+
console.log(` ${import_picocolors3.default.bold("TypeScript:")} ${info.hasTypeScript ? "Yes" : "No"}`);
|
|
2789
|
+
console.log(` ${import_picocolors3.default.bold("Tokens File:")} ${info.tokensFile ? import_picocolors3.default.cyan(info.tokensFile) : import_picocolors3.default.dim("None (using preset defaults)")}`);
|
|
2790
|
+
console.log(` ${import_picocolors3.default.bold("Default Preset:")} ${import_picocolors3.default.yellow(info.preset)}`);
|
|
2791
|
+
console.log(` ${import_picocolors3.default.bold("Auto Includes:")} ${info.suggestedIncludes.join(", ")}`);
|
|
2792
|
+
console.log(` ${import_picocolors3.default.bold("Auto Ignores:")} ${info.suggestedIgnores.slice(0, 5).join(", ")} (+${Math.max(0, info.suggestedIgnores.length - 5)} more)`);
|
|
2793
|
+
console.log("");
|
|
2794
|
+
});
|
|
2795
|
+
program.command("export-agent").description("Export ultra-compact prompt rules for Cursor, Claude, Antigravity, or Copilot").option("-w, --write <file>", "Target file to write/append into (e.g. .cursorrules, AGENTS.md)").action(async (options) => {
|
|
2178
2796
|
const cwd = process.cwd();
|
|
2179
|
-
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2797
|
+
const config = await loadConfig(cwd);
|
|
2798
|
+
const instructions = generateAgentInstructions(config);
|
|
2799
|
+
if (options.write) {
|
|
2800
|
+
const targetPath = import_node_path8.default.resolve(cwd, options.write);
|
|
2801
|
+
if (import_node_fs8.default.existsSync(targetPath)) {
|
|
2802
|
+
let existing = import_node_fs8.default.readFileSync(targetPath, "utf-8");
|
|
2803
|
+
const startTag = "<!-- BEGIN:DSE-AGENT-RULES -->";
|
|
2804
|
+
const endTag = "<!-- END:DSE-AGENT-RULES -->";
|
|
2805
|
+
if (existing.includes(startTag) && existing.includes(endTag)) {
|
|
2806
|
+
const regex = new RegExp(`${startTag}[\\s\\S]*?${endTag}`, "g");
|
|
2807
|
+
existing = existing.replace(regex, instructions.trim());
|
|
2808
|
+
} else {
|
|
2809
|
+
existing = `${existing.trim()}
|
|
2810
|
+
|
|
2811
|
+
${instructions.trim()}
|
|
2812
|
+
`;
|
|
2193
2813
|
}
|
|
2194
|
-
|
|
2195
|
-
console.
|
|
2814
|
+
import_node_fs8.default.writeFileSync(targetPath, existing, "utf-8");
|
|
2815
|
+
console.log(import_picocolors3.default.green(`\u2714 Updated AI guardrails in ${options.write}`));
|
|
2816
|
+
} else {
|
|
2817
|
+
import_node_fs8.default.writeFileSync(targetPath, instructions, "utf-8");
|
|
2818
|
+
console.log(import_picocolors3.default.green(`\u2714 Created ${options.write} with AI guardrails`));
|
|
2196
2819
|
}
|
|
2197
|
-
}
|
|
2198
|
-
if (options.withPrimitives) {
|
|
2199
|
-
console.log(import_picocolors2.default.cyan("Scaffolding primitives and theme tokens..."));
|
|
2200
|
-
const created = scaffoldAllPrimitives(cwd);
|
|
2201
|
-
created.forEach((file) => {
|
|
2202
|
-
console.log(import_picocolors2.default.green("\u2714 Created:"), import_picocolors2.default.dim(import_node_path4.default.relative(cwd, file)));
|
|
2203
|
-
});
|
|
2204
2820
|
} else {
|
|
2205
|
-
console.log(
|
|
2206
|
-
import_picocolors2.default.dim("\nTip: Run 'npx dse scaffold' to generate rule-compliant base primitives and tokens anytime.")
|
|
2207
|
-
);
|
|
2821
|
+
console.log(instructions);
|
|
2208
2822
|
}
|
|
2209
|
-
console.log(import_picocolors2.default.bold(import_picocolors2.default.green("\n\u2714 DSE setup complete! Run 'npm run dse' to verify your project.\n")));
|
|
2210
2823
|
});
|
|
2211
|
-
program.command("
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
);
|
|
2824
|
+
program.command("hook [action]").description("Install or manage git pre-commit hooks to block non-compliant commits").action((action) => {
|
|
2825
|
+
if (action === "install" || !action) {
|
|
2826
|
+
try {
|
|
2827
|
+
installPreCommitHook(process.cwd());
|
|
2828
|
+
console.log(import_picocolors3.default.bold(import_picocolors3.default.green("\n\u2714 Successfully installed pre-commit hook in .git/hooks/pre-commit!")));
|
|
2829
|
+
console.log(import_picocolors3.default.dim(" Every git commit will now be checked with 'dse check --strict'.\n"));
|
|
2830
|
+
} catch (err) {
|
|
2831
|
+
console.error(import_picocolors3.default.red(`
|
|
2832
|
+
\u2716 Could not install hook: ${err.message}
|
|
2833
|
+
`));
|
|
2222
2834
|
}
|
|
2223
2835
|
}
|
|
2224
|
-
console.log("");
|
|
2225
2836
|
});
|
|
2226
|
-
program.command("
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2837
|
+
program.command("init").description("Initialize DSE configuration in current repository").option("-p, --preset <name>", "Preset: berserk | recommended | minimal | python", "berserk").action((options) => {
|
|
2838
|
+
scaffoldConfig(process.cwd(), options.preset);
|
|
2839
|
+
});
|
|
2840
|
+
program.command("scaffold-tokens").description("Generate official design system tokens TypeScript/CSS file").action(() => {
|
|
2841
|
+
scaffoldTokens(process.cwd());
|
|
2842
|
+
});
|
|
2843
|
+
program.command("scaffold-primitives").description("Scaffold all canonical zero-compromise design system UI primitives").action(() => {
|
|
2844
|
+
scaffoldAllPrimitives(process.cwd());
|
|
2234
2845
|
});
|
|
2235
2846
|
program.command("export-rules").description("Export rules in Markdown format for AI coding assistants (AGENTS.md, .cursorrules, CLAUDE.md)").option("-w, --write <file>", "Write directly to target file (e.g. AGENTS.md)").action(async (options) => {
|
|
2236
2847
|
const cwd = process.cwd();
|
|
2237
2848
|
const config = await loadConfig(cwd);
|
|
2238
2849
|
const rulesMd = generateMarkdownRules(config);
|
|
2239
2850
|
if (options.write) {
|
|
2240
|
-
const targetFile =
|
|
2241
|
-
if (
|
|
2242
|
-
let existing =
|
|
2851
|
+
const targetFile = import_node_path8.default.resolve(cwd, options.write);
|
|
2852
|
+
if (import_node_fs8.default.existsSync(targetFile)) {
|
|
2853
|
+
let existing = import_node_fs8.default.readFileSync(targetFile, "utf-8");
|
|
2243
2854
|
const startTag = "<!-- BEGIN:design-system-enforcer-rules -->";
|
|
2244
2855
|
const endTag = "<!-- END:design-system-enforcer-rules -->";
|
|
2245
2856
|
if (existing.includes(startTag) && existing.includes(endTag)) {
|
|
@@ -2251,11 +2862,11 @@ program.command("export-rules").description("Export rules in Markdown format for
|
|
|
2251
2862
|
${rulesMd.trim()}
|
|
2252
2863
|
`;
|
|
2253
2864
|
}
|
|
2254
|
-
|
|
2255
|
-
console.log(
|
|
2865
|
+
import_node_fs8.default.writeFileSync(targetFile, existing, "utf-8");
|
|
2866
|
+
console.log(import_picocolors3.default.green(`\u2714 Updated rules in ${options.write}`));
|
|
2256
2867
|
} else {
|
|
2257
|
-
|
|
2258
|
-
console.log(
|
|
2868
|
+
import_node_fs8.default.writeFileSync(targetFile, rulesMd, "utf-8");
|
|
2869
|
+
console.log(import_picocolors3.default.green(`\u2714 Created ${options.write} with design system rules`));
|
|
2259
2870
|
}
|
|
2260
2871
|
} else {
|
|
2261
2872
|
console.log(rulesMd);
|