@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 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 = "\u5F00\u59CB\u4E0B\u8F7D\u6700\u65B0\u6A21\u677F...";
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\u4E0B\u8F7D\u6587\u4EF6...";
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}MB/${totalMB}MB (${sourceName})`;
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
- saveToCache(template.repoUrl, sourcePath, branch).catch(() => {
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 (downloadError) {
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
- console.log(` ${chalk.yellow(" \u6CE8\u610F: \u4F7F\u7528\u7F13\u5B58\u7248\u672C\uFF08\u975E\u6700\u65B0\uFF09")}`);
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 = downloadError.message;
577
- const isTimeout = errMsg.includes("aborted") || errMsg.includes("timeout") || downloadError.name === "TimeoutError";
578
- let msg = `\u6A21\u677F\u4E0B\u8F7D\u5931\u8D25: ${errMsg}`;
579
- if (isTimeout) {
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agile-team/robot-cli",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "现代化项目脚手架工具,支持多技术栈快速创建项目 - 优先 bun,兼容 npm/pnpm/yarn",
5
5
  "type": "module",
6
6
  "bin": {