@agile-team/robot-cli 1.0.2 → 1.0.4

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 (4) hide show
  1. package/README.md +5 -5
  2. package/bin/index.js +431 -296
  3. package/lib/utils.js +240 -244
  4. package/package.json +1 -1
package/lib/utils.js CHANGED
@@ -1,58 +1,155 @@
1
- // lib/utils.js - 优化版本
1
+ // lib/utils.js - 增强版本
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
- import { execSync } from 'child_process';
5
4
  import chalk from 'chalk';
5
+ import { execSync } from 'child_process';
6
+ import fetch from 'node-fetch';
7
+
8
+ /**
9
+ * 检测当前使用的包管理器
10
+ */
11
+ export function detectPackageManager() {
12
+ try {
13
+ // 检查各种包管理器的可用性
14
+ const managers = [];
15
+
16
+ // 检查 bun
17
+ try {
18
+ execSync('bun --version', { stdio: 'ignore' });
19
+ managers.push('bun');
20
+ } catch {}
21
+
22
+ // 检查 pnpm
23
+ try {
24
+ execSync('pnpm --version', { stdio: 'ignore' });
25
+ managers.push('pnpm');
26
+ } catch {}
27
+
28
+ // 检查 yarn
29
+ try {
30
+ execSync('yarn --version', { stdio: 'ignore' });
31
+ managers.push('yarn');
32
+ } catch {}
33
+
34
+ // npm 通常总是可用的
35
+ try {
36
+ execSync('npm --version', { stdio: 'ignore' });
37
+ managers.push('npm');
38
+ } catch {}
39
+
40
+ return managers;
41
+ } catch (error) {
42
+ return ['npm']; // 默认返回 npm
43
+ }
44
+ }
45
+
46
+ /**
47
+ * 获取当前CLI的安装信息
48
+ */
49
+ export function getInstallationInfo() {
50
+ try {
51
+ const packagePath = process.env.npm_config_global
52
+ ? path.join(process.env.npm_config_global, 'node_modules', '@agile-team', 'robot-cli')
53
+ : null;
54
+
55
+ return {
56
+ binPath: process.argv[1],
57
+ packagePath,
58
+ nodeVersion: process.version,
59
+ platform: process.platform,
60
+ arch: process.arch
61
+ };
62
+ } catch (error) {
63
+ return {
64
+ binPath: process.argv[1],
65
+ packagePath: null,
66
+ nodeVersion: process.version,
67
+ platform: process.platform,
68
+ arch: process.arch
69
+ };
70
+ }
71
+ }
72
+
73
+ /**
74
+ * 诊断安装问题
75
+ */
76
+ export function diagnoseInstallation() {
77
+ const info = getInstallationInfo();
78
+ const managers = detectPackageManager();
79
+
80
+ console.log(chalk.blue('🔍 安装诊断信息:'));
81
+ console.log();
82
+ console.log(` 执行文件: ${chalk.cyan(info.binPath)}`);
83
+ console.log(` Node版本: ${chalk.cyan(info.nodeVersion)}`);
84
+ console.log(` 系统平台: ${chalk.cyan(info.platform)} (${info.arch})`);
85
+ console.log(` 可用包管理器: ${chalk.cyan(managers.join(', '))}`);
86
+
87
+ if (info.packagePath) {
88
+ console.log(` 包路径: ${chalk.cyan(info.packagePath)}`);
89
+ }
90
+
91
+ console.log();
92
+ console.log(chalk.blue('💡 推荐的安装方式:'));
93
+
94
+ if (managers.includes('bun')) {
95
+ console.log(chalk.green(' bun install -g @agile-team/robot-cli'));
96
+ }
97
+ if (managers.includes('pnpm')) {
98
+ console.log(chalk.green(' pnpm install -g @agile-team/robot-cli'));
99
+ }
100
+ if (managers.includes('yarn')) {
101
+ console.log(chalk.green(' yarn global add @agile-team/robot-cli'));
102
+ }
103
+ if (managers.includes('npm')) {
104
+ console.log(chalk.green(' npm install -g @agile-team/robot-cli'));
105
+ }
106
+
107
+ console.log();
108
+ console.log(chalk.yellow('⚠️ 建议只使用一种包管理器来避免冲突'));
109
+ }
6
110
 
