modestbench 0.1.0 → 0.2.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 (267) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +39 -31
  3. package/dist/bootstrap.cjs +10 -10
  4. package/dist/bootstrap.cjs.map +1 -1
  5. package/dist/bootstrap.d.cts.map +1 -1
  6. package/dist/bootstrap.d.ts.map +1 -1
  7. package/dist/bootstrap.js +5 -5
  8. package/dist/bootstrap.js.map +1 -1
  9. package/dist/cli/commands/history.cjs +108 -266
  10. package/dist/cli/commands/history.cjs.map +1 -1
  11. package/dist/cli/commands/history.d.cts +75 -12
  12. package/dist/cli/commands/history.d.cts.map +1 -1
  13. package/dist/cli/commands/history.d.ts +75 -12
  14. package/dist/cli/commands/history.d.ts.map +1 -1
  15. package/dist/cli/commands/history.js +105 -268
  16. package/dist/cli/commands/history.js.map +1 -1
  17. package/dist/cli/commands/run.cjs +18 -5
  18. package/dist/cli/commands/run.cjs.map +1 -1
  19. package/dist/cli/commands/run.d.cts +1 -0
  20. package/dist/cli/commands/run.d.cts.map +1 -1
  21. package/dist/cli/commands/run.d.ts +1 -0
  22. package/dist/cli/commands/run.d.ts.map +1 -1
  23. package/dist/cli/commands/run.js +18 -5
  24. package/dist/cli/commands/run.js.map +1 -1
  25. package/dist/cli/index.cjs +307 -91
  26. package/dist/cli/index.cjs.map +1 -1
  27. package/dist/cli/index.d.cts.map +1 -1
  28. package/dist/cli/index.d.ts.map +1 -1
  29. package/dist/cli/index.js +308 -92
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/core/engine.cjs +8 -1
  32. package/dist/core/engine.cjs.map +1 -1
  33. package/dist/core/engine.d.cts +3 -0
  34. package/dist/core/engine.d.cts.map +1 -1
  35. package/dist/core/engine.d.ts +3 -0
  36. package/dist/core/engine.d.ts.map +1 -1
  37. package/dist/core/engine.js +8 -1
  38. package/dist/core/engine.js.map +1 -1
  39. package/dist/core/output-path-resolver.cjs +34 -0
  40. package/dist/core/output-path-resolver.cjs.map +1 -0
  41. package/dist/core/output-path-resolver.d.cts +10 -0
  42. package/dist/core/output-path-resolver.d.cts.map +1 -0
  43. package/dist/core/output-path-resolver.d.ts +10 -0
  44. package/dist/core/output-path-resolver.d.ts.map +1 -0
  45. package/dist/core/output-path-resolver.js +30 -0
  46. package/dist/core/output-path-resolver.js.map +1 -0
  47. package/dist/formatters/history/base.cjs +9 -0
  48. package/dist/formatters/history/base.cjs.map +1 -0
  49. package/dist/formatters/history/base.d.cts +26 -0
  50. package/dist/formatters/history/base.d.cts.map +1 -0
  51. package/dist/formatters/history/base.d.ts +26 -0
  52. package/dist/formatters/history/base.d.ts.map +1 -0
  53. package/dist/formatters/history/base.js +8 -0
  54. package/dist/formatters/history/base.js.map +1 -0
  55. package/dist/formatters/history/compare.cjs +127 -0
  56. package/dist/formatters/history/compare.cjs.map +1 -0
  57. package/dist/formatters/history/compare.d.cts +21 -0
  58. package/dist/formatters/history/compare.d.cts.map +1 -0
  59. package/dist/formatters/history/compare.d.ts +21 -0
  60. package/dist/formatters/history/compare.d.ts.map +1 -0
  61. package/dist/formatters/history/compare.js +123 -0
  62. package/dist/formatters/history/compare.js.map +1 -0
  63. package/dist/formatters/history/list.cjs +74 -0
  64. package/dist/formatters/history/list.cjs.map +1 -0
  65. package/dist/formatters/history/list.d.cts +25 -0
  66. package/dist/formatters/history/list.d.cts.map +1 -0
  67. package/dist/formatters/history/list.d.ts +25 -0
  68. package/dist/formatters/history/list.d.ts.map +1 -0
  69. package/dist/formatters/history/list.js +70 -0
  70. package/dist/formatters/history/list.js.map +1 -0
  71. package/dist/formatters/history/show.cjs +98 -0
  72. package/dist/formatters/history/show.cjs.map +1 -0
  73. package/dist/formatters/history/show.d.cts +21 -0
  74. package/dist/formatters/history/show.d.cts.map +1 -0
  75. package/dist/formatters/history/show.d.ts +21 -0
  76. package/dist/formatters/history/show.d.ts.map +1 -0
  77. package/dist/formatters/history/show.js +94 -0
  78. package/dist/formatters/history/show.js.map +1 -0
  79. package/dist/formatters/history/trends.cjs +194 -0
  80. package/dist/formatters/history/trends.cjs.map +1 -0
  81. package/dist/formatters/history/trends.d.cts +22 -0
  82. package/dist/formatters/history/trends.d.cts.map +1 -0
  83. package/dist/formatters/history/trends.d.ts +22 -0
  84. package/dist/formatters/history/trends.d.ts.map +1 -0
  85. package/dist/formatters/history/trends.js +190 -0
  86. package/dist/formatters/history/trends.js.map +1 -0
  87. package/dist/formatters/history/visualization.cjs +79 -0
  88. package/dist/formatters/history/visualization.cjs.map +1 -0
  89. package/dist/formatters/history/visualization.d.cts +24 -0
  90. package/dist/formatters/history/visualization.d.cts.map +1 -0
  91. package/dist/formatters/history/visualization.d.ts +24 -0
  92. package/dist/formatters/history/visualization.d.ts.map +1 -0
  93. package/dist/formatters/history/visualization.js +74 -0
  94. package/dist/formatters/history/visualization.js.map +1 -0
  95. package/dist/index.cjs +15 -17
  96. package/dist/index.cjs.map +1 -1
  97. package/dist/index.d.cts +5 -5
  98. package/dist/index.d.cts.map +1 -1
  99. package/dist/index.d.ts +5 -5
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +7 -9
  102. package/dist/index.js.map +1 -1
  103. package/dist/reporters/csv.cjs +2 -2
  104. package/dist/reporters/csv.cjs.map +1 -1
  105. package/dist/reporters/csv.d.cts +1 -1
  106. package/dist/reporters/csv.d.cts.map +1 -1
  107. package/dist/reporters/csv.d.ts +1 -1
  108. package/dist/reporters/csv.d.ts.map +1 -1
  109. package/dist/reporters/csv.js +1 -1
  110. package/dist/reporters/csv.js.map +1 -1
  111. package/dist/reporters/human.cjs +24 -62
  112. package/dist/reporters/human.cjs.map +1 -1
  113. package/dist/reporters/human.d.cts +1 -1
  114. package/dist/reporters/human.d.cts.map +1 -1
  115. package/dist/reporters/human.d.ts +1 -1
  116. package/dist/reporters/human.d.ts.map +1 -1
  117. package/dist/reporters/human.js +2 -40
  118. package/dist/reporters/human.js.map +1 -1
  119. package/dist/reporters/json.cjs +2 -2
  120. package/dist/reporters/json.cjs.map +1 -1
  121. package/dist/reporters/json.d.cts +1 -1
  122. package/dist/reporters/json.d.cts.map +1 -1
  123. package/dist/reporters/json.d.ts +1 -1
  124. package/dist/reporters/json.d.ts.map +1 -1
  125. package/dist/reporters/json.js +1 -1
  126. package/dist/reporters/json.js.map +1 -1
  127. package/dist/reporters/simple.cjs +2 -2
  128. package/dist/reporters/simple.cjs.map +1 -1
  129. package/dist/reporters/simple.d.cts +1 -1
  130. package/dist/reporters/simple.d.cts.map +1 -1
  131. package/dist/reporters/simple.d.ts +1 -1
  132. package/dist/reporters/simple.d.ts.map +1 -1
  133. package/dist/reporters/simple.js +1 -1
  134. package/dist/reporters/simple.js.map +1 -1
  135. package/dist/{config/manager.cjs → services/config-manager.cjs} +2 -2
  136. package/dist/services/config-manager.cjs.map +1 -0
  137. package/dist/{config/manager.d.cts → services/config-manager.d.cts} +1 -1
  138. package/dist/services/config-manager.d.cts.map +1 -0
  139. package/dist/{config/manager.d.ts → services/config-manager.d.ts} +1 -1
  140. package/dist/services/config-manager.d.ts.map +1 -0
  141. package/dist/{config/manager.js → services/config-manager.js} +2 -2
  142. package/dist/services/config-manager.js.map +1 -0
  143. package/dist/{core/loader.cjs → services/file-loader.cjs} +2 -2
  144. package/dist/services/file-loader.cjs.map +1 -0
  145. package/dist/{core/loader.d.cts → services/file-loader.d.cts} +1 -1
  146. package/dist/services/file-loader.d.cts.map +1 -0
  147. package/dist/{core/loader.d.ts → services/file-loader.d.ts} +1 -1
  148. package/dist/services/file-loader.d.ts.map +1 -0
  149. package/dist/{core/loader.js → services/file-loader.js} +2 -2
  150. package/dist/services/file-loader.js.map +1 -0
  151. package/dist/services/history/comparison.cjs +124 -0
  152. package/dist/services/history/comparison.cjs.map +1 -0
  153. package/dist/services/history/comparison.d.cts +18 -0
  154. package/dist/services/history/comparison.d.cts.map +1 -0
  155. package/dist/services/history/comparison.d.ts +18 -0
  156. package/dist/services/history/comparison.d.ts.map +1 -0
  157. package/dist/services/history/comparison.js +120 -0
  158. package/dist/services/history/comparison.js.map +1 -0
  159. package/dist/services/history/models.cjs +9 -0
  160. package/dist/services/history/models.cjs.map +1 -0
  161. package/dist/services/history/models.d.cts +139 -0
  162. package/dist/services/history/models.d.cts.map +1 -0
  163. package/dist/services/history/models.d.ts +139 -0
  164. package/dist/services/history/models.d.ts.map +1 -0
  165. package/dist/services/history/models.js +8 -0
  166. package/dist/services/history/models.js.map +1 -0
  167. package/dist/services/history/query.cjs +97 -0
  168. package/dist/services/history/query.cjs.map +1 -0
  169. package/dist/services/history/query.d.cts +38 -0
  170. package/dist/services/history/query.d.cts.map +1 -0
  171. package/dist/services/history/query.d.ts +38 -0
  172. package/dist/services/history/query.d.ts.map +1 -0
  173. package/dist/services/history/query.js +92 -0
  174. package/dist/services/history/query.js.map +1 -0
  175. package/dist/services/history/trend-analysis.cjs +187 -0
  176. package/dist/services/history/trend-analysis.cjs.map +1 -0
  177. package/dist/services/history/trend-analysis.d.cts +34 -0
  178. package/dist/services/history/trend-analysis.d.cts.map +1 -0
  179. package/dist/services/history/trend-analysis.d.ts +34 -0
  180. package/dist/services/history/trend-analysis.d.ts.map +1 -0
  181. package/dist/services/history/trend-analysis.js +179 -0
  182. package/dist/services/history/trend-analysis.js.map +1 -0
  183. package/dist/{storage/history.cjs → services/history-storage.cjs} +1 -1
  184. package/dist/services/history-storage.cjs.map +1 -0
  185. package/dist/{storage/history.d.cts → services/history-storage.d.cts} +1 -1
  186. package/dist/services/history-storage.d.cts.map +1 -0
  187. package/dist/{storage/history.d.ts → services/history-storage.d.ts} +1 -1
  188. package/dist/services/history-storage.d.ts.map +1 -0
  189. package/dist/{storage/history.js → services/history-storage.js} +1 -1
  190. package/dist/services/history-storage.js.map +1 -0
  191. package/dist/{progress/manager.cjs → services/progress-manager.cjs} +1 -1
  192. package/dist/services/progress-manager.cjs.map +1 -0
  193. package/dist/{progress/manager.d.cts → services/progress-manager.d.cts} +1 -1
  194. package/dist/services/progress-manager.d.cts.map +1 -0
  195. package/dist/{progress/manager.d.ts → services/progress-manager.d.ts} +1 -1
  196. package/dist/services/progress-manager.d.ts.map +1 -0
  197. package/dist/{progress/manager.js → services/progress-manager.js} +1 -1
  198. package/dist/services/progress-manager.js.map +1 -0
  199. package/dist/{reporters/registry.cjs → services/reporter-registry.cjs} +1 -1
  200. package/dist/services/reporter-registry.cjs.map +1 -0
  201. package/dist/{reporters/registry.d.cts → services/reporter-registry.d.cts} +1 -1
  202. package/dist/services/reporter-registry.d.cts.map +1 -0
  203. package/dist/{reporters/registry.d.ts → services/reporter-registry.d.ts} +1 -1
  204. package/dist/services/reporter-registry.d.ts.map +1 -0
  205. package/dist/{reporters/registry.js → services/reporter-registry.js} +1 -1
  206. package/dist/services/reporter-registry.js.map +1 -0
  207. package/dist/types/cli.d.cts +3 -0
  208. package/dist/types/cli.d.cts.map +1 -1
  209. package/dist/types/cli.d.ts +3 -0
  210. package/dist/types/cli.d.ts.map +1 -1
  211. package/dist/utils/ansi.cjs +61 -0
  212. package/dist/utils/ansi.cjs.map +1 -0
  213. package/dist/utils/ansi.d.cts +53 -0
  214. package/dist/utils/ansi.d.cts.map +1 -0
  215. package/dist/utils/ansi.d.ts +53 -0
  216. package/dist/utils/ansi.d.ts.map +1 -0
  217. package/dist/utils/ansi.js +57 -0
  218. package/dist/utils/ansi.js.map +1 -0
  219. package/package.json +5 -4
  220. package/src/bootstrap.ts +5 -5
  221. package/src/cli/commands/history.ts +194 -342
  222. package/src/cli/commands/run.ts +32 -3
  223. package/src/cli/index.ts +361 -106
  224. package/src/core/engine.ts +9 -1
  225. package/src/core/output-path-resolver.ts +38 -0
  226. package/src/formatters/history/base.ts +28 -0
  227. package/src/formatters/history/compare.ts +186 -0
  228. package/src/formatters/history/list.ts +101 -0
  229. package/src/formatters/history/show.ts +155 -0
  230. package/src/formatters/history/trends.ts +281 -0
  231. package/src/formatters/history/visualization.ts +93 -0
  232. package/src/index.ts +7 -11
  233. package/src/reporters/csv.ts +1 -1
  234. package/src/reporters/human.ts +2 -42
  235. package/src/reporters/json.ts +1 -1
  236. package/src/reporters/simple.ts +1 -1
  237. package/src/{config/manager.ts → services/config-manager.ts} +1 -1
  238. package/src/{core/loader.ts → services/file-loader.ts} +1 -1
  239. package/src/services/history/comparison.ts +130 -0
  240. package/src/services/history/models.ts +148 -0
  241. package/src/services/history/query.ts +116 -0
  242. package/src/services/history/trend-analysis.ts +238 -0
  243. package/src/types/cli.ts +3 -0
  244. package/src/utils/ansi.ts +59 -0
  245. package/dist/config/manager.cjs.map +0 -1
  246. package/dist/config/manager.d.cts.map +0 -1
  247. package/dist/config/manager.d.ts.map +0 -1
  248. package/dist/config/manager.js.map +0 -1
  249. package/dist/core/loader.cjs.map +0 -1
  250. package/dist/core/loader.d.cts.map +0 -1
  251. package/dist/core/loader.d.ts.map +0 -1
  252. package/dist/core/loader.js.map +0 -1
  253. package/dist/progress/manager.cjs.map +0 -1
  254. package/dist/progress/manager.d.cts.map +0 -1
  255. package/dist/progress/manager.d.ts.map +0 -1
  256. package/dist/progress/manager.js.map +0 -1
  257. package/dist/reporters/registry.cjs.map +0 -1
  258. package/dist/reporters/registry.d.cts.map +0 -1
  259. package/dist/reporters/registry.d.ts.map +0 -1
  260. package/dist/reporters/registry.js.map +0 -1
  261. package/dist/storage/history.cjs.map +0 -1
  262. package/dist/storage/history.d.cts.map +0 -1
  263. package/dist/storage/history.d.ts.map +0 -1
  264. package/dist/storage/history.js.map +0 -1
  265. /package/src/{storage/history.ts → services/history-storage.ts} +0 -0
  266. /package/src/{progress/manager.ts → services/progress-manager.ts} +0 -0
  267. /package/src/{reporters/registry.ts → services/reporter-registry.ts} +0 -0
