iterate-skill-installer 3.4.1 → 3.4.2

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/bin/cli.js CHANGED
@@ -56,7 +56,10 @@ async function run() {
56
56
 
57
57
  try {
58
58
  const code = await main(options);
59
- process.exit(code);
59
+ // Normalize: a null/non-numeric code (main should never produce one, but a
60
+ // signal-killed child upstream does) must surface as a failure, never as
61
+ // the success exit 0 that `process.exit(null)` would yield.
62
+ process.exit(typeof code === 'number' ? code : 1);
60
63
  } catch (err) {
61
64
  console.error(`\nUnexpected error: ${err.message}`);
62
65
  process.exit(1);
package/lib/installer.js CHANGED
@@ -308,21 +308,69 @@ function parseChecksums(text) {
308
308
  return map;
309
309
  }
310
310
 
311
+ /**
312
+ * Bounded in-memory output buffer: retains only the last ``cap`` characters.
313
+ * Used by ``runCommand`` so a chatty child (a verbose build listing, pip log
314
+ * flood, etc.) can never drive the installer's memory usage unbounded.
315
+ */
316
+ class TailBuffer {
317
+ constructor(cap) {
318
+ this.cap = cap;
319
+ this.text = '';
320
+ }
321
+ push(chunk) {
322
+ if (!chunk) return;
323
+ if (this.text.length + chunk.length > this.cap) {
324
+ this.text = (this.text + chunk).slice(-this.cap);
325
+ } else {
326
+ this.text += chunk;
327
+ }
328
+ }
329
+ toString() {
330
+ return this.text;
331
+ }
332
+ }
333
+
334
+ // Default ceiling for a single child command: 10 minutes then SIGKILL. Long
335
+ // operations in this file (pip/pipx installs, venv creation) can legitimately
336
+ // take minutes on slow networks, but a wedged child must never hang the
337
+ // installer forever.
338
+ const DEFAULT_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
339
+ // Hard cap on retained stdout/stderr per command (memory bound).
340
+ const MAX_CAPTURED_OUTPUT_CHARS = 1024 * 1024;
341
+
311
342
  function runCommand(bin, args, options = {}) {
343
+ const { timeout = DEFAULT_COMMAND_TIMEOUT_MS, ...spawnOptions } = options;
312
344
  return new Promise((resolve, reject) => {
313
- const child = spawn(bin, args, { stdio: 'pipe', ...options });
314
- let stdout = '';
315
- let stderr = '';
316
- child.stdout?.on('data', (data) => { stdout += data.toString(); });
317
- child.stderr?.on('data', (data) => { stderr += data.toString(); });
345
+ const child = spawn(bin, args, { stdio: 'pipe', ...spawnOptions });
346
+ const stdoutTail = new TailBuffer(MAX_CAPTURED_OUTPUT_CHARS);
347
+ const stderrTail = new TailBuffer(MAX_CAPTURED_OUTPUT_CHARS);
348
+ child.stdout?.on('data', (data) => stdoutTail.push(data.toString()));
349
+ child.stderr?.on('data', (data) => stderrTail.push(data.toString()));
350
+ let killed = false;
351
+ const timer =
352
+ timeout > 0
353
+ ? setTimeout(() => {
354
+ killed = true;
355
+ child.kill('SIGKILL');
356
+ }, timeout)
357
+ : null;
318
358
  child.on('close', (code) => {
359
+ if (timer) clearTimeout(timer);
360
+ if (killed) {
361
+ reject(new InstallerError(`${bin} timed out after ${timeout} ms`));
362
+ return;
363
+ }
319
364
  if (code === 0) {
320
- resolve(stdout);
365
+ resolve(stdoutTail.toString());
321
366
  } else {
322
- reject(new InstallerError(`${bin} exited with ${code}: ${stderr || stdout}`));
367
+ reject(new InstallerError(`${bin} exited with ${code}: ${stderrTail.toString() || stdoutTail.toString()}`));
323
368
  }
324
369
  });
325
- child.on('error', reject);
370
+ child.on('error', (err) => {
371
+ if (timer) clearTimeout(timer);
372
+ reject(err);
373
+ });
326
374
  });
327
375
  }
328
376
 
@@ -434,8 +482,10 @@ function runPythonInstall(pythonBin, installScript, args, options = {}) {
434
482
  child.on('close', (code) => {
435
483
  // Resolve with the exit code (rather than rejecting) so the caller can
436
484
  // distinguish "real failure" from "user cancelled the selection" and
437
- // avoid reporting a false success.
438
- resolve(code);
485
+ // avoid reporting a false success. A signal-killed child reports a null
486
+ // code, which the shell would otherwise read as success; normalize to a
487
+ // non-zero failure.
488
+ resolve(typeof code === 'number' ? code : 1);
439
489
  });
440
490
  child.on('error', reject);
441
491
  });
@@ -889,4 +939,10 @@ module.exports = {
889
939
  normalizeToken,
890
940
  buildAuthFlags,
891
941
  supportsCurlFailWithBody,
942
+ // Executed under test for the timeout/null-exit-code hardening; retaining
943
+ // the exports also keeps them statically reachable for QA/debugging.
944
+ runCommand,
945
+ runPythonInstall,
946
+ TailBuffer,
947
+ DEFAULT_COMMAND_TIMEOUT_MS,
892
948
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-skill-installer",
3
- "version": "3.4.1",
3
+ "version": "3.4.2",
4
4
  "description": "One-command installer for iterate-skill across AI coding assistants",
5
5
  "main": "lib/installer.js",
6
6
  "files": [