modestbench 0.0.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 (275) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE.md +55 -0
  3. package/README.md +699 -0
  4. package/dist/bootstrap.cjs +37 -0
  5. package/dist/bootstrap.cjs.map +1 -0
  6. package/dist/bootstrap.d.cts +17 -0
  7. package/dist/bootstrap.d.cts.map +1 -0
  8. package/dist/bootstrap.d.ts +17 -0
  9. package/dist/bootstrap.d.ts.map +1 -0
  10. package/dist/bootstrap.js +33 -0
  11. package/dist/bootstrap.js.map +1 -0
  12. package/dist/cli/commands/history.cjs +459 -0
  13. package/dist/cli/commands/history.cjs.map +1 -0
  14. package/dist/cli/commands/history.d.cts +34 -0
  15. package/dist/cli/commands/history.d.cts.map +1 -0
  16. package/dist/cli/commands/history.d.ts +34 -0
  17. package/dist/cli/commands/history.d.ts.map +1 -0
  18. package/dist/cli/commands/history.js +422 -0
  19. package/dist/cli/commands/history.js.map +1 -0
  20. package/dist/cli/commands/init.cjs +566 -0
  21. package/dist/cli/commands/init.cjs.map +1 -0
  22. package/dist/cli/commands/init.d.cts +26 -0
  23. package/dist/cli/commands/init.d.cts.map +1 -0
  24. package/dist/cli/commands/init.d.ts +26 -0
  25. package/dist/cli/commands/init.d.ts.map +1 -0
  26. package/dist/cli/commands/init.js +562 -0
  27. package/dist/cli/commands/init.js.map +1 -0
  28. package/dist/cli/commands/run.cjs +285 -0
  29. package/dist/cli/commands/run.cjs.map +1 -0
  30. package/dist/cli/commands/run.d.cts +37 -0
  31. package/dist/cli/commands/run.d.cts.map +1 -0
  32. package/dist/cli/commands/run.d.ts +37 -0
  33. package/dist/cli/commands/run.d.ts.map +1 -0
  34. package/dist/cli/commands/run.js +248 -0
  35. package/dist/cli/commands/run.js.map +1 -0
  36. package/dist/cli/index.cjs +523 -0
  37. package/dist/cli/index.cjs.map +1 -0
  38. package/dist/cli/index.d.cts +58 -0
  39. package/dist/cli/index.d.cts.map +1 -0
  40. package/dist/cli/index.d.ts +58 -0
  41. package/dist/cli/index.d.ts.map +1 -0
  42. package/dist/cli/index.js +515 -0
  43. package/dist/cli/index.js.map +1 -0
  44. package/dist/config/manager.cjs +370 -0
  45. package/dist/config/manager.cjs.map +1 -0
  46. package/dist/config/manager.d.cts +46 -0
  47. package/dist/config/manager.d.cts.map +1 -0
  48. package/dist/config/manager.d.ts +46 -0
  49. package/dist/config/manager.d.ts.map +1 -0
  50. package/dist/config/manager.js +333 -0
  51. package/dist/config/manager.js.map +1 -0
  52. package/dist/config/schema.cjs +182 -0
  53. package/dist/config/schema.cjs.map +1 -0
  54. package/dist/config/schema.d.cts +51 -0
  55. package/dist/config/schema.d.cts.map +1 -0
  56. package/dist/config/schema.d.ts +51 -0
  57. package/dist/config/schema.d.ts.map +1 -0
  58. package/dist/config/schema.js +145 -0
  59. package/dist/config/schema.js.map +1 -0
  60. package/dist/constants.cjs +22 -0
  61. package/dist/constants.cjs.map +1 -0
  62. package/dist/constants.d.cts +10 -0
  63. package/dist/constants.d.cts.map +1 -0
  64. package/dist/constants.d.ts +10 -0
  65. package/dist/constants.d.ts.map +1 -0
  66. package/dist/constants.js +19 -0
  67. package/dist/constants.js.map +1 -0
  68. package/dist/core/benchmark-schema.cjs +135 -0
  69. package/dist/core/benchmark-schema.cjs.map +1 -0
  70. package/dist/core/benchmark-schema.d.cts +139 -0
  71. package/dist/core/benchmark-schema.d.cts.map +1 -0
  72. package/dist/core/benchmark-schema.d.ts +139 -0
  73. package/dist/core/benchmark-schema.d.ts.map +1 -0
  74. package/dist/core/benchmark-schema.js +132 -0
  75. package/dist/core/benchmark-schema.js.map +1 -0
  76. package/dist/core/engine.cjs +669 -0
  77. package/dist/core/engine.cjs.map +1 -0
  78. package/dist/core/engine.d.cts +128 -0
  79. package/dist/core/engine.d.cts.map +1 -0
  80. package/dist/core/engine.d.ts +128 -0
  81. package/dist/core/engine.d.ts.map +1 -0
  82. package/dist/core/engine.js +632 -0
  83. package/dist/core/engine.js.map +1 -0
  84. package/dist/core/engines/accurate-engine.cjs +292 -0
  85. package/dist/core/engines/accurate-engine.cjs.map +1 -0
  86. package/dist/core/engines/accurate-engine.d.cts +63 -0
  87. package/dist/core/engines/accurate-engine.d.cts.map +1 -0
  88. package/dist/core/engines/accurate-engine.d.ts +63 -0
  89. package/dist/core/engines/accurate-engine.d.ts.map +1 -0
  90. package/dist/core/engines/accurate-engine.js +288 -0
  91. package/dist/core/engines/accurate-engine.js.map +1 -0
  92. package/dist/core/engines/index.cjs +21 -0
  93. package/dist/core/engines/index.cjs.map +1 -0
  94. package/dist/core/engines/index.d.cts +16 -0
  95. package/dist/core/engines/index.d.cts.map +1 -0
  96. package/dist/core/engines/index.d.ts +16 -0
  97. package/dist/core/engines/index.d.ts.map +1 -0
  98. package/dist/core/engines/index.js +16 -0
  99. package/dist/core/engines/index.js.map +1 -0
  100. package/dist/core/engines/tinybench-engine.cjs +286 -0
  101. package/dist/core/engines/tinybench-engine.cjs.map +1 -0
  102. package/dist/core/engines/tinybench-engine.d.cts +18 -0
  103. package/dist/core/engines/tinybench-engine.d.cts.map +1 -0
  104. package/dist/core/engines/tinybench-engine.d.ts +18 -0
  105. package/dist/core/engines/tinybench-engine.d.ts.map +1 -0
  106. package/dist/core/engines/tinybench-engine.js +282 -0
  107. package/dist/core/engines/tinybench-engine.js.map +1 -0
  108. package/dist/core/error-manager.cjs +303 -0
  109. package/dist/core/error-manager.cjs.map +1 -0
  110. package/dist/core/error-manager.d.cts +77 -0
  111. package/dist/core/error-manager.d.cts.map +1 -0
  112. package/dist/core/error-manager.d.ts +77 -0
  113. package/dist/core/error-manager.d.ts.map +1 -0
  114. package/dist/core/error-manager.js +299 -0
  115. package/dist/core/error-manager.js.map +1 -0
  116. package/dist/core/loader.cjs +287 -0
  117. package/dist/core/loader.cjs.map +1 -0
  118. package/dist/core/loader.d.cts +55 -0
  119. package/dist/core/loader.d.cts.map +1 -0
  120. package/dist/core/loader.d.ts +55 -0
  121. package/dist/core/loader.d.ts.map +1 -0
  122. package/dist/core/loader.js +250 -0
  123. package/dist/core/loader.js.map +1 -0
  124. package/dist/core/stats-utils.cjs +99 -0
  125. package/dist/core/stats-utils.cjs.map +1 -0
  126. package/dist/core/stats-utils.d.cts +50 -0
  127. package/dist/core/stats-utils.d.cts.map +1 -0
  128. package/dist/core/stats-utils.d.ts +50 -0
  129. package/dist/core/stats-utils.d.ts.map +1 -0
  130. package/dist/core/stats-utils.js +94 -0
  131. package/dist/core/stats-utils.js.map +1 -0
  132. package/dist/index.cjs +64 -0
  133. package/dist/index.cjs.map +1 -0
  134. package/dist/index.d.cts +22 -0
  135. package/dist/index.d.cts.map +1 -0
  136. package/dist/index.d.ts +22 -0
  137. package/dist/index.d.ts.map +1 -0
  138. package/dist/index.js +30 -0
  139. package/dist/index.js.map +1 -0
  140. package/dist/progress/manager.cjs +325 -0
  141. package/dist/progress/manager.cjs.map +1 -0
  142. package/dist/progress/manager.d.cts +125 -0
  143. package/dist/progress/manager.d.cts.map +1 -0
  144. package/dist/progress/manager.d.ts +125 -0
  145. package/dist/progress/manager.d.ts.map +1 -0
  146. package/dist/progress/manager.js +321 -0
  147. package/dist/progress/manager.js.map +1 -0
  148. package/dist/reporters/csv.cjs +250 -0
  149. package/dist/reporters/csv.cjs.map +1 -0
  150. package/dist/reporters/csv.d.cts +92 -0
  151. package/dist/reporters/csv.d.cts.map +1 -0
  152. package/dist/reporters/csv.d.ts +92 -0
  153. package/dist/reporters/csv.d.ts.map +1 -0
  154. package/dist/reporters/csv.js +246 -0
  155. package/dist/reporters/csv.js.map +1 -0
  156. package/dist/reporters/human.cjs +516 -0
  157. package/dist/reporters/human.cjs.map +1 -0
  158. package/dist/reporters/human.d.cts +86 -0
  159. package/dist/reporters/human.d.cts.map +1 -0
  160. package/dist/reporters/human.d.ts +86 -0
  161. package/dist/reporters/human.d.ts.map +1 -0
  162. package/dist/reporters/human.js +509 -0
  163. package/dist/reporters/human.js.map +1 -0
  164. package/dist/reporters/index.cjs +17 -0
  165. package/dist/reporters/index.cjs.map +1 -0
  166. package/dist/reporters/index.d.cts +10 -0
  167. package/dist/reporters/index.d.cts.map +1 -0
  168. package/dist/reporters/index.d.ts +10 -0
  169. package/dist/reporters/index.d.ts.map +1 -0
  170. package/dist/reporters/index.js +10 -0
  171. package/dist/reporters/index.js.map +1 -0
  172. package/dist/reporters/json.cjs +215 -0
  173. package/dist/reporters/json.cjs.map +1 -0
  174. package/dist/reporters/json.d.cts +79 -0
  175. package/dist/reporters/json.d.cts.map +1 -0
  176. package/dist/reporters/json.d.ts +79 -0
  177. package/dist/reporters/json.d.ts.map +1 -0
  178. package/dist/reporters/json.js +211 -0
  179. package/dist/reporters/json.js.map +1 -0
  180. package/dist/reporters/registry.cjs +255 -0
  181. package/dist/reporters/registry.cjs.map +1 -0
  182. package/dist/reporters/registry.d.cts +155 -0
  183. package/dist/reporters/registry.d.cts.map +1 -0
  184. package/dist/reporters/registry.d.ts +155 -0
  185. package/dist/reporters/registry.d.ts.map +1 -0
  186. package/dist/reporters/registry.js +249 -0
  187. package/dist/reporters/registry.js.map +1 -0
  188. package/dist/reporters/simple.cjs +328 -0
  189. package/dist/reporters/simple.cjs.map +1 -0
  190. package/dist/reporters/simple.d.cts +51 -0
  191. package/dist/reporters/simple.d.cts.map +1 -0
  192. package/dist/reporters/simple.d.ts +51 -0
  193. package/dist/reporters/simple.d.ts.map +1 -0
  194. package/dist/reporters/simple.js +321 -0
  195. package/dist/reporters/simple.js.map +1 -0
  196. package/dist/schema/modestbench-config.schema.json +162 -0
  197. package/dist/storage/history.cjs +456 -0
  198. package/dist/storage/history.cjs.map +1 -0
  199. package/dist/storage/history.d.cts +99 -0
  200. package/dist/storage/history.d.cts.map +1 -0
  201. package/dist/storage/history.d.ts +99 -0
  202. package/dist/storage/history.d.ts.map +1 -0
  203. package/dist/storage/history.js +452 -0
  204. package/dist/storage/history.js.map +1 -0
  205. package/dist/types/cli.cjs +21 -0
  206. package/dist/types/cli.cjs.map +1 -0
  207. package/dist/types/cli.d.cts +296 -0
  208. package/dist/types/cli.d.cts.map +1 -0
  209. package/dist/types/cli.d.ts +296 -0
  210. package/dist/types/cli.d.ts.map +1 -0
  211. package/dist/types/cli.js +18 -0
  212. package/dist/types/cli.js.map +1 -0
  213. package/dist/types/core.cjs +14 -0
  214. package/dist/types/core.cjs.map +1 -0
  215. package/dist/types/core.d.cts +380 -0
  216. package/dist/types/core.d.cts.map +1 -0
  217. package/dist/types/core.d.ts +380 -0
  218. package/dist/types/core.d.ts.map +1 -0
  219. package/dist/types/core.js +13 -0
  220. package/dist/types/core.js.map +1 -0
  221. package/dist/types/index.cjs +27 -0
  222. package/dist/types/index.cjs.map +1 -0
  223. package/dist/types/index.d.cts +11 -0
  224. package/dist/types/index.d.cts.map +1 -0
  225. package/dist/types/index.d.ts +11 -0
  226. package/dist/types/index.d.ts.map +1 -0
  227. package/dist/types/index.js +11 -0
  228. package/dist/types/index.js.map +1 -0
  229. package/dist/types/interfaces.cjs +10 -0
  230. package/dist/types/interfaces.cjs.map +1 -0
  231. package/dist/types/interfaces.d.cts +381 -0
  232. package/dist/types/interfaces.d.cts.map +1 -0
  233. package/dist/types/interfaces.d.ts +381 -0
  234. package/dist/types/interfaces.d.ts.map +1 -0
  235. package/dist/types/interfaces.js +9 -0
  236. package/dist/types/interfaces.js.map +1 -0
  237. package/dist/types/utility.cjs +92 -0
  238. package/dist/types/utility.cjs.map +1 -0
  239. package/dist/types/utility.d.cts +330 -0
  240. package/dist/types/utility.d.cts.map +1 -0
  241. package/dist/types/utility.d.ts +330 -0
  242. package/dist/types/utility.d.ts.map +1 -0
  243. package/dist/types/utility.js +78 -0
  244. package/dist/types/utility.js.map +1 -0
  245. package/package.json +211 -0
  246. package/src/bootstrap.ts +35 -0
  247. package/src/cli/commands/history.ts +569 -0
  248. package/src/cli/commands/init.ts +658 -0
  249. package/src/cli/commands/run.ts +346 -0
  250. package/src/cli/index.ts +642 -0
  251. package/src/config/manager.ts +387 -0
  252. package/src/config/schema.ts +188 -0
  253. package/src/constants.ts +21 -0
  254. package/src/core/benchmark-schema.ts +185 -0
  255. package/src/core/engine.ts +888 -0
  256. package/src/core/engines/accurate-engine.ts +408 -0
  257. package/src/core/engines/index.ts +16 -0
  258. package/src/core/engines/tinybench-engine.ts +335 -0
  259. package/src/core/error-manager.ts +372 -0
  260. package/src/core/loader.ts +324 -0
  261. package/src/core/stats-utils.ts +135 -0
  262. package/src/index.ts +46 -0
  263. package/src/progress/manager.ts +415 -0
  264. package/src/reporters/csv.ts +368 -0
  265. package/src/reporters/human.ts +707 -0
  266. package/src/reporters/index.ts +10 -0
  267. package/src/reporters/json.ts +302 -0
  268. package/src/reporters/registry.ts +349 -0
  269. package/src/reporters/simple.ts +459 -0
  270. package/src/storage/history.ts +600 -0
  271. package/src/types/cli.ts +312 -0
  272. package/src/types/core.ts +414 -0
  273. package/src/types/index.ts +18 -0
  274. package/src/types/interfaces.ts +451 -0
  275. package/src/types/utility.ts +446 -0
