@quenty/cli-output-helpers 1.10.0 → 1.11.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 (155) hide show
  1. package/CHANGELOG.md +28 -96
  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/formatting.d.ts.map +1 -1
  55. package/dist/reporting/github/formatting.js +9 -14
  56. package/dist/reporting/github/formatting.js.map +1 -1
  57. package/dist/reporting/github/job-summary-reporter.d.ts.map +1 -1
  58. package/dist/reporting/github/job-summary-reporter.js.map +1 -1
  59. package/dist/reporting/grouped-reporter.d.ts.map +1 -1
  60. package/dist/reporting/grouped-reporter.js +6 -2
  61. package/dist/reporting/grouped-reporter.js.map +1 -1
  62. package/dist/reporting/index.d.ts +9 -0
  63. package/dist/reporting/index.d.ts.map +1 -1
  64. package/dist/reporting/index.js +12 -1
  65. package/dist/reporting/index.js.map +1 -1
  66. package/dist/reporting/json-file-reporter.d.ts.map +1 -1
  67. package/dist/reporting/json-file-reporter.js +2 -1
  68. package/dist/reporting/json-file-reporter.js.map +1 -1
  69. package/dist/reporting/progress-format.d.ts.map +1 -1
  70. package/dist/reporting/progress-format.js.map +1 -1
  71. package/dist/reporting/reporter.d.ts.map +1 -1
  72. package/dist/reporting/reporter.js.map +1 -1
  73. package/dist/reporting/result-reporter.d.ts +37 -0
  74. package/dist/reporting/result-reporter.d.ts.map +1 -0
  75. package/dist/reporting/result-reporter.js +22 -0
  76. package/dist/reporting/result-reporter.js.map +1 -0
  77. package/dist/reporting/simple-reporter.d.ts.map +1 -1
  78. package/dist/reporting/simple-reporter.js +3 -1
  79. package/dist/reporting/simple-reporter.js.map +1 -1
  80. package/dist/reporting/spinner-reporter.d.ts.map +1 -1
  81. package/dist/reporting/spinner-reporter.js +4 -4
  82. package/dist/reporting/spinner-reporter.js.map +1 -1
  83. package/dist/reporting/spinner-reporter.test.js.map +1 -1
  84. package/dist/reporting/state/live-state-tracker.d.ts.map +1 -1
  85. package/dist/reporting/state/live-state-tracker.js +1 -1
  86. package/dist/reporting/state/live-state-tracker.js.map +1 -1
  87. package/dist/reporting/state/loaded-state-tracker.d.ts.map +1 -1
  88. package/dist/reporting/state/loaded-state-tracker.js.map +1 -1
  89. package/dist/reporting/state/state-tracker.d.ts.map +1 -1
  90. package/dist/reporting/stdout-result-reporter.d.ts +14 -0
  91. package/dist/reporting/stdout-result-reporter.d.ts.map +1 -0
  92. package/dist/reporting/stdout-result-reporter.js +16 -0
  93. package/dist/reporting/stdout-result-reporter.js.map +1 -0
  94. package/dist/reporting/stdout-result-reporter.test.d.ts +2 -0
  95. package/dist/reporting/stdout-result-reporter.test.d.ts.map +1 -0
  96. package/dist/reporting/stdout-result-reporter.test.js +28 -0
  97. package/dist/reporting/stdout-result-reporter.test.js.map +1 -0
  98. package/dist/reporting/summary-table-reporter.d.ts +2 -0
  99. package/dist/reporting/summary-table-reporter.d.ts.map +1 -1
  100. package/dist/reporting/summary-table-reporter.js +44 -33
  101. package/dist/reporting/summary-table-reporter.js.map +1 -1
  102. package/dist/reporting/watch-renderer.d.ts +16 -0
  103. package/dist/reporting/watch-renderer.d.ts.map +1 -0
  104. package/dist/reporting/watch-renderer.js +110 -0
  105. package/dist/reporting/watch-renderer.js.map +1 -0
  106. package/dist/reporting/watch-renderer.test.d.ts +2 -0
  107. package/dist/reporting/watch-renderer.test.d.ts.map +1 -0
  108. package/dist/reporting/watch-renderer.test.js +103 -0
  109. package/dist/reporting/watch-renderer.test.js.map +1 -0
  110. package/dist/reporting/watch-result-reporter.d.ts +21 -0
  111. package/dist/reporting/watch-result-reporter.d.ts.map +1 -0
  112. package/dist/reporting/watch-result-reporter.js +34 -0
  113. package/dist/reporting/watch-result-reporter.js.map +1 -0
  114. package/dist/reporting/watch-result-reporter.test.d.ts +2 -0
  115. package/dist/reporting/watch-result-reporter.test.d.ts.map +1 -0
  116. package/dist/reporting/watch-result-reporter.test.js +37 -0
  117. package/dist/reporting/watch-result-reporter.test.js.map +1 -0
  118. package/package.json +2 -2
  119. package/src/outputHelper.ts +12 -3
  120. package/src/reporting/build-result-reporter.test.ts +48 -0
  121. package/src/reporting/build-result-reporter.ts +58 -0
  122. package/src/reporting/composite-reporter.ts +10 -2
  123. package/src/reporting/composite-result-reporter.test.ts +71 -0
  124. package/src/reporting/composite-result-reporter.ts +58 -0
  125. package/src/reporting/file-result-reporter.test.ts +112 -0
  126. package/src/reporting/file-result-reporter.ts +75 -0
  127. package/src/reporting/format-json.test.ts +34 -0
  128. package/src/reporting/format-json.ts +16 -0
  129. package/src/reporting/format-table.test.ts +113 -0
  130. package/src/reporting/format-table.ts +97 -0
  131. package/src/reporting/github/annotations.test.ts +1 -3
  132. package/src/reporting/github/annotations.ts +13 -12
  133. package/src/reporting/github/comment-table-reporter.ts +5 -5
  134. package/src/reporting/github/formatting.ts +18 -16
  135. package/src/reporting/github/job-summary-reporter.ts +3 -1
  136. package/src/reporting/grouped-reporter.ts +6 -2
  137. package/src/reporting/index.ts +34 -1
  138. package/src/reporting/json-file-reporter.ts +2 -1
  139. package/src/reporting/progress-format.ts +5 -2
  140. package/src/reporting/reporter.ts +15 -2
  141. package/src/reporting/result-reporter.ts +40 -0
  142. package/src/reporting/simple-reporter.ts +3 -1
  143. package/src/reporting/spinner-reporter.test.ts +7 -6
  144. package/src/reporting/spinner-reporter.ts +20 -8
  145. package/src/reporting/state/live-state-tracker.ts +12 -6
  146. package/src/reporting/state/loaded-state-tracker.ts +2 -7
  147. package/src/reporting/state/state-tracker.ts +5 -1
  148. package/src/reporting/stdout-result-reporter.test.ts +34 -0
  149. package/src/reporting/stdout-result-reporter.ts +23 -0
  150. package/src/reporting/summary-table-reporter.ts +51 -36
  151. package/src/reporting/watch-renderer.test.ts +127 -0
  152. package/src/reporting/watch-renderer.ts +132 -0
  153. package/src/reporting/watch-result-reporter.test.ts +44 -0
  154. package/src/reporting/watch-result-reporter.ts +45 -0
  155. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { createWatchRenderer } from './watch-renderer.js';
