@kinetixui/cli 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +210 -43
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import pc8 from "picocolors";
5
+ import pc9 from "picocolors";
6
6
 
7
7
  // src/commands/add.ts
8
8
  import { existsSync as existsSync4 } from "fs";
@@ -412,24 +412,183 @@ async function inspect(name, options) {
412
412
  console.log(`Add it: ${pc4.cyan(`npx @kinetixui/cli add ${item.name}`)}`);
413
413
  }
414
414
 
415
- // src/commands/list.ts
415
+ // src/commands/lint.ts
416
+ import { existsSync as existsSync8 } from "fs";
417
+ import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
418
+ import path8 from "path";
416
419
  import pc5 from "picocolors";
420
+
421
+ // src/lib/lint.ts
422
+ function stripComments(text) {
423
+ const noBlocks = text.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
424
+ return noBlocks.replace(/(^|[^:])\/\/.*$/gm, (_m, pre) => pre);
425
+ }
426
+ var HEX = "(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})";
427
+ var COLOR_UTILITY_PREFIXES = [
428
+ "bg",
429
+ "text",
430
+ "border",
431
+ "fill",
432
+ "stroke",
433
+ "ring",
434
+ "from",
435
+ "via",
436
+ "to",
437
+ "outline",
438
+ "decoration",
439
+ "caret",
440
+ "accent",
441
+ "shadow",
442
+ "divide"
443
+ ];
444
+ var COLOR_PATTERNS = [
445
+ // bg-[#1a2b3c], text-[#fff], ring-[#1a2b3c80] …
446
+ new RegExp(`\\b(?:${COLOR_UTILITY_PREFIXES.join("|")})-\\[#${HEX}\\]`, "g"),
447
+ // style={{ color: "#1a2b3c" }} / a plain CSS declaration `color: #1a2b3c;`
448
+ new RegExp(`:\\s*["']?#${HEX}["']?`, "g")
449
+ ];
450
+ var SPACING_UTILITY_PATTERN = "-?(?:p|m)[trblxy]?|gap(?:-[xy])?|space-[xy]|inset(?:-[xy])?|top|right|bottom|left";
451
+ var SPACING_VALUE = "-?[\\d.]+(?:px|rem|em|%)?";
452
+ var SPACING_PATTERNS = [
453
+ new RegExp(`\\b(?:${SPACING_UTILITY_PATTERN})-\\[${SPACING_VALUE}\\]`, "g")
454
+ ];
455
+ function lineOf(text, index) {
456
+ let line = 1;
457
+ for (let i = 0; i < index; i++) if (text.charCodeAt(i) === 10) line++;
458
+ return line;
459
+ }
460
+ function snippetAround(text, index, length) {
461
+ const lineStart = text.lastIndexOf("\n", index) + 1;
462
+ let lineEnd = text.indexOf("\n", index);
463
+ if (lineEnd === -1) lineEnd = text.length;
464
+ return text.slice(lineStart, lineEnd).trim().slice(0, 120) || text.slice(index, index + length);
465
+ }
466
+ function lintText(file, rawText) {
467
+ const text = stripComments(rawText);
468
+ const violations = [];
469
+ const seen = /* @__PURE__ */ new Set();
470
+ const scan = (patterns, kind) => {
471
+ for (const pattern of patterns) {
472
+ pattern.lastIndex = 0;
473
+ let match;
474
+ while (match = pattern.exec(text)) {
475
+ const line = lineOf(text, match.index);
476
+ const key = `${kind}:${line}:${match.index}`;
477
+ if (!seen.has(key)) {
478
+ seen.add(key);
479
+ violations.push({ file, line, kind, snippet: snippetAround(text, match.index, match[0].length) });
480
+ }
481
+ if (match[0].length === 0) pattern.lastIndex++;
482
+ }
483
+ }
484
+ };
485
+ scan(COLOR_PATTERNS, "color");
486
+ scan(SPACING_PATTERNS, "spacing");
487
+ violations.sort((a, b) => a.line - b.line);
488
+ return violations;
489
+ }
490
+ var LINT_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js", ".css"];
491
+
492
+ // src/commands/lint.ts
493
+ var SUGGESTION = {
494
+ color: "consider a semantic token utility instead (e.g. bg-primary, text-foreground)",
495
+ spacing: "consider a token-backed spacing utility instead (e.g. p-4, gap-2)"
496
+ };
497
+ async function collectFiles(dir) {
498
+ if (!existsSync8(dir)) return [];
499
+ const files = [];
500
+ const walk = async (current) => {
501
+ const entries = await readdir2(current, { withFileTypes: true });
502
+ for (const entry of entries) {
503
+ const full = path8.join(current, entry.name);
504
+ if (entry.isDirectory()) {
505
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
506
+ await walk(full);
507
+ } else if (entry.isFile() && LINT_EXTENSIONS.includes(path8.extname(entry.name))) {
508
+ files.push(full);
509
+ }
510
+ }
511
+ };
512
+ await walk(dir);
513
+ return files;
514
+ }
515
+ async function lint(targetArg, options) {
516
+ const cwd = process.cwd();
517
+ let dirs;
518
+ if (targetArg) {
519
+ dirs = [path8.resolve(cwd, targetArg)];
520
+ } else {
521
+ if (!existsSync8(path8.join(cwd, CONFIG_FILE))) {
522
+ throw new Error(`No ${CONFIG_FILE} found \u2014 run "kinetixui init" first, or pass a path: kinetixui lint <path>`);
523
+ }
524
+ const config = await readConfig(cwd);
525
+ if (!config) throw new Error(`${CONFIG_FILE} exists but isn't valid JSON.`);
526
+ dirs = [resolveAliasDir(cwd, config, "components"), resolveAliasDir(cwd, config, "ui")];
527
+ }
528
+ const seenFiles = /* @__PURE__ */ new Set();
529
+ const files = [];
530
+ for (const dir of dirs) {
531
+ for (const file of await collectFiles(dir)) {
532
+ if (!seenFiles.has(file)) {
533
+ seenFiles.add(file);
534
+ files.push(file);
535
+ }
536
+ }
537
+ }
538
+ if (files.length === 0) {
539
+ console.log(pc5.dim(`No ${LINT_EXTENSIONS.join("/")} files found in ${dirs.map((d) => path8.relative(cwd, d) || ".").join(", ")}.`));
540
+ return;
541
+ }
542
+ let totalViolations = 0;
543
+ let filesWithViolations = 0;
544
+ for (const file of files) {
545
+ const text = await readFile2(file, "utf8");
546
+ const violations = lintText(file, text);
547
+ if (violations.length === 0) continue;
548
+ filesWithViolations++;
549
+ const rel = path8.relative(cwd, file);
550
+ for (const v of violations) {
551
+ totalViolations++;
552
+ console.log(
553
+ `${pc5.yellow("!")} ${pc5.bold(`${rel}:${v.line}`)} \u2014 hardcoded ${v.kind}: ${pc5.dim(v.snippet)}`
554
+ );
555
+ console.log(` ${pc5.dim(SUGGESTION[v.kind])}`);
556
+ }
557
+ }
558
+ console.log();
559
+ if (totalViolations === 0) {
560
+ console.log(pc5.green("\u2714"), `${files.length} file${files.length === 1 ? "" : "s"} scanned, no hardcoded colors or spacing found.`);
561
+ } else {
562
+ console.log(
563
+ `${totalViolations} violation${totalViolations === 1 ? "" : "s"} in ${filesWithViolations} of ${files.length} file${files.length === 1 ? "" : "s"} scanned.`
564
+ );
565
+ console.log(
566
+ pc5.dim(
567
+ "Checks unknown/deprecated tokens, accessibility violations and cross-platform inconsistencies aren't covered yet \u2014 see https://kinetixui.com/docs/cli#lint."
568
+ )
569
+ );
570
+ if (options.fail) process.exitCode = 1;
571
+ }
572
+ }
573
+
574
+ // src/commands/list.ts
575
+ import pc6 from "picocolors";
417
576
  async function list(options) {
418
577
  const items = await fetchRegistryIndex(options.registry);
419
578
  const components = items.filter((item) => item.type === "registry:ui").sort((a, b) => a.name.localeCompare(b.name));
420
579
  console.log(`${components.length} component(s):`);
421
580
  console.log();
422
581
  for (const item of components) {
423
- const description = item.description ? pc5.dim(` \u2014 ${item.description}`) : "";
424
- console.log(` ${pc5.cyan(item.name)}${description}`);
582
+ const description = item.description ? pc6.dim(` \u2014 ${item.description}`) : "";
583
+ console.log(` ${pc6.cyan(item.name)}${description}`);
425
584
  }
426
585
  console.log();
427
- console.log(`Add one: ${pc5.cyan("npx @kinetixui/cli add <name>")}`);
428
- console.log(`Add everything: ${pc5.cyan("npx @kinetixui/cli add --all")}`);
586
+ console.log(`Add one: ${pc6.cyan("npx @kinetixui/cli add <name>")}`);
587
+ console.log(`Add everything: ${pc6.cyan("npx @kinetixui/cli add --all")}`);
429
588
  }
