modestbench 0.9.1 → 0.9.2

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 (151) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/cli/builder.cjs +259 -0
  3. package/dist/cli/builder.cjs.map +1 -0
  4. package/dist/cli/builder.d.cts +37 -0
  5. package/dist/cli/builder.d.cts.map +1 -0
  6. package/dist/cli/builder.d.ts +37 -0
  7. package/dist/cli/builder.d.ts.map +1 -0
  8. package/dist/cli/builder.js +255 -0
  9. package/dist/cli/builder.js.map +1 -0
  10. package/dist/cli/commands/baseline.cjs +4 -33
  11. package/dist/cli/commands/baseline.cjs.map +1 -1
  12. package/dist/cli/commands/baseline.d.cts.map +1 -1
  13. package/dist/cli/commands/baseline.d.ts.map +1 -1
  14. package/dist/cli/commands/baseline.js +2 -31
  15. package/dist/cli/commands/baseline.js.map +1 -1
  16. package/dist/cli/commands/history.cjs +2 -14
  17. package/dist/cli/commands/history.cjs.map +1 -1
  18. package/dist/cli/commands/history.d.cts.map +1 -1
  19. package/dist/cli/commands/history.d.ts.map +1 -1
  20. package/dist/cli/commands/history.js +1 -13
  21. package/dist/cli/commands/history.js.map +1 -1
  22. package/dist/cli/context.cjs +60 -0
  23. package/dist/cli/context.cjs.map +1 -0
  24. package/dist/cli/context.d.cts +28 -0
  25. package/dist/cli/context.d.cts.map +1 -0
  26. package/dist/cli/context.d.ts +28 -0
  27. package/dist/cli/context.d.ts.map +1 -0
  28. package/dist/cli/context.js +56 -0
  29. package/dist/cli/context.js.map +1 -0
  30. package/dist/cli/handlers.cjs +74 -0
  31. package/dist/cli/handlers.cjs.map +1 -0
  32. package/dist/cli/handlers.d.cts +13 -0
  33. package/dist/cli/handlers.d.cts.map +1 -0
  34. package/dist/cli/handlers.d.ts +13 -0
  35. package/dist/cli/handlers.d.ts.map +1 -0
  36. package/dist/cli/handlers.js +70 -0
  37. package/dist/cli/handlers.js.map +1 -0
  38. package/dist/cli/index.cjs +12 -724
  39. package/dist/cli/index.cjs.map +1 -1
  40. package/dist/cli/index.d.cts +4 -39
  41. package/dist/cli/index.d.cts.map +1 -1
  42. package/dist/cli/index.d.ts +4 -39
  43. package/dist/cli/index.d.ts.map +1 -1
  44. package/dist/cli/index.js +9 -722
  45. package/dist/cli/index.js.map +1 -1
  46. package/dist/cli/parsers/analyze.cjs +54 -0
  47. package/dist/cli/parsers/analyze.cjs.map +1 -0
  48. package/dist/cli/parsers/analyze.d.cts +37 -0
  49. package/dist/cli/parsers/analyze.d.cts.map +1 -0
  50. package/dist/cli/parsers/analyze.d.ts +37 -0
  51. package/dist/cli/parsers/analyze.d.ts.map +1 -0
  52. package/dist/cli/parsers/analyze.js +51 -0
  53. package/dist/cli/parsers/analyze.js.map +1 -0
  54. package/dist/cli/parsers/baseline.cjs +75 -0
  55. package/dist/cli/parsers/baseline.cjs.map +1 -0
  56. package/dist/cli/parsers/baseline.d.cts +59 -0
  57. package/dist/cli/parsers/baseline.d.cts.map +1 -0
  58. package/dist/cli/parsers/baseline.d.ts +59 -0
  59. package/dist/cli/parsers/baseline.d.ts.map +1 -0
  60. package/dist/cli/parsers/baseline.js +72 -0
  61. package/dist/cli/parsers/baseline.js.map +1 -0
  62. package/dist/cli/parsers/global.cjs +49 -0
  63. package/dist/cli/parsers/global.cjs.map +1 -0
  64. package/dist/cli/parsers/global.d.cts +45 -0
  65. package/dist/cli/parsers/global.d.cts.map +1 -0
  66. package/dist/cli/parsers/global.d.ts +45 -0
  67. package/dist/cli/parsers/global.d.ts.map +1 -0
  68. package/dist/cli/parsers/global.js +46 -0
  69. package/dist/cli/parsers/global.js.map +1 -0
  70. package/dist/cli/parsers/history.cjs +138 -0
  71. package/dist/cli/parsers/history.cjs.map +1 -0
  72. package/dist/cli/parsers/history.d.cts +108 -0
  73. package/dist/cli/parsers/history.d.cts.map +1 -0
  74. package/dist/cli/parsers/history.d.ts +108 -0
  75. package/dist/cli/parsers/history.d.ts.map +1 -0
  76. package/dist/cli/parsers/history.js +135 -0
  77. package/dist/cli/parsers/history.js.map +1 -0
  78. package/dist/cli/parsers/index.cjs +35 -0
  79. package/dist/cli/parsers/index.cjs.map +1 -0
  80. package/dist/cli/parsers/index.d.cts +15 -0
  81. package/dist/cli/parsers/index.d.cts.map +1 -0
  82. package/dist/cli/parsers/index.d.ts +15 -0
  83. package/dist/cli/parsers/index.d.ts.map +1 -0
  84. package/dist/cli/parsers/index.js +15 -0
  85. package/dist/cli/parsers/index.js.map +1 -0
  86. package/dist/cli/parsers/init.cjs +39 -0
  87. package/dist/cli/parsers/init.cjs.map +1 -0
  88. package/dist/cli/parsers/init.d.cts +32 -0
  89. package/dist/cli/parsers/init.d.cts.map +1 -0
  90. package/dist/cli/parsers/init.d.ts +32 -0
  91. package/dist/cli/parsers/init.d.ts.map +1 -0
  92. package/dist/cli/parsers/init.js +36 -0
  93. package/dist/cli/parsers/init.js.map +1 -0
  94. package/dist/cli/parsers/run.cjs +99 -0
  95. package/dist/cli/parsers/run.cjs.map +1 -0
  96. package/dist/cli/parsers/run.d.cts +62 -0
  97. package/dist/cli/parsers/run.d.cts.map +1 -0
  98. package/dist/cli/parsers/run.d.ts +62 -0
  99. package/dist/cli/parsers/run.d.ts.map +1 -0
  100. package/dist/cli/parsers/run.js +96 -0
  101. package/dist/cli/parsers/run.js.map +1 -0
  102. package/dist/cli/parsers/test.cjs +42 -0
  103. package/dist/cli/parsers/test.cjs.map +1 -0
  104. package/dist/cli/parsers/test.d.cts +31 -0
  105. package/dist/cli/parsers/test.d.cts.map +1 -0
  106. package/dist/cli/parsers/test.d.ts +31 -0
  107. package/dist/cli/parsers/test.d.ts.map +1 -0
  108. package/dist/cli/parsers/test.js +39 -0
  109. package/dist/cli/parsers/test.js.map +1 -0
  110. package/dist/cli/theme.cjs +35 -0
  111. package/dist/cli/theme.cjs.map +1 -0
  112. package/dist/cli/theme.d.cts +31 -0
  113. package/dist/cli/theme.d.cts.map +1 -0
  114. package/dist/cli/theme.d.ts +31 -0
  115. package/dist/cli/theme.d.ts.map +1 -0
  116. package/dist/cli/theme.js +32 -0
  117. package/dist/cli/theme.js.map +1 -0
  118. package/dist/errors/base.cjs +3 -12
  119. package/dist/errors/base.cjs.map +1 -1
  120. package/dist/errors/base.d.cts +0 -7
  121. package/dist/errors/base.d.cts.map +1 -1
  122. package/dist/errors/base.d.ts +0 -7
  123. package/dist/errors/base.d.ts.map +1 -1
  124. package/dist/errors/base.js +1 -9
  125. package/dist/errors/base.js.map +1 -1
  126. package/dist/services/profiler/profile-runner.cjs +11 -0
  127. package/dist/services/profiler/profile-runner.cjs.map +1 -1
  128. package/dist/services/profiler/profile-runner.d.cts +2 -0
  129. package/dist/services/profiler/profile-runner.d.cts.map +1 -1
  130. package/dist/services/profiler/profile-runner.d.ts +2 -0
  131. package/dist/services/profiler/profile-runner.d.ts.map +1 -1
  132. package/dist/services/profiler/profile-runner.js +11 -0
  133. package/dist/services/profiler/profile-runner.js.map +1 -1
  134. package/package.json +1 -1
  135. package/src/cli/builder.ts +387 -0
  136. package/src/cli/commands/baseline.ts +7 -33
  137. package/src/cli/commands/history.ts +1 -16
  138. package/src/cli/context.ts +117 -0
  139. package/src/cli/handlers.ts +76 -0
  140. package/src/cli/index.ts +10 -1012
  141. package/src/cli/parsers/analyze.ts +61 -0
  142. package/src/cli/parsers/baseline.ts +92 -0
  143. package/src/cli/parsers/global.ts +51 -0
  144. package/src/cli/parsers/history.ts +168 -0
  145. package/src/cli/parsers/index.ts +28 -0
  146. package/src/cli/parsers/init.ts +45 -0
  147. package/src/cli/parsers/run.ts +118 -0
  148. package/src/cli/parsers/test.ts +46 -0
  149. package/src/cli/theme.ts +33 -0
  150. package/src/errors/base.ts +1 -10
  151. package/src/services/profiler/profile-runner.ts +15 -0
