@quenty/cli-output-helpers 1.10.1 → 1.12.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.
Files changed (152) hide show
  1. package/CHANGELOG.md +30 -103
  2. package/dist/outputHelper.d.ts +5 -0
  3. package/dist/outputHelper.d.ts.map +1 -1
  4. package/dist/outputHelper.js +10 -1
  5. package/dist/outputHelper.js.map +1 -1
  6. package/dist/reporting/build-result-reporter.d.ts +33 -0
  7. package/dist/reporting/build-result-reporter.d.ts.map +1 -0
  8. package/dist/reporting/build-result-reporter.js +33 -0
  9. package/dist/reporting/build-result-reporter.js.map +1 -0
  10. package/dist/reporting/build-result-reporter.test.d.ts +2 -0
  11. package/dist/reporting/build-result-reporter.test.d.ts.map +1 -0
  12. package/dist/reporting/build-result-reporter.test.js +42 -0
  13. package/dist/reporting/build-result-reporter.test.js.map +1 -0
  14. package/dist/reporting/composite-reporter.d.ts.map +1 -1
  15. package/dist/reporting/composite-reporter.js.map +1 -1
  16. package/dist/reporting/composite-result-reporter.d.ts +15 -0
  17. package/dist/reporting/composite-result-reporter.d.ts.map +1 -0
  18. package/dist/reporting/composite-result-reporter.js +43 -0
  19. package/dist/reporting/composite-result-reporter.js.map +1 -0
  20. package/dist/reporting/composite-result-reporter.test.d.ts +2 -0
  21. package/dist/reporting/composite-result-reporter.test.d.ts.map +1 -0
  22. package/dist/reporting/composite-result-reporter.test.js +61 -0
  23. package/dist/reporting/composite-result-reporter.test.js.map +1 -0
  24. package/dist/reporting/file-result-reporter.d.ts +29 -0
  25. package/dist/reporting/file-result-reporter.d.ts.map +1 -0
  26. package/dist/reporting/file-result-reporter.js +61 -0
  27. package/dist/reporting/file-result-reporter.js.map +1 -0
  28. package/dist/reporting/file-result-reporter.test.d.ts +2 -0
  29. package/dist/reporting/file-result-reporter.test.d.ts.map +1 -0
  30. package/dist/reporting/file-result-reporter.test.js +78 -0
  31. package/dist/reporting/file-result-reporter.test.js.map +1 -0
  32. package/dist/reporting/format-json.d.ts +9 -0
  33. package/dist/reporting/format-json.d.ts.map +1 -0
  34. package/dist/reporting/format-json.js +12 -0
  35. package/dist/reporting/format-json.js.map +1 -0
  36. package/dist/reporting/format-json.test.d.ts +2 -0
  37. package/dist/reporting/format-json.test.d.ts.map +1 -0
  38. package/dist/reporting/format-json.test.js +29 -0
  39. package/dist/reporting/format-json.test.js.map +1 -0
  40. package/dist/reporting/format-table.d.ts +18 -0
  41. package/dist/reporting/format-table.d.ts.map +1 -0
  42. package/dist/reporting/format-table.js +61 -0
  43. package/dist/reporting/format-table.js.map +1 -0
  44. package/dist/reporting/format-table.test.d.ts +2 -0
  45. package/dist/reporting/format-table.test.d.ts.map +1 -0
  46. package/dist/reporting/format-table.test.js +90 -0
  47. package/dist/reporting/format-table.test.js.map +1 -0
  48. package/dist/reporting/github/annotations.d.ts.map +1 -1
  49. package/dist/reporting/github/annotations.js +1 -4
  50. package/dist/reporting/github/annotations.js.map +1 -1
  51. package/dist/reporting/github/annotations.test.js.map +1 -1
  52. package/dist/reporting/github/comment-table-reporter.d.ts.map +1 -1
  53. package/dist/reporting/github/comment-table-reporter.js.map +1 -1
  54. package/dist/reporting/github/job-summary-reporter.d.ts.map +1 -1
  55. package/dist/reporting/github/job-summary-reporter.js.map +1 -1
  56. package/dist/reporting/grouped-reporter.d.ts.map +1 -1
  57. package/dist/reporting/grouped-reporter.js +6 -2
  58. package/dist/reporting/grouped-reporter.js.map +1 -1
  59. package/dist/reporting/index.d.ts +9 -0
  60. package/dist/reporting/index.d.ts.map +1 -1
  61. package/dist/reporting/index.js +12 -1
  62. package/dist/reporting/index.js.map +1 -1
  63. package/dist/reporting/json-file-reporter.d.ts.map +1 -1
  64. package/dist/reporting/json-file-reporter.js +2 -1
  65. package/dist/reporting/json-file-reporter.js.map +1 -1
  66. package/dist/reporting/progress-format.d.ts.map +1 -1
  67. package/dist/reporting/progress-format.js.map +1 -1
  68. package/dist/reporting/reporter.d.ts.map +1 -1
  69. package/dist/reporting/reporter.js.map +1 -1
  70. package/dist/reporting/result-reporter.d.ts +37 -0
  71. package/dist/reporting/result-reporter.d.ts.map +1 -0
  72. package/dist/reporting/result-reporter.js +22 -0
  73. package/dist/reporting/result-reporter.js.map +1 -0
  74. package/dist/reporting/simple-reporter.d.ts.map +1 -1
  75. package/dist/reporting/simple-reporter.js +3 -1
  76. package/dist/reporting/simple-reporter.js.map +1 -1
  77. package/dist/reporting/spinner-reporter.d.ts +1 -0
  78. package/dist/reporting/spinner-reporter.d.ts.map +1 -1
  79. package/dist/reporting/spinner-reporter.js +25 -10
  80. package/dist/reporting/spinner-reporter.js.map +1 -1
  81. package/dist/reporting/spinner-reporter.test.js.map +1 -1
  82. package/dist/reporting/state/live-state-tracker.d.ts.map +1 -1
  83. package/dist/reporting/state/live-state-tracker.js +1 -1
  84. package/dist/reporting/state/live-state-tracker.js.map +1 -1
  85. package/dist/reporting/state/loaded-state-tracker.d.ts.map +1 -1
  86. package/dist/reporting/state/loaded-state-tracker.js.map +1 -1
  87. package/dist/reporting/state/state-tracker.d.ts.map +1 -1
  88. package/dist/reporting/stdout-result-reporter.d.ts +14 -0
  89. package/dist/reporting/stdout-result-reporter.d.ts.map +1 -0
  90. package/dist/reporting/stdout-result-reporter.js +16 -0
  91. package/dist/reporting/stdout-result-reporter.js.map +1 -0
  92. package/dist/reporting/stdout-result-reporter.test.d.ts +2 -0
  93. package/dist/reporting/stdout-result-reporter.test.d.ts.map +1 -0
  94. package/dist/reporting/stdout-result-reporter.test.js +28 -0
  95. package/dist/reporting/stdout-result-reporter.test.js.map +1 -0
  96. package/dist/reporting/summary-table-reporter.d.ts +2 -0
  97. package/dist/reporting/summary-table-reporter.d.ts.map +1 -1
  98. package/dist/reporting/summary-table-reporter.js +44 -33
  99. package/dist/reporting/summary-table-reporter.js.map +1 -1
  100. package/dist/reporting/watch-renderer.d.ts +16 -0
  101. package/dist/reporting/watch-renderer.d.ts.map +1 -0
  102. package/dist/reporting/watch-renderer.js +110 -0
  103. package/dist/reporting/watch-renderer.js.map +1 -0
  104. package/dist/reporting/watch-renderer.test.d.ts +2 -0
  105. package/dist/reporting/watch-renderer.test.d.ts.map +1 -0
  106. package/dist/reporting/watch-renderer.test.js +103 -0
  107. package/dist/reporting/watch-renderer.test.js.map +1 -0
  108. package/dist/reporting/watch-result-reporter.d.ts +21 -0
  109. package/dist/reporting/watch-result-reporter.d.ts.map +1 -0
  110. package/dist/reporting/watch-result-reporter.js +34 -0
  111. package/dist/reporting/watch-result-reporter.js.map +1 -0
  112. package/dist/reporting/watch-result-reporter.test.d.ts +2 -0
  113. package/dist/reporting/watch-result-reporter.test.d.ts.map +1 -0
  114. package/dist/reporting/watch-result-reporter.test.js +37 -0
  115. package/dist/reporting/watch-result-reporter.test.js.map +1 -0
  116. package/package.json +2 -2
  117. package/src/outputHelper.ts +12 -3
  118. package/src/reporting/build-result-reporter.test.ts +48 -0
  119. package/src/reporting/build-result-reporter.ts +58 -0
  120. package/src/reporting/composite-reporter.ts +10 -2
  121. package/src/reporting/composite-result-reporter.test.ts +71 -0
  122. package/src/reporting/composite-result-reporter.ts +58 -0
  123. package/src/reporting/file-result-reporter.test.ts +112 -0
  124. package/src/reporting/file-result-reporter.ts +75 -0
  125. package/src/reporting/format-json.test.ts +34 -0
  126. package/src/reporting/format-json.ts +16 -0
  127. package/src/reporting/format-table.test.ts +113 -0
  128. package/src/reporting/format-table.ts +97 -0
  129. package/src/reporting/github/annotations.test.ts +1 -3
  130. package/src/reporting/github/annotations.ts +13 -12
  131. package/src/reporting/github/comment-table-reporter.ts +5 -5
  132. package/src/reporting/github/job-summary-reporter.ts +3 -1
  133. package/src/reporting/grouped-reporter.ts +6 -2
  134. package/src/reporting/index.ts +34 -1
  135. package/src/reporting/json-file-reporter.ts +2 -1
  136. package/src/reporting/progress-format.ts +5 -2
  137. package/src/reporting/reporter.ts +15 -2
  138. package/src/reporting/result-reporter.ts +40 -0
  139. package/src/reporting/simple-reporter.ts +3 -1
  140. package/src/reporting/spinner-reporter.test.ts +7 -6
  141. package/src/reporting/spinner-reporter.ts +40 -14
  142. package/src/reporting/state/live-state-tracker.ts +12 -6
  143. package/src/reporting/state/loaded-state-tracker.ts +2 -7
  144. package/src/reporting/state/state-tracker.ts +5 -1
  145. package/src/reporting/stdout-result-reporter.test.ts +34 -0
  146. package/src/reporting/stdout-result-reporter.ts +23 -0
  147. package/src/reporting/summary-table-reporter.ts +51 -36
  148. package/src/reporting/watch-renderer.test.ts +127 -0
  149. package/src/reporting/watch-renderer.ts +132 -0
  150. package/src/reporting/watch-result-reporter.test.ts +44 -0
  151. package/src/reporting/watch-result-reporter.ts +45 -0
  152. package/tsconfig.tsbuildinfo +1 -1