7
111
  /**
8
112
  * 验证项目名称
9
113
  */
10
114
  export function validateProjectName(name) {
11
115
  const errors = [];
12
-
13
- // 检查是否为空
14
- if (!name || !name.trim()) {
116
+
117
+ if (!name || typeof name !== 'string') {
15
118
  errors.push('项目名称不能为空');
16
119
  return { valid: false, errors };
17
120
  }
18
-
121
+
19
122
  const trimmedName = name.trim();
20
-
21
- // 检查长度
22
- if (trimmedName.length < 1) {
123
+
124
+ if (trimmedName.length === 0) {
23
125
  errors.push('项目名称不能为空');
24
126
  }
25
-
127
+
26
128
  if (trimmedName.length > 214) {
27
- errors.push('项目名称过长 (最大214个字符)');
129
+ errors.push('项目名称不能超过214个字符');
28
130
  }
29
-
30
- // 检查字符规则
31
- if (!/^[a-z0-9\-_@.]+$/i.test(trimmedName)) {
32
- errors.push('项目名称只能包含字母、数字、连字符、下划线、@和点');
131
+
132
+ if (trimmedName.toLowerCase() !== trimmedName) {
133
+ errors.push('项目名称只能包含小写字母');
33
134
  }
34
-
35
- // 检查开头字符
135
+
36
136
  if (/^[._]/.test(trimmedName)) {
37
- errors.push('项目名称不能以点或下划线开头');
137
+ errors.push('项目名称不能以 "." 或 "_" 开头');
38
138
  }
39
-
40
- // 检查是否包含空格
41
- if (/\s/.test(trimmedName)) {
42
- errors.push('项目名称不能包含空格');
139
+
140
+ if (!/^[a-z0-9._-]+$/.test(trimmedName)) {
141
+ errors.push('项目名称只能包含字母、数字、点、下划线和短横线');
43
142
  }
44
-
45
- // 检查保留字
143
+
46
144
  const reservedNames = [
47
- 'node_modules', 'favicon.ico', 'con', 'prn', 'aux', 'nul',
48
- 'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9',
49
- 'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9'
145
+ 'node_modules', 'favicon.ico', '.git', '.env', 'package.json',
146
+ 'npm', 'yarn', 'pnpm', 'bun', 'robot'
50
147
  ];
51
-
52
- if (reservedNames.includes(trimmedName.toLowerCase())) {
53
- errors.push(`"${trimmedName}" 是保留字,不能作为项目名称`);
148
+
149
+ if (reservedNames.includes(trimmedName)) {
150
+ errors.push(`"${trimmedName}" 是保留名称,请使用其他名称`);
54
151
  }
55
-
152
+
56
153
  return {
57
154
  valid: errors.length === 0,
58
155
  errors
@@ -63,208 +160,79 @@ export function validateProjectName(name) {
63
160
  * 复制模板文件
64
161
  */
65
162
  export async function copyTemplate(sourcePath, targetPath) {
66
- try {
67
- // 确保目标目录存在
68
- await fs.ensureDir(targetPath);
69
-
70
- // 获取源目录中的所有文件和文件夹
71
- const items = await fs.readdir(sourcePath);
72
-
73
- for (const item of items) {
74
- const sourceItemPath = path.join(sourcePath, item);
75
- const targetItemPath = path.join(targetPath, item);
76
-
77
- // 跳过不需要的文件
78
- if (shouldSkipFile(item)) {
79
- continue;
80
- }
81
-
82
- const stats = await fs.stat(sourceItemPath);
83
-
84
- if (stats.isDirectory()) {
85
- // 递归复制目录
86
- await copyTemplate(sourceItemPath, targetItemPath);
87
- } else {
88
- // 复制文件
89
- await fs.copy(sourceItemPath, targetItemPath);
90
- }
91
- }
92
- } catch (error) {
93
- throw new Error(`复制模板文件失败: ${error.message}`);
163
+ if (!fs.existsSync(sourcePath)) {
164
+ throw new Error(`源路径不存在: ${sourcePath}`);
94
165
  }
95
- }
96
166
 
97
- /**
98
- * 判断是否应该跳过文件/目录
99
- */
100
- function shouldSkipFile(fileName) {
101
- const skipPatterns = [
102
- '.git',
103
- '.DS_Store',
104
- 'Thumbs.db',
105
- '.vscode',
106
- '.idea',
107
- 'node_modules',
108
- '.cache',
109
- 'dist',
110
- 'build',
111
- '*.log',
112
- '.env.local',
113
- '.env.*.local'
114
- ];
115
-
116
- return skipPatterns.some(pattern => {
117
- if (pattern.includes('*')) {
118
- // 简单的通配符匹配
119
- const regex = new RegExp(pattern.replace(/\*/g, '.*'));
120
- return regex.test(fileName);
167
+ await fs.ensureDir(targetPath);
168
+ await fs.copy(sourcePath, targetPath, {
169
+ filter: (src) => {
170
+ const basename = path.basename(src);
171
+ // 排除不需要的文件
172
+ return ![
173
+ '.git',
174
+ 'node_modules',
175
+ '.DS_Store',
176
+ 'Thumbs.db',
177
+ '.vscode',
178
+ '.idea'
179
+ ].includes(basename);
121
180
  }
122
- return fileName === pattern;
123
181
  });
124
182
  }
125
183
 
126
184
  /**
127
- * 安装项目依赖
185
+ * 安装依赖
128
186
  */
129
- export async function installDependencies(projectPath, spinner, packageManager) {
187
+ export async function installDependencies(projectPath, spinner, packageManager = 'npm') {
130
188
  const originalCwd = process.cwd();
131
-
189
+
132
190
  try {
133
191
  process.chdir(projectPath);
134
-
135
- // 如果没有指定包管理器,自动检测(优先bun和pnpm)
136
- const pm = packageManager || detectPackageManager(projectPath);
137
-
138
- if (spinner) {
139
- spinner.text = `使用 ${pm} 安装依赖...`;
140
- }
141
-
142
- // 安装依赖
143
- const installCommand = getInstallCommand(pm);
144
-
145
- execSync(installCommand, {
146
- stdio: 'pipe', // 不显示安装输出
147
- timeout: 300000 // 5分钟超时
148
- });
149
-
150
- } catch (error) {
151
- throw new Error(`安装依赖失败: ${error.message}`);
152
- } finally {
153
- process.chdir(originalCwd);
154
- }
155
- }
156
192
 
157
- /**
158
- * 检测包管理器 - 优先推荐bun和pnpm
159
- */
160
- function detectPackageManager(projectPath) {
161
- // 1. 检查项目中是否有锁文件
162
- if (fs.existsSync(path.join(projectPath, 'bun.lockb'))) {
163
- return 'bun';
164
- }
165
-
166
- if (fs.existsSync(path.join(projectPath, 'pnpm-lock.yaml'))) {
167
- return 'pnpm';
168
- }
169
-
170
- // 2. 检查全局是否安装了bun(最优先)
171
- try {
172
- execSync('bun --version', { stdio: 'ignore' });
173
- return 'bun';
174
- } catch {
175
- // bun 不可用
176
- }
177
-
178
- // 3. 检查全局是否安装了pnpm(次优先)
179
- try {
180
- execSync('pnpm --version', { stdio: 'ignore' });
181
- return 'pnpm';
182
- } catch {
183
- // pnpm 不可用
184
- }
185
-
186
- // 4. 检查yarn(兼容性)
187
- if (fs.existsSync(path.join(projectPath, 'yarn.lock'))) {
188
- return 'yarn';
189
- }
190
-
191
- try {
192
- execSync('yarn --version', { stdio: 'ignore' });
193
- return 'yarn';
194
- } catch {
195
- // yarn 不可用
196
- }
197
-
198
- // 5. 默认使用npm(最后选择)
199
- return 'npm';
200
- }
193
+ // 检查 package.json 是否存在
194
+ const packageJsonPath = path.join(projectPath, 'package.json');
195
+ if (!fs.existsSync(packageJsonPath)) {
196
+ if (spinner) {
197
+ spinner.text = '⚠️ 跳过依赖安装 (无 package.json)';
198
+ }
199
+ return;
200
+ }
201
201
 
202
- /**
203
- * 获取安装命令
204
- */
205
- function getInstallCommand(packageManager) {
206
- const commands = {
207
- bun: 'bun install',
208
- pnpm: 'pnpm install',
209
- yarn: 'yarn install',
210
- npm: 'npm install'
211
- };
212
-
213
- return commands[packageManager] || commands.npm;
214
- }
202
+ // 根据包管理器选择安装命令
203
+ const installCommands = {
204
+ bun: 'bun install',
205
+ pnpm: 'pnpm install',
206
+ yarn: 'yarn install',
207
+ npm: 'npm install'
208
+ };
215
209
 
216
- /**
217
- * 检查命令是否存在
218
- */
219
- export function commandExists(command) {
220
- try {
221
- execSync(`${command} --version`, { stdio: 'ignore' });
222
- return true;
223
- } catch {
224
- return false;
225
- }
226
- }
210
+ const command = installCommands[packageManager] || 'npm install';
227
211
 
228
- /**
229
- * 格式化文件大小
230
- */
231
- export function formatFileSize(bytes) {
232
- if (bytes === 0) return '0 B';
233
-
234
- const k = 1024;
235
- const sizes = ['B', 'KB', 'MB', 'GB'];
236
- const i = Math.floor(Math.log(bytes) / Math.log(k));
237
-
238
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
239
- }
212
+ if (spinner) {
213
+ spinner.text = `📦 使用 ${packageManager} 安装依赖...`;
214
+ }
240
215
 
241
- /**
242
- * 获取文件夹大小
243
- */
244
- export async function getFolderSize(folderPath) {
245
- let size = 0;
216
+ execSync(command, {
217
+ stdio: 'ignore',
218
+ timeout: 300000 // 5分钟超时
219
+ });
246
220
 
247
- async function calculateSize(currentPath) {
248
- try {
249
- const items = await fs.readdir(currentPath);
250
-
251
- for (const item of items) {
252
- const itemPath = path.join(currentPath, item);
253
- const stats = await fs.stat(itemPath);
254
-
255
- if (stats.isDirectory()) {
256
- await calculateSize(itemPath);
257
- } else {
258
- size += stats.size;
259
- }
260
- }
261
- } catch (error) {
262
- // 忽略权限错误等
221
+ } catch (error) {
222
+ if (spinner) {
223
+ spinner.text = `⚠️ 依赖安装失败,请手动安装`;
263
224
  }
225
+ console.log();
226
+ console.log(chalk.yellow('⚠️ 自动安装依赖失败'));
227
+ console.log(chalk.dim(` 错误: ${error.message}`));
228
+ console.log();
229
+ console.log(chalk.blue('💡 请手动安装:'));
230
+ console.log(chalk.cyan(` cd ${path.basename(projectPath)}`));
231
+ console.log(chalk.cyan(` ${packageManager} install`));
232
+ console.log();
233
+ } finally {
234
+ process.chdir(originalCwd);
264
235
  }
265
-
266
- await calculateSize(folderPath);
267
- return size;
268
236
  }
269
237
 
270
238
  /**
@@ -272,19 +240,27 @@ export async function getFolderSize(folderPath) {
272
240
  */
273
241
  export async function checkNetworkConnection() {
274
242
  try {
275
- const { default: fetch } = await import('node-fetch');
276
- const response = await fetch('https://github.com', {
243
+ const response = await fetch('https://api.github.com', {
277
244
  method: 'HEAD',
278
245
  timeout: 5000
279
246
  });
280
247
  return response.ok;
281
- } catch {
282
- return false;
248
+ } catch (error) {
249
+ // 尝试备用检查
250
+ try {
251
+ const response = await fetch('https://www.npmjs.com', {
252
+ method: 'HEAD',
253
+ timeout: 5000
254
+ });
255
+ return response.ok;
256
+ } catch (backupError) {
257
+ return false;
258
+ }
283
259
  }
284
260
  }
285
261
 
286
262
  /**
287
- * 生成项目统计信息
263
+ * 生成项目统计
288
264
  */
289
265
  export async function generateProjectStats(projectPath) {
290
266
  try {
@@ -292,35 +268,35 @@ export async function generateProjectStats(projectPath) {
292
268
  files: 0,
293
269
  directories: 0,
294
270
  size: 0,
295
- types: {}
271
+ fileTypes: {}
296
272
  };
297
273
 
298
- async function analyze(currentPath) {
299
- const items = await fs.readdir(currentPath);
300
-
274
+ async function walkDir(dirPath) {
275
+ const items = await fs.readdir(dirPath);
276
+
301
277
  for (const item of items) {
302
- if (shouldSkipFile(item)) continue;
303
-
304
- const itemPath = path.join(currentPath, item);
305
- const itemStats = await fs.stat(itemPath);
306
-
307
- if (itemStats.isDirectory()) {
308
- stats.directories++;
309
- await analyze(itemPath);
278
+ const itemPath = path.join(dirPath, item);
279
+ const stat = await fs.stat(itemPath);
280
+
281
+ if (stat.isDirectory()) {
282
+ // 跳过 node_modules 等目录
283
+ if (!['node_modules', '.git', '.DS_Store'].includes(item)) {
284
+ stats.directories++;
285
+ await walkDir(itemPath);
286
+ }
310
287
  } else {
311
288
  stats.files++;
312
- stats.size += itemStats.size;
313
-
314
- // 统计文件类型
289
+ stats.size += stat.size;
290
+
315
291
  const ext = path.extname(item).toLowerCase();
316
292
  if (ext) {
317
- stats.types[ext] = (stats.types[ext] || 0) + 1;
293
+ stats.fileTypes[ext] = (stats.fileTypes[ext] || 0) + 1;
318
294
  }
319
295
  }
320
296
  }
321
297
  }
322
298
 
323
- await analyze(projectPath);
299
+ await walkDir(projectPath);
324
300
  return stats;
325
301
  } catch (error) {
326
302
  return null;
@@ -328,17 +304,37 @@ export async function generateProjectStats(projectPath) {
328
304
  }
329
305
 
330
306
  /**
331
- * 美化打印项目统计
307
+ * 打印项目统计
332
308
  */
333
309
  export function printProjectStats(stats) {
334
310
  if (!stats) return;
335
-
311
+
336
312
  console.log(chalk.blue('📊 项目统计:'));
337
- console.log(` 文件数量: ${chalk.cyan(stats.files)}`);
338
- console.log(` 目录数量: ${chalk.cyan(stats.directories)}`);
339
- console.log(` 总大小: ${chalk.cyan(formatFileSize(stats.size))}`);
340
-
341
- if (Object.keys(stats.types).length > 0) {
342
- console.log(` 文件类型: ${chalk.cyan(Object.keys(stats.types).join(', '))}`);
313
+ console.log(` 文件数量: ${chalk.cyan(stats.files)} 个`);
314
+ console.log(` 目录数量: ${chalk.cyan(stats.directories)} 个`);
315
+ console.log(` 项目大小: ${chalk.cyan(formatBytes(stats.size))}`);
316
+
317
+ const topTypes = Object.entries(stats.fileTypes)
318
+ .sort(([,a], [,b]) => b - a)
319
+ .slice(0, 5);
320
+
321
+ if (topTypes.length > 0) {
322
+ console.log(' 主要文件类型:');
323
+ topTypes.forEach(([ext, count]) => {
324
+ console.log(` ${ext}: ${chalk.cyan(count)} 个`);
325
+ });
343
326
  }
327
+ }
328
+
329
+ /**
330
+ * 格式化字节大小
331
+ */
332
+ function formatBytes(bytes) {
333
+ if (bytes === 0) return '0 B';
334
+
335
+ const k = 1024;
336
+ const sizes = ['B', 'KB', 'MB', 'GB'];
337
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
338
+
339
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
344
340
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agile-team/robot-cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "🤖 现代化项目脚手架工具,支持多技术栈快速创建项目",
5
5
  "type": "module",
6
6
  "bin": {