modestbench 0.2.0 → 0.3.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 (357) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +131 -34
  3. package/dist/cli/commands/analyze.cjs +60 -0
  4. package/dist/cli/commands/analyze.cjs.map +1 -0
  5. package/dist/cli/commands/analyze.d.cts +35 -0
  6. package/dist/cli/commands/analyze.d.cts.map +1 -0
  7. package/dist/cli/commands/analyze.d.ts +35 -0
  8. package/dist/cli/commands/analyze.d.ts.map +1 -0
  9. package/dist/cli/commands/analyze.js +56 -0
  10. package/dist/cli/commands/analyze.js.map +1 -0
  11. package/dist/cli/commands/baseline.cjs +404 -0
  12. package/dist/cli/commands/baseline.cjs.map +1 -0
  13. package/dist/cli/commands/baseline.d.cts +72 -0
  14. package/dist/cli/commands/baseline.d.cts.map +1 -0
  15. package/dist/cli/commands/baseline.d.ts +72 -0
  16. package/dist/cli/commands/baseline.d.ts.map +1 -0
  17. package/dist/cli/commands/baseline.js +396 -0
  18. package/dist/cli/commands/baseline.js.map +1 -0
  19. package/dist/cli/commands/history.d.cts +1 -1
  20. package/dist/cli/commands/history.d.cts.map +1 -1
  21. package/dist/cli/commands/history.d.ts +1 -1
  22. package/dist/cli/commands/history.d.ts.map +1 -1
  23. package/dist/cli/commands/init.cjs +99 -166
  24. package/dist/cli/commands/init.cjs.map +1 -1
  25. package/dist/cli/commands/init.d.cts +4 -4
  26. package/dist/cli/commands/init.d.cts.map +1 -1
  27. package/dist/cli/commands/init.d.ts +4 -4
  28. package/dist/cli/commands/init.d.ts.map +1 -1
  29. package/dist/cli/commands/init.js +99 -166
  30. package/dist/cli/commands/init.js.map +1 -1
  31. package/dist/cli/commands/run.cjs +146 -127
  32. package/dist/cli/commands/run.cjs.map +1 -1
  33. package/dist/cli/commands/run.d.cts +16 -3
  34. package/dist/cli/commands/run.d.cts.map +1 -1
  35. package/dist/cli/commands/run.d.ts +16 -3
  36. package/dist/cli/commands/run.d.ts.map +1 -1
  37. package/dist/cli/commands/run.js +145 -93
  38. package/dist/cli/commands/run.js.map +1 -1
  39. package/dist/cli/index.cjs +583 -394
  40. package/dist/cli/index.cjs.map +1 -1
  41. package/dist/cli/index.d.cts +4 -16
  42. package/dist/cli/index.d.cts.map +1 -1
  43. package/dist/cli/index.d.ts +4 -16
  44. package/dist/cli/index.d.ts.map +1 -1
  45. package/dist/cli/index.js +575 -386
  46. package/dist/cli/index.js.map +1 -1
  47. package/dist/config/budget-schema.cjs +172 -0
  48. package/dist/config/budget-schema.cjs.map +1 -0
  49. package/dist/config/budget-schema.d.cts +59 -0
  50. package/dist/config/budget-schema.d.cts.map +1 -0
  51. package/dist/config/budget-schema.d.ts +59 -0
  52. package/dist/config/budget-schema.d.ts.map +1 -0
  53. package/dist/config/budget-schema.js +166 -0
  54. package/dist/config/budget-schema.js.map +1 -0
  55. package/dist/config/schema.cjs +182 -2
  56. package/dist/config/schema.cjs.map +1 -1
  57. package/dist/config/schema.d.cts +122 -3
  58. package/dist/config/schema.d.cts.map +1 -1
  59. package/dist/config/schema.d.ts +122 -3
  60. package/dist/config/schema.d.ts.map +1 -1
  61. package/dist/config/schema.js +180 -1
  62. package/dist/config/schema.js.map +1 -1
  63. package/dist/constants.cjs +45 -2
  64. package/dist/constants.cjs.map +1 -1
  65. package/dist/constants.d.cts +41 -0
  66. package/dist/constants.d.cts.map +1 -1
  67. package/dist/constants.d.ts +41 -0
  68. package/dist/constants.d.ts.map +1 -1
  69. package/dist/constants.js +44 -1
  70. package/dist/constants.js.map +1 -1
  71. package/dist/core/engine.cjs +114 -23
  72. package/dist/core/engine.cjs.map +1 -1
  73. package/dist/core/engine.d.cts +7 -7
  74. package/dist/core/engine.d.cts.map +1 -1
  75. package/dist/core/engine.d.ts +7 -7
  76. package/dist/core/engine.d.ts.map +1 -1
  77. package/dist/core/engine.js +115 -24
  78. package/dist/core/engine.js.map +1 -1
  79. package/dist/core/engines/accurate-engine.cjs +171 -36
  80. package/dist/core/engines/accurate-engine.cjs.map +1 -1
  81. package/dist/core/engines/accurate-engine.d.cts +5 -0
  82. package/dist/core/engines/accurate-engine.d.cts.map +1 -1
  83. package/dist/core/engines/accurate-engine.d.ts +5 -0
  84. package/dist/core/engines/accurate-engine.d.ts.map +1 -1
  85. package/dist/core/engines/accurate-engine.js +171 -36
  86. package/dist/core/engines/accurate-engine.js.map +1 -1
  87. package/dist/core/engines/tinybench-engine.cjs +3 -2
  88. package/dist/core/engines/tinybench-engine.cjs.map +1 -1
  89. package/dist/core/engines/tinybench-engine.d.cts.map +1 -1
  90. package/dist/core/engines/tinybench-engine.d.ts.map +1 -1
  91. package/dist/core/engines/tinybench-engine.js +3 -2
  92. package/dist/core/engines/tinybench-engine.js.map +1 -1
  93. package/dist/core/output-path-resolver.cjs +8 -1
  94. package/dist/core/output-path-resolver.cjs.map +1 -1
  95. package/dist/core/output-path-resolver.d.cts.map +1 -1
  96. package/dist/core/output-path-resolver.d.ts.map +1 -1
  97. package/dist/core/output-path-resolver.js +9 -2
  98. package/dist/core/output-path-resolver.js.map +1 -1
  99. package/dist/errors/base.cjs +12 -3
  100. package/dist/errors/base.cjs.map +1 -1
  101. package/dist/errors/base.d.cts +7 -0
  102. package/dist/errors/base.d.cts.map +1 -1
  103. package/dist/errors/base.d.ts +7 -0
  104. package/dist/errors/base.d.ts.map +1 -1
  105. package/dist/errors/base.js +10 -2
  106. package/dist/errors/base.js.map +1 -1
  107. package/dist/errors/budget.cjs +37 -0
  108. package/dist/errors/budget.cjs.map +1 -0
  109. package/dist/errors/budget.d.cts +31 -0
  110. package/dist/errors/budget.d.cts.map +1 -0
  111. package/dist/errors/budget.d.ts +31 -0
  112. package/dist/errors/budget.d.ts.map +1 -0
  113. package/dist/errors/budget.js +33 -0
  114. package/dist/errors/budget.js.map +1 -0
  115. package/dist/errors/index.cjs +4 -1
  116. package/dist/errors/index.cjs.map +1 -1
  117. package/dist/errors/index.d.cts +1 -0
  118. package/dist/errors/index.d.cts.map +1 -1
  119. package/dist/errors/index.d.ts +1 -0
  120. package/dist/errors/index.d.ts.map +1 -1
  121. package/dist/errors/index.js +2 -0
  122. package/dist/errors/index.js.map +1 -1
  123. package/dist/index.cjs +13 -1
  124. package/dist/index.cjs.map +1 -1
  125. package/dist/index.d.cts +5 -0
  126. package/dist/index.d.cts.map +1 -1
  127. package/dist/index.d.ts +5 -0
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +7 -0
  130. package/dist/index.js.map +1 -1
  131. package/dist/reporters/csv.cjs +37 -17
  132. package/dist/reporters/csv.cjs.map +1 -1
  133. package/dist/reporters/csv.d.cts +3 -6
  134. package/dist/reporters/csv.d.cts.map +1 -1
  135. package/dist/reporters/csv.d.ts +3 -6
  136. package/dist/reporters/csv.d.ts.map +1 -1
  137. package/dist/reporters/csv.js +37 -17
  138. package/dist/reporters/csv.js.map +1 -1
  139. package/dist/reporters/human.cjs +290 -67
  140. package/dist/reporters/human.cjs.map +1 -1
  141. package/dist/reporters/human.d.cts +25 -13
  142. package/dist/reporters/human.d.cts.map +1 -1
  143. package/dist/reporters/human.d.ts +25 -13
  144. package/dist/reporters/human.d.ts.map +1 -1
  145. package/dist/reporters/human.js +290 -67
  146. package/dist/reporters/human.js.map +1 -1
  147. package/dist/reporters/json.cjs +23 -48
  148. package/dist/reporters/json.cjs.map +1 -1
  149. package/dist/reporters/json.d.cts +2 -28
  150. package/dist/reporters/json.d.cts.map +1 -1
  151. package/dist/reporters/json.d.ts +2 -28
  152. package/dist/reporters/json.d.ts.map +1 -1
  153. package/dist/reporters/json.js +25 -50
  154. package/dist/reporters/json.js.map +1 -1
  155. package/dist/reporters/profile-human.cjs +154 -0
  156. package/dist/reporters/profile-human.cjs.map +1 -0
  157. package/dist/reporters/profile-human.d.cts +44 -0
  158. package/dist/reporters/profile-human.d.cts.map +1 -0
  159. package/dist/reporters/profile-human.d.ts +44 -0
  160. package/dist/reporters/profile-human.d.ts.map +1 -0
  161. package/dist/reporters/profile-human.js +147 -0
  162. package/dist/reporters/profile-human.js.map +1 -0
  163. package/dist/reporters/simple.cjs +67 -45
  164. package/dist/reporters/simple.cjs.map +1 -1
  165. package/dist/reporters/simple.d.cts +14 -14
  166. package/dist/reporters/simple.d.cts.map +1 -1
  167. package/dist/reporters/simple.d.ts +14 -14
  168. package/dist/reporters/simple.d.ts.map +1 -1
  169. package/dist/reporters/simple.js +67 -45
  170. package/dist/reporters/simple.js.map +1 -1
  171. package/dist/schema/modestbench-config.schema.json +153 -0
  172. package/dist/services/baseline-storage.cjs +151 -0
  173. package/dist/services/baseline-storage.cjs.map +1 -0
  174. package/dist/services/baseline-storage.d.cts +55 -0
  175. package/dist/services/baseline-storage.d.cts.map +1 -0
  176. package/dist/services/baseline-storage.d.ts +55 -0
  177. package/dist/services/baseline-storage.d.ts.map +1 -0
  178. package/dist/services/baseline-storage.js +147 -0
  179. package/dist/services/baseline-storage.js.map +1 -0
  180. package/dist/services/budget-evaluator.cjs +146 -0
  181. package/dist/services/budget-evaluator.cjs.map +1 -0
  182. package/dist/services/budget-evaluator.d.cts +29 -0
  183. package/dist/services/budget-evaluator.d.cts.map +1 -0
  184. package/dist/services/budget-evaluator.d.ts +29 -0
  185. package/dist/services/budget-evaluator.d.ts.map +1 -0
  186. package/dist/services/budget-evaluator.js +142 -0
  187. package/dist/services/budget-evaluator.js.map +1 -0
  188. package/dist/services/config-manager.cjs +24 -10
  189. package/dist/services/config-manager.cjs.map +1 -1
  190. package/dist/services/config-manager.d.cts +6 -1
  191. package/dist/services/config-manager.d.cts.map +1 -1
  192. package/dist/services/config-manager.d.ts +6 -1
  193. package/dist/services/config-manager.d.ts.map +1 -1
  194. package/dist/services/config-manager.js +24 -10
  195. package/dist/services/config-manager.js.map +1 -1
  196. package/dist/services/file-loader.cjs +3 -6
  197. package/dist/services/file-loader.cjs.map +1 -1
  198. package/dist/services/file-loader.d.cts.map +1 -1
  199. package/dist/services/file-loader.d.ts.map +1 -1
  200. package/dist/services/file-loader.js +3 -6
  201. package/dist/services/file-loader.js.map +1 -1
  202. package/dist/services/profiler/profile-filter.cjs +116 -0
  203. package/dist/services/profiler/profile-filter.cjs.map +1 -0
  204. package/dist/services/profiler/profile-filter.d.cts +20 -0
  205. package/dist/services/profiler/profile-filter.d.cts.map +1 -0
  206. package/dist/services/profiler/profile-filter.d.ts +20 -0
  207. package/dist/services/profiler/profile-filter.d.ts.map +1 -0
  208. package/dist/services/profiler/profile-filter.js +112 -0
  209. package/dist/services/profiler/profile-filter.js.map +1 -0
  210. package/dist/services/profiler/profile-parser.cjs +139 -0
  211. package/dist/services/profiler/profile-parser.cjs.map +1 -0
  212. package/dist/services/profiler/profile-parser.d.cts +18 -0
  213. package/dist/services/profiler/profile-parser.d.cts.map +1 -0
  214. package/dist/services/profiler/profile-parser.d.ts +18 -0
  215. package/dist/services/profiler/profile-parser.d.ts.map +1 -0
  216. package/dist/services/profiler/profile-parser.js +132 -0
  217. package/dist/services/profiler/profile-parser.js.map +1 -0
  218. package/dist/services/profiler/profile-runner.cjs +90 -0
  219. package/dist/services/profiler/profile-runner.cjs.map +1 -0
  220. package/dist/services/profiler/profile-runner.d.cts +29 -0
  221. package/dist/services/profiler/profile-runner.d.cts.map +1 -0
  222. package/dist/services/profiler/profile-runner.d.ts +29 -0
  223. package/dist/services/profiler/profile-runner.d.ts.map +1 -0
  224. package/dist/services/profiler/profile-runner.js +86 -0
  225. package/dist/services/profiler/profile-runner.js.map +1 -0
  226. package/dist/services/progress-manager.cjs +10 -2
  227. package/dist/services/progress-manager.cjs.map +1 -1
  228. package/dist/services/progress-manager.d.cts +2 -0
  229. package/dist/services/progress-manager.d.cts.map +1 -1
  230. package/dist/services/progress-manager.d.ts +2 -0
  231. package/dist/services/progress-manager.d.ts.map +1 -1
  232. package/dist/services/progress-manager.js +10 -2
  233. package/dist/services/progress-manager.js.map +1 -1
  234. package/dist/services/reporter-registry.cjs +18 -24
  235. package/dist/services/reporter-registry.cjs.map +1 -1
  236. package/dist/services/reporter-registry.d.cts +18 -40
  237. package/dist/services/reporter-registry.d.cts.map +1 -1
  238. package/dist/services/reporter-registry.d.ts +18 -40
  239. package/dist/services/reporter-registry.d.ts.map +1 -1
  240. package/dist/services/reporter-registry.js +18 -24
  241. package/dist/services/reporter-registry.js.map +1 -1
  242. package/dist/types/budgets.cjs +8 -0
  243. package/dist/types/budgets.cjs.map +1 -0
  244. package/dist/types/budgets.d.cts +149 -0
  245. package/dist/types/budgets.d.cts.map +1 -0
  246. package/dist/types/budgets.d.ts +149 -0
  247. package/dist/types/budgets.d.ts.map +1 -0
  248. package/dist/types/budgets.js +7 -0
  249. package/dist/types/budgets.js.map +1 -0
  250. package/dist/types/cli.cjs +2 -11
  251. package/dist/types/cli.cjs.map +1 -1
  252. package/dist/types/cli.d.cts +3 -227
  253. package/dist/types/cli.d.cts.map +1 -1
  254. package/dist/types/cli.d.ts +3 -227
  255. package/dist/types/cli.d.ts.map +1 -1
  256. package/dist/types/cli.js +2 -11
  257. package/dist/types/cli.js.map +1 -1
  258. package/dist/types/core.cjs +6 -1
  259. package/dist/types/core.cjs.map +1 -1
  260. package/dist/types/core.d.cts +15 -2
  261. package/dist/types/core.d.cts.map +1 -1
  262. package/dist/types/core.d.ts +15 -2
  263. package/dist/types/core.d.ts.map +1 -1
  264. package/dist/types/core.js +2 -1
  265. package/dist/types/core.js.map +1 -1
  266. package/dist/types/index.cjs +5 -0
  267. package/dist/types/index.cjs.map +1 -1
  268. package/dist/types/index.d.cts +2 -0
  269. package/dist/types/index.d.cts.map +1 -1
  270. package/dist/types/index.d.ts +2 -0
  271. package/dist/types/index.d.ts.map +1 -1
  272. package/dist/types/index.js +2 -0
  273. package/dist/types/index.js.map +1 -1
  274. package/dist/types/interfaces.d.cts +19 -8
  275. package/dist/types/interfaces.d.cts.map +1 -1
  276. package/dist/types/interfaces.d.ts +19 -8
  277. package/dist/types/interfaces.d.ts.map +1 -1
  278. package/dist/types/profiler.cjs +11 -0
  279. package/dist/types/profiler.cjs.map +1 -0
  280. package/dist/types/profiler.d.cts +102 -0
  281. package/dist/types/profiler.d.cts.map +1 -0
  282. package/dist/types/profiler.d.ts +102 -0
  283. package/dist/types/profiler.d.ts.map +1 -0
  284. package/dist/types/profiler.js +10 -0
  285. package/dist/types/profiler.js.map +1 -0
  286. package/dist/types/utility.cjs.map +1 -1
  287. package/dist/types/utility.d.cts +0 -8
  288. package/dist/types/utility.d.cts.map +1 -1
  289. package/dist/types/utility.d.ts +0 -8
  290. package/dist/types/utility.d.ts.map +1 -1
  291. package/dist/types/utility.js.map +1 -1
  292. package/dist/utils/identifiers.cjs +32 -0
  293. package/dist/utils/identifiers.cjs.map +1 -0
  294. package/dist/utils/identifiers.d.cts +32 -0
  295. package/dist/utils/identifiers.d.cts.map +1 -0
  296. package/dist/utils/identifiers.d.ts +32 -0
  297. package/dist/utils/identifiers.d.ts.map +1 -0
  298. package/dist/utils/identifiers.js +27 -0
  299. package/dist/utils/identifiers.js.map +1 -0
  300. package/dist/utils/package.cjs +40 -0
  301. package/dist/utils/package.cjs.map +1 -0
  302. package/dist/utils/package.d.cts +15 -0
  303. package/dist/utils/package.d.cts.map +1 -0
  304. package/dist/utils/package.d.ts +15 -0
  305. package/dist/utils/package.d.ts.map +1 -0
  306. package/dist/utils/package.js +33 -0
  307. package/dist/utils/package.js.map +1 -0
  308. package/dist/utils/type-guards.cjs +48 -0
  309. package/dist/utils/type-guards.cjs.map +1 -0
  310. package/dist/utils/type-guards.d.cts +22 -0
  311. package/dist/utils/type-guards.d.cts.map +1 -0
  312. package/dist/utils/type-guards.d.ts +22 -0
  313. package/dist/utils/type-guards.d.ts.map +1 -0
  314. package/dist/utils/type-guards.js +43 -0
  315. package/dist/utils/type-guards.js.map +1 -0
  316. package/package.json +18 -19
  317. package/src/cli/commands/analyze.ts +101 -0
  318. package/src/cli/commands/baseline.ts +577 -0
  319. package/src/cli/commands/history.ts +1 -1
  320. package/src/cli/commands/init.ts +116 -194
  321. package/src/cli/commands/run.ts +183 -113
  322. package/src/cli/index.ts +425 -183
  323. package/src/config/budget-schema.ts +189 -0
  324. package/src/config/schema.ts +260 -1
  325. package/src/constants.ts +53 -1
  326. package/src/core/engine.ts +169 -22
  327. package/src/core/engines/accurate-engine.ts +195 -44
  328. package/src/core/engines/tinybench-engine.ts +3 -2
  329. package/src/core/output-path-resolver.ts +10 -2
  330. package/src/errors/base.ts +11 -2
  331. package/src/errors/budget.ts +38 -0
  332. package/src/errors/index.ts +3 -0
  333. package/src/index.ts +9 -0
  334. package/src/reporters/csv.ts +54 -25
  335. package/src/reporters/human.ts +434 -115
  336. package/src/reporters/json.ts +26 -71
  337. package/src/reporters/profile-human.ts +210 -0
  338. package/src/reporters/simple.ts +88 -54
  339. package/src/services/baseline-storage.ts +199 -0
  340. package/src/services/budget-evaluator.ts +182 -0
  341. package/src/services/config-manager.ts +24 -9
  342. package/src/services/file-loader.ts +3 -6
  343. package/src/services/profiler/profile-filter.ts +147 -0
  344. package/src/services/profiler/profile-parser.ts +194 -0
  345. package/src/services/profiler/profile-runner.ts +121 -0
  346. package/src/services/progress-manager.ts +12 -2
  347. package/src/services/reporter-registry.ts +46 -81
  348. package/src/types/budgets.ts +180 -0
  349. package/src/types/cli.ts +5 -238
  350. package/src/types/core.ts +52 -10
  351. package/src/types/index.ts +5 -0
  352. package/src/types/interfaces.ts +24 -6
  353. package/src/types/profiler.ts +135 -0
  354. package/src/types/utility.ts +0 -10
  355. package/src/utils/identifiers.ts +58 -0
  356. package/src/utils/package.ts +35 -0
  357. package/src/utils/type-guards.ts +51 -0
