iterate-harness 1.12.8 → 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
 
@@ -44,6 +46,21 @@ If the install during `npm install` can't complete (no Python, no network, or
44
46
  the npm install, and `ih` still installs on its first run — a lazy fallback
45
47
  keeps the package usable either way.
46
48
 
49
+ ## Interactive experience
50
+
51
+ `ih` greets you with a cyan **ITERATE** ASCII banner on every run (claude-code
52
+ style), and the first install runs an **interactive wizard** like
53
+ iterate-skill-installer:
54
+
55
+ - It prints an "Installing" box with the pinned version and runtime location.
56
+ - It asks `Install iterate-harness vX.Y.Z ...? [Y/n]` before downloading —
57
+ decline and it exits cleanly (code 0) instead of failing.
58
+ - When done, it prints a "Done" box with getting-started tips (`ih --help`,
59
+ `ih status`, `ih iterate --help`).
60
+
61
+ All UI output goes to **stderr**, so `ih --version | jq '...'` still gives you
62
+ clean machine-readable stdout.
63
+
47
64
  The React TUI's frontend dependencies (`node_modules`) are installed
48
65
  automatically by the harness itself on first TUI launch — npm users always
49
66
  have Node, so the TUI always works.
package/README.zh-CN.md CHANGED
@@ -27,16 +27,31 @@ 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、无网络,或
37
38
  `ITERATE_HARNESS_SKIP_INSTALL=1`),钩子只会打印提示而不让 npm 安装失败,
38
39
  `ih` 会在首次运行时补装——惰性兑底保证两种情况下包都可用。
39
40
 
41
+ ## 交互式体验
42
+
43
+ 每次运行 `ih`,都会出现青色 **ITERATE** ASCII 横幅(claude-code 风格);首次安装
44
+ 会像 iterate-skill-installer 一样进入**交互式向导**:
45
+
46
+ - 先打印 "Installing" 信息框,包含锁定的版本号与运行时目录。
47
+ - 下载前询问 `Install iterate-harness vX.Y.Z ...? [Y/n]`——选否会以 0 退出码
48
+ 干净退出而不是报错。
49
+ - 装完打印 "Done" 信息框,附上快速上手提示(`ih --help`、`ih status`、
50
+ `ih iterate --help`)。
51
+
52
+ 所有交互输出都写到 **stderr**,所以 `ih --version | jq '...'` 得到的 stdout
53
+ 依旧干净、可机器解析。
54
+
40
55
  React TUI 的前端依赖(`node_modules`)会在首次启动 TUI 时由 harness 自动安装——
41
56
  npm 用户必然有 Node,所以 TUI 始终可用。
42
57
 
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
@@ -36,6 +37,8 @@ const https = require("https");
36
37
  const os = require("os");
37
38
  const path = require("path");
38
39
 
40
+ const ui = require("./ui");
41
+
39
42
  // ---------------------------------------------------------------------------
40
43
  // Constants (no magic strings below — everything is named here)
41
44
  // ---------------------------------------------------------------------------
@@ -58,6 +61,11 @@ const HARNESS_GITHUB_REPO = "jingzhao-l/iterate-harness";
58
61
  const HARNESS_RELEASE_DOWNLOAD_URL = `https://github.com/${HARNESS_GITHUB_REPO}/releases/download`;
59
62
  const HARNESS_REPO_ARCHIVE_URL = `https://github.com/${HARNESS_GITHUB_REPO}/archive/refs/tags`;
60
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
+
61
69
  // Preferred installable artifact: the pre-built wheel (frontend dist baked in).
62
70
  const WHEEL_ASSET_SUFFIX = "-py3-none-any.whl";
63
71
  // Last-resort artifact: the pinned source archive (pip builds from source).
@@ -72,6 +80,10 @@ const IS_WINDOWS = process.platform === "win32";
72
80
 
73
81
  class BootstrapError extends Error {}
74
82
 
83
+ // Thrown when the user declines the interactive install wizard. Treated as a
84
+ // graceful "nothing to do" exit (code 0), never as a failure.
85
+ class CancelledError extends Error {}
86
+
75
87
  // ---------------------------------------------------------------------------
76
88
  // Pure helpers (unit-tested in test/bootstrap.test.js)
77
89
  // ---------------------------------------------------------------------------
@@ -85,6 +97,12 @@ function wheelAssetUrl(version) {
85
97
  return `${HARNESS_RELEASE_DOWNLOAD_URL}/v${version}/${wheelAssetName(version)}`;
86
98
  }
