iterate-harness 1.12.5 → 1.12.7

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
@@ -26,10 +26,14 @@ On first run the wrapper:
26
26
  overridable via `ITERATE_HARNESS_PYTHON`).
27
27
  2. Creates a managed virtualenv at `~/.iterate-harness-npm/venv`
28
28
  (overridable via `ITERATE_HARNESS_NPM_HOME`).
29
- 3. `pip install`s the harness **release tarball pinned to this npm package's
30
- version** — npm `1.6.0` always installs harness `v1.6.0`. Upgrading the npm
31
- package automatically re-installs the matching harness version on the next
32
- run.
29
+ 3. `pip install`s the harness **pinned to this npm package's version** — npm
30
+ `1.6.0` always installs harness `v1.6.0`. Upgrading the npm package
31
+ automatically re-installs the matching harness version on the next run.
32
+ The preferred artifact is the **pre-built wheel** uploaded to the GitHub
33
+ release (`iterate_harness-<version>-py3-none-any.whl`, which already bundles
34
+ the compiled frontend assets — exactly like iterate-skill-installer ships
35
+ pre-wrapped assets). If the wheel is missing, the wrapper falls back to the
36
+ pinned **source archive** as a last resort.
33
37
  4. Delegates to the venv's real `ih` executable with argv, stdio, signals and
34
38
  exit codes forwarded.
35
39
 
@@ -50,7 +54,7 @@ have Node, so the TUI always works.
50
54
  | --- | --- |
51
55
  | `ITERATE_HARNESS_PYTHON` | Use this interpreter instead of auto-detection |
52
56
  | `ITERATE_HARNESS_NPM_HOME` | Runtime dir (default `~/.iterate-harness-npm`) |
53
- | `ITERATE_HARNESS_INSTALL_URL` | Install from this pip URL instead of the pinned release tarball (e.g. a git branch for testing) |
57
+ | `ITERATE_HARNESS_INSTALL_URL` | Install from this pip URL instead of the pinned release wheel (e.g. a git branch for testing) |
54
58
  | `ITERATE_HARNESS_SKIP_INSTALL=1` | Skip bootstrap entirely; just run the already-installed `ih` |
55
59
 
56
60
  ## npx (no global install)
package/README.zh-CN.md CHANGED
@@ -25,8 +25,11 @@ harness 本身是**一个 Python 包**。这个 npm 包是一个薄分发包装
25
25
  可通过 `ITERATE_HARNESS_PYTHON` 覆盖)。
26
26
  2. 在 `~/.iterate-harness-npm/venv` 创建托管虚拟环境
27
27
  (可通过 `ITERATE_HARNESS_NPM_HOME` 覆盖)。
28
- 3. `pip install` **与本 npm 包版本锁定的** harness 发布 tarball —— npm 的 `1.6.0`
28
+ 3. `pip install` **与本 npm 包版本锁定的** harness 发布版本 —— npm 的 `1.6.0`
29
29
  总会安装 harness `v1.6.0`。升级 npm 包会在下次运行时自动重装对应的 harness 版本。
30
+ 优先下载 GitHub 发布里的**预构建 wheel**(`iterate_harness-<version>-py3-none-any.whl`,
31
+ 已内置编译好的前端资源——和 iterate-skill-installer 分发预打包资源一致)。
32
+ 若 wheel 缺失,再兑底使用锁定的**源码归档**。
30
33
  4. 代理调用 venv 里真正的 `ih` 可执行文件,转发 argv、stdio、信号与退出码。
31
34
 
32
35
  React TUI 的前端依赖(`node_modules`)会在首次启动 TUI 时由 harness 自动安装——
@@ -44,7 +47,7 @@ npm 用户必然有 Node,所以 TUI 始终可用。
44
47
  | --- | --- |
45
48
  | `ITERATE_HARNESS_PYTHON` | 使用该解释器,而非自动检测 |