@@ -0,0 +1,707 @@
1
+ /**
2
+ * ModestBench Human-Readable Console Reporter
3
+ *
4
+ * Provides colorized, progressive output for terminal environments. Displays
5
+ * real-time progress, results, and formatted statistics.
6
+ */
7
+
8
+ import path from 'node:path';
9
+
10
+ import type {
11
+ BenchmarkRun,
12
+ FileResult,
13
+ ProgressState,
14
+ SuiteResult,
15
+ TaskResult,
16
+ } from '../types/index.js';
17
+
18
+ import { BaseReporter } from './registry.js';
19
+
20
+ /**
21
+ * ANSI color codes for terminal output
22
+ */
23
+ const colors = {
24
+ bold: '\x1b[1m',
25
+ brightBlue: '\x1b[94m',
26
+ brightCyan: '\x1b[96m',
27
+ brightMagenta: '\x1b[95m',
28
+ brightRed: '\x1b[91m',
29
+ brightWhite: '\x1b[97m',
30
+ cyan: '\x1b[36m',
31
+ dim: '\x1b[2m',
32
+ gray: '\x1b[90m',
33
+ green: '\x1b[32m',
34
+ magenta: '\x1b[35m',
35
+ red: '\x1b[31m',
36
+ reset: '\x1b[0m',
37
+ underline: '\x1b[4m',
38
+ white: '\x1b[37m',
39
+ } as const;
40
+
41
+ /**
42
+ * CP437-inspired ANSI art characters
43
+ */
44
+ const ansiChars = {
45
+ approx: '≈',
46
+ // Block elements for gradients
47
+ block: {
48
+ dark: '▓',
49
+ full: '█',
50
+ light: '░',
51
+ medium: '▒',
52
+ },
53
+ bullet: '•',
54
+ // Symbols
55
+ checkmark: '√',
56
+ cross: '×',
57
+ plusMinus: '±',
58
+ smallSquare: '▪',
59
+ } as const;
60
+
61
+ /**
62
+ * Human-readable console reporter with colorized output
63
+ */
64
+ export class HumanReporter extends BaseReporter {
65
+ private currentFile = '';
66
+
67
+ private currentSuite = '';
68
+
69
+ private failures: Array<{
70
+ error: string;
71
+ file: string;
72
+ suite: string;
73
+ task: string;
74
+ }> = [];
75
+
76
+ private lastProgressLine = '';
77
+
78
+ private progressWindowActive = false; // Track if progress window is rendered
79
+
80
+ private readonly quiet: boolean;
81
+
82
+ private readonly showProgress: boolean;
83
+
84
+ private startTime = 0;
85
+
86
+ private suiteResults: TaskResult[] = [];
87
+
88
+ private readonly useColor: boolean;
89
+
90
+ private readonly verbose: boolean;
91
+
92
+ constructor(
93
+ options: {
94
+ color?: boolean;
95
+ progress?: boolean;
96
+ quiet?: boolean;
97
+ verbose?: boolean;
98
+ } = {},
99
+ ) {
100
+ super('human', options);
101
+
102
+ // Auto-detect color support if not explicitly set
103
+ this.useColor =
104
+ options.color ??
105
+ (process.stdout.isTTY &&
106
+ process.env.FORCE_COLOR !== '0' &&
107
+ process.env.NO_COLOR == null);
108
+
109
+ this.verbose = options.verbose ?? false;
110
+ this.quiet = options.quiet ?? false;
111
+ this.showProgress = options.progress ?? true;
112
+ }
113
+
114
+ onEnd(run: BenchmarkRun): void {
115
+ if (this.quiet) {
116
+ return;
117
+ }
118
+
119
+ this.clearProgress();
120
+
121
+ const duration = Date.now() - this.startTime;
122
+ const totalFiles = run.files.length;
123
+
124
+ // Calculate totals across all files
125
+ let totalSuites = 0;
126
+ let totalPassed = 0;
127
+ let totalFailed = 0;
128
+
129
+ for (const file of run.files) {
130
+ totalSuites += file.suites.length;
131
+ for (const suite of file.suites) {
132
+ totalPassed += suite.tasks.filter((t: TaskResult) => !t.error).length;
133
+ totalFailed += suite.tasks.filter((t: TaskResult) => t.error).length;
134
+ }
135
+ }
136
+
137
+ // Results header
138
+ const resultsHeader = `${this.colorize('magenta', ansiChars.block.full.repeat(2))} ${this.colorize('brightWhite', this.colorize('bold', 'Results'))}`;
139
+ this.printLine(resultsHeader);
140
+ this.printLine();
141
+
142
+ this.printLine(
143
+ `${this.colorize('brightBlue', ' Files:')} ${this.colorize('brightWhite', String(totalFiles))}`,
144
+ );
145
+ this.printLine(
146
+ `${this.colorize('brightBlue', ' Suites:')} ${this.colorize('brightWhite', String(totalSuites))}`,
147
+ );
148
+ this.printLine(
149
+ `${this.colorize('brightBlue', ' Tasks:')} ${this.colorize('brightWhite', String(totalPassed + totalFailed))}`,
150
+ );
151
+ if (totalFailed > 0) {
152
+ this.printLine(
153
+ `${this.colorize('brightRed', ansiChars.cross + ' Failed:')} ${this.colorize('brightWhite', String(totalFailed))}`,
154
+ );
155
+ this.printLine(
156
+ `${this.colorize('brightCyan', ansiChars.checkmark + ' Passed:')} ${this.colorize('brightWhite', String(totalPassed))}`,
157
+ );
158
+ } else {
159
+ this.printLine(
160
+ `${this.colorize('brightCyan', ansiChars.checkmark + ' All tests passed:')} ${this.colorize('brightWhite', String(totalPassed))}`,
161
+ );
162
+ }
163
+ this.printLine(
164
+ `${this.colorize('cyan', ansiChars.approx + ' Duration:')} ${this.colorize('brightWhite', this.formatDuration(duration * 1000000))}`,
165
+ );
166
+ this.printLine();
167
+
168
+ if (totalFailed > 0) {
169
+ // Display failed tasks with details
170
+ if (this.failures.length > 0) {
171
+ this.printLine();
172
+ this.printLine(
173
+ this.colorize('brightRed', this.colorize('bold', 'Failed Tasks:')),
174
+ );
175
+ this.printLine();
176
+
177
+ for (const failure of this.failures) {
178
+ const displayPath = this.formatPath(failure.file);
179
+ this.printLine(
180
+ ` ${this.colorize('dim', displayPath)} ${this.colorize('dim', '›')} ${this.colorize('white', failure.suite)} ${this.colorize('dim', '›')} ${this.colorize('brightWhite', failure.task)}`,
181
+ );
182
+ this.printLine(` ${this.colorize('brightRed', failure.error)}`);
183
+ this.printLine();
184
+ }
185
+ }
186
+ } else {
187
+ const successMessage = `${this.colorize('brightMagenta', 'Rad. ☮')}`;
188
+ this.printLine(successMessage);
189
+ }
190
+ }
191
+
192
+ onError(error: Error): void {
193
+ if (this.quiet) {
194
+ return;
195
+ }
196
+
197
+ this.clearProgress();
198
+ console.error(
199
+ this.colorize('red', ansiChars.cross.repeat(3) + ' Error:'),
200
+ error.message,
201
+ );
202
+
203
+ if (this.verbose && error.stack) {
204
+ console.error(this.colorize('dim', error.stack));
205
+ }
206
+ }
207
+
208
+ onFileEnd(result: FileResult): void {
209
+ if (this.quiet) {
210
+ return;
211
+ }
212
+
213
+ const totalTasks = result.suites.reduce(
214
+ (sum, suite) => sum + suite.tasks.length,
215
+ 0,
216
+ );
217
+ const totalPassed = result.suites.reduce(
218
+ (sum, suite) => sum + suite.tasks.filter((t) => !t.error).length,
219
+ 0,
220
+ );
221
+ const totalFailed = totalTasks - totalPassed;
222
+
223
+ if (totalFailed > 0) {
224
+ this.printLine(
225
+ this.colorize(
226
+ 'red',
227
+ ` ${ansiChars.cross} ${totalFailed} failed, ${totalPassed} passed`,
228
+ ),
229
+ );
230
+ } else {
231
+ this.printLine(
232
+ ` ${this.colorize('magenta', ansiChars.checkmark)} ${totalPassed > 1 ? this.colorize('brightMagenta', 'All ') : ''}${this.colorize('bold', this.colorize('brightMagenta', `${totalPassed}`))} ${this.colorize('brightMagenta', `${this.pluralize('task', totalPassed)} passed`)}`,
233
+ );
234
+ }
235
+
236
+ this.printLine();
237
+ }
238
+
239
+ onFileStart(file: string): void {
240
+ this.currentFile = file;
241
+
242
+ if (this.quiet) {
243
+ return;
244
+ }
245
+
246
+ const displayPath = this.formatPath(file);
247
+ const fileMarker = `${colors.magenta}${ansiChars.block.dark}${ansiChars.block.dark}${colors.reset}`;
248
+ this.printLine(
249
+ `${fileMarker} ${colors.underline}${this.colorize('brightMagenta', this.colorize('bold', displayPath))}${colors.reset}`,
250
+ );
251
+ }
252
+
253
+ onProgress(state: ProgressState): void {
254
+ // Only show progress bar in non-verbose, non-quiet mode with progress enabled
255
+ if (this.quiet || this.verbose || !this.showProgress) {
256
+ return;
257
+ }
258
+
259
+ // Only show in TTY mode (progress bar updates in place)
260
+ if (!process.stdout.isTTY) {
261
+ return;
262
+ }
263
+
264
+ const { elapsed, percentage, tasksCompleted, totalTasks } = state;
265
+
266
+ // Pad task counts for alignment
267
+ const totalTasksWidth = String(totalTasks).length;
268
+ const paddedTasksCompleted = String(tasksCompleted).padStart(
269
+ totalTasksWidth,
270
+ ' ',
271
+ );
272
+
273
+ // Format elapsed time
274
+ const elapsedSeconds = Math.round(elapsed / 1000);
275
+ const elapsedStrRaw = this.formatTimeRemaining(elapsedSeconds);
276
+
277
+ // Calculate ETA if we have completed tasks and determine padding width
278
+ let etaStr = '';
279
+ let padWidth = elapsedStrRaw.length;
280
+ if (tasksCompleted > 0) {
281
+ const avgTimePerTask = elapsed / tasksCompleted;
282
+ const remainingTasks = totalTasks - tasksCompleted;
283
+ const etaMs = avgTimePerTask * remainingTasks;
284
+ const etaSeconds = Math.round(etaMs / 1000);
285
+ const etaTimeStr = this.formatTimeRemaining(etaSeconds);
286
+ padWidth = Math.max(padWidth, etaTimeStr.length);
287
+ etaStr = ` ${this.colorize('dim', '|')} ${this.colorize('dim', 'ETA:')} ${this.colorize('brightBlue', etaTimeStr)}`;
288
+ }
289
+
290
+ // Pad elapsed time to match the longest time string
291
+ const elapsedStr = elapsedStrRaw.padStart(padWidth, ' ');
292
+
293
+ const roundedPercentage = percentage.toFixed(2);
294
+ const line = `${this.colorize('brightCyan', ansiChars.approx)} ${this.colorize('white', paddedTasksCompleted)}${this.colorize('dim', '/')}${this.colorize('white', String(totalTasks))} ${this.colorize('dim', 'tasks')} ${this.colorize('dim', '(')}${this.colorize('brightBlue', roundedPercentage + '%')}${this.colorize('dim', ')')} ${this.colorize('dim', '|')} ${this.colorize('dim', 'Elapsed:')} ${this.colorize('cyan', elapsedStr)}${etaStr}`;
295
+
296
+ this.lastProgressLine = line;
297
+ this.renderProgressWindow();
298
+ }
299
+
300
+ onStart(run: BenchmarkRun): void {
301
+ this.startTime = Date.now();
302
+ this.failures = []; // Reset failures for new run
303
+ this.lastProgressLine = ''; // Reset for new run
304
+
305
+ if (this.quiet) {
306
+ return;
307
+ }
308
+
309
+ let header: string;
310
+ if (run.environment) {
311
+ header = `
312
+ \x1b[49m \x1b[38;5;0;49m▄▄\x1b[38;5;37;48;5;0m▄\x1b[38;5;14;48;5;0m▄\x1b[38;5;6;48;5;0m▄\x1b[38;5;0;49m▄▄\x1b[49m \x1b[m
313
+ \x1b[49m \x1b[38;5;0;49m▄\x1b[38;5;235;48;5;0m▄\x1b[38;5;45;48;5;0m▄\x1b[38;5;14;48;5;23m▄\x1b[48;5;14m \x1b[38;5;14;48;5;14m▄\x1b[38;5;14;48;5;236m▄\x1b[38;5;44;48;5;0m▄\x1b[38;5;233;48;5;0m▄\x1b[38;5;0;49m▄\x1b[49m \x1b[m
314
+ \x1b[38;5;0;49m▄▄\x1b[38;5;30;48;5;0m▄\x1b[38;5;14;48;5;233m▄\x1b[38;5;14;48;5;37m▄\x1b[48;5;14m \x1b[38;5;14;48;5;37m▄\x1b[38;5;14;48;5;0m▄\x1b[38;5;23;48;5;0m▄\x1b[38;5;0;49m▄▄\x1b[m
315
+ \x1b[48;5;0m \x1b[38;5;14;48;5;45m▄\x1b[48;5;14m \x1b[38;5;44;48;5;14m▄\x1b[38;5;24;48;5;14m▄\x1b[38;5;242;48;5;14m▄\x1b[38;5;5;48;5;14m▄\x1b[38;5;60;48;5;14m▄\x1b[38;5;24;48;5;14m▄\x1b[38;5;44;48;5;14m▄\x1b[48;5;14m \x1b[38;5;14;48;5;44m▄\x1b[48;5;0m \x1b[m
316
+ \x1b[48;5;0m \x1b[48;5;14m \x1b[38;5;44;48;5;14m▄\x1b[38;5;53;48;5;45m▄\x1b[38;5;44;48;5;53m▄\x1b[38;5;14;48;5;162m▄\x1b[38;5;14;48;5;89m▄▄\x1b[38;5;14;48;5;162m▄\x1b[38;5;44;48;5;198m▄\x1b[38;5;235;48;5;198m▄\x1b[48;5;198m \x1b[38;5;30;48;5;237m▄\x1b[38;5;38;48;5;14m▄\x1b[48;5;14m \x1b[48;5;0m \x1b[m \x1b[97m\x1b[4;1mmodest\x1b[0m\x1b[4;97mbench\x1b[0m \x1b[4;97m \x1b[0m \x1b[4;97m \x1b[0m
317
+ \x1b[48;5;0m \x1b[48;5;14m \x1b[38;5;237;48;5;45m▄\x1b[38;5;14;48;5;23m▄\x1b[48;5;14m \x1b[38;5;14;48;5;14m▄\x1b[38;5;53;48;5;38m▄\x1b[38;5;44;48;5;23m▄\x1b[38;5;198;48;5;238m▄\x1b[38;5;198;48;5;125m▄\x1b[38;5;23;48;5;14m▄\x1b[48;5;14m \x1b[48;5;0m \x1b[m
318
+ \x1b[48;5;0m \x1b[48;5;14m \x1b[38;5;30;48;5;38m▄\x1b[38;5;14;48;5;14m▄\x1b[48;5;14m \x1b[38;5;45;48;5;14m▄\x1b[38;5;89;48;5;14m▄\x1b[38;5;89;48;5;89m▄\x1b[38;5;14;48;5;31m▄\x1b[48;5;14m \x1b[38;5;37;48;5;89m▄\x1b[48;5;198m \x1b[38;5;198;48;5;198m▄\x1b[38;5;31;48;5;14m▄\x1b[48;5;14m \x1b[48;5;0m \x1b[m \x1b[2mnode.js:\x1b[m \x1b[36m${run.environment.nodeVersion} \x1b[m
319
+ \x1b[48;5;0m \x1b[48;5;14m \x1b[38;5;44;48;5;31m▄\x1b[48;5;14m \x1b[38;5;126;48;5;38m▄\x1b[38;5;198;48;5;237m▄\x1b[38;5;237;48;5;37m▄\x1b[48;5;14m \x1b[38;5;14;48;5;14m▄\x1b[38;5;162;48;5;198m▄▄\x1b[38;5;53;48;5;240m▄\x1b[48;5;14m \x1b[48;5;0m \x1b[m \x1b[2mplatform:\x1b[m \x1b[36m${run.environment.platform} ${run.environment.arch} \x1b[m
320
+ \x1b[48;5;0m \x1b[38;5;45;48;5;14m▄\x1b[48;5;14m \x1b[38;5;14;48;5;37m▄\x1b[38;5;14;48;5;5m▄\x1b[38;5;14;48;5;44m▄\x1b[48;5;14m \x1b[38;5;45;48;5;14m▄\x1b[48;5;0m \x1b[m \x1b[2mcpu:\x1b[m \x1b[36m${run.environment.cpu.model} \x1b[2m(\x1b[m\x1b[36m${run.environment.cpu.cores} cores) \x1b[m
321
+ \x1b[49;38;5;0m▀▀\x1b[38;5;0;48;5;6m▄\x1b[38;5;232;48;5;14m▄\x1b[38;5;38;48;5;14m▄\x1b[48;5;14m \x1b[38;5;30;48;5;14m▄\x1b[38;5;0;48;5;14m▄\x1b[38;5;0;48;5;23m▄\x1b[49;38;5;0m▀▀\x1b[m \x1b[2mmem:\x1b[m \x1b[36m${this.formatBytes(run.environment.memory.total)} \x1b[m
322
+ \x1b[49m \x1b[49;38;5;0m▀\x1b[38;5;0;48;5;236m▄\x1b[38;5;0;48;5;45m▄\x1b[38;5;23;48;5;14m▄\x1b[48;5;14m \x1b[38;5;236;48;5;14m▄\x1b[38;5;0;48;5;44m▄\x1b[38;5;0;48;5;232m▄\x1b[49;38;5;0m▀\x1b[49m \x1b[m
323
+ \x1b[49m \x1b[49;38;5;0m▀▀\x1b[38;5;0;48;5;37m▄\x1b[38;5;0;48;5;14m▄\x1b[38;5;0;48;5;30m▄\x1b[49;38;5;0m▀▀\x1b[49m \x1b[m
324
+ `;
325
+ } else {
326
+ header = `
327
+ \x1b[49m \x1b[38;5;0;49m▄▄\x1b[38;5;37;48;5;0m▄\x1b[38;5;14;48;5;0m▄\x1b[38;5;6;48;5;0m▄\x1b[38;5;0;49m▄▄\x1b[49m \x1b[m
328
+ \x1b[49m \x1b[38;5;0;49m▄\x1b[38;5;235;48;5;0m▄\x1b[38;5;45;48;5;0m▄\x1b[38;5;14;48;5;23m▄\x1b[48;5;14m \x1b[38;5;14;48;5;14m▄\x1b[38;5;14;48;5;236m▄\x1b[38;5;44;48;5;0m▄\x1b[38;5;233;48;5;0m▄\x1b[38;5;0;49m▄\x1b[49m \x1b[m
329
+ \x1b[38;5;0;49m▄▄\x1b[38;5;30;48;5;0m▄\x1b[38;5;14;48;5;233m▄\x1b[38;5;14;48;5;37m▄\x1b[48;5;14m \x1b[38;5;14;48;5;37m▄\x1b[38;5;14;48;5;0m▄\x1b[38;5;23;48;5;0m▄\x1b[38;5;0;49m▄▄\x1b[m
330
+ \x1b[48;5;0m \x1b[38;5;14;48;5;45m▄\x1b[48;5;14m \x1b[38;5;44;48;5;14m▄\x1b[38;5;24;48;5;14m▄\x1b[38;5;242;48;5;14m▄\x1b[38;5;5;48;5;14m▄\x1b[38;5;60;48;5;14m▄\x1b[38;5;24;48;5;14m▄\x1b[38;5;44;48;5;14m▄\x1b[48;5;14m \x1b[38;5;14;48;5;44m▄\x1b[48;5;0m \x1b[m
331
+ \x1b[48;5;0m \x1b[48;5;14m \x1b[38;5;44;48;5;14m▄\x1b[38;5;53;48;5;45m▄\x1b[38;5;44;48;5;53m▄\x1b[38;5;14;48;5;162m▄\x1b[38;5;14;48;5;89m▄▄\x1b[38;5;14;48;5;162m▄\x1b[38;5;44;48;5;198m▄\x1b[38;5;235;48;5;198m▄\x1b[48;5;198m \x1b[38;5;30;48;5;237m▄\x1b[38;5;38;48;5;14m▄\x1b[48;5;14m \x1b[48;5;0m \x1b[m \x1b[97m\x1b[4;1mmodest\x1b[0m\x1b[4;97mbench\x1b[0m \x1b[4;97m \x1b[0m
332
+ \x1b[48;5;0m \x1b[48;5;14m \x1b[38;5;237;48;5;45m▄\x1b[38;5;14;48;5;23m▄\x1b[48;5;14m \x1b[38;5;14;48;5;14m▄\x1b[38;5;53;48;5;38m▄\x1b[38;5;44;48;5;23m▄\x1b[38;5;198;48;5;238m▄\x1b[38;5;198;48;5;125m▄\x1b[38;5;23;48;5;14m▄\x1b[48;5;14m \x1b[48;5;0m \x1b[m
333
+ \x1b[48;5;0m \x1b[48;5;14m \x1b[38;5;30;48;5;38m▄\x1b[38;5;14;48;5;14m▄\x1b[48;5;14m \x1b[38;5;45;48;5;14m▄\x1b[38;5;89;48;5;14m▄\x1b[38;5;89;48;5;89m▄\x1b[38;5;14;48;5;31m▄\x1b[48;5;14m \x1b[38;5;37;48;5;89m▄\x1b[48;5;198m \x1b[38;5;198;48;5;198m▄\x1b[38;5;31;48;5;14m▄\x1b[48;5;14m \x1b[48;5;0m \x1b[m
334
+ \x1b[48;5;0m \x1b[48;5;14m \x1b[38;5;44;48;5;31m▄\x1b[48;5;14m \x1b[38;5;126;48;5;38m▄\x1b[38;5;198;48;5;237m▄\x1b[38;5;237;48;5;37m▄\x1b[48;5;14m \x1b[38;5;14;48;5;14m▄\x1b[38;5;162;48;5;198m▄▄\x1b[38;5;53;48;5;240m▄\x1b[48;5;14m \x1b[48;5;0m \x1b[m
335
+ \x1b[48;5;0m \x1b[38;5;45;48;5;14m▄\x1b[48;5;14m \x1b[38;5;14;48;5;37m▄\x1b[38;5;14;48;5;5m▄\x1b[38;5;14;48;5;44m▄\x1b[48;5;14m \x1b[38;5;45;48;5;14m▄\x1b[48;5;0m \x1b[m
336
+ \x1b[49;38;5;0m▀▀\x1b[38;5;0;48;5;6m▄\x1b[38;5;232;48;5;14m▄\x1b[38;5;38;48;5;14m▄\x1b[48;5;14m \x1b[38;5;30;48;5;14m▄\x1b[38;5;0;48;5;14m▄\x1b[38;5;0;48;5;23m▄\x1b[49;38;5;0m▀▀\x1b[m
337
+ \x1b[49m \x1b[49;38;5;0m▀\x1b[38;5;0;48;5;236m▄\x1b[38;5;0;48;5;45m▄\x1b[38;5;23;48;5;14m▄\x1b[48;5;14m \x1b[38;5;236;48;5;14m▄\x1b[38;5;0;48;5;44m▄\x1b[38;5;0;48;5;232m▄\x1b[49;38;5;0m▀\x1b[49m \x1b[m
338
+ \x1b[49m \x1b[49;38;5;0m▀▀\x1b[38;5;0;48;5;37m▄\x1b[38;5;0;48;5;14m▄\x1b[38;5;0;48;5;30m▄\x1b[49;38;5;0m▀▀\x1b[49m \x1b[m
339
+ `;
340
+ }
341
+ this.printLine(header);
342
+ this.printLine();
343
+
344
+ if (run.git) {
345
+ this.printLine(` Git: ${this.colorize('cyan', run.git.commit)}`);
346
+ }
347
+
348
+ if (run.ci) {
349
+ this.printLine(` CI: ${this.colorize('cyan', run.ci.provider)}`);
350
+ this.printLine();
351
+ }
352
+ }
353
+
354
+ onSuiteEnd(result: SuiteResult): void {
355
+ if (this.quiet) {
356
+ return;
357
+ }
358
+
359
+ // Print all buffered task results with aligned columns
360
+ this.printAlignedSuiteResults();
361
+
362
+ // Skip displaying summary for the implicit "default" suite
363
+ if (result.name === 'default') {
364
+ return;
365
+ }
366
+
367
+ const passed = result.tasks.filter((t) => !t.error).length;
368
+ const failed = result.tasks.filter((t) => t.error).length;
369
+
370
+ if (failed > 0) {
371
+ this.printLine(
372
+ ` ${this.colorize('red', `${ansiChars.cross} ${failed} failed`)}, ${this.colorize('green', `${passed} passed`)}`,
373
+ );
374
+ } else {
375
+ this.printLine(
376
+ ` ${this.colorize('magenta', ansiChars.checkmark)} ${this.colorize('bold', this.colorize('brightWhite', `${passed}`))} ${this.colorize('brightWhite', `${this.pluralize('task', passed)} passed`)}`,
377
+ );
378
+ }
379
+ this.printLine();
380
+ }
381
+
382
+ onSuiteStart(suite: string): void {
383
+ this.currentSuite = suite;
384
+
385
+ if (this.quiet) {
386
+ return;
387
+ }
388
+
389
+ this.suiteResults = []; // Reset buffer for new suite
390
+
391
+ // Skip displaying the implicit "default" suite header
392
+ if (suite === 'default') {
393
+ return;
394
+ }
395
+
396
+ this.printLine();
397
+ const suiteMarker = `${colors.magenta}${ansiChars.block.light}${ansiChars.block.light}${colors.reset}`;
398
+ this.printLine(
399
+ ` ${suiteMarker} ${this.colorize('bold', this.colorize('brightWhite', suite))}`,
400
+ );
401
+ }
402
+
403
+ onTaskResult(result: TaskResult): void {
404
+ if (this.quiet) {
405
+ return;
406
+ }
407
+
408
+ // Buffer the result for later printing with proper alignment
409
+ this.suiteResults.push(result);
410
+ }
411
+
412
+ onTaskStart(task: string): void {
413
+ if (this.quiet) {
414
+ return;
415
+ }
416
+
417
+ // Only show static markers in verbose mode
418
+ if (this.verbose) {
419
+ this.printLine(
420
+ ` ${this.colorize('gray', ansiChars.smallSquare)} ${task}`,
421
+ );
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Clear current progress display
427
+ */
428
+ private clearProgress(): void {
429
+ this.clearProgressWindow();
430
+ this.lastProgressLine = '';
431
+ }
432
+
433
+ /**
434
+ * Clear the progress window at the bottom (Vitest-style)
435
+ */
436
+ private clearProgressWindow(): void {
437
+ if (!process.stdout.isTTY || !this.progressWindowActive) {
438
+ return;
439
+ }
440
+ // Move up and clear two lines (blank line + progress line)
441
+ // '\x1b[1A' moves cursor up one line, '\x1b[K' clears the current line
442
+ // This sequence moves up and clears two lines in total
443
+ process.stdout.write('\x1b[1A\x1b[K\x1b[1A\x1b[K');
444
+ this.progressWindowActive = false;
445
+ }
446
+
447
+ /**
448
+ * Apply color to text if colors are enabled
449
+ */
450
+ private colorize(color: keyof typeof colors, text: string): string {
451
+ if (!this.useColor) {
452
+ return text;
453
+ }
454
+ return `${colors[color]}${text}${colors.reset}`;
455
+ }
456
+
457
+ /**
458
+ * Format bytes in human-readable format
459
+ */
460
+ private formatBytes(bytes: number): string {
461
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
462
+ let size = bytes;
463
+ let unitIndex = 0;
464
+
465
+ while (size >= 1024 && unitIndex < units.length - 1) {
466
+ size /= 1024;
467
+ unitIndex++;
468
+ }
469
+
470
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
471
+ }
472
+
473
+ /**
474
+ * Format file path - show relative path if within CWD, otherwise absolute
475
+ */
476
+ private formatPath(filePath: string): string {
477
+ const cwd = process.cwd();
478
+ const absolutePath = path.resolve(filePath);
479
+
480
+ // Check if the file is within the current working directory
481
+ if (absolutePath.startsWith(cwd + path.sep) || absolutePath === cwd) {
482
+ return path.relative(cwd, absolutePath);
483
+ }
484
+
485
+ return absolutePath;
486
+ }
487
+
488
+ /**
489
+ * Format duration in human-readable format for progress display
490
+ */
491
+ private formatTimeRemaining(seconds: number): string {
492
+ if (seconds < 60) {
493
+ return `${seconds}s`;
494
+ } else if (seconds < 3600) {
495
+ const minutes = Math.floor(seconds / 60);
496
+ const remainingSeconds = seconds % 60;
497
+ return `${minutes}m${remainingSeconds}s`;
498
+ } else {
499
+ const hours = Math.floor(seconds / 3600);
500
+ const minutes = Math.floor((seconds % 3600) / 60);
501
+ return `${hours}h${minutes}m`;
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Get visible length of string (excluding ANSI escape codes)
507
+ */
508
+ private getVisibleLength(str: string): number {
509
+ // Remove ANSI escape codes to get actual visible length
510
+ // eslint-disable-next-line no-control-regex
511
+ return str.replace(/\x1b\[[0-9;]*m/g, '').length;
512
+ }
513
+
514
+ /**
515
+ * Simple pluralization helper
516
+ */
517
+ private pluralize(str: string, count: number): string {
518
+ return count === 1 ? str : `${str}s`;
519
+ }
520
+
521
+ /**
522
+ * Print all task results in a suite with aligned columns
523
+ */
524
+ private printAlignedSuiteResults(): void {
525
+ if (this.suiteResults.length === 0) {
526
+ return;
527
+ }
528
+
529
+ const MAX_NAME_WIDTH = 60;
530
+ const BASE_INDENT = ' '; // 4 spaces
531
+ const bullet = this.colorize(
532
+ 'dim',
533
+ this.colorize('gray', ansiChars.bullet),
534
+ );
535
+
536
+ // Prepare formatted data for each task
537
+ interface FormattedTask {
538
+ durationLen: number;
539
+ durationStr: string;
540
+ error: boolean;
541
+ errorMessage?: string;
542
+ iterations: number;
543
+ name: string;
544
+ nameLength: number;
545
+ opsPerSecLen: number;
546
+ opsPerSecStr: string;
547
+ rmeLen: number;
548
+ rmeStr: string;
549
+ status: string;
550
+ }
551
+
552
+ const formatted: FormattedTask[] = this.suiteResults.map((result) => {
553
+ const status = result.error
554
+ ? this.colorize('red', ansiChars.cross)
555
+ : this.colorize('brightCyan', ansiChars.checkmark);
556
+
557
+ const name = result.name.trim();
558
+ const nameLength = this.getVisibleLength(name);
559
+
560
+ if (result.error) {
561
+ return {
562
+ durationLen: 0,
563
+ durationStr: '',
564
+ error: true,
565
+ errorMessage: result.error?.message || String(result.error),
566
+ iterations: 0,
567
+ name,
568
+ nameLength,
569
+ opsPerSecLen: 0,
570
+ opsPerSecStr: '',
571
+ rmeLen: 0,
572
+ rmeStr: '',
573
+ status,
574
+ };
575
+ }
576
+
577
+ const duration = this.formatDuration(result.mean); // already in nanoseconds
578
+ const opsPerSec = this.formatOpsPerSecond(result.opsPerSecond);
579
+ const rme = this.formatPercentage(result.marginOfError * 100);
580
+
581
+ return {
582
+ durationLen: this.getVisibleLength(duration),
583
+ durationStr: duration,
584
+ error: false,
585
+ iterations: result.iterations,
586
+ name,
587
+ nameLength,
588
+ opsPerSecLen: this.getVisibleLength(opsPerSec),
589
+ opsPerSecStr: opsPerSec,
590
+ rmeLen: this.getVisibleLength(rme),
591
+ rmeStr: rme,
592
+ status,
593
+ };
594
+ });
595
+
596
+ // Find max widths
597
+ const nonWrappingTasks = formatted.filter(
598
+ (t) => t.nameLength <= MAX_NAME_WIDTH,
599
+ );
600
+ const maxNameLen =
601
+ nonWrappingTasks.length > 0
602
+ ? Math.max(...nonWrappingTasks.map((t) => t.nameLength))
603
+ : 40; // Default if all tasks wrap
604
+
605
+ const maxDurationLen = Math.max(
606
+ ...formatted.filter((t) => !t.error).map((t) => t.durationLen),
607
+ 0,
608
+ );
609
+ const maxRmeLen = Math.max(
610
+ ...formatted.filter((t) => !t.error).map((t) => t.rmeLen),
611
+ 0,
612
+ );
613
+ const maxOpsLen = Math.max(
614
+ ...formatted.filter((t) => !t.error).map((t) => t.opsPerSecLen),
615
+ 0,
616
+ );
617
+
618
+ // Calculate the position where numbers start for unwrapped lines
619
+ // BASE_INDENT (4) + status (1 char) + space (1) + maxNameLen + ": " (2) = 8 + maxNameLen
620
+ const numbersStartPos = BASE_INDENT.length + 2 + maxNameLen + 2;
621
+
622
+ // Print each task with aligned columns
623
+ for (const task of formatted) {
624
+ if (task.error) {
625
+ // Track failure for end summary
626
+ this.failures.push({
627
+ error: task.errorMessage || 'Unknown error',
628
+ file: this.currentFile,
629
+ suite: this.currentSuite,
630
+ task: task.name,
631
+ });
632
+
633
+ this.printLine(
634
+ `${BASE_INDENT}${task.status} ${this.colorize('white', task.name)} ${this.colorize('red', 'FAILED')}`,
635
+ );
636
+ } else if (task.nameLength > MAX_NAME_WIDTH) {
637
+ // Long name - wrap to next line, but align numbers with unwrapped lines
638
+ this.printLine(
639
+ `${BASE_INDENT}${task.status} ${this.colorize('white', task.name)}:`,
640
+ );
641
+
642
+ // Calculate padding to align with unwrapped lines
643
+ // We need to get to numbersStartPos from the beginning of the line
644
+ const leadingPad = ' '.repeat(numbersStartPos);
645
+ const durationPad = ' '.repeat(maxDurationLen - task.durationLen);
646
+ const rmePad = ' '.repeat(maxRmeLen - task.rmeLen);
647
+ const opsPad = ' '.repeat(maxOpsLen - task.opsPerSecLen);
648
+
649
+ this.printLine(
650
+ `${leadingPad}${durationPad}${this.colorize('cyan', task.durationStr)} ${bullet} ${ansiChars.plusMinus}${rmePad}${this.colorize('brightBlue', task.rmeStr)} ${bullet} ${opsPad}${this.colorize('magenta', task.opsPerSecStr)}`,
651
+ );
652
+
653
+ if (this.verbose && task.iterations > 0) {
654
+ this.printLine(
655
+ ` ${this.colorize('dim', `${task.iterations} iterations`)}`,
656
+ );
657
+ }
658
+ } else {
659
+ // Normal length - align on same line
660
+ const namePad = ' '.repeat(maxNameLen - task.nameLength);
661
+ const durationPad = ' '.repeat(maxDurationLen - task.durationLen);
662
+ const rmePad = ' '.repeat(maxRmeLen - task.rmeLen);
663
+ const opsPad = ' '.repeat(maxOpsLen - task.opsPerSecLen);
664
+
665
+ this.printLine(
666
+ `${BASE_INDENT}${task.status} ${this.colorize('white', task.name)}${namePad}: ${durationPad}${this.colorize('cyan', task.durationStr)} ${bullet} ${ansiChars.plusMinus}${rmePad}${this.colorize('brightBlue', task.rmeStr)} ${bullet} ${opsPad}${this.colorize('magenta', task.opsPerSecStr)}`,
667
+ );
668
+
669
+ if (this.verbose && task.iterations > 0) {
670
+ this.printLine(
671
+ ` ${this.colorize('dim', `${task.iterations} iterations`)}`,
672
+ );
673
+ }
674
+ }
675
+ }
676
+ }
677
+
678
+ /**
679
+ * Print a line while maintaining the progress window at the bottom
680
+ * (Vitest-style)
681
+ */
682
+ private printLine(message: string = ''): void {
683
+ // Clear progress window, print content, re-render progress window
684
+ this.clearProgressWindow();
685
+ console.log(message);
686
+ this.renderProgressWindow();
687
+ }
688
+
689
+ /**
690
+ * Render the progress window at the bottom
691
+ */
692
+ private renderProgressWindow(): void {
693
+ if (!process.stdout.isTTY || !this.lastProgressLine) {
694
+ return;
695
+ }
696
+
697
+ // Clear existing window if present
698
+ if (this.progressWindowActive) {
699
+ this.clearProgressWindow();
700
+ }
701
+
702
+ // Write blank line for spacing, then progress line
703
+ console.log('');
704
+ console.log(this.lastProgressLine);
705
+ this.progressWindowActive = true;
706
+ }
707
+ }