iterate-skill-installer 3.4.0 → 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 +4 -1
- package/lib/installer.js +66 -10
- package/package.json +1 -1
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
|
-
|
|
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', ...
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
child.stdout?.on('data', (data) =>
|
|
317
|
-
child.stderr?.on('data', (data) =>
|
|
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(
|
|
365
|
+
resolve(stdoutTail.toString());
|
|
321
366
|
} else {
|
|
322
|
-
reject(new InstallerError(`${bin} exited with ${code}: ${
|
|
367
|
+
reject(new InstallerError(`${bin} exited with ${code}: ${stderrTail.toString() || stdoutTail.toString()}`));
|
|
323
368
|
}
|
|
324
369
|
});
|
|
325
|
-
child.on('error',
|
|
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
|
-
|
|
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
|
};
|