87
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
+
88
106
  // Last-resort installable source: the pinned source archive (builds on pip).
89
107
  function releaseTarballUrl(version) {
90
108
  return `${HARNESS_REPO_ARCHIVE_URL}/v${version}.tar.gz`;
@@ -268,25 +286,17 @@ function runStep(command, args, options) {
268
286
  }
269
287
  }
270
288
 
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).
273
- function installUrl(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) {
274
295
  const override = env ? env[INSTALL_URL_ENV_VAR] : undefined;
275
296
  if (override) {
276
- return override;
297
+ return [override];
277
298
  }
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
- }
289
- return releaseTarballUrl(version);
299
+ return [pypiInstallSpec(version), wheelAssetUrl(version), releaseTarballUrl(version)];
290
300
  }
291
301
 
292
302
  // ---------------------------------------------------------------------------
@@ -415,41 +425,55 @@ async function downloadTarballTo(url, dest, deps) {
415
425
 
416
426
  async function installHarness(options) {
417
427
  const python = options.python;
418
- const url = options.url;
419
428
  const homeDir = options.homeDir;
420
429
  const version = options.version;
430
+ const candidates = options.candidates;
421
431
  const runStepFn = options.runStepFn || runStep;
422
432
  const downloader = options.downloader || downloadTarballTo;
423
- // Optional terminal fallback (e.g. the source archive if the wheel is missing).
424
- const fallback = options.fallback;
425
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) {
426
466
  try {
427
467
  runStepFn(python, pipInstallArgs(url));
428
468
  return;
429
469
  } catch (primaryError) {
430
- if (!isRemoteHttpUrl(url)) {
431
- throw primaryError;
432
- }
433
470
  process.stderr.write(
434
471
  `[iterate-harness] pip install from ${url} failed; retrying via direct download ...\n`
435
472
  );
436
- const cachePath = downloadCachePath(homeDir, version, artifactExtensionFor(url));
437
- try {
438
- await downloader(url, cachePath);
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
- }
447
- throw new BootstrapError(
448
- `${primaryError.message}\n\n(direct-download fallback also failed: ${downloadError.message})`
449
- );
450
- }
451
- runStepFn(python, pipInstallArgs(cachePath));
452
473
  }
474
+ const cachePath = downloadCachePath(homeDir, version, artifactExtensionFor(url));
475
+ await downloader(url, cachePath);
476
+ runStepFn(python, pipInstallArgs(cachePath));
453
477
  }
454
478
 
455
479
  async function bootstrap(homeDir, version, env) {
@@ -467,30 +491,27 @@ async function bootstrap(homeDir, version, env) {
467
491
  fs.rmSync(venvDir, { recursive: true, force: true });
468
492
  }
469
493
  if (!fs.existsSync(activateMarker)) {
470
- process.stderr.write(`[iterate-harness] creating virtualenv at ${venvDir} ...\n`);
494
+ ui.step(`Creating virtualenv at ${venvDir}`);
471
495
  runStep(interpreter.command, [...interpreter.preArgs, "-m", "venv", venvDir]);
472
496
  }
473
497
 
474
- const url = installUrl(version, env);
475
- const fallbackUrl = installFallbackUrl(version, env);
476
- process.stderr.write(`[iterate-harness] installing iterate-harness ${version} ...\n`);
498
+ const candidates = installCandidates(version, env);
499
+ ui.step(`Installing iterate-harness ${version}`);
477
500
  await installHarness({
478
501
  python: executable.python,
479
- url,
502
+ candidates,
480
503
  homeDir,
481
504
  version,
482
- fallback:
483
- fallbackUrl != null
484
- ? { python: executable.python, url: fallbackUrl, homeDir, version }
485
- : undefined,
486
505
  });
487
506
 
488
507
  const stampPath = path.join(homeDir, STAMP_FILE_NAME);
489
508
  fs.writeFileSync(stampPath, `${version}\n`, "utf8");
509
+ ui.success(`iterate-harness ${version} installed`);
490
510
  }
491
511
 
