@expcat/tigercat-cli 1.0.7 → 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 +138 -1
  2. package/dist/index.js +819 -106
  3. package/package.json +11 -11
package/dist/index.js CHANGED
@@ -3,13 +3,35 @@ 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
10
10
  var CLI_NAME = "tigercat";
11
11
  var CLI_VERSION = "0.9.0";
12
12
  var TEMPLATES = ["vue3", "react"];
13
+ var TEMPLATE_VERSIONS = {
14
+ // Tigercat packages (use caret on latest major)
15
+ tigercat: "^1.0.0",
16
+ // Frameworks
17
+ vue: "^3.5.33",
18
+ react: "^19.2.5",
19
+ reactDom: "^19.2.5",
20
+ // Build toolchain
21
+ typescript: "^6.0.3",
22
+ vite: "^8.0.10",
23
+ tailwindcss: "^4.2.4",
24
+ tailwindcssVite: "^4.2.4",
25
+ // Vite plugins
26
+ vitejsPluginVue: "^6.0.6",
27
+ vitejsPluginReact: "^6.0.1",
28
+ // Type definitions
29
+ typesReact: "^19.2.14",
30
+ typesReactDom: "^19.2.3",
31
+ // Vue-specific
32
+ vueTsconfig: "^0.9.1",
33
+ vueTsc: "^3.2.7"
34
+ };
13
35
  var COMPONENT_CATEGORIES = {
14
36
  basic: [
15
37
  "Alert",
@@ -65,19 +87,19 @@ var COMPONENT_CATEGORIES = {
65
87
  };
66
88
  var ALL_COMPONENTS = Object.values(COMPONENT_CATEGORIES).flat();
67
89
  function logSuccess(msg) {
68
- console.log(pc.green("\u2714") + " " + msg);
90
+ console.log(pc2.green("\u2714") + " " + msg);
69
91
  }
70
92
  function logInfo(msg) {
71
- console.log(pc.blue("\u2139") + " " + msg);
93
+ console.log(pc2.blue("\u2139") + " " + msg);
72
94
  }
73
- function logWarn(msg) {
74
- console.log(pc.yellow("\u26A0") + " " + msg);
95
+ function logWarn2(msg) {
96
+ console.log(pc2.yellow("\u26A0") + " " + msg);
75
97
  }
76
98
  function logError(msg) {
77
- console.error(pc.red("\u2716") + " " + msg);
99
+ console.error(pc2.red("\u2716") + " " + msg);
78
100
  }
79
101
  function logStep(step, total, msg) {
80
- console.log(pc.dim(`[${step}/${total}]`) + " " + msg);
102
+ console.log(pc2.dim(`[${step}/${total}]`) + " " + msg);
81
103
  }
82
104
  function ensureDir(dir) {
83
105
  if (!existsSync(dir)) {
@@ -123,17 +145,18 @@ function vue3PackageJson(name) {
123
145
  preview: "vite preview"
124
146
  },
125
147
  dependencies: {
126
- "@expcat/tigercat-vue": "^0.9.0",
127
- vue: "^3.5.26"
148
+ "@expcat/tigercat-vue": TEMPLATE_VERSIONS.tigercat,
149
+ vue: TEMPLATE_VERSIONS.vue
128
150
  },
129
151
  devDependencies: {
130
- "@tailwindcss/vite": "^4.1.18",
131
- "@vitejs/plugin-vue": "^6.0.3",
132
- "@vue/tsconfig": "^0.7.0",
133
- tailwindcss: "^4.1.18",
134
- typescript: "^5.9.3",
135
- vite: "^7.3.0",
136
- "vue-tsc": "^2.2.0"
152
+ "@expcat/tigercat-core": TEMPLATE_VERSIONS.tigercat,
153
+ "@tailwindcss/vite": TEMPLATE_VERSIONS.tailwindcssVite,
154
+ "@vitejs/plugin-vue": TEMPLATE_VERSIONS.vitejsPluginVue,
155
+ "@vue/tsconfig": TEMPLATE_VERSIONS.vueTsconfig,
156
+ tailwindcss: TEMPLATE_VERSIONS.tailwindcss,
157
+ typescript: TEMPLATE_VERSIONS.typescript,
158
+ vite: TEMPLATE_VERSIONS.vite,
159
+ "vue-tsc": TEMPLATE_VERSIONS.vueTsc
137
160
  }
138
161
  },
139
162
  null,
@@ -145,9 +168,9 @@ function vue3Tsconfig() {
145
168
  {
146
169
  extends: "@vue/tsconfig/tsconfig.dom.json",
147
170
  compilerOptions: {
148
- target: "ES2020",
171
+ target: "ES2022",
149
172
  module: "ESNext",
150
- lib: ["ES2020", "DOM", "DOM.Iterable"],
173
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
151
174
  skipLibCheck: true,
152
175
  moduleResolution: "bundler",
153
176
  resolveJsonModule: true,
@@ -204,18 +227,58 @@ createApp(App).mount('#app')
204
227
  }
205
228
  function vue3App() {
206
229
  return `<script setup lang="ts">
207
- import { Button, Alert } from '@expcat/tigercat-vue'
230
+ import { ref } from 'vue'
231
+ import { Button, Alert, Switch } from '@expcat/tigercat-vue'
232
+
233
+ const dark = ref(false)
234
+ const modern = ref(true)
235
+
236
+ function syncRoot() {
237
+ const root = document.documentElement
238
+ root.classList.toggle('dark', dark.value)
239
+ if (modern.value) {
240
+ root.setAttribute('data-tiger-style', 'modern')
241
+ } else {
242
+ root.removeAttribute('data-tiger-style')
243
+ }
244
+ }
245
+
246
+ syncRoot()
247
+
248
+ function toggleDark(v: boolean) {
249
+ dark.value = v
250
+ syncRoot()
251
+ }
252
+
253
+ function toggleModern(v: boolean) {
254
+ modern.value = v
255
+ syncRoot()
256
+ }
208
257
  </script>
209
258
 
210
259
  <template>
211
260
  <div class="min-h-screen bg-[var(--tiger-surface,#ffffff)] p-8">
212
- <h1 class="text-2xl font-bold text-[var(--tiger-text,#111827)] mb-6">
213
- Tigercat + Vue 3
214
- </h1>
261
+ <div class="flex items-center justify-between mb-6">
262
+ <h1 class="text-2xl font-bold text-[var(--tiger-text,#111827)]">
263
+ Tigercat + Vue 3
264
+ </h1>
265
+ <div class="flex items-center gap-4 text-sm text-[var(--tiger-text-muted,#6b7280)]">
266
+ <label class="flex items-center gap-2">
267
+ <span>Modern</span>
268
+ <Switch :checked="modern" size="sm" @update:checked="toggleModern" />
269
+ </label>
270
+ <label class="flex items-center gap-2">
271
+ <span>Dark</span>
272
+ <Switch :checked="dark" size="sm" @update:checked="toggleDark" />
273
+ </label>
274
+ </div>
275
+ </div>
215
276
 
216
277
  <div class="space-y-4">
217
278
  <Alert variant="info">
218
279
  Welcome to your Tigercat project! Edit src/App.vue to get started.
280
+ Toggle <code>Modern</code> to preview the opt-in modern visual style
281
+ (radius / shadow / motion tokens).
219
282
  </Alert>
220
283
 
221
284
  <div class="flex gap-2">
@@ -233,38 +296,26 @@ function vue3EnvDts() {
233
296
 
234
297
  declare module '*.vue' {
235
298
  import type { DefineComponent } from 'vue'
236
- const component: DefineComponent<{}, {}, any>
299
+ const component: DefineComponent<{}, {}, unknown>
237
300
  export default component
238
301
  }
239
302
  `;
240
303
  }
241
304
  function commonStyleCss() {
242
305
  return `@import "tailwindcss";
306
+ @plugin "@expcat/tigercat-core/tailwind/modern";
243
307
 
244
- :root {
245
- --tiger-primary: #2563eb;
246
- --tiger-primary-hover: #1d4ed8;
247
- --tiger-surface: #ffffff;
248
- --tiger-surface-muted: #f9fafb;
249
- --tiger-text: #111827;
250
- --tiger-text-muted: #6b7280;
251
- --tiger-border: #e5e7eb;
252
- }
253
-
254
- @media (prefers-color-scheme: dark) {
255
- :root {
256
- --tiger-primary: #3b82f6;
257
- --tiger-primary-hover: #2563eb;
258
- --tiger-surface: #111827;
259
- --tiger-surface-muted: #1f2937;
260
- --tiger-text: #f9fafb;
261
- --tiger-text-muted: #9ca3af;
262
- --tiger-border: #374151;
263
- }
308
+ /*
309
+ * The tigercat tailwind plugin injects every --tiger-* design token for
310
+ * both light (:root) and dark (.dark) modes, plus the opt-in modern
311
+ * overrides activated by data-tiger-style="modern". The demo App toggles
312
+ * dark mode via .dark on <html> and prefers-color-scheme via the rule
313
+ * below. Swap the @plugin line for a tailwind.config.ts calling
314
+ * createTigercatPlugin({ preset }) to use a custom preset.
315
+ */
264
316
 
265
- html {
266
- color-scheme: dark;
267
- }
317
+ html {
318
+ color-scheme: light dark;
268
319
  }
269
320
  `;
270
321
  }
@@ -295,18 +346,19 @@ function reactPackageJson(name) {
295
346
  preview: "vite preview"
296
347
  },
297
348
  dependencies: {
298
- "@expcat/tigercat-react": "^0.9.0",
299
- react: "^19.2.3",
300
- "react-dom": "^19.2.3"
349
+ "@expcat/tigercat-react": TEMPLATE_VERSIONS.tigercat,
350
+ react: TEMPLATE_VERSIONS.react,
351
+ "react-dom": TEMPLATE_VERSIONS.reactDom
301
352
  },
302
353
  devDependencies: {
303
- "@tailwindcss/vite": "^4.1.18",
304
- "@types/react": "^19.2.7",
305
- "@types/react-dom": "^19.2.2",
306
- "@vitejs/plugin-react": "^4.3.4",
307
- tailwindcss: "^4.1.18",
308
- typescript: "^5.9.3",
309
- vite: "^7.3.0"
354
+ "@expcat/tigercat-core": TEMPLATE_VERSIONS.tigercat,
355
+ "@tailwindcss/vite": TEMPLATE_VERSIONS.tailwindcssVite,
356
+ "@types/react": TEMPLATE_VERSIONS.typesReact,
357
+ "@types/react-dom": TEMPLATE_VERSIONS.typesReactDom,
358
+ "@vitejs/plugin-react": TEMPLATE_VERSIONS.vitejsPluginReact,
359
+ tailwindcss: TEMPLATE_VERSIONS.tailwindcss,
360
+ typescript: TEMPLATE_VERSIONS.typescript,
361
+ vite: TEMPLATE_VERSIONS.vite
310
362
  }
311
363
  },
312
364
  null,
@@ -317,9 +369,9 @@ function reactTsconfig() {
317
369
  return JSON.stringify(
318
370
  {
319
371
  compilerOptions: {
320
- target: "ES2020",
372
+ target: "ES2022",
321
373
  useDefineForClassFields: true,
322
- lib: ["ES2020", "DOM", "DOM.Iterable"],
374
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
323
375
  module: "ESNext",
324
376
  skipLibCheck: true,
325
377
  moduleResolution: "bundler",
@@ -400,18 +452,49 @@ createRoot(document.getElementById('root')!).render(
400
452
  `;
401
453
  }
402
454
  function reactApp() {
403
- return `import { Button, Alert } from '@expcat/tigercat-react'
455
+ return `import { useState, useCallback, useEffect } from 'react'
456
+ import { Button, Alert, Switch } from '@expcat/tigercat-react'
404
457
 
405
458
  export default function App() {
459
+ const [dark, setDark] = useState(false)
460
+ const [modern, setModern] = useState(true)
461
+
462
+ useEffect(() => {
463
+ const root = document.documentElement
464
+ root.classList.toggle('dark', dark)
465
+ if (modern) {
466
+ root.setAttribute('data-tiger-style', 'modern')
467
+ } else {
468
+ root.removeAttribute('data-tiger-style')
469
+ }
470
+ }, [dark, modern])
471
+
472
+ const onDark = useCallback((v: boolean) => setDark(v), [])
473
+ const onModern = useCallback((v: boolean) => setModern(v), [])
474
+
406
475
  return (
407
476
  <div className="min-h-screen bg-[var(--tiger-surface,#ffffff)] p-8">
408
- <h1 className="text-2xl font-bold text-[var(--tiger-text,#111827)] mb-6">
409
- Tigercat + React
410
- </h1>
477
+ <div className="flex items-center justify-between mb-6">
478
+ <h1 className="text-2xl font-bold text-[var(--tiger-text,#111827)]">
479
+ Tigercat + React
480
+ </h1>
481
+ <div className="flex items-center gap-4 text-sm text-[var(--tiger-text-muted,#6b7280)]">
482
+ <label className="flex items-center gap-2">
483
+ <span>Modern</span>
484
+ <Switch checked={modern} size="sm" onChange={onModern} />
485
+ </label>
486
+ <label className="flex items-center gap-2">
487
+ <span>Dark</span>
488
+ <Switch checked={dark} size="sm" onChange={onDark} />
489
+ </label>
490
+ </div>
491
+ </div>
411
492
 
412
493
  <div className="space-y-4">
413
494
  <Alert variant="info">
414
495
  Welcome to your Tigercat project! Edit src/App.tsx to get started.
496
+ Toggle <code>Modern</code> to preview the opt-in modern visual style
497
+ (radius / shadow / motion tokens).
415
498
  </Alert>
416
499
 
417
500
  <div className="flex gap-2">
@@ -427,42 +510,30 @@ export default function App() {
427
510
  }
428
511
  function commonStyleCss2() {
429
512
  return `@import "tailwindcss";
513
+ @plugin "@expcat/tigercat-core/tailwind/modern";
430
514
 
431
- :root {
432
- --tiger-primary: #2563eb;
433
- --tiger-primary-hover: #1d4ed8;
434
- --tiger-surface: #ffffff;
435
- --tiger-surface-muted: #f9fafb;
436
- --tiger-text: #111827;
437
- --tiger-text-muted: #6b7280;
438
- --tiger-border: #e5e7eb;
439
- }
440
-
441
- @media (prefers-color-scheme: dark) {
442
- :root {
443
- --tiger-primary: #3b82f6;
444
- --tiger-primary-hover: #2563eb;
445
- --tiger-surface: #111827;
446
- --tiger-surface-muted: #1f2937;
447
- --tiger-text: #f9fafb;
448
- --tiger-text-muted: #9ca3af;
449
- --tiger-border: #374151;
450
- }
515
+ /*
516
+ * The tigercat tailwind plugin injects every --tiger-* design token for
517
+ * both light (:root) and dark (.dark) modes, plus the opt-in modern
518
+ * overrides activated by data-tiger-style="modern". The demo App toggles
519
+ * dark mode via .dark on <html> and prefers-color-scheme via the rule
520
+ * below. Swap the @plugin line for a tailwind.config.ts calling
521
+ * createTigercatPlugin({ preset }) to use a custom preset.
522
+ */
451
523
 
452
- html {
453
- color-scheme: dark;
454
- }
524
+ html {
525
+ color-scheme: light dark;
455
526
  }
456
527
  `;
457
528
  }
458
529
 
459
530
  // src/commands/create.ts
460
531
  function createCreateCommand() {
461
- 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) => {
462
- 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));
463
534
  });
464
535
  }
465
- async function runCreate(name, templateArg) {
536
+ async function runCreate(name, templateArg, dryRun = false) {
466
537
  let template;
467
538
  if (templateArg && TEMPLATES.includes(templateArg)) {
468
539
  template = templateArg;
@@ -483,7 +554,7 @@ async function runCreate(name, templateArg) {
483
554
  template = response.template;
484
555
  }
485
556
  const targetDir = resolve(process.cwd(), name);
486
- if (existsSync(targetDir) && !isDirEmpty(targetDir)) {
557
+ if (!dryRun && existsSync(targetDir) && !isDirEmpty(targetDir)) {
487
558
  const { overwrite } = await prompts({
488
559
  type: "confirm",
489
560
  name: "overwrite",
@@ -497,6 +568,13 @@ async function runCreate(name, templateArg) {
497
568
  }
498
569
  logInfo(`Creating ${template} project in ${targetDir}...`);
499
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
+ }
500
578
  const totalSteps = Object.keys(files).length;
501
579
  let step = 0;
502
580
  ensureDir(targetDir);
@@ -513,8 +591,8 @@ async function runCreate(name, templateArg) {
513
591
  console.log(" pnpm dev\n");
514
592
  }
515
593
  function createAddCommand() {
516
- 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) => {
517
- 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);
518
596
  });
519
597
  }
520
598
  function detectFramework(cwd) {
@@ -529,6 +607,45 @@ function detectFramework(cwd) {
529
607
  }
530
608
  return null;
531
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
+ }
532
649
  function validateComponents(names) {
533
650
  const valid = [];
534
651
  const invalid = [];
@@ -542,18 +659,20 @@ function validateComponents(names) {
542
659
  }
543
660
  return { valid, invalid };
544
661
  }
545
- async function runAdd(components) {
662
+ async function runAdd(components, options = {}) {
546
663
  const cwd = process.cwd();
547
- const framework = detectFramework(cwd);
664
+ const framework = normalizeFramework(options.framework) ?? detectFramework(cwd);
665
+ const dryRun = Boolean(options.dryRun);
548
666
  if (!framework) {
549
667
  logError(
550
668
  "Could not detect framework. Make sure you are in a project with @expcat/tigercat-vue or @expcat/tigercat-react installed."
551
669
  );
552
670
  process.exit(1);
553
671
  }
554
- const { valid, invalid } = validateComponents(components);
672
+ const selectedComponents = await resolveComponents(components);
673
+ const { valid, invalid } = validateComponents(selectedComponents);
555
674
  if (invalid.length > 0) {
556
- logWarn(`Unknown components: ${invalid.join(", ")}`);
675
+ logWarn2(`Unknown components: ${invalid.join(", ")}`);
557
676
  logInfo(`Available: ${ALL_COMPONENTS.join(", ")}`);
558
677
  }
559
678
  if (valid.length === 0) {
@@ -561,11 +680,34 @@ async function runAdd(components) {
561
680
  process.exit(1);
562
681
  }
563
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
+ }
564
696
  const importLine = `import { ${valid.join(", ")} } from '${pkgName}'`;
565
697
  logSuccess(`Add this import to your project:
566
698
  `);
567
699
  console.log(` ${importLine}
568
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
+ }
569
711
  if (framework === "vue3") {
570
712
  logInfo("Vue 3 usage example:\n");
571
713
  for (const comp of valid) {
@@ -582,11 +724,18 @@ async function runAdd(components) {
582
724
  if (!existsSync(sampleDir)) {
583
725
  return;
584
726
  }
727
+ if (dryRun) {
728
+ logInfo("Dry run: no demo files will be written.");
729
+ }
585
730
  for (const comp of valid) {
586
731
  const ext = framework === "vue3" ? "vue" : "tsx";
587
732
  const sampleFile = join(sampleDir, `${comp}Demo.${ext}`);
588
733
  if (existsSync(sampleFile)) {
589
- logWarn(`${sampleFile} already exists, skipping`);
734
+ logWarn2(`${sampleFile} already exists, skipping`);
735
+ continue;
736
+ }
737
+ if (dryRun) {
738
+ logInfo(`Would create ${sampleFile}`);
590
739
  continue;
591
740
  }
592
741
  const content = framework === "vue3" ? generateVue3Demo(comp, pkgName) : generateReactDemo(comp, pkgName);
@@ -594,6 +743,14 @@ async function runAdd(components) {
594
743
  logSuccess(`Created ${sampleFile}`);
595
744
  }
596
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
+ }
597
754
  function generateVue3Demo(component, pkg) {
598
755
  return `<script setup lang="ts">
599
756
  import { ${component} } from '${pkg}'
@@ -621,11 +778,11 @@ export default function ${component}Demo() {
621
778
  `;
622
779
  }
623
780
  function createPlaygroundCommand() {
624
- 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) => {
625
- 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));
626
783
  });
627
784
  }
628
- async function runPlayground(templateArg, port = "3456") {
785
+ async function runPlayground(templateArg, port = "3456", open = true, dryRun = false) {
629
786
  let template;
630
787
  if (templateArg && TEMPLATES.includes(templateArg)) {
631
788
  template = templateArg;
@@ -647,6 +804,19 @@ async function runPlayground(templateArg, port = "3456") {
647
804
  }
648
805
  const tmpDir = resolve(process.cwd(), ".tigercat-playground");
649
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
+ }
650
820
  if (!existsSync(projectDir)) {
651
821
  logInfo(`Setting up ${template} playground...`);
652
822
  const files = template === "vue3" ? getVue3Template("playground") : getReactTemplate("playground");
@@ -670,14 +840,23 @@ async function runPlayground(templateArg, port = "3456") {
670
840
  `);
671
841
  try {
672
842
  const safePort = /^\d+$/.test(port) ? port : "3456";
673
- 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" });
674
845
  } catch {
675
846
  }
676
847
  }
677
848
  function createGenerateCommand() {
678
849
  const cmd = new Command("generate").description("Code generation utilities");
679
- 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) => {
680
- 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));
681
860
  });
682
861
  return cmd;
683
862
  }
@@ -734,7 +913,7 @@ function generateMarkdown(doc) {
734
913
  }
735
914
  return lines.join("\n");
736
915
  }
737
- async function runGenerateDocs(inputDir, outputDir) {
916
+ async function runGenerateDocs(inputDir, outputDir, dryRun = false) {
738
917
  const resolvedInput = resolve(process.cwd(), inputDir);
739
918
  const resolvedOutput = resolve(process.cwd(), outputDir);
740
919
  if (!existsSync(resolvedInput)) {
@@ -743,7 +922,11 @@ async function runGenerateDocs(inputDir, outputDir) {
743
922
  }
744
923
  const files = readdirSync(resolvedInput).filter((f) => f.endsWith(".ts") && f !== "index.ts").sort();
745
924
  logInfo(`Found ${files.length} type files in ${inputDir}`);
746
- ensureDir(resolvedOutput);
925
+ if (dryRun) {
926
+ logInfo("Dry run: no documentation files will be written.");
927
+ } else {
928
+ ensureDir(resolvedOutput);
929
+ }
747
930
  const docs = [];
748
931
  let step = 0;
749
932
  for (const file of files) {
@@ -753,7 +936,12 @@ async function runGenerateDocs(inputDir, outputDir) {
753
936
  if (doc) {
754
937
  docs.push(doc);
755
938
  const md = generateMarkdown(doc);
756
- 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
+ }
757
945
  }
758
946
  }
759
947
  const indexLines = [
@@ -776,9 +964,533 @@ async function runGenerateDocs(inputDir, outputDir) {
776
964
  }
777
965
  indexLines.push("");
778
966
  }
779
- 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"));
780
974
  logSuccess(`Generated docs for ${docs.length} components in ${outputDir}`);
781
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
+ }
1134
+ var MIN_NODE_MAJOR = 20;
1135
+ var MIN_PNPM_MAJOR = 8;
1136
+ var REQUIRED_TAILWIND_MAJOR = 4;
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
+ ];
1145
+ var FRAMEWORK_REQUIREMENTS = {
1146
+ vue3: {
1147
+ peers: ["@expcat/tigercat-vue", "@expcat/tigercat-core", "vue"],
1148
+ templateDeps: ["@tailwindcss/vite", "@vitejs/plugin-vue", "typescript", "vite", "vue-tsc"]
1149
+ },
1150
+ react: {
1151
+ peers: ["@expcat/tigercat-react", "@expcat/tigercat-core", "react", "react-dom"],
1152
+ templateDeps: ["@tailwindcss/vite", "@vitejs/plugin-react", "typescript", "vite"]
1153
+ }
1154
+ };
1155
+ function createDoctorCommand() {
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));
1158
+ });
1159
+ }
1160
+ function collectDoctorChecks(options = {}) {
1161
+ const cwd = options.cwd ?? process.cwd();
1162
+ const env = options.env ?? process.env;
1163
+ const packageResult = readProjectPackage(cwd);
1164
+ const nodeVersion = options.nodeVersion ?? process.versions.node;
1165
+ const checks = [createPackageCheck(packageResult)];
1166
+ checks.push(createNodeCheck(nodeVersion));
1167
+ checks.push(createPnpmCheck(packageResult.packageJson, env));
1168
+ if (!packageResult.packageJson) {
1169
+ return checks;
1170
+ }
1171
+ checks.push(createTailwindCheck(packageResult.packageJson));
1172
+ checks.push(createPeerDepsCheck(packageResult.packageJson));
1173
+ checks.push(createTemplateCompatibilityCheck(packageResult.packageJson));
1174
+ checks.push(createCompatibilityMatrixCheck(packageResult.packageJson));
1175
+ return checks;
1176
+ }
1177
+ function runDoctor(json = false) {
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
+ }
1196
+ logInfo("Running Tigercat project checks...");
1197
+ console.log();
1198
+ for (const check of checks) {
1199
+ printCheck(check);
1200
+ }
1201
+ console.log();
1202
+ const failures = checks.filter((check) => check.status === "fail");
1203
+ const warnings = checks.filter((check) => check.status === "warn");
1204
+ if (failures.length > 0) {
1205
+ logError(`${failures.length} check${failures.length === 1 ? "" : "s"} failed`);
1206
+ process.exit(1);
1207
+ }
1208
+ if (warnings.length > 0) {
1209
+ logWarn2(`${warnings.length} warning${warnings.length === 1 ? "" : "s"} found`);
1210
+ return;
1211
+ }
1212
+ logSuccess("All checks passed");
1213
+ }
1214
+ function readProjectPackage(cwd) {
1215
+ const packagePath = join(cwd, "package.json");
1216
+ const content = readFileSafe(packagePath);
1217
+ if (!content) {
1218
+ return { packageJson: null, error: `package.json not found in ${cwd}` };
1219
+ }
1220
+ try {
1221
+ return { packageJson: JSON.parse(content) };
1222
+ } catch {
1223
+ return { packageJson: null, error: "package.json is not valid JSON" };
1224
+ }
1225
+ }
1226
+ function createPackageCheck(result) {
1227
+ if (!result.packageJson) {
1228
+ return {
1229
+ name: "Project package",
1230
+ status: "fail",
1231
+ message: result.error ?? "package.json could not be read",
1232
+ suggestions: ["Run this command from a project root that contains package.json"]
1233
+ };
1234
+ }
1235
+ return {
1236
+ name: "Project package",
1237
+ status: "pass",
1238
+ message: "package.json is readable"
1239
+ };
1240
+ }
1241
+ function createNodeCheck(version) {
1242
+ const parsed = parseVersion(version);
1243
+ if (!parsed || parsed.major < MIN_NODE_MAJOR) {
1244
+ return {
1245
+ name: "Node.js",
1246
+ status: "fail",
1247
+ message: `Node ${MIN_NODE_MAJOR}+ is required, current version is ${version}`,
1248
+ suggestions: [`Install Node ${MIN_NODE_MAJOR}+ and rerun tigercat doctor`]
1249
+ };
1250
+ }
1251
+ return {
1252
+ name: "Node.js",
1253
+ status: "pass",
1254
+ message: `Node ${version} satisfies >=${MIN_NODE_MAJOR}`
1255
+ };
1256
+ }
1257
+ function createPnpmCheck(packageJson, env) {
1258
+ const version = getPnpmVersion(packageJson, env);
1259
+ if (!version) {
1260
+ return {
1261
+ name: "pnpm",
1262
+ status: "warn",
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"]
1265
+ };
1266
+ }
1267
+ const parsed = parseVersion(version);
1268
+ if (!parsed || parsed.major < MIN_PNPM_MAJOR) {
1269
+ return {
1270
+ name: "pnpm",
1271
+ status: "fail",
1272
+ message: `pnpm ${MIN_PNPM_MAJOR}+ is required, detected ${version}`,
1273
+ suggestions: [`Upgrade pnpm to ${MIN_PNPM_MAJOR}+`]
1274
+ };
1275
+ }
1276
+ return {
1277
+ name: "pnpm",
1278
+ status: "pass",
1279
+ message: `pnpm ${version} satisfies >=${MIN_PNPM_MAJOR}`
1280
+ };
1281
+ }
1282
+ function createTailwindCheck(packageJson) {
1283
+ const allDeps = collectDependencies2(packageJson);
1284
+ const tailwindRange = allDeps.tailwindcss;
1285
+ const vitePluginRange = allDeps["@tailwindcss/vite"];
1286
+ if (!tailwindRange) {
1287
+ return {
1288
+ name: "Tailwind CSS",
1289
+ status: "fail",
1290
+ message: "tailwindcss is missing; Tigercat requires Tailwind CSS 4+",
1291
+ suggestions: ["Install tailwindcss and @tailwindcss/vite"]
1292
+ };
1293
+ }
1294
+ const tailwindMajor = getRangeMajor(tailwindRange);
1295
+ if (tailwindMajor !== null && tailwindMajor < REQUIRED_TAILWIND_MAJOR) {
1296
+ return {
1297
+ name: "Tailwind CSS",
1298
+ status: "fail",
1299
+ message: `tailwindcss ${tailwindRange} is not compatible; use Tailwind CSS ${REQUIRED_TAILWIND_MAJOR}+`,
1300
+ suggestions: [`Upgrade tailwindcss to ${REQUIRED_TAILWIND_MAJOR}+`]
1301
+ };
1302
+ }
1303
+ if (tailwindMajor === null) {
1304
+ return {
1305
+ name: "Tailwind CSS",
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+"]
1317
+ };
1318
+ }
1319
+ const pluginMajor = getRangeMajor(vitePluginRange);
1320
+ if (pluginMajor !== null && pluginMajor < REQUIRED_TAILWIND_MAJOR) {
1321
+ return {
1322
+ name: "Tailwind CSS",
1323
+ status: "fail",
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`]
1334
+ };
1335
+ }
1336
+ return {
1337
+ name: "Tailwind CSS",
1338
+ status: "pass",
1339
+ message: `tailwindcss ${tailwindRange} uses the Tailwind CSS ${REQUIRED_TAILWIND_MAJOR} build pipeline`,
1340
+ details: [`@tailwindcss/vite ${vitePluginRange}`]
1341
+ };
1342
+ }
1343
+ function createPeerDepsCheck(packageJson) {
1344
+ const allDeps = collectDependencies2(packageJson);
1345
+ const frameworks = detectTigercatFrameworks(allDeps);
1346
+ if (frameworks.length === 0) {
1347
+ return {
1348
+ name: "Peer dependencies",
1349
+ status: "warn",
1350
+ message: "No Tigercat Vue or React package was detected",
1351
+ suggestions: ["Install @expcat/tigercat-vue or @expcat/tigercat-react"]
1352
+ };
1353
+ }
1354
+ const missing = [
1355
+ ...new Set(
1356
+ frameworks.flatMap(
1357
+ (framework) => FRAMEWORK_REQUIREMENTS[framework].peers.filter((dependency) => !allDeps[dependency])
1358
+ )
1359
+ )
1360
+ ];
1361
+ const incompatible = frameworks.flatMap(
1362
+ (framework) => FRAMEWORK_REQUIREMENTS[framework].peers.filter((dependency) => dependency.startsWith("@expcat/tigercat-")).filter((dependency) => isOlderMajor(allDeps[dependency], REQUIRED_TIGERCAT_MAJOR)).map((dependency) => `${dependency}@${allDeps[dependency]}`)
1363
+ );
1364
+ if (missing.length > 0 || incompatible.length > 0) {
1365
+ return {
1366
+ name: "Peer dependencies",
1367
+ status: "fail",
1368
+ message: "Tigercat peer dependencies are incomplete or incompatible",
1369
+ details: [...missing.map((dependency) => `Missing ${dependency}`), ...incompatible],
1370
+ suggestions: ["Run tigercat add --install or install the listed dependencies manually"]
1371
+ };
1372
+ }
1373
+ return {
1374
+ name: "Peer dependencies",
1375
+ status: "pass",
1376
+ message: `${frameworks.map(formatFramework).join(" + ")} peer dependencies are present`
1377
+ };
1378
+ }
1379
+ function createTemplateCompatibilityCheck(packageJson) {
1380
+ const allDeps = collectDependencies2(packageJson);
1381
+ const frameworks = detectTigercatFrameworks(allDeps);
1382
+ if (frameworks.length === 0) {
1383
+ return {
1384
+ name: "Template compatibility",
1385
+ status: "warn",
1386
+ message: "Skipped because no supported Tigercat framework package was detected",
1387
+ suggestions: ["Install a Tigercat framework package to enable template compatibility checks"]
1388
+ };
1389
+ }
1390
+ const missing = [
1391
+ ...new Set(
1392
+ frameworks.flatMap(
1393
+ (framework) => FRAMEWORK_REQUIREMENTS[framework].templateDeps.filter((dependency) => !allDeps[dependency])
1394
+ )
1395
+ )
1396
+ ];
1397
+ if (missing.length > 0) {
1398
+ return {
1399
+ name: "Template compatibility",
1400
+ status: "warn",
1401
+ message: "Project differs from current CLI template dependencies",
1402
+ details: missing.map((dependency) => `Template dependency not found: ${dependency}`),
1403
+ suggestions: ["Compare your project dependencies with the latest tigercat create template"]
1404
+ };
1405
+ }
1406
+ return {
1407
+ name: "Template compatibility",
1408
+ status: "pass",
1409
+ message: `${frameworks.map(formatFramework).join(" + ")} template dependencies are present`
1410
+ };
1411
+ }
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) {
1435
+ return {
1436
+ ...packageJson.peerDependencies,
1437
+ ...packageJson.dependencies,
1438
+ ...packageJson.devDependencies
1439
+ };
1440
+ }
1441
+ function detectTigercatFrameworks(dependencies) {
1442
+ const frameworks = [];
1443
+ if (dependencies["@expcat/tigercat-vue"]) {
1444
+ frameworks.push("vue3");
1445
+ }
1446
+ if (dependencies["@expcat/tigercat-react"]) {
1447
+ frameworks.push("react");
1448
+ }
1449
+ return frameworks;
1450
+ }
1451
+ function getPnpmVersion(packageJson, env) {
1452
+ const packageManager = packageJson?.packageManager;
1453
+ if (typeof packageManager === "string") {
1454
+ const match2 = /^pnpm@(.+)$/.exec(packageManager);
1455
+ if (match2) return match2[1];
1456
+ }
1457
+ const userAgent = env.npm_config_user_agent;
1458
+ const match = userAgent ? /pnpm\/(\d+\.\d+\.\d+)/.exec(userAgent) : null;
1459
+ return match?.[1] ?? null;
1460
+ }
1461
+ function parseVersion(value) {
1462
+ const match = /^(?:v)?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(value.trim());
1463
+ if (!match) return null;
1464
+ return {
1465
+ major: Number(match[1]),
1466
+ minor: Number(match[2] ?? 0),
1467
+ patch: Number(match[3] ?? 0)
1468
+ };
1469
+ }
1470
+ function getRangeMajor(range) {
1471
+ if (!range) return null;
1472
+ if (/^(workspace|file|link|catalog):/.test(range)) return null;
1473
+ const match = /(?:\^|~|>=|<=|>|<|=)?\s*v?(\d+)/.exec(range);
1474
+ return match ? Number(match[1]) : null;
1475
+ }
1476
+ function isOlderMajor(range, expectedMajor) {
1477
+ const major = getRangeMajor(range);
1478
+ return major !== null && major < expectedMajor;
1479
+ }
1480
+ function formatFramework(framework) {
1481
+ return framework === "vue3" ? "Vue 3" : "React";
1482
+ }
1483
+ function printCheck(check) {
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);
1486
+ console.log(`${symbol} ${name}: ${check.message}`);
1487
+ for (const detail of check.details ?? []) {
1488
+ console.log(` ${pc2.dim("-")} ${detail}`);
1489
+ }
1490
+ for (const suggestion of check.suggestions ?? []) {
1491
+ console.log(` ${pc2.dim("fix:")} ${suggestion}`);
1492
+ }
1493
+ }
782
1494
 
783
1495
  // src/index.ts
784
1496
  var program = new Command();
@@ -787,4 +1499,5 @@ program.addCommand(createCreateCommand());
787
1499
  program.addCommand(createAddCommand());
788
1500
  program.addCommand(createPlaygroundCommand());
789
1501
  program.addCommand(createGenerateCommand());
1502
+ program.addCommand(createDoctorCommand());
790
1503
  program.parse();