@agile-team/robot-cli 1.1.8 → 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/download.js
CHANGED
|
@@ -1,205 +1,205 @@
|
|
|
1
|
-
// lib/download.js - 简化版,移除缓存功能
|
|
2
|
-
import fs from 'fs-extra';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import os from 'os';
|
|
5
|
-
import fetch from 'node-fetch';
|
|
6
|
-
import extract from 'extract-zip';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 解析仓库URL,构建下载链接
|
|
10
|
-
*/
|
|
11
|
-
function buildDownloadUrl(repoUrl) {
|
|
12
|
-
try {
|
|
13
|
-
const url = new URL(repoUrl);
|
|
14
|
-
const hostname = url.hostname;
|
|
15
|
-
|
|
16
|
-
if (hostname === 'github.com') {
|
|
17
|
-
return `${repoUrl}/archive/refs/heads/main.zip`;
|
|
18
|
-
} else if (hostname === 'gitee.com') {
|
|
19
|
-
return `${repoUrl}/repository/archive/master.zip`;
|
|
20
|
-
} else if (hostname === 'gitlab.com') {
|
|
21
|
-
const repoName = repoUrl.split('/').pop();
|
|
22
|
-
return `${repoUrl}/-/archive/main/${repoName}-main.zip`;
|
|
23
|
-
} else {
|
|
24
|
-
return `${repoUrl}/archive/refs/heads/main.zip`;
|
|
25
|
-
}
|
|
26
|
-
} catch (error) {
|
|
27
|
-
return `${repoUrl}/archive/refs/heads/main.zip`;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 尝试从多个源下载
|
|
33
|
-
*/
|
|
34
|
-
async function tryDownload(repoUrl, spinner) {
|
|
35
|
-
const url = new URL(repoUrl);
|
|
36
|
-
const hostname = url.hostname;
|
|
37
|
-
|
|
38
|
-
// 根据平台选择镜像源
|
|
39
|
-
let mirrors = [];
|
|
40
|
-
|
|
41
|
-
if (hostname === 'github.com') {
|
|
42
|
-
mirrors = [
|
|
43
|
-
repoUrl, // 官方源
|
|
44
|
-
`https://ghproxy.com/${repoUrl}` // GitHub 代理
|
|
45
|
-
];
|
|
46
|
-
} else {
|
|
47
|
-
mirrors = [repoUrl];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
for (let i = 0; i < mirrors.length; i++) {
|
|
51
|
-
const currentUrl = mirrors[i];
|
|
52
|
-
const isOriginal = currentUrl === repoUrl;
|
|
53
|
-
const sourceName = isOriginal ? `${hostname} 官方` : `${hostname} 镜像`;
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
if (spinner) {
|
|
57
|
-
spinner.text = `🔍 连接到 ${sourceName}...`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const downloadUrl = buildDownloadUrl(currentUrl);
|
|
61
|
-
|
|
62
|
-
if (spinner) {
|
|
63
|
-
spinner.text = `📦 从 ${sourceName} 下载模板...`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const response = await fetch(downloadUrl, {
|
|
67
|
-
timeout: isOriginal ? 15000 : 10000,
|
|
68
|
-
headers: {
|
|
69
|
-
'User-Agent': 'Robot-CLI/1.0.0'
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
if (!response.ok) {
|
|
74
|
-
if (response.status === 404) {
|
|
75
|
-
throw new Error(`仓库不存在: ${repoUrl}`);
|
|
76
|
-
}
|
|
77
|
-
throw new Error(`HTTP ${response.status}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (spinner) {
|
|
81
|
-
const contentLength = response.headers.get('content-length');
|
|
82
|
-
if (contentLength) {
|
|
83
|
-
const sizeInMB = (parseInt(contentLength) / 1024 / 1024).toFixed(1);
|
|
84
|
-
spinner.text = `📦 下载中... (${sizeInMB}MB from ${sourceName})`;
|
|
85
|
-
} else {
|
|
86
|
-
spinner.text = `📦 下载中... (from ${sourceName})`;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return { response, sourceName };
|
|
91
|
-
|
|
92
|
-
} catch (error) {
|
|
93
|
-
if (i === mirrors.length - 1) {
|
|
94
|
-
throw error;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (spinner) {
|
|
98
|
-
spinner.text = `⚠️ ${sourceName} 访问失败,尝试其他源...`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* 下载模板 - 简化版,总是下载最新版本
|
|
108
|
-
*/
|
|
109
|
-
export async function downloadTemplate(template, options = {}) {
|
|
110
|
-
const { spinner } = options;
|
|
111
|
-
|
|
112
|
-
// 验证模板参数
|
|
113
|
-
if (!template || !template.repoUrl) {
|
|
114
|
-
throw new Error(`模板配置无效: ${JSON.stringify(template)}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
if (spinner) {
|
|
119
|
-
spinner.text = '🌐 开始下载最新模板...';
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// 尝试从不同源下载
|
|
123
|
-
const { response, sourceName } = await tryDownload(template.repoUrl, spinner);
|
|
124
|
-
|
|
125
|
-
if (spinner) {
|
|
126
|
-
spinner.text = '💾 保存下载文件...';
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// 创建临时目录和文件
|
|
130
|
-
const timestamp = Date.now();
|
|
131
|
-
const tempZipPath = path.join(os.tmpdir(), `robot-template-${timestamp}.zip`);
|
|
132
|
-
const tempExtractPath = path.join(os.tmpdir(), `robot-extract-${timestamp}`);
|
|
133
|
-
|
|
134
|
-
// 保存下载的文件
|
|
135
|
-
const buffer = await response.buffer();
|
|
136
|
-
await fs.writeFile(tempZipPath, buffer);
|
|
137
|
-
|
|
138
|
-
if (spinner) {
|
|
139
|
-
spinner.text = '📂 解压模板文件...';
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// 解压文件
|
|
143
|
-
await extract(tempZipPath, { dir: tempExtractPath });
|
|
144
|
-
|
|
145
|
-
if (spinner) {
|
|
146
|
-
spinner.text = '🔍 查找项目结构...';
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// 查找项目目录
|
|
150
|
-
const extractedItems = await fs.readdir(tempExtractPath);
|
|
151
|
-
const projectDir = extractedItems.find(item =>
|
|
152
|
-
item.endsWith('-main') ||
|
|
153
|
-
item.endsWith('-master') ||
|
|
154
|
-
item === template.repoUrl.split('/').pop()
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
if (!projectDir) {
|
|
158
|
-
throw new Error(`解压后找不到项目目录,可用目录: ${extractedItems.join(', ')}`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const sourcePath = path.join(tempExtractPath, projectDir);
|
|
162
|
-
|
|
163
|
-
// 验证模板完整性
|
|
164
|
-
if (spinner) {
|
|
165
|
-
spinner.text = '✅ 验证模板完整性...';
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const packageJsonPath = path.join(sourcePath, 'package.json');
|
|
169
|
-
if (!fs.existsSync(packageJsonPath)) {
|
|
170
|
-
throw new Error(`模板缺少 package.json 文件`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (spinner) {
|
|
174
|
-
spinner.text = `🎉 模板下载完成 (via ${sourceName})`;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// 清理zip文件,但保留解压的源码目录供后续使用
|
|
178
|
-
await fs.remove(tempZipPath).catch(() => {});
|
|
179
|
-
|
|
180
|
-
return sourcePath;
|
|
181
|
-
|
|
182
|
-
} catch (error) {
|
|
183
|
-
// 清理临时文件
|
|
184
|
-
try {
|
|
185
|
-
const tempFiles = await fs.readdir(os.tmpdir());
|
|
186
|
-
const robotTempFiles = tempFiles.filter(file =>
|
|
187
|
-
file.includes('robot-template-') || file.includes('robot-extract-')
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
for (const file of robotTempFiles) {
|
|
191
|
-
await fs.remove(path.join(os.tmpdir(), file)).catch(() => {});
|
|
192
|
-
}
|
|
193
|
-
} catch (cleanupError) {
|
|
194
|
-
// 忽略清理错误
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
let errorMessage = `模板下载失败: ${error.message}`;
|
|
198
|
-
|
|
199
|
-
if (error.code === 'ENOTFOUND' || error.message.includes('网络')) {
|
|
200
|
-
errorMessage += '\n\n💡 建议:\n1. 检查网络连接\n2. 如果在国内,尝试使用科学上网\n3. 稍后重试';
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
throw new Error(errorMessage);
|
|
204
|
-
}
|
|
1
|
+
// lib/download.js - 简化版,移除缓存功能
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import fetch from 'node-fetch';
|
|
6
|
+
import extract from 'extract-zip';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 解析仓库URL,构建下载链接
|
|
10
|
+
*/
|
|
11
|
+
function buildDownloadUrl(repoUrl) {
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(repoUrl);
|
|
14
|
+
const hostname = url.hostname;
|
|
15
|
+
|
|
16
|
+
if (hostname === 'github.com') {
|
|
17
|
+
return `${repoUrl}/archive/refs/heads/main.zip`;
|
|
18
|
+
} else if (hostname === 'gitee.com') {
|
|
19
|
+
return `${repoUrl}/repository/archive/master.zip`;
|
|
20
|
+
} else if (hostname === 'gitlab.com') {
|
|
21
|
+
const repoName = repoUrl.split('/').pop();
|
|
22
|
+
return `${repoUrl}/-/archive/main/${repoName}-main.zip`;
|
|
23
|
+
} else {
|
|
24
|
+
return `${repoUrl}/archive/refs/heads/main.zip`;
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return `${repoUrl}/archive/refs/heads/main.zip`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 尝试从多个源下载
|
|
33
|
+
*/
|
|
34
|
+
async function tryDownload(repoUrl, spinner) {
|
|
35
|
+
const url = new URL(repoUrl);
|
|
36
|
+
const hostname = url.hostname;
|
|
37
|
+
|
|
38
|
+
// 根据平台选择镜像源
|
|
39
|
+
let mirrors = [];
|
|
40
|
+
|
|
41
|
+
if (hostname === 'github.com') {
|
|
42
|
+
mirrors = [
|
|
43
|
+
repoUrl, // 官方源
|
|
44
|
+
`https://ghproxy.com/${repoUrl}` // GitHub 代理
|
|
45
|
+
];
|
|
46
|
+
} else {
|
|
47
|
+
mirrors = [repoUrl];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < mirrors.length; i++) {
|
|
51
|
+
const currentUrl = mirrors[i];
|
|
52
|
+
const isOriginal = currentUrl === repoUrl;
|
|
53
|
+
const sourceName = isOriginal ? `${hostname} 官方` : `${hostname} 镜像`;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
if (spinner) {
|
|
57
|
+
spinner.text = `🔍 连接到 ${sourceName}...`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const downloadUrl = buildDownloadUrl(currentUrl);
|
|
61
|
+
|
|
62
|
+
if (spinner) {
|
|
63
|
+
spinner.text = `📦 从 ${sourceName} 下载模板...`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const response = await fetch(downloadUrl, {
|
|
67
|
+
timeout: isOriginal ? 15000 : 10000,
|
|
68
|
+
headers: {
|
|
69
|
+
'User-Agent': 'Robot-CLI/1.0.0'
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
if (response.status === 404) {
|
|
75
|
+
throw new Error(`仓库不存在: ${repoUrl}`);
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`HTTP ${response.status}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (spinner) {
|
|
81
|
+
const contentLength = response.headers.get('content-length');
|
|
82
|
+
if (contentLength) {
|
|
83
|
+
const sizeInMB = (parseInt(contentLength) / 1024 / 1024).toFixed(1);
|
|
84
|
+
spinner.text = `📦 下载中... (${sizeInMB}MB from ${sourceName})`;
|
|
85
|
+
} else {
|
|
86
|
+
spinner.text = `📦 下载中... (from ${sourceName})`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { response, sourceName };
|
|
91
|
+
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (i === mirrors.length - 1) {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (spinner) {
|
|
98
|
+
spinner.text = `⚠️ ${sourceName} 访问失败,尝试其他源...`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 下载模板 - 简化版,总是下载最新版本
|
|
108
|
+
*/
|
|
109
|
+
export async function downloadTemplate(template, options = {}) {
|
|
110
|
+
const { spinner } = options;
|
|
111
|
+
|
|
112
|
+
// 验证模板参数
|
|
113
|
+
if (!template || !template.repoUrl) {
|
|
114
|
+
throw new Error(`模板配置无效: ${JSON.stringify(template)}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
if (spinner) {
|
|
119
|
+
spinner.text = '🌐 开始下载最新模板...';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 尝试从不同源下载
|
|
123
|
+
const { response, sourceName } = await tryDownload(template.repoUrl, spinner);
|
|
124
|
+
|
|
125
|
+
if (spinner) {
|
|
126
|
+
spinner.text = '💾 保存下载文件...';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 创建临时目录和文件
|
|
130
|
+
const timestamp = Date.now();
|
|
131
|
+
const tempZipPath = path.join(os.tmpdir(), `robot-template-${timestamp}.zip`);
|
|
132
|
+
const tempExtractPath = path.join(os.tmpdir(), `robot-extract-${timestamp}`);
|
|
133
|
+
|
|
134
|
+
// 保存下载的文件
|
|
135
|
+
const buffer = await response.buffer();
|
|
136
|
+
await fs.writeFile(tempZipPath, buffer);
|
|
137
|
+
|
|
138
|
+
if (spinner) {
|
|
139
|
+
spinner.text = '📂 解压模板文件...';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 解压文件
|
|
143
|
+
await extract(tempZipPath, { dir: tempExtractPath });
|
|
144
|
+
|
|
145
|
+
if (spinner) {
|
|
146
|
+
spinner.text = '🔍 查找项目结构...';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 查找项目目录
|
|
150
|
+
const extractedItems = await fs.readdir(tempExtractPath);
|
|
151
|
+
const projectDir = extractedItems.find(item =>
|
|
152
|
+
item.endsWith('-main') ||
|
|
153
|
+
item.endsWith('-master') ||
|
|
154
|
+
item === template.repoUrl.split('/').pop()
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (!projectDir) {
|
|
158
|
+
throw new Error(`解压后找不到项目目录,可用目录: ${extractedItems.join(', ')}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const sourcePath = path.join(tempExtractPath, projectDir);
|
|
162
|
+
|
|
163
|
+
// 验证模板完整性
|
|
164
|
+
if (spinner) {
|
|
165
|
+
spinner.text = '✅ 验证模板完整性...';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const packageJsonPath = path.join(sourcePath, 'package.json');
|
|
169
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
170
|
+
throw new Error(`模板缺少 package.json 文件`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (spinner) {
|
|
174
|
+
spinner.text = `🎉 模板下载完成 (via ${sourceName})`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 清理zip文件,但保留解压的源码目录供后续使用
|
|
178
|
+
await fs.remove(tempZipPath).catch(() => {});
|
|
179
|
+
|
|
180
|
+
return sourcePath;
|
|
181
|
+
|
|
182
|
+
} catch (error) {
|
|
183
|
+
// 清理临时文件
|
|
184
|
+
try {
|
|
185
|
+
const tempFiles = await fs.readdir(os.tmpdir());
|
|
186
|
+
const robotTempFiles = tempFiles.filter(file =>
|
|
187
|
+
file.includes('robot-template-') || file.includes('robot-extract-')
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
for (const file of robotTempFiles) {
|
|
191
|
+
await fs.remove(path.join(os.tmpdir(), file)).catch(() => {});
|
|
192
|
+
}
|
|
193
|
+
} catch (cleanupError) {
|
|
194
|
+
// 忽略清理错误
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let errorMessage = `模板下载失败: ${error.message}`;
|
|
198
|
+
|
|
199
|
+
if (error.code === 'ENOTFOUND' || error.message.includes('网络')) {
|
|
200
|
+
errorMessage += '\n\n💡 建议:\n1. 检查网络连接\n2. 如果在国内,尝试使用科学上网\n3. 稍后重试';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
throw new Error(errorMessage);
|
|
204
|
+
}
|
|
205
205
|
}
|