430
589
 
431
590
  // src/commands/parity.ts
432
- import pc6 from "picocolors";
591
+ import pc7 from "picocolors";
433
592
  var PLATFORMS = ["React", "SwiftUI", "Compose", "Flutter"];
434
593
  var PLATFORM_ABBR = {
435
594
  React: "RE",
@@ -447,11 +606,11 @@ async function parity(names, options) {
447
606
  }) : components;
448
607
  const nameWidth = Math.max(9, ...rows.map((r) => r.name.length));
449
608
  const header = `${"COMPONENT".padEnd(nameWidth)} ${PLATFORMS.map((p) => PLATFORM_ABBR[p]).join(" ")} STATUS`;
450
- console.log(pc6.dim(header));
609
+ console.log(pc7.dim(header));
451
610
  for (const item of rows) {
452
611
  const on = new Set(item.platforms ?? PLATFORMS);
453
- const marks = PLATFORMS.map((p) => on.has(p) ? pc6.green("\u2713 ") : pc6.dim("\u2014 "));
454
- const status = item.status ? pc6.yellow(item.status) : "";
612
+ const marks = PLATFORMS.map((p) => on.has(p) ? pc7.green("\u2713 ") : pc7.dim("\u2014 "));
613
+ const status = item.status ? pc7.yellow(item.status) : "";
455
614
  console.log(`${item.name.padEnd(nameWidth)} ${marks.join(" ")} ${status}`);
456
615
  }
