@kinetixui/cli 0.14.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 +455 -21
  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 pc7 from "picocolors";
5
+ import pc9 from "picocolors";
6
6
 
7
7
  // src/commands/add.ts
8
8
  import { existsSync as existsSync4 } from "fs";
@@ -248,11 +248,11 @@ async function doctor() {
248
248
  levels.push(report("warn", `"${alias}" alias points to ${path5.relative(cwd, dir) || "."}, which doesn't exist yet.`));
249
249
  }
250
250
  }
251
- const cssPath = path5.join(cwd, config.srcDir ? "src" : "", config.tailwind.css);
252
- if (existsSync5(cssPath)) {
253
- levels.push(report("pass", `Tailwind CSS file found at ${path5.relative(cwd, cssPath)}`));
251
+ const cssPath2 = path5.join(cwd, config.srcDir ? "src" : "", config.tailwind.css);
252
+ if (existsSync5(cssPath2)) {
253
+ levels.push(report("pass", `Tailwind CSS file found at ${path5.relative(cwd, cssPath2)}`));
254
254
  } else {
255
- levels.push(report("warn", `Tailwind CSS file not found at ${path5.relative(cwd, cssPath)} \u2014 the token contract has nowhere to land.`));
255
+ levels.push(report("warn", `Tailwind CSS file not found at ${path5.relative(cwd, cssPath2)} \u2014 the token contract has nowhere to land.`));
256
256
  }
257
257
  }
258
258
  let registryNames = null;
@@ -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,21 +606,271 @@ 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();
458
617
  console.log(`${rows.length} of ${components.length} component(s) shown.`);
459
618
  }
460
619
 
