@quenty/cli-output-helpers 1.9.0 → 1.10.1

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 (62) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/reporting/composite-reporter.d.ts +3 -1
  3. package/dist/reporting/composite-reporter.d.ts.map +1 -1
  4. package/dist/reporting/composite-reporter.js +9 -0
  5. package/dist/reporting/composite-reporter.js.map +1 -1
  6. package/dist/reporting/github/comment-table-reporter.d.ts +2 -1
  7. package/dist/reporting/github/comment-table-reporter.d.ts.map +1 -1
  8. package/dist/reporting/github/comment-table-reporter.js +3 -0
  9. package/dist/reporting/github/comment-table-reporter.js.map +1 -1
  10. package/dist/reporting/github/formatting.d.ts +3 -3
  11. package/dist/reporting/github/formatting.d.ts.map +1 -1
  12. package/dist/reporting/github/formatting.js +31 -16
  13. package/dist/reporting/github/formatting.js.map +1 -1
  14. package/dist/reporting/grouped-reporter.d.ts.map +1 -1
  15. package/dist/reporting/grouped-reporter.js +14 -2
  16. package/dist/reporting/grouped-reporter.js.map +1 -1
  17. package/dist/reporting/index.d.ts +2 -1
  18. package/dist/reporting/index.d.ts.map +1 -1
  19. package/dist/reporting/index.js +2 -0
  20. package/dist/reporting/index.js.map +1 -1
  21. package/dist/reporting/progress-format.d.ts +35 -0
  22. package/dist/reporting/progress-format.d.ts.map +1 -0
  23. package/dist/reporting/progress-format.js +88 -0
  24. package/dist/reporting/progress-format.js.map +1 -0
  25. package/dist/reporting/reporter.d.ts +27 -1
  26. package/dist/reporting/reporter.d.ts.map +1 -1
  27. package/dist/reporting/reporter.js +1 -0
  28. package/dist/reporting/reporter.js.map +1 -1
  29. package/dist/reporting/simple-reporter.d.ts.map +1 -1
  30. package/dist/reporting/simple-reporter.js +4 -1
  31. package/dist/reporting/simple-reporter.js.map +1 -1
  32. package/dist/reporting/spinner-reporter.d.ts.map +1 -1
  33. package/dist/reporting/spinner-reporter.js +25 -6
  34. package/dist/reporting/spinner-reporter.js.map +1 -1
  35. package/dist/reporting/state/live-state-tracker.d.ts +3 -1
  36. package/dist/reporting/state/live-state-tracker.d.ts.map +1 -1
  37. package/dist/reporting/state/live-state-tracker.js +15 -0
  38. package/dist/reporting/state/live-state-tracker.js.map +1 -1
  39. package/dist/reporting/state/loaded-state-tracker.d.ts +2 -1
  40. package/dist/reporting/state/loaded-state-tracker.d.ts.map +1 -1
  41. package/dist/reporting/state/loaded-state-tracker.js +4 -0
  42. package/dist/reporting/state/loaded-state-tracker.js.map +1 -1
  43. package/dist/reporting/state/state-tracker.d.ts +3 -1
  44. package/dist/reporting/state/state-tracker.d.ts.map +1 -1
  45. package/dist/reporting/summary-table-reporter.d.ts.map +1 -1
  46. package/dist/reporting/summary-table-reporter.js +34 -6
  47. package/dist/reporting/summary-table-reporter.js.map +1 -1
  48. package/package.json +2 -2
  49. package/src/reporting/composite-reporter.ts +12 -1
  50. package/src/reporting/github/comment-table-reporter.ts +5 -0
  51. package/src/reporting/github/formatting.ts +43 -17
  52. package/src/reporting/grouped-reporter.ts +15 -4
  53. package/src/reporting/index.ts +12 -0
  54. package/src/reporting/progress-format.ts +97 -0
  55. package/src/reporting/reporter.ts +34 -1
  56. package/src/reporting/simple-reporter.ts +4 -1
  57. package/src/reporting/spinner-reporter.ts +24 -12
  58. package/src/reporting/state/live-state-tracker.ts +16 -1
  59. package/src/reporting/state/loaded-state-tracker.ts +6 -0
  60. package/src/reporting/state/state-tracker.ts +3 -1
  61. package/src/reporting/summary-table-reporter.ts +40 -6
  62. package/tsconfig.tsbuildinfo +1 -1