package/src/cli/index.ts CHANGED
@@ -3,897 +3,24 @@
3
3
  /**
4
4
  * ModestBench CLI Entry Point
5
5
  *
6
- * Command-line interface using bargs for command parsing and routing. Provides
7
- * global options, help generation, and dependency injection setup.
6
+ * Command-line interface using bargs for command parsing and routing. This
7
+ * module provides the main entry points and re-exports key types.
8
8
  *
9
9
  * @packageDocumentation
10
10
  */
11
11
 
12
- import {
13
- ansi,
14
- bargs,
15
- BargsError,
16
- HelpError,
17
- type InferParserValues,
18
- map,
19
- merge,
20
- opt,
21
- pos,
22
- } from '@boneskull/bargs';
12
+ import { BargsError, HelpError } from '@boneskull/bargs';
23
13
  import { realpathSync } from 'node:fs';
24
14
  import { fileURLToPath } from 'node:url';
25
15
 
26
- import type {
27
- BenchmarkEngine,
28
- ConfigurationManager,
29
- Engine,
30
- HistoryStorage,
31
- ProgressManager,
32
- ReporterRegistry,
33
- } from '../types/index.js';
16
+ import { ErrorCodes, ExitCodes } from '../constants.js';
17
+ import { isModestBenchError } from '../errors/index.js';
18
+ import { createCli } from './builder.js';
19
+ import { setupSignalHandlers } from './handlers.js';
34
20
 
