iterate-harness 1.12.7 → 1.12.9

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
@@ -20,7 +20,9 @@ The harness itself is a **Python package**. This npm package is a thin
20
20
  distribution wrapper: it does not re-implement anything, it just makes `ih`
21
21
  installable with npm and keeps it up to date.
22
22
 
23
- On first run the wrapper:
23
+ Right after `npm install` finishes, a `postinstall` hook creates the managed
24
+ venv and installs the harness — so `ih` is ready to use immediately. The
25
+ delegation logic used at install time and at runtime is the same:
24
26
 
25
27
  1. Finds a Python interpreter >= 3.10 (`py -3` / `python` / `python3`,
26
28
  overridable via `ITERATE_HARNESS_PYTHON`).
@@ -28,15 +30,35 @@ On first run the wrapper:
28
30
  (overridable via `ITERATE_HARNESS_NPM_HOME`).
29
31
  3. `pip install`s the harness **pinned to this npm package's version** — npm
30
32
  `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
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
35
37
  pre-wrapped assets). If the wheel is missing, the wrapper falls back to the
36
38
  pinned **source archive** as a last resort.
37
39
  4. Delegates to the venv's real `ih` executable with argv, stdio, signals and
38
40
  exit codes forwarded.
39
41
 
42
+ If the install during `npm install` can't complete (no Python, no network, or
43
+ `ITERATE_HARNESS_SKIP_INSTALL=1`), the hook prints a notice without failing
44
+ the npm install, and `ih` still installs on its first run — a lazy fallback
45
+ keeps the package usable either way.
46
+
47
+ ## Interactive experience
48
+
49
+ `ih` greets you with a cyan **ITERATE** ASCII banner on every run (claude-code
50
+ style), and the first install runs an **interactive wizard** like
51
+ iterate-skill-installer:
52
+
53
+ - It prints an "Installing" box with the pinned version and runtime location.
54
+ - It asks `Install iterate-harness vX.Y.Z ...? [Y/n]` before downloading —
55
+ decline and it exits cleanly (code 0) instead of failing.
56
+ - When done, it prints a "Done" box with getting-started tips (`ih --help`,
57
+ `ih status`, `ih iterate --help`).
58
+
59
+ All UI output goes to **stderr**, so `ih --version | jq '...'` still gives you
60
+ clean machine-readable stdout.
61
+
40
62
  The React TUI's frontend dependencies (`node_modules`) are installed
41
63
  automatically by the harness itself on first TUI launch — npm users always
42
64
  have Node, so the TUI always works.
package/README.zh-CN.md CHANGED
@@ -19,19 +19,38 @@ ih --version # 首次运行引导安装 Python 端,随后打印版本
19
19
  harness 本身是**一个 Python 包**。这个 npm 包是一个薄分发包装器:它不重新实现任何
20
20
  逻辑,只是让 `ih` 可以通过 npm 安装并保持更新。
21
21
 
22
- 首次运行时,包装器会:
22
+ `npm install` 一结束,`postinstall` 钩子就会创建托管虚拟环境并安装 harness——
23
+ 所以 `ih` 装完即用。安装时与运行时共用同一套代理逻辑:
23
24
 
24
25
  1. 找到一个 >= 3.10 的 Python 解释器(`py -3` / `python` / `python3`,
25
26
  可通过 `ITERATE_HARNESS_PYTHON` 覆盖)。
26
27
  2. 在 `~/.iterate-harness-npm/venv` 创建托管虚拟环境
27
28
  (可通过 `ITERATE_HARNESS_NPM_HOME` 覆盖)。
28
29
  3. `pip install` **与本 npm 包版本锁定的** harness 发布版本 —— npm 的 `1.6.0`
29
- 总会安装 harness `v1.6.0`。升级 npm 包会在下次运行时自动重装对应的 harness 版本。
30
+ 总会安装 harness `v1.6.0`。升级 npm 包会自动重装对应的 harness 版本。
30
31
  优先下载 GitHub 发布里的**预构建 wheel**(`iterate_harness-<version>-py3-none-any.whl`,
31
32
  已内置编译好的前端资源——和 iterate-skill-installer 分发预打包资源一致)。
32
33
  若 wheel 缺失,再兑底使用锁定的**源码归档**。
33
34
  4. 代理调用 venv 里真正的 `ih` 可执行文件,转发 argv、stdio、信号与退出码。
34
35
 
36
+ 若 `npm install` 期间无法完成安装(无 Python、无网络,或
37
+ `ITERATE_HARNESS_SKIP_INSTALL=1`),钩子只会打印提示而不让 npm 安装失败,
38
+ `ih` 会在首次运行时补装——惰性兑底保证两种情况下包都可用。
39
+
40
+ ## 交互式体验
41
+
42
+ 每次运行 `ih`,都会出现青色 **ITERATE** ASCII 横幅(claude-code 风格);首次安装
43
+ 会像 iterate-skill-installer 一样进入**交互式向导**:
44
+
45
+ - 先打印 "Installing" 信息框,包含锁定的版本号与运行时目录。
46
+ - 下载前询问 `Install iterate-harness vX.Y.Z ...? [Y/n]`——选否会以 0 退出码
47
+ 干净退出而不是报错。
48
+ - 装完打印 "Done" 信息框,附上快速上手提示(`ih --help`、`ih status`、
49
+ `ih iterate --help`)。
50
+
51
+ 所有交互输出都写到 **stderr**,所以 `ih --version | jq '...'` 得到的 stdout
52
+ 依旧干净、可机器解析。
53
+
35
54
  React TUI 的前端依赖(`node_modules`)会在首次启动 TUI 时由 harness 自动安装——
36
55
  npm 用户必然有 Node,所以 TUI 始终可用。
37
56
 
package/lib/bootstrap.js CHANGED
@@ -36,6 +36,8 @@ const https = require("https");
36
36
  const os = require("os");
37
37
  const path = require("path");
38
38
 
39
+ const ui = require("./ui");
40
+
39
41
  // ---------------------------------------------------------------------------
40
42
  // Constants (no magic strings below — everything is named here)
41
43
  // ---------------------------------------------------------------------------
@@ -72,6 +74,10 @@ const IS_WINDOWS = process.platform === "win32";
72
74
 
73
75
  class BootstrapError extends Error {}
74
76
 
77
+ // Thrown when the user declines the interactive install wizard. Treated as a
78
+ // graceful "nothing to do" exit (code 0), never as a failure.
79
+ class CancelledError extends Error {}
80
+
75
81
  // ---------------------------------------------------------------------------
76
82
  // Pure helpers (unit-tested in test/bootstrap.test.js)
77
83
  // ---------------------------------------------------------------------------
@@ -467,13 +473,13 @@ async function bootstrap(homeDir, version, env) {
467
473
  fs.rmSync(venvDir, { recursive: true, force: true });
468
474
  }
469
475
  if (!fs.existsSync(activateMarker)) {
470
- process.stderr.write(`[iterate-harness] creating virtualenv at ${venvDir} ...\n`);
476
+ ui.step(`Creating virtualenv at ${venvDir}`);
471
477
  runStep(interpreter.command, [...interpreter.preArgs, "-m", "venv", venvDir]);
472
478
  }
473
479
 
474
480
  const url = installUrl(version, env);
475
481
  const fallbackUrl = installFallbackUrl(version, env);
476
- process.stderr.write(`[iterate-harness] installing iterate-harness ${version} ...\n`);
482
+ ui.step(`Installing iterate-harness ${version}`);
477
483
  await installHarness({
478
484
  python: executable.python,
479
485
  url,
@@ -487,10 +493,12 @@ async function bootstrap(homeDir, version, env) {
487
493
 
488
494
  const stampPath = path.join(homeDir, STAMP_FILE_NAME);
489
495
  fs.writeFileSync(stampPath, `${version}\n`, "utf8");
496
+ ui.success(`iterate-harness ${version} installed`);
490
497
  }
491
498
 
492
- async function ensureRuntime(env) {
499
+ async function ensureRuntime(env, options) {
493
500
  const environment = env || process.env;
501
+ const opts = options || {};
494
502
  const version = packageVersion();
495
503
  const homeDir = runtimeHomeDir(environment);
496
504
  const venvDir = path.join(homeDir, VENV_DIR_NAME);
@@ -503,7 +511,30 @@ async function ensureRuntime(env) {
503
511
  : "";
504
512
 
505
513
  if (!skipInstall && needsBootstrap(stampContent, version)) {
514
+ if (opts.interactive) {
515
+ // Interactive install wizard: ask the user before downloading.
516
+ const confirmed = await ui.askYesNo(
517
+ `Install iterate-harness v${version} into ${homeDir}?`,
518
+ true
519
+ );
520
+ if (!confirmed) {
521
+ throw new CancelledError(
522
+ "Skipped installing iterate-harness. Re-run `ih` whenever you are ready."
523
+ );
524
+ }
525
+ ui.frameSection("Installing", [
526
+ `\x1b[36m◆\x1b[0m Version: v${version}`,
527
+ `\x1b[36m◆\x1b[0m Runtime: ${homeDir}`,
528
+ ]);
529
+ }
506
530
  await bootstrap(homeDir, version, environment);
531
+ if (opts.interactive) {
532
+ ui.frameSection("Done", [
533
+ `\x1b[32m✓\x1b[0m iterate-harness v${version} installed`,
534
+ ` Run \x1b[36mih --help\x1b[0m to see all commands.`,
535
+ ` Try \x1b[36mih status\x1b[0m or \x1b[36mih iterate --help\x1b[0m to get started.`,
536
+ ]);
537
+ }
507
538
  }
508
539
 
509
540
  if (!fs.existsSync(executable.ih)) {
@@ -530,10 +561,19 @@ function reportBootstrapFailure(error) {
530
561
  }
531
562
 
532
563
  async function runHarness(args, env) {
564
+ // Print the banner on every run (claude-code style). Skipped automatically
565
+ // when stderr is not a TTY (e.g. piped output), so `ih --version | jq` stays clean.
566
+ ui.printBanner();
567
+
533
568
  let target;
534
569
  try {
535
- target = await ensureRuntime(env);
570
+ target = await ensureRuntime(env, { interactive: true });
536
571
  } catch (error) {
572
+ if (error instanceof CancelledError) {
573
+ ui.info(error.message);
574
+ process.exit(0);
575
+ return;
576
+ }
537
577
  reportBootstrapFailure(error);
538
578
  process.exit(1);
539
579
  return;
@@ -567,6 +607,7 @@ async function runHarness(args, env) {
567
607
 
568
608
  module.exports = {
569
609
  BootstrapError,
610
+ CancelledError,
570
611
  DEFAULT_ARTIFACT_EXT,
571
612
  MAX_DOWNLOAD_REDIRECTS,
572
613
  MIN_PYTHON_MAJOR,
package/lib/ui.js ADDED
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+
3
+ /*
4
+ * Terminal UI helpers for the iterate-harness npm wrapper.
5
+ *
6
+ * Mirrors the style of iterate-skill-installer's interactive installer:
7
+ * - ASCII banner (ITERATE brand)
8
+ * - Colored step/success/warning/error/info prefixes
9
+ * - Framed section box for summary
10
+ * - Yes/no prompt for interactive choices
11
+ *
12
+ * Every function writes to stderr so piped stdout (e.g. `ih --version | jq`)
13
+ * is never polluted with UI noise. Non-TTY stderr silently skips the banner.
14
+ */
15
+
16
+ const readline = require("node:readline");
17
+
18
+ const ITERATE_BANNER = [
19
+ "██╗████████╗███████╗██████╗ █████╗ ████████╗███████╗",
20
+ "██║╚══██╔══╝██╔════╝██╔══██╗██╔══██╗╚══██╔══╝██╔════╝",
21
+ "██║ ██║ █████╗ ██████╔╝███████║ ██║ █████╗ ",
22
+ "██║ ██║ ██╔══╝ ██╔══██╗██╔══██║ ██║ ██╔══╝ ",
23
+ "██║ ██║ ███████╗██║ ██║██║ ██║ ██║ ███████╗",
24
+ "╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝",
25
+ ];
26
+
27
+ function printBanner() {
28
+ if (!process.stderr.isTTY) return;
29
+ console.error();
30
+ for (const line of ITERATE_BANNER) {
31
+ console.error(`\x1b[36m${line}\x1b[0m`);
32
+ }
33
+ console.error(`\x1b[36m iterate-harness\x1b[0m\x1b[2m · jingzhao-l/iterate-harness\x1b[0m`);
34
+ console.error();
35
+ }
36
+
37
+ function info(message) {
38
+ console.error(`\x1b[34mℹ\x1b[0m ${message}`);
39
+ }
40
+
41
+ function success(message) {
42
+ console.error(`\x1b[32m✓\x1b[0m ${message}`);
43
+ }
44
+
45
+ function warning(message) {
46
+ console.error(`\x1b[33m⚠\x1b[0m ${message}`);
47
+ }
48
+
49
+ function error(message) {
50
+ console.error(`\x1b[31m✗\x1b[0m ${message}`);
51
+ }
52
+
53
+ function step(message) {
54
+ console.error(`\x1b[36m◆\x1b[0m ${message}`);
55
+ }
56
+
57
+ function hint(message) {
58
+ console.error(`\x1b[2m ${message}\x1b[0m`);
59
+ }
60
+
61
+ function stripAnsi(str) {
62
+ // eslint-disable-next-line no-control-regex
63
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
64
+ }
65
+
66
+ function frameSection(title, lines) {
67
+ const maxLen = Math.max(
68
+ title.length,
69
+ ...lines.map((l) => stripAnsi(l).length),
70
+ );
71
+ const innerWidth = maxLen + 2;
72
+ const top = `┌─ ${title} ${"─".repeat(Math.max(0, innerWidth - title.length - 2))}┐`;
73
+ const bottom = `└${"─".repeat(innerWidth + 1)}┘`;
74
+ console.error(top);
75
+ for (const line of lines) {
76
+ const visibleLen = stripAnsi(line).length;
77
+ const padding = " ".repeat(Math.max(0, innerWidth - visibleLen));
78
+ console.error(`│ ${line}${padding}│`);
79
+ }
80
+ console.error(bottom);
81
+ }
82
+
83
+ function askYesNo(question, defaultNo = false) {
84
+ return new Promise((resolve) => {
85
+ const rl = readline.createInterface({
86
+ input: process.stdin,
87
+ output: process.stdout,
88
+ });
89
+ const hint = defaultNo ? "[y/N]" : "[Y/n]";
90
+ rl.question(`\x1b[36m◆\x1b[0m ${question} ${hint} `, (answer) => {
91
+ rl.close();
92
+ const a = answer.trim().toLowerCase();
93
+ if (a === "y" || a === "yes") resolve(true);
94
+ else if (a === "n" || a === "no") resolve(false);
95
+ else resolve(defaultNo);
96
+ });
97
+ });
98
+ }
99
+
100
+ module.exports = {
101
+ ITERATE_BANNER,
102
+ printBanner,
103
+ info,
104
+ success,
105
+ warning,
106
+ error,
107
+ step,
108
+ hint,
109
+ stripAnsi,
110
+ frameSection,
111
+ askYesNo,
112
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "iterate-harness",
3
- "version": "1.12.7",
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",
3
+ "version": "1.12.9",
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",
7
7
  "repository": {
@@ -22,6 +22,7 @@
22
22
  "files": [
23
23
  "bin",
24
24
  "lib",
25
+ "scripts",
25
26
  "README.md",
26
27
  "LICENSE"
27
28
  ],
@@ -33,6 +34,7 @@
33
34
  "installer"
34
35
  ],
35
36
  "scripts": {
36
- "test": "node --test test/bootstrap.test.js"
37
+ "test": "node --test test/bootstrap.test.js test/postinstall.test.js",
38
+ "postinstall": "node scripts/postinstall.js"
37
39
  }
38
40
  }
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * npm `postinstall` hook for the iterate-harness wrapper.
4
+ *
5
+ * Runs right after `npm install`/`npm install -g` finishes, so the Python
6
+ * harness is ready the moment npm finishes — no wait on the first `ih` run.
7
+ *
8
+ * It deliberately NEVER fails the npm install: if the managed venv cannot be
9
+ * created here (e.g. no Python interpreter, no network, user opted out with
10
+ * ITERATE_HARNESS_SKIP_INSTALL=1), we print a notice and exit 0. The regular
11
+ * `ih` entry point keeps its own lazy-bootstrap fallback, so the first run
12
+ * still installs the harness on demand.
13
+ */
14
+
15
+ "use strict";
16
+
17
+ const { ensureRuntime, reportBootstrapFailure, SKIP_INSTALL_ENV_VAR } = require("../lib/bootstrap");
18
+ const ui = require("../lib/ui");
19
+
20
+ async function main() {
21
+ if (process.env[SKIP_INSTALL_ENV_VAR] === "1") {
22
+ ui.warning(`Skipped install during npm install (${SKIP_INSTALL_ENV_VAR}=1); harness installs on the first \`ih\` run.`);
23
+ return;
24
+ }
25
+ try {
26
+ await ensureRuntime();
27
+ ui.success("iterate-harness installed during npm install — `ih` is ready to use.");
28
+ } catch (error) {
29
+ reportBootstrapFailure(error);
30
+ ui.warning(
31
+ "Could not install the harness during npm install (see above). " +
32
+ "It will be retried automatically on the first `ih` run."
33
+ );
34
+ }
35
+ }
36
+
37
+ main().catch((error) => {
38
+ reportBootstrapFailure(error);
39
+ process.exit(0);
40
+ });