@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.
Files changed (2) hide show
  1. package/dist/index.js +177 -73
  2. 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.3.4";
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.3.4",
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
- function logSuccess(msg) {
90
- console.log(pc2.green("\u2714") + " " + msg);
91
- }
92
- function logInfo(msg) {
93
- console.log(pc2.blue("\u2139") + " " + msg);
94
- }
95
- function logWarn2(msg) {
96
- console.log(pc2.yellow("\u26A0") + " " + msg);
97
- }
98
- function logError(msg) {
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 writeFileSafe(filePath, content) {
110
- ensureDir(dirname(filePath));
111
- writeFileSync(filePath, content, "utf-8");
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 isDirEmpty(dir) {
114
- if (!existsSync(dir)) return true;
115
- return readdirSync(dir).length === 0;
143
+ function isFramework(value) {
144
+ return value === "vue3" || value === "react";
116
145
  }
117
- function readFileSafe(filePath) {
118
- if (!existsSync(filePath)) return null;
119
- return readFileSync(filePath, "utf-8");
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
- let template;
538
- if (templateArg && TEMPLATES.includes(templateArg)) {
539
- template = templateArg;
540
- } else {
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. Remove existing files and continue?`,
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
- if (value === "vue3" || value === "react") return value;
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
- logWarn2(`Unknown components: ${invalid.join(", ")}`);
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
- logWarn2(`${sampleFile} already exists, skipping`);
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
- let template;
787
- if (templateArg && TEMPLATES.includes(templateArg)) {
788
- template = templateArg;
789
- } else {
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
- logWarn2(`${warnings.length} warning${warnings.length === 1 ? "" : "s"} found`);
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 is available`,
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.4",
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"