UNPKG

@quenty/cli-output-helpers

Version:

Helpers to generate Nevermore package and game templates

176 lines 7.32 kB
import { OutputHelper } from '../outputHelper.js'; import { formatDurationMs } from '../cli-utils.js'; import { BaseReporter } from './reporter.js'; import { formatProgressInline, formatProgressResult, isEmptyTestRun } from './progress-format.js'; const SPINNER_FRAMES = ['◐', '◓', '◑', '◒']; /** Emoji + label for each active phase in the spinner. */ const PHASE_LABELS = { waiting: '◇ Waiting', building: '⚙ Building', combining: '🔗 Combining', uploading: '▲ Uploading', scheduling: '◇ Scheduling', launching: '🚀 Launching', connecting: '🔌 Connecting', executing: '▶ Executing', }; /** * TTY spinner rendering for batch job progress. * Reads all state from IStateTracker; re-renders on a timer interval. */ export class SpinnerReporter extends BaseReporter { _state; _options; _renderedLineCount = 0; _renderInterval; _spinnerFrame = 0; _extraLines = 0; _originalStdoutWrite; _isRendering = false; constructor(state, options) { super(); this._state = state; this._options = options; } async startAsync() { const count = this._state.total; const verb = this._options.actionVerb ?? 'Processing'; console.log(OutputHelper.formatInfo(`${verb} ${count} ${count === 1 ? 'package' : 'packages'}\n`)); process.stdout.write('\x1b[?25l'); this._renderedLineCount = 0; // Intercept stdout to track external writes that shift the cursor this._originalStdoutWrite = process.stdout.write.bind(process.stdout); const self = this; process.stdout.write = function (chunk, ...args) { if (!self._isRendering) { const str = typeof chunk === 'string' ? chunk : chunk.toString(); self._extraLines += (str.match(/\n/g) || []).length; } return self._originalStdoutWrite.call(process.stdout, chunk, ...args); }; this._render(); this._renderInterval = setInterval(() => { this._spinnerFrame = (this._spinnerFrame + 1) % SPINNER_FRAMES.length; this._render(); }, 80); } async stopAsync() { if (this._renderInterval) { clearInterval(this._renderInterval); this._renderInterval = undefined; } this._render(); // Restore original stdout.write if (this._originalStdoutWrite) { process.stdout.write = this._originalStdoutWrite; this._originalStdoutWrite = undefined; } process.stdout.write('\x1b[?25h'); console.log(''); if (this._options.showLogs) { this._printAllLogs(); } else { this._printFailureLogs(); } } _printFailureLogs() { const failures = this._state.getFailures(); if (failures.length === 0) return; console.log(OutputHelper.formatError(`\n${failures.length} failed:\n`)); for (const result of failures) { this._printResultLogs(result); } } _printAllLogs() { for (const result of this._state.getResults()) { this._printResultLogs(result); } } _printResultLogs(result) { const icon = result.success ? OutputHelper.formatSuccess('✓') : OutputHelper.formatError('✗'); const status = result.success ? (this._options.successLabel ?? 'Passed') : (this._options.failureLabel ?? 'FAILED'); const formatted = result.success ? OutputHelper.formatSuccess(status) : OutputHelper.formatError(status); console.log(`${icon} ${OutputHelper.formatDim('──')} ${result.packageName} ${OutputHelper.formatDim('──')} ${formatted}`); if (result.logs) { console.log(result.logs); } else { console.log(OutputHelper.formatDim(' (no output)')); } if (result.error) { console.log(` ${OutputHelper.formatError(result.error)}`); } console.log(''); } _render() { const lines = []; const spinner = SPINNER_FRAMES[this._spinnerFrame]; for (const state of this._state.getAllPackages()) { const elapsed = state.startMs ? state.durationMs ?? Date.now() - state.startMs : 0; const time = elapsed > 0 ? formatDurationMs(elapsed) : ''; let line; const phaseLabel = PHASE_LABELS[state.status]; if (state.status === 'pending') { const icon = OutputHelper.formatDim('○'); const statusText = OutputHelper.formatDim('Queued'); line = ` ${icon} ${OutputHelper.formatDim(state.name.padEnd(30))} ${statusText}`; } else if (phaseLabel) { const icon = OutputHelper.formatInfo(spinner); const progressText = formatProgressInline(state.progress); const plain = progressText ? `${phaseLabel} ${progressText}` : phaseLabel; const statusText = OutputHelper.formatInfo(plain.padEnd(22)); line = ` ${icon} ${state.name.padEnd(30)} ${statusText} ${OutputHelper.formatDim(time)}`; } else if (state.status === 'passed') { const icon = OutputHelper.formatSuccess('✓'); const progressText = formatProgressResult(state.result?.progressSummary); const label = this._options.successLabel ?? 'Passed'; const empty = isEmptyTestRun(state.result?.progressSummary); let plain = progressText ? `${label} ${progressText}` : label; if (empty) plain += ' ⚠'; const statusText = empty ? OutputHelper.formatWarning(plain.padEnd(22)) : OutputHelper.formatSuccess(plain.padEnd(22)); line = ` ${icon} ${state.name.padEnd(30)} ${statusText} ${OutputHelper.formatDim(time)}`; } else { const icon = OutputHelper.formatError('✗'); const failedPhase = state.result?.failedPhase; const plain = failedPhase ? `${this._options.failureLabel ?? 'FAILED'} at ${failedPhase}` : (this._options.failureLabel ?? 'FAILED'); const statusText = OutputHelper.formatError(plain.padEnd(22)); line = ` ${icon} ${state.name.padEnd(30)} ${statusText} ${OutputHelper.formatDim(time)}`; } lines.push(line); } lines.push(''); lines.push(OutputHelper.formatDim(`${this._state.completed}/${this._state.total} complete`)); this._isRendering = true; let frame = ''; const totalLines = this._renderedLineCount + this._extraLines; this._extraLines = 0; if (totalLines > 0) { frame += `\x1b[${totalLines}A\x1b[0J`; } frame += lines.join('\n') + '\n'; process.stdout.write(frame); this._renderedLineCount = lines.length; this._isRendering = false; } } //# sourceMappingURL=spinner-reporter.js.map