@agile-team/robot-cli 3.0.0 → 3.0.1

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,20 @@ 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.1] - 2026-03-28
9
+
10
+ ### Fixed
11
+
12
+ - **下载策略全面重写** — 参考 giget (unjs/giget, 306k+ 项目使用) 的下载方式:
13
+ - Gitee 备用源提升为首选 (国内用户最快)
14
+ - 使用 `codeload.github.com` 直连 CDN 替代 `github.com/archive/` (跳过 302 重定向)
15
+ - 使用 `api.github.com` REST API 作为二级备选 (与 giget 一致)
16
+ - `github.com` 原始链接降为最后兜底
17
+ - **移除不稳定第三方镜像** — 移除 `hub.gitmirror.com` 和 `ghproxy.net` (不可控第三方代理)
18
+ - **超时优化** — 快速源 (Gitee/codeload) 30s, 慢速源 (API/github.com) 60s, 不再需要 120s
19
+ - **重试缩减** — 每源 2 次重试 (原 3 次), 退避时间 1s/2s (原 2s/4s), 快速失败切换下一源
20
+ - **错误信息增强** — 失败时列出每个源的具体错误原因
21
+
8
22
  ## [3.0.0] - 2026-03-28
9
23
 
10
24
  ### ⚠️ BREAKING CHANGES