620
+ // src/commands/theme.ts
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";
625
+
626
+ // src/lib/color-math.ts
627
+ function hexToRgb(hex) {
628
+ const h = hex.replace("#", "");
629
+ const n = parseInt(h.length === 3 ? h.replace(/(.)/g, "$1$1") : h, 16);
630
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
631
+ }
632
+ function rgbToHsl(r, g, b) {
633
+ r /= 255, g /= 255, b /= 255;
634
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
635
+ let h = 0, s = 0;
636
+ const l = (max + min) / 2;
637
+ const d = max - min;
638
+ if (d !== 0) {
639
+ s = d / (1 - Math.abs(2 * l - 1));
640
+ switch (max) {
641
+ case r:
642
+ h = (g - b) / d % 6;
643
+ break;
644
+ case g:
645
+ h = (b - r) / d + 2;
646
+ break;
647
+ default:
648
+ h = (r - g) / d + 4;
649
+ }
650
+ h *= 60;
651
+ if (h < 0) h += 360;
652
+ }
653
+ return [Math.round(h), Math.round(s * 100), Math.round(l * 100)];
654
+ }
655
+ function hexToHslChannels(hex) {
656
+ const [h, s, l] = rgbToHsl(...hexToRgb(hex));
657
+ return `${h} ${s}% ${l}%`;
658
+ }
659
+ function relLuminance([r, g, b]) {
660
+ const [cr, cg, cb] = [r, g, b].map((v) => {
661
+ const s = v / 255;
662
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
663
+ });
664
+ return 0.2126 * cr + 0.7152 * cg + 0.0722 * cb;
665
+ }
666
+ function contrastRatio(hexA, hexB) {
667
+ const la = relLuminance(hexToRgb(hexA)) + 0.05;
668
+ const lb = relLuminance(hexToRgb(hexB)) + 0.05;
669
+ return la > lb ? la / lb : lb / la;
670
+ }
671
+ function bestTextHex(bgHex) {
672
+ return contrastRatio(bgHex, "#ffffff") >= contrastRatio(bgHex, "#000000") ? "#ffffff" : "#000000";
673
+ }
674
+ function isHex(v) {
675
+ return /^#?[0-9a-fA-F]{6}$/.test(v.trim());
676
+ }
677
+ function normalizeHex(v) {
678
+ const t = v.trim().replace(/^#/, "");
679
+ return `#${t.toLowerCase()}`;
680
+ }
681
+
682
+ // src/lib/theme.ts
683
+ var ACCEPTED_TOKENS = [
684
+ "background",
685
+ "foreground",
686
+ "card",
687
+ "card-foreground",
688
+ "popover",
689
+ "popover-foreground",
690
+ "primary",
691
+ "primary-foreground",
692
+ "secondary",
693
+ "secondary-foreground",
694
+ "muted",
695
+ "muted-foreground",
696
+ "accent",
697
+ "accent-foreground",
698
+ "destructive",
699
+ "destructive-foreground",
700
+ "border",
701
+ "input",
702
+ "ring"
703
+ ];
704
+ var EXAMPLE_VALUES = {
705
+ background: "#ffffff",
706
+ foreground: "#0f172a",
707
+ card: "#ffffff",
708
+ "card-foreground": "#0f172a",
709
+ popover: "#ffffff",
710
+ "popover-foreground": "#0f172a",
711
+ primary: "#2563eb",
712
+ "primary-foreground": "#ffffff",
713
+ secondary: "#f1f5f9",
714
+ "secondary-foreground": "#0f172a",
715
+ muted: "#f1f5f9",
716
+ "muted-foreground": "#64748b",
717
+ accent: "#f1f5f9",
718
+ "accent-foreground": "#0f172a",
719
+ destructive: "#dc2626",
720
+ "destructive-foreground": "#ffffff",
721
+ border: "#e2e8f0",
722
+ input: "#e2e8f0",
723
+ ring: "#2563eb"
724
+ };
725
+ var FOREGROUND_OF = {
726
+ background: "foreground",
727
+ card: "card-foreground",
728
+ popover: "popover-foreground",
729
+ primary: "primary-foreground",
730
+ secondary: "secondary-foreground",
731
+ muted: "muted-foreground",
732
+ accent: "accent-foreground",
733
+ destructive: "destructive-foreground"
734
+ };
735
+ var CONTRAST_PAIRS = [
736
+ ["background", "foreground"],
737
+ ["card", "card-foreground"],
738
+ ["popover", "popover-foreground"],
739
+ ["primary", "primary-foreground"],
740
+ ["secondary", "secondary-foreground"],
741
+ ["muted", "muted-foreground"],
742
+ ["accent", "accent-foreground"],
743
+ ["destructive", "destructive-foreground"]
744
+ ];
745
+ var isAccepted = (t) => ACCEPTED_TOKENS.includes(t);
746
+ function parseThemeFile(text) {
747
+ const values = {};
748
+ const errors = [];
749
+ const unknownTokens = [];
750
+ const lines = text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
751
+ lines.forEach((line, i) => {
752
+ const cells = line.split(/[,\t]/).map((c) => c.trim().replace(/^"|"$/g, ""));
753
+ if (cells.length < 2) {
754
+ errors.push(`Line ${i + 1}: expected "token,hex" \u2014 got "${line}"`);
755
+ return;
756
+ }
757
+ const [rawToken, rawHex] = cells;
758
+ const token = rawToken.toLowerCase().replace(/^--/, "").replace(/\s+/g, "-");
759
+ if (!isHex(rawHex)) {
760
+ errors.push(`Line ${i + 1}: "${rawHex}" isn't a hex color`);
761
+ return;
762
+ }
763
+ if (!isAccepted(token)) {
764
+ unknownTokens.push(rawToken);
765
+ return;
766
+ }
767
+ values[token] = normalizeHex(rawHex);
768
+ });
769
+ return { values, errors, unknownTokens };
770
+ }
771
+ function deriveForegrounds(values) {
772
+ const out = { ...values };
773
+ for (const [base, fg] of Object.entries(FOREGROUND_OF)) {
774
+ if (out[base] && !out[fg]) out[fg] = bestTextHex(out[base]);
775
+ }
776
+ return out;
777
+ }
778
+ function toCssBlock(values) {
779
+ const lines = ACCEPTED_TOKENS.filter((t) => values[t]).map((t) => ` --${t}: ${hexToHslChannels(values[t])};`);
780
+ return `:root {
781
+ ${lines.join("\n")}
782
+ }
783
+ `;
784
+ }
785
+ function checkContrast(values) {
786
+ return CONTRAST_PAIRS.filter(([bg, fg]) => values[bg] && values[fg]).map(([bg, fg]) => {
787
+ const ratio = contrastRatio(values[bg], values[fg]);
788
+ return { pair: [bg, fg], ratio, pass: ratio >= 4.5 };
789
+ });
790
+ }
791
+ function scaffoldThemeFile(name) {
792
+ const lines = [
793
+ `# ${name} \u2014 KinetixUI theme override`,
794
+ "#",
795
+ "# One `token,hex` pair per line. Uncomment (remove the leading `# `) and",
796
+ "# edit any token you want to override; leave the rest commented out to",
797
+ "# keep KinetixUI's default. Run `kinetixui theme build " + name + "` to",
798
+ `# compile this into ${name}.css and get a WCAG AA contrast report.`,
799
+ "#",
800
+ "# Setting a base color (e.g. `primary`) without its `-foreground` pair",
801
+ "# auto-derives the foreground by contrast (best of white/black) \u2014 you",
802
+ "# don't have to set both unless you want a specific foreground.",
803
+ "#"
804
+ ];
805
+ for (const token of ACCEPTED_TOKENS) {
806
+ lines.push(`# ${token},${EXAMPLE_VALUES[token]}`);
807
+ }
808
+ return lines.join("\n") + "\n";
809
+ }
810
+
811
+ // src/commands/theme.ts
812
+ var THEME_DIR = "kinetixui-themes";
813
+ function csvPath(cwd, name) {
814
+ return path9.join(cwd, THEME_DIR, `${name}.csv`);
815
+ }
816
+ function cssPath(cwd, name) {
817
+ return path9.join(cwd, THEME_DIR, `${name}.css`);
818
+ }
819
+ async function themeCreate(name, options) {
820
+ const cwd = process.cwd();
821
+ const file = csvPath(cwd, name);
822
+ if (existsSync9(file) && !options.force) {
823
+ throw new Error(`${path9.relative(cwd, file)} already exists. Pass --force to overwrite it.`);
824
+ }
825
+ await mkdir2(path9.dirname(file), { recursive: true });
826
+ await writeFile3(file, scaffoldThemeFile(name), "utf8");
827
+ console.log(pc8.green("\u2714"), `Wrote ${path9.relative(cwd, file)}`);
828
+ console.log(`Uncomment and edit the tokens you want to override, then run:`);
829
+ console.log(pc8.dim(` kinetixui theme build ${name}`));
830
+ }
831
+ async function themeBuild(name, options) {
832
+ const cwd = process.cwd();
833
+ const src = csvPath(cwd, name);
834
+ if (!existsSync9(src)) {
835
+ throw new Error(`${path9.relative(cwd, src)} doesn't exist. Run "kinetixui theme create ${name}" first.`);
836
+ }
837
+ const text = await readFile3(src, "utf8");
838
+ const { values: parsed, errors, unknownTokens } = parseThemeFile(text);
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.`);
841
+ const values = deriveForegrounds(parsed);
842
+ const setCount = Object.keys(values).length;
843
+ if (setCount === 0) {
844
+ throw new Error(`No tokens set in ${path9.relative(cwd, src)} \u2014 nothing to build. Uncomment at least one line.`);
845
+ }
846
+ const dest = cssPath(cwd, name);
847
+ await writeFile3(dest, toCssBlock(values), "utf8");
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.`);
850
+ console.log();
851
+ console.log(pc8.dim("WCAG AA contrast (4.5:1)"));
852
+ const results = checkContrast(values);
853
+ let failed = 0;
854
+ for (const { pair, ratio, pass } of results) {
855
+ const mark = pass ? pc8.green("\u2714") : pc8.red("\u2716");
856
+ if (!pass) failed++;
857
+ console.log(`${mark} ${pair[0]} / ${pair[1]}`, pc8.dim(`${ratio.toFixed(2)}:1`));
858
+ }
859
+ if (results.length === 0) {
860
+ console.log(pc8.dim("(no checkable pairs \u2014 set at least one base + foreground, or just a base to auto-derive one)"));
861
+ }
862
+ console.log();
863
+ console.log("Native (SwiftUI/Compose/Flutter) theme output isn't built yet \u2014 this compiles to CSS only.");
864
+ console.log(pc8.dim("See https://kinetixui.com/docs/cli#theme for the current scope."));
865
+ if (failed > 0 && options.failOnContrast) {
866
+ process.exitCode = 1;
867
+ }
868
+ }
869
+
461
870
  // package.json
