modestbench 0.2.0 → 0.3.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 (332) hide show
  1. package/CHANGELOG.md +20 -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 +88 -155
  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 +88 -155
  30. package/dist/cli/commands/init.js.map +1 -1
  31. package/dist/cli/commands/run.cjs +132 -114
  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 +131 -80
  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 +103 -21
  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 +104 -22
  78. package/dist/core/engine.js.map +1 -1
  79. package/dist/core/output-path-resolver.cjs +8 -1
  80. package/dist/core/output-path-resolver.cjs.map +1 -1
  81. package/dist/core/output-path-resolver.d.cts.map +1 -1
  82. package/dist/core/output-path-resolver.d.ts.map +1 -1
  83. package/dist/core/output-path-resolver.js +9 -2
  84. package/dist/core/output-path-resolver.js.map +1 -1
  85. package/dist/errors/base.cjs +12 -3
  86. package/dist/errors/base.cjs.map +1 -1
  87. package/dist/errors/base.d.cts +7 -0
  88. package/dist/errors/base.d.cts.map +1 -1
  89. package/dist/errors/base.d.ts +7 -0
  90. package/dist/errors/base.d.ts.map +1 -1
  91. package/dist/errors/base.js +10 -2
  92. package/dist/errors/base.js.map +1 -1
  93. package/dist/errors/budget.cjs +37 -0
  94. package/dist/errors/budget.cjs.map +1 -0
  95. package/dist/errors/budget.d.cts +31 -0
  96. package/dist/errors/budget.d.cts.map +1 -0
  97. package/dist/errors/budget.d.ts +31 -0
  98. package/dist/errors/budget.d.ts.map +1 -0
  99. package/dist/errors/budget.js +33 -0
  100. package/dist/errors/budget.js.map +1 -0
  101. package/dist/errors/index.cjs +4 -1
  102. package/dist/errors/index.cjs.map +1 -1
  103. package/dist/errors/index.d.cts +1 -0
  104. package/dist/errors/index.d.cts.map +1 -1
  105. package/dist/errors/index.d.ts +1 -0
  106. package/dist/errors/index.d.ts.map +1 -1
  107. package/dist/errors/index.js +2 -0
  108. package/dist/errors/index.js.map +1 -1
  109. package/dist/index.cjs +13 -1
  110. package/dist/index.cjs.map +1 -1
  111. package/dist/index.d.cts +5 -0
  112. package/dist/index.d.cts.map +1 -1
  113. package/dist/index.d.ts +5 -0
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +7 -0
  116. package/dist/index.js.map +1 -1
  117. package/dist/reporters/csv.cjs +37 -17
  118. package/dist/reporters/csv.cjs.map +1 -1
  119. package/dist/reporters/csv.d.cts +3 -6
  120. package/dist/reporters/csv.d.cts.map +1 -1
  121. package/dist/reporters/csv.d.ts +3 -6
  122. package/dist/reporters/csv.d.ts.map +1 -1
  123. package/dist/reporters/csv.js +37 -17
  124. package/dist/reporters/csv.js.map +1 -1
  125. package/dist/reporters/human.cjs +66 -40
  126. package/dist/reporters/human.cjs.map +1 -1
  127. package/dist/reporters/human.d.cts +14 -13
  128. package/dist/reporters/human.d.cts.map +1 -1
  129. package/dist/reporters/human.d.ts +14 -13
  130. package/dist/reporters/human.d.ts.map +1 -1
  131. package/dist/reporters/human.js +66 -40
  132. package/dist/reporters/human.js.map +1 -1
  133. package/dist/reporters/json.cjs +23 -48
  134. package/dist/reporters/json.cjs.map +1 -1
  135. package/dist/reporters/json.d.cts +2 -28
  136. package/dist/reporters/json.d.cts.map +1 -1
  137. package/dist/reporters/json.d.ts +2 -28
  138. package/dist/reporters/json.d.ts.map +1 -1
  139. package/dist/reporters/json.js +25 -50
  140. package/dist/reporters/json.js.map +1 -1
  141. package/dist/reporters/profile-human.cjs +149 -0
  142. package/dist/reporters/profile-human.cjs.map +1 -0
  143. package/dist/reporters/profile-human.d.cts +44 -0
  144. package/dist/reporters/profile-human.d.cts.map +1 -0
  145. package/dist/reporters/profile-human.d.ts +44 -0
  146. package/dist/reporters/profile-human.d.ts.map +1 -0
  147. package/dist/reporters/profile-human.js +142 -0
  148. package/dist/reporters/profile-human.js.map +1 -0
  149. package/dist/reporters/simple.cjs +64 -44
  150. package/dist/reporters/simple.cjs.map +1 -1
  151. package/dist/reporters/simple.d.cts +14 -14
  152. package/dist/reporters/simple.d.cts.map +1 -1
  153. package/dist/reporters/simple.d.ts +14 -14
  154. package/dist/reporters/simple.d.ts.map +1 -1
  155. package/dist/reporters/simple.js +64 -44
  156. package/dist/reporters/simple.js.map +1 -1
  157. package/dist/schema/modestbench-config.schema.json +153 -0
  158. package/dist/services/baseline-storage.cjs +151 -0
  159. package/dist/services/baseline-storage.cjs.map +1 -0
  160. package/dist/services/baseline-storage.d.cts +55 -0
  161. package/dist/services/baseline-storage.d.cts.map +1 -0
  162. package/dist/services/baseline-storage.d.ts +55 -0
  163. package/dist/services/baseline-storage.d.ts.map +1 -0
  164. package/dist/services/baseline-storage.js +147 -0
  165. package/dist/services/baseline-storage.js.map +1 -0
  166. package/dist/services/budget-evaluator.cjs +146 -0
  167. package/dist/services/budget-evaluator.cjs.map +1 -0
  168. package/dist/services/budget-evaluator.d.cts +29 -0
  169. package/dist/services/budget-evaluator.d.cts.map +1 -0
  170. package/dist/services/budget-evaluator.d.ts +29 -0
  171. package/dist/services/budget-evaluator.d.ts.map +1 -0
  172. package/dist/services/budget-evaluator.js +142 -0
  173. package/dist/services/budget-evaluator.js.map +1 -0
  174. package/dist/services/config-manager.cjs +23 -9
  175. package/dist/services/config-manager.cjs.map +1 -1
  176. package/dist/services/config-manager.d.cts +6 -1
  177. package/dist/services/config-manager.d.cts.map +1 -1
  178. package/dist/services/config-manager.d.ts +6 -1
  179. package/dist/services/config-manager.d.ts.map +1 -1
  180. package/dist/services/config-manager.js +23 -9
  181. package/dist/services/config-manager.js.map +1 -1
  182. package/dist/services/file-loader.cjs +3 -6
  183. package/dist/services/file-loader.cjs.map +1 -1
  184. package/dist/services/file-loader.d.cts.map +1 -1
  185. package/dist/services/file-loader.d.ts.map +1 -1
  186. package/dist/services/file-loader.js +3 -6
  187. package/dist/services/file-loader.js.map +1 -1
  188. package/dist/services/profiler/profile-filter.cjs +113 -0
  189. package/dist/services/profiler/profile-filter.cjs.map +1 -0
  190. package/dist/services/profiler/profile-filter.d.cts +20 -0
  191. package/dist/services/profiler/profile-filter.d.cts.map +1 -0
  192. package/dist/services/profiler/profile-filter.d.ts +20 -0
  193. package/dist/services/profiler/profile-filter.d.ts.map +1 -0
  194. package/dist/services/profiler/profile-filter.js +109 -0
  195. package/dist/services/profiler/profile-filter.js.map +1 -0
  196. package/dist/services/profiler/profile-parser.cjs +139 -0
  197. package/dist/services/profiler/profile-parser.cjs.map +1 -0
  198. package/dist/services/profiler/profile-parser.d.cts +18 -0
  199. package/dist/services/profiler/profile-parser.d.cts.map +1 -0
  200. package/dist/services/profiler/profile-parser.d.ts +18 -0
  201. package/dist/services/profiler/profile-parser.d.ts.map +1 -0
  202. package/dist/services/profiler/profile-parser.js +132 -0
  203. package/dist/services/profiler/profile-parser.js.map +1 -0
  204. package/dist/services/profiler/profile-runner.cjs +90 -0
  205. package/dist/services/profiler/profile-runner.cjs.map +1 -0
  206. package/dist/services/profiler/profile-runner.d.cts +29 -0
  207. package/dist/services/profiler/profile-runner.d.cts.map +1 -0
  208. package/dist/services/profiler/profile-runner.d.ts +29 -0
  209. package/dist/services/profiler/profile-runner.d.ts.map +1 -0
  210. package/dist/services/profiler/profile-runner.js +86 -0
  211. package/dist/services/profiler/profile-runner.js.map +1 -0
  212. package/dist/services/reporter-registry.cjs +18 -24
  213. package/dist/services/reporter-registry.cjs.map +1 -1
  214. package/dist/services/reporter-registry.d.cts +18 -40
  215. package/dist/services/reporter-registry.d.cts.map +1 -1
  216. package/dist/services/reporter-registry.d.ts +18 -40
  217. package/dist/services/reporter-registry.d.ts.map +1 -1
  218. package/dist/services/reporter-registry.js +18 -24
  219. package/dist/services/reporter-registry.js.map +1 -1
  220. package/dist/types/budgets.cjs +8 -0
  221. package/dist/types/budgets.cjs.map +1 -0
  222. package/dist/types/budgets.d.cts +149 -0
  223. package/dist/types/budgets.d.cts.map +1 -0
  224. package/dist/types/budgets.d.ts +149 -0
  225. package/dist/types/budgets.d.ts.map +1 -0
  226. package/dist/types/budgets.js +7 -0
  227. package/dist/types/budgets.js.map +1 -0
  228. package/dist/types/cli.cjs +2 -11
  229. package/dist/types/cli.cjs.map +1 -1
  230. package/dist/types/cli.d.cts +3 -227
  231. package/dist/types/cli.d.cts.map +1 -1
  232. package/dist/types/cli.d.ts +3 -227
  233. package/dist/types/cli.d.ts.map +1 -1
  234. package/dist/types/cli.js +2 -11
  235. package/dist/types/cli.js.map +1 -1
  236. package/dist/types/core.cjs +6 -1
  237. package/dist/types/core.cjs.map +1 -1
  238. package/dist/types/core.d.cts +13 -2
  239. package/dist/types/core.d.cts.map +1 -1
  240. package/dist/types/core.d.ts +13 -2
  241. package/dist/types/core.d.ts.map +1 -1
  242. package/dist/types/core.js +2 -1
  243. package/dist/types/core.js.map +1 -1
  244. package/dist/types/index.cjs +5 -0
  245. package/dist/types/index.cjs.map +1 -1
  246. package/dist/types/index.d.cts +2 -0
  247. package/dist/types/index.d.cts.map +1 -1
  248. package/dist/types/index.d.ts +2 -0
  249. package/dist/types/index.d.ts.map +1 -1
  250. package/dist/types/index.js +2 -0
  251. package/dist/types/index.js.map +1 -1
  252. package/dist/types/interfaces.d.cts +15 -8
  253. package/dist/types/interfaces.d.cts.map +1 -1
  254. package/dist/types/interfaces.d.ts +15 -8
  255. package/dist/types/interfaces.d.ts.map +1 -1
  256. package/dist/types/profiler.cjs +11 -0
  257. package/dist/types/profiler.cjs.map +1 -0
  258. package/dist/types/profiler.d.cts +100 -0
  259. package/dist/types/profiler.d.cts.map +1 -0
  260. package/dist/types/profiler.d.ts +100 -0
  261. package/dist/types/profiler.d.ts.map +1 -0
  262. package/dist/types/profiler.js +10 -0
  263. package/dist/types/profiler.js.map +1 -0
  264. package/dist/types/utility.cjs.map +1 -1
  265. package/dist/types/utility.d.cts +0 -8
  266. package/dist/types/utility.d.cts.map +1 -1
  267. package/dist/types/utility.d.ts +0 -8
  268. package/dist/types/utility.d.ts.map +1 -1
  269. package/dist/types/utility.js.map +1 -1
  270. package/dist/utils/identifiers.cjs +32 -0
  271. package/dist/utils/identifiers.cjs.map +1 -0
  272. package/dist/utils/identifiers.d.cts +32 -0
  273. package/dist/utils/identifiers.d.cts.map +1 -0
  274. package/dist/utils/identifiers.d.ts +32 -0
  275. package/dist/utils/identifiers.d.ts.map +1 -0
  276. package/dist/utils/identifiers.js +27 -0
  277. package/dist/utils/identifiers.js.map +1 -0
  278. package/dist/utils/package.cjs +40 -0
  279. package/dist/utils/package.cjs.map +1 -0
  280. package/dist/utils/package.d.cts +15 -0
  281. package/dist/utils/package.d.cts.map +1 -0
  282. package/dist/utils/package.d.ts +15 -0
  283. package/dist/utils/package.d.ts.map +1 -0
  284. package/dist/utils/package.js +33 -0
  285. package/dist/utils/package.js.map +1 -0
  286. package/dist/utils/type-guards.cjs +48 -0
  287. package/dist/utils/type-guards.cjs.map +1 -0
  288. package/dist/utils/type-guards.d.cts +22 -0
  289. package/dist/utils/type-guards.d.cts.map +1 -0
  290. package/dist/utils/type-guards.d.ts +22 -0
  291. package/dist/utils/type-guards.d.ts.map +1 -0
  292. package/dist/utils/type-guards.js +43 -0
  293. package/dist/utils/type-guards.js.map +1 -0
  294. package/package.json +10 -10
  295. package/src/cli/commands/analyze.ts +101 -0
  296. package/src/cli/commands/baseline.ts +577 -0
  297. package/src/cli/commands/history.ts +1 -1
  298. package/src/cli/commands/init.ts +105 -183
  299. package/src/cli/commands/run.ts +167 -98
  300. package/src/cli/index.ts +425 -183
  301. package/src/config/budget-schema.ts +189 -0
  302. package/src/config/schema.ts +260 -1
  303. package/src/constants.ts +53 -1
  304. package/src/core/engine.ts +151 -20
  305. package/src/core/output-path-resolver.ts +10 -2
  306. package/src/errors/base.ts +11 -2
  307. package/src/errors/budget.ts +38 -0
  308. package/src/errors/index.ts +3 -0
  309. package/src/index.ts +9 -0
  310. package/src/reporters/csv.ts +54 -25
  311. package/src/reporters/human.ts +88 -47
  312. package/src/reporters/json.ts +26 -71
  313. package/src/reporters/profile-human.ts +204 -0
  314. package/src/reporters/simple.ts +84 -53
  315. package/src/services/baseline-storage.ts +199 -0
  316. package/src/services/budget-evaluator.ts +182 -0
  317. package/src/services/config-manager.ts +23 -8
  318. package/src/services/file-loader.ts +3 -6
  319. package/src/services/profiler/profile-filter.ts +143 -0
  320. package/src/services/profiler/profile-parser.ts +194 -0
  321. package/src/services/profiler/profile-runner.ts +121 -0
  322. package/src/services/reporter-registry.ts +46 -81
  323. package/src/types/budgets.ts +180 -0
  324. package/src/types/cli.ts +5 -238
  325. package/src/types/core.ts +50 -10
  326. package/src/types/index.ts +5 -0
  327. package/src/types/interfaces.ts +16 -6
  328. package/src/types/profiler.ts +132 -0
  329. package/src/types/utility.ts +0 -10
  330. package/src/utils/identifiers.ts +58 -0
  331. package/src/utils/package.ts +35 -0
  332. package/src/utils/type-guards.ts +51 -0