@@ -13,8 +13,10 @@ import { ansiChars, colors } from "../utils/ansi.js";
13
13
  export class HumanReporter extends BaseReporter {
14
14
  currentFile = '';
15
15
  currentSuite = '';
16
+ currentSuiteMaxNameLen = 0; // Track max name length for current suite alignment
16
17
  failures = [];
17
18
  lastProgressLine = '';
19
+ maxTimePadWidth = 0; // Track maximum time padding width to prevent jitter
18
20
  progressWindowActive = false; // Track if progress window is rendered
19
21
  quiet;
20
22
  showProgress;
@@ -34,6 +36,63 @@ export class HumanReporter extends BaseReporter {
34
36
  this.quiet = options.quiet ?? false;
35
37
  this.showProgress = options.progress ?? true;
36
38
  }
39
+ /**
40
+ * Format bytes in human-readable format
41
+ */
42
+ static formatBytes(bytes) {
43
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
44
+ let size = bytes;
45
+ let unitIndex = 0;
46
+ while (size >= 1024 && unitIndex < units.length - 1) {
47
+ size /= 1024;
48
+ unitIndex++;
49
+ }
50
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
51
+ }
52
+ /**
53
+ * Format file path - show relative path if within CWD, otherwise absolute
54
+ */
55
+ static formatPath(filePath) {
56
+ const cwd = process.cwd();
57
+ const absolutePath = path.resolve(filePath);
58
+ // Check if the file is within the current working directory
59
+ if (absolutePath.startsWith(cwd + path.sep) || absolutePath === cwd) {
60
+ return path.relative(cwd, absolutePath);
61
+ }
62
+ return absolutePath;
63
+ }
64
+ /**
65
+ * Simple pluralization helper
66
+ */
67
+ static pluralize(str, count) {
68
+ return count === 1 ? str : `${str}s`;
69
+ }
70
+ onBudgetResult(summary) {
71
+ if (summary.total === 0 || this.quiet) {
72
+ return;
73
+ }
74
+ this.clearProgress();
75
+ this.printLine();
76
+ const budgetHeader = `${this.colorize('magenta', ansiChars.block.full.repeat(2))} ${this.colorize('brightWhite', this.colorize('bold', 'Performance Budgets'))}`;
77
+ this.printLine(budgetHeader);
78
+ this.printLine();
79
+ for (const result of summary.results) {
80
+ const icon = result.passed ? ansiChars.checkmark : ansiChars.cross;
81
+ const iconColor = result.passed ? 'brightCyan' : 'brightRed';
82
+ this.printLine(` ${this.colorize(iconColor, icon)} ${this.colorize('white', result.taskId)}`);
83
+ if (!result.passed && result.violations.length > 0) {
84
+ for (const violation of result.violations) {
85
+ this.printLine(` ${this.colorize('brightRed', violation.message)}`);
86
+ }
87
+ }
88
+ }
89
+ this.printLine();
90
+ const statusText = summary.failed === 0
91
+ ? `${this.colorize('brightCyan', ansiChars.checkmark)} All ${summary.total} budget(s) passed`
92
+ : `${this.colorize('brightRed', ansiChars.cross)} ${summary.failed} of ${summary.total} budget(s) failed`;
93
+ this.printLine(` ${statusText}`);
94
+ this.printLine();
95
+ }
37
96
  onEnd(run) {
38
97
  if (this.quiet) {
39
98
  return;
@@ -45,11 +104,13 @@ export class HumanReporter extends BaseReporter {
45
104
  let totalSuites = 0;
46
105
  let totalPassed = 0;
47
106
  let totalFailed = 0;
107
+ let totalAborted = 0;
48
108
  for (const file of run.files) {
49
109
  totalSuites += file.suites.length;
50
110
  for (const suite of file.suites) {
51
- totalPassed += suite.tasks.filter((t) => !t.error).length;
111
+ totalPassed += suite.tasks.filter((t) => !t.error && !t.aborted).length;
52
112
  totalFailed += suite.tasks.filter((t) => t.error).length;
113
+ totalAborted += suite.tasks.filter((t) => t.aborted).length;
53
114
  }
54
115
  }
55
116
  // Results header
@@ -58,15 +119,22 @@ export class HumanReporter extends BaseReporter {
58
119
  this.printLine();
59
120
  this.printLine(`${this.colorize('brightBlue', ' Files:')} ${this.colorize('brightWhite', String(totalFiles))}`);
60
121
  this.printLine(`${this.colorize('brightBlue', ' Suites:')} ${this.colorize('brightWhite', String(totalSuites))}`);
61
- this.printLine(`${this.colorize('brightBlue', ' Tasks:')} ${this.colorize('brightWhite', String(totalPassed + totalFailed))}`);
62
- if (totalFailed > 0) {
63
- this.printLine(`${this.colorize('brightRed', ansiChars.cross + ' Failed:')} ${this.colorize('brightWhite', String(totalFailed))}`);
64
- this.printLine(`${this.colorize('brightCyan', ansiChars.checkmark + ' Passed:')} ${this.colorize('brightWhite', String(totalPassed))}`);
122
+ this.printLine(`${this.colorize('brightBlue', ' Tasks:')} ${this.colorize('brightWhite', String(totalPassed + totalFailed + totalAborted))}`);
123
+ if (totalFailed > 0 || totalAborted > 0) {
124
+ if (totalFailed > 0) {
125
+ this.printLine(`${this.colorize('brightRed', ansiChars.cross + ' Failed:')} ${this.colorize('brightWhite', String(totalFailed))}`);
126
+ }
127
+ if (totalPassed > 0) {
128
+ this.printLine(`${this.colorize('brightCyan', ansiChars.checkmark + ' Passed:')} ${this.colorize('brightWhite', String(totalPassed))}`);
129
+ }
130
+ if (totalAborted > 0) {
131
+ this.printLine(`${this.colorize('brightYellow', ansiChars.approx + ' Aborted:')} ${this.colorize('brightWhite', String(totalAborted))}`);
132
+ }
65
133
  }
66
134
  else {
67
135
  this.printLine(`${this.colorize('brightCyan', ansiChars.checkmark + ' All tasks passed:')} ${this.colorize('brightWhite', String(totalPassed))}`);
68
136
  }
69
- this.printLine(`${this.colorize('cyan', ansiChars.approx + ' Duration:')} ${this.colorize('brightWhite', this.formatDuration(duration * 1000000))}`);
137
+ this.printLine(`${this.colorize('cyan', ansiChars.approx + ' Duration:')} ${this.colorize('brightWhite', BaseReporter.formatDuration(duration * 1000000))}`);
70
138
  this.printLine();
71
139
  if (totalFailed > 0) {
72
140
  // Display failed tasks with details
@@ -75,14 +143,15 @@ export class HumanReporter extends BaseReporter {
75
143
  this.printLine(this.colorize('brightRed', this.colorize('bold', 'Failed Tasks:')));
76
144
  this.printLine();
77
145
  for (const failure of this.failures) {
78
- const displayPath = this.formatPath(failure.file);
146
+ const displayPath = HumanReporter.formatPath(failure.file);
79
147
  this.printLine(` ${this.colorize('dim', displayPath)} ${this.colorize('dim', '›')} ${this.colorize('white', failure.suite)} ${this.colorize('dim', '›')} ${this.colorize('brightWhite', failure.task)}`);
80
148
  this.printLine(` ${this.colorize('brightRed', failure.error)}`);
81
149
  this.printLine();
82
150
  }
83
151
  }
84
152
  }
85
- else {
153
+ else if (totalAborted === 0) {
154
+ // Only show "Rad" if no failures AND no aborts
86
155
  const successMessage = `${this.colorize('brightMagenta', 'Rad. ☮')}`;
87
156
  this.printLine(successMessage);
88
157
  }
@@ -108,7 +177,7 @@ export class HumanReporter extends BaseReporter {
108
177
  this.printLine(this.colorize('red', ` ${ansiChars.cross} ${totalFailed} failed, ${totalPassed} passed`));
109
178
  }
110
179
  else {
111
- this.printLine(` ${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`)}`);
180
+ this.printLine(` ${this.colorize('magenta', ansiChars.checkmark)} ${totalPassed > 1 ? this.colorize('brightMagenta', 'All ') : ''}${this.colorize('bold', this.colorize('brightMagenta', `${totalPassed}`))} ${this.colorize('brightMagenta', `${HumanReporter.pluralize('task', totalPassed)} passed`)}`);
112
181
  }
113
182
  this.printLine();
114
183
  }
@@ -117,7 +186,7 @@ export class HumanReporter extends BaseReporter {
117
186
  if (this.quiet) {
118
187
  return;
119
188
  }
120
- const displayPath = this.formatPath(file);
189
+ const displayPath = HumanReporter.formatPath(file);
121
190
  const fileMarker = `${colors.magenta}${ansiChars.block.dark}${ansiChars.block.dark}${colors.reset}`;
122
191
  this.printLine(`${fileMarker} ${colors.underline}${this.colorize('brightMagenta', this.colorize('bold', displayPath))}${colors.reset}`);
123
192
  }
@@ -130,7 +199,7 @@ export class HumanReporter extends BaseReporter {
130
199
  if (!process.stdout.isTTY) {
131
200
  return;
132
201
  }
133
- const { elapsed, percentage, tasksCompleted, totalTasks } = state;
202
+ const { currentTask, elapsed, percentage, tasksCompleted, totalTasks } = state;
134
203
  // Pad task counts for alignment
135
204
  const totalTasksWidth = String(totalTasks).length;
136
205
  const paddedTasksCompleted = String(tasksCompleted).padStart(totalTasksWidth, ' ');
@@ -139,7 +208,7 @@ export class HumanReporter extends BaseReporter {
139
208
  const elapsedStrRaw = this.formatTimeRemaining(elapsedSeconds);
140
209
  // Calculate ETA if we have completed tasks and determine padding width
141
210
  let etaStr = '';
142
- let padWidth = elapsedStrRaw.length;
211
+ let padWidth = Math.max(this.maxTimePadWidth, elapsedStrRaw.length);
143
212
  if (tasksCompleted > 0) {
144
213
  const avgTimePerTask = elapsed / tasksCompleted;
145
214
  const remainingTasks = totalTasks - tasksCompleted;
@@ -147,12 +216,21 @@ export class HumanReporter extends BaseReporter {
147
216
  const etaSeconds = Math.round(etaMs / 1000);
148
217
  const etaTimeStr = this.formatTimeRemaining(etaSeconds);
149
218
  padWidth = Math.max(padWidth, etaTimeStr.length);
150
- etaStr = ` ${this.colorize('dim', '|')} ${this.colorize('dim', 'ETA:')} ${this.colorize('brightBlue', etaTimeStr)}`;
219
+ etaStr = ` ${this.colorize('gray', '|')} ${this.colorize('gray', 'ETA:')} ${this.colorize('brightBlue', etaTimeStr)}`;
151
220
  }
221
+ // Remember the maximum width we've ever used to prevent jitter
222
+ this.maxTimePadWidth = Math.max(this.maxTimePadWidth, padWidth);
152
223
  // Pad elapsed time to match the longest time string
153
- const elapsedStr = elapsedStrRaw.padStart(padWidth, ' ');
224
+ const elapsedStr = elapsedStrRaw.padStart(this.maxTimePadWidth, ' ');
154
225
  const roundedPercentage = percentage.toFixed(2);
155
- 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}`;
226
+ // Build progress line with current task if available
227
+ let line = `${this.colorize('brightCyan', ansiChars.approx)} ${this.colorize('white', paddedTasksCompleted)}${this.colorize('gray', '/')}${this.colorize('white', String(totalTasks))} ${this.colorize('gray', 'tasks')} ${this.colorize('gray', '(')}${this.colorize('brightBlue', roundedPercentage + '%')}${this.colorize('gray', ')')} ${this.colorize('gray', '|')} ${this.colorize('gray', 'Elapsed:')} ${this.colorize('cyan', elapsedStr)}${etaStr}`;
228
+ if (currentTask) {
229
+ const truncatedTask = currentTask.length > 60
230
+ ? currentTask.substring(0, 57) + '...'
231
+ : currentTask;
232
+ line += ` ${this.colorize('gray', '|')} ${this.colorize('white', truncatedTask)}`;
233
+ }
156
234
  this.lastProgressLine = line;
157
235
  this.renderProgressWindow();
158
236
  }
@@ -160,6 +238,7 @@ export class HumanReporter extends BaseReporter {
160
238
  this.startTime = Date.now();
161
239
  this.failures = []; // Reset failures for new run
162
240
  this.lastProgressLine = ''; // Reset for new run
241
+ this.maxTimePadWidth = 0; // Reset time padding width for new run
163
242
  if (this.quiet) {
164
243
  return;
165
244
  }
@@ -175,7 +254,7 @@ export class HumanReporter extends BaseReporter {
175
254
  \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
176
255
  \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
177
256
  \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[2m) \x1b[m
178
- \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
257
+ \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${HumanReporter.formatBytes(run.environment.memory.total)} \x1b[m
179
258
  \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
180
259
  \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
181
260
  `;
@@ -210,22 +289,53 @@ export class HumanReporter extends BaseReporter {
210
289
  if (this.quiet) {
211
290
  return;
212
291
  }
213
- // Print all buffered task results with aligned columns
214
- this.printAlignedSuiteResults();
292
+ // Tasks are printed immediately in onTaskResult, so just print suite summary
215
293
  // Skip displaying summary for the implicit "default" suite
216
294
  if (result.name === 'default') {
217
295
  return;
218
296
  }
219
- const passed = result.tasks.filter((t) => !t.error).length;
297
+ const passed = result.tasks.filter((t) => !t.error && !t.aborted).length;
220
298
  const failed = result.tasks.filter((t) => t.error).length;
299
+ const aborted = result.tasks.filter((t) => t.aborted).length;
300
+ const durationStr = BaseReporter.formatDuration(result.duration * 1000000); // ms to ns
301
+ // Build summary parts
302
+ const parts = [];
221
303
  if (failed > 0) {
222
- this.printLine(` ${this.colorize('red', `${ansiChars.cross} ${failed} failed`)}, ${this.colorize('green', `${passed} passed`)}`);
304
+ parts.push(this.colorize('red', `${ansiChars.cross} ${failed} failed`));
305
+ }
306
+ if (passed > 0) {
307
+ parts.push(this.colorize('green', `${passed} passed`));
308
+ }
309
+ if (aborted > 0) {
310
+ parts.push(this.colorize('brightYellow', `${aborted} aborted`));
311
+ }
312
+ const summary = parts.join(', ');
313
+ const timeInfo = `${this.colorize('gray', 'in')} ${this.colorize('cyan', durationStr)}`;
314
+ if (failed > 0 || aborted > 0) {
315
+ this.printLine(` ${summary} ${timeInfo}`);
223
316
  }
224
317
  else {
225
- this.printLine(` ${this.colorize('magenta', ansiChars.checkmark)} ${this.colorize('bold', this.colorize('brightWhite', `${passed}`))} ${this.colorize('brightWhite', `${this.pluralize('task', passed)} passed`)}`);
318
+ this.printLine(` ${this.colorize('magenta', ansiChars.checkmark)} ${this.colorize('bold', this.colorize('brightWhite', `${passed}`))} ${this.colorize('brightWhite', `${HumanReporter.pluralize('task', passed)} passed`)} ${timeInfo}`);
226
319
  }
227
320
  this.printLine();
228
321
  }
322
+ onSuiteInit(suite, taskNames) {
323
+ // Pre-calculate max name length for optimal alignment
324
+ const terminalWidth = process.stdout.columns || 80;
325
+ const STATS_RESERVED_WIDTH = 70;
326
+ const MAX_NAME_WIDTH = Math.max(40, Math.min(60, terminalWidth - 4 - 2 - 2 - STATS_RESERVED_WIDTH));
327
+ // Calculate the actual max name length from non-wrapped names
328
+ let maxLen = 0;
329
+ for (const name of taskNames) {
330
+ const nameLen = this.getVisibleLength(name.trim());
331
+ // Only count names that won't wrap
332
+ if (nameLen <= MAX_NAME_WIDTH) {
333
+ maxLen = Math.max(maxLen, nameLen);
334
+ }
335
+ }
336
+ // Use the max of actual names or MAX_NAME_WIDTH for consistency
337
+ this.currentSuiteMaxNameLen = Math.max(maxLen, MAX_NAME_WIDTH);
338
+ }
229
339
  onSuiteStart(suite) {
230
340
  this.currentSuite = suite;
231
341
  if (this.quiet) {
@@ -244,8 +354,14 @@ export class HumanReporter extends BaseReporter {
244
354
  if (this.quiet) {
245
355
  return;
246
356
  }
247
- // Buffer the result for later printing with proper alignment
357
+ // Always buffer the result for suite summary (including aborted tasks)
248
358
  this.suiteResults.push(result);
359
+ // Skip printing aborted tasks (they're counted in summary but not shown individually)
360
+ if (result.aborted) {
361
+ return;
362
+ }
363
+ // Print immediately with current alignment
364
+ this.printTaskResult(result);
249
365
  }
250
366
  onTaskStart(task) {
251
367
  if (this.quiet) {
@@ -285,31 +401,6 @@ export class HumanReporter extends BaseReporter {
285
401
  }
286
402
  return `${colors[color]}${text}${colors.reset}`;
287
403
  }
288
- /**
289
- * Format bytes in human-readable format
290
- */
291
- formatBytes(bytes) {
292
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
293
- let size = bytes;
294
- let unitIndex = 0;
295
- while (size >= 1024 && unitIndex < units.length - 1) {
296
- size /= 1024;
297
- unitIndex++;
298
- }
299
- return `${size.toFixed(1)} ${units[unitIndex]}`;
300
- }
301
- /**
302
- * Format file path - show relative path if within CWD, otherwise absolute
303
- */
304
- formatPath(filePath) {
305
- const cwd = process.cwd();
306
- const absolutePath = path.resolve(filePath);
307
- // Check if the file is within the current working directory
308
- if (absolutePath.startsWith(cwd + path.sep) || absolutePath === cwd) {
309
- return path.relative(cwd, absolutePath);
310
- }
311
- return absolutePath;
312
- }
313
404
  /**
314
405
  * Format duration in human-readable format for progress display
315
406
  */
@@ -336,12 +427,6 @@ export class HumanReporter extends BaseReporter {
336
427
  // eslint-disable-next-line no-control-regex
337
428
  return str.replace(/\x1b\[[0-9;]*m/g, '').length;
338
429
  }
339
- /**
340
- * Simple pluralization helper
341
- */
342
- pluralize(str, count) {
343
- return count === 1 ? str : `${str}s`;
344
- }
345
430
  /**
346
431
  * Print all task results in a suite with aligned columns
347
432
  */
@@ -349,10 +434,17 @@ export class HumanReporter extends BaseReporter {
349
434
  if (this.suiteResults.length === 0) {
350
435
  return;
351
436
  }
352
- const MAX_NAME_WIDTH = 60;
353
437
  const BASE_INDENT = ' '; // 4 spaces
354
438
  const bullet = this.colorize('dim', this.colorize('gray', ansiChars.bullet));
355
- const formatted = this.suiteResults.map((result) => {
439
+ // Calculate maximum name width based on terminal width
440
+ // Reserve space for: indent (4) + status (1) + space (1) + name + ": " (2) + stats (~60 chars)
441
+ const terminalWidth = process.stdout.columns || 80;
442
+ const STATS_RESERVED_WIDTH = 70; // Approx space for duration + rme + ops/sec with padding
443
+ const MAX_NAME_WIDTH = Math.max(40, Math.min(60, terminalWidth - BASE_INDENT.length - 2 - 2 - STATS_RESERVED_WIDTH));
444
+ // Filter out aborted tasks (they're counted in suite summary but not printed)
445
+ const formatted = this.suiteResults
446
+ .filter((result) => !result.aborted)
447
+ .map((result) => {
356
448
  const status = result.error
357
449
  ? this.colorize('red', ansiChars.cross)
358
450
  : this.colorize('brightCyan', ansiChars.checkmark);
@@ -374,9 +466,9 @@ export class HumanReporter extends BaseReporter {
374
466
  status,
375
467
  };
376
468
  }
377
- const duration = this.formatDuration(result.mean); // already in nanoseconds
378
- const opsPerSec = this.formatOpsPerSecond(result.opsPerSecond);
379
- const rme = this.formatPercentage(result.marginOfError * 100);
469
+ const duration = BaseReporter.formatDuration(result.mean); // already in nanoseconds
470
+ const opsPerSec = BaseReporter.formatOpsPerSecond(result.opsPerSecond);
471
+ const rme = BaseReporter.formatPercentage(result.marginOfError); // already a percentage
380
472
  return {
381
473
  durationLen: this.getVisibleLength(duration),
382
474
  durationStr: duration,
@@ -399,9 +491,6 @@ export class HumanReporter extends BaseReporter {
399
491
  const maxDurationLen = Math.max(...formatted.filter((t) => !t.error).map((t) => t.durationLen), 0);
400
492
  const maxRmeLen = Math.max(...formatted.filter((t) => !t.error).map((t) => t.rmeLen), 0);
401
493
  const maxOpsLen = Math.max(...formatted.filter((t) => !t.error).map((t) => t.opsPerSecLen), 0);
402
- // Calculate the position where numbers start for unwrapped lines
403
- // BASE_INDENT (4) + status (1 char) + space (1) + maxNameLen + ": " (2) = 8 + maxNameLen
404
- const numbersStartPos = BASE_INDENT.length + 2 + maxNameLen + 2;
405
494
  // Print each task with aligned columns
406
495
  for (const task of formatted) {
407
496
  if (task.error) {
@@ -415,15 +504,33 @@ export class HumanReporter extends BaseReporter {
415
504
  this.printLine(`${BASE_INDENT}${task.status} ${this.colorize('white', task.name)} ${this.colorize('red', 'FAILED')}`);
416
505
  }
417
506
  else if (task.nameLength > MAX_NAME_WIDTH) {
418
- // Long name - wrap to next line, but align numbers with unwrapped lines
419
- this.printLine(`${BASE_INDENT}${task.status} ${this.colorize('white', task.name)}:`);
420
- // Calculate padding to align with unwrapped lines
421
- // We need to get to numbersStartPos from the beginning of the line
422
- const leadingPad = ' '.repeat(numbersStartPos);
507
+ // Long name - wrap to multiple lines, align last line with short names
508
+ const wrappedLines = this.wrapText(task.name, MAX_NAME_WIDTH);
509
+ const continueIndent = BASE_INDENT + ' '; // 6 spaces for continuation lines
510
+ // Format stats string
423
511
  const durationPad = ' '.repeat(maxDurationLen - task.durationLen);
424
512
  const rmePad = ' '.repeat(maxRmeLen - task.rmeLen);
425
513
  const opsPad = ' '.repeat(maxOpsLen - task.opsPerSecLen);
426
- this.printLine(`${leadingPad}${durationPad}${this.colorize('cyan', task.durationStr)} ${bullet} ${ansiChars.plusMinus}${rmePad}${this.colorize('brightBlue', task.rmeStr)} ${bullet} ${opsPad}${this.colorize('magenta', task.opsPerSecStr)}`);
514
+ const statsStr = `${durationPad}${this.colorize('cyan', task.durationStr)} ${bullet} ${ansiChars.plusMinus}${rmePad}${this.colorize('brightBlue', task.rmeStr)} ${bullet} ${opsPad}${this.colorize('magenta', task.opsPerSecStr)}`;
515
+ // Print first line with status
516
+ this.printLine(`${BASE_INDENT}${task.status} ${this.colorize('white', wrappedLines[0])}`);
517
+ // Print middle continuation lines (all but first and last)
518
+ for (let i = 1; i < wrappedLines.length - 1; i++) {
519
+ this.printLine(`${continueIndent}${this.colorize('white', wrappedLines[i])}`);
520
+ }
521
+ // Print last line with colon and stats aligned with short names
522
+ if (wrappedLines.length > 1) {
523
+ const lastLine = wrappedLines[wrappedLines.length - 1];
524
+ const lastLineLen = this.getVisibleLength(lastLine);
525
+ // Pad the last line to align the ':' with short names
526
+ const lastLinePad = ' '.repeat(Math.max(0, maxNameLen - lastLineLen));
527
+ this.printLine(`${continueIndent}${this.colorize('white', lastLine)}${lastLinePad}: ${statsStr}`);
528
+ }
529
+ else {
530
+ // Single wrapped line
531
+ const lastLinePad = ' '.repeat(maxNameLen - task.nameLength);
532
+ this.printLine(`${BASE_INDENT}${task.status} ${this.colorize('white', task.name)}${lastLinePad}: ${statsStr}`);
533
+ }
427
534
  if (this.verbose && task.iterations > 0) {
428
535
  this.printLine(` ${this.colorize('dim', `${task.iterations} iterations`)}`);
429
536
  }
@@ -451,6 +558,84 @@ export class HumanReporter extends BaseReporter {
451
558
  console.log(message);
452
559
  this.renderProgressWindow();
453
560
  }
561
+ /**
562
+ * Print a single task result immediately with current alignment
563
+ */
564
+ printTaskResult(result) {
565
+ // Clear progress bar temporarily
566
+ this.clearProgress();
567
+ const BASE_INDENT = ' '; // 4 spaces
568
+ const bullet = this.colorize('dim', this.colorize('gray', ansiChars.bullet));
569
+ // Calculate terminal width constraints
570
+ const terminalWidth = process.stdout.columns || 80;
571
+ const STATS_RESERVED_WIDTH = 70;
572
+ const MAX_NAME_WIDTH = Math.max(40, Math.min(60, terminalWidth - BASE_INDENT.length - 2 - 2 - STATS_RESERVED_WIDTH));
573
+ // Status marker
574
+ const status = result.error
575
+ ? this.colorize('red', ansiChars.cross)
576
+ : this.colorize('brightCyan', ansiChars.checkmark);
577
+ const name = result.name.trim();
578
+ const nameLength = this.getVisibleLength(name);
579
+ // Handle errors
580
+ if (result.error) {
581
+ this.failures.push({
582
+ error: result.error?.message || String(result.error),
583
+ file: this.currentFile,
584
+ suite: this.currentSuite,
585
+ task: name,
586
+ });
587
+ this.printLine(`${BASE_INDENT}${status} ${this.colorize('white', name)} ${this.colorize('red', 'FAILED')}`);
588
+ return;
589
+ }
590
+ // Format stats
591
+ const duration = BaseReporter.formatDuration(result.mean);
592
+ const opsPerSec = BaseReporter.formatOpsPerSecond(result.opsPerSecond);
593
+ const rme = BaseReporter.formatPercentage(result.marginOfError);
594
+ // Use fixed widths for stats columns (reasonable maximums)
595
+ const DURATION_WIDTH = 10; // "999.99ms" max
596
+ const RME_WIDTH = 8; // "±999.99%" max
597
+ const OPS_WIDTH = 15; // "999.99K ops/sec" max
598
+ const durationLen = this.getVisibleLength(duration);
599
+ const rmeLen = this.getVisibleLength(rme);
600
+ const opsLen = this.getVisibleLength(opsPerSec);
601
+ // Stats formatting with fixed widths
602
+ const durationPad = ' '.repeat(DURATION_WIDTH - durationLen);
603
+ const rmePad = ' '.repeat(RME_WIDTH - rmeLen);
604
+ const opsPad = ' '.repeat(OPS_WIDTH - opsLen);
605
+ const statsStr = `${durationPad}${this.colorize('cyan', duration)} ${bullet} ${ansiChars.plusMinus}${rmePad}${this.colorize('brightBlue', rme)} ${bullet} ${opsPad}${this.colorize('magenta', opsPerSec)}`;
606
+ // Handle long names (wrap)
607
+ if (nameLength > MAX_NAME_WIDTH) {
608
+ const wrappedLines = this.wrapText(name, MAX_NAME_WIDTH);
609
+ const continueIndent = BASE_INDENT + ' '; // 6 spaces for continuation lines
610
+ // Print first line with status
611
+ this.printLine(`${BASE_INDENT}${status} ${this.colorize('white', wrappedLines[0])}`);
612
+ // Print middle lines (all but first and last)
613
+ for (let i = 1; i < wrappedLines.length - 1; i++) {
614
+ this.printLine(`${continueIndent}${this.colorize('white', wrappedLines[i])}`);
615
+ }
616
+ // Print last line with colon and stats aligned
617
+ // Use pre-calculated currentSuiteMaxNameLen for perfect alignment
618
+ if (wrappedLines.length > 1) {
619
+ const lastLine = wrappedLines[wrappedLines.length - 1];
620
+ const lastLineLen = this.getVisibleLength(lastLine);
621
+ const lastLinePad = ' '.repeat(Math.max(0, this.currentSuiteMaxNameLen - lastLineLen));
622
+ this.printLine(`${continueIndent}${this.colorize('white', lastLine)}${lastLinePad}: ${statsStr}`);
623
+ }
624
+ else {
625
+ // Single wrapped line (shouldn't happen if nameLength > MAX but handle it)
626
+ const lastLinePad = ' '.repeat(Math.max(0, this.currentSuiteMaxNameLen - nameLength));
627
+ this.printLine(`${BASE_INDENT}${status} ${this.colorize('white', name)}${lastLinePad}: ${statsStr}`);
628
+ }
629
+ }
630
+ else {
631
+ // Normal length - print on same line with pre-calculated alignment
632
+ const namePad = ' '.repeat(Math.max(0, this.currentSuiteMaxNameLen - nameLength));
633
+ this.printLine(`${BASE_INDENT}${status} ${this.colorize('white', name)}${namePad}: ${statsStr}`);
634
+ }
635
+ if (this.verbose && result.iterations > 0) {
636
+ this.printLine(` ${this.colorize('dim', `${result.iterations} iterations`)}`);
637
+ }
638
+ }
454
639
  /**
455
640
  * Render the progress window at the bottom
456
641
  */
@@ -467,5 +652,43 @@ export class HumanReporter extends BaseReporter {
467
652
  console.log(this.lastProgressLine);
468
653
  this.progressWindowActive = true;
469
654
  }
655
+ /**
656
+ * Wrap text to a maximum width, breaking at word boundaries when possible
657
+ */
658
+ wrapText(text, maxWidth) {
659
+ if (this.getVisibleLength(text) <= maxWidth) {
660
+ return [text];
661
+ }
662
+ const lines = [];
663
+ let currentLine = '';
664
+ const words = text.split(/(\s+)/); // Keep whitespace in split
665
+ for (const word of words) {
666
+ const testLine = currentLine + word;
667
+ if (this.getVisibleLength(testLine) <= maxWidth) {
668
+ currentLine = testLine;
669
+ }
670
+ else {
671
+ // If current line has content, save it
672
+ if (currentLine.trim()) {
673
+ lines.push(currentLine.trimEnd());
674
+ currentLine = word.trim() + ' ';
675
+ }
676
+ else {
677
+ // Single word is too long, force break it
678
+ if (this.getVisibleLength(word) > maxWidth) {
679
+ lines.push(word.substring(0, maxWidth));
680
+ currentLine = word.substring(maxWidth);
681
+ }
682
+ else {
683
+ currentLine = word;
684
+ }
685
+ }
686
+ }
687
+ }
688
+ if (currentLine.trim()) {
689
+ lines.push(currentLine.trimEnd());
690
+ }
691
+ return lines;
692
+ }
470
693
  }
471
694
  //# sourceMappingURL=human.js.map