35
- import { bootstrap } from '../bootstrap.js';
36
- import {
37
- ABORT_TIMEOUT,
38
- DEFAULT_ENGINE,
39
- DEFAULT_REPORTER,
40
- Engines,
41
- ErrorCodes,
42
- ExitCodes,
43
- Reporters,
44
- } from '../constants.js';
45
- import { AccurateEngine, TinybenchEngine } from '../core/engines/index.js';
46
- import { isError } from '../errors/base.js';
47
- import { isModestBenchError, UnknownError } from '../errors/index.js';
48
- import {
49
- CsvReporter,
50
- HumanReporter,
51
- JsonReporter,
52
- NyanReporter,
53
- SimpleReporter,
54
- } from '../reporters/index.js';
55
- // Import commands
56
- import {
57
- handleAnalyzeCommand as analyzeCommand,
58
- type AnalyzeOptions,
59
- } from './commands/analyze.js';
60
- import {
61
- handleAnalyzeCommand as handleBaselineAnalyzeCommand,
62
- handleDeleteCommand as handleBaselineDeleteCommand,
63
- handleListCommand as handleBaselineListCommand,
64
- handleSetCommand as handleBaselineSetCommand,
65
- handleShowCommand as handleBaselineShowCommand,
66
- } from './commands/baseline.js';
67
- import {
68
- handleCleanCommand,
69
- handleCompareCommand,
70
- handleExportCommand,
71
- handleListCommand,
72
- handleShowCommand,
73
- handleTrendsCommand,
74
- } from './commands/history.js';
75
- import { handleInitCommand as initCommand } from './commands/init.js';
76
- import { handleRunCommand as runCommand } from './commands/run.js';
77
- import {
78
- handleTestCommand as testCommand,
79
- type TestOptions,
80
- } from './commands/test.js';
81
-
82
- /**
83
- * CLI context with initialized services
84
- */
85
- export interface CliContext {
86
- readonly abortController: AbortController;
87
- readonly configManager: ConfigurationManager;
88
- readonly engine: BenchmarkEngine;
89
- readonly historyStorage: HistoryStorage;
90
- readonly options: InferParserValues<typeof globalOptions>;
91
- readonly progressManager: ProgressManager;
92
- readonly reporterRegistry: ReporterRegistry;
93
- }
94
-
95
- // ============================================================================
96
- // Global Options Parser
97
- // ============================================================================
98
-
99
- const globalOptions = opt.options({
100
- config: opt.string({
101
- aliases: ['c'],
102
- description: 'Path to configuration file',
103
- }),
104
- cwd: opt.string({
105
- description: 'Working directory',
106
- }),
107
- json: opt.boolean({
108
- description: 'Output results in JSON format',
109
- }),
110
- 'no-color': opt.boolean({
111
- description: 'Disable colored output',
112
- }),
113
- progress: opt.boolean({
114
- description: 'Show animated progress bar',
115
- }),
116
- verbose: opt.boolean({
117
- aliases: ['v'],
118
- description: 'Enable verbose output',
119
- }),
120
- });
121
-
122
- // ============================================================================
123
- // History Command Parsers
124
- // ============================================================================
125
-
126
- const historyListParser = opt.options({
127
- format: opt.enum(['human', 'json', 'csv'] as const, {
128
- description: 'Output format',
129
- }),
130
- limit: opt.number({
131
- description: 'Maximum number of results',
132
- }),
133
- pattern: opt.string({
134
- description: 'Filter by benchmark name pattern',
135
- }),
136
- since: opt.string({
137
- description:
138
- 'Show runs since date (ISO 8601 or relative like "1 week ago")',
139
- }),
140
- tag: opt.array('string', {
141
- aliases: ['t'],
142
- description: 'Filter by tags',
143
- }),
144
- until: opt.string({
145
- description: 'Show runs until date (ISO 8601 or relative like "1 day ago")',
146
- }),
147
- });
148
-
149
- const historyShowParser = merge(
150
- opt.options({
151
- format: opt.enum(['human', 'json', 'csv'] as const, {
152
- description: 'Output format',
153
- }),
154
- }),
155
- pos.positionals(
156
- pos.string({
157
- description: 'ID of the benchmark run to show',
158
- name: 'run-id',
159
- required: true,
160
- }),
161
- ),
162
- );
163
-
164
- const historyCompareParser = merge(
165
- opt.options({
166
- format: opt.enum(['human', 'json'] as const, {
167
- description: 'Output format',
168
- }),
169
- }),
170
- pos.positionals(
171
- pos.string({
172
- description: 'ID of the first benchmark run',
173
- name: 'run-id1',
174
- required: true,
175
- }),
176
- pos.string({
177
- description: 'ID of the second benchmark run',
178
- name: 'run-id2',
179
- required: true,
180
- }),
181
- ),
182
- );
183
-
184
- const historyTrendsParser = merge(
185
- opt.options({
186
- all: opt.boolean({
187
- aliases: ['a'],
188
- description: 'Analyze all runs (ignore limit)',
189
- }),
190
- format: opt.enum(['human', 'json'] as const, {
191
- description: 'Output format',
192
- }),
193
- limit: opt.number({
194
- description: 'Maximum number of runs to analyze',
195
- }),
196
- since: opt.string({
197
- description:
198
- 'Show trends since date (ISO 8601 or relative like "1 week ago")',
199
- }),
200
- tag: opt.array('string', {
201
- aliases: ['t'],
202
- description: 'Filter by tags',
203
- }),
204
- until: opt.string({
205
- description:
206
- 'Show trends until date (ISO 8601 or relative like "1 day ago")',
207
- }),
208
- }),
209
- pos.positionals(
210
- pos.string({
211
- description: 'Filter by benchmark name pattern',
212
- name: 'pattern',
213
- }),
214
- ),
215
- );
216
-
217
- const historyCleanParser = map(
218
- opt.options({
219
- 'max-age': opt.number({
220
- description: 'Remove runs older than this many days',
221
- }),
222
- 'max-runs': opt.number({
223
- description: 'Keep only this many most recent runs',
224
- }),
225
- 'max-size': opt.number({
226
- description: 'Keep history under this size in bytes',
227
- }),
228
- yes: opt.boolean({
229
- aliases: ['y'],
230
- description: 'Confirm cleanup without prompting',
231
- }),
232
- }),
233
- ({ positionals, values }) => {
234
- if (!values['max-age'] && !values['max-runs'] && !values['max-size']) {
235
- throw new Error(
236
- 'At least one cleanup criterion must be specified (--max-age, --max-runs, or --max-size)',
237
- );
238
- }
239
- return { positionals, values };
240
- },
241
- );
242
-
243
- const historyExportParser = opt.options({
244
- format: opt.enum(['json', 'csv'] as const, {
245
- description: 'Export format',
246
- }),
247
- output: opt.string({
248
- aliases: ['o'],
249
- description: 'Output file path',
250
- required: true,
251
- }),
252
- since: opt.string({
253
- description: 'Export runs since date',
254
- }),
255
- until: opt.string({
256
- description: 'Export runs until date',
257
- }),
258
- });
259
-
260
- // ============================================================================
261
- // Baseline Command Parsers
262
- // ============================================================================
263
-
264
- const baselineSetParser = merge(
265
- opt.options({
266
- branch: opt.string({
267
- description: 'Git branch name',
268
- }),
269
- commit: opt.string({
270
- description: 'Git commit SHA (40 characters)',
271
- }),
272
- default: opt.boolean({
273
- description: 'Set as default baseline',
274
- }),
275
- 'run-id': opt.string({
276
- description: 'Specific run ID to save (default: most recent)',
277
- }),
278
- }),
279
- pos.positionals(
280
- pos.string({
281
- description: 'Name for the baseline',
282
- name: 'name',
283
- required: true,
284
- }),
285
- ),
286
- );
287
-
288
- const baselineListParser = opt.options({
289
- format: opt.enum(['human', 'json'] as const, {
290
- description: 'Output format',
291
- }),
292
- });
293
-
294
- const baselineShowParser = merge(
295
- opt.options({
296
- format: opt.enum(['human', 'json'] as const, {
297
- description: 'Output format',
298
- }),
299
- }),
300
- pos.positionals(
301
- pos.string({
302
- description: 'Baseline name to show',
303
- name: 'name',
304
- required: true,
305
- }),
306
- ),
307
- );
308
-
309
- const baselineDeleteParser = pos.positionals(
310
- pos.string({
311
- description: 'Baseline name to delete',
312
- name: 'name',
313
- required: true,
314
- }),
315
- );
316
-
317
- const baselineAnalyzeParser = opt.options({
318
- confidence: opt.number({
319
- description: 'Confidence level (0.5-0.999, default 0.95)',
320
- }),
321
- runs: opt.number({
322
- description: 'Number of recent runs to analyze',
323
- }),
324
- });
325
-
326
- // ============================================================================
327
- // Run Command Parser
328
- // ============================================================================
329
-
330
- const runParserBase = merge(
331
- opt.options({
332
- bail: opt.boolean({
333
- aliases: ['b'],
334
- description: 'Stop on first failure',
335
- }),
336
- engine: opt.enum([Engines.TINYBENCH, Engines.ACCURATE] as const, {
337
- aliases: ['e'],
338
- description:
339
- 'Benchmark engine: tinybench (default) or accurate (requires --allow-natives-syntax)',
340
- }),
341
- exclude: opt.array('string', {
342
- aliases: ['X'],
343
- description: 'Exclude patterns',
344
- }),
345
- 'exclude-tag': opt.array('string', {
346
- aliases: ['T'],
347
- description: 'Exclude benchmarks with any of these tags',
348
- }),
349
- iterations: opt.number({
350
- aliases: ['i'],
351
- description: 'Number of iterations per benchmark',
352
- }),
353
- 'json-pretty': opt.boolean({
354
- description: 'Pretty-print JSON output (only affects json reporter)',
355
- }),
356
- 'limit-by': opt.enum(['time', 'iterations', 'any', 'all'] as const, {
357
- aliases: ['l', 'limit'],
358
- description:
359
- 'How to limit benchmarks: time (time budget), iterations (sample count), any (either threshold), all (both thresholds)',
360
- }),
361
- output: opt.string({
362
- aliases: ['o'],
363
- description: 'Output directory for reports',
364
- }),
365
- 'output-file': opt.string({
366
- aliases: ['of', 'file'],
367
- description:
368
- 'Custom filename for reporter output (use with single reporter only)',
369
- }),
370
- quiet: opt.boolean({
371
- aliases: ['q'],
372
- description: 'Minimal output',
373
- }),
374
- reporter: opt.array(
375
- [
376
- Reporters.HUMAN,
377
- Reporters.JSON,
378
- Reporters.CSV,
379
- Reporters.NYAN,
380
- Reporters.SIMPLE,
381
- ] as const,
382
- {
383
- aliases: ['r'],
384
- default: [DEFAULT_REPORTER],
385
- description: 'Output reporters to use (human,json,csv)',
386
- },
387
- ),
388
- tag: opt.array('string', {
389
- description: 'Include only benchmarks with any of these tags',
390
- }),
391
- time: opt.number({
392
- aliases: ['t'],
393
- description: 'Time budget per benchmark in milliseconds',
394
- }),
395
- timeout: opt.number({
396
- description: 'Timeout per benchmark in milliseconds',
397
- }),
398
- warmup: opt.number({
399
- aliases: ['w', 'warm'],
400
- description: 'Number of warmup iterations',
401
- }),
402
- }),
403
- pos.positionals(
404
- pos.variadic('string', {
405
- description:
406
- 'File paths, directory paths, or glob patterns for benchmark files',
407
- name: 'pattern',
408
- }),
409
- ),
410
- );
411
-
412
- // Add validation via map()
413
- const runParser = map(runParserBase, ({ positionals, values }) => {
414
- if (values.reporter && values.reporter.length > 1 && values['output-file']) {
415
- throw new Error(
416
- '--output-file can only be used with a single reporter. Use --output <dir> for multiple reporters.',
417
- );
418
- }
419
- return { positionals, values };
420
- });
421
-
422
- // ============================================================================
423
- // Init Command Parser
424
- // ============================================================================
425
-
426
- const initParser = merge(
427
- opt.options({
428
- 'config-type': opt.enum(['json', 'yaml', 'js', 'ts'] as const, {
429
- description: 'Configuration file format',
430
- }),
431
- examples: opt.boolean({
432
- description: 'Include example benchmark files',
433
- }),
434
- force: opt.boolean({
435
- description: 'Overwrite existing files',
436
- }),
437
- quiet: opt.boolean({
438
- aliases: ['q'],
439
- description: 'Minimal output',
440
- }),
441
- yes: opt.boolean({
442
- aliases: ['y'],
443
- description: 'Accept all prompts automatically',
444
- }),
445
- }),
446
- pos.positionals(
447
- pos.enum(['basic', 'advanced', 'library'] as const, {
448
- description: 'Type of project to initialize',
449
- name: 'type',
450
- }),
451
- ),
452
- );
453
-
454
- // ============================================================================
455
- // Analyze Command Parser
456
- // ============================================================================
457
-
458
- const analyzeParserBase = merge(
459
- opt.options({
460
- 'filter-file': opt.string({
461
- description: 'Filter functions by file glob pattern',
462
- }),
463
- 'group-by-file': opt.boolean({
464
- description: 'Group results by file',
465
- }),
466
- input: opt.string({
467
- aliases: ['i'],
468
- description: 'Path to existing *.cpuprofile file',
469
- }),
470
- 'min-percent': opt.number({
471
- aliases: ['m', 'min'],
472
- default: 0.5,
473
- description: 'Minimum execution percentage to show',
474
- }),
475
- top: opt.number({
476
- aliases: ['n'],
477
- default: 25,
478
- description: 'Number of top functions to show',
479
- }),
480
- }),
481
- pos.positionals(
482
- pos.string({
483
- description: 'Command to analyze (e.g., "npm test")',
484
- name: 'command',
485
- }),
486
- ),
487
- );
488
-
489
- // Add validation
490
- const analyzeParser = map(analyzeParserBase, ({ positionals, values }) => {
491
- const [command] = positionals;
492
- if (!command && !values.input) {
493
- throw new Error('Either [command] or --input must be provided');
494
- }
495
- return { positionals, values };
496
- });
497
-
498
- // ============================================================================
499
- // Test Command Parser
500
- // ============================================================================
501
-
502
- const testParser = merge(
503
- opt.options({
504
- bail: opt.boolean({
505
- aliases: ['b'],
506
- description: 'Stop on first failure',
507
- }),
508
- iterations: opt.number({
509
- aliases: ['i'],
510
- default: 100,
511
- description: 'Number of iterations per test',
512
- }),
513
- quiet: opt.boolean({
514
- aliases: ['q'],
515
- description: 'Minimal output',
516
- }),
517
- warmup: opt.number({
518
- aliases: ['w'],
519
- default: 5,
520
- description: 'Number of warmup iterations',
521
- }),
522
- }),
523
- pos.positionals(
524
- pos.enum(['ava', 'jest', 'mocha', 'node-test'] as const, {
525
- description: 'Test framework to use',
526
- name: 'framework',
527
- required: true,
528
- }),
529
- pos.variadic('string', {
530
- description: 'Test file paths or glob patterns',
531
- name: 'files',
532
- }),
533
- ),
534
- );
535
-
536
- // ============================================================================
537
- // Subcommand-specific options
538
- // ============================================================================
539
-
540
- /**
541
- * Additional global options for history and baseline subcommands
542
- */
543
- const quietOption = opt.options({
544
- quiet: opt.boolean({
545
- description: 'Minimal output',
546
- }),
547
- });
548
-
549
- // ============================================================================
550
- // Main CLI Builder
551
- // ============================================================================
552
-
553
- /**
554
- * Synthwave-inspired theme for CLI help output
555
- *
556
- * Matches the retro aesthetic used in modestbench reporters
557
- */
558
- const synthwaveTheme = {
559
- colors: {
560
- command: ansi.brightMagenta,
561
- defaultText: ansi.dim,
562
- defaultValue: ansi.brightYellow,
563
- description: ansi.brightWhite,
564
- epilog: ansi.brightWhite,
565
- example: ansi.cyan,
566
- flag: ansi.brightCyan,
567
- positional: ansi.brightMagenta,
568
- scriptName: ansi.brightCyan + ansi.bold,
569
- sectionHeader: ansi.magenta + ansi.bold,
570
- type: ansi.brightWhite + ansi.dim,
571
- url: ansi.brightCyan + ansi.underline,
572
- usage: ansi.white,
573
- },
574
- };
575
-
576
- const createCli = (abortController: AbortController) => {
577
- return bargs('modestbench', {
578
- description: 'A modern benchmark runner for Node.js',
579
- theme: synthwaveTheme,
580
- })
581
- .globals(globalOptions)
582
- .command(
583
- 'run',
584
- runParser,
585
- async ({ positionals, values }) => {
586
- const [pattern] = positionals;
587
- const context = await createCliContext(
588
- values,
589
- abortController,
590
- values.engine,
591
- );
592
- const exitCode = await runCommand(context, {
593
- bail: values.bail,
594
- config: values.config,
595
- cwd: values.cwd,
596
- engine: values.engine,
597
- exclude: values.exclude,
598
- excludeTags: values['exclude-tag'],
599
- iterations: values.iterations,
600
- json: values.json,
601
- jsonPretty: values['json-pretty'],
602
- noColor: values['no-color'],
603
- outputDir: values.output,
604
- outputFile: values['output-file'],
605
- pattern,
606
- progress: values.progress,
607
- quiet: values.quiet,
608
- reporters: values.reporter,
609
- tags: values.tag,
610
- time: values.time,
611
- timeout: values.timeout,
612
- verbose: values.verbose,
613
- warmup: values.warmup,
614
- });
615
- process.exit(exitCode);
616
- },
617
- 'Run benchmark files',
618
- )
619
- .command(
620
- 'history',
621
- (history) =>
622
- history
623
- .globals(quietOption)
624
- .command(
625
- 'list',
626
- historyListParser,
627
- async ({ values }) => {
628
- const context = await createCliContext(values, abortController);
629
- const exitCode = await handleListCommand(context, {
630
- cwd: values.cwd,
631
- format: values.format,
632
- limit: values.limit,
633
- pattern: values.pattern,
634
- since: values.since,
635
- tags: values.tag,
636
- until: values.until,
637
- verbose: values.verbose,
638
- });
639
- process.exit(exitCode);
640
- },
641
- 'List recent benchmark runs',
642
- )
643
- .command(
644
- 'show',
645
- historyShowParser,
646
- async ({ positionals, values }) => {
647
- const [runId] = positionals;
648
- const context = await createCliContext(values, abortController);
649
- const exitCode = await handleShowCommand(context, {
650
- cwd: values.cwd,
651
- format: values.format,
652
- runId,
653
- verbose: values.verbose,
654
- });
655
- process.exit(exitCode);
656
- },
657
- 'Show detailed results for a specific run',
658
- )
659
- .command(
660
- 'compare',
661
- historyCompareParser,
662
- async ({ positionals, values }) => {
663
- const [runId1, runId2] = positionals;
664
- const context = await createCliContext(values, abortController);
665
- const exitCode = await handleCompareCommand(context, {
666
- cwd: values.cwd,
667
- format: values.format,
668
- runId1,
669
- runId2,
670
- verbose: values.verbose,
671
- });
672
- process.exit(exitCode);
673
- },
674
- 'Compare two benchmark runs',
675
- )
676
- .command(
677
- 'trends',
678
- historyTrendsParser,
679
- async ({ positionals, values }) => {
680
- const [pattern] = positionals;
681
- const context = await createCliContext(values, abortController);
682
- const exitCode = await handleTrendsCommand(context, {
683
- all: values.all,
684
- cwd: values.cwd,
685
- format: values.format,
686
- limit: values.limit,
687
- pattern,
688
- since: values.since,
689
- tags: values.tag,
690
- until: values.until,
691
- verbose: values.verbose,
692
- });
693
- process.exit(exitCode);
694
- },
695
- 'Show performance trends over time',
696
- )
697
- .command(
698
- 'clean',
699
- historyCleanParser,
700
- async ({ values }) => {
701
- const context = await createCliContext(values, abortController);
702
- const exitCode = await handleCleanCommand(context, {
703
- confirm: values.yes,
704
- cwd: values.cwd,
705
- maxAge: values['max-age'],
706
- maxRuns: values['max-runs'],
707
- maxSize: values['max-size'],
708
- quiet: values.quiet,
709
- verbose: values.verbose,
710
- });
711
- process.exit(exitCode);
712
- },
713
- 'Clean up old benchmark history',
714
- )
715
- .command(
716
- 'export',
717
- historyExportParser,
718
- async ({ values }) => {
719
- const context = await createCliContext(values, abortController);
720
- const exitCode = await handleExportCommand(context, {
721
- cwd: values.cwd,
722
- format: values.format,
723
- outputPath: values.output,
724
- quiet: Boolean(values.quiet),
725
- since: values.since,
726
- until: values.until,
727
- verbose: values.verbose,
728
- });
729
- process.exitCode = exitCode;
730
- },
731
- 'Export benchmark history to a file',
732
- ),
733
- 'View and manage benchmark history',
734
- )
735
- .command(
736
- 'baseline',
737
- (baseline) =>
738
- baseline
739
- .globals(quietOption)
740
- .command(
741
- 'set',
742
- baselineSetParser,
743
- async ({ positionals, values }) => {
744
- const [name] = positionals;
745
- const context = await createCliContext(values, abortController);
746
- const exitCode = await handleBaselineSetCommand(context, {
747
- branch: values.branch,
748
- commit: values.commit,
749
- cwd: values.cwd,
750
- default: values.default,
751
- name,
752
- quiet: Boolean(values.quiet),
753
- runId: values['run-id'],
754
- verbose: values.verbose,
755
- });
756
- process.exit(exitCode);
757
- },
758
- 'Save a benchmark run as a baseline',
759
- )
760
- .command(
761
- 'list',
762
- baselineListParser,
763
- async ({ values }) => {
764
- const context = await createCliContext(values, abortController);
765
- const exitCode = await handleBaselineListCommand(context, {
766
- cwd: values.cwd,
767
- format: values.format,
768
- quiet: Boolean(values.quiet),
769
- verbose: values.verbose,
770
- });
771
- process.exit(exitCode);
772
- },
773
- 'List all saved baselines',
774
- )
775
- .command(
776
- 'show',
777
- baselineShowParser,
778
- async ({ positionals, values }) => {
779
- const [name] = positionals;
780
- const context = await createCliContext(values, abortController);
781
- const exitCode = await handleBaselineShowCommand(context, {
782
- cwd: values.cwd,
783
- format: values.format,
784
- name,
785
- quiet: Boolean(values.quiet),
786
- verbose: values.verbose,
787
- });
788
- process.exit(exitCode);
789
- },
790
- 'Show baseline details',
791
- )
792
- .command(
793
- 'delete',
794
- baselineDeleteParser,
795
- async ({ positionals, values }) => {
796
- const [name] = positionals;
797
- const context = await createCliContext(values, abortController);
798
- const exitCode = await handleBaselineDeleteCommand(context, {
799
- cwd: values.cwd,
800
- name,
801
- quiet: Boolean(values.quiet),
802
- verbose: values.verbose,
803
- });
804
- process.exit(exitCode);
805
- },
806
- 'Delete a baseline',
807
- )
808
- .command(
809
- 'analyze',
810
- baselineAnalyzeParser,
811
- async ({ values }) => {
812
- const context = await createCliContext(values, abortController);
813
- const exitCode = await handleBaselineAnalyzeCommand(context, {
814
- confidence: values.confidence,
815
- cwd: values.cwd,
816
- quiet: Boolean(values.quiet),
817
- runs: values.runs,
818
- verbose: values.verbose,
819
- });
820
- process.exit(exitCode);
821
- },
822
- 'Analyze history and suggest performance budgets',
823
- ),
824
- 'Manage performance baselines',
825
- )
826
- .command(
827
- 'init',
828
- initParser,
829
- async ({ positionals, values }) => {
830
- const [type] = positionals;
831
- const context = await createCliContext(values, abortController);
832
- const exitCode = await initCommand(context, {
833
- configType: values['config-type'],
834
- cwd: values.cwd,
835
- examples: values.examples,
836
- force: values.force,
837
- quiet: values.quiet,
838
- type,
839
- verbose: values.verbose,
840
- yes: values.yes,
841
- });
842
- process.exitCode = exitCode;
843
- },
844
- 'Initialize a new benchmark project',
845
- )
846
- .command(
847
- 'analyze',
848
- analyzeParser,
849
- async ({ positionals, values }) => {
850
- const [command] = positionals;
851
- // Context not needed for analyze command currently
852
- const context = {} as CliContext;
853
-
854
- const options: AnalyzeOptions = {
855
- color: !values['no-color'],
856
- command,
857
- cwd: values.cwd || process.cwd(),
858
- filterFile: values['filter-file'],
859
- groupByFile: values['group-by-file'],
860
- input: values.input,
861
- minPercent: values['min-percent'],
862
- top: values.top,
863
- };
864
-
865
- process.exitCode = await analyzeCommand(context, options);
866
- },
867
- {
868
- aliases: ['profile'],
869
- description: 'Analyze code execution and identify benchmark candidates',
870
- },
871
- )
872
- .command(
873
- 'test',
874
- testParser,
875
- async ({ positionals, values }) => {
876
- const [framework, files] = positionals;
877
- const context = await createCliContext(values, abortController);
878
- const options: TestOptions = {
879
- bail: values.bail,
880
- cwd: values.cwd,
881
- framework,
882
- iterations: values.iterations,
883
- json: values.json,
884
- noColor: values['no-color'],
885
- pattern: files,
886
- quiet: values.quiet,
887
- verbose: values.verbose,
888
- warmup: values.warmup,
889
- };
890
- const exitCode = await testCommand(context, options);
891
- process.exit(exitCode);
892
- },
893
- 'Run test files as benchmarks',
894
- )
895
- .defaultCommand('run');
896
- };
21
+ // Re-export types and utilities for external use
22
+ export { createCliContext } from './context.js';
23
+ export type { CliContext } from './context.js';
897
24
 
