iterate-harness 1.12.8 → 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 +15 -0
- package/README.zh-CN.md +14 -0
- package/lib/bootstrap.js +45 -4
- package/lib/ui.js +112 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +6 -8
package/README.md
CHANGED
|
@@ -44,6 +44,21 @@ If the install during `npm install` can't complete (no Python, no network, or
|
|
|
44
44
|
the npm install, and `ih` still installs on its first run — a lazy fallback
|
|
45
45
|
keeps the package usable either way.
|
|
46
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
|
+
|
|
47
62
|
The React TUI's frontend dependencies (`node_modules`) are installed
|
|
48
63
|
automatically by the harness itself on first TUI launch — npm users always
|
|
49
64
|
have Node, so the TUI always works.
|
package/README.zh-CN.md
CHANGED
|
@@ -37,6 +37,20 @@ harness 本身是**一个 Python 包**。这个 npm 包是一个薄分发包装
|
|
|
37
37
|
`ITERATE_HARNESS_SKIP_INSTALL=1`),钩子只会打印提示而不让 npm 安装失败,
|
|
38
38
|
`ih` 会在首次运行时补装——惰性兑底保证两种情况下包都可用。
|
|
39
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
|
+
|
|
40
54
|
React TUI 的前端依赖(`node_modules`)会在首次启动 TUI 时由 harness 自动安装——
|
|
41
55
|
npm 用户必然有 Node,所以 TUI 始终可用。
|
|
42
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
|
-
|
|
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
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-harness",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.9",
|
|
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",
|
package/scripts/postinstall.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
27
|
+
ui.success("iterate-harness installed during npm install — `ih` is ready to use.");
|
|
30
28
|
} catch (error) {
|
|
31
29
|
reportBootstrapFailure(error);
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
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
|
}
|