@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.
- package/README.md +138 -1
- package/dist/index.js +819 -106
- 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
|
|
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(
|
|
90
|
+
console.log(pc2.green("\u2714") + " " + msg);
|
|
69
91
|
}
|
|
70
92
|
function logInfo(msg) {
|
|
71
|
-
console.log(
|
|
93
|
+
console.log(pc2.blue("\u2139") + " " + msg);
|
|
72
94
|
}
|
|
73
|
-
function
|
|
74
|
-
console.log(
|
|
95
|
+
function logWarn2(msg) {
|
|
96
|
+
console.log(pc2.yellow("\u26A0") + " " + msg);
|
|
75
97
|
}
|
|
76
98
|
function logError(msg) {
|
|
77
|
-
console.error(
|
|
99
|
+
console.error(pc2.red("\u2716") + " " + msg);
|
|
78
100
|
}
|
|
79
101
|
function logStep(step, total, msg) {
|
|
80
|
-
console.log(
|
|
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":
|
|
127
|
-
vue:
|
|
148
|
+
"@expcat/tigercat-vue": TEMPLATE_VERSIONS.tigercat,
|
|
149
|
+
vue: TEMPLATE_VERSIONS.vue
|
|
128
150
|
},
|
|
129
151
|
devDependencies: {
|
|
130
|
-
"@
|
|
131
|
-
"@
|
|
132
|
-
"@vue
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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: "
|
|
171
|
+
target: "ES2022",
|
|
149
172
|
module: "ESNext",
|
|
150
|
-
lib: ["
|
|
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 {
|
|
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
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
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<{}, {},
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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":
|
|
299
|
-
react:
|
|
300
|
-
"react-dom":
|
|
349
|
+
"@expcat/tigercat-react": TEMPLATE_VERSIONS.tigercat,
|
|
350
|
+
react: TEMPLATE_VERSIONS.react,
|
|
351
|
+
"react-dom": TEMPLATE_VERSIONS.reactDom
|
|
301
352
|
},
|
|
302
353
|
devDependencies: {
|
|
303
|
-
"@
|
|
304
|
-
"@
|
|
305
|
-
"@types/react
|
|
306
|
-
"@
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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: "
|
|
372
|
+
target: "ES2022",
|
|
321
373
|
useDefineForClassFields: true,
|
|
322
|
-
lib: ["
|
|
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 {
|
|
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
|
-
<
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
453
|
-
|
|
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("
|
|
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
|
|
672
|
+
const selectedComponents = await resolveComponents(components);
|
|
673
|
+
const { valid, invalid } = validateComponents(selectedComponents);
|
|
555
674
|
if (invalid.length > 0) {
|
|
556
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|