package/dist/index.js CHANGED
@@ -266,12 +266,29 @@ var START_COMMAND_MAP = {
266
266
  // src/download.ts
267
267
  var CACHE_DIR = path.join(os.homedir(), ".robot-cli", CACHE_DIR_NAME);
268
268
  var CACHE_INDEX_PATH = path.join(CACHE_DIR, "index.json");
269
+ function parseGitHubRepo(repoUrl) {
270
+ try {
271
+ const url = new URL(repoUrl);
272
+ if (url.hostname !== "github.com") return null;
273
+ const parts = url.pathname.replace(/^\/|\/$/g, "").split("/");
274
+ if (parts.length >= 2) return { owner: parts[0], repo: parts[1] };
275
+ return null;
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
269
280
  function buildDownloadUrl(repoUrl, branch = "main") {
270
281
  try {
271
282
  const url = new URL(repoUrl);
272
283
  const host = url.hostname;
273
284
  const cleanUrl = repoUrl.replace(/\/+$/, "");
274
- if (host === "github.com") return `${cleanUrl}/archive/refs/heads/${branch}.zip`;
285
+ if (host === "github.com") {
286
+ const gh = parseGitHubRepo(cleanUrl);
287
+ if (gh) return `https://codeload.github.com/${gh.owner}/${gh.repo}/zip/refs/heads/${branch}`;
288
+ return `${cleanUrl}/archive/refs/heads/${branch}.zip`;
289
+ }
290
+ if (host === "codeload.github.com") return `${cleanUrl}/zip/refs/heads/${branch}`;
291
+ if (host === "api.github.com") return cleanUrl;
275
292
  if (host === "gitee.com")
276
293
  return `${cleanUrl}/repository/archive/${branch}.zip`;
277
294
  if (host === "gitlab.com") {
@@ -343,28 +360,61 @@ async function getCacheStats() {
343
360
  async function clearCache() {
344
361
  await fs.remove(CACHE_DIR);
345
362
  }
346
- var TIMEOUT_PRIMARY = 12e4;
347
- var TIMEOUT_MIRROR = 6e4;
348
- var MAX_RETRIES = 3;
349
- function getGitHubMirrors(repoUrl, giteeUrl) {
350
- const mirrors = [
351
- repoUrl,
352
- // 1. 原始 GitHub
353
- repoUrl.replace("github.com", "hub.gitmirror.com"),
354
- // 2. gitmirror
355
- `https://ghproxy.net/${repoUrl}`
356
- // 3. ghproxy.net
357
- ];
358
- if (giteeUrl) mirrors.push(giteeUrl);
359
- return mirrors;
363
+ var TIMEOUT_FAST = 3e4;
364
+ var TIMEOUT_SLOW = 6e4;
365
+ var MAX_RETRIES = 2;
366
+ function buildDownloadSources(repoUrl, branch, giteeUrl) {
367
+ const sources = [];
368
+ const gh = parseGitHubRepo(repoUrl);
369
+ if (giteeUrl) {
370
+ sources.push({
371
+ url: giteeUrl,
372
+ name: "gitee.com",
373
+ timeout: TIMEOUT_FAST
374
+ });
375
+ }
376
+ if (gh) {
377
+ sources.push({
378
+ url: `https://codeload.github.com/${gh.owner}/${gh.repo}/zip/refs/heads/${branch}`,
379
+ name: "codeload.github.com",
380
+ timeout: TIMEOUT_FAST,
381
+ isDirect: true
382
+ });
383
+ sources.push({
384
+ url: `https://api.github.com/repos/${gh.owner}/${gh.repo}/zipball/${branch}`,
385
+ name: "api.github.com",
386
+ timeout: TIMEOUT_SLOW,
387
+ isDirect: true,
388
+ headers: {
389
+ Accept: "application/vnd.github+json",
390
+ "X-GitHub-Api-Version": "2022-11-28"
391
+ }
392
+ });
393
+ sources.push({
394
+ url: repoUrl,
395
+ name: "github.com",
396
+ timeout: TIMEOUT_SLOW
397
+ });
398
+ } else {
399
+ sources.push({
400
+ url: repoUrl,
401
+ name: new URL(repoUrl).hostname,
402
+ timeout: TIMEOUT_SLOW
403
+ });
404
+ }
405
+ return sources;
360
406
  }
361
- async function fetchWithRetry(downloadUrl, timeout, retries) {
407
+ async function fetchWithRetry(downloadUrl, timeout, retries, extraHeaders) {
362
408
  let lastError;
363
409
  for (let attempt = 1; attempt <= retries; attempt++) {
364
410
  try {
365
411
  const response = await fetch(downloadUrl, {
366
412
  signal: AbortSignal.timeout(timeout),
367
- headers: { "User-Agent": "Robot-CLI" }
413
+ redirect: "follow",
414
+ headers: {
415
+ "User-Agent": "Robot-CLI/3.0",
416
+ ...extraHeaders
417
+ }
368
418
  });
369
419
  if (!response.ok) {
370
420
  if (response.status === 404) throw new Error(`\u4ED3\u5E93\u4E0D\u5B58\u5728 (404)`);
@@ -375,44 +425,46 @@ async function fetchWithRetry(downloadUrl, timeout, retries) {
375
425
  lastError = error;
376
426
  if (lastError.message.includes("404")) throw lastError;
377
427
  if (attempt < retries) {
378
- await new Promise((r) => setTimeout(r, 2e3 * attempt));
428
+ await new Promise((r) => setTimeout(r, 1e3 * attempt));
379
429
  }
380
430
  }
381
431
  }
382
432
  throw lastError;
383
433
  }
384
434
  async function tryDownload(repoUrl, branch = "main", spinner, giteeUrl) {
385
- const url = new URL(repoUrl);
386
- const host = url.hostname;
387
- const mirrors = host === "github.com" ? getGitHubMirrors(repoUrl, giteeUrl) : [repoUrl];
388
- for (let i = 0; i < mirrors.length; i++) {
389
- const current = mirrors[i];
390
- const isOriginal = i === 0;
391
- let sourceName;
392
- try {
393
- sourceName = isOriginal ? host : new URL(current.replace(/^(https?:\/\/[^/]+)\/.*/, "$1")).hostname;
394
- } catch {
395
- sourceName = `\u955C\u50CF ${i}`;
396
- }
435
+ const sources = buildDownloadSources(repoUrl, branch, giteeUrl);
436
+ const errors = [];
437
+ for (let i = 0; i < sources.length; i++) {
438
+ const source = sources[i];
397
439
  try {
398
- if (spinner) spinner.text = `\u8FDE\u63A5 ${sourceName} ...`;
399
- const downloadUrl = current.endsWith(".zip") ? current : buildDownloadUrl(current, branch);
400
- const timeout = isOriginal ? TIMEOUT_PRIMARY : TIMEOUT_MIRROR;
401
- if (spinner) spinner.text = `\u4ECE ${sourceName} \u4E0B\u8F7D\u6A21\u677F (\u6700\u591A\u91CD\u8BD5${MAX_RETRIES}\u6B21)...`;
402
- const response = await fetchWithRetry(downloadUrl, timeout, MAX_RETRIES);
440
+ if (spinner) spinner.text = `\u8FDE\u63A5 ${source.name} ...`;
441
+ const downloadUrl = source.isDirect ? source.url : buildDownloadUrl(source.url, branch);
442
+ if (spinner) spinner.text = `\u4ECE ${source.name} \u4E0B\u8F7D\u6A21\u677F...`;
443
+ const response = await fetchWithRetry(
444
+ downloadUrl,
445
+ source.timeout,
446
+ MAX_RETRIES,
447
+ source.headers
448
+ );
403
449
  if (spinner) {
404
450
  const len = response.headers.get("content-length");
405
451
  const sizeInfo = len ? `${(parseInt(len) / 1024 / 1024).toFixed(1)}MB ` : "";
406
- spinner.text = `\u4E0B\u8F7D\u4E2D ${sizeInfo}(${sourceName})`;
452
+ spinner.text = `\u4E0B\u8F7D\u4E2D ${sizeInfo}(${source.name})`;
407
453
  }
408
- return { response, sourceName };
454
+ return { response, sourceName: source.name };
409
455
  } catch (error) {
410
- if (i === mirrors.length - 1) throw error;
411
- if (spinner) spinner.text = `${sourceName} \u4E0D\u53EF\u7528\uFF0C\u5207\u6362\u4E0B\u4E00\u4E2A\u6E90...`;
412
- await new Promise((r) => setTimeout(r, 1e3));
456
+ const errMsg = error.message || String(error);
457
+ errors.push(`${source.name}: ${errMsg}`);
458
+ if (i < sources.length - 1 && spinner) {
459
+ spinner.text = `${source.name} \u4E0D\u53EF\u7528\uFF0C\u5207\u6362\u5230 ${sources[i + 1].name}...`;
460
+ await new Promise((r) => setTimeout(r, 500));
461
+ }
413
462
  }
414
463
  }
415
- throw new Error("\u6240\u6709\u4E0B\u8F7D\u6E90\u5747\u4E0D\u53EF\u7528");
464
+ throw new Error(
465
+ `\u6240\u6709\u4E0B\u8F7D\u6E90\u5747\u4E0D\u53EF\u7528:
466
+ ${errors.map((e) => ` - ${e}`).join("\n")}`
467
+ );
416
468
  }
417
469
  async function downloadTemplate(template, options = {}) {
418
470
  const { spinner, noCache, giteeUrl: optGiteeUrl } = options;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agile-team/robot-cli",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "现代化项目脚手架工具,支持多技术栈快速创建项目 - 优先 bun,兼容 npm/pnpm/yarn",
5
5
  "type": "module",
6
6
  "bin": {