@@ -52,10 +52,7 @@ function _escapeProperty(value: string): string {
52
52
 
53
53
  /** Escape a workflow command message (data portion). */
54
54
  function _escapeMessage(value: string): string {
55
- return value
56
- .replace(/%/g, '%25')
57
- .replace(/\r/g, '%0D')
58
- .replace(/\n/g, '%0A');
55
+ return value.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
59
56
  }
60
57
 
61
58
  // ── Annotation emission ─────────────────────────────────────────────────────
@@ -153,12 +150,12 @@ export function formatAnnotationSummaryMarkdown(
153
150
  );
154
151
  }
155
152
  if (summary.notices > 0) {
156
- parts.push(
157
- `${summary.notices} notice${summary.notices !== 1 ? 's' : ''}`
158
- );
153
+ parts.push(`${summary.notices} notice${summary.notices !== 1 ? 's' : ''}`);
159
154
  }
160
155
 
161
- md += `**${summary.total} issue${summary.total !== 1 ? 's' : ''}** across ${summary.fileCount} file${summary.fileCount !== 1 ? 's' : ''}: ${parts.join(', ')}\n\n`;
156
+ md += `**${summary.total} issue${summary.total !== 1 ? 's' : ''}** across ${
157
+ summary.fileCount
158
+ } file${summary.fileCount !== 1 ? 's' : ''}: ${parts.join(', ')}\n\n`;
162
159
 
