Files
ECC/scripts/lib/terminal-spinner.js
T
28e53a0bc1 feat(install): add guided multi-harness installer (#2649)
* feat(install): add guided Claude plugin setup

* fix: support Claude command shims on Windows

* feat: support safe Claude plugin scope migration

* fix(install): preserve interactive setup terminal

* fix(install): auto-migrate setup scope changes

* feat(install): add guided multi-harness installer

* fix(install): sync Yarn binary metadata

* fix(install): handle wizard EOF on Node 18

* ci: allow installer matrix tests to finish

* test(install): allow slower PowerShell delegation

* fix(install): harden guided provider reconciliation

* test(install): harden packaged and local compatibility

* chore: prepare guided installer release 2.2.0

* fix(install): report refreshed Codex marketplace state

* fix(install): verify managed content provenance

* test(install): allow empty Yarn smoke fixture

* test(install): invoke Windows package shims safely

* fix(install): close cross-platform release gaps

* fix(install): require trusted GitHub origins

* fix(install): preserve hook profile precedence

* refactor(install): centralize trusted GitHub origins

* ci: retrigger workflow run after merge of main

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 15:39:49 -04:00

78 lines
2.1 KiB
JavaScript

'use strict';
const { spawn } = require('child_process');
const CLEAR_LINE = '\r\x1b[2K';
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
const FRAME_INTERVAL_MS = 80;
function runAnimator(label, options = {}) {
const output = options.output || process.stdout;
const schedule = options.schedule || setInterval;
const clearSchedule = options.clearSchedule || clearInterval;
const onDisconnect = options.onDisconnect
|| (handler => process.on('disconnect', handler));
const exit = options.exit || (code => process.exit(code));
let frameIndex = 1;
const timer = schedule(() => {
output.write(`\r${FRAMES[frameIndex]} ${label}`);
frameIndex = (frameIndex + 1) % FRAMES.length;
}, FRAME_INTERVAL_MS);
onDisconnect(() => {
clearSchedule(timer);
exit(0);
});
}
function startTerminalSpinner(label, options = {}) {
const output = options.output || process.stdout;
const spawnProcess = options.spawnProcess || spawn;
const onAnimatorError = options.onAnimatorError;
output.write(`${FRAMES[0]} ${label}`);
let animator;
try {
animator = spawnProcess(
process.execPath,
[__filename, '--animate', label],
{
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
}
);
animator.on?.('error', error => {
// Preserve a stable visible fallback if the child cannot animate.
output.write(`\r${FRAMES[0]} ${label}`);
onAnimatorError?.(error);
});
} catch {
// The first frame still provides visible progress if animation cannot start.
}
let stopped = false;
return {
stop() {
if (stopped) return;
stopped = true;
animator?.once?.('close', () => {
// A child can render between kill() and close; clear that final frame.
output.write(CLEAR_LINE);
});
animator?.kill();
output.write(CLEAR_LINE);
},
};
}
// c8 ignore next 3 -- exercised as the independently instrumented child process.
if (require.main === module && process.argv[2] === '--animate') {
runAnimator(process.argv[3] || 'Working...');
}
module.exports = {
CLEAR_LINE,
FRAMES,
runAnimator,
startTerminalSpinner,
};