@agile-team/robot-cli 3.0.2 → 3.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.
- package/CHANGELOG.md +24 -0
- package/dist/index.js +100 -28
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [3.0.4] - 2026-03-29
|
|
9
|
+
|
|
10
|
+
### Changed (Breaking)
|
|
11
|
+
|
|
12
|
+
- **彻底重写下载策略 — `git clone --depth=1` 作为主方案**
|
|
13
|
+
- 根本原因分析:HTTP ZIP 下载受到国内网络封锁、Gitee 登录拦截、Content-Type 不一致等各种干扰,是错误的技术路线
|
|
14
|
+
- `git clone --depth=1` 是 create-vue / create-vite / degit / giget 等所有主流 CLI 的底层实现
|
|
15
|
+
- 自动继承用户系统级 git 代理配置 (`http.proxy` / `https.proxy`),无需 CLI 工具自己处理网络问题
|
|
16
|
+
- 实时显示 git 进度条(解析 git stderr 百分比输出)
|
|
17
|
+
- 无超时问题:git 自己管理连接,有数据在流就不断
|
|
18
|
+
- 无 ZIP 解析问题:直接输出目录结构
|
|
19
|
+
|
|
20
|
+
- **优先顺序**:Gitee git clone(国内快)→ GitHub git clone → HTTP ZIP 兜底(仅当系统无 git 时)
|
|
21
|
+
|
|
22
|
+
- **缓存前置**:启动时先检查缓存,命中则直接用,不发起任何网络请求
|
|
23
|
+
|
|
24
|
+
## [3.0.3] - 2026-03-29
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **根治下载超时的真正根因** — `AbortSignal.timeout(ms)` 是总时限(含 body 流式下载),大文件(10-20MB)在国内网络 30 秒内根本下不完,必然中断
|
|
29
|
+
- 改用 `AbortController` + 手动 `clearTimeout`:只对"连接建立/收到响应头"计时(30s),一旦连接成功立即取消计时器,body 流式下载不再受任何时间限制
|
|
30
|
+
- 与 v1.x 的 `node-fetch` `timeout` 行为完全一致(活跃超时,而非总时限)
|
|
31
|
+
|
|
8
32
|
## [3.0.2] - 2026-03-29
|
|
9
33
|
|
|
10
34
|
### Fixed
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { execSync as execSync2 } from "child_process";
|
|
|
18
18
|
import fs from "fs-extra";
|
|
19
19
|
import path from "path";
|
|
20
20
|
import os from "os";
|
|
21
|
+
import { spawn } from "child_process";
|
|
21
22
|
import chalk from "chalk";
|
|
22
23
|
import extract from "extract-zip";
|
|
23
24
|
|
|
@@ -404,18 +405,21 @@ function buildDownloadSources(repoUrl, branch, giteeUrl) {
|
|
|
404
405
|
}
|
|
405
406
|
return sources;
|
|
406
407
|
}
|
|
407
|
-
async function fetchWithRetry(downloadUrl,
|
|
408
|
+
async function fetchWithRetry(downloadUrl, connectionTimeoutMs, retries, extraHeaders) {
|
|
408
409
|
let lastError;
|
|
409
410
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
411
|
+
const controller = new AbortController();
|
|
412
|
+
const timer = setTimeout(() => controller.abort(), connectionTimeoutMs);
|
|
410
413
|
try {
|
|
411
414
|
const response = await fetch(downloadUrl, {
|
|
412
|
-
signal:
|
|
415
|
+
signal: controller.signal,
|
|
413
416
|
redirect: "follow",
|
|
414
417
|
headers: {
|
|
415
418
|
"User-Agent": "Robot-CLI/3.0",
|
|
416
419
|
...extraHeaders
|
|
417
420
|
}
|
|
418
421
|
});
|
|
422
|
+
clearTimeout(timer);
|
|
419
423
|
if (!response.ok) {
|
|
420
424
|
if (response.status === 404) throw new Error(`\u4ED3\u5E93\u4E0D\u5B58\u5728 (404)`);
|
|
421
425
|
throw new Error(`HTTP ${response.status}`);
|
|
@@ -426,6 +430,7 @@ async function fetchWithRetry(downloadUrl, timeout, retries, extraHeaders) {
|
|
|
426
430
|
}
|
|
427
431
|
return response;
|
|
428
432
|
} catch (error) {
|
|
433
|
+
clearTimeout(timer);
|
|
429
434
|
lastError = error;
|
|
430
435
|
if (lastError.message.includes("404")) throw lastError;
|
|
431
436
|
if (attempt < retries) {
|
|
@@ -478,6 +483,43 @@ function assertZipBuffer(buffer, sourceName) {
|
|
|
478
483
|
);
|
|
479
484
|
}
|
|
480
485
|
}
|
|
486
|
+
async function gitCloneTemplate(repoUrl, branch, targetDir, spinner) {
|
|
487
|
+
return new Promise((resolve, reject) => {
|
|
488
|
+
if (spinner) spinner.text = `\u8FDE\u63A5\u4E2D...`;
|
|
489
|
+
const args = [
|
|
490
|
+
"clone",
|
|
491
|
+
"--depth=1",
|
|
492
|
+
"--single-branch",
|
|
493
|
+
"--branch",
|
|
494
|
+
branch,
|
|
495
|
+
repoUrl,
|
|
496
|
+
targetDir
|
|
497
|
+
];
|
|
498
|
+
const proc = spawn("git", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
499
|
+
proc.stderr?.on("data", (chunk) => {
|
|
500
|
+
if (!spinner) return;
|
|
501
|
+
const text3 = chunk.toString();
|
|
502
|
+
const pctMatch = text3.match(/(\d+)%\s*\((\d+)\/(\d+)\)/);
|
|
503
|
+
if (pctMatch) {
|
|
504
|
+
const pct = parseInt(pctMatch[1]);
|
|
505
|
+
const filled = Math.round(pct / 5);
|
|
506
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(20 - filled);
|
|
507
|
+
const speed = text3.match(/([\d.]+\s*[KMG]iB\/s)/)?.[1] ?? "";
|
|
508
|
+
spinner.text = `\u4E0B\u8F7D\u4E2D [${bar}] ${pct}%${speed ? " " + speed : ""}`;
|
|
509
|
+
} else {
|
|
510
|
+
const line = text3.split(/\r?\n/).find((l) => l.trim())?.trim() ?? "";
|
|
511
|
+
if (line && line.length < 80) spinner.text = `\u514B\u9686\u4E2D... ${line}`;
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
proc.on("error", (err) => {
|
|
515
|
+
reject(new Error(err.code === "ENOENT" ? "GIT_NOT_FOUND" : `git \u542F\u52A8\u5931\u8D25: ${err.message}`));
|
|
516
|
+
});
|
|
517
|
+
proc.on("close", (code) => {
|
|
518
|
+
if (code === 0) resolve();
|
|
519
|
+
else reject(new Error(`git clone \u5931\u8D25 (exit ${code})`));
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
}
|
|
481
523
|
async function downloadTemplate(template, options = {}) {
|
|
482
524
|
const { spinner, noCache, giteeUrl: optGiteeUrl } = options;
|
|
483
525
|
const branch = template.branch || "main";
|
|
@@ -485,15 +527,56 @@ async function downloadTemplate(template, options = {}) {
|
|
|
485
527
|
if (!template?.repoUrl) {
|
|
486
528
|
throw new Error(`\u6A21\u677F\u914D\u7F6E\u65E0\u6548: ${JSON.stringify(template)}`);
|
|
487
529
|
}
|
|
530
|
+
if (!noCache) {
|
|
531
|
+
const cached = await getCachedTemplate(template.repoUrl);
|
|
532
|
+
if (cached) {
|
|
533
|
+
if (spinner) spinner.text = "\u5DF2\u4F7F\u7528\u7F13\u5B58\u6A21\u677F\uFF0C\u5982\u9700\u66F4\u65B0\u8BF7\u8FD0\u884C robot cache clear";
|
|
534
|
+
return cached;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const cloneSources = [];
|
|
538
|
+
if (giteeUrl) cloneSources.push({ url: giteeUrl, name: "Gitee" });
|
|
539
|
+
cloneSources.push({ url: template.repoUrl, name: "GitHub" });
|
|
540
|
+
let noGit = false;
|
|
541
|
+
const cloneErrors = [];
|
|
542
|
+
for (const { url: cloneUrl, name: srcName } of cloneSources) {
|
|
543
|
+
const tempDir = path.join(os.tmpdir(), `robot-git-${Date.now()}`);
|
|
544
|
+
try {
|
|
545
|
+
if (spinner) spinner.text = `\u8FDE\u63A5 ${srcName}...`;
|
|
546
|
+
await gitCloneTemplate(cloneUrl, branch, tempDir, spinner);
|
|
547
|
+
await fs.remove(path.join(tempDir, ".git")).catch(() => {
|
|
548
|
+
});
|
|
549
|
+
if (spinner) spinner.text = "\u9A8C\u8BC1\u6A21\u677F\u5B8C\u6574\u6027...";
|
|
550
|
+
if (!fs.existsSync(path.join(tempDir, "package.json"))) {
|
|
551
|
+
throw new Error("\u6A21\u677F\u7F3A\u5C11 package.json");
|
|
552
|
+
}
|
|
553
|
+
if (!noCache) saveToCache(template.repoUrl, tempDir, branch).catch(() => {
|
|
554
|
+
});
|
|
555
|
+
if (spinner) spinner.text = `\u6A21\u677F\u4E0B\u8F7D\u5B8C\u6210 (${srcName})`;
|
|
556
|
+
return tempDir;
|
|
557
|
+
} catch (err) {
|
|
558
|
+
await fs.remove(tempDir).catch(() => {
|
|
559
|
+
});
|
|
560
|
+
const msg = err.message;
|
|
561
|
+
if (msg === "GIT_NOT_FOUND") {
|
|
562
|
+
noGit = true;
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
cloneErrors.push(`${srcName}: ${msg}`);
|
|
566
|
+
if (spinner) spinner.text = `${srcName} \u514B\u9686\u5931\u8D25\uFF0C\u5C1D\u8BD5\u4E0B\u4E00\u4E2A\u6E90...`;
|
|
567
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (noGit && spinner) spinner.text = "\u7CFB\u7EDF\u672A\u5B89\u88C5 git\uFF0C\u5207\u6362\u5230 HTTP \u4E0B\u8F7D...";
|
|
488
571
|
try {
|
|
489
|
-
if (spinner) spinner.text = "\
|
|
572
|
+
if (spinner) spinner.text = "HTTP \u4E0B\u8F7D\u6A21\u677F\u4E2D...";
|
|
490
573
|
const { response, sourceName } = await tryDownload(
|
|
491
574
|
template.repoUrl,
|
|
492
575
|
branch,
|
|
493
576
|
spinner,
|
|
494
577
|
giteeUrl
|
|
495
578
|
);
|
|
496
|
-
if (spinner) spinner.text = "\u4FDD\u5B58\
|
|
579
|
+
if (spinner) spinner.text = "\u4FDD\u5B58\u6587\u4EF6...";
|
|
497
580
|
const timestamp = Date.now();
|
|
498
581
|
const tempZip = path.join(os.tmpdir(), `robot-template-${timestamp}.zip`);
|
|
499
582
|
const tempExtract = path.join(os.tmpdir(), `robot-extract-${timestamp}`);
|
|
@@ -513,7 +596,7 @@ async function downloadTemplate(template, options = {}) {
|
|
|
513
596
|
const bar = "\u2588".repeat(filled) + "\u2591".repeat(20 - filled);
|
|
514
597
|
const sizeMB = (received / 1024 / 1024).toFixed(1);
|
|
515
598
|
const totalMB = (totalSize / 1024 / 1024).toFixed(1);
|
|
516
|
-
spinner.text = `\u4E0B\u8F7D\u4E2D [${bar}] ${pct}% ${sizeMB}
|
|
599
|
+
spinner.text = `\u4E0B\u8F7D\u4E2D [${bar}] ${pct}% ${sizeMB}/${totalMB}MB (${sourceName})`;
|
|
517
600
|
}
|
|
518
601
|
const buffer = Buffer.concat(chunks);
|
|
519
602
|
assertZipBuffer(buffer, sourceName);
|
|
@@ -525,59 +608,48 @@ async function downloadTemplate(template, options = {}) {
|
|
|
525
608
|
}
|
|
526
609
|
if (spinner) spinner.text = "\u89E3\u538B\u6A21\u677F\u6587\u4EF6...";
|
|
527
610
|
await extract(tempZip, { dir: tempExtract });
|
|
528
|
-
if (spinner) spinner.text = "\u67E5\u627E\u9879\u76EE\u7ED3\u6784...";
|
|
529
611
|
const extractedItems = await fs.readdir(tempExtract);
|
|
530
612
|
const repoName = template.repoUrl.split("/").pop() || "";
|
|
531
613
|
const projectDir = extractedItems.find(
|
|
532
614
|
(item) => item === `${repoName}-${branch}` || item.endsWith(`-${branch}`) || item.endsWith("-main") || item.endsWith("-master") || item === repoName
|
|
533
615
|
);
|
|
534
616
|
if (!projectDir) {
|
|
535
|
-
throw new Error(
|
|
536
|
-
`\u89E3\u538B\u540E\u627E\u4E0D\u5230\u9879\u76EE\u76EE\u5F55\uFF0C\u53EF\u7528\u76EE\u5F55: ${extractedItems.join(", ")}`
|
|
537
|
-
);
|
|
617
|
+
throw new Error(`\u89E3\u538B\u540E\u627E\u4E0D\u5230\u9879\u76EE\u76EE\u5F55\uFF0C\u53EF\u7528\u76EE\u5F55: ${extractedItems.join(", ")}`);
|
|
538
618
|
}
|
|
539
619
|
const sourcePath = path.join(tempExtract, projectDir);
|
|
540
|
-
if (spinner) spinner.text = "\u9A8C\u8BC1\u6A21\u677F\u5B8C\u6574\u6027...";
|
|
541
620
|
if (!fs.existsSync(path.join(sourcePath, "package.json"))) {
|
|
542
621
|
throw new Error("\u6A21\u677F\u7F3A\u5C11 package.json \u6587\u4EF6");
|
|
543
622
|
}
|
|
544
|
-
if (!noCache) {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
if (spinner) spinner.text = `\u6A21\u677F\u4E0B\u8F7D\u5B8C\u6210 (via ${sourceName})`;
|
|
623
|
+
if (!noCache) saveToCache(template.repoUrl, sourcePath, branch).catch(() => {
|
|
624
|
+
});
|
|
625
|
+
if (spinner) spinner.text = `\u6A21\u677F\u4E0B\u8F7D\u5B8C\u6210 (HTTP/${sourceName})`;
|
|
549
626
|
await fs.remove(tempZip).catch(() => {
|
|
550
627
|
});
|
|
551
628
|
return sourcePath;
|
|
552
|
-
} catch (
|
|
629
|
+
} catch (httpError) {
|
|
553
630
|
if (!noCache) {
|
|
554
631
|
const cached = await getCachedTemplate(template.repoUrl);
|
|
555
632
|
if (cached) {
|
|
556
633
|
if (spinner) spinner.text = "\u7F51\u7EDC\u4E0D\u53EF\u7528\uFF0C\u4F7F\u7528\u7F13\u5B58\u6A21\u677F...";
|
|
557
|
-
console.log(
|
|
558
|
-
|
|
634
|
+
console.log(`
|
|
635
|
+
${chalk.yellow("\u6CE8\u610F: \u5DF2\u4F7F\u7528\u7F13\u5B58\u7248\u672C\uFF08\u975E\u6700\u65B0\uFF09")}`);
|
|
559
636
|
return cached;
|
|
560
637
|
}
|
|
561
638
|
}
|
|
562
639
|
try {
|
|
563
640
|
const tmpFiles = await fs.readdir(os.tmpdir());
|
|
564
641
|
for (const f of tmpFiles.filter(
|
|
565
|
-
(x) => x.includes("robot-template-") || x.includes("robot-extract-")
|
|
642
|
+
(x) => x.includes("robot-template-") || x.includes("robot-extract-") || x.includes("robot-git-")
|
|
566
643
|
)) {
|
|
567
644
|
await fs.remove(path.join(os.tmpdir(), f)).catch(() => {
|
|
568
645
|
});
|
|
569
646
|
}
|
|
570
647
|
} catch {
|
|
571
648
|
}
|
|
572
|
-
const errMsg =
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
msg += "\n\n \u5EFA\u8BAE:\n 1. \u5F53\u524D\u7F51\u7EDC\u8FDE\u63A5\u8F83\u6162\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\n 2. \u5982\u679C\u5728\u56FD\u5185\uFF0C\u5C1D\u8BD5\u4F7F\u7528\u4EE3\u7406\u6216\u79D1\u5B66\u4E0A\u7F51\n 3. \u4F7F\u7528 robot doctor \u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5";
|
|
577
|
-
} else if (downloadError.code === "ENOTFOUND") {
|
|
578
|
-
msg += "\n\n \u5EFA\u8BAE:\n 1. \u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u662F\u5426\u6B63\u5E38\n 2. \u5982\u679C\u5728\u56FD\u5185\uFF0C\u5C1D\u8BD5\u4F7F\u7528\u4EE3\u7406\n 3. \u7A0D\u540E\u91CD\u8BD5";
|
|
579
|
-
}
|
|
580
|
-
throw new Error(msg);
|
|
649
|
+
const errMsg = httpError.message;
|
|
650
|
+
const allErrors = [...cloneErrors, `HTTP: ${errMsg}`].map((e) => ` - ${e}`).join("\n");
|
|
651
|
+
throw new Error(`\u6A21\u677F\u4E0B\u8F7D\u5931\u8D25\uFF0C\u6240\u6709\u65B9\u5F0F\u5747\u4E0D\u53EF\u7528:
|
|
652
|
+
${allErrors}`);
|
|
581
653
|
}
|
|
582
654
|
}
|
|
583
655
|
|