@agile-team/robot-cli 1.1.9 → 1.1.12
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 +438 -438
- package/bin/index.js +404 -396
- package/lib/create.js +1200 -1145
- package/lib/download.js +204 -204
- package/lib/templates.js +352 -352
- package/lib/utils.js +333 -333
- package/package.json +6 -5
package/lib/utils.js
CHANGED
|
@@ -1,334 +1,334 @@
|
|
|
1
|
-
// lib/utils.js - 增强版本,添加详细进度展示
|
|
2
|
-
import fs from 'fs-extra';
|
|
3
|
-
import path from 'path';
|
|
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
|
-
const managers = [];
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
execSync('bun --version', { stdio: 'ignore' });
|
|
17
|
-
managers.push('bun');
|
|
18
|
-
} catch {}
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
execSync('pnpm --version', { stdio: 'ignore' });
|
|
22
|
-
managers.push('pnpm');
|
|
23
|
-
} catch {}
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
execSync('yarn --version', { stdio: 'ignore' });
|
|
27
|
-
managers.push('yarn');
|
|
28
|
-
} catch {}
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
execSync('npm --version', { stdio: 'ignore' });
|
|
32
|
-
managers.push('npm');
|
|
33
|
-
} catch {}
|
|
34
|
-
|
|
35
|
-
return managers;
|
|
36
|
-
} catch (error) {
|
|
37
|
-
return ['npm'];
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 验证项目名称
|
|
43
|
-
*/
|
|
44
|
-
export function validateProjectName(name) {
|
|
45
|
-
const errors = [];
|
|
46
|
-
|
|
47
|
-
if (!name || typeof name !== 'string') {
|
|
48
|
-
errors.push('项目名称不能为空');
|
|
49
|
-
return { valid: false, errors };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const trimmedName = name.trim();
|
|
53
|
-
|
|
54
|
-
if (trimmedName.length === 0) {
|
|
55
|
-
errors.push('项目名称不能为空');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (trimmedName.length > 214) {
|
|
59
|
-
errors.push('项目名称不能超过214个字符');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (trimmedName.toLowerCase() !== trimmedName) {
|
|
63
|
-
errors.push('项目名称只能包含小写字母');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (/^[._]/.test(trimmedName)) {
|
|
67
|
-
errors.push('项目名称不能以 "." 或 "_" 开头');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (!/^[a-z0-9._-]+$/.test(trimmedName)) {
|
|
71
|
-
errors.push('项目名称只能包含字母、数字、点、下划线和短横线');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const reservedNames = [
|
|
75
|
-
'node_modules', 'favicon.ico', '.git', '.env', 'package.json',
|
|
76
|
-
'npm', 'yarn', 'pnpm', 'bun', 'robot'
|
|
77
|
-
];
|
|
78
|
-
|
|
79
|
-
if (reservedNames.includes(trimmedName)) {
|
|
80
|
-
errors.push(`"${trimmedName}" 是保留名称,请使用其他名称`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
valid: errors.length === 0,
|
|
85
|
-
errors
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* 统计目录中的文件数量
|
|
91
|
-
*/
|
|
92
|
-
async function countFiles(dirPath) {
|
|
93
|
-
let count = 0;
|
|
94
|
-
|
|
95
|
-
async function walkDir(currentPath) {
|
|
96
|
-
const items = await fs.readdir(currentPath);
|
|
97
|
-
|
|
98
|
-
for (const item of items) {
|
|
99
|
-
const itemPath = path.join(currentPath, item);
|
|
100
|
-
const stat = await fs.stat(itemPath);
|
|
101
|
-
|
|
102
|
-
if (stat.isDirectory()) {
|
|
103
|
-
// 跳过不需要的目录
|
|
104
|
-
if (!['node_modules', '.git', '.DS_Store'].includes(item)) {
|
|
105
|
-
await walkDir(itemPath);
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
count++;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
await walkDir(dirPath);
|
|
114
|
-
return count;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* 复制模板文件 - 带详细进度展示
|
|
119
|
-
*/
|
|
120
|
-
export async function copyTemplate(sourcePath, targetPath, spinner) {
|
|
121
|
-
if (!fs.existsSync(sourcePath)) {
|
|
122
|
-
throw new Error(`源路径不存在: ${sourcePath}`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
await fs.ensureDir(targetPath);
|
|
126
|
-
|
|
127
|
-
// 1. 统计文件数量
|
|
128
|
-
if (spinner) {
|
|
129
|
-
spinner.text = '📊 统计文件数量...';
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const totalFiles = await countFiles(sourcePath);
|
|
133
|
-
|
|
134
|
-
if (spinner) {
|
|
135
|
-
spinner.text = `📋 开始复制 ${totalFiles} 个文件...`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
let copiedFiles = 0;
|
|
139
|
-
|
|
140
|
-
// 2. 递归复制文件,带进度更新
|
|
141
|
-
async function copyWithProgress(srcDir, destDir) {
|
|
142
|
-
const items = await fs.readdir(srcDir);
|
|
143
|
-
|
|
144
|
-
for (const item of items) {
|
|
145
|
-
const srcPath = path.join(srcDir, item);
|
|
146
|
-
const destPath = path.join(destDir, item);
|
|
147
|
-
const stat = await fs.stat(srcPath);
|
|
148
|
-
|
|
149
|
-
if (stat.isDirectory()) {
|
|
150
|
-
// 排除不需要的文件夹
|
|
151
|
-
if (['node_modules', '.git', '.DS_Store', '.vscode', '.idea'].includes(item)) {
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
await fs.ensureDir(destPath);
|
|
156
|
-
await copyWithProgress(srcPath, destPath);
|
|
157
|
-
} else {
|
|
158
|
-
// 复制文件
|
|
159
|
-
await fs.copy(srcPath, destPath);
|
|
160
|
-
copiedFiles++;
|
|
161
|
-
|
|
162
|
-
// 更新进度 (每10个文件或重要节点更新一次)
|
|
163
|
-
if (spinner && (copiedFiles % 10 === 0 || copiedFiles === totalFiles)) {
|
|
164
|
-
const percentage = Math.round((copiedFiles / totalFiles) * 100);
|
|
165
|
-
spinner.text = `📋 复制中... ${copiedFiles}/${totalFiles} (${percentage}%)`;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
await copyWithProgress(sourcePath, targetPath);
|
|
172
|
-
|
|
173
|
-
if (spinner) {
|
|
174
|
-
spinner.text = `✅ 文件复制完成 (${copiedFiles} 个文件)`;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* 安装依赖
|
|
180
|
-
*/
|
|
181
|
-
export async function installDependencies(projectPath, spinner, packageManager = 'npm') {
|
|
182
|
-
const originalCwd = process.cwd();
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
process.chdir(projectPath);
|
|
186
|
-
|
|
187
|
-
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
188
|
-
if (!fs.existsSync(packageJsonPath)) {
|
|
189
|
-
if (spinner) {
|
|
190
|
-
spinner.text = '⚠️ 跳过依赖安装 (无 package.json)';
|
|
191
|
-
}
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const installCommands = {
|
|
196
|
-
bun: 'bun install',
|
|
197
|
-
pnpm: 'pnpm install',
|
|
198
|
-
yarn: 'yarn install',
|
|
199
|
-
npm: 'npm install'
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const command = installCommands[packageManager] || 'npm install';
|
|
203
|
-
|
|
204
|
-
if (spinner) {
|
|
205
|
-
spinner.text = `📦 使用 ${packageManager} 安装依赖...`;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
execSync(command, {
|
|
209
|
-
stdio: 'ignore',
|
|
210
|
-
timeout: 300000
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
if (spinner) {
|
|
214
|
-
spinner.text = `✅ 依赖安装完成 (${packageManager})`;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
} catch (error) {
|
|
218
|
-
if (spinner) {
|
|
219
|
-
spinner.text = `⚠️ 依赖安装失败,请手动安装`;
|
|
220
|
-
}
|
|
221
|
-
console.log();
|
|
222
|
-
console.log(chalk.yellow('⚠️ 自动安装依赖失败'));
|
|
223
|
-
console.log(chalk.dim(` 错误: ${error.message}`));
|
|
224
|
-
console.log();
|
|
225
|
-
console.log(chalk.blue('💡 请手动安装:'));
|
|
226
|
-
console.log(chalk.cyan(` cd ${path.basename(projectPath)}`));
|
|
227
|
-
console.log(chalk.cyan(` ${packageManager} install`));
|
|
228
|
-
console.log();
|
|
229
|
-
} finally {
|
|
230
|
-
process.chdir(originalCwd);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* 检查网络连接
|
|
236
|
-
*/
|
|
237
|
-
export async function checkNetworkConnection() {
|
|
238
|
-
try {
|
|
239
|
-
const response = await fetch('https://api.github.com', {
|
|
240
|
-
method: 'HEAD',
|
|
241
|
-
timeout: 5000
|
|
242
|
-
});
|
|
243
|
-
return response.ok;
|
|
244
|
-
} catch (error) {
|
|
245
|
-
try {
|
|
246
|
-
const response = await fetch('https://www.npmjs.com', {
|
|
247
|
-
method: 'HEAD',
|
|
248
|
-
timeout: 5000
|
|
249
|
-
});
|
|
250
|
-
return response.ok;
|
|
251
|
-
} catch (backupError) {
|
|
252
|
-
return false;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* 生成项目统计
|
|
259
|
-
*/
|
|
260
|
-
export async function generateProjectStats(projectPath) {
|
|
261
|
-
try {
|
|
262
|
-
const stats = {
|
|
263
|
-
files: 0,
|
|
264
|
-
directories: 0,
|
|
265
|
-
size: 0,
|
|
266
|
-
fileTypes: {}
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
async function walkDir(dirPath) {
|
|
270
|
-
const items = await fs.readdir(dirPath);
|
|
271
|
-
|
|
272
|
-
for (const item of items) {
|
|
273
|
-
const itemPath = path.join(dirPath, item);
|
|
274
|
-
const stat = await fs.stat(itemPath);
|
|
275
|
-
|
|
276
|
-
if (stat.isDirectory()) {
|
|
277
|
-
if (!['node_modules', '.git', '.DS_Store'].includes(item)) {
|
|
278
|
-
stats.directories++;
|
|
279
|
-
await walkDir(itemPath);
|
|
280
|
-
}
|
|
281
|
-
} else {
|
|
282
|
-
stats.files++;
|
|
283
|
-
stats.size += stat.size;
|
|
284
|
-
|
|
285
|
-
const ext = path.extname(item).toLowerCase();
|
|
286
|
-
if (ext) {
|
|
287
|
-
stats.fileTypes[ext] = (stats.fileTypes[ext] || 0) + 1;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
await walkDir(projectPath);
|
|
294
|
-
return stats;
|
|
295
|
-
} catch (error) {
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* 打印项目统计
|
|
302
|
-
*/
|
|
303
|
-
export function printProjectStats(stats) {
|
|
304
|
-
if (!stats) return;
|
|
305
|
-
|
|
306
|
-
console.log(chalk.blue('📊 项目统计:'));
|
|
307
|
-
console.log(` 文件数量: ${chalk.cyan(stats.files)} 个`);
|
|
308
|
-
console.log(` 目录数量: ${chalk.cyan(stats.directories)} 个`);
|
|
309
|
-
console.log(` 项目大小: ${chalk.cyan(formatBytes(stats.size))}`);
|
|
310
|
-
|
|
311
|
-
const topTypes = Object.entries(stats.fileTypes)
|
|
312
|
-
.sort(([,a], [,b]) => b - a)
|
|
313
|
-
.slice(0, 5);
|
|
314
|
-
|
|
315
|
-
if (topTypes.length > 0) {
|
|
316
|
-
console.log(' 主要文件类型:');
|
|
317
|
-
topTypes.forEach(([ext, count]) => {
|
|
318
|
-
console.log(` ${ext}: ${chalk.cyan(count)} 个`);
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* 格式化字节大小
|
|
325
|
-
*/
|
|
326
|
-
function formatBytes(bytes) {
|
|
327
|
-
if (bytes === 0) return '0 B';
|
|
328
|
-
|
|
329
|
-
const k = 1024;
|
|
330
|
-
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
331
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
332
|
-
|
|
333
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
1
|
+
// lib/utils.js - 增强版本,添加详细进度展示
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
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
|
+
const managers = [];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
execSync('bun --version', { stdio: 'ignore' });
|
|
17
|
+
managers.push('bun');
|
|
18
|
+
} catch {}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
execSync('pnpm --version', { stdio: 'ignore' });
|
|
22
|
+
managers.push('pnpm');
|
|
23
|
+
} catch {}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
execSync('yarn --version', { stdio: 'ignore' });
|
|
27
|
+
managers.push('yarn');
|
|
28
|
+
} catch {}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
execSync('npm --version', { stdio: 'ignore' });
|
|
32
|
+
managers.push('npm');
|
|
33
|
+
} catch {}
|
|
34
|
+
|
|
35
|
+
return managers;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return ['npm'];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 验证项目名称
|
|
43
|
+
*/
|
|
44
|
+
export function validateProjectName(name) {
|
|
45
|
+
const errors = [];
|
|
46
|
+
|
|
47
|
+
if (!name || typeof name !== 'string') {
|
|
48
|
+
errors.push('项目名称不能为空');
|
|
49
|
+
return { valid: false, errors };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const trimmedName = name.trim();
|
|
53
|
+
|
|
54
|
+
if (trimmedName.length === 0) {
|
|
55
|
+
errors.push('项目名称不能为空');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (trimmedName.length > 214) {
|
|
59
|
+
errors.push('项目名称不能超过214个字符');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (trimmedName.toLowerCase() !== trimmedName) {
|
|
63
|
+
errors.push('项目名称只能包含小写字母');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (/^[._]/.test(trimmedName)) {
|
|
67
|
+
errors.push('项目名称不能以 "." 或 "_" 开头');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!/^[a-z0-9._-]+$/.test(trimmedName)) {
|
|
71
|
+
errors.push('项目名称只能包含字母、数字、点、下划线和短横线');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const reservedNames = [
|
|
75
|
+
'node_modules', 'favicon.ico', '.git', '.env', 'package.json',
|
|
76
|
+
'npm', 'yarn', 'pnpm', 'bun', 'robot'
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
if (reservedNames.includes(trimmedName)) {
|
|
80
|
+
errors.push(`"${trimmedName}" 是保留名称,请使用其他名称`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
valid: errors.length === 0,
|
|
85
|
+
errors
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 统计目录中的文件数量
|
|
91
|
+
*/
|
|
92
|
+
async function countFiles(dirPath) {
|
|
93
|
+
let count = 0;
|
|
94
|
+
|
|
95
|
+
async function walkDir(currentPath) {
|
|
96
|
+
const items = await fs.readdir(currentPath);
|
|
97
|
+
|
|
98
|
+
for (const item of items) {
|
|
99
|
+
const itemPath = path.join(currentPath, item);
|
|
100
|
+
const stat = await fs.stat(itemPath);
|
|
101
|
+
|
|
102
|
+
if (stat.isDirectory()) {
|
|
103
|
+
// 跳过不需要的目录
|
|
104
|
+
if (!['node_modules', '.git', '.DS_Store'].includes(item)) {
|
|
105
|
+
await walkDir(itemPath);
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
count++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await walkDir(dirPath);
|
|
114
|
+
return count;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 复制模板文件 - 带详细进度展示
|
|
119
|
+
*/
|
|
120
|
+
export async function copyTemplate(sourcePath, targetPath, spinner) {
|
|
121
|
+
if (!fs.existsSync(sourcePath)) {
|
|
122
|
+
throw new Error(`源路径不存在: ${sourcePath}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await fs.ensureDir(targetPath);
|
|
126
|
+
|
|
127
|
+
// 1. 统计文件数量
|
|
128
|
+
if (spinner) {
|
|
129
|
+
spinner.text = '📊 统计文件数量...';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const totalFiles = await countFiles(sourcePath);
|
|
133
|
+
|
|
134
|
+
if (spinner) {
|
|
135
|
+
spinner.text = `📋 开始复制 ${totalFiles} 个文件...`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let copiedFiles = 0;
|
|
139
|
+
|
|
140
|
+
// 2. 递归复制文件,带进度更新
|
|
141
|
+
async function copyWithProgress(srcDir, destDir) {
|
|
142
|
+
const items = await fs.readdir(srcDir);
|
|
143
|
+
|
|
144
|
+
for (const item of items) {
|
|
145
|
+
const srcPath = path.join(srcDir, item);
|
|
146
|
+
const destPath = path.join(destDir, item);
|
|
147
|
+
const stat = await fs.stat(srcPath);
|
|
148
|
+
|
|
149
|
+
if (stat.isDirectory()) {
|
|
150
|
+
// 排除不需要的文件夹
|
|
151
|
+
if (['node_modules', '.git', '.DS_Store', '.vscode', '.idea'].includes(item)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await fs.ensureDir(destPath);
|
|
156
|
+
await copyWithProgress(srcPath, destPath);
|
|
157
|
+
} else {
|
|
158
|
+
// 复制文件
|
|
159
|
+
await fs.copy(srcPath, destPath);
|
|
160
|
+
copiedFiles++;
|
|
161
|
+
|
|
162
|
+
// 更新进度 (每10个文件或重要节点更新一次)
|
|
163
|
+
if (spinner && (copiedFiles % 10 === 0 || copiedFiles === totalFiles)) {
|
|
164
|
+
const percentage = Math.round((copiedFiles / totalFiles) * 100);
|
|
165
|
+
spinner.text = `📋 复制中... ${copiedFiles}/${totalFiles} (${percentage}%)`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await copyWithProgress(sourcePath, targetPath);
|
|
172
|
+
|
|
173
|
+
if (spinner) {
|
|
174
|
+
spinner.text = `✅ 文件复制完成 (${copiedFiles} 个文件)`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 安装依赖
|
|
180
|
+
*/
|
|
181
|
+
export async function installDependencies(projectPath, spinner, packageManager = 'npm') {
|
|
182
|
+
const originalCwd = process.cwd();
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
process.chdir(projectPath);
|
|
186
|
+
|
|
187
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
188
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
189
|
+
if (spinner) {
|
|
190
|
+
spinner.text = '⚠️ 跳过依赖安装 (无 package.json)';
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const installCommands = {
|
|
196
|
+
bun: 'bun install',
|
|
197
|
+
pnpm: 'pnpm install',
|
|
198
|
+
yarn: 'yarn install',
|
|
199
|
+
npm: 'npm install'
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const command = installCommands[packageManager] || 'npm install';
|
|
203
|
+
|
|
204
|
+
if (spinner) {
|
|
205
|
+
spinner.text = `📦 使用 ${packageManager} 安装依赖...`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
execSync(command, {
|
|
209
|
+
stdio: 'ignore',
|
|
210
|
+
timeout: 300000
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (spinner) {
|
|
214
|
+
spinner.text = `✅ 依赖安装完成 (${packageManager})`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (spinner) {
|
|
219
|
+
spinner.text = `⚠️ 依赖安装失败,请手动安装`;
|
|
220
|
+
}
|
|
221
|
+
console.log();
|
|
222
|
+
console.log(chalk.yellow('⚠️ 自动安装依赖失败'));
|
|
223
|
+
console.log(chalk.dim(` 错误: ${error.message}`));
|
|
224
|
+
console.log();
|
|
225
|
+
console.log(chalk.blue('💡 请手动安装:'));
|
|
226
|
+
console.log(chalk.cyan(` cd ${path.basename(projectPath)}`));
|
|
227
|
+
console.log(chalk.cyan(` ${packageManager} install`));
|
|
228
|
+
console.log();
|
|
229
|
+
} finally {
|
|
230
|
+
process.chdir(originalCwd);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 检查网络连接
|
|
236
|
+
*/
|
|
237
|
+
export async function checkNetworkConnection() {
|
|
238
|
+
try {
|
|
239
|
+
const response = await fetch('https://api.github.com', {
|
|
240
|
+
method: 'HEAD',
|
|
241
|
+
timeout: 5000
|
|
242
|
+
});
|
|
243
|
+
return response.ok;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
try {
|
|
246
|
+
const response = await fetch('https://www.npmjs.com', {
|
|
247
|
+
method: 'HEAD',
|
|
248
|
+
timeout: 5000
|
|
249
|
+
});
|
|
250
|
+
return response.ok;
|
|
251
|
+
} catch (backupError) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 生成项目统计
|
|
259
|
+
*/
|
|
260
|
+
export async function generateProjectStats(projectPath) {
|
|
261
|
+
try {
|
|
262
|
+
const stats = {
|
|
263
|
+
files: 0,
|
|
264
|
+
directories: 0,
|
|
265
|
+
size: 0,
|
|
266
|
+
fileTypes: {}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
async function walkDir(dirPath) {
|
|
270
|
+
const items = await fs.readdir(dirPath);
|
|
271
|
+
|
|
272
|
+
for (const item of items) {
|
|
273
|
+
const itemPath = path.join(dirPath, item);
|
|
274
|
+
const stat = await fs.stat(itemPath);
|
|
275
|
+
|
|
276
|
+
if (stat.isDirectory()) {
|
|
277
|
+
if (!['node_modules', '.git', '.DS_Store'].includes(item)) {
|
|
278
|
+
stats.directories++;
|
|
279
|
+
await walkDir(itemPath);
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
stats.files++;
|
|
283
|
+
stats.size += stat.size;
|
|
284
|
+
|
|
285
|
+
const ext = path.extname(item).toLowerCase();
|
|
286
|
+
if (ext) {
|
|
287
|
+
stats.fileTypes[ext] = (stats.fileTypes[ext] || 0) + 1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
await walkDir(projectPath);
|
|
294
|
+
return stats;
|
|
295
|
+
} catch (error) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 打印项目统计
|
|
302
|
+
*/
|
|
303
|
+
export function printProjectStats(stats) {
|
|
304
|
+
if (!stats) return;
|
|
305
|
+
|
|
306
|
+
console.log(chalk.blue('📊 项目统计:'));
|
|
307
|
+
console.log(` 文件数量: ${chalk.cyan(stats.files)} 个`);
|
|
308
|
+
console.log(` 目录数量: ${chalk.cyan(stats.directories)} 个`);
|
|
309
|
+
console.log(` 项目大小: ${chalk.cyan(formatBytes(stats.size))}`);
|
|
310
|
+
|
|
311
|
+
const topTypes = Object.entries(stats.fileTypes)
|
|
312
|
+
.sort(([,a], [,b]) => b - a)
|
|
313
|
+
.slice(0, 5);
|
|
314
|
+
|
|
315
|
+
if (topTypes.length > 0) {
|
|
316
|
+
console.log(' 主要文件类型:');
|
|
317
|
+
topTypes.forEach(([ext, count]) => {
|
|
318
|
+
console.log(` ${ext}: ${chalk.cyan(count)} 个`);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 格式化字节大小
|
|
325
|
+
*/
|
|
326
|
+
function formatBytes(bytes) {
|
|
327
|
+
if (bytes === 0) return '0 B';
|
|
328
|
+
|
|
329
|
+
const k = 1024;
|
|
330
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
331
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
332
|
+
|
|
333
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
334
334
|
}
|