@agile-team/robot-cli 3.0.3 → 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 +16 -0
- package/dist/index.js +94 -26
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ 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
|
+
|
|
8
24
|
## [3.0.3] - 2026-03-29
|
|
9
25
|
|
|
10
26
|
### 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
|
|
|
@@ -482,6 +483,43 @@ function assertZipBuffer(buffer, sourceName) {
|
|
|
482
483
|
);
|
|
483
484
|
}
|
|
484
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
|
+
}
|
|
485
523
|
async function downloadTemplate(template, options = {}) {
|
|
486
524
|
const { spinner, noCache, giteeUrl: optGiteeUrl } = options;
|
|
487
525
|
const branch = template.branch || "main";
|
|
@@ -489,15 +527,56 @@ async function downloadTemplate(template, options = {}) {
|
|
|
489
527
|
if (!template?.repoUrl) {
|
|
490
528
|
throw new Error(`\u6A21\u677F\u914D\u7F6E\u65E0\u6548: ${JSON.stringify(template)}`);
|
|
491
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...";
|
|
492
571
|
try {
|
|
493
|
-
if (spinner) spinner.text = "\
|
|
572
|
+
if (spinner) spinner.text = "HTTP \u4E0B\u8F7D\u6A21\u677F\u4E2D...";
|
|
494
573
|
const { response, sourceName } = await tryDownload(
|
|
495
574
|
template.repoUrl,
|
|
496
575
|
branch,
|
|
497
576
|
spinner,
|
|
498
577
|
giteeUrl
|
|
499
578
|
);
|
|
500
|
-
if (spinner) spinner.text = "\u4FDD\u5B58\
|
|
579
|
+
if (spinner) spinner.text = "\u4FDD\u5B58\u6587\u4EF6...";
|
|
501
580
|
const timestamp = Date.now();
|
|
502
581
|
const tempZip = path.join(os.tmpdir(), `robot-template-${timestamp}.zip`);
|
|
503
582
|
const tempExtract = path.join(os.tmpdir(), `robot-extract-${timestamp}`);
|
|
@@ -517,7 +596,7 @@ async function downloadTemplate(template, options = {}) {
|
|
|
517
596
|
const bar = "\u2588".repeat(filled) + "\u2591".repeat(20 - filled);
|
|
518
597
|
const sizeMB = (received / 1024 / 1024).toFixed(1);
|
|
519
598
|
const totalMB = (totalSize / 1024 / 1024).toFixed(1);
|
|
520
|
-
spinner.text = `\u4E0B\u8F7D\u4E2D [${bar}] ${pct}% ${sizeMB}
|
|
599
|
+
spinner.text = `\u4E0B\u8F7D\u4E2D [${bar}] ${pct}% ${sizeMB}/${totalMB}MB (${sourceName})`;
|
|
521
600
|
}
|
|
522
601
|
const buffer = Buffer.concat(chunks);
|
|
523
602
|
assertZipBuffer(buffer, sourceName);
|
|
@@ -529,59 +608,48 @@ async function downloadTemplate(template, options = {}) {
|
|
|
529
608
|
}
|
|
530
609
|
if (spinner) spinner.text = "\u89E3\u538B\u6A21\u677F\u6587\u4EF6...";
|
|
531
610
|
await extract(tempZip, { dir: tempExtract });
|
|
532
|
-
if (spinner) spinner.text = "\u67E5\u627E\u9879\u76EE\u7ED3\u6784...";
|
|
533
611
|
const extractedItems = await fs.readdir(tempExtract);
|
|
534
612
|
const repoName = template.repoUrl.split("/").pop() || "";
|
|
535
613
|
const projectDir = extractedItems.find(
|
|
536
614
|
(item) => item === `${repoName}-${branch}` || item.endsWith(`-${branch}`) || item.endsWith("-main") || item.endsWith("-master") || item === repoName
|
|
537
615
|
);
|
|
538
616
|
if (!projectDir) {
|
|
539
|
-
throw new Error(
|
|
540
|
-
`\u89E3\u538B\u540E\u627E\u4E0D\u5230\u9879\u76EE\u76EE\u5F55\uFF0C\u53EF\u7528\u76EE\u5F55: ${extractedItems.join(", ")}`
|
|
541
|
-
);
|
|
617
|
+
throw new Error(`\u89E3\u538B\u540E\u627E\u4E0D\u5230\u9879\u76EE\u76EE\u5F55\uFF0C\u53EF\u7528\u76EE\u5F55: ${extractedItems.join(", ")}`);
|
|
542
618
|
}
|
|
543
619
|
const sourcePath = path.join(tempExtract, projectDir);
|
|
544
|
-
if (spinner) spinner.text = "\u9A8C\u8BC1\u6A21\u677F\u5B8C\u6574\u6027...";
|
|
545
620
|
if (!fs.existsSync(path.join(sourcePath, "package.json"))) {
|
|
546
621
|
throw new Error("\u6A21\u677F\u7F3A\u5C11 package.json \u6587\u4EF6");
|
|
547
622
|
}
|
|
548
|
-
if (!noCache) {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
}
|
|
552
|
-
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})`;
|
|
553
626
|
await fs.remove(tempZip).catch(() => {
|
|
554
627
|
});
|
|
555
628
|
return sourcePath;
|
|
556
|
-
} catch (
|
|
629
|
+
} catch (httpError) {
|
|
557
630
|
if (!noCache) {
|
|
558
631
|
const cached = await getCachedTemplate(template.repoUrl);
|
|
559
632
|
if (cached) {
|
|
560
633
|
if (spinner) spinner.text = "\u7F51\u7EDC\u4E0D\u53EF\u7528\uFF0C\u4F7F\u7528\u7F13\u5B58\u6A21\u677F...";
|
|
561
|
-
console.log(
|
|
562
|
-
|
|
634
|
+
console.log(`
|
|
635
|
+
${chalk.yellow("\u6CE8\u610F: \u5DF2\u4F7F\u7528\u7F13\u5B58\u7248\u672C\uFF08\u975E\u6700\u65B0\uFF09")}`);
|
|
563
636
|
return cached;
|
|
564
637
|
}
|
|
565
638
|
}
|
|
566
639
|
try {
|
|
567
640
|
const tmpFiles = await fs.readdir(os.tmpdir());
|
|
568
641
|
for (const f of tmpFiles.filter(
|
|
569
|
-
(x) => x.includes("robot-template-") || x.includes("robot-extract-")
|
|
642
|
+
(x) => x.includes("robot-template-") || x.includes("robot-extract-") || x.includes("robot-git-")
|
|
570
643
|
)) {
|
|
571
644
|
await fs.remove(path.join(os.tmpdir(), f)).catch(() => {
|
|
572
645
|
});
|
|
573
646
|
}
|
|
574
647
|
} catch {
|
|
575
648
|
}
|
|
576
|
-
const errMsg =
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
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";
|
|
581
|
-
} else if (downloadError.code === "ENOTFOUND") {
|
|
582
|
-
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";
|
|
583
|
-
}
|
|
584
|
-
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}`);
|
|
585
653
|
}
|
|
586
654
|
}
|
|
587
655
|
|