46
49
  | `ITERATE_HARNESS_NPM_HOME` | 运行时目录(默认 `~/.iterate-harness-npm`) |
47
- | `ITERATE_HARNESS_INSTALL_URL` | 从这个 pip URL 安装,而非锁定的发布 tarball(例如测试用的 git 分支) |
50
+ | `ITERATE_HARNESS_INSTALL_URL` | 从这个 pip URL 安装,而非锁定的发布 wheel(例如测试用的 git 分支) |
48
51
  | `ITERATE_HARNESS_SKIP_INSTALL=1` | 完全跳过引导安装,直接运行已装好的 `ih` |
49
52
 
50
53
  ## npx(免全局安装)
package/lib/bootstrap.js CHANGED
@@ -7,12 +7,18 @@
7
7
  *
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
- * 3. pip-install the harness release tarball pinned to THIS npm package's
11
- * version (lockstep: npm 1.6.0 installs harness v1.6.0). When pip cannot
12
- * fetch the tarball (typically a broken Python TLS trust store on macOS
13
- * python.org builds), the wrapper retries by downloading the tarball with
14
- * Node's own https stack — and, failing that, with curl (system trust
15
- * store) — then pip-installs the local file.
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.
16
22
  * 4. Stamp the installed version; a mismatch (npm upgrade) triggers a
17
23
  * re-install on the next run.
18
24
  * 5. Spawn the venv's `ih` with argv/stdio/signals forwarded and the exit
@@ -48,7 +54,15 @@ const HOME_ENV_VAR = "ITERATE_HARNESS_NPM_HOME";
48
54
  const INSTALL_URL_ENV_VAR = "ITERATE_HARNESS_INSTALL_URL";
49
55
  const SKIP_INSTALL_ENV_VAR = "ITERATE_HARNESS_SKIP_INSTALL";
50
56
 
51
- const HARNESS_REPO_ARCHIVE_URL = "https://github.com/jingzhao-l/iterate-harness/archive/refs/tags";
57
+ const HARNESS_GITHUB_REPO = "jingzhao-l/iterate-harness";
58
+ const HARNESS_RELEASE_DOWNLOAD_URL = `https://github.com/${HARNESS_GITHUB_REPO}/releases/download`;
59
+ const HARNESS_REPO_ARCHIVE_URL = `https://github.com/${HARNESS_GITHUB_REPO}/archive/refs/tags`;
60
+
61
+ // Preferred installable artifact: the pre-built wheel (frontend dist baked in).
62
+ const WHEEL_ASSET_SUFFIX = "-py3-none-any.whl";
63
+ // Last-resort artifact: the pinned source archive (pip builds from source).
64
+ const DEFAULT_ARTIFACT_EXT = ".tar.gz";
65
+ const WHEEL_ARTIFACT_EXT = ".whl";
52
66
 
53
67
  const MIN_PYTHON_MAJOR = 3;
54
68
  const MIN_PYTHON_MINOR = 10;
@@ -62,6 +76,16 @@ class BootstrapError extends Error {}
62
76
  // Pure helpers (unit-tested in test/bootstrap.test.js)
63
77
  // ---------------------------------------------------------------------------
64
78
 
79
+ function wheelAssetName(version) {
80
+ return `iterate_harness-${version}${WHEEL_ASSET_SUFFIX}`;
81
+ }
82
+
83
+ // Preferred installable source: the pre-built wheel (frontend dist baked in).
84
+ function wheelAssetUrl(version) {
85
+ return `${HARNESS_RELEASE_DOWNLOAD_URL}/v${version}/${wheelAssetName(version)}`;
86
+ }
87
+
88
+ // Last-resort installable source: the pinned source archive (builds on pip).
65
89
  function releaseTarballUrl(version) {
66
90
  return `${HARNESS_REPO_ARCHIVE_URL}/v${version}.tar.gz`;
67
91
  }
