@expcat/tigercat-cli 1.3.4 → 1.4.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/dist/index.js +177 -73
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -5,14 +5,49 @@ import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync } from
|
|
|
5
5
|
import { resolve, join, dirname, basename } from 'path';
|
|
6
6
|
import pc2 from 'picocolors';
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
import { pathToFileURL } from 'url';
|
|
10
|
+
|
|
11
|
+
function logSuccess(msg) {
|
|
12
|
+
console.log(pc2.green("\u2714") + " " + msg);
|
|
13
|
+
}
|
|
14
|
+
function logInfo(msg) {
|
|
15
|
+
console.log(pc2.blue("\u2139") + " " + msg);
|
|
16
|
+
}
|
|
17
|
+
function logWarn(msg) {
|
|
18
|
+
console.log(pc2.yellow("\u26A0") + " " + msg);
|
|
19
|
+
}
|
|
20
|
+
function logError(msg) {
|
|
21
|
+
console.error(pc2.red("\u2716") + " " + msg);
|
|
22
|
+
}
|
|
23
|
+
function logStep(step, total, msg) {
|
|
24
|
+
console.log(pc2.dim(`[${step}/${total}]`) + " " + msg);
|
|
25
|
+
}
|
|
26
|
+
function ensureDir(dir) {
|
|
27
|
+
if (!existsSync(dir)) {
|
|
28
|
+
mkdirSync(dir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function writeFileSafe(filePath, content) {
|
|
32
|
+
ensureDir(dirname(filePath));
|
|
33
|
+
writeFileSync(filePath, content, "utf-8");
|
|
34
|
+
}
|
|
35
|
+
function isDirEmpty(dir) {
|
|
36
|
+
if (!existsSync(dir)) return true;
|
|
37
|
+
return readdirSync(dir).length === 0;
|
|
38
|
+
}
|
|
39
|
+
function readFileSafe(filePath) {
|
|
40
|
+
if (!existsSync(filePath)) return null;
|
|
41
|
+
return readFileSync(filePath, "utf-8");
|
|
42
|
+
}
|
|
8
43
|
|
|
9
44
|
// src/constants.ts
|
|
10
45
|
var CLI_NAME = "tigercat";
|
|
11
|
-
var CLI_VERSION = "1.
|
|
46
|
+
var CLI_VERSION = "1.4.0";
|
|
12
47
|
var TEMPLATES = ["vue3", "react"];
|
|
13
48
|
var TEMPLATE_VERSIONS = {
|
|
14
49
|
// Tigercat packages (use caret on latest major)
|
|
15
|
-
tigercat: "^1.
|
|
50
|
+
tigercat: "^1.4.0",
|
|
16
51
|
// Frameworks
|
|
17
52
|
vue: "^3.5.33",
|
|
18
53
|
react: "^19.2.5",
|
|
@@ -86,37 +121,50 @@ var COMPONENT_CATEGORIES = {
|
|
|
86
121
|
]
|
|
87
122
|
};
|
|
88
123
|
var ALL_COMPONENTS = Object.values(COMPONENT_CATEGORIES).flat();
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
console.error(pc2.red("\u2716") + " " + msg);
|
|
100
|
-
}
|
|
101
|
-
function logStep(step, total, msg) {
|
|
102
|
-
console.log(pc2.dim(`[${step}/${total}]`) + " " + msg);
|
|
103
|
-
}
|
|
104
|
-
function ensureDir(dir) {
|
|
105
|
-
if (!existsSync(dir)) {
|
|
106
|
-
mkdirSync(dir, { recursive: true });
|
|
124
|
+
|
|
125
|
+
// src/utils/validate.ts
|
|
126
|
+
var PACKAGE_NAME_REGEX = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
127
|
+
function validateProjectName(name) {
|
|
128
|
+
if (name.trim().length === 0) return "Project name must not be empty";
|
|
129
|
+
if (name.trim() !== name) return "Project name must not have leading or trailing spaces";
|
|
130
|
+
if (name.length > 214) return "Project name must be 214 characters or fewer";
|
|
131
|
+
if (/[A-Z]/.test(name)) return "Project name must not contain uppercase letters";
|
|
132
|
+
if (!PACKAGE_NAME_REGEX.test(name)) {
|
|
133
|
+
return 'Project name is not a valid npm package name (use lowercase letters, digits, "-", "_" or ".")';
|
|
107
134
|
}
|
|
135
|
+
return null;
|
|
108
136
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
137
|
+
function suggestProjectName(name) {
|
|
138
|
+
const sanitizeSegment = (segment) => segment.trim().toLowerCase().replace(/[^a-z0-9-._~]+/g, "-").replace(/^[-._~]+|[-._~]+$/g, "").replace(/-{2,}/g, "-");
|
|
139
|
+
const scopeMatch = /^@(.+?)\/(.+)$/.exec(name.trim());
|
|
140
|
+
const suggestion = scopeMatch ? `@${sanitizeSegment(scopeMatch[1])}/${sanitizeSegment(scopeMatch[2])}` : sanitizeSegment(name);
|
|
141
|
+
return suggestion || "tigercat-app";
|
|
112
142
|
}
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
return readdirSync(dir).length === 0;
|
|
143
|
+
function isFramework(value) {
|
|
144
|
+
return value === "vue3" || value === "react";
|
|
116
145
|
}
|
|
117
|
-
function
|
|
118
|
-
if (
|
|
119
|
-
|
|
146
|
+
async function resolveTemplateOption(arg, promptMessage) {
|
|
147
|
+
if (arg !== void 0) {
|
|
148
|
+
if (!TEMPLATES.includes(arg)) {
|
|
149
|
+
logError(`Invalid template "${arg}". Valid templates: ${TEMPLATES.join(", ")}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
return arg;
|
|
153
|
+
}
|
|
154
|
+
const response = await prompts({
|
|
155
|
+
type: "select",
|
|
156
|
+
name: "template",
|
|
157
|
+
message: promptMessage,
|
|
158
|
+
choices: [
|
|
159
|
+
{ title: "Vue 3", value: "vue3" },
|
|
160
|
+
{ title: "React", value: "react" }
|
|
161
|
+
]
|
|
162
|
+
});
|
|
163
|
+
if (!response.template) {
|
|
164
|
+
logError("Operation cancelled");
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
return response.template;
|
|
120
168
|
}
|
|
121
169
|
|
|
122
170
|
// src/templates/vue3.ts
|
|
@@ -534,31 +582,18 @@ function createCreateCommand() {
|
|
|
534
582
|
});
|
|
535
583
|
}
|
|
536
584
|
async function runCreate(name, templateArg, dryRun = false) {
|
|
537
|
-
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const response = await prompts({
|
|
542
|
-
type: "select",
|
|
543
|
-
name: "template",
|
|
544
|
-
message: "Select a framework",
|
|
545
|
-
choices: [
|
|
546
|
-
{ title: "Vue 3", value: "vue3" },
|
|
547
|
-
{ title: "React", value: "react" }
|
|
548
|
-
]
|
|
549
|
-
});
|
|
550
|
-
if (!response.template) {
|
|
551
|
-
logError("Operation cancelled");
|
|
552
|
-
process.exit(1);
|
|
553
|
-
}
|
|
554
|
-
template = response.template;
|
|
585
|
+
const nameError = validateProjectName(name);
|
|
586
|
+
if (nameError) {
|
|
587
|
+
logError(`${nameError}. Try "${suggestProjectName(name)}" instead.`);
|
|
588
|
+
process.exit(1);
|
|
555
589
|
}
|
|
590
|
+
const template = await resolveTemplateOption(templateArg, "Select a framework");
|
|
556
591
|
const targetDir = resolve(process.cwd(), name);
|
|
557
592
|
if (!dryRun && existsSync(targetDir) && !isDirEmpty(targetDir)) {
|
|
558
593
|
const { overwrite } = await prompts({
|
|
559
594
|
type: "confirm",
|
|
560
595
|
name: "overwrite",
|
|
561
|
-
message: `Directory "${name}" is not empty.
|
|
596
|
+
message: `Directory "${name}" is not empty. Overwrite conflicting template files? (other files are kept)`,
|
|
562
597
|
initial: false
|
|
563
598
|
});
|
|
564
599
|
if (!overwrite) {
|
|
@@ -608,8 +643,7 @@ function detectFramework(cwd) {
|
|
|
608
643
|
return null;
|
|
609
644
|
}
|
|
610
645
|
function normalizeFramework(value) {
|
|
611
|
-
|
|
612
|
-
return null;
|
|
646
|
+
return value !== void 0 && isFramework(value) ? value : null;
|
|
613
647
|
}
|
|
614
648
|
async function resolveComponents(components) {
|
|
615
649
|
if (components.length > 0) return components;
|
|
@@ -661,6 +695,10 @@ function validateComponents(names) {
|
|
|
661
695
|
}
|
|
662
696
|
async function runAdd(components, options = {}) {
|
|
663
697
|
const cwd = process.cwd();
|
|
698
|
+
if (options.framework !== void 0 && !isFramework(options.framework)) {
|
|
699
|
+
logError(`Invalid framework "${options.framework}". Valid frameworks: vue3, react`);
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
664
702
|
const framework = normalizeFramework(options.framework) ?? detectFramework(cwd);
|
|
665
703
|
const dryRun = Boolean(options.dryRun);
|
|
666
704
|
if (!framework) {
|
|
@@ -672,7 +710,7 @@ async function runAdd(components, options = {}) {
|
|
|
672
710
|
const selectedComponents = await resolveComponents(components);
|
|
673
711
|
const { valid, invalid } = validateComponents(selectedComponents);
|
|
674
712
|
if (invalid.length > 0) {
|
|
675
|
-
|
|
713
|
+
logWarn(`Unknown components: ${invalid.join(", ")}`);
|
|
676
714
|
logInfo(`Available: ${ALL_COMPONENTS.join(", ")}`);
|
|
677
715
|
}
|
|
678
716
|
if (valid.length === 0) {
|
|
@@ -731,7 +769,7 @@ async function runAdd(components, options = {}) {
|
|
|
731
769
|
const ext = framework === "vue3" ? "vue" : "tsx";
|
|
732
770
|
const sampleFile = join(sampleDir, `${comp}Demo.${ext}`);
|
|
733
771
|
if (existsSync(sampleFile)) {
|
|
734
|
-
|
|
772
|
+
logWarn(`${sampleFile} already exists, skipping`);
|
|
735
773
|
continue;
|
|
736
774
|
}
|
|
737
775
|
if (dryRun) {
|
|
@@ -783,25 +821,10 @@ function createPlaygroundCommand() {
|
|
|
783
821
|
});
|
|
784
822
|
}
|
|
785
823
|
async function runPlayground(templateArg, port = "3456", open = true, dryRun = false) {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
const response = await prompts({
|
|
791
|
-
type: "select",
|
|
792
|
-
name: "template",
|
|
793
|
-
message: "Select a framework for playground",
|
|
794
|
-
choices: [
|
|
795
|
-
{ title: "Vue 3", value: "vue3" },
|
|
796
|
-
{ title: "React", value: "react" }
|
|
797
|
-
]
|
|
798
|
-
});
|
|
799
|
-
if (!response.template) {
|
|
800
|
-
logError("Operation cancelled");
|
|
801
|
-
process.exit(1);
|
|
802
|
-
}
|
|
803
|
-
template = response.template;
|
|
804
|
-
}
|
|
824
|
+
const template = await resolveTemplateOption(
|
|
825
|
+
templateArg,
|
|
826
|
+
"Select a framework for playground"
|
|
827
|
+
);
|
|
805
828
|
const tmpDir = resolve(process.cwd(), ".tigercat-playground");
|
|
806
829
|
const projectDir = join(tmpDir, `playground-${template}`);
|
|
807
830
|
if (dryRun) {
|
|
@@ -1135,6 +1158,20 @@ var MIN_NODE_MAJOR = 20;
|
|
|
1135
1158
|
var MIN_PNPM_MAJOR = 8;
|
|
1136
1159
|
var REQUIRED_TAILWIND_MAJOR = 4;
|
|
1137
1160
|
var REQUIRED_TIGERCAT_MAJOR = 1;
|
|
1161
|
+
var FRAMEWORK_PEER_RANGES = {
|
|
1162
|
+
vue3: [{ dep: "vue", major: 3 }],
|
|
1163
|
+
react: [
|
|
1164
|
+
{ dep: "react", major: 19 },
|
|
1165
|
+
{ dep: "react-dom", major: 19 }
|
|
1166
|
+
]
|
|
1167
|
+
};
|
|
1168
|
+
var REQUIRED_CORE_EXPORTS = [
|
|
1169
|
+
".",
|
|
1170
|
+
"./tailwind",
|
|
1171
|
+
"./tailwind/modern",
|
|
1172
|
+
"./tokens.css",
|
|
1173
|
+
"./figma-variables.json"
|
|
1174
|
+
];
|
|
1138
1175
|
var VERSION_COMPATIBILITY_MATRIX = [
|
|
1139
1176
|
{ name: "Node.js", range: ">=20.11.0", reason: "Matches workspace engines and CLI templates" },
|
|
1140
1177
|
{ name: "pnpm", range: ">=8.0.0", reason: "Required by workspace package management" },
|
|
@@ -1160,6 +1197,7 @@ function createDoctorCommand() {
|
|
|
1160
1197
|
function collectDoctorChecks(options = {}) {
|
|
1161
1198
|
const cwd = options.cwd ?? process.cwd();
|
|
1162
1199
|
const env = options.env ?? process.env;
|
|
1200
|
+
const readCorePackageJson = options.readCorePackageJson ?? defaultReadCorePackageJson;
|
|
1163
1201
|
const packageResult = readProjectPackage(cwd);
|
|
1164
1202
|
const nodeVersion = options.nodeVersion ?? process.versions.node;
|
|
1165
1203
|
const checks = [createPackageCheck(packageResult)];
|
|
@@ -1172,6 +1210,14 @@ function collectDoctorChecks(options = {}) {
|
|
|
1172
1210
|
checks.push(createPeerDepsCheck(packageResult.packageJson));
|
|
1173
1211
|
checks.push(createTemplateCompatibilityCheck(packageResult.packageJson));
|
|
1174
1212
|
checks.push(createCompatibilityMatrixCheck(packageResult.packageJson));
|
|
1213
|
+
const coreExportsCheck = createCoreExportsCheck(
|
|
1214
|
+
packageResult.packageJson,
|
|
1215
|
+
cwd,
|
|
1216
|
+
readCorePackageJson
|
|
1217
|
+
);
|
|
1218
|
+
if (coreExportsCheck) {
|
|
1219
|
+
checks.push(coreExportsCheck);
|
|
1220
|
+
}
|
|
1175
1221
|
return checks;
|
|
1176
1222
|
}
|
|
1177
1223
|
function runDoctor(json = false) {
|
|
@@ -1206,7 +1252,7 @@ function runDoctor(json = false) {
|
|
|
1206
1252
|
process.exit(1);
|
|
1207
1253
|
}
|
|
1208
1254
|
if (warnings.length > 0) {
|
|
1209
|
-
|
|
1255
|
+
logWarn(`${warnings.length} warning${warnings.length === 1 ? "" : "s"} found`);
|
|
1210
1256
|
return;
|
|
1211
1257
|
}
|
|
1212
1258
|
logSuccess("All checks passed");
|
|
@@ -1424,13 +1470,71 @@ function createCompatibilityMatrixCheck(packageJson) {
|
|
|
1424
1470
|
suggestions: ["Install a Tigercat Vue or React package to validate framework peer ranges"]
|
|
1425
1471
|
};
|
|
1426
1472
|
}
|
|
1473
|
+
const incompatible = frameworks.flatMap(
|
|
1474
|
+
(framework) => FRAMEWORK_PEER_RANGES[framework].filter(({ dep, major }) => isOlderMajor(dependencies[dep], major)).map(({ dep, major }) => `${dep}@${dependencies[dep]} is below the supported major ${major}`)
|
|
1475
|
+
);
|
|
1476
|
+
if (incompatible.length > 0) {
|
|
1477
|
+
return {
|
|
1478
|
+
name: "Version compatibility matrix",
|
|
1479
|
+
status: "fail",
|
|
1480
|
+
message: "Installed framework versions are outside the supported range",
|
|
1481
|
+
details: [...incompatible, ...details],
|
|
1482
|
+
suggestions: ["Upgrade the listed framework packages to the supported major versions"]
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1427
1485
|
return {
|
|
1428
1486
|
name: "Version compatibility matrix",
|
|
1429
1487
|
status: "pass",
|
|
1430
|
-
message: `${frameworks.map(formatFramework).join(" + ")} compatibility matrix
|
|
1488
|
+
message: `${frameworks.map(formatFramework).join(" + ")} versions satisfy the compatibility matrix`,
|
|
1431
1489
|
details
|
|
1432
1490
|
};
|
|
1433
1491
|
}
|
|
1492
|
+
function createCoreExportsCheck(packageJson, cwd, readCorePackageJson) {
|
|
1493
|
+
const dependencies = collectDependencies2(packageJson);
|
|
1494
|
+
if (!dependencies["@expcat/tigercat-core"]) {
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
const corePackageJson = readCorePackageJson(cwd);
|
|
1498
|
+
if (!corePackageJson) {
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
const exportsMap = corePackageJson.exports ?? {};
|
|
1502
|
+
const missing = REQUIRED_CORE_EXPORTS.filter((subpath) => !(subpath in exportsMap));
|
|
1503
|
+
if (missing.length > 0) {
|
|
1504
|
+
return {
|
|
1505
|
+
name: "Core exports",
|
|
1506
|
+
status: "fail",
|
|
1507
|
+
message: "@expcat/tigercat-core is missing required export subpaths",
|
|
1508
|
+
details: missing.map((subpath) => `Missing export: ${subpath}`),
|
|
1509
|
+
suggestions: ["Upgrade @expcat/tigercat-core or reinstall dependencies"]
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
return {
|
|
1513
|
+
name: "Core exports",
|
|
1514
|
+
status: "pass",
|
|
1515
|
+
message: "@expcat/tigercat-core exposes the required Tailwind, token and entry exports"
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
function defaultReadCorePackageJson(cwd) {
|
|
1519
|
+
try {
|
|
1520
|
+
const requireFromProject = createRequire(pathToFileURL(join(cwd, "package.json")));
|
|
1521
|
+
const mainEntry = requireFromProject.resolve("@expcat/tigercat-core");
|
|
1522
|
+
let dir = dirname(mainEntry);
|
|
1523
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
1524
|
+
const content = readFileSafe(join(dir, "package.json"));
|
|
1525
|
+
if (content) {
|
|
1526
|
+
const parsed = JSON.parse(content);
|
|
1527
|
+
if (parsed.name === "@expcat/tigercat-core") return parsed;
|
|
1528
|
+
}
|
|
1529
|
+
const parent = dirname(dir);
|
|
1530
|
+
if (parent === dir) break;
|
|
1531
|
+
dir = parent;
|
|
1532
|
+
}
|
|
1533
|
+
return null;
|
|
1534
|
+
} catch {
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1434
1538
|
function collectDependencies2(packageJson) {
|
|
1435
1539
|
return {
|
|
1436
1540
|
...packageJson.peerDependencies,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expcat/tigercat-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI tooling for Tigercat UI library — project scaffolding, component generation, and more",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,8 +28,7 @@
|
|
|
28
28
|
"module": "./dist/index.js",
|
|
29
29
|
"types": "./dist/index.d.ts",
|
|
30
30
|
"files": [
|
|
31
|
-
"dist"
|
|
32
|
-
"templates"
|
|
31
|
+
"dist"
|
|
33
32
|
],
|
|
34
33
|
"publishConfig": {
|
|
35
34
|
"access": "public"
|