@@ -6,8 +6,20 @@ export {
6
6
  type PackageStatus,
7
7
  type PackageResult,
8
8
  type BatchSummary,
9
+ type ProgressSummary,
10
+ type TestCountProgress,
11
+ type ByteProgress,
12
+ type StepProgress,
9
13
  } from './reporter.js';
10
14
 
15
+ // Progress formatting helpers
16
+ export {
17
+ formatProgressInline,
18
+ formatProgressResult,
19
+ isEmptyTestRun,
20
+ summarizeFailure,
21
+ } from './progress-format.js';
22
+
11
23
  // State tracking
12
24
  export {
13
25
  type IStateTracker,
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Formatting helpers for ProgressSummary values.
3
+ */
4
+
5
+ import { type ProgressSummary, type JobPhase } from './reporter.js';
6
+
7
+ /**
8
+ * Format progress for inline display in spinners and running status.
9
+ * Returns empty string when progress is undefined.
10
+ *
11
+ * - test-counts: "(5/23)"
12
+ * - bytes: "12.3 MB" or "45%"
13
+ * - steps: "(3/10)"
14
+ */
15
+ export function formatProgressInline(progress?: ProgressSummary): string {
16
+ if (!progress) return '';
17
+
18
+ switch (progress.kind) {
19
+ case 'test-counts':
20
+ return `(${progress.passed}/${progress.total})`;
21
+ case 'bytes':
22
+ if (progress.totalBytes > 0 && progress.transferredBytes > 0) {
23
+ return `(${_formatBytes(progress.transferredBytes)}/${_formatBytes(progress.totalBytes)})`;
24
+ }
25
+ return `(${_formatBytes(progress.totalBytes)})`;
26
+ case 'steps':
27
+ if (progress.total > 0) {
28
+ return `(${progress.completed}/${progress.total})`;
29
+ }
30
+ // Indeterminate: show label or just the count
31
+ return progress.label ? `(${progress.label})` : `(${progress.completed})`;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Format progress for final result display (passed/failed lines).
37
+ * Returns empty string when progress is undefined.
38
+ *
39
+ * - test-counts: "(23/100)" or "(0/0)"
40
+ * - bytes: "(12.3 MB)"
41
+ * - steps: "(3/10)"
42
+ */
43
+ export function formatProgressResult(progress?: ProgressSummary): string {
44
+ if (!progress) return '';
45
+
46
+ switch (progress.kind) {
47
+ case 'test-counts':
48
+ return `(${progress.passed}/${progress.total})`;
49
+ case 'bytes':
50
+ return `(${_formatBytes(progress.totalBytes)})`;
51
+ case 'steps':
52
+ return `(${progress.completed}/${progress.total})`;
53
+ }
54
+ }
55
+
56
+ /** True when progress is test-counts with total === 0. */
57
+ export function isEmptyTestRun(progress?: ProgressSummary): boolean {
58
+ return progress?.kind === 'test-counts' && progress.total === 0;
59
+ }
60
+
61
+ /**
62
+ * Condense a raw error string and optional failedPhase into a short one-liner.
63
+ *
64
+ * Examples:
65
+ * summarizeFailure("Upload failed: 409 Conflict: {...}", "uploading")
66
+ * → "at uploading: Upload failed (409)"
67
+ * summarizeFailure("timeout after 120s", "executing")
68
+ * → "at executing: timeout after 120s"
69
+ */
70
+ export function summarizeFailure(
71
+ error?: string,
72
+ failedPhase?: JobPhase
73
+ ): string {
74
+ const parts: string[] = [];
75
+
76
+ if (failedPhase) {
77
+ parts.push(`at ${failedPhase}`);
78
+ }
79
+
80
+ if (error) {
81
+ const firstLine = error.split('\n')[0];
82
+ const short = firstLine.length > 60 ? firstLine.slice(0, 57) + '...' : firstLine;
83
+ if (parts.length > 0) {
84
+ parts.push(`: ${short}`);
85
+ } else {
86
+ parts.push(short);
87
+ }
88
+ }
89
+
90
+ return parts.join('');
91
+ }
92
+
93
+ function _formatBytes(bytes: number): string {
94
+ if (bytes < 1024) return `${bytes} B`;
95
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
96
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
97
+ }
@@ -6,11 +6,38 @@
6
6
  */
7
7
 
8
8
  /** Execution phases a package can move through. */
9
- export type JobPhase = 'building' | 'downloading' | 'merging' | 'uploading' | 'scheduling' | 'launching' | 'connecting' | 'executing';
9
+ export type JobPhase = 'waiting' | 'building' | 'downloading' | 'merging' | 'combining' | 'uploading' | 'scheduling' | 'launching' | 'connecting' | 'executing';
10
10
 
11
11
  /** Unified status for a package moving through the job lifecycle. */
12
12
  export type PackageStatus = 'pending' | JobPhase | 'passed' | 'failed';
13
13
 
14
+ // ── Progress summary types ─────────────────────────────────────────────────
15
+
16
+ /** Test count progress: "23/100 passed" */
17
+ export interface TestCountProgress {
18
+ kind: 'test-counts';
19
+ passed: number;
20
+ failed: number;
21
+ total: number;
22
+ }
23
+
24
+ /** Byte-level transfer progress: "45% of 12.3 MB" */
25
+ export interface ByteProgress {
26
+ kind: 'bytes';
27
+ transferredBytes: number;
28
+ totalBytes: number;
29
+ }
30
+
31
+ /** Generic step progress: "3/10 packages" */
32
+ export interface StepProgress {
33
+ kind: 'steps';
34
+ completed: number;
35
+ total: number;
36
+ label?: string;
37
+ }
38
+
39
+ export type ProgressSummary = TestCountProgress | ByteProgress | StepProgress;
40
+
14
41
  /** Result for a single package in a batch run. */
15
42
  export interface PackageResult {
16
43
  packageName: string;
@@ -18,6 +45,8 @@ export interface PackageResult {
18
45
  logs: string;
19
46
  durationMs: number;
20
47
  error?: string;
48
+ progressSummary?: ProgressSummary;
49
+ failedPhase?: JobPhase;
21
50
  }
22
51
 
23
52
  /** Summary of a complete batch run. */
@@ -47,6 +76,9 @@ export interface Reporter {
47
76
  /** Called when a package transitions phases (building, uploading, executing, etc). */
48
77
  onPackagePhaseChange(packageName: string, phase: JobPhase): void;
49
78
 
79
+ /** Called when a package's in-progress metrics update (high-frequency). */
80
+ onPackageProgressUpdate(packageName: string, progress: ProgressSummary): void;
81
+
50
82
  /** Called when a single package job completes. */
51
83
  onPackageResult(result: PackageResult, bufferedOutput?: string[]): void;
52
84
 
@@ -62,6 +94,7 @@ export class BaseReporter implements Reporter {
62
94
  async startAsync(): Promise<void> {}
63
95
  onPackageStart(_packageName: string): void {}
64
96
  onPackagePhaseChange(_packageName: string, _phase: JobPhase): void {}
97
+ onPackageProgressUpdate(_packageName: string, _progress: ProgressSummary): void {}
65
98
  onPackageResult(_result: PackageResult, _bufferedOutput?: string[]): void {}
66
99
  async stopAsync(): Promise<void> {}
67
100
  }
@@ -1,6 +1,7 @@
1
1
  import { OutputHelper } from '../outputHelper.js';
2
2
  import { type PackageResult, BaseReporter } from './reporter.js';
3
3
  import { type IStateTracker } from './state/state-tracker.js';
4
+ import { formatProgressResult } from './progress-format.js';
4
5
 
5
6
  export interface SimpleReporterOptions {
6
7
  alwaysShowLogs: boolean;
@@ -37,8 +38,10 @@ export class SimpleReporter extends BaseReporter {
37
38
  OutputHelper.info('(no output)');
38
39
  }
39
40
 
41
+ const progressText = formatProgressResult(result.progressSummary);
40
42
  if (result.success) {
41
- OutputHelper.info(this._successMessage);
43
+ const msg = progressText ? `${this._successMessage} ${progressText}` : this._successMessage;
44
+ OutputHelper.info(msg);
42
45
  } else {
43
46
  OutputHelper.error(this._failureMessage);
44
47
  }
@@ -2,6 +2,7 @@ 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
6
 
6
7
  export interface SpinnerReporterOptions {
7
8
  showLogs: boolean;
@@ -17,7 +18,9 @@ const SPINNER_FRAMES = ['◐', '◓', '◑', '◒'];
17
18
 
18
19
  /** Emoji + label for each active phase in the spinner. */
19
20
  const PHASE_LABELS: Record<string, string> = {
21
+ waiting: '◇ Waiting',
20
22
  building: '⚙ Building',
23
+ combining: '🔗 Combining',
21
24
  uploading: '▲ Uploading',
22
25
  scheduling: '◇ Scheduling',
23
26
  launching: '🚀 Launching',
@@ -164,22 +167,31 @@ export class SpinnerReporter extends BaseReporter {
164
167
  )} ${statusText}`;
165
168
  } else if (phaseLabel) {
166
169
  const icon = OutputHelper.formatInfo(spinner);
167
- const statusText = OutputHelper.formatInfo(phaseLabel);
168
- line = ` ${icon} ${state.name.padEnd(30)} ${statusText.padEnd(
169
- 22
170
- )} ${OutputHelper.formatDim(time)}`;
170
+ const progressText = formatProgressInline(state.progress);
171
+ const plain = progressText
172
+ ? `${phaseLabel} ${progressText}`
173
+ : phaseLabel;
174
+ const statusText = OutputHelper.formatInfo(plain.padEnd(22));
175
+ line = ` ${icon} ${state.name.padEnd(30)} ${statusText} ${OutputHelper.formatDim(time)}`;
171
176
  } else if (state.status === 'passed') {
172
177
  const icon = OutputHelper.formatSuccess('✓');
173
- const statusText = OutputHelper.formatSuccess(this._options.successLabel ?? 'Passed');
174
- line = ` ${icon} ${state.name.padEnd(30)} ${statusText.padEnd(
175
- 20
176
- )} ${OutputHelper.formatDim(time)}`;
178
+ const progressText = formatProgressResult(state.result?.progressSummary);
179
+ const label = this._options.successLabel ?? 'Passed';
180
+ const empty = isEmptyTestRun(state.result?.progressSummary);
181
+ let plain = progressText ? `${label} ${progressText}` : label;
182
+ if (empty) plain += ' ⚠';
183
+ const statusText = empty
184
+ ? OutputHelper.formatWarning(plain.padEnd(22))
185
+ : OutputHelper.formatSuccess(plain.padEnd(22));
186
+ line = ` ${icon} ${state.name.padEnd(30)} ${statusText} ${OutputHelper.formatDim(time)}`;
177
187
  } else {
178
188
  const icon = OutputHelper.formatError('✗');
179
- const statusText = OutputHelper.formatError(this._options.failureLabel ?? 'FAILED');
180
- line = ` ${icon} ${state.name.padEnd(30)} ${statusText.padEnd(
181
- 20
182
- )} ${OutputHelper.formatDim(time)}`;
189
+ const failedPhase = state.result?.failedPhase;
190
+ const plain = failedPhase
191
+ ? `${this._options.failureLabel ?? 'FAILED'} at ${failedPhase}`
192
+ : (this._options.failureLabel ?? 'FAILED');
193
+ const statusText = OutputHelper.formatError(plain.padEnd(22));
194
+ line = ` ${icon} ${state.name.padEnd(30)} ${statusText} ${OutputHelper.formatDim(time)}`;
183
195
  }
184
196
 
185
197
  lines.push(line);
@@ -1,4 +1,4 @@
1
- import { type PackageResult, type JobPhase, BaseReporter } from '../reporter.js';
1
+ import { type PackageResult, type PackageStatus, type JobPhase, type ProgressSummary, BaseReporter } from '../reporter.js';
2
2
  import { type IStateTracker, type PackageState } from './state-tracker.js';
3
3
 
4
4
  export type { PackageState } from './state-tracker.js';
@@ -54,6 +54,10 @@ export class LiveStateTracker
54
54
  return this._failures;
55
55
  }
56
56
 
57
+ getCurrentPhase(name: string): PackageStatus | undefined {
58
+ return this._packages.get(name)?.status;
59
+ }
60
+
57
61
  override async startAsync(): Promise<void> {
58
62
  this._startTimeMs = Date.now();
59
63
  }
@@ -68,7 +72,15 @@ export class LiveStateTracker
68
72
  override onPackagePhaseChange(name: string, phase: JobPhase): void {
69
73
  const state = this._packages.get(name);
70
74
  if (!state) return;
75
+ if (state.status === 'passed' || state.status === 'failed') return; // don't regress terminal states
71
76
  state.status = phase;
77
+ state.progress = undefined; // clear progress on phase transition
78
+ }
79
+
80
+ override onPackageProgressUpdate(name: string, progress: ProgressSummary): void {
81
+ const state = this._packages.get(name);
82
+ if (!state) return;
83
+ state.progress = progress;
72
84
  }
73
85
 
74
86
  override onPackageResult(
@@ -82,6 +94,9 @@ export class LiveStateTracker
82
94
  state.durationMs = result.durationMs;
83
95
  state.result = result;
84
96
  state.bufferedOutput = bufferedOutput;
97
+ if (result.progressSummary) {
98
+ state.progress = result.progressSummary;
99
+ }
85
100
  this._completed++;
86
101
 
87
102
  this._allResults.push(result);
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import {
3
3
  type PackageResult,
4
+ type PackageStatus,
4
5
  type BatchSummary,
5
6
  } from '../reporter.js';
6
7
  import {
@@ -48,6 +49,7 @@ export class LoadedStateTracker implements IStateTracker {
48
49
  status: result.success ? 'passed' : 'failed',
49
50
  durationMs: result.durationMs,
50
51
  result,
52
+ progress: result.progressSummary,
51
53
  });
52
54
  if (!result.success) {
53
55
  failures.push(result);
@@ -92,4 +94,8 @@ export class LoadedStateTracker implements IStateTracker {
92
94
  getFailures(): PackageResult[] {
93
95
  return this._failures;
94
96
  }
97
+
98
+ getCurrentPhase(name: string): PackageStatus | undefined {
99
+ return this._packages.get(name)?.status;
100
+ }
95
101
  }
@@ -1,4 +1,4 @@
1
- import { type PackageResult, type PackageStatus } from '../reporter.js';
1
+ import { type PackageResult, type PackageStatus, type ProgressSummary } from '../reporter.js';
2
2
 
3
3
  export interface PackageState {
4
4
  name: string;
@@ -7,6 +7,7 @@ export interface PackageState {
7
7
  durationMs?: number;
8
8
  result?: PackageResult;
9
9
  bufferedOutput?: string[];
10
+ progress?: ProgressSummary;
10
11
  }
11
12
 
12
13
  /**
@@ -21,4 +22,5 @@ export interface IStateTracker {
21
22
  getAllPackages(): PackageState[];
22
23
  getResults(): PackageResult[];
23
24
  getFailures(): PackageResult[];
25
+ getCurrentPhase(name: string): PackageStatus | undefined;
24
26
  }
@@ -2,6 +2,7 @@ import { OutputHelper } from '../outputHelper.js';
2
2
  import { formatDurationMs } from '../cli-utils.js';
3
3
  import { BaseReporter } from './reporter.js';
4
4
  import { type IStateTracker } from './state/state-tracker.js';
5
+ import { formatProgressResult, isEmptyTestRun } from './progress-format.js';
5
6
 
6
7
  export interface SummaryTableReporterOptions {
7
8
  /** Label for successful results in the table. Default: "Passed" */
@@ -38,18 +39,43 @@ export class SummaryTableReporter extends BaseReporter {
38
39
  const passed = results.length - failures.length;
39
40
  const durationMs = Date.now() - this._state.startTimeMs;
40
41
 
42
+ const STATUS_WIDTH = 26;
43
+
41
44
  console.log('');
42
- console.log('Package'.padEnd(40) + 'Status'.padEnd(10) + 'Duration');
43
- console.log('-'.repeat(60));
45
+ console.log('Package'.padEnd(40) + 'Status'.padEnd(STATUS_WIDTH) + 'Duration');
46
+ console.log(''.repeat(40 + STATUS_WIDTH + 8));
44
47
 
48
+ let emptyRunCount = 0;
45
49
  for (const result of results) {
46
- const status = result.success
47
- ? OutputHelper.formatSuccess(this._successLabel)
48
- : OutputHelper.formatError(this._failureLabel);
50
+ const progressText = formatProgressResult(result.progressSummary);
51
+ const empty = isEmptyTestRun(result.progressSummary);
52
+ if (empty) emptyRunCount++;
53
+
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
+ }
63
+
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
+
49
75
  const duration = OutputHelper.formatDim(
50
76
  formatDurationMs(result.durationMs)
51
77
  );
52
- console.log(result.packageName.padEnd(40) + status.padEnd(20) + duration);
78
+ console.log(result.packageName.padEnd(40) + status + duration);
53
79
  }
54
80
 
55
81
  console.log('');
@@ -64,5 +90,13 @@ export class SummaryTableReporter extends BaseReporter {
64
90
  console.log(
65
91
  `${results.length} ${this._summaryVerb}, ${passedText}, ${failedText} ${totalTime}`
66
92
  );
93
+
94
+ if (emptyRunCount > 0) {
95
+ console.log(
96
+ OutputHelper.formatWarning(
97
+ `⚠ ${emptyRunCount} package(s) ran 0 tests — check test discovery`
98
+ )
99
+ );
100
+ }
67
101
  }
68
102
  }