@@ -125,8 +149,27 @@ function isRemoteHttpUrl(target) {
125
149
  return /^https?:\/\//i.test(String(target || ""));
126
150
  }
127
151
 
128
- function downloadCachePath(homeDir, version) {
129
- return path.join(homeDir, TARBALL_CACHE_DIR_NAME, `iterate-harness-${version}.tar.gz`);
152
+ // Infer the on-disk extension (for the cache file) from a download URL so
153
+ // pip-installing the local copy keeps a correct .whl / .tar.gz suffix.
154
+ function artifactExtensionFor(url) {
155
+ const match = /\.(tar\.gz|whl)$/i.exec(String(url || ""));
156
+ if (!match) {
157
+ return DEFAULT_ARTIFACT_EXT;
158
+ }
159
+ return match[1].toLowerCase() === "whl" ? WHEEL_ARTIFACT_EXT : DEFAULT_ARTIFACT_EXT;
160
+ }
161
+
162
+ function downloadCachePath(homeDir, version, extension) {
163
+ const ext = extension || DEFAULT_ARTIFACT_EXT;
164
+ if (ext === WHEEL_ARTIFACT_EXT) {
165
+ // A locally-cached wheel must keep a valid PEP 427 filename (e.g.
166
+ // `iterate_harness-1.12.6-py3-none-any.whl`). pip refuses any `*.whl`
167
+ // whose name lacks the `{python}-{abi}-{platform}` tags, so the fallback
168
+ // cache file reuses the real release asset name rather than a bare
169
+ // `iterate-harness-{version}.whl`.
170
+ return path.join(homeDir, TARBALL_CACHE_DIR_NAME, wheelAssetName(version));
171
+ }
172
+ return path.join(homeDir, TARBALL_CACHE_DIR_NAME, `iterate-harness-${version}${ext}`);
130
173
  }
131
174
 
132
175
  function pipInstallArgs(target) {
@@ -225,16 +268,29 @@ function runStep(command, args, options) {
225
268
  }
226
269
  }
227
270
 
271
+ // Preferred install URL: the pre-built wheel pinned to this npm version.
272
+ // An explicit env override always wins (e.g. a local dev wheel or git ref).
228
273
  function installUrl(version, env) {
229
274
  const override = env ? env[INSTALL_URL_ENV_VAR] : undefined;
230
275
  if (override) {
231
276
  return override;
232
277
  }
278
+ return wheelAssetUrl(version);
279
+ }
280
+
281
+ // Last-resort fallback URL: the pinned source archive. Only relevant when the
282
+ // user did not override the install URL (an explicit override means they own
283
+ // the source and no archive fallback should kick in).
284
+ function installFallbackUrl(version, env) {
285
+ const override = env ? env[INSTALL_URL_ENV_VAR] : undefined;
286
+ if (override) {
287
+ return null;
288
+ }
233
289
  return releaseTarballUrl(version);
234
290
  }
235
291
 
236
292
  // ---------------------------------------------------------------------------
237
- // Tarball download fallback (Node-side TLS — survives broken pip trust stores)
293
+ // Artifact download fallback (Node-side TLS — survives broken pip trust stores)
238
294
  // ---------------------------------------------------------------------------
239
295
 
240
296
  function downloadFile(url, dest, redirectBudget) {
@@ -353,14 +409,19 @@ async function downloadTarballTo(url, dest, deps) {
353
409
  }
354
410
 
355
411
  // ---------------------------------------------------------------------------
356
- // Install chain: pip from URL first, Node-download + local pip as fallback
412
+ // Install chain: pip from URL (pre-built wheel) first, Node-download + local
413
+ // pip as TLS fallback; missing wheel falls back to the source archive.
357
414
  // ---------------------------------------------------------------------------
358
415
 
359
416
  async function installHarness(options) {
360
417
  const python = options.python;
361
418
  const url = options.url;
419
+ const homeDir = options.homeDir;
420
+ const version = options.version;
362
421
  const runStepFn = options.runStepFn || runStep;
363
422
  const downloader = options.downloader || downloadTarballTo;
423
+ // Optional terminal fallback (e.g. the source archive if the wheel is missing).
424
+ const fallback = options.fallback;
364
425
 
365
426
  try {
366
427
  runStepFn(python, pipInstallArgs(url));
@@ -372,10 +433,17 @@ async function installHarness(options) {
372
433
  process.stderr.write(
373
434
  `[iterate-harness] pip install from ${url} failed; retrying via direct download ...\n`
374
435
  );
375
- const cachePath = downloadCachePath(options.homeDir, options.version);
436
+ const cachePath = downloadCachePath(homeDir, version, artifactExtensionFor(url));
376
437
  try {
377
438
  await downloader(url, cachePath);
378
439
  } catch (downloadError) {
440
+ if (fallback) {
441
+ process.stderr.write(
442
+ `[iterate-harness] primary artifact unavailable (${downloadError.message}); ` +
443
+ `falling back to source archive ${fallback.url} ...\n`
444
+ );
445
+ return installHarness(fallback);
446
+ }
379
447
  throw new BootstrapError(
380
448
  `${primaryError.message}\n\n(direct-download fallback also failed: ${downloadError.message})`
381
449
  );
@@ -404,8 +472,18 @@ async function bootstrap(homeDir, version, env) {
404
472
  }
405
473
 
406
474
  const url = installUrl(version, env);
407
- process.stderr.write(`[iterate-harness] pip installing iterate-harness ${version} ...\n`);
408
- await installHarness({ python: executable.python, url, homeDir, version });
475
+ const fallbackUrl = installFallbackUrl(version, env);
476
+ process.stderr.write(`[iterate-harness] installing iterate-harness ${version} ...\n`);
477
+ await installHarness({
478
+ python: executable.python,
479
+ url,
480
+ homeDir,
481
+ version,
482
+ fallback:
483
+ fallbackUrl != null
484
+ ? { python: executable.python, url: fallbackUrl, homeDir, version }
485
+ : undefined,
486
+ });
409
487
 
410
488
  const stampPath = path.join(homeDir, STAMP_FILE_NAME);
411
489
  fs.writeFileSync(stampPath, `${version}\n`, "utf8");
@@ -489,6 +567,7 @@ async function runHarness(args, env) {
489
567
 
490
568
  module.exports = {
491
569
  BootstrapError,
570
+ DEFAULT_ARTIFACT_EXT,
492
571
  MAX_DOWNLOAD_REDIRECTS,
493
572
  MIN_PYTHON_MAJOR,
494
573
  MIN_PYTHON_MINOR,
@@ -496,12 +575,15 @@ module.exports = {
496
575
  HOME_ENV_VAR,
497
576
  INSTALL_URL_ENV_VAR,
498
577
  SKIP_INSTALL_ENV_VAR,
578
+ WHEEL_ASSET_SUFFIX,
579
+ artifactExtensionFor,
499
580
  detectPython,
500
581
  curlDownload,
501
582
  downloadCachePath,
502
583
  downloadFile,
503
584
  downloadTarballTo,
504
585
  ensureRuntime,
586
+ installFallbackUrl,
505
587
  installHarness,
506
588
  installUrl,
507
589
  isRemoteHttpUrl,
@@ -515,4 +597,6 @@ module.exports = {
515
597
  reportBootstrapFailure,
516
598
  runHarness,
517
599
  venvExecutablePaths,
600
+ wheelAssetName,
601
+ wheelAssetUrl,
518
602
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-harness",
3
- "version": "1.12.5",
3
+ "version": "1.12.7",
4
4
  "description": "npm distribution wrapper for iterate-harness — installs the Python harness (ih CLI) into a managed venv on first run and exposes it as a global command",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",