@quenty/cli-output-helpers 1.12.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 +13 -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 +11 -1
- package/dist/reporting/spinner-reporter.d.ts.map +1 -1
- package/dist/reporting/spinner-reporter.js +57 -33
- 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 +74 -42
- 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,7 +56,7 @@ 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;
|
|
47
61
|
private _originalStderrWrite: typeof process.stderr.write | undefined;
|
|
48
62
|
private _isRendering = false;
|
|
@@ -56,33 +70,47 @@ export class SpinnerReporter extends BaseReporter {
|
|
|
56
70
|
override async startAsync(): Promise<void> {
|
|
57
71
|
const count = this._state.total;
|
|
58
72
|
const verb = this._options.actionVerb ?? 'Processing';
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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`));
|
|
64
79
|
process.stdout.write('\x1b[?25l');
|
|
65
80
|
this._renderedLineCount = 0;
|
|
66
81
|
|
|
67
|
-
// Intercept stdout/stderr
|
|
68
|
-
//
|
|
69
|
-
//
|
|
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.
|
|
70
88
|
this._originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
71
89
|
this._originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
72
90
|
const self = this;
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
+
}
|
|
99
|
+
const str = typeof chunk === 'string' ? chunk : chunk.toString();
|
|
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;
|
|
86
114
|
|
|
87
115
|
this._render();
|
|
88
116
|
this._renderInterval = setInterval(() => {
|
|
@@ -111,6 +139,13 @@ export class SpinnerReporter extends BaseReporter {
|
|
|
111
139
|
process.stdout.write('\x1b[?25h');
|
|
112
140
|
console.log('');
|
|
113
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
|
+
|
|
114
149
|
if (this._options.showLogs) {
|
|
115
150
|
this._printAllLogs();
|
|
116
151
|
} else {
|
|
@@ -175,24 +210,12 @@ export class SpinnerReporter extends BaseReporter {
|
|
|
175
210
|
|
|
176
211
|
let line: string;
|
|
177
212
|
|
|
178
|
-
const phaseLabel = PHASE_LABELS[state.status];
|
|
179
|
-
|
|
180
213
|
if (state.status === 'pending') {
|
|
181
214
|
const icon = OutputHelper.formatDim('○');
|
|
182
215
|
const statusText = OutputHelper.formatDim('Queued');
|
|
183
216
|
line = ` ${icon} ${OutputHelper.formatDim(
|
|
184
217
|
state.name.padEnd(30)
|
|
185
218
|
)} ${statusText}`;
|
|
186
|
-
} else if (phaseLabel) {
|
|
187
|
-
const icon = OutputHelper.formatInfo(spinner);
|
|
188
|
-
const progressText = formatProgressInline(state.progress);
|
|
189
|
-
const plain = progressText
|
|
190
|
-
? `${phaseLabel} ${progressText}`
|
|
191
|
-
: phaseLabel;
|
|
192
|
-
const statusText = OutputHelper.formatInfo(plain.padEnd(22));
|
|
193
|
-
line = ` ${icon} ${state.name.padEnd(
|
|
194
|
-
30
|
|
195
|
-
)} ${statusText} ${OutputHelper.formatDim(time)}`;
|
|
196
219
|
} else if (state.status === 'passed') {
|
|
197
220
|
const icon = OutputHelper.formatSuccess('✓');
|
|
198
221
|
const progressText = formatProgressResult(
|
|
@@ -208,7 +231,7 @@ export class SpinnerReporter extends BaseReporter {
|
|
|
208
231
|
line = ` ${icon} ${state.name.padEnd(
|
|
209
232
|
30
|
|
210
233
|
)} ${statusText} ${OutputHelper.formatDim(time)}`;
|
|
211
|
-
} else {
|
|
234
|
+
} else if (state.status === 'failed') {
|
|
212
235
|
const icon = OutputHelper.formatError('✗');
|
|
213
236
|
const failedPhase = state.result?.failedPhase;
|
|
214
237
|
const plain = failedPhase
|
|
@@ -218,6 +241,17 @@ export class SpinnerReporter extends BaseReporter {
|
|
|
218
241
|
line = ` ${icon} ${state.name.padEnd(
|
|
219
242
|
30
|
|
220
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)}`;
|
|
221
255
|
}
|
|
222
256
|
|
|
223
257
|
lines.push(line);
|
|
@@ -232,10 +266,8 @@ export class SpinnerReporter extends BaseReporter {
|
|
|
232
266
|
|
|
233
267
|
this._isRendering = true;
|
|
234
268
|
let frame = '';
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (totalLines > 0) {
|
|
238
|
-
frame += `\x1b[${totalLines}A\x1b[0J`;
|
|
269
|
+
if (this._renderedLineCount > 0) {
|
|
270
|
+
frame += `\x1b[${this._renderedLineCount}A\x1b[0J`;
|
|
239
271
|
}
|
|
240
272
|
frame += lines.join('\n') + '\n';
|
|
241
273
|
process.stdout.write(frame);
|