462
871
  var package_default = {
463
872
  name: "@kinetixui/cli",
464
- version: "0.14.0",
873
+ version: "0.16.0",
465
874
  description: "Add KinetixUI components and tokens to your project.",
466
875
  type: "module",
467
876
  license: "MIT",
@@ -505,7 +914,7 @@ program.command("init").description("Set up kinetixui.json and pull in the token
505
914
  try {
506
915
  await init({ registry: opts.registry, yes: opts.yes });
507
916
  } catch (err) {
508
- console.error(pc7.red("\u2716"), err.message);
917
+ console.error(pc9.red("\u2716"), err.message);
509
918
  process.exitCode = 1;
510
919
  }
511
920
  });
@@ -513,7 +922,7 @@ program.command("add").description("Add one or more components to your project."
513
922
  try {
514
923
  await add(components, { registry: opts.registry, overwrite: opts.overwrite, all: opts.all });
515
924
  } catch (err) {
516
- console.error(pc7.red("\u2716"), err.message);
925
+ console.error(pc9.red("\u2716"), err.message);
517
926
  process.exitCode = 1;
518
927
  }
519
928
  });
@@ -521,7 +930,7 @@ program.command("list").description("List every component available in the regis
521
930
  try {
522
931
  await list({ registry: opts.registry });
523
932
  } catch (err) {
524
- console.error(pc7.red("\u2716"), err.message);
933
+ console.error(pc9.red("\u2716"), err.message);
525
934
  process.exitCode = 1;
526
935
  }
527
936
  });