457
616
  console.log();
@@ -459,10 +618,10 @@ async function parity(names, options) {
459
618
  }
460
619
 
461
620
  // src/commands/theme.ts
462
- import { existsSync as existsSync8 } from "fs";
463
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
464
- import path8 from "path";
465
- import pc7 from "picocolors";
621
+ import { existsSync as existsSync9 } from "fs";
622
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
623
+ import path9 from "path";
624
+ import pc8 from "picocolors";
466
625
 
467
626
  // src/lib/color-math.ts
468
627
  function hexToRgb(hex) {
@@ -652,57 +811,57 @@ function scaffoldThemeFile(name) {
652
811
  // src/commands/theme.ts
653
812
  var THEME_DIR = "kinetixui-themes";
654
813
  function csvPath(cwd, name) {
655
- return path8.join(cwd, THEME_DIR, `${name}.csv`);
814
+ return path9.join(cwd, THEME_DIR, `${name}.csv`);
656
815
  }
657
816
  function cssPath(cwd, name) {
658
- return path8.join(cwd, THEME_DIR, `${name}.css`);
817
+ return path9.join(cwd, THEME_DIR, `${name}.css`);
659
818
  }
660
819
  async function themeCreate(name, options) {
661
820
  const cwd = process.cwd();
662
821
  const file = csvPath(cwd, name);
663
- if (existsSync8(file) && !options.force) {
664
- throw new Error(`${path8.relative(cwd, file)} already exists. Pass --force to overwrite it.`);
822
+ if (existsSync9(file) && !options.force) {
823
+ throw new Error(`${path9.relative(cwd, file)} already exists. Pass --force to overwrite it.`);
665
824
  }
666
- await mkdir2(path8.dirname(file), { recursive: true });
825
+ await mkdir2(path9.dirname(file), { recursive: true });
667
826
  await writeFile3(file, scaffoldThemeFile(name), "utf8");
668
- console.log(pc7.green("\u2714"), `Wrote ${path8.relative(cwd, file)}`);
827
+ console.log(pc8.green("\u2714"), `Wrote ${path9.relative(cwd, file)}`);
669
828
  console.log(`Uncomment and edit the tokens you want to override, then run:`);
670
- console.log(pc7.dim(` kinetixui theme build ${name}`));
829
+ console.log(pc8.dim(` kinetixui theme build ${name}`));
671
830
  }
672
831
  async function themeBuild(name, options) {
673
832
  const cwd = process.cwd();
674
833
  const src = csvPath(cwd, name);
675
- if (!existsSync8(src)) {
676
- throw new Error(`${path8.relative(cwd, src)} doesn't exist. Run "kinetixui theme create ${name}" first.`);
834
+ if (!existsSync9(src)) {
835
+ throw new Error(`${path9.relative(cwd, src)} doesn't exist. Run "kinetixui theme create ${name}" first.`);
677
836
  }
678
- const text = await readFile2(src, "utf8");
837
+ const text = await readFile3(src, "utf8");
679
838
  const { values: parsed, errors, unknownTokens } = parseThemeFile(text);
680
- for (const err of errors) console.log(pc7.yellow("!"), err);
681
- for (const unknown of unknownTokens) console.log(pc7.yellow("!"), `"${unknown}" isn't a known token \u2014 skipped.`);
839
+ for (const err of errors) console.log(pc8.yellow("!"), err);
840
+ for (const unknown of unknownTokens) console.log(pc8.yellow("!"), `"${unknown}" isn't a known token \u2014 skipped.`);
682
841
  const values = deriveForegrounds(parsed);
683
842
  const setCount = Object.keys(values).length;
684
843
  if (setCount === 0) {
685
- throw new Error(`No tokens set in ${path8.relative(cwd, src)} \u2014 nothing to build. Uncomment at least one line.`);
844
+ throw new Error(`No tokens set in ${path9.relative(cwd, src)} \u2014 nothing to build. Uncomment at least one line.`);
686
845
  }
687
846
  const dest = cssPath(cwd, name);
688
847
  await writeFile3(dest, toCssBlock(values), "utf8");
689
- console.log(pc7.green("\u2714"), `Wrote ${path8.relative(cwd, dest)} (${setCount} token${setCount === 1 ? "" : "s"})`);
690
- console.log(`Paste it after ${pc7.dim('@import "@kinetixui/tokens/css";')} in your global CSS.`);
848
+ console.log(pc8.green("\u2714"), `Wrote ${path9.relative(cwd, dest)} (${setCount} token${setCount === 1 ? "" : "s"})`);
849
+ console.log(`Paste it after ${pc8.dim('@import "@kinetixui/tokens/css";')} in your global CSS.`);
691
850
  console.log();
692
- console.log(pc7.dim("WCAG AA contrast (4.5:1)"));
851
+ console.log(pc8.dim("WCAG AA contrast (4.5:1)"));
693
852
  const results = checkContrast(values);
694
853
  let failed = 0;
695
854
  for (const { pair, ratio, pass } of results) {
696
- const mark = pass ? pc7.green("\u2714") : pc7.red("\u2716");
855
+ const mark = pass ? pc8.green("\u2714") : pc8.red("\u2716");
697
856
  if (!pass) failed++;
698
- console.log(`${mark} ${pair[0]} / ${pair[1]}`, pc7.dim(`${ratio.toFixed(2)}:1`));
857
+ console.log(`${mark} ${pair[0]} / ${pair[1]}`, pc8.dim(`${ratio.toFixed(2)}:1`));
699
858
  }
700
859
  if (results.length === 0) {
701
- console.log(pc7.dim("(no checkable pairs \u2014 set at least one base + foreground, or just a base to auto-derive one)"));
860
+ console.log(pc8.dim("(no checkable pairs \u2014 set at least one base + foreground, or just a base to auto-derive one)"));
702
861
  }
703
862
  console.log();
704
863
  console.log("Native (SwiftUI/Compose/Flutter) theme output isn't built yet \u2014 this compiles to CSS only.");
705
- console.log(pc7.dim("See https://kinetixui.com/docs/cli#theme for the current scope."));
864
+ console.log(pc8.dim("See https://kinetixui.com/docs/cli#theme for the current scope."));
706
865
  if (failed > 0 && options.failOnContrast) {
707
866
  process.exitCode = 1;
708
867
  }
@@ -711,7 +870,7 @@ async function themeBuild(name, options) {
711
870
  // package.json
712
871
  var package_default = {
713
872
  name: "@kinetixui/cli",
714
- version: "0.15.0",
873
+ version: "0.16.0",
715
874
  description: "Add KinetixUI components and tokens to your project.",
716
875
  type: "module",
717
876
  license: "MIT",
@@ -755,7 +914,7 @@ program.command("init").description("Set up kinetixui.json and pull in the token
755
914
  try {
756
915
  await init({ registry: opts.registry, yes: opts.yes });
757
916
  } catch (err) {
758
- console.error(pc8.red("\u2716"), err.message);
917
+ console.error(pc9.red("\u2716"), err.message);
759
918
  process.exitCode = 1;
760
919
  }
761
920
  });
@@ -763,7 +922,7 @@ program.command("add").description("Add one or more components to your project."
763
922
  try {
764
923
  await add(components, { registry: opts.registry, overwrite: opts.overwrite, all: opts.all });
765
924
  } catch (err) {
766
- console.error(pc8.red("\u2716"), err.message);
925
+ console.error(pc9.red("\u2716"), err.message);
767
926
  process.exitCode = 1;
768
927
  }
769
928
  });
@@ -771,7 +930,7 @@ program.command("list").description("List every component available in the regis
771
930
  try {
772
931
  await list({ registry: opts.registry });
773
932
  } catch (err) {
774
- console.error(pc8.red("\u2716"), err.message);
933
+ console.error(pc9.red("\u2716"), err.message);
775
934
  process.exitCode = 1;
776
935
  }
777
936
  });
@@ -779,7 +938,7 @@ program.command("inspect").description("Show everything the registry knows about
779
938
  try {
780
939
  await inspect(name, { registry: opts.registry });
781
940
  } catch (err) {
782
- console.error(pc8.red("\u2716"), err.message);
941
+ console.error(pc9.red("\u2716"), err.message);
783
942
  process.exitCode = 1;
784
943
  }
785
944
  });
@@ -787,7 +946,15 @@ program.command("parity").description("Show which platforms carry each component
787
946
  try {
788
947
  await parity(components, { registry: opts.registry });
789
948
  } catch (err) {
790
- console.error(pc8.red("\u2716"), err.message);
949
+ console.error(pc9.red("\u2716"), err.message);
950
+ process.exitCode = 1;
951
+ }
952
+ });
953
+ program.command("lint").description("Scan for hardcoded colors and spacing that should be semantic tokens.").argument("[path]", "directory to scan \u2014 defaults to the components/ui aliases from kinetixui.json").option("--no-fail", "exit 0 even if violations are found").action(async (target, opts) => {
954
+ try {
955
+ await lint(target, { fail: opts.fail });
956
+ } catch (err) {
957
+ console.error(pc9.red("\u2716"), err.message);
791
958
  process.exitCode = 1;
792
959
  }
793
960
  });
@@ -795,7 +962,7 @@ program.command("doctor").description("Check kinetixui.json, its aliases, and th
795
962
  try {
796
963
  await doctor();
797
964
  } catch (err) {
798
- console.error(pc8.red("\u2716"), err.message);
965
+ console.error(pc9.red("\u2716"), err.message);
799
966
  process.exitCode = 1;
800
967
  }
801
968
  });
@@ -804,7 +971,7 @@ theme.command("create").description("Scaffold kinetixui-themes/<name>.csv with e
804
971
  try {
805
972
  await themeCreate(name, { force: opts.force });
806
973
  } catch (err) {
807
- console.error(pc8.red("\u2716"), err.message);
974
+ console.error(pc9.red("\u2716"), err.message);
808
975
  process.exitCode = 1;
809
976
  }
810
977
  });
@@ -812,7 +979,7 @@ theme.command("build").description("Compile kinetixui-themes/<name>.csv to CSS a
812
979
  try {
813
980
  await themeBuild(name, { failOnContrast: opts.failOnContrast });
814
981
  } catch (err) {
815
- console.error(pc8.red("\u2716"), err.message);
982
+ console.error(pc9.red("\u2716"), err.message);
816
983
  process.exitCode = 1;
817
984
  }
818
985
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinetixui/cli",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Add KinetixUI components and tokens to your project.",
5
5
  "type": "module",
6
6
  "license": "MIT",