package/dist/cli/index.js CHANGED
@@ -10,25 +10,17 @@ import { fileURLToPath } from 'node:url';
10
10
  import yargs from 'yargs';
11
11
  import { hideBin } from 'yargs/helpers';
12
12
  import { bootstrap } from "../bootstrap.js";
13
+ import { ABORT_TIMEOUT, DEFAULT_ENGINE, DEFAULT_REPORTER, Engines, ErrorCodes, ExitCodes, Reporters, } from "../constants.js";
13
14
  import { AccurateEngine, TinybenchEngine } from "../core/engines/index.js";
15
+ import { isError } from "../errors/base.js";
14
16
  import { isModestBenchError, UnknownError } from "../errors/index.js";
15
17
  import { CsvReporter, HumanReporter, JsonReporter, SimpleReporter, } from "../reporters/index.js";
16
18
  // Import commands
19
+ import { handleAnalyzeCommand as analyzeCommand, } from "./commands/analyze.js";
20
+ import { handleAnalyzeCommand as handleBaselineAnalyzeCommand, handleDeleteCommand as handleBaselineDeleteCommand, handleListCommand as handleBaselineListCommand, handleSetCommand as handleBaselineSetCommand, handleShowCommand as handleBaselineShowCommand, } from "./commands/baseline.js";
17
21
  import { handleCleanCommand, handleCompareCommand, handleExportCommand, handleListCommand, handleShowCommand, handleTrendsCommand, } from "./commands/history.js";