@@ -529,7 +938,7 @@ program.command("inspect").description("Show everything the registry knows about
529
938
  try {
530
939
  await inspect(name, { registry: opts.registry });
531
940
  } catch (err) {
532
- console.error(pc7.red("\u2716"), err.message);
941
+ console.error(pc9.red("\u2716"), err.message);
533
942
  process.exitCode = 1;
534
943
  }
535
944
  });
@@ -537,7 +946,15 @@ program.command("parity").description("Show which platforms carry each component
537
946
  try {
538
947
  await parity(components, { registry: opts.registry });
539
948
  } catch (err) {
540
- console.error(pc7.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);
541
958
  process.exitCode = 1;
542
959
  }
543
960
  });
@@ -545,7 +962,24 @@ program.command("doctor").description("Check kinetixui.json, its aliases, and th
545
962
  try {
546
963
  await doctor();
547
964
  } catch (err) {
548
- console.error(pc7.red("\u2716"), err.message);
965
+ console.error(pc9.red("\u2716"), err.message);
966
+ process.exitCode = 1;
967
+ }
968
+ });
969
+ var theme = program.command("theme").description("Scaffold and compile a local token override (CSS only \u2014 see 'theme build --help').");
970
+ theme.command("create").description("Scaffold kinetixui-themes/<name>.csv with every overridable token, commented out.").argument("<name>", "theme name, e.g. acme").option("-f, --force", "overwrite an existing theme file", false).action(async (name, opts) => {
971
+ try {
972
+ await themeCreate(name, { force: opts.force });
973
+ } catch (err) {
974
+ console.error(pc9.red("\u2716"), err.message);
975
+ process.exitCode = 1;
976
+ }
977
+ });
978
+ theme.command("build").description("Compile kinetixui-themes/<name>.csv to CSS and print a WCAG AA contrast report. CSS only \u2014 no native (SwiftUI/Compose/Flutter) output yet.").argument("<name>", "theme name, e.g. acme").option("--no-fail-on-contrast", "exit 0 even if a pair fails WCAG AA").action(async (name, opts) => {
979
+ try {
980
+ await themeBuild(name, { failOnContrast: opts.failOnContrast });
981
+ } catch (err) {
982
+ console.error(pc9.red("\u2716"), err.message);
549
983
  process.exitCode = 1;
550
984
  }
551
985
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinetixui/cli",
3
- "version": "0.14.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",