898
25
  /**
899
26
  * Initialize and run the CLI
@@ -968,125 +95,6 @@ export const main = async (
968
95
  }
969
96
  };
970
97
 
971
- /**
972
- * Create CLI context with dependency injection
973
- */
974
- const createCliContext = async (
975
- options: InferParserValues<typeof globalOptions>,
976
- abortController: AbortController,
977
- engineType: Engine = DEFAULT_ENGINE,
978
- ): Promise<CliContext> => {
979
- try {
980
- const dependencies = bootstrap();
981
-
982
- // Select engine based on type
983
- const engine =
984
- engineType === Engines.ACCURATE
985
- ? new AccurateEngine(dependencies)
986
- : new TinybenchEngine(dependencies);
987
-
988
- // Register built-in reporters
989
- engine.registerReporter(
990
- Reporters.HUMAN,
991
- new HumanReporter({
992
- color: !options['no-color'],
993
- verbose: options.verbose,
994
- }),
995
- );
996
-
997
- engine.registerReporter(
998
- 'json',
999
- new JsonReporter({
1000
- prettyPrint: false,
1001
- }),
1002
- );
1003
-
1004
- engine.registerReporter(
1005
- 'csv',
1006
- new CsvReporter({
1007
- includeHeaders: true,
1008
- includeMetadata: true,
1009
- }),
1010
- );
1011
-
1012
- engine.registerReporter(
1013
- 'simple',
1014
- new SimpleReporter({
1015
- verbose: options.verbose,
1016
- }),
1017
- );
1018
-
1019
- engine.registerReporter(
1020
- 'nyan',
1021
- new NyanReporter({
1022
- color: !options['no-color'],
1023
- }),
1024
- );
1025
-
1026
- return {
1027
- abortController,
1028
- configManager: engine.configManager,
1029
- engine,
1030
- historyStorage: engine.historyStorage,
1031
- options,
1032
- progressManager: engine.progressManager,
1033
- reporterRegistry: engine.reporterRegistry,
1034
- };
1035
- } catch (error) {
1036
- console.error(
1037
- 'Failed to initialize ModestBench:',
1038
- error instanceof Error ? error.message : String(error),
1039
- );
1040
- process.exit(ExitCodes.CONFIG_ERROR);
1041
- }
1042
- };
1043
-
1044
- /**
1045
- * Handle process signals gracefully
1046
- */
1047
- const setupSignalHandlers = (abortController: AbortController): void => {
1048
- let abortRequested = false;
1049
-
1050
- const handleSignal = (signal: NodeJS.Signals) => {
1051
- if (abortRequested) {
1052
- // Second signal, force exit
1053
- console.log(`\nReceived ${signal} again, forcing exit...`);
1054
- process.exit(computeExitCode(signal));
1055
- }
1056
-
1057
- console.log(`\nReceived ${signal}, aborting benchmarks...`);
1058
- abortRequested = true;
1059
- abortController.abort();
1060
-
1061
- // Give a short grace period for cleanup, then exit
1062
- setTimeout(() => {
1063
- console.log('\nBenchmark aborted.');
1064
- process.exit(computeExitCode(signal));
1065
- }, ABORT_TIMEOUT);
1066
- };
1067
-
1068
- process
1069
- .once('SIGINT', handleSignal)
1070
- .once('SIGQUIT', handleSignal)
1071
- .once('SIGTERM', handleSignal)
1072
- .once('uncaughtException', (error) => {
1073
- // Wrap non-ModestBench errors with UnknownError
1074
- const wrappedError: Error = isModestBenchError(error)
1075
- ? error
1076
- : new UnknownError(error.message, { cause: error });
1077
- console.error(`${wrappedError}`);
1078
- process.exit(ExitCodes.RUNTIME_ERROR);
1079
- })
1080
- .once('unhandledRejection', (reason) => {
1081
- const wrappedError = new UnknownError(
1082
- isError(reason) ? reason.message : String(reason),
1083
- { cause: reason },
1084
- );
1085
- console.error(`${wrappedError}`);
1086
- process.exit(ExitCodes.RUNTIME_ERROR);
1087
- });
1088
- };
1089
-
1090
98
  // Run CLI if this file is executed directly
1091
99
  const scriptPath = fileURLToPath(import.meta.url);
1092
100
  const argPath = process.argv[1];
@@ -1105,13 +113,3 @@ try {
1105
113
  cli();
1106
114
  }
1107
115
  }
1108
-
1109
- /**
1110
- * Compute the exit code based on the signal
1111
- *
1112
- * @param signal - The signal that caused the exit
1113
- * @returns The exit code
1114
- */
1115
- const computeExitCode = (signal: NodeJS.Signals): number => {
1116
- return 128 + (signal === 'SIGINT' ? 2 : signal === 'SIGQUIT' ? 3 : 15);
1117
- };