iterate-harness 1.12.9 → 1.12.10

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 CHANGED
@@ -30,12 +30,14 @@ delegation logic used at install time and at runtime is the same:
30
30
  (overridable via `ITERATE_HARNESS_NPM_HOME`).
31
31
  3. `pip install`s the harness **pinned to this npm package's version** — npm
32
32
  `1.6.0` always installs harness `v1.6.0`. Upgrading the npm package
33
- automatically re-installs the matching harness version. The preferred
34
- artifact is the **pre-built wheel** uploaded to the GitHub release
35
- (`iterate_harness-<version>-py3-none-any.whl`, which already bundles the
36
- compiled frontend assets — exactly like iterate-skill-installer ships
37
- pre-wrapped assets). If the wheel is missing, the wrapper falls back to the
38
- pinned **source archive** as a last resort.
33
+ automatically re-installs the matching harness version on the next run.
34
+ The preferred source is the official **PyPI** index: pip resolves
35
+ `iterate-harness==X.Y.Z` against your configured mirror (e.g.
36
+ `pypi.tuna.tsinghua.edu.cn`), so installs work even when GitHub is
37
+ unreachable or its TLS cert fails to verify. If PyPI is unavailable, the
38
+ wrapper falls back to the pre-built **wheel** on the GitHub release
39
+ (frontend assets baked in, like iterate-skill-installer ships pre-wrapped
40
+ assets), then the pinned **source archive** as a final resort.
39
41
  4. Delegates to the venv's real `ih` executable with argv, stdio, signals and
40
42
  exit codes forwarded.
41
43
 
package/README.zh-CN.md CHANGED
@@ -27,10 +27,11 @@ harness 本身是**一个 Python 包**。这个 npm 包是一个薄分发包装
27
27
  2. 在 `~/.iterate-harness-npm/venv` 创建托管虚拟环境
28
28
  (可通过 `ITERATE_HARNESS_NPM_HOME` 覆盖)。
29
29
  3. `pip install` **与本 npm 包版本锁定的** harness 发布版本 —— npm 的 `1.6.0`
30
- 总会安装 harness `v1.6.0`。升级 npm 包会自动重装对应的 harness 版本。
31
- 优先下载 GitHub 发布里的**预构建 wheel**(`iterate_harness-<version>-py3-none-any.whl`,
32
- 已内置编译好的前端资源——和 iterate-skill-installer 分发预打包资源一致)。
33
- 若 wheel 缺失,再兑底使用锁定的**源码归档**。
30
+ 总会安装 harness `v1.6.0`。升级 npm 包会在下次运行时自动重装对应的 harness 版本。
31
+ **首选官方 PyPI 索引**:pip 会按你配置的镜像源(如 `pypi.tuna.tsinghua.edu.cn`)
32
+ 解析 `iterate-harness==X.Y.Z`,即使 GitHub 不可达或 TLS 证书校验失败也能安装。
33
+ 若 PyPI 不可用,再兑底 GitHub release 上的**预构建 wheel**(已内置编译好的前端
34
+ 资源——与 iterate-skill-installer 分发预打包资源一致),最后才用锁定的**源码归档**。
34
35
  4. 代理调用 venv 里真正的 `ih` 可执行文件,转发 argv、stdio、信号与退出码。
35
36
 
