@agile-team/robot-cli 1.0.2
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 +443 -0
- package/bin/index.js +341 -0
- package/lib/cache.js +120 -0
- package/lib/create.js +946 -0
- package/lib/download.js +191 -0
- package/lib/templates.js +355 -0
- package/lib/utils.js +344 -0
- package/package.json +82 -0
package/lib/utils.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// lib/utils.js - 优化版本
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 验证项目名称
|
|
9
|
+
*/
|
|
10
|
+
export function validateProjectName(name) {
|
|
11
|
+
const errors = [];
|
|
12
|
+
|
|
13
|
+
// 检查是否为空
|
|
14
|
+
if (!name || !name.trim()) {
|
|
15
|
+
errors.push('项目名称不能为空');
|
|
16
|
+
return { valid: false, errors };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const trimmedName = name.trim();
|
|
20
|
+
|
|
21
|
+
// 检查长度
|
|
22
|
+
if (trimmedName.length < 1) {
|
|
23
|
+
errors.push('项目名称不能为空');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (trimmedName.length > 214) {
|
|
27
|
+
errors.push('项目名称过长 (最大214个字符)');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 检查字符规则
|
|
31
|
+
if (!/^[a-z0-9\-_@.]+$/i.test(trimmedName)) {
|
|
32
|
+
errors.push('项目名称只能包含字母、数字、连字符、下划线、@和点');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 检查开头字符
|
|
36
|
+
if (/^[._]/.test(trimmedName)) {
|
|
37
|
+
errors.push('项目名称不能以点或下划线开头');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 检查是否包含空格
|
|
41
|
+
if (/\s/.test(trimmedName)) {
|
|
42
|
+
errors.push('项目名称不能包含空格');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 检查保留字
|
|
46
|
+
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'
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
if (reservedNames.includes(trimmedName.toLowerCase())) {
|
|
53
|
+
errors.push(`"${trimmedName}" 是保留字,不能作为项目名称`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
valid: errors.length === 0,
|
|
58
|
+
errors
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 复制模板文件
|
|
64
|
+
*/
|
|
65
|
+
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}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
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);
|
|
121
|
+
}
|
|
122
|
+
return fileName === pattern;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 安装项目依赖
|
|
128
|
+
*/
|
|
129
|
+
export async function installDependencies(projectPath, spinner, packageManager) {
|
|
130
|
+
const originalCwd = process.cwd();
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
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
|
+
|
|
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
|
+
}
|
|
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
|
+
}
|
|
215
|
+
|
|
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
|
+
}
|
|
227
|
+
|
|
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
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 获取文件夹大小
|
|
243
|
+
*/
|
|
244
|
+
export async function getFolderSize(folderPath) {
|
|
245
|
+
let size = 0;
|
|
246
|
+
|
|
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
|
+
// 忽略权限错误等
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await calculateSize(folderPath);
|
|
267
|
+
return size;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 检查网络连接
|
|
272
|
+
*/
|
|
273
|
+
export async function checkNetworkConnection() {
|
|
274
|
+
try {
|
|
275
|
+
const { default: fetch } = await import('node-fetch');
|
|
276
|
+
const response = await fetch('https://github.com', {
|
|
277
|
+
method: 'HEAD',
|
|
278
|
+
timeout: 5000
|
|
279
|
+
});
|
|
280
|
+
return response.ok;
|
|
281
|
+
} catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 生成项目统计信息
|
|
288
|
+
*/
|
|
289
|
+
export async function generateProjectStats(projectPath) {
|
|
290
|
+
try {
|
|
291
|
+
const stats = {
|
|
292
|
+
files: 0,
|
|
293
|
+
directories: 0,
|
|
294
|
+
size: 0,
|
|
295
|
+
types: {}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
async function analyze(currentPath) {
|
|
299
|
+
const items = await fs.readdir(currentPath);
|
|
300
|
+
|
|
301
|
+
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);
|
|
310
|
+
} else {
|
|
311
|
+
stats.files++;
|
|
312
|
+
stats.size += itemStats.size;
|
|
313
|
+
|
|
314
|
+
// 统计文件类型
|
|
315
|
+
const ext = path.extname(item).toLowerCase();
|
|
316
|
+
if (ext) {
|
|
317
|
+
stats.types[ext] = (stats.types[ext] || 0) + 1;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
await analyze(projectPath);
|
|
324
|
+
return stats;
|
|
325
|
+
} catch (error) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 美化打印项目统计
|
|
332
|
+
*/
|
|
333
|
+
export function printProjectStats(stats) {
|
|
334
|
+
if (!stats) return;
|
|
335
|
+
|
|
336
|
+
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(', '))}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agile-team/robot-cli",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "🤖 现代化项目脚手架工具,支持多技术栈快速创建项目",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"robot": "bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"robot": "node bin/index.js",
|
|
11
|
+
"robot:create": "node bin/index.js create",
|
|
12
|
+
"robot:list": "node bin/index.js list",
|
|
13
|
+
"robot:search": "node bin/index.js search",
|
|
14
|
+
"robot:cache": "node bin/index.js cache --info",
|
|
15
|
+
"robot:cache:clear": "node bin/index.js cache --clear",
|
|
16
|
+
"test": "node test/local-test.js",
|
|
17
|
+
"test:setup": "node test/local-test.js --setup",
|
|
18
|
+
"test:clean": "node test/local-test.js --clean",
|
|
19
|
+
"dev": "node bin/index.js",
|
|
20
|
+
"dev:create": "node bin/index.js create",
|
|
21
|
+
"dev:list": "node bin/index.js list",
|
|
22
|
+
"dev:search": "node bin/index.js search",
|
|
23
|
+
"dev:cache": "node bin/index.js cache --info",
|
|
24
|
+
"dev:cache:clear": "node bin/index.js cache --clear",
|
|
25
|
+
"install:deps": "bun install || npm install",
|
|
26
|
+
"clean:modules": "rm -rf node_modules",
|
|
27
|
+
"reinstall": "npm run clean:modules && npm run install:deps",
|
|
28
|
+
"prepublishOnly": "npm run test",
|
|
29
|
+
"version:patch": "npm version patch",
|
|
30
|
+
"version:minor": "npm version minor",
|
|
31
|
+
"version:major": "npm version major",
|
|
32
|
+
"publish:npm": "npm publish --access public",
|
|
33
|
+
"release:patch": "npm run version:patch && npm run publish:npm",
|
|
34
|
+
"release:minor": "npm run version:minor && npm run publish:npm",
|
|
35
|
+
"release:major": "npm run version:major && npm run publish:npm",
|
|
36
|
+
"clean:all": "npm run test:clean && npm run reinstall"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"boxen": "^7.1.0",
|
|
40
|
+
"chalk": "^5.3.0",
|
|
41
|
+
"commander": "^11.0.0",
|
|
42
|
+
"extract-zip": "^2.0.1",
|
|
43
|
+
"fs-extra": "^11.1.0",
|
|
44
|
+
"inquirer": "^9.2.0",
|
|
45
|
+
"node-fetch": "^3.3.0",
|
|
46
|
+
"ora": "^7.0.0"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"bin",
|
|
50
|
+
"lib",
|
|
51
|
+
"README.md"
|
|
52
|
+
],
|
|
53
|
+
"keywords": [
|
|
54
|
+
"cli",
|
|
55
|
+
"scaffold",
|
|
56
|
+
"template",
|
|
57
|
+
"vue",
|
|
58
|
+
"react",
|
|
59
|
+
"node",
|
|
60
|
+
"uni-app",
|
|
61
|
+
"electron",
|
|
62
|
+
"project-generator",
|
|
63
|
+
"boilerplate",
|
|
64
|
+
"cheny"
|
|
65
|
+
],
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=20.0.0"
|
|
68
|
+
},
|
|
69
|
+
"homepage": "https://github.com/ChenyCHENYU/robot-cli#readme",
|
|
70
|
+
"repository": {
|
|
71
|
+
"type": "git",
|
|
72
|
+
"url": "git+https://github.com/ChenyCHENYU/robot-cli.git"
|
|
73
|
+
},
|
|
74
|
+
"bugs": {
|
|
75
|
+
"url": "https://github.com/ChenyCHENYU/robot-cli/issues"
|
|
76
|
+
},
|
|
77
|
+
"author": "CHENY <ycyplus@gmail.com>",
|
|
78
|
+
"license": "MIT",
|
|
79
|
+
"publishConfig": {
|
|
80
|
+
"access": "public"
|
|
81
|
+
}
|
|
82
|
+
}
|