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 +23 -6
- package/README.zh-CN.md +19 -4
- package/lib/bootstrap.js +117 -62
- package/lib/ui.js +112 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +6 -8
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
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.
|
|
12
|
-
* the
|
|
13
|
-
* the
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
//
|
|
272
|
-
//
|
|
273
|
-
|
|
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
|
-
|
|
494
|
+
ui.step(`Creating virtualenv at ${venvDir}`);
|
|
471
495
|
runStep(interpreter.command, [...interpreter.preArgs, "-m", "venv", venvDir]);
|
|
472
496
|
}
|
|
473
497
|
|
|
474
|
-
const
|
|
475
|
-
|
|
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
|
-
|
|
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
|
-
|
|
640
|
+
installCandidates,
|
|
587
641
|
installHarness,
|
|
588
|
-
|
|
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.
|
|
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",
|
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
|
}
|