36
37
  若 `npm install` 期间无法完成安装(无 Python、无网络,或
package/lib/bootstrap.js CHANGED
@@ -8,17 +8,18 @@
8
8
  * 1. Resolve a Python interpreter >= 3.10 (env override first).
9
9
  * 2. Create/reuse the venv at ~/.iterate-harness-npm/venv.
10
10
  * 3. pip-install the harness release pinned to THIS npm package's version
11
- * (lockstep: npm 1.12.6 installs harness v1.12.6). The preferred artifact is
12
- * the pre-built wheel uploaded to the GitHub release (it already contains
13
- * the compiled frontend assets, exactly like iterate-skill-installer ships
14
- * pre-wrapped assets). pip-installing the wheel avoids building from source,
15
- * which previously failed because the source archive has no frontend/web/
16
- * dist for pyproject.toml's force-include. When pip cannot fetch the wheel
17
- * (typically a broken Python TLS trust store on macOS python.org builds),
18
- * the wrapper retries by downloading the wheel with Node's own https stack
19
- * — and, failing that, with curl (system trust store) — then pip-installs
20
- * the local file. If the wheel is missing from the release entirely, the
21
- * wrapper falls back to the pinned source archive as a last resort.
11
+ * (lockstep: npm 1.12.9 installs harness v1.12.9). The preferred source is
12
+ * the official **PyPI** index — pip resolves `iterate-harness==X.Y.Z` against
13
+ * the user's configured mirror (e.g. `pypi.tuna.tsinghua.edu.cn`), so it
14
+ * works even when GitHub is unreachable or its TLS cert fails to verify.
15
+ * If PyPI is unavailable, the wrapper falls back to the pre-built GitHub
16
+ * **wheel** release asset (frontend dist baked in, like iterate-skill-
17
+ * installer ships pre-wrapped assets), pip-installing it directly and, if
18
+ * pip's trust store is broken (common on macOS python.org builds), retrying
19
+ * by downloading the wheel with Node's own https stack — and, failing that,
20
+ * with curl (system trust store) — then pip-installing the local file. If
21
+ * the wheel is missing too, the worker falls back to the pinned source
22
+ * archive as a final resort.
22
23
  * 4. Stamp the installed version; a mismatch (npm upgrade) triggers a
23
24
  * re-install on the next run.
24
25
  * 5. Spawn the venv's `ih` with argv/stdio/signals forwarded and the exit
@@ -60,6 +61,11 @@ const HARNESS_GITHUB_REPO = "jingzhao-l/iterate-harness";
60
61
  const HARNESS_RELEASE_DOWNLOAD_URL = `https://github.com/${HARNESS_GITHUB_REPO}/releases/download`;
61
62
  const HARNESS_REPO_ARCHIVE_URL = `https://github.com/${HARNESS_GITHUB_REPO}/archive/refs/tags`;
62
63
 
64
+ // Preferred install channel: the official PyPI index. pip reads the user's
65
+ // mirror config (e.g. `pypi.tuna.tsinghua.edu.cn`), so it stays fast and
66
+ // stable even when GitHub is unreachable — unlike the GitHub wheel fallback.
67
+ const PYPI_PACKAGE_NAME = "iterate-harness";
68
+
63
69
  // Preferred installable artifact: the pre-built wheel (frontend dist baked in).
64
70
  const WHEEL_ASSET_SUFFIX = "-py3-none-any.whl";
65
71
  // Last-resort artifact: the pinned source archive (pip builds from source).
@@ -91,6 +97,12 @@ function wheelAssetUrl(version) {
91
97
  return `${HARNESS_RELEASE_DOWNLOAD_URL}/v${version}/${wheelAssetName(version)}`;
92
98
  }
93
99
 
100
+ // PyPI package spec pinned to the version (e.g. `iterate-harness==1.12.9`).
101
+ // pip resolves this against the user's configured index/mirror, not GitHub.
102
+ function pypiInstallSpec(version) {
103
+ return `${PYPI_PACKAGE_NAME}==${version}`;
104
+ }
105
+
94
106
  // Last-resort installable source: the pinned source archive (builds on pip).
95
107
  function releaseTarballUrl(version) {
96
108
  return `${HARNESS_REPO_ARCHIVE_URL}/v${version}.tar.gz`;
@@ -274,25 +286,17 @@ function runStep(command, args, options) {
274
286
  }
275
287
  }
276
288
 
277
- // Preferred install URL: the pre-built wheel pinned to this npm version.
278
- // An explicit env override always wins (e.g. a local dev wheel or git ref).
279
- function installUrl(version, env) {
280
- const override = env ? env[INSTALL_URL_ENV_VAR] : undefined;
281
- if (override) {
282
- return override;
283
- }
284
- return wheelAssetUrl(version);
285
- }
286
-
287
- // Last-resort fallback URL: the pinned source archive. Only relevant when the
288
- // user did not override the install URL (an explicit override means they own
289
- // the source and no archive fallback should kick in).
290
- function installFallbackUrl(version, env) {
289
+ // Ordered install candidates, best-effort first:
290
+ // 1. PyPI spec — pip installs via the user's mirror (stable, no GitHub).
291
+ // 2. Pre-built wheel on the GitHub release (needs GitHub, but no source build).
292
+ // 3. Pinned source archive on GitHub (last resort; pip builds from source).
293
+ // An explicit env override short-circuits the chain (the user owns the source).
294
+ function installCandidates(version, env) {
291
295
  const override = env ? env[INSTALL_URL_ENV_VAR] : undefined;
292
296
  if (override) {
293
- return null;
297
+ return [override];
294
298
  }
295
- return releaseTarballUrl(version);
299
+ return [pypiInstallSpec(version), wheelAssetUrl(version), releaseTarballUrl(version)];
296
300
  }
297
301
 
298
302
  // ---------------------------------------------------------------------------
@@ -421,41 +425,55 @@ async function downloadTarballTo(url, dest, deps) {
421
425
 
422
426
  async function installHarness(options) {
423
427
  const python = options.python;
424
- const url = options.url;
425
428
  const homeDir = options.homeDir;
426
429
  const version = options.version;
430
+ const candidates = options.candidates;
427
431
  const runStepFn = options.runStepFn || runStep;
428
432
  const downloader = options.downloader || downloadTarballTo;
429
- // Optional terminal fallback (e.g. the source archive if the wheel is missing).
430
- const fallback = options.fallback;
431
433
 
434
+ if (!candidates || candidates.length === 0) {
435
+ throw new BootstrapError("no install candidate URLs were provided");
436
+ }
437
+
438
+ const failures = [];
439
+ for (const candidate of candidates) {
440
+ try {
441
+ if (isRemoteHttpUrl(candidate)) {
442
+ // HTTP(S) candidate: `pip` first, then node/curl download + local pip
443
+ // (survives broken Python TLS trust stores on macOS python.org builds).
444
+ await installRemoteArtifact(python, candidate, homeDir, version, runStepFn, downloader);
445
+ } else {
446
+ // Non-URL candidate (e.g. `iterate-harness==1.12.9` resolved by pip
447
+ // against the user's mirror): no artifact to download, just pip it.
448
+ runStepFn(python, pipInstallArgs(candidate));
449
+ }
450
+ return;
451
+ } catch (error) {
452
+ failures.push(`${candidate}: ${error.message}`);
453
+ process.stderr.write(
454
+ `[iterate-harness] install from "${candidate}" failed (${error.message}); ` +
455
+ "trying the next candidate ...\n"
456
+ );
457
+ }
458
+ }
459
+
460
+ throw new BootstrapError(`all install candidates failed:\n ${failures.join("\n ")}`);
461
+ }
462
+
463
+ // pip-install a downloadable HTTP artifact, retrying with a node/curl download
464
+ // of the raw file + local pip install when pip's TLS trust store is broken.
465
+ async function installRemoteArtifact(python, url, homeDir, version, runStepFn, downloader) {
432
466
  try {
433
467
  runStepFn(python, pipInstallArgs(url));
434
468
  return;
435
469
  } catch (primaryError) {
436
- if (!isRemoteHttpUrl(url)) {
437
- throw primaryError;
438
- }
439
470
  process.stderr.write(
440
471
  `[iterate-harness] pip install from ${url} failed; retrying via direct download ...\n`
441
472
  );
442
- const cachePath = downloadCachePath(homeDir, version, artifactExtensionFor(url));
443
- try {
444
- await downloader(url, cachePath);
445
- } catch (downloadError) {
446
- if (fallback) {
447
- process.stderr.write(
448
- `[iterate-harness] primary artifact unavailable (${downloadError.message}); ` +
449
- `falling back to source archive ${fallback.url} ...\n`
450
- );
451
- return installHarness(fallback);
452
- }
453
- throw new BootstrapError(
454
- `${primaryError.message}\n\n(direct-download fallback also failed: ${downloadError.message})`
455
- );
456
- }
457
- runStepFn(python, pipInstallArgs(cachePath));
458
473
  }
474
+ const cachePath = downloadCachePath(homeDir, version, artifactExtensionFor(url));
475
+ await downloader(url, cachePath);
476
+ runStepFn(python, pipInstallArgs(cachePath));
459
477
  }
460
478
 
461
479
  async function bootstrap(homeDir, version, env) {
@@ -477,18 +495,13 @@ async function bootstrap(homeDir, version, env) {
477
495
  runStep(interpreter.command, [...interpreter.preArgs, "-m", "venv", venvDir]);
478
496
  }
479
497
 
480
- const url = installUrl(version, env);
481
- const fallbackUrl = installFallbackUrl(version, env);
498
+ const candidates = installCandidates(version, env);
482
499
  ui.step(`Installing iterate-harness ${version}`);
483
500
  await installHarness({
484
501
  python: executable.python,
485
- url,
502
+ candidates,
486
503
  homeDir,
487
504
  version,
488
- fallback:
489
- fallbackUrl != null
490
- ? { python: executable.python, url: fallbackUrl, homeDir, version }
491
- : undefined,
492
505
  });
493
506
 
494
507
  const stampPath = path.join(homeDir, STAMP_FILE_NAME);
@@ -624,15 +637,16 @@ module.exports = {
624
637
  downloadFile,
625
638
  downloadTarballTo,
626
639
  ensureRuntime,
627
- installFallbackUrl,
640
+ installCandidates,
628
641
  installHarness,
629
- installUrl,
642
+ installRemoteArtifact,
630
643
  isRemoteHttpUrl,
631
644
  isSupportedPython,
632
645
  needsBootstrap,
633
646
  packageVersion,
634
647
  parsePythonVersion,
635
648
  pipInstallArgs,
649
+ pypiInstallSpec,
636
650
  pythonCandidates,
637
651
  releaseTarballUrl,
638
652
  reportBootstrapFailure,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-harness",
3
- "version": "1.12.9",
3
+ "version": "1.12.10",
4
4
  "description": "npm distribution wrapper for iterate-harness — installs the Python harness (ih CLI) into a managed venv on install and exposes it as a global command",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",