design-system-enforcer 1.0.0 → 1.0.3

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 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 import_node_fs4 = __toESM(require("fs"), 1);
28
- var import_node_path4 = __toESM(require("path"), 1);
27
+ var import_node_fs6 = __toESM(require("fs"), 1);
28
+ var import_node_path6 = __toESM(require("path"), 1);
29
29
  var import_commander = require("commander");
30
30
  var import_picocolors2 = __toESM(require("picocolors"), 1);
31
31
 
32
32
  // src/config/loader.ts
33
- var import_node_fs = __toESM(require("fs"), 1);
34
- var import_node_path = __toESM(require("path"), 1);
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 = import_node_path.default.resolve(cwd, customPath);
301
- if (import_node_fs.default.existsSync(resolved)) {
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 = import_node_path.default.resolve(cwd, filename);
307
- if (import_node_fs.default.existsSync(candidate)) {
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 = import_node_fs.default.readFileSync(foundConfigPath, "utf-8");
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 = import_node_fs.default.readFileSync(foundConfigPath, "utf-8");
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
- return mergeConfigs(basePreset, userConfig);
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 import_node_fs2 = __toESM(require("fs"), 1);
399
- var import_node_path2 = __toESM(require("path"), 1);
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>'.`,
1038
+ lineSnippet
1039
+ );
1040
+ } else if (typeVal === "radio") {
1041
+ addViolation(
1042
+ filePath,
1043
+ pos.line,
1044
+ pos.col,
1045
+ "no-raw-html-elements",
1046
+ `Raw browser default '<input type="radio">' detected. Never use unstyled browser radio buttons. Use standard design system '<RadioGroup>' or styled toggle.`,
813
1047
  lineSnippet
814
1048
  );
815
- } else if (["text", "email", "search"].includes(typeVal)) {
1049
+ } else if (typeVal === "range") {
816
1050
  addViolation(
817
1051
  filePath,
818
1052
  pos.line,
819
1053
  pos.col,
820
1054
  "no-raw-html-elements",
821
- `Raw '<input type="${typeVal}">' element detected. Use standard design system '<Input>' component.`,
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 = import_node_path2.default.resolve(cwd, config.targetDir || "src");
1341
- if (!import_node_fs2.default.existsSync(targetDir)) {
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 = import_node_path2.default.relative(cwd, file).replace(/\\/g, "/");
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 = import_node_fs2.default.readFileSync(filePath, "utf-8");
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 import_node_fs3 = __toESM(require("fs"), 1);
1513
- var import_node_path3 = __toESM(require("path"), 1);
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 = import_node_path3.default.resolve(cwd, "dse.config.json");
2011
- if (import_node_fs3.default.existsSync(jsonPath)) return jsonPath;
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
- import_node_fs3.default.writeFileSync(jsonPath, content2, "utf-8");
2319
+ import_node_fs5.default.writeFileSync(jsonPath, content2, "utf-8");
2029
2320
  return jsonPath;
2030
2321
  }
2031
- const configPath = import_node_path3.default.resolve(cwd, "dse.config.js");
2032
- if (import_node_fs3.default.existsSync(configPath)) {
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
- import_node_fs3.default.writeFileSync(configPath, content, "utf-8");
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 = import_node_path3.default.resolve(cwd, "src/lib");
2065
- const appDir = import_node_path3.default.resolve(cwd, "src/app");
2066
- if (!import_node_fs3.default.existsSync(libDir)) import_node_fs3.default.mkdirSync(libDir, { recursive: true });
2067
- if (!import_node_fs3.default.existsSync(appDir)) import_node_fs3.default.mkdirSync(appDir, { recursive: true });
2068
- const tokensPath = import_node_path3.default.join(libDir, "tokens.ts");
2069
- if (!import_node_fs3.default.existsSync(tokensPath)) {
2070
- import_node_fs3.default.writeFileSync(tokensPath, tokensTemplate, "utf-8");
2071
- }
2072
- const cssPath = import_node_path3.default.join(appDir, "globals.css");
2073
- if (!import_node_fs3.default.existsSync(cssPath)) {
2074
- import_node_fs3.default.writeFileSync(cssPath, cssTemplate, "utf-8");
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 = import_node_path3.default.resolve(cwd, "src/components/ui");
2085
- if (!import_node_fs3.default.existsSync(uiDir)) {
2086
- import_node_fs3.default.mkdirSync(uiDir, { recursive: true });
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 = import_node_path3.default.join(uiDir, template.filename);
2089
- if (!import_node_fs3.default.existsSync(targetPath)) {
2090
- import_node_fs3.default.writeFileSync(targetPath, template.content, "utf-8");
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
  }
@@ -2157,7 +2448,7 @@ var program = new import_commander.Command();
2157
2448
  program.name("dse").description("Design System Enforcer (DSE) - AST-based Linter and Scaffold Engine").version("1.0.0");
2158
2449
  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
2450
  try {
2160
- const cwd = targetDirArg ? import_node_path4.default.resolve(process.cwd(), targetDirArg) : process.cwd();
2451
+ const cwd = targetDirArg ? import_node_path6.default.resolve(process.cwd(), targetDirArg) : process.cwd();
2161
2452
  const config = await loadConfig(cwd, options.config);
2162
2453
  const result = await runDseScan(config, cwd);
2163
2454
  if (options.format === "json") {
@@ -2177,18 +2468,18 @@ program.command("check [targetDir]", { isDefault: true }).description("Scan code
2177
2468
  program.command("init").description("Initialize DSE configuration and optionally scaffold base primitives").option("--preset <name>", "Preset to use: 'berserk' | 'recommended' | 'python'").option("--with-primitives", "Scaffold full base primitives and tokens suite", false).action((options) => {
2178
2469
  const cwd = process.cwd();
2179
2470
  console.log(import_picocolors2.default.bold(import_picocolors2.default.cyan("\nInitializing Design System Enforcer (DSE)...")));
2180
- const isPythonProject = import_node_fs4.default.existsSync(import_node_path4.default.resolve(cwd, "requirements.txt")) || import_node_fs4.default.existsSync(import_node_path4.default.resolve(cwd, "pyproject.toml")) || import_node_fs4.default.existsSync(import_node_path4.default.resolve(cwd, ".streamlit"));
2471
+ const isPythonProject = import_node_fs6.default.existsSync(import_node_path6.default.resolve(cwd, "requirements.txt")) || import_node_fs6.default.existsSync(import_node_path6.default.resolve(cwd, "pyproject.toml")) || import_node_fs6.default.existsSync(import_node_path6.default.resolve(cwd, ".streamlit"));
2181
2472
  const preset = options.preset || (isPythonProject ? "python" : "berserk");
2182
2473
  const configPath = scaffoldConfig(cwd, preset);
2183
- console.log(import_picocolors2.default.green("\u2714 Created config:"), import_picocolors2.default.bold(import_node_path4.default.relative(cwd, configPath)), import_picocolors2.default.dim(`(preset: ${preset})`));
2184
- const pkgPath = import_node_path4.default.resolve(cwd, "package.json");
2185
- if (import_node_fs4.default.existsSync(pkgPath)) {
2474
+ console.log(import_picocolors2.default.green("\u2714 Created config:"), import_picocolors2.default.bold(import_node_path6.default.relative(cwd, configPath)), import_picocolors2.default.dim(`(preset: ${preset})`));
2475
+ const pkgPath = import_node_path6.default.resolve(cwd, "package.json");
2476
+ if (import_node_fs6.default.existsSync(pkgPath)) {
2186
2477
  try {
2187
- const pkg = JSON.parse(import_node_fs4.default.readFileSync(pkgPath, "utf-8"));
2478
+ const pkg = JSON.parse(import_node_fs6.default.readFileSync(pkgPath, "utf-8"));
2188
2479
  pkg.scripts = pkg.scripts || {};
2189
2480
  if (!pkg.scripts.dse) {
2190
2481
  pkg.scripts.dse = "dse";
2191
- import_node_fs4.default.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2482
+ import_node_fs6.default.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2192
2483
  console.log(import_picocolors2.default.green("\u2714 Added script 'dse' to package.json"));
2193
2484
  }
2194
2485
  } catch (err) {
@@ -2199,7 +2490,7 @@ program.command("init").description("Initialize DSE configuration and optionally
2199
2490
  console.log(import_picocolors2.default.cyan("Scaffolding primitives and theme tokens..."));
2200
2491
  const created = scaffoldAllPrimitives(cwd);
2201
2492
  created.forEach((file) => {
2202
- console.log(import_picocolors2.default.green("\u2714 Created:"), import_picocolors2.default.dim(import_node_path4.default.relative(cwd, file)));
2493
+ console.log(import_picocolors2.default.green("\u2714 Created:"), import_picocolors2.default.dim(import_node_path6.default.relative(cwd, file)));
2203
2494
  });
2204
2495
  } else {
2205
2496
  console.log(
@@ -2214,7 +2505,7 @@ program.command("add <components...>").description("Scaffold specific rule-compl
2214
2505
  for (const comp of components) {
2215
2506
  const filePath = scaffoldComponent(comp, cwd);
2216
2507
  if (filePath) {
2217
- console.log(import_picocolors2.default.green("\u2714 Created:"), import_picocolors2.default.bold(import_node_path4.default.relative(cwd, filePath)));
2508
+ console.log(import_picocolors2.default.green("\u2714 Created:"), import_picocolors2.default.bold(import_node_path6.default.relative(cwd, filePath)));
2218
2509
  } else {
2219
2510
  console.log(
2220
2511
  import_picocolors2.default.yellow(`\u26A0 Unknown component '${comp}'. Available: ${Object.keys(COMPONENT_TEMPLATES).join(", ")}`)
@@ -2228,7 +2519,7 @@ program.command("scaffold").description("Scaffold all base primitives and HSL to
2228
2519
  console.log(import_picocolors2.default.bold(import_picocolors2.default.cyan("\nScaffolding full design system suite...")));
2229
2520
  const created = scaffoldAllPrimitives(cwd);
2230
2521
  created.forEach((file) => {
2231
- console.log(import_picocolors2.default.green("\u2714 Created:"), import_picocolors2.default.bold(import_node_path4.default.relative(cwd, file)));
2522
+ console.log(import_picocolors2.default.green("\u2714 Created:"), import_picocolors2.default.bold(import_node_path6.default.relative(cwd, file)));
2232
2523
  });
2233
2524
  console.log(import_picocolors2.default.bold(import_picocolors2.default.green("\n\u2714 All design system primitives scaffolded successfully!\n")));
2234
2525
  });
@@ -2237,9 +2528,9 @@ program.command("export-rules").description("Export rules in Markdown format for
2237
2528
  const config = await loadConfig(cwd);
2238
2529
  const rulesMd = generateMarkdownRules(config);
2239
2530
  if (options.write) {
2240
- const targetFile = import_node_path4.default.resolve(cwd, options.write);
2241
- if (import_node_fs4.default.existsSync(targetFile)) {
2242
- let existing = import_node_fs4.default.readFileSync(targetFile, "utf-8");
2531
+ const targetFile = import_node_path6.default.resolve(cwd, options.write);
2532
+ if (import_node_fs6.default.existsSync(targetFile)) {
2533
+ let existing = import_node_fs6.default.readFileSync(targetFile, "utf-8");
2243
2534
  const startTag = "<!-- BEGIN:design-system-enforcer-rules -->";
2244
2535
  const endTag = "<!-- END:design-system-enforcer-rules -->";
2245
2536
  if (existing.includes(startTag) && existing.includes(endTag)) {
@@ -2251,15 +2542,34 @@ program.command("export-rules").description("Export rules in Markdown format for
2251
2542
  ${rulesMd.trim()}
2252
2543
  `;
2253
2544
  }
2254
- import_node_fs4.default.writeFileSync(targetFile, existing, "utf-8");
2545
+ import_node_fs6.default.writeFileSync(targetFile, existing, "utf-8");
2255
2546
  console.log(import_picocolors2.default.green(`\u2714 Updated rules in ${options.write}`));
2256
2547
  } else {
2257
- import_node_fs4.default.writeFileSync(targetFile, rulesMd, "utf-8");
2548
+ import_node_fs6.default.writeFileSync(targetFile, rulesMd, "utf-8");
2258
2549
  console.log(import_picocolors2.default.green(`\u2714 Created ${options.write} with design system rules`));
2259
2550
  }
2260
2551
  } else {
2261
2552
  console.log(rulesMd);
2262
2553
  }
2263
2554
  });
2555
+ program.command("detect [targetDir]").description("Inspect project architecture, framework, libraries, and design tokens autonomously").action((targetDirArg) => {
2556
+ const cwd = targetDirArg ? import_node_path6.default.resolve(process.cwd(), targetDirArg) : process.cwd();
2557
+ const info = detectProject(cwd);
2558
+ console.log(import_picocolors2.default.bold(import_picocolors2.default.cyan("\n\u{1F50D} DSE Autonomous Project Diagnostics\n")));
2559
+ console.log(` ${import_picocolors2.default.bold("Target Directory:")} ${info.root}`);
2560
+ console.log(` ${import_picocolors2.default.bold("Framework:")} ${import_picocolors2.default.green(info.framework)}`);
2561
+ console.log(` ${import_picocolors2.default.bold("Monorepo:")} ${info.isMonorepo ? import_picocolors2.default.yellow("Yes") : "No"}`);
2562
+ if (info.isMonorepo && info.monorepoWorkspaces.length > 0) {
2563
+ console.log(` ${import_picocolors2.default.bold("Workspaces:")} ${info.monorepoWorkspaces.join(", ")}`);
2564
+ }
2565
+ console.log(` ${import_picocolors2.default.bold("Libraries:")} ${info.libraries.length ? import_picocolors2.default.magenta(info.libraries.join(", ")) : "None"}`);
2566
+ console.log(` ${import_picocolors2.default.bold("Tailwind CSS:")} ${info.hasTailwind ? import_picocolors2.default.green(info.tailwindVersion || "Yes") : "No"}`);
2567
+ console.log(` ${import_picocolors2.default.bold("TypeScript:")} ${info.hasTypeScript ? "Yes" : "No"}`);
2568
+ console.log(` ${import_picocolors2.default.bold("Tokens File:")} ${info.tokensFile ? import_picocolors2.default.cyan(info.tokensFile) : import_picocolors2.default.dim("None (using preset defaults)")}`);
2569
+ console.log(` ${import_picocolors2.default.bold("Default Preset:")} ${import_picocolors2.default.yellow(info.preset)}`);
2570
+ console.log(` ${import_picocolors2.default.bold("Auto Includes:")} ${info.suggestedIncludes.join(", ")}`);
2571
+ console.log(` ${import_picocolors2.default.bold("Auto Ignores:")} ${info.suggestedIgnores.slice(0, 5).join(", ")} (+${Math.max(0, info.suggestedIgnores.length - 5)} more)`);
2572
+ console.log("");
2573
+ });
2264
2574
  program.parse(process.argv);
2265
2575
  //# sourceMappingURL=cli.cjs.map