3
+
4
+ describe('createWatchRenderer', () => {
5
+ let writes: string[];
6
+
7
+ beforeEach(() => {
8
+ vi.useFakeTimers();
9
+ writes = [];
10
+ vi.spyOn(process.stdout, 'write').mockImplementation(((chunk: any) => {
11
+ writes.push(typeof chunk === 'string' ? chunk : chunk.toString());
12
+ return true;
13
+ }) as any);
14
+ });
15
+
16
+ afterEach(() => {
17
+ vi.useRealTimers();
18
+ vi.restoreAllMocks();
19
+ });
20
+
21
+ it('start renders immediately and stop clears interval with final render', () => {
22
+ let count = 0;
23
+ const renderer = createWatchRenderer(() => `frame-${count++}`, {
24
+ rewrite: false,
25
+ intervalMs: 1000,
26
+ });
27
+
28
+ renderer.start();
29
+ expect(writes).toEqual(['frame-0\n']);
30
+
31
+ renderer.stop();
32
+ // stop does a final render
33
+ expect(writes).toEqual(['frame-0\n', 'frame-1\n']);
34
+
35
+ // After stop, no more renders should happen
36
+ writes.length = 0;
37
+ vi.advanceTimersByTime(3000);
38
+ expect(writes).toEqual([]);
39
+ });
40
+
41
+ it('update forces immediate render and resets interval', () => {
42
+ let count = 0;
43
+ const renderer = createWatchRenderer(() => `frame-${count++}`, {
44
+ rewrite: false,
45
+ intervalMs: 1000,
46
+ });
47
+
48
+ renderer.start(); // frame-0
49
+ expect(writes).toEqual(['frame-0\n']);
50
+
51
+ // Advance 500ms, then force update
52
+ vi.advanceTimersByTime(500);
53
+ renderer.update(); // frame-1 (forced)
54
+ expect(writes).toEqual(['frame-0\n', 'frame-1\n']);
55
+
56
+ // Advance 500ms more — the old interval would have fired at 1000ms total,
57
+ // but update() reset it, so nothing fires yet
58
+ vi.advanceTimersByTime(500);
59
+ expect(writes).toEqual(['frame-0\n', 'frame-1\n']);
60
+
61
+ // Advance another 500ms (1000ms since update), new interval fires
62
+ vi.advanceTimersByTime(500);
63
+ expect(writes).toEqual(['frame-0\n', 'frame-1\n', 'frame-2\n']);
64
+
65
+ renderer.stop();
66
+ });
67
+
68
+ it('non-TTY mode only writes when content changes', () => {
69
+ let value = 'same';
70
+ const renderer = createWatchRenderer(() => value, {
71
+ rewrite: false,
72
+ intervalMs: 100,
73
+ });
74
+
75
+ renderer.start(); // writes "same"
76
+ expect(writes).toEqual(['same\n']);
77
+
78
+ // Same content on next interval — should NOT write
79
+ vi.advanceTimersByTime(100);
80
+ expect(writes).toEqual(['same\n']);
81
+
82
+ // Change content — should write
83
+ value = 'different';
84
+ vi.advanceTimersByTime(100);
85
+ expect(writes).toEqual(['same\n', 'different\n']);
86
+
87
+ renderer.stop();
88
+ });
89
+
90
+ it('stop clears the interval so no more renders happen', () => {
91
+ let count = 0;
92
+ const renderer = createWatchRenderer(() => `f-${count++}`, {
93
+ rewrite: false,
94
+ intervalMs: 100,
95
+ });
96
+
97
+ renderer.start(); // f-0
98
+ renderer.stop(); // f-1 (final)
99
+
100
+ writes.length = 0;
101
+ vi.advanceTimersByTime(1000);
102
+ expect(writes).toEqual([]);
103
+ });
104
+
105
+ it('TTY rewrite mode hides/shows cursor and uses escape codes', () => {
106
+ let count = 0;
107
+ const renderer = createWatchRenderer(() => `line-${count++}`, {
108
+ rewrite: true,
109
+ intervalMs: 100,
110
+ });
111
+
112
+ renderer.start();
113
+ // Should have written hide-cursor + first frame
114
+ expect(writes[0]).toBe('\x1b[?25l');
115
+ expect(writes[1]).toBe('line-0\n');
116
+
117
+ // Advance to trigger second render — should include cursor-up + clear
118
+ vi.advanceTimersByTime(100);
119
+ const cursorUpWrite = writes.find((w) => w.includes('\x1b[1A\x1b[J'));
120
+ expect(cursorUpWrite).toBeDefined();
121
+
122
+ renderer.stop();
123
+ // Should have written show-cursor
124
+ const lastWrite = writes[writes.length - 1];
125
+ expect(lastWrite).toBe('\x1b[?25h');
126
+ });
127
+ });
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Live-updating renderer for watch/monitoring commands. In TTY mode,
3
+ * rewrites output in-place using cursor control. In non-TTY mode,
4
+ * appends new output only when it changes.
5
+ */
6
+
7
+ export interface WatchRendererOptions {
8
+ intervalMs?: number;
9
+ rewrite?: boolean;
10
+ }
11
+
12
+ export interface WatchRenderer {
13
+ start(): void;
14
+ update(): void;
15
+ stop(): void;
16
+ }
17
+
18
+ export function createWatchRenderer(
19
+ render: () => string,
20
+ options?: WatchRendererOptions
21
+ ): WatchRenderer {
22
+ const intervalMs = options?.intervalMs ?? 1000;
23
+ const rewrite = options?.rewrite ?? (process.stdout.isTTY ? true : false);
24
+
25
+ let _intervalHandle: ReturnType<typeof setInterval> | null = null;
26
+ let _previousOutput: string = '';
27
+ let _previousLineCount: number = 0;
28
+
29
+ function _render(): void {
30
+ const output = render();
31
+
32
+ if (rewrite) {
33
+ // TTY rewrite mode: move cursor up and clear previous output
34
+ if (_previousLineCount > 0) {
35
+ process.stdout.write(`\x1b[${_previousLineCount}A\x1b[J`);
36
+ }
37
+ process.stdout.write(output + '\n');
38
+ _previousLineCount = output.split('\n').length;
39
+ } else {
40
+ // Non-TTY append mode: only write when content changes
41
+ if (output !== _previousOutput) {
42
+ process.stdout.write(output + '\n');
43
+ }
44
+ }
45
+
46
+ _previousOutput = output;
47
+ }
48
+
49
+ function _startInterval(): void {
50
+ _intervalHandle = setInterval(_render, intervalMs);
51
+ }
52
+
53
+ function _clearInterval(): void {
54
+ if (_intervalHandle !== null) {
55
+ clearInterval(_intervalHandle);
56
+ _intervalHandle = null;
57
+ }
58
+ }
59
+
60
+ let _cursorHidden = false;
61
+ let _signalsBound = false;
62
+
63
+ function _showCursor(): void {
64
+ if (_cursorHidden) {
65
+ _cursorHidden = false;
66
+ process.stdout.write('\x1b[?25h');
67
+ }
68
+ }
69
+
70
+ function _onSignal(signal: NodeJS.Signals): void {
71
+ _clearInterval();
72
+ _showCursor();
73
+ // Restore default behavior so the process actually exits
74
+ process.removeListener('SIGINT', _onSignal);
75
+ process.removeListener('SIGTERM', _onSignal);
76
+ process.kill(process.pid, signal);
77
+ }
78
+
79
+ function _bindSignals(): void {
80
+ if (_signalsBound) return;
81
+ _signalsBound = true;
82
+ process.once('SIGINT', _onSignal);
83
+ process.once('SIGTERM', _onSignal);
84
+ }
85
+
86
+ function _unbindSignals(): void {
87
+ if (!_signalsBound) return;
88
+ _signalsBound = false;
89
+ process.removeListener('SIGINT', _onSignal);
90
+ process.removeListener('SIGTERM', _onSignal);
91
+ }
92
+
93
+ return {
94
+ start(): void {
95
+ if (rewrite) {
96
+ process.stdout.write('\x1b[?25l'); // hide cursor
97
+ _cursorHidden = true;
98
+ _bindSignals();
99
+ }
100
+ try {
101
+ _render();
102
+ } catch (err) {
103
+ _showCursor();
104
+ _unbindSignals();
105
+ throw err;
106
+ }
107
+ _startInterval();
108
+ },
109
+
110
+ update(): void {
111
+ _clearInterval();
112
+ try {
113
+ _render();
114
+ } catch (err) {
115
+ _showCursor();
116
+ _unbindSignals();
117
+ throw err;
118
+ }
119
+ _startInterval();
120
+ },
121
+
122
+ stop(): void {
123
+ _clearInterval();
124
+ try {
125
+ _render();
126
+ } finally {
127
+ _showCursor();
128
+ _unbindSignals();
129
+ }
130
+ },
131
+ };
132
+ }
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { WatchResultReporter } from './watch-result-reporter.js';
3
+
4
+ describe('WatchResultReporter', () => {
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ let writeSpy: any;
7
+ const originalIsTTY = process.stdout.isTTY;
8
+
9
+ beforeEach(() => {
10
+ writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
11
+ vi.useFakeTimers();
12
+ });
13
+
14
+ afterEach(() => {
15
+ vi.useRealTimers();
16
+ writeSpy.mockRestore();
17
+ process.stdout.isTTY = originalIsTTY;
18
+ });
19
+
20
+ it('starts the renderer on the first onResult and updates on subsequent calls', () => {
21
+ process.stdout.isTTY = false;
22
+ const renderFn = vi.fn((r: { v: number }) => `v=${r.v}`);
23
+ const reporter = new WatchResultReporter<{ v: number }>({
24
+ render: renderFn,
25
+ intervalMs: 1000,
26
+ });
27
+
28
+ reporter.onResult({ v: 1 });
29
+ expect(renderFn).toHaveBeenCalledWith({ v: 1 });
30
+ expect(writeSpy).toHaveBeenCalled();
31
+
32
+ reporter.onResult({ v: 2 });
33
+ expect(renderFn).toHaveBeenCalledWith({ v: 2 });
34
+ });
35
+
36
+ it('stopAsync without any onResult does not start the renderer', async () => {
37
+ process.stdout.isTTY = false;
38
+ const renderFn = vi.fn(() => '');
39
+ const reporter = new WatchResultReporter<unknown>({ render: renderFn });
40
+
41
+ await reporter.stopAsync();
42
+ expect(renderFn).not.toHaveBeenCalled();
43
+ });
44
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Live-redraw reporter for watch-mode commands. Wraps the WatchRenderer —
3
+ * stores the latest result and redraws via cursor manipulation.
4
+ *
5
+ * The first onResult starts the underlying renderer; subsequent calls update
6
+ * it. stopAsync stops the renderer and restores the cursor.
7
+ */
8
+
9
+ import { BaseResultReporter } from './result-reporter.js';
10
+ import { createWatchRenderer, type WatchRenderer } from './watch-renderer.js';
11
+
12
+ export interface WatchResultReporterOptions<T> {
13
+ render: (result: T) => string;
14
+ intervalMs?: number;
15
+ }
16
+
17
+ export class WatchResultReporter<T = unknown> extends BaseResultReporter<T> {
18
+ private _renderer: WatchRenderer;
19
+ private _latest: T | undefined;
20
+ private _started = false;
21
+
22
+ constructor(options: WatchResultReporterOptions<T>) {
23
+ super();
24
+ this._renderer = createWatchRenderer(
25
+ () => (this._latest === undefined ? '' : options.render(this._latest)),
26
+ { intervalMs: options.intervalMs }
27
+ );
28
+ }
29
+
30
+ override onResult(result: T): void {
31
+ this._latest = result;
32
+ if (!this._started) {
33
+ this._started = true;
34
+ this._renderer.start();
35
+ } else {
36
+ this._renderer.update();
37
+ }
38
+ }
39
+
40
+ override async stopAsync(): Promise<void> {
41
+ if (this._started) {
42
+ this._renderer.stop();
43
+ }
44
+ }
45
+ }