@@ -11,6 +11,7 @@ import type { BenchmarkRun } from '../../types/index.js';
11
11
  import type { CliContext } from '../index.js';
12
12
 
13
13
  import { ErrorCodes } from '../../constants.js';
14
+ import { resolveOutputPath } from '../../core/output-path-resolver.js';
14
15
  import {
15
16
  InvalidArgumentError,
16
17
  type ModestBenchError,
@@ -32,6 +33,7 @@ interface RunOptions {
32
33
  json?: boolean | undefined;
33
34
  noColor?: boolean | undefined;
34
35
  outputDir?: string | undefined;
36
+ outputFile?: string | undefined;
35
37
  pattern: string[];
36
38
  progress?: boolean | undefined;
37
39
  quiet?: boolean | undefined;
@@ -61,6 +63,18 @@ export const handleRunCommand = async (
61
63
  const showCliMessages = verbose && !options.quiet;
62
64
 
63
65
  try {
66
+ // Validate --output-file usage
67
+ if (
68
+ options.outputFile &&
69
+ options.reporters &&
70
+ options.reporters.length > 1
71
+ ) {
72
+ throw new InvalidArgumentError(
73
+ '--output-file can only be used with a single reporter. ' +
74
+ 'Use --output <dir> for multiple reporters.',
75
+ );
76
+ }
77
+
64
78
  // Step 1: Load and merge configuration
65
79
  if (showCliMessages) {
66
80
  console.error('Loading configuration...');
@@ -79,6 +93,7 @@ export const handleRunCommand = async (
79
93
  showCliMessages,
80
94
  options.quiet ?? false,
81
95
  options.outputDir,
96
+ options.outputFile,
82
97
  options.progress,
83
98
  );
84
99
 
@@ -161,10 +176,13 @@ export const handleRunCommand = async (
161
176
 
162
177
  return handleResults(executionResult, options, shouldBeQuiet);
163
178
  } catch (error) {
164
- // Re-throw FileDiscoveryError so yargs fail handler can show help
179
+ // Re-throw CLI errors so yargs fail handler can show help
165
180
  if ((error as ModestBenchError).code === ErrorCodes.FILE_DISCOVERY_FAILED) {
166
181
  throw error;
167
182
  }
183
+ if ((error as ModestBenchError).code === ErrorCodes.CLI_INVALID_ARGUMENT) {
184
+ throw error;
185
+ }
168
186
 
169
187
  if (!shouldBeQuiet) {
170
188
  console.error(
@@ -298,6 +316,7 @@ const setupReporters = async (
298
316
  showCliMessages: boolean,
299
317
  explicitQuiet: boolean,
300
318
  explicitOutputDir?: string,
319
+ explicitOutputFile?: string,
301
320
  progressOption?: boolean,
302
321
  ) => {
303
322
  try {
@@ -328,17 +347,27 @@ const setupReporters = async (
328
347
  verbose: isVerbose,
329
348
  });
330
349
  } else if (reporterName === 'json') {
350
+ const outputPath = resolveOutputPath(
351
+ outputDir,
352
+ explicitOutputFile,
353
+ 'results.json',
354
+ );
331
355
  reporter = new JsonReporter({
332
- ...(outputDir ? { outputPath: `${outputDir}/results.json` } : {}),
356
+ ...(outputPath ? { outputPath } : {}),
333
357
  prettyPrint: true,
334
358
  quiet: shouldBeQuiet, // JSON uses shouldBeQuiet to avoid polluting stdout
335
359
  verbose: isVerbose,
336
360
  });
337
361
  } else if (reporterName === 'csv') {
362
+ const outputPath = resolveOutputPath(
363
+ outputDir,
364
+ explicitOutputFile,
365
+ 'results.csv',
366
+ );
338
367
  reporter = new CsvReporter({
339
368
  includeHeaders: true,
340
369
  includeMetadata: true,
341
- ...(outputDir ? { outputPath: `${outputDir}/results.csv` } : {}),
370
+ ...(outputPath ? { outputPath } : {}),
342
371
  quiet: explicitQuiet, // Only applies explicit --quiet flag; CSV output can coexist with progress messages on different streams
343
372
  verbose: isVerbose,
344
373
  });
package/src/cli/index.ts CHANGED
@@ -32,7 +32,14 @@ import {
32
32
  SimpleReporter,
33
33
  } from '../reporters/index.js';
34
34
  // Import commands
35
- import { handleHistoryCommand as historyCommand } from './commands/history.js';
35
+ import {
36
+ handleCleanCommand,
37
+ handleCompareCommand,
38
+ handleExportCommand,
39
+ handleListCommand,
40
+ handleShowCommand,
41
+ handleTrendsCommand,
42
+ } from './commands/history.js';
36
43
  import { handleInitCommand as initCommand } from './commands/init.js';
37
44
  import { handleRunCommand as runCommand } from './commands/run.js';
38
45
 
@@ -104,6 +111,7 @@ export const main = async (
104
111
 
105
112
  // Configure global options and commands
106
113
  await cli
114
+ .scriptName('modestbench')
107
115
  .option('config', {
108
116
  alias: 'c',
109
117
  description: 'Path to configuration file',
@@ -137,6 +145,7 @@ export const main = async (
137
145
  })
138
146
  .option('cwd', {
139
147
  default: process.cwd(),
148
+ defaultDescription: '.',
140
149
  description: 'Working directory',
141
150
  global: true,
142
151
  type: 'string',
@@ -187,6 +196,13 @@ export const main = async (
187
196
  description: 'Output directory for reports',
188
197
  type: 'string',
189
198
  })
199
+ .option('output-file', {
200
+ alias: 'of',
201
+ description:
202
+ 'Custom filename for reporter output (use with single reporter only)',
203
+ requiresArg: true,
204
+ type: 'string',
205
+ })
190
206
  .option('iterations', {
191
207
  alias: 'i',
192
208
  description: 'Number of iterations per benchmark',
@@ -301,6 +317,7 @@ export const main = async (
301
317
  json: argv.json,
302
318
  noColor: argv.noColor,
303
319
  outputDir: argv.output,
320
+ outputFile: argv['output-file'],
304
321
  pattern: argv.pattern,
305
322
  progress: argv.progress,
306
323
  quiet: argv.quiet,
@@ -314,111 +331,349 @@ export const main = async (
314
331
  process.exit(exitCode);
315
332
  },
316
333
  )
317
- .command(
318
- 'history <subcommand> [args..]',
319
- 'View and manage benchmark history',
320
- (yargs) => {
321
- return yargs
322
- .positional('subcommand', {
323
- choices: [
324
- 'list',
325
- 'show',
326
- 'compare',
327
- 'trends',
328
- 'clean',
329
- 'export',
330
- ] as const,
331
- demandOption: true,
332
- describe: 'History subcommand',
333
- type: 'string',
334
- })
335
- .positional('args', {
336
- array: true,
337
- describe: 'Additional arguments for the subcommand',
338
- type: 'string',
339
- })
340
- .option('since', {
341
- description:
342
- 'Show runs since date (ISO 8601 or relative like "1 week ago")',
343
- type: 'string',
344
- })
345
- .option('until', {
346
- description:
347
- 'Show runs until date (ISO 8601 or relative like "1 day ago")',
348
- type: 'string',
349
- })
350
- .option('pattern', {
351
- description: 'Filter by benchmark name pattern',
352
- type: 'string',
353
- })
354
- .option('tags', {
355
- description: 'Filter by tags',
356
- type: 'array',
357
- })
358
- .option('limit', {
359
- default: 10,
360
- description: 'Maximum number of results',
361
- type: 'number',
362
- })
363
- .option('format', {
364
- choices: ['human', 'json', 'csv'] as const,
365
- default: 'human' as const,
366
- description: 'Output format',
367
- type: 'string',
368
- })
369
- .option('maxAge', {
370
- description: 'Maximum age in days for cleanup',
371
- type: 'number',
372
- })
373
- .option('maxRuns', {
374
- description: 'Maximum number of runs to keep',
375
- type: 'number',
376
- })
377
- .option('maxSize', {
378
- description: 'Maximum storage size in bytes',
379
- type: 'number',
380
- })
381
- .option('confirm', {
382
- default: false,
383
- description: 'Confirm cleanup operations',
384
- type: 'boolean',
385
- })
386
- .option('output', {
387
- description: 'Output file path',
388
- type: 'string',
389
- })
390
- .example([
391
- ['$0 history list', 'List recent benchmark runs'],
392
- ['$0 history show <run-id>', 'Show detailed results for run'],
393
- ['$0 history compare <run-id1> <run-id2>', 'Compare two runs'],
394
- ['$0 history trends [pattern]', 'Show performance trends'],
395
- ['$0 history clean --max-runs 50', 'Keep only latest 50 runs'],
396
- ['$0 history export --format csv', 'Export to CSV'],
397
- ]);
398
- },
399
- async (argv) => {
400
- const context = await createCliContext(argv, abortController!);
401
- const exitCode = await historyCommand(context, {
402
- args: argv.args,
403
- confirm: argv.confirm,
404
- cwd: argv.cwd,
405
- format: argv.format,
406
- limit: argv.limit,
407
- maxAge: argv.maxAge,
408
- maxRuns: argv.maxRuns,
409
- maxSize: argv.maxSize,
410
- outputDir: argv.output,
411
- pattern: argv.pattern,
412
- quiet: Boolean(argv.quiet),
413
- since: argv.since,
414
- subcommand: argv.subcommand,
415
- tags: argv.tags as string[] | undefined,
416
- until: argv.until,
417
- verbose: argv.verbose,
418
- });
419
- process.exit(exitCode);
420
- },
421
- )
334
+ .command('history', 'View and manage benchmark history', (yargs) => {
335
+ return yargs
336
+ .command(
337
+ 'list',
338
+ 'List recent benchmark runs',
339
+ (yargs) => {
340
+ return yargs
341
+ .option('since', {
342
+ description:
343
+ 'Show runs since date (ISO 8601 or relative like "1 week ago")',
344
+ type: 'string',
345
+ })
346
+ .option('until', {
347
+ description:
348
+ 'Show runs until date (ISO 8601 or relative like "1 day ago")',
349
+ type: 'string',
350
+ })
351
+ .option('pattern', {
352
+ description: 'Filter by benchmark name pattern',
353
+ type: 'string',
354
+ })
355
+ .option('tags', {
356
+ description: 'Filter by tags (comma-separated)',
357
+ type: 'array',
358
+ })
359
+ .option('limit', {
360
+ default: 10,
361
+ description: 'Maximum number of results',
362
+ type: 'number',
363
+ })
364
+ .option('format', {
365
+ choices: ['human', 'json', 'csv'] as const,
366
+ default: 'human' as const,
367
+ description: 'Output format',
368
+ type: 'string',
369
+ })
370
+ .example([
371
+ ['$0 history list', 'List recent benchmark runs'],
372
+ [
373
+ '$0 history list --since "1 week ago"',
374
+ 'List runs from last week',
375
+ ],
376
+ ['$0 history list --limit 20', 'List 20 most recent runs'],
377
+ ['$0 history list --format json', 'List runs in JSON format'],
378
+ ]);
379
+ },
380
+ async (argv) => {
381
+ const context = await createCliContext(argv, abortController!);
382
+ const exitCode = await handleListCommand(context, {
383
+ cwd: argv.cwd,
384
+ format: argv.format,
385
+ limit: argv.limit,
386
+ pattern: argv.pattern,
387
+ quiet: Boolean(argv.quiet),
388
+ since: argv.since,
389
+ tags: argv.tags as string[] | undefined,
390
+ until: argv.until,
391
+ verbose: argv.verbose,
392
+ });
393
+ process.exit(exitCode);
394
+ },
395
+ )
396
+ .command(
397
+ 'show <run-id>',
398
+ 'Show detailed results for a specific run',
399
+ (yargs) => {
400
+ return yargs
401
+ .positional('run-id', {
402
+ describe: 'ID of the benchmark run to show',
403
+ type: 'string',
404
+ })
405
+ .option('format', {
406
+ choices: ['human', 'json', 'csv'] as const,
407
+ default: 'human' as const,
408
+ description: 'Output format',
409
+ type: 'string',
410
+ })
411
+ .example([
412
+ [
413
+ '$0 history show abc123',
414
+ 'Show detailed results for run abc123',
415
+ ],
416
+ [
417
+ '$0 history show abc123 --format json',
418
+ 'Show run in JSON format',
419
+ ],
420
+ ]);
421
+ },
422
+ async (argv) => {
423
+ const context = await createCliContext(argv, abortController!);
424
+ const exitCode = await handleShowCommand(context, {
425
+ cwd: argv.cwd,
426
+ format: argv.format,
427
+ quiet: Boolean(argv.quiet),
428
+ runId: String(argv['run-id']),
429
+ verbose: argv.verbose,
430
+ });
431
+ process.exit(exitCode);
432
+ },
433
+ )
434
+ .command(
435
+ 'compare <run-id1> <run-id2>',
436
+ 'Compare two benchmark runs',
437
+ (yargs) => {
438
+ return yargs
439
+ .positional('run-id1', {
440
+ describe: 'ID of the first benchmark run',
441
+ type: 'string',
442
+ })
443
+ .positional('run-id2', {
444
+ describe: 'ID of the second benchmark run',
445
+ type: 'string',
446
+ })
447
+ .option('format', {
448
+ choices: ['human', 'json'] as const,
449
+ default: 'human' as const,
450
+ description: 'Output format',
451
+ type: 'string',
452
+ })
453
+ .example([
454
+ ['$0 history compare abc123 def456', 'Compare two runs'],
455
+ [
456
+ '$0 history compare abc123 def456 --format json',
457
+ 'Compare in JSON format',
458
+ ],
459
+ ]);
460
+ },
461
+ async (argv) => {
462
+ const context = await createCliContext(argv, abortController!);
463
+ const exitCode = await handleCompareCommand(context, {
464
+ cwd: argv.cwd,
465
+ format: argv.format,
466
+ quiet: Boolean(argv.quiet),
467
+ runId1: String(argv['run-id1']),
468
+ runId2: String(argv['run-id2']),
469
+ verbose: argv.verbose,
470
+ });
471
+ process.exit(exitCode);
472
+ },
473
+ )
474
+ .command(
475
+ 'trends [pattern]',
476
+ 'Show performance trends over time',
477
+ (yargs) => {
478
+ return yargs
479
+ .positional('pattern', {
480
+ describe: 'Filter by benchmark name pattern',
481
+ type: 'string',
482
+ })
483
+ .option('since', {
484
+ description:
485
+ 'Show trends since date (ISO 8601 or relative like "1 week ago")',
486
+ type: 'string',
487
+ })
488
+ .option('until', {
489
+ description:
490
+ 'Show trends until date (ISO 8601 or relative like "1 day ago")',
491
+ type: 'string',
492
+ })
493
+ .option('tags', {
494
+ description: 'Filter by tags (comma-separated)',
495
+ type: 'array',
496
+ })
497
+ .option('limit', {
498
+ description: 'Maximum number of runs to analyze',
499
+ type: 'number',
500
+ })
501
+ .option('all', {
502
+ alias: 'a',
503
+ default: false,
504
+ description: 'Analyze all runs (ignore limit)',
505
+ type: 'boolean',
506
+ })
507
+ .option('format', {
508
+ choices: ['human', 'json'] as const,
509
+ default: 'human' as const,
510
+ description: 'Output format',
511
+ type: 'string',
512
+ })
513
+ .example([
514
+ [
515
+ '$0 history trends',
516
+ 'Show performance trends for all benchmarks',
517
+ ],
518
+ [
519
+ '$0 history trends --since "1 month ago"',
520
+ 'Show trends from last month',
521
+ ],
522
+ [
523
+ '$0 history trends "array-*"',
524
+ 'Show trends for array benchmarks',
525
+ ],
526
+ [
527
+ '$0 history trends --format json',
528
+ 'Output trends in JSON format',
529
+ ],
530
+ ]);
531
+ },
532
+ async (argv) => {
533
+ const context = await createCliContext(argv, abortController!);
534
+ const exitCode = await handleTrendsCommand(context, {
535
+ all: Boolean(argv.all),
536
+ cwd: argv.cwd,
537
+ format: argv.format,
538
+ limit: argv.limit,
539
+ pattern: argv.pattern,
540
+ quiet: Boolean(argv.quiet),
541
+ since: argv.since,
542
+ tags: argv.tags as string[] | undefined,
543
+ until: argv.until,
544
+ verbose: argv.verbose,
545
+ });
546
+ process.exit(exitCode);
547
+ },
548
+ )
549
+ .command(
550
+ 'clean',
551
+ 'Clean up old benchmark history',
552
+ (yargs) => {
553
+ return yargs
554
+ .option('max-age', {
555
+ description: 'Remove runs older than this many days',
556
+ type: 'number',
557
+ })
558
+ .option('max-runs', {
559
+ description: 'Keep only this many most recent runs',
560
+ type: 'number',
561
+ })
562
+ .option('max-size', {
563
+ description: 'Keep history under this size in bytes',
564
+ type: 'number',
565
+ })
566
+ .option('confirm', {
567
+ default: false,
568
+ description: 'Confirm cleanup without prompting',
569
+ type: 'boolean',
570
+ })
571
+ .check((argv) => {
572
+ if (
573
+ !argv['max-age'] &&
574
+ !argv['max-runs'] &&
575
+ !argv['max-size']
576
+ ) {
577
+ throw new Error(
578
+ 'At least one cleanup criterion must be specified (--max-age, --max-runs, or --max-size)',
579
+ );
580
+ }
581
+ return true;
582
+ })
583
+ .example([
584
+ [
585
+ '$0 history clean --max-runs 50 --confirm',
586
+ 'Keep only latest 50 runs',
587
+ ],
588
+ [
589
+ '$0 history clean --max-age 30',
590
+ 'Preview removing runs older than 30 days',
591
+ ],
592
+ [
593
+ '$0 history clean --max-size 10485760',
594
+ 'Keep history under 10MB',
595
+ ],
596
+ ]);
597
+ },
598
+ async (argv) => {
599
+ const context = await createCliContext(argv, abortController!);
600
+ const exitCode = await handleCleanCommand(context, {
601
+ confirm: argv.confirm,
602
+ cwd: argv.cwd,
603
+ maxAge: argv['max-age'],
604
+ maxRuns: argv['max-runs'],
605
+ maxSize: argv['max-size'],
606
+ quiet: Boolean(argv.quiet),
607
+ verbose: argv.verbose,
608
+ });
609
+ process.exit(exitCode);
610
+ },
611
+ )
612
+ .command(
613
+ 'export',
614
+ 'Export benchmark history to a file',
615
+ (yargs) => {
616
+ return yargs
617
+ .option('format', {
618
+ choices: ['json', 'csv'] as const,
619
+ default: 'json' as const,
620
+ description: 'Export format',
621
+ type: 'string',
622
+ })
623
+ .option('output', {
624
+ alias: 'o',
625
+ demandOption: true,
626
+ description: 'Output file path',
627
+ type: 'string',
628
+ })
629
+ .option('since', {
630
+ description: 'Export runs since date',
631
+ type: 'string',
632
+ })
633
+ .option('until', {
634
+ description: 'Export runs until date',
635
+ type: 'string',
636
+ })
637
+ .example([
638
+ [
639
+ '$0 history export -o history.json',
640
+ 'Export all history to JSON',
641
+ ],
642
+ [
643
+ '$0 history export -o history.csv --format csv',
644
+ 'Export to CSV',
645
+ ],
646
+ [
647
+ '$0 history export -o recent.json --since "1 week ago"',
648
+ 'Export recent runs',
649
+ ],
650
+ ]);
651
+ },
652
+ async (argv) => {
653
+ const context = await createCliContext(argv, abortController!);
654
+ const exitCode = await handleExportCommand(context, {
655
+ cwd: argv.cwd,
656
+ format: argv.format,
657
+ outputPath: argv.output,
658
+ quiet: Boolean(argv.quiet),
659
+ since: argv.since,
660
+ until: argv.until,
661
+ verbose: argv.verbose,
662
+ });
663
+ process.exit(exitCode);
664
+ },
665
+ )
666
+ .demandCommand(1, 'You must specify a history subcommand')
667
+ .strict()
668
+ .example([
669
+ ['$0 history list', 'List recent benchmark runs'],
670
+ ['$0 history show <run-id>', 'Show detailed results'],
671
+ ['$0 history compare <run-id1> <run-id2>', 'Compare two runs'],
672
+ ['$0 history trends', 'Show performance trends'],
673
+ ['$0 history clean --max-runs 50', 'Keep only latest 50 runs'],
674
+ ['$0 history export -o data.json', 'Export history'],
675
+ ]);
676
+ })
422
677
  .command(
423
678
  'init [type]',
424
679
  'Initialize a new benchmark project',
@@ -6,6 +6,8 @@
6
6
  * architecture.
7
7
  */
8
8
 
9
+ import { randomBytes } from 'node:crypto';
10
+
9
11
  import type {
10
12
  BenchmarkDefinition,
11
13
  BenchmarkEngine,
@@ -697,9 +699,15 @@ export abstract class ModestBenchEngine implements BenchmarkEngine {
697
699
 
698
700
  /**
699
701
  * Generate a unique run ID
702
+ *
703
+ * Uses crypto.randomBytes for cryptographically random 7-character IDs.
704
+ * Format: 7 lowercase alphanumeric characters (e.g., "k3m9x2p")
700
705
  */
701
706
  private generateRunId(): string {
702
- return `run-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
707
+ // Generate random bytes, convert to hex, then to base36, take first 7 chars
708
+ const hex = randomBytes(4).toString('hex');
709
+ const num = parseInt(hex, 16);
710
+ return num.toString(36).padStart(7, '0').substring(0, 7);
703
711
  }
704
712
 
705
713
  /**
@@ -0,0 +1,38 @@
1
+ import { isAbsolute, join, resolve } from 'node:path';
2
+
3
+ /**
4
+ * Resolves the final output path for a reporter
5
+ *
6
+ * @param outputDir - Optional output directory from --output flag
7
+ * @param outputFile - Optional output filename from --output-file flag
8
+ * @param defaultFilename - Default filename to use if none specified
9
+ * @returns Resolved output path, or undefined if no output to file requested
10
+ */
11
+ export const resolveOutputPath = (
12
+ outputDir?: string,
13
+ outputFile?: string,
14
+ defaultFilename?: string,
15
+ ): string | undefined => {
16
+ // If outputFile is provided
17
+ if (outputFile) {
18
+ // If outputFile is absolute, use as-is
19
+ if (isAbsolute(outputFile)) {
20
+ return outputFile;
21
+ }
22
+
23
+ // If outputDir specified, join them
24
+ if (outputDir) {
25
+ return join(outputDir, outputFile);
26
+ }
27
+
28
+ // Otherwise, resolve relative to cwd
29
+ return resolve(process.cwd(), outputFile);
30
+ }
31
+
32
+ // Fall back to default behavior
33
+ if (outputDir && defaultFilename) {
34
+ return join(outputDir, defaultFilename);
35
+ }
36
+
37
+ return undefined;
38
+ };