18
22
  import { handleInitCommand as initCommand } from "./commands/init.js";
19
- import { handleRunCommand as runCommand } from "./commands/run.js";
20
- /**
21
- * Exit codes for the CLI
22
- */
23
- export const ExitCodes = {
24
- BENCHMARK_FAILURES: 1,
25
- CONFIG_ERROR: 2,
26
- DISCOVERY_ERROR: 3,
27
- RUNTIME_ERROR: 5,
28
- SUCCESS: 0,
29
- UNKNOWN_ERROR: 99,
30
- VALIDATION_ERROR: 4,
31
- };
23
+ import { RUN_COMMAND_DEFAULTS, handleRunCommand as runCommand, } from "./commands/run.js";
32
24
  /**
33
25
  * Initialize and run the CLI
34
26
  */
@@ -58,34 +50,34 @@ export const main = async (argv, abortController) => {
58
50
  })
59
51
  .option('verbose', {
60
52
  alias: 'v',
61
- default: false,
53
+ defaultDescription: String(RUN_COMMAND_DEFAULTS.verbose),
62
54
  description: 'Enable verbose output',
63
55
  global: true,
64
56
  type: 'boolean',
65
57
  })
66
58
  .option('no-color', {
67
- default: false,
59
+ defaultDescription: 'false',
68
60
  description: 'Disable colored output',
69
61
  global: true,
70
62
  type: 'boolean',
71
63
  })
72
64
  .option('progress', {
73
- default: true,
65
+ defaultDescription: 'true',
74
66
  description: 'Show animated progress bar',
75
67
  global: true,
76
68
  type: 'boolean',
77
69
  })
78
70
  .option('json', {
79
- default: false,
71
+ defaultDescription: 'false',
80
72
  description: 'Output results in JSON format',
81
73
  global: true,
82
74
  type: 'boolean',
83
75
  })
84
76
  .option('cwd', {
85
- default: process.cwd(),
86
77
  defaultDescription: '.',
87
78
  description: 'Working directory',
88
79
  global: true,
80
+ normalize: true,
89
81
  type: 'string',
90
82
  })
91
83
  .help()