492
- async function ensureRuntime(env) {
512
+ async function ensureRuntime(env, options) {
493
513
  const environment = env || process.env;
514
+ const opts = options || {};
494
515
  const version = packageVersion();
495
516
  const homeDir = runtimeHomeDir(environment);
496
517
  const venvDir = path.join(homeDir, VENV_DIR_NAME);
@@ -503,7 +524,30 @@ async function ensureRuntime(env) {
503
524
  : "";
504
525
 
505
526
  if (!skipInstall && needsBootstrap(stampContent, version)) {
527
+ if (opts.interactive) {
528
+ // Interactive install wizard: ask the user before downloading.
529
+ const confirmed = await ui.askYesNo(
530
+ `Install iterate-harness v${version} into ${homeDir}?`,
531
+ true
532
+ );
533
+ if (!confirmed) {
534
+ throw new CancelledError(
535
+ "Skipped installing iterate-harness. Re-run `ih` whenever you are ready."
536
+ );
537
+ }
538
+ ui.frameSection("Installing", [
539
+ `\x1b[36m◆\x1b[0m Version: v${version}`,
540
+ `\x1b[36m◆\x1b[0m Runtime: ${homeDir}`,
541
+ ]);
542
+ }
506
543
  await bootstrap(homeDir, version, environment);
544
+ if (opts.interactive) {
545
+ ui.frameSection("Done", [
546
+ `\x1b[32m✓\x1b[0m iterate-harness v${version} installed`,
547
+ ` Run \x1b[36mih --help\x1b[0m to see all commands.`,
548
+ ` Try \x1b[36mih status\x1b[0m or \x1b[36mih iterate --help\x1b[0m to get started.`,
549
+ ]);
550
+ }
507
551
  }
508
552
 
509
553
  if (!fs.existsSync(executable.ih)) {
@@ -530,10 +574,19 @@ function reportBootstrapFailure(error) {
530
574
  }
531
575
 
532
576
  async function runHarness(args, env) {
577
+ // Print the banner on every run (claude-code style). Skipped automatically
578
+ // when stderr is not a TTY (e.g. piped output), so `ih --version | jq` stays clean.
579
+ ui.printBanner();
580
+
533
581
  let target;
534
582
  try {
535
- target = await ensureRuntime(env);
583
+ target = await ensureRuntime(env, { interactive: true });
536
584
  } catch (error) {
585
+ if (error instanceof CancelledError) {
586
+ ui.info(error.message);
587
+ process.exit(0);
588
+ return;
589
+ }
537
590
  reportBootstrapFailure(error);
538
591
  process.exit(1);
539
592
  return;
@@ -567,6 +620,7 @@ async function runHarness(args, env) {
567
620
 
568
621
  module.exports = {
569
622
  BootstrapError,
623
+ CancelledError,
570
624
  DEFAULT_ARTIFACT_EXT,
571
625
  MAX_DOWNLOAD_REDIRECTS,
572
626
  MIN_PYTHON_MAJOR,
@@ -583,15 +637,16 @@ module.exports = {
583
637
  downloadFile,
584
638
  downloadTarballTo,
585
639
  ensureRuntime,
586
- installFallbackUrl,
640
+ installCandidates,
587
641
  installHarness,
588
- installUrl,
642
+ installRemoteArtifact,
589
643
  isRemoteHttpUrl,
590
644
  isSupportedPython,
591
645
  needsBootstrap,
592
646
  packageVersion,
593
647
  parsePythonVersion,
594
648
  pipInstallArgs,
649
+ pypiInstallSpec,
595
650
  pythonCandidates,
596
651
  releaseTarballUrl,
597
652
  reportBootstrapFailure,
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,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-harness",
3
- "version": "1.12.8",
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",
@@ -15,23 +15,21 @@
15
15
  "use strict";
16
16
 
17
17
  const { ensureRuntime, reportBootstrapFailure, SKIP_INSTALL_ENV_VAR } = require("../lib/bootstrap");
18
+ const ui = require("../lib/ui");
18
19
 
19
20
  async function main() {
20
21
  if (process.env[SKIP_INSTALL_ENV_VAR] === "1") {
21
- process.stderr.write(
22
- `[iterate-harness] skipped install during npm install (${SKIP_INSTALL_ENV_VAR}=1); ` +
23
- "the harness will be installed on the first `ih` run.\n"
24
- );
22
+ ui.warning(`Skipped install during npm install (${SKIP_INSTALL_ENV_VAR}=1); harness installs on the first \`ih\` run.`);
25
23
  return;
26
24
  }
27
25
  try {
28
26
  await ensureRuntime();
29
- process.stderr.write("[iterate-harness] harness installed during npm install.\n");
27
+ ui.success("iterate-harness installed during npm install — `ih` is ready to use.");
30
28
  } catch (error) {
31
29
  reportBootstrapFailure(error);
32
- process.stderr.write(
33
- "[iterate-harness] could not install the harness during npm install " +
34
- "(see above). It will be retried automatically on the first `ih` run.\n"
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."
35
33
  );
36
34
  }
37
35
  }