@expcat/tigercat-cli 1.1.0 → 1.2.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 (3) hide show
  1. package/README.md +110 -6
  2. package/dist/index.js +417 -59
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @expcat/tigercat-cli
2
2
 
3
- CLI tooling for the [Tigercat](https://github.com/expcats/Tigercat) UI component library.
3
+ CLI tooling for the [Tigercat](https://github.com/expcat/Tigercat) UI component library.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,6 +10,84 @@ pnpm add -g @expcat/tigercat-cli
10
10
  npx @expcat/tigercat-cli
11
11
  ```
12
12
 
13
+ ## Usage Examples
14
+
15
+ ### Start a Vue 3 project
16
+
17
+ ```bash
18
+ tigercat create admin-console --template vue3
19
+ cd admin-console
20
+ pnpm install
21
+ pnpm dev
22
+ ```
23
+
24
+ Preview the generated file list without writing files:
25
+
26
+ ```bash
27
+ tigercat create admin-console --template vue3 --dry-run
28
+ ```
29
+
30
+ ### Start a React project
31
+
32
+ ```bash
33
+ tigercat create design-lab --template react
34
+ cd design-lab
35
+ pnpm install
36
+ pnpm dev
37
+ ```
38
+
39
+ ### Add component boilerplate to an existing project
40
+
41
+ Run from a project that already depends on `@expcat/tigercat-vue` or `@expcat/tigercat-react`:
42
+
43
+ ```bash
44
+ tigercat add Button Form Input Select
45
+ tigercat add --framework vue3 --install --snippet src/tigercat-components.ts
46
+ ```
47
+
48
+ When no component names are provided, `add` opens an interactive multi-select prompt.
49
+ The command prints the correct package import, detects missing peer dependencies,
50
+ can install them with `--install`, and creates `src/components/*Demo.vue` or
51
+ `src/components/*Demo.tsx` files when a `src/components` directory exists.
52
+
53
+ ```bash
54
+ tigercat add Button Form Input Select --dry-run
55
+ ```
56
+
57
+ ### Open a temporary playground
58
+
59
+ ```bash
60
+ tigercat playground --template vue3 --port 3456
61
+ tigercat playground --template react --port 3457
62
+ tigercat playground --template react --no-open
63
+ ```
64
+
65
+ Playground files are created under `.tigercat-playground/` in the current working directory.
66
+
67
+ ```bash
68
+ tigercat playground --template react --port 3457 --dry-run
69
+ ```
70
+
71
+ ### Generate API docs from type definitions
72
+
73
+ ```bash
74
+ tigercat generate docs --input packages/core/src/types --output docs/api
75
+ tigercat generate docs --input packages/core/src/types --output docs/api --dry-run
76
+ tigercat generate test Button --framework both
77
+ tigercat generate doc-template Button --output docs/components
78
+ ```
79
+
80
+ ### Check project compatibility
81
+
82
+ ```bash
83
+ tigercat doctor
84
+ tigercat doctor --json
85
+ ```
86
+
87
+ `doctor` verifies `package.json`, Node.js, pnpm, Tailwind CSS, Tigercat peer dependencies,
88
+ template dependency compatibility, and the supported version compatibility matrix. JSON output is
89
+ designed for CI and automated diagnostics.
90
+
13
91
  ## Commands
14
92
 
15
93
  ### `tigercat create <name>`
@@ -19,6 +97,7 @@ Create a new project with Tigercat pre-configured.
19
97
  ```bash
20
98
  tigercat create my-app --template vue3
21
99
  tigercat create my-app --template react
100
+ tigercat create my-app --template vue3 --dry-run
22
101
  ```
23
102
 
24
103
  ### `tigercat add <component>`
@@ -28,6 +107,8 @@ Add a component to your project with import boilerplate.
28
107
  ```bash
29
108
  tigercat add Button
30
109
  tigercat add Form Input Select DatePicker
110
+ tigercat add --framework react --install --snippet src/tigercat-components.ts
111
+ tigercat add Button --dry-run
31
112
  ```
32
113
 
33
114
  ### `tigercat playground`
@@ -37,6 +118,8 @@ Launch an interactive playground for testing components.
37
118
  ```bash
38
119
  tigercat playground
39
120
  tigercat playground --template react
121
+ tigercat playground --template react --no-open
122
+ tigercat playground --template react --dry-run
40
123
  ```
41
124
 
42
125
  ### `tigercat generate docs`
@@ -46,6 +129,26 @@ Generate API documentation from component type definitions.
46
129
  ```bash
47
130
  tigercat generate docs
48
131
  tigercat generate docs --output ./docs/api
132
+ tigercat generate docs --output ./docs/api --dry-run
133
+ ```
134
+
135
+ ### `tigercat generate test`
136
+
137
+ Generate Vue and/or React starter test templates for a component.
138
+
139
+ ```bash
140
+ tigercat generate test Button --framework both
141
+ tigercat generate test Button --framework vue3
142
+ tigercat generate test Button --framework react --dry-run
143
+ ```
144
+
145
+ ### `tigercat generate doc-template`
146
+
147
+ Generate a component documentation page template.
148
+
149
+ ```bash
150
+ tigercat generate doc-template Button
151
+ tigercat generate doc-template Button --output docs/components --dry-run
49
152
  ```
50
153
 
51
154
  ### `tigercat doctor`
@@ -54,6 +157,7 @@ Check whether the current project has compatible Node, pnpm, Tailwind CSS, Tiger
54
157
 
55
158
  ```bash
56
159
  tigercat doctor
160
+ tigercat doctor --json
57
161
  ```
58
162
 
59
163
  ## Windows Support
@@ -67,11 +171,11 @@ with backslash paths, paths containing spaces, and UNC paths.
67
171
  When installed globally or locally, each package manager creates platform-specific shims
68
172
  so that `tigercat` can be invoked directly from PowerShell, CMD, or Git Bash:
69
173
 
70
- | Package manager | Global install | Shim files created |
71
- | --------------- | ------------------------------------- | -------------------------------- |
72
- | **pnpm** | `pnpm add -g @expcat/tigercat-cli` | `tigercat.cmd`, `tigercat` (sh) |
73
- | **npm** | `npm i -g @expcat/tigercat-cli` | `tigercat.cmd`, `tigercat` (sh), `tigercat.ps1` |
74
- | **bun** | `bun add -g @expcat/tigercat-cli` | `tigercat.cmd`, `tigercat` (sh) |
174
+ | Package manager | Global install | Shim files created |
175
+ | --------------- | ---------------------------------- | ----------------------------------------------- |
176
+ | **pnpm** | `pnpm add -g @expcat/tigercat-cli` | `tigercat.cmd`, `tigercat` (sh) |
177
+ | **npm** | `npm i -g @expcat/tigercat-cli` | `tigercat.cmd`, `tigercat` (sh), `tigercat.ps1` |
178
+ | **bun** | `bun add -g @expcat/tigercat-cli` | `tigercat.cmd`, `tigercat` (sh) |
75
179
 
76
180
  For local (non-global) installs, run via `npx tigercat`, `pnpm exec tigercat`, or
77
181
  `bunx tigercat`. The `#!/usr/bin/env node` shebang in the built output is used by all
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { Command } from 'commander';
3
3
  import prompts from 'prompts';
4
4
  import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
5
5
  import { resolve, join, dirname, basename } from 'path';
6
- import pc from 'picocolors';
6
+ import pc2 from 'picocolors';
7
7
  import { execSync } from 'child_process';
8
8
 
9
9
  // src/constants.ts
@@ -87,19 +87,19 @@ var COMPONENT_CATEGORIES = {
87
87
  };
88
88
  var ALL_COMPONENTS = Object.values(COMPONENT_CATEGORIES).flat();
89
89
  function logSuccess(msg) {
90
- console.log(pc.green("\u2714") + " " + msg);
90
+ console.log(pc2.green("\u2714") + " " + msg);
91
91
  }
92
92
  function logInfo(msg) {
93
- console.log(pc.blue("\u2139") + " " + msg);
93
+ console.log(pc2.blue("\u2139") + " " + msg);
94
94
  }
95
- function logWarn(msg) {
96
- console.log(pc.yellow("\u26A0") + " " + msg);
95
+ function logWarn2(msg) {
96
+ console.log(pc2.yellow("\u26A0") + " " + msg);
97
97
  }
98
98
  function logError(msg) {
99
- console.error(pc.red("\u2716") + " " + msg);
99
+ console.error(pc2.red("\u2716") + " " + msg);
100
100
  }
101
101
  function logStep(step, total, msg) {
102
- console.log(pc.dim(`[${step}/${total}]`) + " " + msg);
102
+ console.log(pc2.dim(`[${step}/${total}]`) + " " + msg);
103
103
  }
104
104
  function ensureDir(dir) {
105
105
  if (!existsSync(dir)) {
@@ -296,7 +296,7 @@ function vue3EnvDts() {
296
296
 
297
297
  declare module '*.vue' {
298
298
  import type { DefineComponent } from 'vue'
299
- const component: DefineComponent<{}, {}, any>
299
+ const component: DefineComponent<{}, {}, unknown>
300
300
  export default component
301
301
  }
302
302
  `;
@@ -529,11 +529,11 @@ html {
529
529
 
530
530
  // src/commands/create.ts
531
531
  function createCreateCommand() {
532
- return new Command("create").argument("<name>", "Project name").option("-t, --template <template>", "Project template (vue3 | react)").description("Create a new project with Tigercat pre-configured").action(async (name, opts) => {
533
- await runCreate(name, opts.template);
532
+ return new Command("create").argument("<name>", "Project name").option("-t, --template <template>", "Project template (vue3 | react)").option("--dry-run", "Preview files without writing them").description("Create a new project with Tigercat pre-configured").action(async (name, opts) => {
533
+ await runCreate(name, opts.template, Boolean(opts.dryRun));
534
534
  });
535
535
  }
536
- async function runCreate(name, templateArg) {
536
+ async function runCreate(name, templateArg, dryRun = false) {
537
537
  let template;
538
538
  if (templateArg && TEMPLATES.includes(templateArg)) {
539
539
  template = templateArg;
@@ -554,7 +554,7 @@ async function runCreate(name, templateArg) {
554
554
  template = response.template;
555
555
  }
556
556
  const targetDir = resolve(process.cwd(), name);
557
- if (existsSync(targetDir) && !isDirEmpty(targetDir)) {
557
+ if (!dryRun && existsSync(targetDir) && !isDirEmpty(targetDir)) {
558
558
  const { overwrite } = await prompts({
559
559
  type: "confirm",
560
560
  name: "overwrite",
@@ -568,6 +568,13 @@ async function runCreate(name, templateArg) {
568
568
  }
569
569
  logInfo(`Creating ${template} project in ${targetDir}...`);
570
570
  const files = template === "vue3" ? getVue3Template(name) : getReactTemplate(name);
571
+ if (dryRun) {
572
+ logInfo("Dry run: no files will be written.");
573
+ for (const filePath of Object.keys(files)) {
574
+ console.log(` ${filePath}`);
575
+ }
576
+ return;
577
+ }
571
578
  const totalSteps = Object.keys(files).length;
572
579
  let step = 0;
573
580
  ensureDir(targetDir);
@@ -584,8 +591,8 @@ async function runCreate(name, templateArg) {
584
591
  console.log(" pnpm dev\n");
585
592
  }
586
593
  function createAddCommand() {
587
- return new Command("add").argument("<components...>", "Component names to add (e.g. Button Input Select)").description("Add component import boilerplate to your project").action(async (components) => {
588
- await runAdd(components);
594
+ return new Command("add").argument("[components...]", "Component names to add (e.g. Button Input Select)").option("-f, --framework <framework>", "Framework override (vue3 | react)").option("--install", "Install missing Tigercat dependencies before generating snippets").option("--snippet <file>", "Generate a reusable import snippet file").option("--dry-run", "Preview generated demo files without writing them").description("Add component import boilerplate to your project").action(async (components, opts) => {
595
+ await runAdd(components ?? [], opts);
589
596
  });
590
597
  }
591
598
  function detectFramework(cwd) {
@@ -600,6 +607,45 @@ function detectFramework(cwd) {
600
607
  }
601
608
  return null;
602
609
  }
610
+ function normalizeFramework(value) {
611
+ if (value === "vue3" || value === "react") return value;
612
+ return null;
613
+ }
614
+ async function resolveComponents(components) {
615
+ if (components.length > 0) return components;
616
+ const response = await prompts({
617
+ type: "multiselect",
618
+ name: "components",
619
+ message: "Select components to add",
620
+ choices: ALL_COMPONENTS.map((component) => ({ title: component, value: component })),
621
+ min: 1
622
+ });
623
+ return response.components ?? [];
624
+ }
625
+ function collectDependencies(framework) {
626
+ return framework === "vue3" ? ["@expcat/tigercat-vue", "@expcat/tigercat-core", "vue"] : ["@expcat/tigercat-react", "@expcat/tigercat-core", "react", "react-dom"];
627
+ }
628
+ function readPackageDeps(cwd) {
629
+ const pkg = readFileSafe(join(cwd, "package.json"));
630
+ if (!pkg) return {};
631
+ try {
632
+ const parsed = JSON.parse(pkg);
633
+ return { ...parsed.dependencies, ...parsed.devDependencies, ...parsed.peerDependencies };
634
+ } catch {
635
+ return {};
636
+ }
637
+ }
638
+ function detectPackageManager(cwd) {
639
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
640
+ if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
641
+ return "npm";
642
+ }
643
+ function formatAddCommand(packageManager, dependencies) {
644
+ const deps = dependencies.join(" ");
645
+ if (packageManager === "yarn") return `yarn add ${deps}`;
646
+ if (packageManager === "npm") return `npm install ${deps}`;
647
+ return `pnpm add ${deps}`;
648
+ }
603
649
  function validateComponents(names) {
604
650
  const valid = [];
605
651
  const invalid = [];
@@ -613,18 +659,20 @@ function validateComponents(names) {
613
659
  }
614
660
  return { valid, invalid };
615
661
  }
616
- async function runAdd(components) {
662
+ async function runAdd(components, options = {}) {
617
663
  const cwd = process.cwd();
618
- const framework = detectFramework(cwd);
664
+ const framework = normalizeFramework(options.framework) ?? detectFramework(cwd);
665
+ const dryRun = Boolean(options.dryRun);
619
666
  if (!framework) {
620
667
  logError(
621
668
  "Could not detect framework. Make sure you are in a project with @expcat/tigercat-vue or @expcat/tigercat-react installed."
622
669
  );
623
670
  process.exit(1);
624
671
  }
625
- const { valid, invalid } = validateComponents(components);
672
+ const selectedComponents = await resolveComponents(components);
673
+ const { valid, invalid } = validateComponents(selectedComponents);
626
674
  if (invalid.length > 0) {
627
- logWarn(`Unknown components: ${invalid.join(", ")}`);
675
+ logWarn2(`Unknown components: ${invalid.join(", ")}`);
628
676
  logInfo(`Available: ${ALL_COMPONENTS.join(", ")}`);
629
677
  }
630
678
  if (valid.length === 0) {
@@ -632,11 +680,34 @@ async function runAdd(components) {
632
680
  process.exit(1);
633
681
  }
634
682
  const pkgName = framework === "vue3" ? "@expcat/tigercat-vue" : "@expcat/tigercat-react";
683
+ const requiredDeps = collectDependencies(framework);
684
+ const installedDeps = readPackageDeps(cwd);
685
+ const missingDeps = requiredDeps.filter((dependency) => !installedDeps[dependency]);
686
+ if (missingDeps.length > 0) {
687
+ const packageManager = detectPackageManager(cwd);
688
+ const installCommand = formatAddCommand(packageManager, missingDeps);
689
+ if (options.install && !dryRun) {
690
+ logInfo(`Installing missing dependencies: ${missingDeps.join(", ")}`);
691
+ execSync(installCommand, { cwd, stdio: "inherit" });
692
+ } else {
693
+ logInfo(`Missing dependencies detected. Run: ${installCommand}`);
694
+ }
695
+ }
635
696
  const importLine = `import { ${valid.join(", ")} } from '${pkgName}'`;
636
697
  logSuccess(`Add this import to your project:
637
698
  `);
638
699
  console.log(` ${importLine}
639
700
  `);
701
+ if (options.snippet) {
702
+ const snippetFile = resolve(cwd, options.snippet);
703
+ const snippet = generateImportSnippet(valid, pkgName);
704
+ if (dryRun) {
705
+ logInfo(`Would create import snippet ${snippetFile}`);
706
+ } else {
707
+ writeFileSafe(snippetFile, snippet);
708
+ logSuccess(`Created import snippet ${snippetFile}`);
709
+ }
710
+ }
640
711
  if (framework === "vue3") {
641
712
  logInfo("Vue 3 usage example:\n");
642
713
  for (const comp of valid) {
@@ -653,11 +724,18 @@ async function runAdd(components) {
653
724
  if (!existsSync(sampleDir)) {
654
725
  return;
655
726
  }
727
+ if (dryRun) {
728
+ logInfo("Dry run: no demo files will be written.");
729
+ }
656
730
  for (const comp of valid) {
657
731
  const ext = framework === "vue3" ? "vue" : "tsx";
658
732
  const sampleFile = join(sampleDir, `${comp}Demo.${ext}`);
659
733
  if (existsSync(sampleFile)) {
660
- logWarn(`${sampleFile} already exists, skipping`);
734
+ logWarn2(`${sampleFile} already exists, skipping`);
735
+ continue;
736
+ }
737
+ if (dryRun) {
738
+ logInfo(`Would create ${sampleFile}`);
661
739
  continue;
662
740
  }
663
741
  const content = framework === "vue3" ? generateVue3Demo(comp, pkgName) : generateReactDemo(comp, pkgName);
@@ -665,6 +743,14 @@ async function runAdd(components) {
665
743
  logSuccess(`Created ${sampleFile}`);
666
744
  }
667
745
  }
746
+ function generateImportSnippet(components, pkg) {
747
+ return `import { ${components.join(", ")} } from '${pkg}'
748
+
749
+ export const tigercatComponents = {
750
+ ${components.map((component) => ` ${component}`).join(",\n")}
751
+ }
752
+ `;
753
+ }
668
754
  function generateVue3Demo(component, pkg) {
669
755
  return `<script setup lang="ts">
670
756
  import { ${component} } from '${pkg}'
@@ -692,11 +778,11 @@ export default function ${component}Demo() {
692
778
  `;
693
779
  }
694
780
  function createPlaygroundCommand() {
695
- return new Command("playground").option("-t, --template <template>", "Framework template (vue3 | react)").option("-p, --port <port>", "Dev server port", "3456").description("Launch an interactive playground for testing components").action(async (opts) => {
696
- await runPlayground(opts.template, opts.port);
781
+ return new Command("playground").option("-t, --template <template>", "Framework template (vue3 | react)").option("-p, --port <port>", "Dev server port", "3456").option("--no-open", "Do not open the playground in the default browser").option("--dry-run", "Preview playground setup without writing files or starting Vite").description("Launch an interactive playground for testing components").action(async (opts) => {
782
+ await runPlayground(opts.template, opts.port, opts.open !== false, Boolean(opts.dryRun));
697
783
  });
698
784
  }
699
- async function runPlayground(templateArg, port = "3456") {
785
+ async function runPlayground(templateArg, port = "3456", open = true, dryRun = false) {
700
786
  let template;
701
787
  if (templateArg && TEMPLATES.includes(templateArg)) {
702
788
  template = templateArg;
@@ -718,6 +804,19 @@ async function runPlayground(templateArg, port = "3456") {
718
804
  }
719
805
  const tmpDir = resolve(process.cwd(), ".tigercat-playground");
720
806
  const projectDir = join(tmpDir, `playground-${template}`);
807
+ if (dryRun) {
808
+ const safePort = /^\d+$/.test(port) ? port : "3456";
809
+ logInfo(`Dry run: would prepare ${template} playground in ${projectDir}.`);
810
+ if (!existsSync(projectDir)) {
811
+ const files = template === "vue3" ? getVue3Template("playground") : getReactTemplate("playground");
812
+ for (const filePath of Object.keys(files)) {
813
+ console.log(` ${filePath}`);
814
+ }
815
+ logInfo("Would run pnpm install");
816
+ }
817
+ logInfo(`Would start Vite on port ${safePort}${open ? " and open the browser" : ""}`);
818
+ return;
819
+ }
721
820
  if (!existsSync(projectDir)) {
722
821
  logInfo(`Setting up ${template} playground...`);
723
822
  const files = template === "vue3" ? getVue3Template("playground") : getReactTemplate("playground");
@@ -741,14 +840,23 @@ async function runPlayground(templateArg, port = "3456") {
741
840
  `);
742
841
  try {
743
842
  const safePort = /^\d+$/.test(port) ? port : "3456";
744
- execSync(`npx vite --port ${safePort}`, { cwd: projectDir, stdio: "inherit" });
843
+ const openFlag = open ? " --open" : "";
844
+ execSync(`npx vite --port ${safePort}${openFlag}`, { cwd: projectDir, stdio: "inherit" });
745
845
  } catch {
746
846
  }
747
847
  }
748
848
  function createGenerateCommand() {
749
849
  const cmd = new Command("generate").description("Code generation utilities");
750
- cmd.command("docs").option("-i, --input <dir>", "Types directory", "packages/core/src/types").option("-o, --output <dir>", "Output directory", "docs/api").description("Generate API documentation from component type definitions").action(async (opts) => {
751
- await runGenerateDocs(opts.input, opts.output);
850
+ cmd.command("docs").option("-i, --input <dir>", "Types directory", "packages/core/src/types").option("-o, --output <dir>", "Output directory", "docs/api").option("--dry-run", "Preview generated docs without writing files").description("Generate API documentation from component type definitions").action(async (opts) => {
851
+ await runGenerateDocs(opts.input, opts.output, Boolean(opts.dryRun));
852
+ });
853
+ cmd.command("test <component>").option("-f, --framework <framework>", "Target framework (vue3 | react | both)", "both").option("-o, --output <dir>", "Tests root directory", "tests").option("--dry-run", "Preview generated test files without writing files").description("Generate starter test templates for a component").action(
854
+ async (component, opts) => {
855
+ await runGenerateTest(component, opts.framework, opts.output, Boolean(opts.dryRun));
856
+ }
857
+ );
858
+ cmd.command("doc-template <component>").option("-o, --output <dir>", "Documentation output directory", "docs/components").option("--dry-run", "Preview generated documentation without writing files").description("Generate a component documentation page template").action(async (component, opts) => {
859
+ await runGenerateDocTemplate(component, opts.output, Boolean(opts.dryRun));
752
860
  });
753
861
  return cmd;
754
862
  }
@@ -805,7 +913,7 @@ function generateMarkdown(doc) {
805
913
  }
806
914
  return lines.join("\n");
807
915
  }
808
- async function runGenerateDocs(inputDir, outputDir) {
916
+ async function runGenerateDocs(inputDir, outputDir, dryRun = false) {
809
917
  const resolvedInput = resolve(process.cwd(), inputDir);
810
918
  const resolvedOutput = resolve(process.cwd(), outputDir);
811
919
  if (!existsSync(resolvedInput)) {
@@ -814,7 +922,11 @@ async function runGenerateDocs(inputDir, outputDir) {
814
922
  }
815
923
  const files = readdirSync(resolvedInput).filter((f) => f.endsWith(".ts") && f !== "index.ts").sort();
816
924
  logInfo(`Found ${files.length} type files in ${inputDir}`);
817
- ensureDir(resolvedOutput);
925
+ if (dryRun) {
926
+ logInfo("Dry run: no documentation files will be written.");
927
+ } else {
928
+ ensureDir(resolvedOutput);
929
+ }
818
930
  const docs = [];
819
931
  let step = 0;
820
932
  for (const file of files) {
@@ -824,7 +936,12 @@ async function runGenerateDocs(inputDir, outputDir) {
824
936
  if (doc) {
825
937
  docs.push(doc);
826
938
  const md = generateMarkdown(doc);
827
- writeFileSafe(join(resolvedOutput, `${doc.fileName}.md`), md);
939
+ const outputPath = join(resolvedOutput, `${doc.fileName}.md`);
940
+ if (dryRun) {
941
+ logInfo(`Would generate ${outputPath}`);
942
+ } else {
943
+ writeFileSafe(outputPath, md);
944
+ }
828
945
  }
829
946
  }
830
947
  const indexLines = [
@@ -847,13 +964,184 @@ async function runGenerateDocs(inputDir, outputDir) {
847
964
  }
848
965
  indexLines.push("");
849
966
  }
850
- writeFileSafe(join(resolvedOutput, "index.md"), indexLines.join("\n"));
967
+ const indexPath = join(resolvedOutput, "index.md");
968
+ if (dryRun) {
969
+ logInfo(`Would generate ${indexPath}`);
970
+ logSuccess(`Dry run completed for ${docs.length} component docs in ${outputDir}`);
971
+ return;
972
+ }
973
+ writeFileSafe(indexPath, indexLines.join("\n"));
851
974
  logSuccess(`Generated docs for ${docs.length} components in ${outputDir}`);
852
975
  }
976
+ async function runGenerateTest(component, frameworkArg, outputDir, dryRun = false) {
977
+ const componentName = normalizeComponentName(component);
978
+ if (!componentName) {
979
+ logError(`Unknown component: ${component}`);
980
+ process.exit(1);
981
+ }
982
+ const framework = normalizeFrameworkTarget(frameworkArg);
983
+ if (!framework) {
984
+ logError(`Unknown framework target: ${frameworkArg}. Use vue3, react, or both.`);
985
+ process.exit(1);
986
+ }
987
+ const targets = framework === "both" ? ["vue3", "react"] : [framework];
988
+ for (const target of targets) {
989
+ const ext = target === "vue3" ? "ts" : "tsx";
990
+ const folder = target === "vue3" ? "vue" : "react";
991
+ const filePath = resolve(process.cwd(), outputDir, folder, `${componentName}.spec.${ext}`);
992
+ const content = target === "vue3" ? generateVueTest(componentName) : generateReactTest(componentName);
993
+ if (existsSync(filePath)) {
994
+ logWarn(`${filePath} already exists, skipping`);
995
+ continue;
996
+ }
997
+ if (dryRun) {
998
+ logInfo(`Would generate ${filePath}`);
999
+ continue;
1000
+ }
1001
+ writeFileSafe(filePath, content);
1002
+ logSuccess(`Generated ${filePath}`);
1003
+ }
1004
+ }
1005
+ async function runGenerateDocTemplate(component, outputDir, dryRun = false) {
1006
+ const componentName = normalizeComponentName(component);
1007
+ if (!componentName) {
1008
+ logError(`Unknown component: ${component}`);
1009
+ process.exit(1);
1010
+ }
1011
+ const fileName = `${componentName.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()}.md`;
1012
+ const filePath = resolve(process.cwd(), outputDir, fileName);
1013
+ const content = generateComponentDocTemplate(componentName);
1014
+ if (existsSync(filePath)) {
1015
+ logWarn(`${filePath} already exists, skipping`);
1016
+ return;
1017
+ }
1018
+ if (dryRun) {
1019
+ logInfo(`Would generate ${filePath}`);
1020
+ return;
1021
+ }
1022
+ writeFileSafe(filePath, content);
1023
+ logSuccess(`Generated ${filePath}`);
1024
+ }
1025
+ function normalizeComponentName(component) {
1026
+ return ALL_COMPONENTS.find((name) => name.toLowerCase() === component.toLowerCase()) ?? null;
1027
+ }
1028
+ function normalizeFrameworkTarget(value) {
1029
+ if (value === "vue3" || value === "react" || value === "both") return value;
1030
+ return null;
1031
+ }
1032
+ function generateVueTest(component) {
1033
+ return `/**
1034
+ * @vitest-environment happy-dom
1035
+ */
1036
+
1037
+ import { describe, it, expect } from 'vitest'
1038
+ import { render, screen } from '@testing-library/vue'
1039
+ import { ${component} } from '@expcat/tigercat-vue'
1040
+ import { expectNoA11yViolationsIsolated } from '../utils'
1041
+
1042
+ describe('${component}', () => {
1043
+ it('renders without crashing', () => {
1044
+ const { container } = render(${component}, {
1045
+ attrs: { 'data-testid': '${component.toLowerCase()}' }
1046
+ })
1047
+
1048
+ expect(screen.getByTestId('${component.toLowerCase()}')).toBeInTheDocument()
1049
+ expect(container.firstElementChild).toBeTruthy()
1050
+ })
1051
+
1052
+ describe('a11y', () => {
1053
+ it('has no accessibility violations', async () => {
1054
+ const { container } = render(${component})
1055
+ await expectNoA11yViolationsIsolated(container)
1056
+ })
1057
+ })
1058
+
1059
+ describe('Edge Cases', () => {
1060
+ it('keeps rendering with empty props', () => {
1061
+ const { container } = render(${component})
1062
+ expect(container.firstElementChild).toBeTruthy()
1063
+ })
1064
+ })
1065
+ })
1066
+ `;
1067
+ }
1068
+ function generateReactTest(component) {
1069
+ return `/**
1070
+ * @vitest-environment happy-dom
1071
+ */
1072
+
1073
+ import { describe, it, expect } from 'vitest'
1074
+ import { render, screen } from '@testing-library/react'
1075
+ import React from 'react'
1076
+ import { ${component} } from '@expcat/tigercat-react'
1077
+ import { expectNoA11yViolationsIsolated } from '../utils/react'
1078
+
1079
+ describe('${component}', () => {
1080
+ it('renders without crashing', () => {
1081
+ const { container } = render(<${component} data-testid="${component.toLowerCase()}" />)
1082
+
1083
+ expect(screen.getByTestId('${component.toLowerCase()}')).toBeInTheDocument()
1084
+ expect(container.firstElementChild).toBeTruthy()
1085
+ })
1086
+
1087
+ describe('a11y', () => {
1088
+ it('has no accessibility violations', async () => {
1089
+ const { container } = render(<${component} />)
1090
+ await expectNoA11yViolationsIsolated(container)
1091
+ })
1092
+ })
1093
+
1094
+ describe('Edge Cases', () => {
1095
+ it('keeps rendering with empty props', () => {
1096
+ const { container } = render(<${component} />)
1097
+ expect(container.firstElementChild).toBeTruthy()
1098
+ })
1099
+ })
1100
+ })
1101
+ `;
1102
+ }
1103
+ function generateComponentDocTemplate(component) {
1104
+ return `# ${component}
1105
+
1106
+ ## Overview
1107
+
1108
+ Describe the user-facing purpose and primary workflow for ${component}.
1109
+
1110
+ ## Import
1111
+
1112
+ \`\`\`ts
1113
+ import { ${component} } from '@expcat/tigercat-vue'
1114
+ import { ${component} } from '@expcat/tigercat-react'
1115
+ \`\`\`
1116
+
1117
+ ## Basic Usage
1118
+
1119
+ Add one minimal Vue example and one minimal React example from \`skills/tigercat/references\`.
1120
+
1121
+ ## Props
1122
+
1123
+ Keep this section aligned with \`packages/core/src/types\` and regenerate API docs after changes.
1124
+
1125
+ ## Accessibility
1126
+
1127
+ Document keyboard behavior, roles, labels, and focus management.
1128
+
1129
+ ## Edge Cases
1130
+
1131
+ List boundary states, empty states, loading states, and controlled/uncontrolled behavior.
1132
+ `;
1133
+ }
853
1134
  var MIN_NODE_MAJOR = 20;
854
1135
  var MIN_PNPM_MAJOR = 8;
855
1136
  var REQUIRED_TAILWIND_MAJOR = 4;
856
1137
  var REQUIRED_TIGERCAT_MAJOR = 1;
1138
+ var VERSION_COMPATIBILITY_MATRIX = [
1139
+ { name: "Node.js", range: ">=20.11.0", reason: "Matches workspace engines and CLI templates" },
1140
+ { name: "pnpm", range: ">=8.0.0", reason: "Required by workspace package management" },
1141
+ { name: "Tailwind CSS", range: ">=4.0.0", reason: "Required by Tigercat theme utilities" },
1142
+ { name: "Vue", range: "^3.0.0", reason: "Peer range for @expcat/tigercat-vue" },
1143
+ { name: "React", range: "^19.0.0", reason: "Peer range for @expcat/tigercat-react" }
1144
+ ];
857
1145
  var FRAMEWORK_REQUIREMENTS = {
858
1146
  vue3: {
859
1147
  peers: ["@expcat/tigercat-vue", "@expcat/tigercat-core", "vue"],
@@ -865,8 +1153,8 @@ var FRAMEWORK_REQUIREMENTS = {
865
1153
  }
866
1154
  };
867
1155
  function createDoctorCommand() {
868
- return new Command("doctor").description("Check whether the current project is compatible with Tigercat").action(() => {
869
- runDoctor();
1156
+ return new Command("doctor").option("--json", "Print structured JSON output").description("Check whether the current project is compatible with Tigercat").action((opts) => {
1157
+ runDoctor(Boolean(opts.json));
870
1158
  });
871
1159
  }
872
1160
  function collectDoctorChecks(options = {}) {
@@ -883,10 +1171,28 @@ function collectDoctorChecks(options = {}) {
883
1171
  checks.push(createTailwindCheck(packageResult.packageJson));
884
1172
  checks.push(createPeerDepsCheck(packageResult.packageJson));
885
1173
  checks.push(createTemplateCompatibilityCheck(packageResult.packageJson));
1174
+ checks.push(createCompatibilityMatrixCheck(packageResult.packageJson));
886
1175
  return checks;
887
1176
  }
888
- function runDoctor() {
1177
+ function runDoctor(json = false) {
889
1178
  const checks = collectDoctorChecks();
1179
+ if (json) {
1180
+ const failures2 = checks.filter((check) => check.status === "fail");
1181
+ const warnings2 = checks.filter((check) => check.status === "warn");
1182
+ console.log(
1183
+ JSON.stringify(
1184
+ {
1185
+ status: failures2.length > 0 ? "fail" : warnings2.length > 0 ? "warn" : "pass",
1186
+ checks,
1187
+ compatibilityMatrix: VERSION_COMPATIBILITY_MATRIX
1188
+ },
1189
+ null,
1190
+ 2
1191
+ )
1192
+ );
1193
+ if (failures2.length > 0) process.exit(1);
1194
+ return;
1195
+ }
890
1196
  logInfo("Running Tigercat project checks...");
891
1197
  console.log();
892
1198
  for (const check of checks) {
@@ -900,7 +1206,7 @@ function runDoctor() {
900
1206
  process.exit(1);
901
1207
  }
902
1208
  if (warnings.length > 0) {
903
- logWarn(`${warnings.length} warning${warnings.length === 1 ? "" : "s"} found`);
1209
+ logWarn2(`${warnings.length} warning${warnings.length === 1 ? "" : "s"} found`);
904
1210
  return;
905
1211
  }
906
1212
  logSuccess("All checks passed");
@@ -922,7 +1228,8 @@ function createPackageCheck(result) {
922
1228
  return {
923
1229
  name: "Project package",
924
1230
  status: "fail",
925
- message: result.error ?? "package.json could not be read"
1231
+ message: result.error ?? "package.json could not be read",
1232
+ suggestions: ["Run this command from a project root that contains package.json"]
926
1233
  };
927
1234
  }
928
1235
  return {
@@ -937,7 +1244,8 @@ function createNodeCheck(version) {
937
1244
  return {
938
1245
  name: "Node.js",
939
1246
  status: "fail",
940
- message: `Node ${MIN_NODE_MAJOR}+ is required, current version is ${version}`
1247
+ message: `Node ${MIN_NODE_MAJOR}+ is required, current version is ${version}`,
1248
+ suggestions: [`Install Node ${MIN_NODE_MAJOR}+ and rerun tigercat doctor`]
941
1249
  };
942
1250
  }
943
1251
  return {
@@ -952,7 +1260,8 @@ function createPnpmCheck(packageJson, env) {
952
1260
  return {
953
1261
  name: "pnpm",
954
1262
  status: "warn",
955
- message: `Could not detect pnpm version; Tigercat templates expect pnpm ${MIN_PNPM_MAJOR}+`
1263
+ message: `Could not detect pnpm version; Tigercat templates expect pnpm ${MIN_PNPM_MAJOR}+`,
1264
+ suggestions: ["Add packageManager: pnpm@10.26.2 to package.json or run through pnpm"]
956
1265
  };
957
1266
  }
958
1267
  const parsed = parseVersion(version);
@@ -960,7 +1269,8 @@ function createPnpmCheck(packageJson, env) {
960
1269
  return {
961
1270
  name: "pnpm",
962
1271
  status: "fail",
963
- message: `pnpm ${MIN_PNPM_MAJOR}+ is required, detected ${version}`
1272
+ message: `pnpm ${MIN_PNPM_MAJOR}+ is required, detected ${version}`,
1273
+ suggestions: [`Upgrade pnpm to ${MIN_PNPM_MAJOR}+`]
964
1274
  };
965
1275
  }
966
1276
  return {
@@ -970,55 +1280,75 @@ function createPnpmCheck(packageJson, env) {
970
1280
  };
971
1281
  }
972
1282
  function createTailwindCheck(packageJson) {
973
- const allDeps = collectDependencies(packageJson);
1283
+ const allDeps = collectDependencies2(packageJson);
974
1284
  const tailwindRange = allDeps.tailwindcss;
975
1285
  const vitePluginRange = allDeps["@tailwindcss/vite"];
976
1286
  if (!tailwindRange) {
977
1287
  return {
978
1288
  name: "Tailwind CSS",
979
1289
  status: "fail",
980
- message: "tailwindcss is missing; Tigercat requires Tailwind CSS 4+"
1290
+ message: "tailwindcss is missing; Tigercat requires Tailwind CSS 4+",
1291
+ suggestions: ["Install tailwindcss and @tailwindcss/vite"]
981
1292
  };
982
1293
  }
983
1294
  const tailwindMajor = getRangeMajor(tailwindRange);
984
- const pluginMajor = vitePluginRange ? getRangeMajor(vitePluginRange) : null;
985
1295
  if (tailwindMajor !== null && tailwindMajor < REQUIRED_TAILWIND_MAJOR) {
986
1296
  return {
987
1297
  name: "Tailwind CSS",
988
1298
  status: "fail",
989
- message: `tailwindcss ${tailwindRange} is not compatible; use Tailwind CSS ${REQUIRED_TAILWIND_MAJOR}+`
1299
+ message: `tailwindcss ${tailwindRange} is not compatible; use Tailwind CSS ${REQUIRED_TAILWIND_MAJOR}+`,
1300
+ suggestions: [`Upgrade tailwindcss to ${REQUIRED_TAILWIND_MAJOR}+`]
990
1301
  };
991
1302
  }
992
1303
  if (tailwindMajor === null) {
993
1304
  return {
994
1305
  name: "Tailwind CSS",
995
- status: "warn",
996
- message: `Could not verify tailwindcss range ${tailwindRange}; expected ${REQUIRED_TAILWIND_MAJOR}+`
1306
+ status: "fail",
1307
+ message: `Could not verify tailwindcss range ${tailwindRange}; Tigercat builds with Tailwind CSS ${REQUIRED_TAILWIND_MAJOR}+ only`,
1308
+ suggestions: [`Use an explicit Tailwind CSS ${REQUIRED_TAILWIND_MAJOR}+ semver range`]
1309
+ };
1310
+ }
1311
+ if (!vitePluginRange) {
1312
+ return {
1313
+ name: "Tailwind CSS",
1314
+ status: "fail",
1315
+ message: "@tailwindcss/vite is required; Tigercat builds with Tailwind CSS 4 only",
1316
+ suggestions: ["Install @tailwindcss/vite 4+"]
997
1317
  };
998
1318
  }
999
- if (vitePluginRange && pluginMajor !== null && pluginMajor < REQUIRED_TAILWIND_MAJOR) {
1319
+ const pluginMajor = getRangeMajor(vitePluginRange);
1320
+ if (pluginMajor !== null && pluginMajor < REQUIRED_TAILWIND_MAJOR) {
1000
1321
  return {
1001
1322
  name: "Tailwind CSS",
1002
1323
  status: "fail",
1003
- message: `@tailwindcss/vite ${vitePluginRange} is not compatible; use ${REQUIRED_TAILWIND_MAJOR}+`
1324
+ message: `@tailwindcss/vite ${vitePluginRange} is not compatible; use ${REQUIRED_TAILWIND_MAJOR}+`,
1325
+ suggestions: [`Upgrade @tailwindcss/vite to ${REQUIRED_TAILWIND_MAJOR}+`]
1326
+ };
1327
+ }
1328
+ if (pluginMajor === null) {
1329
+ return {
1330
+ name: "Tailwind CSS",
1331
+ status: "fail",
1332
+ message: `Could not verify @tailwindcss/vite range ${vitePluginRange}; Tigercat builds with Tailwind CSS ${REQUIRED_TAILWIND_MAJOR}+ only`,
1333
+ suggestions: [`Use an explicit @tailwindcss/vite ${REQUIRED_TAILWIND_MAJOR}+ semver range`]
1004
1334
  };
1005
1335
  }
1006
- const details = vitePluginRange ? [`@tailwindcss/vite ${vitePluginRange}`] : ["@tailwindcss/vite is recommended for Vite templates"];
1007
1336
  return {
1008
1337
  name: "Tailwind CSS",
1009
- status: vitePluginRange ? "pass" : "warn",
1010
- message: `tailwindcss ${tailwindRange} satisfies Tigercat requirements`,
1011
- details
1338
+ status: "pass",
1339
+ message: `tailwindcss ${tailwindRange} uses the Tailwind CSS ${REQUIRED_TAILWIND_MAJOR} build pipeline`,
1340
+ details: [`@tailwindcss/vite ${vitePluginRange}`]
1012
1341
  };
1013
1342
  }
1014
1343
  function createPeerDepsCheck(packageJson) {
1015
- const allDeps = collectDependencies(packageJson);
1344
+ const allDeps = collectDependencies2(packageJson);
1016
1345
  const frameworks = detectTigercatFrameworks(allDeps);
1017
1346
  if (frameworks.length === 0) {
1018
1347
  return {
1019
1348
  name: "Peer dependencies",
1020
1349
  status: "warn",
1021
- message: "No Tigercat Vue or React package was detected"
1350
+ message: "No Tigercat Vue or React package was detected",
1351
+ suggestions: ["Install @expcat/tigercat-vue or @expcat/tigercat-react"]
1022
1352
  };
1023
1353
  }
1024
1354
  const missing = [
@@ -1036,7 +1366,8 @@ function createPeerDepsCheck(packageJson) {
1036
1366
  name: "Peer dependencies",
1037
1367
  status: "fail",
1038
1368
  message: "Tigercat peer dependencies are incomplete or incompatible",
1039
- details: [...missing.map((dependency) => `Missing ${dependency}`), ...incompatible]
1369
+ details: [...missing.map((dependency) => `Missing ${dependency}`), ...incompatible],
1370
+ suggestions: ["Run tigercat add --install or install the listed dependencies manually"]
1040
1371
  };
1041
1372
  }
1042
1373
  return {
@@ -1046,13 +1377,14 @@ function createPeerDepsCheck(packageJson) {
1046
1377
  };
1047
1378
  }
1048
1379
  function createTemplateCompatibilityCheck(packageJson) {
1049
- const allDeps = collectDependencies(packageJson);
1380
+ const allDeps = collectDependencies2(packageJson);
1050
1381
  const frameworks = detectTigercatFrameworks(allDeps);
1051
1382
  if (frameworks.length === 0) {
1052
1383
  return {
1053
1384
  name: "Template compatibility",
1054
1385
  status: "warn",
1055
- message: "Skipped because no supported Tigercat framework package was detected"
1386
+ message: "Skipped because no supported Tigercat framework package was detected",
1387
+ suggestions: ["Install a Tigercat framework package to enable template compatibility checks"]
1056
1388
  };
1057
1389
  }
1058
1390
  const missing = [
@@ -1067,7 +1399,8 @@ function createTemplateCompatibilityCheck(packageJson) {
1067
1399
  name: "Template compatibility",
1068
1400
  status: "warn",
1069
1401
  message: "Project differs from current CLI template dependencies",
1070
- details: missing.map((dependency) => `Template dependency not found: ${dependency}`)
1402
+ details: missing.map((dependency) => `Template dependency not found: ${dependency}`),
1403
+ suggestions: ["Compare your project dependencies with the latest tigercat create template"]
1071
1404
  };
1072
1405
  }
1073
1406
  return {
@@ -1076,7 +1409,29 @@ function createTemplateCompatibilityCheck(packageJson) {
1076
1409
  message: `${frameworks.map(formatFramework).join(" + ")} template dependencies are present`
1077
1410
  };
1078
1411
  }
1079
- function collectDependencies(packageJson) {
1412
+ function createCompatibilityMatrixCheck(packageJson) {
1413
+ const dependencies = collectDependencies2(packageJson);
1414
+ const details = VERSION_COMPATIBILITY_MATRIX.map(
1415
+ (item) => `${item.name} ${item.range} - ${item.reason}`
1416
+ );
1417
+ const frameworks = detectTigercatFrameworks(dependencies);
1418
+ if (frameworks.length === 0) {
1419
+ return {
1420
+ name: "Version compatibility matrix",
1421
+ status: "warn",
1422
+ message: "Framework-specific matrix checks were skipped",
1423
+ details,
1424
+ suggestions: ["Install a Tigercat Vue or React package to validate framework peer ranges"]
1425
+ };
1426
+ }
1427
+ return {
1428
+ name: "Version compatibility matrix",
1429
+ status: "pass",
1430
+ message: `${frameworks.map(formatFramework).join(" + ")} compatibility matrix is available`,
1431
+ details
1432
+ };
1433
+ }
1434
+ function collectDependencies2(packageJson) {
1080
1435
  return {
1081
1436
  ...packageJson.peerDependencies,
1082
1437
  ...packageJson.dependencies,
@@ -1126,11 +1481,14 @@ function formatFramework(framework) {
1126
1481
  return framework === "vue3" ? "Vue 3" : "React";
1127
1482
  }
1128
1483
  function printCheck(check) {
1129
- const symbol = check.status === "pass" ? pc.green("\u2714") : check.status === "warn" ? pc.yellow("\u26A0") : pc.red("\u2716");
1130
- const name = pc.bold(check.name);
1484
+ const symbol = check.status === "pass" ? pc2.green("\u2714") : check.status === "warn" ? pc2.yellow("\u26A0") : pc2.red("\u2716");
1485
+ const name = pc2.bold(check.name);
1131
1486
  console.log(`${symbol} ${name}: ${check.message}`);
1132
1487
  for (const detail of check.details ?? []) {
1133
- console.log(` ${pc.dim("-")} ${detail}`);
1488
+ console.log(` ${pc2.dim("-")} ${detail}`);
1489
+ }
1490
+ for (const suggestion of check.suggestions ?? []) {
1491
+ console.log(` ${pc2.dim("fix:")} ${suggestion}`);
1134
1492
  }
1135
1493
  }
1136
1494
 
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@expcat/tigercat-cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "CLI tooling for Tigercat UI library — project scaffolding, component generation, and more",
6
6
  "license": "MIT",
7
7
  "author": "Yizhe Wang",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/expcats/Tigercat.git",
10
+ "url": "https://github.com/expcat/Tigercat",
11
11
  "directory": "packages/cli"
12
12
  },
13
- "homepage": "https://github.com/expcats/Tigercat#readme",
13
+ "homepage": "https://github.com/expcat/Tigercat#readme",
14
14
  "bugs": {
15
- "url": "https://github.com/expcats/Tigercat/issues"
15
+ "url": "https://github.com/expcat/Tigercat/issues"
16
16
  },
17
17
  "keywords": [
18
18
  "tigercat",