@quenty/cli-output-helpers 1.11.0 → 1.13.0
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/CHANGELOG.md +19 -0
- package/dist/reporting/github/formatting.d.ts +2 -2
- package/dist/reporting/github/formatting.d.ts.map +1 -1
- package/dist/reporting/github/formatting.js +7 -1
- package/dist/reporting/github/formatting.js.map +1 -1
- package/dist/reporting/progress-format.d.ts.map +1 -1
- package/dist/reporting/progress-format.js +4 -0
- package/dist/reporting/progress-format.js.map +1 -1
- package/dist/reporting/reporter.d.ts +6 -1
- package/dist/reporting/reporter.d.ts.map +1 -1
- package/dist/reporting/reporter.js.map +1 -1
- package/dist/reporting/simple-reporter.d.ts.map +1 -1
- package/dist/reporting/simple-reporter.js +4 -1
- package/dist/reporting/simple-reporter.js.map +1 -1
- package/dist/reporting/spinner-reporter.d.ts +12 -1
- package/dist/reporting/spinner-reporter.d.ts.map +1 -1
- package/dist/reporting/spinner-reporter.js +64 -25
- package/dist/reporting/spinner-reporter.js.map +1 -1
- package/dist/reporting/spinner-reporter.test.js +50 -30
- package/dist/reporting/spinner-reporter.test.js.map +1 -1
- package/package.json +2 -2
- package/src/reporting/github/formatting.ts +9 -4
- package/src/reporting/progress-format.ts +4 -0
- package/src/reporting/reporter.ts +11 -1
- package/src/reporting/simple-reporter.ts +5 -1
- package/src/reporting/spinner-reporter.test.ts +61 -31
- package/src/reporting/spinner-reporter.ts +80 -34
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spinner-reporter.test.js","sourceRoot":"","sources":["../../src/reporting/spinner-reporter.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEjE;;;GAGG;AACH,SAAS,KAAK;IACZ,MAAM,KAAK,GAAG,IAAI,gBAAgB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,IAAI,eAAe,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;IAEhE,uCAAuC;IACvC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,
|
|
1
|
+
{"version":3,"file":"spinner-reporter.test.js","sourceRoot":"","sources":["../../src/reporting/spinner-reporter.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/cli-output-helpers",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "Helpers to generate Nevermore package and game templates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -59,5 +59,5 @@
|
|
|
59
59
|
"engines": {
|
|
60
60
|
"node": ">=16"
|
|
61
61
|
},
|
|
62
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "9bca59da624eab5ee3f6ee7e0c0edb38e6466722"
|
|
63
63
|
}
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
import { formatDurationMs } from '../../cli-utils.js';
|
|
9
9
|
import {
|
|
10
10
|
type PackageResult,
|
|
11
|
-
type PackageStatus,
|
|
12
11
|
type ProgressSummary,
|
|
13
12
|
type JobPhase,
|
|
14
13
|
} from '../reporter.js';
|
|
@@ -106,8 +105,14 @@ function _extractJsonMessage(text: string): string | undefined {
|
|
|
106
105
|
|
|
107
106
|
// ── Table rendering ─────────────────────────────────────────────────────────
|
|
108
107
|
|
|
109
|
-
|
|
108
|
+
// Typed Record<JobPhase, string> so adding a new JobPhase fails the build
|
|
109
|
+
// until a label is supplied here.
|
|
110
|
+
const RUNNING_PHASE_LABELS: Record<JobPhase, string> = {
|
|
111
|
+
waiting: '⏸ Waiting...',
|
|
110
112
|
building: '🔨 Building...',
|
|
113
|
+
downloading: '⬇ Downloading...',
|
|
114
|
+
merging: '🔀 Merging...',
|
|
115
|
+
combining: '🔗 Combining...',
|
|
111
116
|
uploading: '📤 Uploading...',
|
|
112
117
|
scheduling: '⏳ Scheduling...',
|
|
113
118
|
launching: '🚀 Launching...',
|
|
@@ -116,10 +121,10 @@ const RUNNING_PHASE_LABELS: Record<string, string> = {
|
|
|
116
121
|
};
|
|
117
122
|
|
|
118
123
|
export function formatRunningStatus(
|
|
119
|
-
phase:
|
|
124
|
+
phase: JobPhase,
|
|
120
125
|
progress?: ProgressSummary
|
|
121
126
|
): string {
|
|
122
|
-
const label = RUNNING_PHASE_LABELS[phase]
|
|
127
|
+
const label = RUNNING_PHASE_LABELS[phase];
|
|
123
128
|
if (progress) {
|
|
124
129
|
const progressText = formatProgressInline(progress);
|
|
125
130
|
return progressText ? `${label} ${progressText}` : label;
|
|
@@ -31,6 +31,8 @@ export function formatProgressInline(progress?: ProgressSummary): string {
|
|
|
31
31
|
}
|
|
32
32
|
// Indeterminate: show label or just the count
|
|
33
33
|
return progress.label ? `(${progress.label})` : `(${progress.completed})`;
|
|
34
|
+
case 'version':
|
|
35
|
+
return `(v${progress.version})`;
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -52,6 +54,8 @@ export function formatProgressResult(progress?: ProgressSummary): string {
|
|
|
52
54
|
return `(${_formatBytes(progress.totalBytes)})`;
|
|
53
55
|
case 'steps':
|
|
54
56
|
return `(${progress.completed}/${progress.total})`;
|
|
57
|
+
case 'version':
|
|
58
|
+
return `(v${progress.version})`;
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
|
|
@@ -46,7 +46,17 @@ export interface StepProgress {
|
|
|
46
46
|
label?: string;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
/** Place version label: "v14" */
|
|
50
|
+
export interface VersionProgress {
|
|
51
|
+
kind: 'version';
|
|
52
|
+
version: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type ProgressSummary =
|
|
56
|
+
| TestCountProgress
|
|
57
|
+
| ByteProgress
|
|
58
|
+
| StepProgress
|
|
59
|
+
| VersionProgress;
|
|
50
60
|
|
|
51
61
|
/** Result for a single package in a batch run. */
|
|
52
62
|
export interface PackageResult {
|
|
@@ -34,10 +34,14 @@ export class SimpleReporter extends BaseReporter {
|
|
|
34
34
|
|
|
35
35
|
if (result.logs && showLogs) {
|
|
36
36
|
console.log(result.logs);
|
|
37
|
-
} else if (showLogs) {
|
|
37
|
+
} else if (showLogs && !result.error) {
|
|
38
38
|
OutputHelper.info('(no output)');
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
if (result.error) {
|
|
42
|
+
OutputHelper.error(result.error);
|
|
43
|
+
}
|
|
44
|
+
|
|
41
45
|
const progressText = formatProgressResult(result.progressSummary);
|
|
42
46
|
if (result.success) {
|
|
43
47
|
const msg = progressText
|
|
@@ -12,10 +12,9 @@ function setup() {
|
|
|
12
12
|
|
|
13
13
|
// Capture everything written to stdout
|
|
14
14
|
const writes: string[] = [];
|
|
15
|
-
const realWrite = process.stdout.write.bind(process.stdout);
|
|
16
15
|
vi.spyOn(process.stdout, 'write').mockImplementation(((
|
|
17
16
|
chunk: any,
|
|
18
|
-
...
|
|
17
|
+
..._args: any[]
|
|
19
18
|
) => {
|
|
20
19
|
writes.push(typeof chunk === 'string' ? chunk : chunk.toString());
|
|
21
20
|
return true;
|
|
@@ -24,7 +23,7 @@ function setup() {
|
|
|
24
23
|
// Suppress console.log (used by startAsync header)
|
|
25
24
|
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
26
25
|
|
|
27
|
-
return { state, spinner, writes
|
|
26
|
+
return { state, spinner, writes };
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
/** Extract cursor-up escape codes (\x1b[NA) from captured writes. */
|
|
@@ -39,7 +38,7 @@ function extractCursorUps(writes: string[]): number[] {
|
|
|
39
38
|
return results;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
describe('SpinnerReporter
|
|
41
|
+
describe('SpinnerReporter cursor math', () => {
|
|
43
42
|
beforeEach(() => {
|
|
44
43
|
vi.useFakeTimers();
|
|
45
44
|
});
|
|
@@ -49,7 +48,7 @@ describe('SpinnerReporter stdout resilience', () => {
|
|
|
49
48
|
vi.restoreAllMocks();
|
|
50
49
|
});
|
|
51
50
|
|
|
52
|
-
it('cursor-up matches rendered line count
|
|
51
|
+
it('cursor-up matches rendered line count', async () => {
|
|
53
52
|
const { state, spinner, writes } = setup();
|
|
54
53
|
|
|
55
54
|
state.onPackageStart('pkg-a');
|
|
@@ -69,65 +68,96 @@ describe('SpinnerReporter stdout resilience', () => {
|
|
|
69
68
|
await spinner.stopAsync();
|
|
70
69
|
});
|
|
71
70
|
|
|
72
|
-
it('
|
|
71
|
+
it('cursor-up ignores external writes (they are captured, not emitted)', async () => {
|
|
73
72
|
const { state, spinner, writes } = setup();
|
|
74
73
|
|
|
75
74
|
state.onPackageStart('pkg-a');
|
|
76
75
|
await spinner.startAsync();
|
|
77
76
|
|
|
78
|
-
//
|
|
77
|
+
// External writes during the spinner are buffered, not sent to the terminal.
|
|
79
78
|
process.stdout.write('external log\n');
|
|
79
|
+
process.stdout.write('more\nstuff\n');
|
|
80
80
|
|
|
81
81
|
writes.length = 0;
|
|
82
82
|
vi.advanceTimersByTime(80);
|
|
83
83
|
|
|
84
|
-
//
|
|
84
|
+
// Still just 3 — external writes never actually reach stdout, so they
|
|
85
|
+
// can't shift the cursor.
|
|
85
86
|
const ups = extractCursorUps(writes);
|
|
86
87
|
expect(ups.length).toBe(1);
|
|
87
|
-
expect(ups[0]).toBe(
|
|
88
|
+
expect(ups[0]).toBe(3);
|
|
88
89
|
|
|
89
90
|
await spinner.stopAsync();
|
|
90
91
|
});
|
|
92
|
+
});
|
|
91
93
|
|
|
92
|
-
|
|
94
|
+
describe('SpinnerReporter output capture', () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
vi.useFakeTimers();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterEach(() => {
|
|
100
|
+
vi.useRealTimers();
|
|
101
|
+
vi.restoreAllMocks();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('captures writes during the run and flushes them on stop', async () => {
|
|
93
105
|
const { state, spinner, writes } = setup();
|
|
94
106
|
|
|
95
107
|
state.onPackageStart('pkg-a');
|
|
96
108
|
await spinner.startAsync();
|
|
97
109
|
|
|
98
|
-
//
|
|
99
|
-
process.stdout.write('
|
|
100
|
-
process.stdout.write('
|
|
101
|
-
|
|
102
|
-
writes.length = 0;
|
|
103
|
-
vi.advanceTimersByTime(80);
|
|
110
|
+
// Capture index where post-spinner output begins.
|
|
111
|
+
process.stdout.write('hello\n');
|
|
112
|
+
process.stdout.write('world\n');
|
|
104
113
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
expect(
|
|
108
|
-
expect(ups[0]).toBe(6);
|
|
114
|
+
// Verify nothing was emitted live (only spinner frames so far).
|
|
115
|
+
expect(writes.join('')).not.toContain('hello');
|
|
116
|
+
expect(writes.join('')).not.toContain('world');
|
|
109
117
|
|
|
110
118
|
await spinner.stopAsync();
|
|
119
|
+
|
|
120
|
+
// After stopAsync, the captured output must have been flushed.
|
|
121
|
+
const all = writes.join('');
|
|
122
|
+
expect(all).toContain('hello');
|
|
123
|
+
expect(all).toContain('world');
|
|
124
|
+
// Order is preserved
|
|
125
|
+
expect(all.indexOf('hello')).toBeLessThan(all.indexOf('world'));
|
|
111
126
|
});
|
|
112
127
|
|
|
113
|
-
it('
|
|
128
|
+
it('captures stderr writes too', async () => {
|
|
114
129
|
const { state, spinner, writes } = setup();
|
|
130
|
+
// Intercept stderr the same way stdout is mocked.
|
|
131
|
+
vi.spyOn(process.stderr, 'write').mockImplementation(((
|
|
132
|
+
chunk: any,
|
|
133
|
+
..._args: any[]
|
|
134
|
+
) => {
|
|
135
|
+
writes.push(typeof chunk === 'string' ? chunk : chunk.toString());
|
|
136
|
+
return true;
|
|
137
|
+
}) as any);
|
|
115
138
|
|
|
116
139
|
state.onPackageStart('pkg-a');
|
|
117
140
|
await spinner.startAsync();
|
|
118
141
|
|
|
119
|
-
|
|
120
|
-
process.stdout.write('noise\n');
|
|
121
|
-
vi.advanceTimersByTime(80);
|
|
142
|
+
process.stderr.write('err line\n');
|
|
122
143
|
|
|
123
|
-
|
|
124
|
-
writes.length = 0;
|
|
125
|
-
vi.advanceTimersByTime(80);
|
|
144
|
+
expect(writes.join('')).not.toContain('err line');
|
|
126
145
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
expect(
|
|
130
|
-
|
|
146
|
+
await spinner.stopAsync();
|
|
147
|
+
|
|
148
|
+
expect(writes.join('')).toContain('err line');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('invokes the Node-style completion callback on captured writes', async () => {
|
|
152
|
+
const { state, spinner } = setup();
|
|
153
|
+
|
|
154
|
+
state.onPackageStart('pkg-a');
|
|
155
|
+
await spinner.startAsync();
|
|
156
|
+
|
|
157
|
+
const cb = vi.fn();
|
|
158
|
+
process.stdout.write('payload\n', cb);
|
|
159
|
+
|
|
160
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
131
161
|
|
|
132
162
|
await spinner.stopAsync();
|
|
133
163
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { OutputHelper } from '../outputHelper.js';
|
|
2
2
|
import { formatDurationMs } from '../cli-utils.js';
|
|
3
|
-
import { type PackageResult, BaseReporter } from './reporter.js';
|
|
3
|
+
import { type JobPhase, type PackageResult, BaseReporter } from './reporter.js';
|
|
4
4
|
import { type IStateTracker } from './state/state-tracker.js';
|
|
5
5
|
import {
|
|
6
6
|
formatProgressInline,
|
|
@@ -12,6 +12,8 @@ export interface SpinnerReporterOptions {
|
|
|
12
12
|
showLogs: boolean;
|
|
13
13
|
/** Verb used in the header, e.g. "Testing", "Deploying". Default: "Processing" */
|
|
14
14
|
actionVerb?: string;
|
|
15
|
+
/** Extra context appended to the header line, e.g. "to target 'integration'". */
|
|
16
|
+
actionContext?: string;
|
|
15
17
|
/** Label for successful results, e.g. "Deployed". Default: "Passed" */
|
|
16
18
|
successLabel?: string;
|
|
17
19
|
/** Label for failed results, e.g. "DEPLOY FAILED". Default: "FAILED" */
|
|
@@ -20,10 +22,14 @@ export interface SpinnerReporterOptions {
|
|
|
20
22
|
|
|
21
23
|
const SPINNER_FRAMES = ['◐', '◓', '◑', '◒'];
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
// Typed Record<JobPhase, string> so adding a new JobPhase fails the build
|
|
26
|
+
// until a label is supplied here — otherwise the renderer's else branch
|
|
27
|
+
// would silently flash the failure label during the missing phase.
|
|
28
|
+
const PHASE_LABELS: Record<JobPhase, string> = {
|
|
25
29
|
waiting: '◇ Waiting',
|
|
26
30
|
building: '⚙ Building',
|
|
31
|
+
downloading: '⬇ Downloading',
|
|
32
|
+
merging: '🔀 Merging',
|
|
27
33
|
combining: '🔗 Combining',
|
|
28
34
|
uploading: '▲ Uploading',
|
|
29
35
|
scheduling: '◇ Scheduling',
|
|
@@ -35,6 +41,14 @@ const PHASE_LABELS: Record<string, string> = {
|
|
|
35
41
|
/**
|
|
36
42
|
* TTY spinner rendering for batch job progress.
|
|
37
43
|
* Reads all state from IStateTracker; re-renders on a timer interval.
|
|
44
|
+
*
|
|
45
|
+
* Stdout/stderr writes between startAsync() and stopAsync() are *captured*,
|
|
46
|
+
* not passed through. The spinner repaints by rewinding the cursor with
|
|
47
|
+
* `\x1b[NA\x1b[0J`, so any write that landed inside the spinner's render
|
|
48
|
+
* region would be erased on the next 80ms tick. Callers should not have to
|
|
49
|
+
* think about that — we buffer writes and flush them in stopAsync(), so
|
|
50
|
+
* `console.log` / `OutputHelper.info` / etc. during a run still surface,
|
|
51
|
+
* just after the spinner has finished.
|
|
38
52
|
*/
|
|
39
53
|
export class SpinnerReporter extends BaseReporter {
|
|
40
54
|
private _state: IStateTracker;
|
|
@@ -42,8 +56,9 @@ export class SpinnerReporter extends BaseReporter {
|
|
|
42
56
|
private _renderedLineCount: number = 0;
|
|
43
57
|
private _renderInterval?: ReturnType<typeof setInterval>;
|
|
44
58
|
private _spinnerFrame: number = 0;
|
|
45
|
-
private
|
|
59
|
+
private _capturedOutput: string = '';
|
|
46
60
|
private _originalStdoutWrite: typeof process.stdout.write | undefined;
|
|
61
|
+
private _originalStderrWrite: typeof process.stderr.write | undefined;
|
|
47
62
|
private _isRendering = false;
|
|
48
63
|
|
|
49
64
|
constructor(state: IStateTracker, options: SpinnerReporterOptions) {
|
|
@@ -55,24 +70,47 @@ export class SpinnerReporter extends BaseReporter {
|
|
|
55
70
|
override async startAsync(): Promise<void> {
|
|
56
71
|
const count = this._state.total;
|
|
57
72
|
const verb = this._options.actionVerb ?? 'Processing';
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
const noun = count === 1 ? 'package' : 'packages';
|
|
74
|
+
const context = this._options.actionContext;
|
|
75
|
+
const header = context
|
|
76
|
+
? `${verb} ${count} ${noun} ${context}`
|
|
77
|
+
: `${verb} ${count} ${noun}`;
|
|
78
|
+
console.log(OutputHelper.formatInfo(`${header}\n`));
|
|
63
79
|
process.stdout.write('\x1b[?25l');
|
|
64
80
|
this._renderedLineCount = 0;
|
|
65
81
|
|
|
66
|
-
// Intercept stdout
|
|
82
|
+
// Intercept stdout/stderr. External writes during the spinner are
|
|
83
|
+
// captured into a buffer instead of going to the terminal, otherwise the
|
|
84
|
+
// next 80ms render tick would clobber them via the cursor-rewind. The
|
|
85
|
+
// buffer is flushed in stopAsync() so callers still see their output —
|
|
86
|
+
// just after the spinner finishes. Writes made *by* the spinner itself
|
|
87
|
+
// (_isRendering=true) pass through normally.
|
|
67
88
|
this._originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
89
|
+
this._originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
68
90
|
const self = this;
|
|
69
|
-
|
|
70
|
-
|
|
91
|
+
const intercept = (
|
|
92
|
+
originalWrite: typeof process.stdout.write,
|
|
93
|
+
stream: NodeJS.WriteStream
|
|
94
|
+
) =>
|
|
95
|
+
function (chunk: any, ...args: any[]) {
|
|
96
|
+
if (self._isRendering) {
|
|
97
|
+
return originalWrite.call(stream, chunk, ...args);
|
|
98
|
+
}
|
|
71
99
|
const str = typeof chunk === 'string' ? chunk : chunk.toString();
|
|
72
|
-
self.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
100
|
+
self._capturedOutput += str;
|
|
101
|
+
// Invoke the optional Node-style completion callback if present.
|
|
102
|
+
const cb = args.find((a) => typeof a === 'function');
|
|
103
|
+
if (cb) cb();
|
|
104
|
+
return true;
|
|
105
|
+
};
|
|
106
|
+
process.stdout.write = intercept(
|
|
107
|
+
this._originalStdoutWrite,
|
|
108
|
+
process.stdout
|
|
109
|
+
) as any;
|
|
110
|
+
process.stderr.write = intercept(
|
|
111
|
+
this._originalStderrWrite,
|
|
112
|
+
process.stderr
|
|
113
|
+
) as any;
|
|
76
114
|
|
|
77
115
|
this._render();
|
|
78
116
|
this._renderInterval = setInterval(() => {
|
|
@@ -88,15 +126,26 @@ export class SpinnerReporter extends BaseReporter {
|
|
|
88
126
|
}
|
|
89
127
|
this._render();
|
|
90
128
|
|
|
91
|
-
// Restore original stdout.write
|
|
129
|
+
// Restore original stdout.write / stderr.write
|
|
92
130
|
if (this._originalStdoutWrite) {
|
|
93
131
|
process.stdout.write = this._originalStdoutWrite;
|
|
94
132
|
this._originalStdoutWrite = undefined;
|
|
95
133
|
}
|
|
134
|
+
if (this._originalStderrWrite) {
|
|
135
|
+
process.stderr.write = this._originalStderrWrite;
|
|
136
|
+
this._originalStderrWrite = undefined;
|
|
137
|
+
}
|
|
96
138
|
|
|
97
139
|
process.stdout.write('\x1b[?25h');
|
|
98
140
|
console.log('');
|
|
99
141
|
|
|
142
|
+
// Flush anything captured during the run. Goes out *after* the final
|
|
143
|
+
// spinner frame so callers see their late prints below the progress.
|
|
144
|
+
if (this._capturedOutput.length > 0) {
|
|
145
|
+
process.stdout.write(this._capturedOutput);
|
|
146
|
+
this._capturedOutput = '';
|
|
147
|
+
}
|
|
148
|
+
|
|
100
149
|
if (this._options.showLogs) {
|
|
101
150
|
this._printAllLogs();
|
|
102
151
|
} else {
|
|
@@ -161,24 +210,12 @@ export class SpinnerReporter extends BaseReporter {
|
|
|
161
210
|
|
|
162
211
|
let line: string;
|
|
163
212
|
|
|
164
|
-
const phaseLabel = PHASE_LABELS[state.status];
|
|
165
|
-
|
|
166
213
|
if (state.status === 'pending') {
|
|
167
214
|
const icon = OutputHelper.formatDim('○');
|
|
168
215
|
const statusText = OutputHelper.formatDim('Queued');
|
|
169
216
|
line = ` ${icon} ${OutputHelper.formatDim(
|
|
170
217
|
state.name.padEnd(30)
|
|
171
218
|
)} ${statusText}`;
|
|
172
|
-
} else if (phaseLabel) {
|
|
173
|
-
const icon = OutputHelper.formatInfo(spinner);
|
|
174
|
-
const progressText = formatProgressInline(state.progress);
|
|
175
|
-
const plain = progressText
|
|
176
|
-
? `${phaseLabel} ${progressText}`
|
|
177
|
-
: phaseLabel;
|
|
178
|
-
const statusText = OutputHelper.formatInfo(plain.padEnd(22));
|
|
179
|
-
line = ` ${icon} ${state.name.padEnd(
|
|
180
|
-
30
|
|
181
|
-
)} ${statusText} ${OutputHelper.formatDim(time)}`;
|
|
182
219
|
} else if (state.status === 'passed') {
|
|
183
220
|
const icon = OutputHelper.formatSuccess('✓');
|
|
184
221
|
const progressText = formatProgressResult(
|
|
@@ -194,7 +231,7 @@ export class SpinnerReporter extends BaseReporter {
|
|
|
194
231
|
line = ` ${icon} ${state.name.padEnd(
|
|
195
232
|
30
|
|
196
233
|
)} ${statusText} ${OutputHelper.formatDim(time)}`;
|
|
197
|
-
} else {
|
|
234
|
+
} else if (state.status === 'failed') {
|
|
198
235
|
const icon = OutputHelper.formatError('✗');
|
|
199
236
|
const failedPhase = state.result?.failedPhase;
|
|
200
237
|
const plain = failedPhase
|
|
@@ -204,6 +241,17 @@ export class SpinnerReporter extends BaseReporter {
|
|
|
204
241
|
line = ` ${icon} ${state.name.padEnd(
|
|
205
242
|
30
|
|
206
243
|
)} ${statusText} ${OutputHelper.formatDim(time)}`;
|
|
244
|
+
} else {
|
|
245
|
+
const phaseLabel = PHASE_LABELS[state.status];
|
|
246
|
+
const icon = OutputHelper.formatInfo(spinner);
|
|
247
|
+
const progressText = formatProgressInline(state.progress);
|
|
248
|
+
const plain = progressText
|
|
249
|
+
? `${phaseLabel} ${progressText}`
|
|
250
|
+
: phaseLabel;
|
|
251
|
+
const statusText = OutputHelper.formatInfo(plain.padEnd(22));
|
|
252
|
+
line = ` ${icon} ${state.name.padEnd(
|
|
253
|
+
30
|
|
254
|
+
)} ${statusText} ${OutputHelper.formatDim(time)}`;
|
|
207
255
|
}
|
|
208
256
|
|
|
209
257
|
lines.push(line);
|
|
@@ -218,10 +266,8 @@ export class SpinnerReporter extends BaseReporter {
|
|
|
218
266
|
|
|
219
267
|
this._isRendering = true;
|
|
220
268
|
let frame = '';
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (totalLines > 0) {
|
|
224
|
-
frame += `\x1b[${totalLines}A\x1b[0J`;
|
|
269
|
+
if (this._renderedLineCount > 0) {
|
|
270
|
+
frame += `\x1b[${this._renderedLineCount}A\x1b[0J`;
|
|
225
271
|
}
|
|
226
272
|
frame += lines.join('\n') + '\n';
|
|
227
273
|
process.stdout.write(frame);
|