163
160
  // Group diagnostics by file
164
161
  const byFile = new Map<string, Diagnostic[]>();
@@ -189,14 +186,16 @@ export function formatAnnotationSummaryMarkdown(
189
186
  d.severity === 'error'
190
187
  ? '`error`'
191
188
  : d.severity === 'warning'
192
- ? '`warning`'
193
- : '`notice`';
189
+ ? '`warning`'
190
+ : '`notice`';
194
191
  const escapedMsg = d.message.replace(/\|/g, '\\|').replace(/\n/g, ' ');
195
192
  md += `| ${d.line} | ${sev} | ${escapedMsg} |\n`;
196
193
  }
197
194
 
198
195
  if (diags.length > MAX_PER_FILE) {
199
- md += `\n_... and ${diags.length - MAX_PER_FILE} more issue(s) in this file_\n`;
196
+ md += `\n_... and ${
197
+ diags.length - MAX_PER_FILE
198
+ } more issue(s) in this file_\n`;
200
199
  }
201
200
 
202
201
  md += '\n</details>\n\n';
@@ -226,7 +225,9 @@ export async function writeAnnotationSummaryAsync(
226
225
  OutputHelper.info('Written lint results to GitHub job summary.');
227
226
  } catch (err) {
228
227
  OutputHelper.warn(
229
- `Failed to write job summary: ${err instanceof Error ? err.message : String(err)}`
228
+ `Failed to write job summary: ${
229
+ err instanceof Error ? err.message : String(err)
230
+ }`
230
231
  );
231
232
  }
232
233
  }
@@ -80,14 +80,14 @@ export class GithubCommentTableReporter extends BaseReporter {
80
80
  this._scheduleUpdate();
81
81
  }
82
82
 