@@ -93,136 +85,122 @@ export const main = async (argv, abortController) => {
93
85
  .version()
94
86
  .alias('version', 'V')
95
87
  .strict()
96
- .demandCommand(1, 'You must specify a command')
88
+ .demandCommand(1)
97
89
  .recommendCommands()
98
90
  .completion()
99
91
  .wrap(Math.min(120, cli.terminalWidth()))
100
- .command(['$0 [pattern..]', 'run [pattern..]'], 'Run benchmark files', (yargs) => {
101
- return yargs
102
- .positional('pattern', {
103
- array: true,
104
- default: ['./bench/**/*.bench.{js,mjs,cjs,ts}'],
105
- describe: 'File paths, directory paths, or glob patterns for benchmark files',
106
- type: 'string',
107
- })
108
- .option('config', {
109
- alias: 'c',
110
- description: 'Path to configuration file',
111
- type: 'string',
112
- })
113
- .option('reporters', {
114
- alias: 'r',
115
- coerce: (value) => {
116
- // Handle comma-separated values
117
- if (Array.isArray(value)) {
118
- return value.flatMap((v) => v.split(',').map((s) => s.trim()));
119
- }
120
- return value.split(',').map((s) => s.trim());
121
- },
122
- default: ['human'],
123
- description: 'Output reporters to use (human,json,csv)',
124
- type: 'array',
125
- })
126
- .option('output', {
127
- alias: 'o',
128
- description: 'Output directory for reports',
129
- type: 'string',
130
- })
131
- .option('output-file', {
132
- alias: 'of',
133
- description: 'Custom filename for reporter output (use with single reporter only)',
134
- requiresArg: true,
135
- type: 'string',
136
- })
137
- .option('iterations', {
138
- alias: 'i',
139
- description: 'Number of iterations per benchmark',
140
- type: 'number',
141
- })
142
- .option('time', {
143
- alias: 't',
144
- description: 'Time budget per benchmark in milliseconds',
145
- type: 'number',
146
- })
147
- .option('warmup', {
148
- alias: 'w',
149
- description: 'Number of warmup iterations',
150
- type: 'number',
151
- })
152
- .option('limit-by', {
153
- choices: ['time', 'iterations', 'any', 'all'],
154
- description: 'How to limit benchmarks: time (time budget), iterations (sample count), any (either threshold), all (both thresholds)',
155
- type: 'string',
156
- })
157
- .option('bail', {
158
- alias: 'b',
159
- default: false,
160
- description: 'Stop on first failure',
161
- type: 'boolean',
162
- })
163
- .option('exclude', {
164
- coerce: (value) => {
165
- // Handle comma-separated values
166
- if (Array.isArray(value)) {
167
- return value.flatMap((v) => v.split(',').map((s) => s.trim()));
168
- }
169
- return value.split(',').map((s) => s.trim());
170
- },
171
- description: 'Exclude patterns (comma-separated)',
172
- type: 'array',
173
- })
174
- .option('timeout', {
175
- description: 'Timeout per benchmark in milliseconds',
176
- type: 'number',
177
- })
178
- .option('quiet', {
179
- alias: 'q',
180
- default: false,
181
- description: 'Minimal output',
182
- type: 'boolean',
183
- })
184
- .option('tags', {
185
- coerce: (value) => {
186
- // Handle comma-separated values
187
- if (Array.isArray(value)) {
188
- return value.flatMap((v) => v.split(',').map((s) => s.trim()));
189
- }
190
- return value.split(',').map((s) => s.trim());
191
- },
192
- description: 'Include only benchmarks with any of these tags',
193
- type: 'array',
194
- })
195
- .option('exclude-tags', {
196
- coerce: (value) => {
197
- // Handle comma-separated values
198
- if (Array.isArray(value)) {
199
- return value.flatMap((v) => v.split(',').map((s) => s.trim()));
200
- }
201
- return value.split(',').map((s) => s.trim());
202
- },
203
- description: 'Exclude benchmarks with any of these tags',
204
- type: 'array',
205
- })
206
- .option('engine', {
207
- alias: 'e',
208
- choices: ['tinybench', 'accurate'],
209
- default: 'tinybench',
210
- description: 'Benchmark engine: tinybench (default) or accurate (requires --allow-natives-syntax)',
211
- type: 'string',
212
- })
213
- .example([
214
- ['$0 run', 'Run benchmarks in current directory and bench/'],
215
- ['$0 run benchmarks/', 'Run all benchmarks in a directory'],
216
- ['$0 run src/perf/', 'Run benchmarks in specific directory'],
217
- ['$0 run "src/**/*.bench.js"', 'Run specific glob pattern'],
218
- ['$0 run file1.bench.js file2.bench.js', 'Run specific files'],
219
- ['$0 run benchmarks/ tests/perf/', 'Run multiple directories'],
220
- ['$0 run --reporters json,csv', 'Use multiple reporters'],
221
- ['$0 run --iterations 1000', 'Set iteration count'],
222
- ['$0 run --engine accurate', 'Use high-accuracy engine'],
223
- ['$0 run --bail', 'Stop on first failure'],
224
- ]);
225
- }, async (argv) => {
92
+ .command(['$0 [pattern..]', 'run [pattern..]'], 'Run benchmark files', (yargs) => yargs
93
+ .positional('pattern', {
94
+ array: true,
95
+ defaultDescription: '(auto-discovered from bench/ directory)',
96
+ describe: 'File paths, directory paths, or glob patterns for benchmark files',
97
+ type: 'string',
98
+ })
99
+ .option('config', {
100
+ alias: 'c',
101
+ description: 'Path to configuration file',
102
+ type: 'string',
103
+ })
104
+ .option('reporter', {
105
+ alias: 'r',
106
+ array: true,
107
+ choices: Object.values(Reporters).sort(),
108
+ defaultDescription: DEFAULT_REPORTER,
109
+ description: 'Output reporters to use (human,json,csv)',
110
+ type: 'string',
111
+ })
112
+ .option('output', {
113
+ alias: 'o',
114
+ description: 'Output directory for reports',
115
+ type: 'string',
116
+ })
117
+ .option('output-file', {
118
+ alias: ['of', 'file'],
119
+ description: 'Custom filename for reporter output (use with single reporter only)',
120
+ requiresArg: true,
121
+ type: 'string',
122
+ })
123
+ .option('iterations', {
124
+ alias: 'i',
125
+ description: 'Number of iterations per benchmark',
126
+ type: 'number',
127
+ })
128
+ .option('time', {
129
+ alias: 't',
130
+ description: 'Time budget per benchmark in milliseconds',
131
+ type: 'number',
132
+ })
133
+ .option('warmup', {
134
+ alias: ['w', 'warm'],
135
+ description: 'Number of warmup iterations',
136
+ type: 'number',
137
+ })
138
+ .option('limit-by', {
139
+ alias: ['l', 'limit'],
140
+ choices: ['time', 'iterations', 'any', 'all'],
141
+ description: 'How to limit benchmarks: time (time budget), iterations (sample count), any (either threshold), all (both thresholds)',
142
+ type: 'string',
143
+ })
144
+ .option('bail', {
145
+ alias: 'b',
146
+ defaultDescription: String(RUN_COMMAND_DEFAULTS.bail),
147
+ description: 'Stop on first failure',
148
+ type: 'boolean',
149
+ })
150
+ .option('exclude', {
151
+ alias: 'X',
152
+ array: true,
153
+ description: 'Exclude patterns (comma-separated)',
154
+ type: 'string',
155
+ })
156
+ .option('timeout', {
157
+ description: 'Timeout per benchmark in milliseconds',
158
+ type: 'number',
159
+ })
160
+ .option('quiet', {
161
+ alias: 'q',
162
+ defaultDescription: String(RUN_COMMAND_DEFAULTS.quiet),
163
+ description: 'Minimal output',
164
+ type: 'boolean',
165
+ })
166
+ .option('tag', {
167
+ array: true,
168
+ description: 'Include only benchmarks with any of these tags',
169
+ type: 'string',
170
+ })
171
+ .option('exclude-tag', {
172
+ alias: 'T',
173
+ array: true,
174
+ description: 'Exclude benchmarks with any of these tags',
175
+ type: 'string',
176
+ })
177
+ .option('engine', {
178
+ alias: 'e',
179
+ choices: Object.values(Engines),
180
+ defaultDescription: DEFAULT_ENGINE,
181
+ description: 'Benchmark engine: tinybench (default) or accurate (requires --allow-natives-syntax)',
182
+ type: 'string',
183
+ })
184
+ .example([
185
+ ['$0 run', 'Run benchmarks in current directory and bench/'],
186
+ ['$0 run benchmarks/', 'Run all benchmarks in a directory'],
187
+ ['$0 run src/perf/', 'Run benchmarks in specific directory'],
188
+ ['$0 run "src/**/*.bench.js"', 'Run specific glob pattern'],
189
+ ['$0 run file1.bench.js file2.bench.js', 'Run specific files'],
190
+ ['$0 run benchmarks/ tests/perf/', 'Run multiple directories'],
191
+ ['$0 run -r json -r csv', 'Use multiple reporters'],
192
+ ['$0 run --iterations 1000', 'Set iteration count'],
193
+ ['$0 run --engine accurate', 'Use high-accuracy engine'],
194
+ ['$0 run --bail', 'Stop on first failure'],
195
+ ])
196
+ .check((argv) => {
197
+ if (argv.reporter &&
198
+ argv.reporter.length > 1 &&
199
+ argv['output-file']) {
200
+ throw new Error('--output-file can only be used with a single reporter. Use --output <dir> for multiple reporters.');
201
+ }
202
+ return true;
203
+ }), async (argv) => {
226
204
  const context = await createCliContext(argv, abortController, argv.engine);
227
205
  const exitCode = await runCommand(context, {
228
206
  bail: argv.bail,
@@ -230,7 +208,7 @@ export const main = async (argv, abortController) => {
230
208
  cwd: argv.cwd,
231
209
  engine: argv.engine,
232
210
  exclude: argv.exclude,
233
- excludeTags: argv['exclude-tags'],
211
+ excludeTags: argv['exclude-tag'],
234
212
  iterations: argv.iterations,
235
213
  json: argv.json,
236
214
  noColor: argv.noColor,
@@ -239,8 +217,8 @@ export const main = async (argv, abortController) => {
239
217
  pattern: argv.pattern,
240
218
  progress: argv.progress,
241
219
  quiet: argv.quiet,
242
- reporters: argv.reporters,
243
- tags: argv.tags,
220
+ reporters: argv.reporter,
221
+ tags: argv.tag,
244
222
  time: argv.time,
245
223
  timeout: argv.timeout,
246
224
  verbose: argv.verbose,
@@ -248,344 +226,503 @@ export const main = async (argv, abortController) => {
248
226
  });
249
227
  process.exit(exitCode);
250
228
  })
251
- .command('history', 'View and manage benchmark history', (yargs) => {
229
+ .command('history', 'View and manage benchmark history', (yargs) => yargs
230
+ .command('list', 'List recent benchmark runs', (yargs) => yargs
231
+ .option('since', {
232
+ description: 'Show runs since date (ISO 8601 or relative like "1 week ago")',
233
+ type: 'string',
234
+ })
235
+ .option('until', {
236
+ description: 'Show runs until date (ISO 8601 or relative like "1 day ago")',
237
+ type: 'string',
238
+ })
239
+ .option('pattern', {
240
+ description: 'Filter by benchmark name pattern',
241
+ type: 'string',
242
+ })
243
+ .option('tag', {
244
+ alias: 't',
245
+ array: true,
246
+ description: 'Filter by tags (comma-separated)',
247
+ type: 'string',
248
+ })
249
+ .option('limit', {
250
+ defaultDescription: '10',
251
+ description: 'Maximum number of results',
252
+ type: 'number',
253
+ })
254
+ .option('format', {
255
+ choices: ['human', 'json', 'csv'],
256
+ defaultDescription: 'human',
257
+ description: 'Output format',
258
+ type: 'string',
259
+ })
260
+ .example([
261
+ ['$0 history list', 'List recent benchmark runs'],
262
+ [
263
+ '$0 history list --since "1 week ago"',
264
+ 'List runs from last week',
265
+ ],
266
+ ['$0 history list --limit 20', 'List 20 most recent runs'],
267
+ ['$0 history list --format json', 'List runs in JSON format'],
268
+ ]), async (argv) => {
269
+ const context = await createCliContext(argv, abortController);
270
+ const exitCode = await handleListCommand(context, {
271
+ cwd: argv.cwd,
272
+ format: argv.format,
273
+ limit: argv.limit,
274
+ pattern: argv.pattern,
275
+ since: argv.since,
276
+ tags: argv.tag,
277
+ until: argv.until,
278
+ verbose: argv.verbose,
279
+ });
280
+ process.exit(exitCode);
281
+ })
282
+ .command('show <run-id>', 'Show detailed results for a specific run', (yargs) => yargs
283
+ .positional('run-id', {
284
+ demandOption: true,
285
+ describe: 'ID of the benchmark run to show',
286
+ type: 'string',
287
+ })
288
+ .option('format', {
289
+ choices: ['human', 'json', 'csv'],
290
+ defaultDescription: 'human',
291
+ description: 'Output format',
292
+ type: 'string',
293
+ })
294
+ .example([
295
+ [
296
+ '$0 history show abc123',
297
+ 'Show detailed results for run abc123',
298
+ ],
299
+ [
300
+ '$0 history show abc123 --format json',
301
+ 'Show run in JSON format',
302
+ ],
303
+ ]), async (argv) => {
304
+ const context = await createCliContext(argv, abortController);
305
+ const exitCode = await handleShowCommand(context, {
306
+ cwd: argv.cwd,
307
+ format: argv.format,
308
+ runId: argv['run-id'],
309
+ verbose: argv.verbose,
310
+ });
311
+ process.exit(exitCode);
312
+ })
313
+ .command('compare <run-id1> <run-id2>', 'Compare two benchmark runs', (yargs) => yargs
314
+ .positional('run-id1', {
315
+ demandOption: true,
316
+ describe: 'ID of the first benchmark run',
317
+ type: 'string',
318
+ })
319
+ .positional('run-id2', {
320
+ demandOption: true,
321
+ describe: 'ID of the second benchmark run',
322
+ type: 'string',
323
+ })
324
+ .option('format', {
325
+ choices: ['human', 'json'],
326
+ defaultDescription: 'human',
327
+ description: 'Output format',
328
+ type: 'string',
329
+ })
330
+ .example([
331
+ ['$0 history compare abc123 def456', 'Compare two runs'],
332
+ [
333
+ '$0 history compare abc123 def456 --format json',
334
+ 'Compare in JSON format',
335
+ ],
336
+ ]), async (argv) => {
337
+ const context = await createCliContext(argv, abortController);
338
+ const exitCode = await handleCompareCommand(context, {
339
+ cwd: argv.cwd,
340
+ format: argv.format,
341
+ runId1: argv['run-id1'],
342
+ runId2: argv['run-id2'],
343
+ verbose: argv.verbose,
344
+ });
345
+ process.exit(exitCode);
346
+ })
347
+ .command('trends [pattern]', 'Show performance trends over time', (yargs) => yargs
348
+ .positional('pattern', {
349
+ describe: 'Filter by benchmark name pattern',
350
+ type: 'string',
351
+ })
352
+ .option('since', {
353
+ description: 'Show trends since date (ISO 8601 or relative like "1 week ago")',
354
+ type: 'string',
355
+ })
356
+ .option('until', {
357
+ description: 'Show trends until date (ISO 8601 or relative like "1 day ago")',
358
+ type: 'string',
359
+ })
360
+ .option('tag', {
361
+ alias: 't',
362
+ array: true,
363
+ description: 'Filter by tags (comma-separated)',
364
+ type: 'string',
365
+ })
366
+ .option('limit', {
367
+ description: 'Maximum number of runs to analyze',
368
+ type: 'number',
369
+ })
370
+ .option('all', {
371
+ alias: 'a',
372
+ defaultDescription: 'false',
373
+ description: 'Analyze all runs (ignore limit)',
374
+ type: 'boolean',
375
+ })
376
+ .option('format', {
377
+ choices: ['human', 'json'],
378
+ defaultDescription: 'human',
379
+ description: 'Output format',
380
+ type: 'string',
381
+ })
382
+ .example([
383
+ [
384
+ '$0 history trends',
385
+ 'Show performance trends for all benchmarks',
386
+ ],
387
+ [
388
+ '$0 history trends --since "1 month ago"',
389
+ 'Show trends from last month',
390
+ ],
391
+ [
392
+ '$0 history trends "array-*"',
393
+ 'Show trends for array benchmarks',
394
+ ],
395
+ [
396
+ '$0 history trends --format json',
397
+ 'Output trends in JSON format',
398
+ ],
399
+ ]), async (argv) => {
400
+ const context = await createCliContext(argv, abortController);
401
+ const exitCode = await handleTrendsCommand(context, {
402
+ all: argv.all,
403
+ cwd: argv.cwd,
404
+ format: argv.format,
405
+ limit: argv.limit,
406
+ pattern: argv.pattern,
407
+ since: argv.since,
408
+ tags: argv.tag,
409
+ until: argv.until,
410
+ verbose: argv.verbose,
411
+ });
412
+ process.exit(exitCode);
413
+ })
414
+ .command('clean', 'Clean up old benchmark history', (yargs) => yargs
415
+ .option('max-age', {
416
+ description: 'Remove runs older than this many days',
417
+ type: 'number',
418
+ })
419
+ .option('max-runs', {
420
+ description: 'Keep only this many most recent runs',
421
+ type: 'number',
422
+ })
423
+ .option('max-size', {
424
+ description: 'Keep history under this size in bytes',
425
+ type: 'number',
426
+ })
427
+ .option('yes', {
428
+ alias: 'y',
429
+ description: 'Confirm cleanup without prompting',
430
+ type: 'boolean',
431
+ })
432
+ .option('quiet', {
433
+ default: false,
434
+ description: 'Minimal output',
435
+ type: 'boolean',
436
+ })
437
+ .check((argv) => {
438
+ if (!argv['max-age'] &&
439
+ !argv['max-runs'] &&
440
+ !argv['max-size']) {
441
+ throw new Error('At least one cleanup criterion must be specified (--max-age, --max-runs, or --max-size)');
442
+ }
443
+ return true;
444
+ })
445
+ .example([
446
+ [
447
+ '$0 history clean --max-runs 50 --yes',
448
+ 'Keep only latest 50 runs',
449
+ ],
450
+ [
451
+ '$0 history clean --max-age 30',
452
+ 'Preview removing runs older than 30 days',
453
+ ],
454
+ [
455
+ '$0 history clean --max-size 10485760',
456
+ 'Keep history under 10MB',
457
+ ],
458
+ ]), async (argv) => {
459
+ const context = await createCliContext(argv, abortController);
460
+ const exitCode = await handleCleanCommand(context, {
461
+ confirm: argv.yes,
462
+ cwd: argv.cwd,
463
+ maxAge: argv['max-age'],
464
+ maxRuns: argv['max-runs'],
465
+ maxSize: argv['max-size'],
466
+ quiet: argv.quiet,
467
+ verbose: argv.verbose,
468
+ });
469
+ process.exit(exitCode);
470
+ })
471
+ .command('export', 'Export benchmark history to a file', (yargs) => yargs
472
+ .option('format', {
473
+ choices: ['json', 'csv'],
474
+ defaultDescription: 'json',
475
+ description: 'Export format',
476
+ type: 'string',
477
+ })
478
+ .option('output', {
479
+ alias: 'o',
480
+ demandOption: true,
481
+ description: 'Output file path',
482
+ type: 'string',
483
+ })
484
+ .option('since', {
485
+ description: 'Export runs since date',
486
+ type: 'string',
487
+ })
488
+ .option('until', {
489
+ description: 'Export runs until date',
490
+ type: 'string',
491
+ })
492
+ .example([
493
+ [
494
+ '$0 history export -o history.json',
495
+ 'Export all history to JSON',
496
+ ],
497
+ [
498
+ '$0 history export -o history.csv --format csv',
499
+ 'Export to CSV',
500
+ ],
501
+ [
502
+ '$0 history export -o recent.json --since "1 week ago"',
503
+ 'Export recent runs',
504
+ ],
505
+ ]), async (argv) => {
506
+ const context = await createCliContext(argv, abortController);
507
+ const exitCode = await handleExportCommand(context, {
508
+ cwd: argv.cwd,
509
+ format: argv.format,
510
+ outputPath: argv.output,
511
+ quiet: Boolean(argv.quiet),
512
+ since: argv.since,
513
+ until: argv.until,
514
+ verbose: argv.verbose,
515
+ });
516
+ process.exitCode = exitCode;
517
+ })
518
+ .demandCommand(1, 'You must specify a history subcommand')
519
+ .strict()
520
+ .example([
521
+ ['$0 history list', 'List recent benchmark runs'],
522
+ ['$0 history show <run-id>', 'Show detailed results'],
523
+ ['$0 history compare <run-id1> <run-id2>', 'Compare two runs'],
524
+ ['$0 history trends', 'Show performance trends'],
525
+ ['$0 history clean --max-runs 50', 'Keep only latest 50 runs'],
526
+ ['$0 history export -o data.json', 'Export history'],
527
+ ]))
528
+ .command('baseline', 'Manage performance baselines', (yargs) => {
252
529
  return yargs
253
- .command('list', 'List recent benchmark runs', (yargs) => {
530
+ .command('set <name>', 'Save a benchmark run as a baseline', (yargs) => {
254
531
  return yargs
255
- .option('since', {
256
- description: 'Show runs since date (ISO 8601 or relative like "1 week ago")',
532
+ .positional('name', {
533
+ describe: 'Name for the baseline',
257
534
  type: 'string',
258
535
  })
259
- .option('until', {
260
- description: 'Show runs until date (ISO 8601 or relative like "1 day ago")',
536
+ .option('run-id', {
537
+ description: 'Specific run ID to save (default: most recent)',
261
538
  type: 'string',
262
539
  })
263
- .option('pattern', {
264
- description: 'Filter by benchmark name pattern',
540
+ .option('commit', {
541
+ description: 'Git commit SHA (40 characters)',
265
542
  type: 'string',
266
543
  })
267
- .option('tags', {
268
- description: 'Filter by tags (comma-separated)',
269
- type: 'array',
270
- })
271
- .option('limit', {
272
- default: 10,
273
- description: 'Maximum number of results',
274
- type: 'number',
275
- })
276
- .option('format', {
277
- choices: ['human', 'json', 'csv'],
278
- default: 'human',
279
- description: 'Output format',
544
+ .option('branch', {
545
+ description: 'Git branch name',
280
546
  type: 'string',
281
547
  })
282
- .example([
283
- ['$0 history list', 'List recent benchmark runs'],
284
- [
285
- '$0 history list --since "1 week ago"',
286
- 'List runs from last week',
287
- ],
288
- ['$0 history list --limit 20', 'List 20 most recent runs'],
289
- ['$0 history list --format json', 'List runs in JSON format'],
290
- ]);
291
- }, async (argv) => {
292
- const context = await createCliContext(argv, abortController);
293
- const exitCode = await handleListCommand(context, {
294
- cwd: argv.cwd,
295
- format: argv.format,
296
- limit: argv.limit,
297
- pattern: argv.pattern,
298
- quiet: Boolean(argv.quiet),
299
- since: argv.since,
300
- tags: argv.tags,
301
- until: argv.until,
302
- verbose: argv.verbose,
303
- });
304
- process.exit(exitCode);
305
- })
306
- .command('show <run-id>', 'Show detailed results for a specific run', (yargs) => {
307
- return yargs
308
- .positional('run-id', {
309
- describe: 'ID of the benchmark run to show',
310
- type: 'string',
311
- })
312
- .option('format', {
313
- choices: ['human', 'json', 'csv'],
314
- default: 'human',
315
- description: 'Output format',
316
- type: 'string',
548
+ .option('default', {
549
+ defaultDescription: 'false',
550
+ description: 'Set as default baseline',
551
+ type: 'boolean',
317
552
  })
318
553
  .example([
319
554
  [
320
- '$0 history show abc123',
321
- 'Show detailed results for run abc123',
555
+ '$0 baseline set production-v1.0',
556
+ 'Save most recent run as baseline',
322
557
  ],
558
+ ['$0 baseline set v1.0 --default', 'Save and set as default'],
323
559
  [
324
- '$0 history show abc123 --format json',
325
- 'Show run in JSON format',
560
+ '$0 baseline set v1.0 --commit abc123...',
561
+ 'Save with commit info',
326
562
  ],
327
563
  ]);
328
564
  }, async (argv) => {
329
565
  const context = await createCliContext(argv, abortController);
330
- const exitCode = await handleShowCommand(context, {
566
+ const exitCode = await handleBaselineSetCommand(context, {
567
+ branch: argv.branch,
568
+ commit: argv.commit,
331
569
  cwd: argv.cwd,
332
- format: argv.format,
570
+ default: argv.default,
571
+ name: String(argv.name),
333
572
  quiet: Boolean(argv.quiet),
334
- runId: String(argv['run-id']),
573
+ runId: argv['run-id'],
335
574
  verbose: argv.verbose,
336
575
  });
337
576
  process.exit(exitCode);
338
577
  })
339
- .command('compare <run-id1> <run-id2>', 'Compare two benchmark runs', (yargs) => {
578
+ .command('list', 'List all saved baselines', (yargs) => {
340
579
  return yargs
341
- .positional('run-id1', {
342
- describe: 'ID of the first benchmark run',
343
- type: 'string',
344
- })
345
- .positional('run-id2', {
346
- describe: 'ID of the second benchmark run',
347
- type: 'string',
348
- })
349
580
  .option('format', {
350
581
  choices: ['human', 'json'],
351
- default: 'human',
582
+ defaultDescription: 'human',
352
583
  description: 'Output format',
353
584
  type: 'string',
354
585
  })
355
586
  .example([
356
- ['$0 history compare abc123 def456', 'Compare two runs'],
357
- [
358
- '$0 history compare abc123 def456 --format json',
359
- 'Compare in JSON format',
360
- ],
587
+ ['$0 baseline list', 'List all baselines'],
588
+ ['$0 baseline list --format json', 'List in JSON format'],
361
589
  ]);
362
590
  }, async (argv) => {
363
591
  const context = await createCliContext(argv, abortController);
364
- const exitCode = await handleCompareCommand(context, {
592
+ const exitCode = await handleBaselineListCommand(context, {
365
593
  cwd: argv.cwd,
366
594
  format: argv.format,
367
595
  quiet: Boolean(argv.quiet),
368
- runId1: String(argv['run-id1']),
369
- runId2: String(argv['run-id2']),
370
596
  verbose: argv.verbose,
371
597
  });
372
598
  process.exit(exitCode);
373
599
  })
374
- .command('trends [pattern]', 'Show performance trends over time', (yargs) => {
600
+ .command('show <name>', 'Show baseline details', (yargs) => {
375
601
  return yargs
376
- .positional('pattern', {
377
- describe: 'Filter by benchmark name pattern',
378
- type: 'string',
379
- })
380
- .option('since', {
381
- description: 'Show trends since date (ISO 8601 or relative like "1 week ago")',
602
+ .positional('name', {
603
+ describe: 'Baseline name to show',
382
604
  type: 'string',
383
- })
384
- .option('until', {
385
- description: 'Show trends until date (ISO 8601 or relative like "1 day ago")',
386
- type: 'string',
387
- })
388
- .option('tags', {
389
- description: 'Filter by tags (comma-separated)',
390
- type: 'array',
391
- })
392
- .option('limit', {
393
- description: 'Maximum number of runs to analyze',
394
- type: 'number',
395
- })
396
- .option('all', {
397
- alias: 'a',
398
- default: false,
399
- description: 'Analyze all runs (ignore limit)',
400
- type: 'boolean',
401
605
  })
402
606
  .option('format', {
403
607
  choices: ['human', 'json'],
404
- default: 'human',
608
+ defaultDescription: 'human',
405
609
  description: 'Output format',
406
610
  type: 'string',
407
611
  })
408
612
  .example([
613
+ ['$0 baseline show production-v1.0', 'Show baseline details'],
409
614
  [
410
- '$0 history trends',
411
- 'Show performance trends for all benchmarks',
412
- ],
413
- [
414
- '$0 history trends --since "1 month ago"',
415
- 'Show trends from last month',
416
- ],
417
- [
418
- '$0 history trends "array-*"',
419
- 'Show trends for array benchmarks',
420
- ],
421
- [
422
- '$0 history trends --format json',
423
- 'Output trends in JSON format',
615
+ '$0 baseline show v1.0 --format json',
616
+ 'Show in JSON format',
424
617
  ],
425
618
  ]);
426
619
  }, async (argv) => {
427
620
  const context = await createCliContext(argv, abortController);
428
- const exitCode = await handleTrendsCommand(context, {
429
- all: Boolean(argv.all),
621
+ const exitCode = await handleBaselineShowCommand(context, {
430
622
  cwd: argv.cwd,
431
623
  format: argv.format,
432
- limit: argv.limit,
433
- pattern: argv.pattern,
624
+ name: String(argv.name),
434
625
  quiet: Boolean(argv.quiet),
435
- since: argv.since,
436
- tags: argv.tags,
437
- until: argv.until,
438
626
  verbose: argv.verbose,
439
627
  });
440
628
  process.exit(exitCode);
441
629
  })
442
- .command('clean', 'Clean up old benchmark history', (yargs) => {
630
+ .command('delete <name>', 'Delete a baseline', (yargs) => {
443
631
  return yargs
444
- .option('max-age', {
445
- description: 'Remove runs older than this many days',
446
- type: 'number',
447
- })
448
- .option('max-runs', {
449
- description: 'Keep only this many most recent runs',
450
- type: 'number',
451
- })
452
- .option('max-size', {
453
- description: 'Keep history under this size in bytes',
454
- type: 'number',
455
- })
456
- .option('confirm', {
457
- default: false,
458
- description: 'Confirm cleanup without prompting',
459
- type: 'boolean',
460
- })
461
- .check((argv) => {
462
- if (!argv['max-age'] &&
463
- !argv['max-runs'] &&
464
- !argv['max-size']) {
465
- throw new Error('At least one cleanup criterion must be specified (--max-age, --max-runs, or --max-size)');
466
- }
467
- return true;
632
+ .positional('name', {
633
+ describe: 'Baseline name to delete',
634
+ type: 'string',
468
635
  })
469
636
  .example([
470
- [
471
- '$0 history clean --max-runs 50 --confirm',
472
- 'Keep only latest 50 runs',
473
- ],
474
- [
475
- '$0 history clean --max-age 30',
476
- 'Preview removing runs older than 30 days',
477
- ],
478
- [
479
- '$0 history clean --max-size 10485760',
480
- 'Keep history under 10MB',
481
- ],
637
+ ['$0 baseline delete old-baseline', 'Delete a baseline'],
482
638
  ]);
483
639
  }, async (argv) => {
484
640
  const context = await createCliContext(argv, abortController);
485
- const exitCode = await handleCleanCommand(context, {
486
- confirm: argv.confirm,
641
+ const exitCode = await handleBaselineDeleteCommand(context, {
487
642
  cwd: argv.cwd,
488
- maxAge: argv['max-age'],
489
- maxRuns: argv['max-runs'],
490
- maxSize: argv['max-size'],
643
+ name: String(argv.name),
491
644
  quiet: Boolean(argv.quiet),
492
645
  verbose: argv.verbose,
493
646
  });
494
647
  process.exit(exitCode);
495
648
  })
496
- .command('export', 'Export benchmark history to a file', (yargs) => {
649
+ .command('analyze', 'Analyze history and suggest performance budgets', (yargs) => {
497
650
  return yargs
498
- .option('format', {
499
- choices: ['json', 'csv'],
500
- default: 'json',
501
- description: 'Export format',
502
- type: 'string',
503
- })
504
- .option('output', {
505
- alias: 'o',
506
- demandOption: true,
507
- description: 'Output file path',
508
- type: 'string',
509
- })
510
- .option('since', {
511
- description: 'Export runs since date',
512
- type: 'string',
651
+ .option('runs', {
652
+ defaultDescription: '10',
653
+ description: 'Number of recent runs to analyze',
654
+ type: 'number',
513
655
  })
514
- .option('until', {
515
- description: 'Export runs until date',
516
- type: 'string',
656
+ .option('confidence', {
657
+ defaultDescription: '0.95',
658
+ description: 'Confidence level (0.5-0.999, default 0.95)',
659
+ type: 'number',
517
660
  })
518
661
  .example([
519
662
  [
520
- '$0 history export -o history.json',
521
- 'Export all history to JSON',
522
- ],
523
- [
524
- '$0 history export -o history.csv --format csv',
525
- 'Export to CSV',
663
+ '$0 baseline analyze',
664
+ 'Analyze last 10 runs with 95% confidence',
526
665
  ],
666
+ ['$0 baseline analyze --runs 20', 'Analyze last 20 runs'],
527
667
  [
528
- '$0 history export -o recent.json --since "1 week ago"',
529
- 'Export recent runs',
668
+ '$0 baseline analyze --confidence 0.90',
669
+ 'Use 90% confidence level',
530
670
  ],
531
671
  ]);
532
672
  }, async (argv) => {
533
673
  const context = await createCliContext(argv, abortController);
534
- const exitCode = await handleExportCommand(context, {
674
+ const exitCode = await handleBaselineAnalyzeCommand(context, {
675
+ confidence: argv.confidence,
535
676
  cwd: argv.cwd,
536
- format: argv.format,
537
- outputPath: argv.output,
538
677
  quiet: Boolean(argv.quiet),
539
- since: argv.since,
540
- until: argv.until,
678
+ runs: argv.runs,
541
679
  verbose: argv.verbose,
542
680
  });
543
681
  process.exit(exitCode);
544
682
  })
545
- .demandCommand(1, 'You must specify a history subcommand')
683
+ .demandCommand(1, 'You must specify a baseline subcommand')
546
684
  .strict()
547
685
  .example([
548
- ['$0 history list', 'List recent benchmark runs'],
549
- ['$0 history show <run-id>', 'Show detailed results'],
550
- ['$0 history compare <run-id1> <run-id2>', 'Compare two runs'],
551
- ['$0 history trends', 'Show performance trends'],
552
- ['$0 history clean --max-runs 50', 'Keep only latest 50 runs'],
553
- ['$0 history export -o data.json', 'Export history'],
686
+ ['$0 baseline set production-v1.0', 'Save current run as baseline'],
687
+ ['$0 baseline list', 'List all baselines'],
688
+ ['$0 baseline show production-v1.0', 'Show baseline details'],
689
+ ['$0 baseline delete old-baseline', 'Delete a baseline'],
690
+ ['$0 baseline analyze', 'Suggest budgets from history'],
554
691
  ]);
555
692
  })
556
693
  .command('init [type]', 'Initialize a new benchmark project', (yargs) => {
557
694
  return yargs
558
695
  .positional('type', {
559
696
  choices: ['basic', 'advanced', 'library'],
560
- default: 'basic',
697
+ defaultDescription: 'basic',
561
698
  describe: 'Type of project to initialize',
562
699
  type: 'string',
563
700
  })
564
701
  .option('examples', {
565
- default: true,
702
+ defaultDescription: 'true',
566
703
  description: 'Include example benchmark files',
567
704
  type: 'boolean',
568
705
  })
569
706
  .option('config-type', {
570
707
  choices: ['json', 'yaml', 'js', 'ts'],
571
- default: 'json',
708
+ defaultDescription: 'json',
572
709
  description: 'Configuration file format',
573
710
  type: 'string',
574
711
  })
575
712
  .option('force', {
576
- default: false,
713
+ defaultDescription: 'false',
577
714
  description: 'Overwrite existing files',
578
715
  type: 'boolean',
579
716
  })
580
717
  .option('yes', {
581
718
  alias: 'y',
582
- default: false,
719
+ defaultDescription: 'false',
583
720
  description: 'Accept all prompts automatically',
584
721
  type: 'boolean',
585
722
  })
586
723
  .option('quiet', {
587
724
  alias: 'q',
588
- default: false,
725
+ defaultDescription: 'false',
589
726
  description: 'Minimal output',
590
727
  type: 'boolean',
591
728
  })
@@ -607,23 +744,77 @@ export const main = async (argv, abortController) => {
607
744
  cwd: argv.cwd,
608
745
  examples: argv.examples,
609
746
  force: argv.force,
610
- quiet: Boolean(argv.quiet),
747
+ quiet: argv.quiet,
611
748
  type: argv.type,
612
749
  verbose: argv.verbose,
613
750
  yes: argv.yes,
614
751
  });
615
- process.exit(exitCode);
752
+ process.exitCode = exitCode;
616
753
  })
617
- .fail((msg, err, yargsInstance) => {
754
+ .command(['analyze [command]', 'profile [command]'], 'Analyze code execution and identify benchmark candidates', (yargs) => {
755
+ return yargs
756
+ .positional('command', {
757
+ description: 'Command to analyze (e.g., "npm test")',
758
+ type: 'string',
759
+ })
760
+ .option('input', {
761
+ alias: 'i',
762
+ description: 'Path to existing *.cpuprofile file',
763
+ type: 'string',
764
+ })
765
+ .option('filter-file', {
766
+ description: 'Filter functions by file glob pattern',
767
+ type: 'string',
768
+ })
769
+ .option('min-percent', {
770
+ alias: ['m', 'min'],
771
+ default: 0.5,
772
+ description: 'Minimum execution percentage to show',
773
+ type: 'number',
774
+ })
775
+ .option('top', {
776
+ alias: 'n',
777
+ default: 25,
778
+ description: 'Number of top functions to show',
779
+ type: 'number',
780
+ })
781
+ .option('group-by-file', {
782
+ default: false,
783
+ description: 'Group results by file',
784
+ type: 'boolean',
785
+ })
786
+ .check((argv) => {
787
+ if (!argv.command && !argv.input) {
788
+ throw new Error('Either [command] or --input must be provided');
789
+ }
790
+ return true;
791
+ });
792
+ }, async (argv) => {
793
+ // Context not needed for analyze command currently
794
+ const context = {};
795
+ const options = {
796
+ color: !argv.noColor,
797
+ command: argv.command,
798
+ cwd: argv.cwd || process.cwd(),
799
+ filterFile: argv.filterFile,
800
+ groupByFile: argv.groupByFile,
801
+ input: argv.input,
802
+ minPercent: argv.minPercent,
803
+ top: argv.top,
804
+ };
805
+ process.exitCode = await analyzeCommand(context, options);
806
+ })
807
+ .fail((msg, err, yargs) => {
618
808
  if (err) {
619
809
  console.error('Error:', err.message);
620
810
  if (process.env.DEBUG) {
621
811
  console.error(err.stack);
622
812
  }
623
813
  // Show help for file discovery errors (similar to usage errors)
624
- if (err.name === 'FileDiscoveryError') {
814
+ if (isModestBenchError(err) &&
815
+ err.code === ErrorCodes.FILE_DISCOVERY_FAILED) {
625
816
  console.error();
626
- yargsInstance.showHelp();
817
+ yargs.showHelp();
627
818
  process.exit(ExitCodes.DISCOVERY_ERROR);
628
819
  }
629
820
  process.exit(ExitCodes.RUNTIME_ERROR);
@@ -632,7 +823,7 @@ export const main = async (argv, abortController) => {
632
823
  // Show help for usage errors (unknown arguments, etc.)
633
824
  console.error(msg);
634
825
  console.error();
635
- yargsInstance.showHelp();
826
+ yargs.showHelp();
636
827
  process.exit(ExitCodes.CONFIG_ERROR);
637
828
  }
638
829
  })
@@ -649,15 +840,15 @@ export const main = async (argv, abortController) => {
649
840
  /**
650
841
  * Create CLI context with dependency injection
651
842
  */
652
- const createCliContext = async (options, abortController, engineType = 'tinybench') => {
843
+ const createCliContext = async (options, abortController, engineType = DEFAULT_ENGINE) => {
653
844
  try {
654
845
  const dependencies = bootstrap();
655
846
  // Select engine based on type
656
- const engine = engineType === 'accurate'
847
+ const engine = engineType === Engines.ACCURATE
657
848
  ? new AccurateEngine(dependencies)
658
849
  : new TinybenchEngine(dependencies);
659
850
  // Register built-in reporters
660
- engine.registerReporter('human', new HumanReporter({
851
+ engine.registerReporter(Reporters.HUMAN, new HumanReporter({
661
852
  color: !options.noColor,
662
853
  verbose: options.verbose,
663
854
  }));
@@ -704,25 +895,23 @@ const setupSignalHandlers = (abortController) => {
704
895
  setTimeout(() => {
705
896
  console.log('\nBenchmark aborted.');
706
897
  process.exit(computeExitCode(signal));
707
- }, 100);
898
+ }, ABORT_TIMEOUT);
708
899
  };
709
- process.on('SIGINT', () => handleSignal('SIGINT'));
710
- process.on('SIGQUIT', () => handleSignal('SIGQUIT'));
711
- process.on('SIGTERM', () => handleSignal('SIGTERM'));
712
- process.once('uncaughtException', (error) => {
900
+ process
901
+ .once('SIGINT', handleSignal)
902
+ .once('SIGQUIT', handleSignal)
903
+ .once('SIGTERM', handleSignal)
904
+ .once('uncaughtException', (error) => {
713
905
  // Wrap non-ModestBench errors with UnknownError
714
906
  const wrappedError = isModestBenchError(error)
715
907
  ? error
716
- : new UnknownError(error instanceof Error ? error.message : String(error), { cause: error });
717
- console.error(wrappedError.toString());
908
+ : new UnknownError(error.message, { cause: error });
909
+ console.error(`${wrappedError}`);
718
910
  process.exit(ExitCodes.RUNTIME_ERROR);
719
- });
720
- process.once('unhandledRejection', (reason) => {
721
- // Wrap non-ModestBench errors with UnknownError
722
- const wrappedError = isModestBenchError(reason)
723
- ? reason
724
- : new UnknownError(reason instanceof Error ? reason.message : String(reason), { cause: reason });
725
- console.error(wrappedError.toString());
911
+ })
912
+ .once('unhandledRejection', (reason) => {
913
+ const wrappedError = new UnknownError(isError(reason) ? reason.message : String(reason), { cause: reason });
914
+ console.error(`${wrappedError}`);
726
915
  process.exit(ExitCodes.RUNTIME_ERROR);
727
916
  });
728
917
  };