iterate-harness 2.2.4 → 2.2.6

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/lib/bootstrap.js CHANGED
@@ -587,6 +587,13 @@ function reportBootstrapFailure(error) {
587
587
  process.stderr.write(`[iterate-harness] ${message}\n`);
588
588
  }
589
589
 
590
+ // The install wizard only makes sense when a human can actually answer: both
591
+ // stdin and stderr must be TTYs. Piped/CI/`< /dev/null` runs bootstrap
592
+ // non-interactively so they never block on a prompt that nobody can see.
593
+ function interactiveSession() {
594
+ return Boolean(process.stdin.isTTY && process.stderr.isTTY);
595
+ }
596
+
590
597
  async function runHarness(args, env) {
591
598
  // Print the banner on every run (claude-code style). Skipped automatically
592
599
  // when stderr is not a TTY (e.g. piped output), so `ih --version | jq` stays clean.
@@ -594,7 +601,7 @@ async function runHarness(args, env) {
594
601
 
595
602
  let target;
596
603
  try {
597
- target = await ensureRuntime(env, { interactive: true });
604
+ target = await ensureRuntime(env, { interactive: interactiveSession() });
598
605
  } catch (error) {
599
606
  if (error instanceof CancelledError) {
600
607
  ui.info(error.message);
@@ -658,6 +665,7 @@ module.exports = {
658
665
  installCandidates,
659
666
  installHarness,
660
667
  installRemoteArtifact,
668
+ interactiveSession,
661
669
  isRemoteHttpUrl,
662
670
  isSupportedPython,
663
671
  needsBootstrap,
package/lib/ui.js CHANGED
@@ -80,24 +80,50 @@ function frameSection(title, lines) {
80
80
  console.error(bottom);
81
81
  }
82
82
 
83
- function askYesNo(question, defaultNo = false) {
83
+ // Safety net for unattended runs: if stdin is closed (EOF) or the user never
84
+ // answers, fall back to the default instead of hanging forever. Override per
85
+ // call with `options.timeoutMs` (0 disables the timeout entirely).
86
+ const DEFAULT_PROMPT_TIMEOUT_MS = 120000;
87
+
88
+ function askYesNo(question, defaultNo = false, options = {}) {
89
+ const input = options.input || process.stdin;
90
+ const output = options.output || process.stdout;
91
+ const timeoutMs =
92
+ options.timeoutMs === undefined ? DEFAULT_PROMPT_TIMEOUT_MS : options.timeoutMs;
84
93
  return new Promise((resolve) => {
85
- const rl = readline.createInterface({
86
- input: process.stdin,
87
- output: process.stdout,
88
- });
94
+ let settled = false;
95
+ let timer = null;
96
+ const rl = readline.createInterface({ input, output });
97
+ const finish = (value) => {
98
+ if (settled) return;
99
+ settled = true;
100
+ if (timer) clearTimeout(timer);
101
+ rl.close();
102
+ resolve(value);
103
+ };
104
+ // readline emits "close" on EOF (piped/`< /dev/null` stdin) — never hang.
105
+ rl.on("close", () => finish(defaultNo));
106
+ if (timeoutMs > 0) {
107
+ timer = setTimeout(() => {
108
+ process.stderr.write(
109
+ `\x1b[2m no response after ${Math.round(timeoutMs / 1000)}s; using default\x1b[0m\n`
110
+ );
111
+ finish(defaultNo);
112
+ }, timeoutMs);
113
+ if (typeof timer.unref === "function") timer.unref();
114
+ }
89
115
  const hint = defaultNo ? "[y/N]" : "[Y/n]";
90
116
  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);
117
+ const a = String(answer).trim().toLowerCase();
118
+ if (a === "y" || a === "yes") finish(true);
119
+ else if (a === "n" || a === "no") finish(false);
120
+ else finish(defaultNo);
96
121
  });
97
122
  });
98
123
  }
99
124
 
100
125
  module.exports = {
126
+ DEFAULT_PROMPT_TIMEOUT_MS,
101
127
  ITERATE_BANNER,
102
128
  printBanner,
103
129
  info,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-harness",
3
- "version": "2.2.4",
3
+ "version": "2.2.6",
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",