83
- override onPackagePhaseChange(
84
- _name: string,
85
- _phase: PackageStatus
86
- ): void {
83
+ override onPackagePhaseChange(_name: string, _phase: PackageStatus): void {
87
84
  this._scheduleUpdate();
88
85
  }
89
86
 
90
- override onPackageProgressUpdate(_name: string, _progress: ProgressSummary): void {
87
+ override onPackageProgressUpdate(
88
+ _name: string,
89
+ _progress: ProgressSummary
90
+ ): void {
91
91
  this._scheduleUpdate();
92
92
  }
93
93
 
@@ -79,7 +79,9 @@ export class GithubJobSummaryReporter extends BaseReporter {
79
79
  OutputHelper.info('Written results to GitHub job summary.');
80
80
  } catch (err) {
81
81
  OutputHelper.warn(
82
- `Failed to write job summary: ${err instanceof Error ? err.message : String(err)}`
82
+ `Failed to write job summary: ${
83
+ err instanceof Error ? err.message : String(err)
84
+ }`
83
85
  );
84
86
  }
85
87
  }
@@ -77,11 +77,15 @@ export class GroupedReporter extends BaseReporter {
77
77
  const empty = isEmptyTestRun(result.progressSummary);
78
78
 
79
79
  if (result.success) {
80
- const label = progressText ? `${successLabel} ${progressText}` : successLabel;
80
+ const label = progressText
81
+ ? `${successLabel} ${progressText}`
82
+ : successLabel;
81
83
  const formatted = empty
82
84
  ? OutputHelper.formatWarning(`${label} ⚠`)
83
85
  : OutputHelper.formatSuccess(label);
84
- const icon = empty ? OutputHelper.formatWarning('⚠') : OutputHelper.formatSuccess('✓');
86
+ const icon = empty
87
+ ? OutputHelper.formatWarning('⚠')
88
+ : OutputHelper.formatSuccess('✓');
85
89
  console.log(
86
90
  ` ${icon} ${formatted} ${OutputHelper.formatDim(`(${duration})`)}`
87
91
  );
@@ -1,4 +1,4 @@
1
- // Core types and base class
1
+ // Core types and base class — batch lifecycle (multi-package, phases, progress).
2
2
  export {
3
3
  BaseReporter,
4
4
  type Reporter,
@@ -12,6 +12,39 @@ export {
12
12
  type StepProgress,
13
13
  } from './reporter.js';
14
14
 
15
+ // Single-result reporter — for one-shot or polled command output.
16
+ export { BaseResultReporter, type ResultReporter } from './result-reporter.js';
17
+ export {
18
+ StdoutResultReporter,
19
+ type StdoutResultReporterOptions,
20
+ } from './stdout-result-reporter.js';
21
+ export {
22
+ FileResultReporter,
23
+ type FileResultReporterOptions,
24
+ } from './file-result-reporter.js';
25
+ export {
26
+ WatchResultReporter,
27
+ type WatchResultReporterOptions,
28
+ } from './watch-result-reporter.js';
29
+ export { CompositeResultReporter } from './composite-result-reporter.js';
30
+ export {
31
+ buildResultReporter,
32
+ type BuildResultReporterOptions,
33
+ } from './build-result-reporter.js';
34
+
35
+ // Output formatting primitives.
36
+ export {
37
+ formatTable,
38
+ type TableColumn,
39
+ type TableOptions,
40
+ } from './format-table.js';
41
+ export { formatJson, type JsonOutputOptions } from './format-json.js';
42
+ export {
43
+ createWatchRenderer,
44
+ type WatchRenderer,
45
+ type WatchRendererOptions,
46
+ } from './watch-renderer.js';
47
+
15
48
  // Progress formatting helpers
16
49
  export {
17
50
  formatProgressInline,
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import { OutputHelper } from '../outputHelper.js';
3
3
  import { BaseReporter } from './reporter.js';
4
+ import { formatJson } from './format-json.js';
4
5
  import { type IStateTracker } from './state/state-tracker.js';
5
6
 
6
7
  /**
@@ -32,7 +33,7 @@ export class JsonFileReporter extends BaseReporter {
32
33
  },
33
34
  };
34
35
 
35
- await fs.writeFile(this._outputPath, JSON.stringify(summary, null, 2));
36
+ await fs.writeFile(this._outputPath, formatJson(summary, { pretty: true }));
36
37
  OutputHelper.info(`Results written to ${this._outputPath}`);
37
38
  }
38
39
  }
@@ -20,7 +20,9 @@ export function formatProgressInline(progress?: ProgressSummary): string {
20
20
  return `(${progress.passed}/${progress.total})`;
21
21
  case 'bytes':
22
22
  if (progress.totalBytes > 0 && progress.transferredBytes > 0) {
23
- return `(${_formatBytes(progress.transferredBytes)}/${_formatBytes(progress.totalBytes)})`;
23
+ return `(${_formatBytes(progress.transferredBytes)}/${_formatBytes(
24
+ progress.totalBytes
25
+ )})`;
24
26
  }
25
27
  return `(${_formatBytes(progress.totalBytes)})`;
26
28
  case 'steps':
@@ -79,7 +81,8 @@ export function summarizeFailure(
79
81
 
80
82
  if (error) {
81
83
  const firstLine = error.split('\n')[0];
82
- const short = firstLine.length > 60 ? firstLine.slice(0, 57) + '...' : firstLine;
84
+ const short =
85
+ firstLine.length > 60 ? firstLine.slice(0, 57) + '...' : firstLine;
83
86
  if (parts.length > 0) {
84
87
  parts.push(`: ${short}`);
85
88
  } else {
@@ -6,7 +6,17 @@
6
6
  */
7
7
 
8
8
  /** Execution phases a package can move through. */
9
- export type JobPhase = 'waiting' | 'building' | 'downloading' | 'merging' | 'combining' | 'uploading' | 'scheduling' | 'launching' | 'connecting' | 'executing';
9
+ export type JobPhase =
10
+ | 'waiting'
11
+ | 'building'
12
+ | 'downloading'
13
+ | 'merging'
14
+ | 'combining'
15
+ | 'uploading'
16
+ | 'scheduling'
17
+ | 'launching'
18
+ | 'connecting'
19
+ | 'executing';
10
20
 
11
21
  /** Unified status for a package moving through the job lifecycle. */
12
22
  export type PackageStatus = 'pending' | JobPhase | 'passed' | 'failed';
@@ -94,7 +104,10 @@ export class BaseReporter implements Reporter {
94
104
  async startAsync(): Promise<void> {}
95
105
  onPackageStart(_packageName: string): void {}
96
106
  onPackagePhaseChange(_packageName: string, _phase: JobPhase): void {}
97
- onPackageProgressUpdate(_packageName: string, _progress: ProgressSummary): void {}
107
+ onPackageProgressUpdate(
108
+ _packageName: string,
109
+ _progress: ProgressSummary
110
+ ): void {}
98
111
  onPackageResult(_result: PackageResult, _bufferedOutput?: string[]): void {}
99
112
  async stopAsync(): Promise<void> {}
100
113
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Single-result reporter — for commands that produce one result (or a polled
3
+ * series of results), as opposed to batch jobs with per-package lifecycle.
4
+ *
5
+ * Use this for any CLI command that:
6
+ * - Runs once and prints a result to stdout
7
+ * - Writes a result to a file via --output
8
+ * - Polls a result on an interval and redraws (--watch)
9
+ *
10
+ * For batch jobs (multi-package, lifecycle phases, progress events), use the
11
+ * Reporter interface in reporter.ts instead.
12
+ */
13
+
14
+ /**
15
+ * Lifecycle hooks for a single-result reporter. Implementations decide how
16
+ * to render the result (stdout, file, watch redraw, etc).
17
+ */
18
+ export interface ResultReporter<T = unknown> {
19
+ /** Called once before any results are reported. */
20
+ startAsync(): Promise<void>;
21
+
22
+ /**
23
+ * Called when a result is available. May be called multiple times for
24
+ * watch mode (each tick produces a fresh result).
25
+ */
26
+ onResult(result: T): void;
27
+
28
+ /** Called once after the final result. */
29
+ stopAsync(): Promise<void>;
30
+ }
31
+
32
+ /**
33
+ * Base class with no-op defaults. Reporters extend this and override only
34
+ * the hooks they need.
35
+ */
36
+ export class BaseResultReporter<T = unknown> implements ResultReporter<T> {
37
+ async startAsync(): Promise<void> {}
38
+ onResult(_result: T): void {}
39
+ async stopAsync(): Promise<void> {}
40
+ }
@@ -40,7 +40,9 @@ export class SimpleReporter extends BaseReporter {
40
40
 
41
41
  const progressText = formatProgressResult(result.progressSummary);
42
42
  if (result.success) {
43
- const msg = progressText ? `${this._successMessage} ${progressText}` : this._successMessage;
43
+ const msg = progressText
44
+ ? `${this._successMessage} ${progressText}`
45
+ : this._successMessage;
44
46
  OutputHelper.info(msg);
45
47
  } else {
46
48
  OutputHelper.error(this._failureMessage);
@@ -13,12 +13,13 @@ function setup() {
13
13
  // Capture everything written to stdout
14
14
  const writes: string[] = [];
15
15
  const realWrite = process.stdout.write.bind(process.stdout);
16
- vi.spyOn(process.stdout, 'write').mockImplementation(
17
- ((chunk: any, ...args: any[]) => {
18
- writes.push(typeof chunk === 'string' ? chunk : chunk.toString());
19
- return true;
20
- }) as any
21
- );
16
+ vi.spyOn(process.stdout, 'write').mockImplementation(((
17
+ chunk: any,
18
+ ...args: any[]
19
+ ) => {
20
+ writes.push(typeof chunk === 'string' ? chunk : chunk.toString());
21
+ return true;
22
+ }) as any);
22
23
 
23
24
  // Suppress console.log (used by startAsync header)
24
25
  vi.spyOn(console, 'log').mockImplementation(() => {});
@@ -2,7 +2,11 @@ import { OutputHelper } from '../outputHelper.js';
2
2
  import { formatDurationMs } from '../cli-utils.js';
3
3
  import { type PackageResult, BaseReporter } from './reporter.js';
4
4
  import { type IStateTracker } from './state/state-tracker.js';
5
- import { formatProgressInline, formatProgressResult, isEmptyTestRun } from './progress-format.js';
5
+ import {
6
+ formatProgressInline,
7
+ formatProgressResult,
8
+ isEmptyTestRun,
9
+ } from './progress-format.js';
6
10
 
7
11
  export interface SpinnerReporterOptions {
8
12
  showLogs: boolean;
@@ -40,6 +44,7 @@ export class SpinnerReporter extends BaseReporter {
40
44
  private _spinnerFrame: number = 0;
41
45
  private _extraLines = 0;
42
46
  private _originalStdoutWrite: typeof process.stdout.write | undefined;
47
+ private _originalStderrWrite: typeof process.stderr.write | undefined;
43
48
  private _isRendering = false;
44
49
 
45
50
  constructor(state: IStateTracker, options: SpinnerReporterOptions) {
@@ -59,16 +64,25 @@ export class SpinnerReporter extends BaseReporter {
59
64
  process.stdout.write('\x1b[?25l');
60
65
  this._renderedLineCount = 0;
61
66
 
62
- // Intercept stdout to track external writes that shift the cursor
67
+ // Intercept stdout/stderr to track external writes that shift the cursor.
68
+ // Both streams visually consume rows in the same terminal, so both must
69
+ // count toward the cursor-rewind math.
63
70
  this._originalStdoutWrite = process.stdout.write.bind(process.stdout);
71
+ this._originalStderrWrite = process.stderr.write.bind(process.stderr);
64
72
  const self = this;
73
+ const countNewlines = (chunk: any): void => {
74
+ if (self._isRendering) return;
75
+ const str = typeof chunk === 'string' ? chunk : chunk.toString();
76
+ self._extraLines += (str.match(/\n/g) || []).length;
77
+ };
65
78
  process.stdout.write = function (chunk: any, ...args: any[]) {
66
- if (!self._isRendering) {
67
- const str = typeof chunk === 'string' ? chunk : chunk.toString();
68
- self._extraLines += (str.match(/\n/g) || []).length;
69
- }
79
+ countNewlines(chunk);
70
80
  return self._originalStdoutWrite!.call(process.stdout, chunk, ...args);
71
81
  } as any;
82
+ process.stderr.write = function (chunk: any, ...args: any[]) {
83
+ countNewlines(chunk);
84
+ return self._originalStderrWrite!.call(process.stderr, chunk, ...args);
85
+ } as any;
72
86
 
73
87
  this._render();
74
88
  this._renderInterval = setInterval(() => {
@@ -84,11 +98,15 @@ export class SpinnerReporter extends BaseReporter {
84
98
  }
85
99
  this._render();
86
100
 
87
- // Restore original stdout.write
101
+ // Restore original stdout.write / stderr.write
88
102
  if (this._originalStdoutWrite) {
89
103
  process.stdout.write = this._originalStdoutWrite;
90
104
  this._originalStdoutWrite = undefined;
91
105
  }
106
+ if (this._originalStderrWrite) {
107
+ process.stderr.write = this._originalStderrWrite;
108
+ this._originalStderrWrite = undefined;
109
+ }
92
110
 
93
111
  process.stdout.write('\x1b[?25h');
94
112
  console.log('');
@@ -122,8 +140,8 @@ export class SpinnerReporter extends BaseReporter {
122
140
  ? OutputHelper.formatSuccess('✓')
123
141
  : OutputHelper.formatError('✗');
124
142
  const status = result.success
125
- ? (this._options.successLabel ?? 'Passed')
126
- : (this._options.failureLabel ?? 'FAILED');
143
+ ? this._options.successLabel ?? 'Passed'
144
+ : this._options.failureLabel ?? 'FAILED';
127
145
  const formatted = result.success
128
146
  ? OutputHelper.formatSuccess(status)
129
147
  : OutputHelper.formatError(status);
@@ -172,10 +190,14 @@ export class SpinnerReporter extends BaseReporter {
172
190
  ? `${phaseLabel} ${progressText}`
173
191
  : phaseLabel;
174
192
  const statusText = OutputHelper.formatInfo(plain.padEnd(22));
175
- line = ` ${icon} ${state.name.padEnd(30)} ${statusText} ${OutputHelper.formatDim(time)}`;
193
+ line = ` ${icon} ${state.name.padEnd(
194
+ 30
195
+ )} ${statusText} ${OutputHelper.formatDim(time)}`;
176
196
  } else if (state.status === 'passed') {
177
197
  const icon = OutputHelper.formatSuccess('✓');
178
- const progressText = formatProgressResult(state.result?.progressSummary);
198
+ const progressText = formatProgressResult(
199
+ state.result?.progressSummary
200
+ );
179
201
  const label = this._options.successLabel ?? 'Passed';
180
202
  const empty = isEmptyTestRun(state.result?.progressSummary);
181
203
  let plain = progressText ? `${label} ${progressText}` : label;
@@ -183,15 +205,19 @@ export class SpinnerReporter extends BaseReporter {
183
205
  const statusText = empty
184
206
  ? OutputHelper.formatWarning(plain.padEnd(22))
185
207
  : OutputHelper.formatSuccess(plain.padEnd(22));
186
- line = ` ${icon} ${state.name.padEnd(30)} ${statusText} ${OutputHelper.formatDim(time)}`;
208
+ line = ` ${icon} ${state.name.padEnd(
209
+ 30
210
+ )} ${statusText} ${OutputHelper.formatDim(time)}`;
187
211
  } else {
188
212
  const icon = OutputHelper.formatError('✗');
189
213
  const failedPhase = state.result?.failedPhase;
190
214
  const plain = failedPhase
191
215
  ? `${this._options.failureLabel ?? 'FAILED'} at ${failedPhase}`
192
- : (this._options.failureLabel ?? 'FAILED');
216
+ : this._options.failureLabel ?? 'FAILED';
193
217
  const statusText = OutputHelper.formatError(plain.padEnd(22));
194
- line = ` ${icon} ${state.name.padEnd(30)} ${statusText} ${OutputHelper.formatDim(time)}`;
218
+ line = ` ${icon} ${state.name.padEnd(
219
+ 30
220
+ )} ${statusText} ${OutputHelper.formatDim(time)}`;
195
221
  }
196
222
 
197
223
  lines.push(line);
@@ -1,4 +1,10 @@
1
- import { type PackageResult, type PackageStatus, type JobPhase, type ProgressSummary, BaseReporter } from '../reporter.js';
1
+ import {
2
+ type PackageResult,
3
+ type PackageStatus,
4
+ type JobPhase,
5
+ type ProgressSummary,
6
+ BaseReporter,
7
+ } from '../reporter.js';
2
8
  import { type IStateTracker, type PackageState } from './state-tracker.js';
3
9
 
4
10
  export type { PackageState } from './state-tracker.js';
@@ -8,10 +14,7 @@ export type { PackageState } from './state-tracker.js';
8
14
  * Extends BaseReporter to receive lifecycle hooks and mutate state.
9
15
  * Reporters read from it via the IStateTracker interface.
10
16
  */
11
- export class LiveStateTracker
12
- extends BaseReporter
13
- implements IStateTracker
14
- {
17
+ export class LiveStateTracker extends BaseReporter implements IStateTracker {
15
18
  private _packages: Map<string, PackageState>;
16
19
  private _startTimeMs = 0;
17
20
  private _completed = 0;
@@ -77,7 +80,10 @@ export class LiveStateTracker
77
80
  state.progress = undefined; // clear progress on phase transition
78
81
  }
79
82
 
80
- override onPackageProgressUpdate(name: string, progress: ProgressSummary): void {
83
+ override onPackageProgressUpdate(
84
+ name: string,
85
+ progress: ProgressSummary
86
+ ): void {
81
87
  const state = this._packages.get(name);
82
88
  if (!state) return;
83
89
  state.progress = progress;
@@ -4,10 +4,7 @@ import {
4
4
  type PackageStatus,
5
5
  type BatchSummary,
6
6
  } from '../reporter.js';
7
- import {
8
- type IStateTracker,
9
- type PackageState,
10
- } from './state-tracker.js';
7
+ import { type IStateTracker, type PackageState } from './state-tracker.js';
11
8
 
12
9
  /**
13
10
  * Batch state loaded from a previously-saved BatchSummary JSON file.
@@ -31,9 +28,7 @@ export class LoadedStateTracker implements IStateTracker {
31
28
  this._startTimeMs = startTimeMs;
32
29
  }
33
30
 
34
- static async fromFileAsync(
35
- filePath: string
36
- ): Promise<LoadedStateTracker> {
31
+ static async fromFileAsync(filePath: string): Promise<LoadedStateTracker> {
37
32
  const raw = await fs.readFile(filePath, 'utf-8');
38
33
  const summary = JSON.parse(raw) as BatchSummary;
39
34
  return LoadedStateTracker.fromSummary(summary);
@@ -1,4 +1,8 @@
1
- import { type PackageResult, type PackageStatus, type ProgressSummary } from '../reporter.js';
1
+ import {
2
+ type PackageResult,
3
+ type PackageStatus,
4
+ type ProgressSummary,
5
+ } from '../reporter.js';
2
6
 
3
7
  export interface PackageState {
4
8
  name: string;
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { StdoutResultReporter } from './stdout-result-reporter.js';
3
+
4
+ describe('StdoutResultReporter', () => {
5
+ let logSpy: ReturnType<typeof vi.spyOn>;
6
+
7
+ beforeEach(() => {
8
+ logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
9
+ });
10
+
11
+ afterEach(() => {
12
+ logSpy.mockRestore();
13
+ });
14
+
15
+ it('writes rendered result to stdout on each onResult', () => {
16
+ const reporter = new StdoutResultReporter<{ name: string }>({
17
+ render: (r) => `name=${r.name}`,
18
+ });
19
+
20
+ reporter.onResult({ name: 'first' });
21
+ reporter.onResult({ name: 'second' });
22
+
23
+ expect(logSpy).toHaveBeenCalledTimes(2);
24
+ expect(logSpy).toHaveBeenNthCalledWith(1, 'name=first');
25
+ expect(logSpy).toHaveBeenNthCalledWith(2, 'name=second');
26
+ });
27
+
28
+ it('startAsync and stopAsync are no-ops', async () => {
29
+ const reporter = new StdoutResultReporter<unknown>({ render: () => '' });
30
+ await reporter.startAsync();
31
+ await reporter.stopAsync();
32
+ expect(logSpy).not.toHaveBeenCalled();
33
+ });
34
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Writes a rendered result to stdout. One-shot — calls render(result) and
3
+ * console.log(...) on each onResult.
4
+ */
5
+
6
+ import { BaseResultReporter } from './result-reporter.js';
7
+
8
+ export interface StdoutResultReporterOptions<T> {
9
+ render: (result: T) => string;
10
+ }
11
+
12
+ export class StdoutResultReporter<T = unknown> extends BaseResultReporter<T> {
13
+ private _render: (result: T) => string;
14
+
15
+ constructor(options: StdoutResultReporterOptions<T>) {
16
+ super();
17
+ this._render = options.render;
18
+ }
19
+
20
+ override onResult(result: T): void {
21
+ console.log(this._render(result));
22
+ }
23
+ }
@@ -1,6 +1,7 @@
1
1
  import { OutputHelper } from '../outputHelper.js';
2
2
  import { formatDurationMs } from '../cli-utils.js';
3
- import { BaseReporter } from './reporter.js';
3
+ import { BaseReporter, type PackageResult } from './reporter.js';
4
+ import { formatTable, type TableColumn } from './format-table.js';
4
5
  import { type IStateTracker } from './state/state-tracker.js';
5
6
  import { formatProgressResult, isEmptyTestRun } from './progress-format.js';
6
7
 
@@ -39,44 +40,30 @@ export class SummaryTableReporter extends BaseReporter {
39
40
  const passed = results.length - failures.length;
40
41
  const durationMs = Date.now() - this._state.startTimeMs;
41
42
 
42
- const STATUS_WIDTH = 26;
43
-
44
- console.log('');
45
- console.log('Package'.padEnd(40) + 'Status'.padEnd(STATUS_WIDTH) + 'Duration');
46
- console.log('─'.repeat(40 + STATUS_WIDTH + 8));
47
-
48
43
  let emptyRunCount = 0;
49
- for (const result of results) {
50
- const progressText = formatProgressResult(result.progressSummary);
51
- const empty = isEmptyTestRun(result.progressSummary);
52
- if (empty) emptyRunCount++;
53
44
 
54
- let label: string;
55
- if (result.success) {
56
- label = progressText ? `${this._successLabel} ${progressText}` : this._successLabel;
57
- } else {
58
- const failedPhase = result.failedPhase;
59
- label = failedPhase
60
- ? `${this._failureLabel} at ${failedPhase}`
61
- : this._failureLabel;
62
- }
45
+ const columns: TableColumn<PackageResult>[] = [
46
+ {
47
+ header: 'Package',
48
+ value: (r) => r.packageName,
49
+ minWidth: 40,
50
+ },
51
+ {
52
+ header: 'Status',
53
+ value: (r) => this._statusLabel(r),
54
+ format: (label, r) =>
55
+ this._colorStatus(label, r, () => emptyRunCount++),
56
+ minWidth: 26,
57
+ },
58
+ {
59
+ header: 'Duration',
60
+ value: (r) => formatDurationMs(r.durationMs),
61
+ format: (v) => OutputHelper.formatDim(v),
62
+ },
63
+ ];
63
64
 
64
- // Pad the plain text BEFORE wrapping in ANSI so padEnd counts visible chars
65
- const paddedLabel = label.padEnd(STATUS_WIDTH);
66
- let status: string;
67
- if (result.success) {
68
- status = empty
69
- ? OutputHelper.formatWarning(paddedLabel)
70
- : OutputHelper.formatSuccess(paddedLabel);
71
- } else {
72
- status = OutputHelper.formatError(paddedLabel);
73
- }
74
-
75
- const duration = OutputHelper.formatDim(
76
- formatDurationMs(result.durationMs)
77
- );
78
- console.log(result.packageName.padEnd(40) + status + duration);
79
- }
65
+ console.log('');
66
+ console.log(formatTable(results, columns));
80
67
 
81
68
  console.log('');
82
69
  const passedText = OutputHelper.formatSuccess(`${passed} passed`);
@@ -99,4 +86,32 @@ export class SummaryTableReporter extends BaseReporter {
99
86
  );
100
87
  }
101
88
  }
89
+
90
+ private _statusLabel(result: PackageResult): string {
91
+ if (result.success) {
92
+ const progressText = formatProgressResult(result.progressSummary);
93
+ return progressText
94
+ ? `${this._successLabel} ${progressText}`
95
+ : this._successLabel;
96
+ }
97
+ const failedPhase = result.failedPhase;
98
+ return failedPhase
99
+ ? `${this._failureLabel} at ${failedPhase}`
100
+ : this._failureLabel;
101
+ }
102
+
103
+ private _colorStatus(
104
+ label: string,
105
+ result: PackageResult,
106
+ countEmpty: () => void
107
+ ): string {
108
+ if (result.success) {
109
+ const empty = isEmptyTestRun(result.progressSummary);
110
+ if (empty) countEmpty();
111
+ return empty
112
+ ? OutputHelper.formatWarning(label)
113
+ : OutputHelper.formatSuccess(label);
114
+ }
115
+ return OutputHelper.formatError(label);
116
+ }
102
117
  }