modestbench 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE.md +55 -0
  3. package/README.md +699 -0
  4. package/dist/bootstrap.cjs +37 -0
  5. package/dist/bootstrap.cjs.map +1 -0
  6. package/dist/bootstrap.d.cts +17 -0
  7. package/dist/bootstrap.d.cts.map +1 -0
  8. package/dist/bootstrap.d.ts +17 -0
  9. package/dist/bootstrap.d.ts.map +1 -0
  10. package/dist/bootstrap.js +33 -0
  11. package/dist/bootstrap.js.map +1 -0
  12. package/dist/cli/commands/history.cjs +459 -0
  13. package/dist/cli/commands/history.cjs.map +1 -0
  14. package/dist/cli/commands/history.d.cts +34 -0
  15. package/dist/cli/commands/history.d.cts.map +1 -0
  16. package/dist/cli/commands/history.d.ts +34 -0
  17. package/dist/cli/commands/history.d.ts.map +1 -0
  18. package/dist/cli/commands/history.js +422 -0
  19. package/dist/cli/commands/history.js.map +1 -0
  20. package/dist/cli/commands/init.cjs +566 -0
  21. package/dist/cli/commands/init.cjs.map +1 -0
  22. package/dist/cli/commands/init.d.cts +26 -0
  23. package/dist/cli/commands/init.d.cts.map +1 -0
  24. package/dist/cli/commands/init.d.ts +26 -0
  25. package/dist/cli/commands/init.d.ts.map +1 -0
  26. package/dist/cli/commands/init.js +562 -0
  27. package/dist/cli/commands/init.js.map +1 -0
  28. package/dist/cli/commands/run.cjs +285 -0
  29. package/dist/cli/commands/run.cjs.map +1 -0
  30. package/dist/cli/commands/run.d.cts +37 -0
  31. package/dist/cli/commands/run.d.cts.map +1 -0
  32. package/dist/cli/commands/run.d.ts +37 -0
  33. package/dist/cli/commands/run.d.ts.map +1 -0
  34. package/dist/cli/commands/run.js +248 -0
  35. package/dist/cli/commands/run.js.map +1 -0
  36. package/dist/cli/index.cjs +523 -0
  37. package/dist/cli/index.cjs.map +1 -0
  38. package/dist/cli/index.d.cts +58 -0
  39. package/dist/cli/index.d.cts.map +1 -0
  40. package/dist/cli/index.d.ts +58 -0
  41. package/dist/cli/index.d.ts.map +1 -0
  42. package/dist/cli/index.js +515 -0
  43. package/dist/cli/index.js.map +1 -0
  44. package/dist/config/manager.cjs +370 -0
  45. package/dist/config/manager.cjs.map +1 -0
  46. package/dist/config/manager.d.cts +46 -0
  47. package/dist/config/manager.d.cts.map +1 -0
  48. package/dist/config/manager.d.ts +46 -0
  49. package/dist/config/manager.d.ts.map +1 -0
  50. package/dist/config/manager.js +333 -0
  51. package/dist/config/manager.js.map +1 -0
  52. package/dist/config/schema.cjs +182 -0
  53. package/dist/config/schema.cjs.map +1 -0
  54. package/dist/config/schema.d.cts +51 -0
  55. package/dist/config/schema.d.cts.map +1 -0
  56. package/dist/config/schema.d.ts +51 -0
  57. package/dist/config/schema.d.ts.map +1 -0
  58. package/dist/config/schema.js +145 -0
  59. package/dist/config/schema.js.map +1 -0
  60. package/dist/constants.cjs +22 -0
  61. package/dist/constants.cjs.map +1 -0
  62. package/dist/constants.d.cts +10 -0
  63. package/dist/constants.d.cts.map +1 -0
  64. package/dist/constants.d.ts +10 -0
  65. package/dist/constants.d.ts.map +1 -0
  66. package/dist/constants.js +19 -0
  67. package/dist/constants.js.map +1 -0
  68. package/dist/core/benchmark-schema.cjs +135 -0
  69. package/dist/core/benchmark-schema.cjs.map +1 -0
  70. package/dist/core/benchmark-schema.d.cts +139 -0
  71. package/dist/core/benchmark-schema.d.cts.map +1 -0
  72. package/dist/core/benchmark-schema.d.ts +139 -0
  73. package/dist/core/benchmark-schema.d.ts.map +1 -0
  74. package/dist/core/benchmark-schema.js +132 -0
  75. package/dist/core/benchmark-schema.js.map +1 -0
  76. package/dist/core/engine.cjs +669 -0
  77. package/dist/core/engine.cjs.map +1 -0
  78. package/dist/core/engine.d.cts +128 -0
  79. package/dist/core/engine.d.cts.map +1 -0
  80. package/dist/core/engine.d.ts +128 -0
  81. package/dist/core/engine.d.ts.map +1 -0
  82. package/dist/core/engine.js +632 -0
  83. package/dist/core/engine.js.map +1 -0
  84. package/dist/core/engines/accurate-engine.cjs +292 -0
  85. package/dist/core/engines/accurate-engine.cjs.map +1 -0
  86. package/dist/core/engines/accurate-engine.d.cts +63 -0
  87. package/dist/core/engines/accurate-engine.d.cts.map +1 -0
  88. package/dist/core/engines/accurate-engine.d.ts +63 -0
  89. package/dist/core/engines/accurate-engine.d.ts.map +1 -0
  90. package/dist/core/engines/accurate-engine.js +288 -0
  91. package/dist/core/engines/accurate-engine.js.map +1 -0
  92. package/dist/core/engines/index.cjs +21 -0
  93. package/dist/core/engines/index.cjs.map +1 -0
  94. package/dist/core/engines/index.d.cts +16 -0
  95. package/dist/core/engines/index.d.cts.map +1 -0
  96. package/dist/core/engines/index.d.ts +16 -0
  97. package/dist/core/engines/index.d.ts.map +1 -0
  98. package/dist/core/engines/index.js +16 -0
  99. package/dist/core/engines/index.js.map +1 -0
  100. package/dist/core/engines/tinybench-engine.cjs +286 -0
  101. package/dist/core/engines/tinybench-engine.cjs.map +1 -0
  102. package/dist/core/engines/tinybench-engine.d.cts +18 -0
  103. package/dist/core/engines/tinybench-engine.d.cts.map +1 -0
  104. package/dist/core/engines/tinybench-engine.d.ts +18 -0
  105. package/dist/core/engines/tinybench-engine.d.ts.map +1 -0
  106. package/dist/core/engines/tinybench-engine.js +282 -0
  107. package/dist/core/engines/tinybench-engine.js.map +1 -0
  108. package/dist/core/error-manager.cjs +303 -0
  109. package/dist/core/error-manager.cjs.map +1 -0
  110. package/dist/core/error-manager.d.cts +77 -0
  111. package/dist/core/error-manager.d.cts.map +1 -0
  112. package/dist/core/error-manager.d.ts +77 -0
  113. package/dist/core/error-manager.d.ts.map +1 -0
  114. package/dist/core/error-manager.js +299 -0
  115. package/dist/core/error-manager.js.map +1 -0
  116. package/dist/core/loader.cjs +287 -0
  117. package/dist/core/loader.cjs.map +1 -0
  118. package/dist/core/loader.d.cts +55 -0
  119. package/dist/core/loader.d.cts.map +1 -0
  120. package/dist/core/loader.d.ts +55 -0
  121. package/dist/core/loader.d.ts.map +1 -0
  122. package/dist/core/loader.js +250 -0
  123. package/dist/core/loader.js.map +1 -0
  124. package/dist/core/stats-utils.cjs +99 -0
  125. package/dist/core/stats-utils.cjs.map +1 -0
  126. package/dist/core/stats-utils.d.cts +50 -0
  127. package/dist/core/stats-utils.d.cts.map +1 -0
  128. package/dist/core/stats-utils.d.ts +50 -0
  129. package/dist/core/stats-utils.d.ts.map +1 -0
  130. package/dist/core/stats-utils.js +94 -0
  131. package/dist/core/stats-utils.js.map +1 -0
  132. package/dist/index.cjs +64 -0
  133. package/dist/index.cjs.map +1 -0
  134. package/dist/index.d.cts +22 -0
  135. package/dist/index.d.cts.map +1 -0
  136. package/dist/index.d.ts +22 -0
  137. package/dist/index.d.ts.map +1 -0
  138. package/dist/index.js +30 -0
  139. package/dist/index.js.map +1 -0
  140. package/dist/progress/manager.cjs +325 -0
  141. package/dist/progress/manager.cjs.map +1 -0
  142. package/dist/progress/manager.d.cts +125 -0
  143. package/dist/progress/manager.d.cts.map +1 -0
  144. package/dist/progress/manager.d.ts +125 -0
  145. package/dist/progress/manager.d.ts.map +1 -0
  146. package/dist/progress/manager.js +321 -0
  147. package/dist/progress/manager.js.map +1 -0
  148. package/dist/reporters/csv.cjs +250 -0
  149. package/dist/reporters/csv.cjs.map +1 -0
  150. package/dist/reporters/csv.d.cts +92 -0
  151. package/dist/reporters/csv.d.cts.map +1 -0
  152. package/dist/reporters/csv.d.ts +92 -0
  153. package/dist/reporters/csv.d.ts.map +1 -0
  154. package/dist/reporters/csv.js +246 -0
  155. package/dist/reporters/csv.js.map +1 -0
  156. package/dist/reporters/human.cjs +516 -0
  157. package/dist/reporters/human.cjs.map +1 -0
  158. package/dist/reporters/human.d.cts +86 -0
  159. package/dist/reporters/human.d.cts.map +1 -0
  160. package/dist/reporters/human.d.ts +86 -0
  161. package/dist/reporters/human.d.ts.map +1 -0
  162. package/dist/reporters/human.js +509 -0
  163. package/dist/reporters/human.js.map +1 -0
  164. package/dist/reporters/index.cjs +17 -0
  165. package/dist/reporters/index.cjs.map +1 -0
  166. package/dist/reporters/index.d.cts +10 -0
  167. package/dist/reporters/index.d.cts.map +1 -0
  168. package/dist/reporters/index.d.ts +10 -0
  169. package/dist/reporters/index.d.ts.map +1 -0
  170. package/dist/reporters/index.js +10 -0
  171. package/dist/reporters/index.js.map +1 -0
  172. package/dist/reporters/json.cjs +215 -0
  173. package/dist/reporters/json.cjs.map +1 -0
  174. package/dist/reporters/json.d.cts +79 -0
  175. package/dist/reporters/json.d.cts.map +1 -0
  176. package/dist/reporters/json.d.ts +79 -0
  177. package/dist/reporters/json.d.ts.map +1 -0
  178. package/dist/reporters/json.js +211 -0
  179. package/dist/reporters/json.js.map +1 -0
  180. package/dist/reporters/registry.cjs +255 -0
  181. package/dist/reporters/registry.cjs.map +1 -0
  182. package/dist/reporters/registry.d.cts +155 -0
  183. package/dist/reporters/registry.d.cts.map +1 -0
  184. package/dist/reporters/registry.d.ts +155 -0
  185. package/dist/reporters/registry.d.ts.map +1 -0
  186. package/dist/reporters/registry.js +249 -0
  187. package/dist/reporters/registry.js.map +1 -0
  188. package/dist/reporters/simple.cjs +328 -0
  189. package/dist/reporters/simple.cjs.map +1 -0
  190. package/dist/reporters/simple.d.cts +51 -0
  191. package/dist/reporters/simple.d.cts.map +1 -0
  192. package/dist/reporters/simple.d.ts +51 -0
  193. package/dist/reporters/simple.d.ts.map +1 -0
  194. package/dist/reporters/simple.js +321 -0
  195. package/dist/reporters/simple.js.map +1 -0
  196. package/dist/schema/modestbench-config.schema.json +162 -0
  197. package/dist/storage/history.cjs +456 -0
  198. package/dist/storage/history.cjs.map +1 -0
  199. package/dist/storage/history.d.cts +99 -0
  200. package/dist/storage/history.d.cts.map +1 -0
  201. package/dist/storage/history.d.ts +99 -0
  202. package/dist/storage/history.d.ts.map +1 -0
  203. package/dist/storage/history.js +452 -0
  204. package/dist/storage/history.js.map +1 -0
  205. package/dist/types/cli.cjs +21 -0
  206. package/dist/types/cli.cjs.map +1 -0
  207. package/dist/types/cli.d.cts +296 -0
  208. package/dist/types/cli.d.cts.map +1 -0
  209. package/dist/types/cli.d.ts +296 -0
  210. package/dist/types/cli.d.ts.map +1 -0
  211. package/dist/types/cli.js +18 -0
  212. package/dist/types/cli.js.map +1 -0
  213. package/dist/types/core.cjs +14 -0
  214. package/dist/types/core.cjs.map +1 -0
  215. package/dist/types/core.d.cts +380 -0
  216. package/dist/types/core.d.cts.map +1 -0
  217. package/dist/types/core.d.ts +380 -0
  218. package/dist/types/core.d.ts.map +1 -0
  219. package/dist/types/core.js +13 -0
  220. package/dist/types/core.js.map +1 -0
  221. package/dist/types/index.cjs +27 -0
  222. package/dist/types/index.cjs.map +1 -0
  223. package/dist/types/index.d.cts +11 -0
  224. package/dist/types/index.d.cts.map +1 -0
  225. package/dist/types/index.d.ts +11 -0
  226. package/dist/types/index.d.ts.map +1 -0
  227. package/dist/types/index.js +11 -0
  228. package/dist/types/index.js.map +1 -0
  229. package/dist/types/interfaces.cjs +10 -0
  230. package/dist/types/interfaces.cjs.map +1 -0
  231. package/dist/types/interfaces.d.cts +381 -0
  232. package/dist/types/interfaces.d.cts.map +1 -0
  233. package/dist/types/interfaces.d.ts +381 -0
  234. package/dist/types/interfaces.d.ts.map +1 -0
  235. package/dist/types/interfaces.js +9 -0
  236. package/dist/types/interfaces.js.map +1 -0
  237. package/dist/types/utility.cjs +92 -0
  238. package/dist/types/utility.cjs.map +1 -0
  239. package/dist/types/utility.d.cts +330 -0
  240. package/dist/types/utility.d.cts.map +1 -0
  241. package/dist/types/utility.d.ts +330 -0
  242. package/dist/types/utility.d.ts.map +1 -0
  243. package/dist/types/utility.js +78 -0
  244. package/dist/types/utility.js.map +1 -0
  245. package/package.json +211 -0
  246. package/src/bootstrap.ts +35 -0
  247. package/src/cli/commands/history.ts +569 -0
  248. package/src/cli/commands/init.ts +658 -0
  249. package/src/cli/commands/run.ts +346 -0
  250. package/src/cli/index.ts +642 -0
  251. package/src/config/manager.ts +387 -0
  252. package/src/config/schema.ts +188 -0
  253. package/src/constants.ts +21 -0
  254. package/src/core/benchmark-schema.ts +185 -0
  255. package/src/core/engine.ts +888 -0
  256. package/src/core/engines/accurate-engine.ts +408 -0
  257. package/src/core/engines/index.ts +16 -0
  258. package/src/core/engines/tinybench-engine.ts +335 -0
  259. package/src/core/error-manager.ts +372 -0
  260. package/src/core/loader.ts +324 -0
  261. package/src/core/stats-utils.ts +135 -0
  262. package/src/index.ts +46 -0
  263. package/src/progress/manager.ts +415 -0
  264. package/src/reporters/csv.ts +368 -0
  265. package/src/reporters/human.ts +707 -0
  266. package/src/reporters/index.ts +10 -0
  267. package/src/reporters/json.ts +302 -0
  268. package/src/reporters/registry.ts +349 -0
  269. package/src/reporters/simple.ts +459 -0
  270. package/src/storage/history.ts +600 -0
  271. package/src/types/cli.ts +312 -0
  272. package/src/types/core.ts +414 -0
  273. package/src/types/index.ts +18 -0
  274. package/src/types/interfaces.ts +451 -0
  275. package/src/types/utility.ts +446 -0
@@ -0,0 +1,569 @@
1
+ /**
2
+ * ModestBench History Command
3
+ *
4
+ * View and manage benchmark run history with subcommands for listing, showing,
5
+ * comparing, and cleaning historical data.
6
+ */
7
+
8
+ import type { HistoryQuery, RetentionPolicy } from '../../types/index.js';
9
+ import type { CliContext } from '../index.js';
10
+
11
+ /**
12
+ * History command options interface
13
+ */
14
+ interface HistoryOptions {
15
+ args?: string[] | undefined; // Additional arguments after subcommand
16
+ confirm?: boolean | undefined;
17
+ cwd: string;
18
+ format?: 'csv' | 'human' | 'json' | undefined;
19
+ limit?: number | undefined;
20
+ maxAge?: number | undefined;
21
+ maxRuns?: number | undefined;
22
+ maxSize?: number | undefined;
23
+ outputDir?: string | undefined;
24
+ pattern?: string | undefined;
25
+ quiet?: boolean | undefined;
26
+ since?: string | undefined;
27
+ subcommand: 'clean' | 'compare' | 'export' | 'list' | 'show' | 'trends';
28
+ tags?: string[] | undefined;
29
+ until?: string | undefined;
30
+ verbose?: boolean | undefined;
31
+ }
32
+
33
+ /**
34
+ * Handle history command
35
+ */
36
+ export const handleHistoryCommand = async (
37
+ context: CliContext,
38
+ options: HistoryOptions,
39
+ ): Promise<number> => {
40
+ try {
41
+ // Get the subcommand
42
+ const subcommand = options.subcommand;
43
+
44
+ switch (subcommand) {
45
+ case 'clean':
46
+ return await handleCleanCommand(context, options);
47
+ case 'compare':
48
+ return await handleCompareCommand(context, options);
49
+ case 'export':
50
+ return await handleExportCommand(context, options);
51
+ case 'list':
52
+ return await handleListCommand(context, options);
53
+ case 'show':
54
+ return await handleShowCommand(context, options);
55
+ case 'trends':
56
+ return await handleTrendsCommand(context, options);
57
+ default:
58
+ console.error(`Unknown history subcommand: ${subcommand || '(none)'}`);
59
+ console.error(
60
+ 'Available subcommands: list, show, compare, trends, clean, export',
61
+ );
62
+ return 2; // Config error
63
+ }
64
+ } catch (error) {
65
+ console.error(
66
+ 'History command failed:',
67
+ error instanceof Error ? error.message : String(error),
68
+ );
69
+ return 2; // Configuration/runtime errors
70
+ }
71
+ };
72
+
73
+ /**
74
+ * Format bytes in human-readable format
75
+ */
76
+ const formatBytes = (bytes: number): string => {
77
+ const units = ['B', 'KB', 'MB', 'GB'];
78
+ let size = bytes;
79
+ let unitIndex = 0;
80
+
81
+ while (size >= 1024 && unitIndex < units.length - 1) {
82
+ size /= 1024;
83
+ unitIndex++;
84
+ }
85
+
86
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
87
+ };
88
+
89
+ /**
90
+ * Handle the clean subcommand
91
+ */
92
+ const handleCleanCommand = async (
93
+ context: CliContext,
94
+ options: HistoryOptions,
95
+ ): Promise<number> => {
96
+ try {
97
+ // Build retention policy from arguments
98
+ const policy = {
99
+ ...(options.maxAge && { maxAge: options.maxAge }),
100
+ ...(options.maxRuns && { maxRuns: options.maxRuns }),
101
+ ...(options.maxSize && { maxSize: options.maxSize }),
102
+ } as Partial<RetentionPolicy>;
103
+
104
+ if (Object.keys(policy).length === 0) {
105
+ console.error(
106
+ 'At least one cleanup criterion must be specified (--max-age, --max-runs, or --max-size)',
107
+ );
108
+ return 2;
109
+ }
110
+
111
+ // Show what would be cleaned unless confirmed
112
+ if (!options.confirm) {
113
+ console.log(
114
+ 'This will clean up historical data based on the following policy:',
115
+ );
116
+ if (policy.maxAge) {
117
+ console.log(` - Remove runs older than ${policy.maxAge}ms`);
118
+ }
119
+ if (policy.maxRuns) {
120
+ console.log(` - Keep only the latest ${policy.maxRuns} runs`);
121
+ }
122
+ if (policy.maxSize) {
123
+ console.log(
124
+ ` - Remove runs until storage is under ${policy.maxSize} bytes`,
125
+ );
126
+ }
127
+ console.log();
128
+ console.log('Use --confirm to proceed with cleanup.');
129
+ return 0;
130
+ }
131
+
132
+ // Perform cleanup
133
+ const result = await context.historyStorage.cleanup(policy);
134
+
135
+ if (!options.quiet) {
136
+ console.log(`Cleanup completed:`);
137
+ console.log(` Removed ${result.removedRuns} run(s)`);
138
+ console.log(` Freed ${formatBytes(result.freedBytes)} of storage`);
139
+
140
+ if (options.verbose && result.removedFiles.length > 0) {
141
+ console.log(` Removed files:`);
142
+ for (const file of result.removedFiles) {
143
+ console.log(` ${file}`);
144
+ }
145
+ }
146
+ }
147
+
148
+ return 0;
149
+ } catch (error) {
150
+ console.error(
151
+ 'Failed to clean history:',
152
+ error instanceof Error ? error.message : String(error),
153
+ );
154
+ return 5;
155
+ }
156
+ };
157
+
158
+ /**
159
+ * Handle the compare subcommand
160
+ */
161
+ const handleCompareCommand = async (
162
+ context: CliContext,
163
+ options: HistoryOptions,
164
+ ): Promise<number> => {
165
+ try {
166
+ // For compare command, IDs come from args after the subcommand
167
+ const id1 = options.args?.[0];
168
+ const id2 = options.args?.[1];
169
+
170
+ if (!id1 || !id2) {
171
+ console.error('Two run IDs are required for compare command');
172
+ console.error('Usage: modestbench history compare <run-id1> <run-id2>');
173
+ return 2;
174
+ }
175
+
176
+ const [run1, run2] = await Promise.all([
177
+ context.historyStorage.loadRun(id1),
178
+ context.historyStorage.loadRun(id2),
179
+ ]);
180
+
181
+ if (!run1) {
182
+ console.error(`Run not found: ${id1}`);
183
+ return 1;
184
+ }
185
+
186
+ if (!run2) {
187
+ console.error(`Run not found: ${id2}`);
188
+ return 1;
189
+ }
190
+
191
+ if (options.format === 'json') {
192
+ const comparison = {
193
+ run1: { id: run1.id, summary: run1.summary },
194
+ run2: { id: run2.id, summary: run2.summary },
195
+ // TODO: Add detailed comparison logic
196
+ };
197
+ console.log(JSON.stringify(comparison, null, 2));
198
+ } else {
199
+ // Human format comparison
200
+ console.log(`Comparing runs:`);
201
+ console.log(` Run 1: ${run1.id} (${run1.startTime.toLocaleString()})`);
202
+ console.log(` Run 2: ${run2.id} (${run2.startTime.toLocaleString()})`);
203
+ console.log();
204
+
205
+ console.log('Summary comparison:');
206
+ console.log(
207
+ ` Files: ${run1.summary.totalFiles} vs ${run2.summary.totalFiles}`,
208
+ );
209
+ console.log(
210
+ ` Tasks: ${run1.summary.totalTasks} vs ${run2.summary.totalTasks}`,
211
+ );
212
+ console.log(
213
+ ` Passed: ${run1.summary.passedTasks} vs ${run2.summary.passedTasks}`,
214
+ );
215
+ console.log(
216
+ ` Failed: ${run1.summary.failedTasks} vs ${run2.summary.failedTasks}`,
217
+ );
218
+
219
+ // TODO: Add detailed performance comparison
220
+ console.log();
221
+ console.log('Note: Detailed performance comparison not yet implemented.');
222
+ }
223
+
224
+ return 0;
225
+ } catch (error) {
226
+ console.error(
227
+ 'Failed to compare runs:',
228
+ error instanceof Error ? error.message : String(error),
229
+ );
230
+ return 5;
231
+ }
232
+ };
233
+
234
+ /**
235
+ * Handle the export subcommand
236
+ */
237
+ const handleExportCommand = async (
238
+ context: CliContext,
239
+ options: HistoryOptions,
240
+ ): Promise<number> => {
241
+ try {
242
+ const format = options.format || 'json';
243
+
244
+ // Build query for export
245
+ const query = {
246
+ ...(options.since && { since: parseDate(options.since) }),
247
+ ...(options.until && { until: parseDate(options.until) }),
248
+ } as Partial<HistoryQuery>;
249
+
250
+ const exportData = await context.historyStorage.export(
251
+ format as 'csv' | 'json',
252
+ query,
253
+ );
254
+
255
+ if (options.outputDir) {
256
+ // Write to file
257
+ const fs = await import('node:fs/promises');
258
+ await fs.writeFile(options.outputDir, exportData, 'utf8');
259
+ if (!options.quiet) {
260
+ console.log(`Exported history to ${options.outputDir}`);
261
+ }
262
+ } else {
263
+ // Write to stdout
264
+ console.log(exportData);
265
+ }
266
+
267
+ return 0;
268
+ } catch (error) {
269
+ console.error(
270
+ 'Failed to export history:',
271
+ error instanceof Error ? error.message : String(error),
272
+ );
273
+ return 5;
274
+ }
275
+ };
276
+
277
+ /**
278
+ * Handle the list subcommand
279
+ */
280
+ const handleListCommand = async (
281
+ context: CliContext,
282
+ options: HistoryOptions,
283
+ ): Promise<number> => {
284
+ try {
285
+ // Build query from command line arguments
286
+ let parsedSince: Date | undefined;
287
+ let parsedUntil: Date | undefined;
288
+
289
+ if (options.since) {
290
+ try {
291
+ parsedSince = parseDate(options.since);
292
+ } catch (error) {
293
+ console.error(
294
+ 'Invalid since date:',
295
+ error instanceof Error ? error.message : String(error),
296
+ );
297
+ return 2; // Invalid date format
298
+ }
299
+ }
300
+
301
+ if (options.until) {
302
+ try {
303
+ parsedUntil = parseDate(options.until);
304
+ } catch (error) {
305
+ console.error(
306
+ 'Invalid until date:',
307
+ error instanceof Error ? error.message : String(error),
308
+ );
309
+ return 2; // Invalid date format
310
+ }
311
+ }
312
+
313
+ const query = {
314
+ ...(parsedSince && { since: parsedSince }),
315
+ ...(parsedUntil && { until: parsedUntil }),
316
+ ...(options.pattern && { pattern: options.pattern }),
317
+ ...(options.tags && options.tags.length > 0 && { tags: options.tags }),
318
+ ...(options.limit && { limit: options.limit }),
319
+ } as Partial<HistoryQuery>;
320
+
321
+ // Query historical runs
322
+ const runs = await context.historyStorage.queryRuns(query);
323
+
324
+ // Display results based on format
325
+ if (options.format === 'json') {
326
+ if (runs.length === 0) {
327
+ console.log('[]'); // Empty JSON array for no data
328
+ } else {
329
+ console.log(
330
+ JSON.stringify(
331
+ runs.map((run) => ({
332
+ duration: run.duration,
333
+ failed: run.summary.failedTasks,
334
+ files: run.summary.totalFiles,
335
+ id: run.id,
336
+ passed: run.summary.passedTasks,
337
+ startTime: run.startTime,
338
+ tasks: run.summary.totalTasks,
339
+ })),
340
+ null,
341
+ 2,
342
+ ),
343
+ );
344
+ }
345
+ } else if (options.format === 'csv') {
346
+ console.log('id,startTime,duration,files,tasks,passed,failed');
347
+ if (runs.length > 0) {
348
+ for (const run of runs) {
349
+ console.log(
350
+ `${run.id},${run.startTime.toISOString()},${run.duration},${run.summary.totalFiles},${run.summary.totalTasks},${run.summary.passedTasks},${run.summary.failedTasks}`,
351
+ );
352
+ }
353
+ }
354
+ // For CSV, no additional message needed - header is sufficient
355
+ } else {
356
+ // Human format
357
+ if (runs.length === 0) {
358
+ if (!options.quiet) {
359
+ console.log('No historical data found matching criteria.');
360
+ }
361
+ } else {
362
+ console.log('Recent benchmark runs:');
363
+ console.log();
364
+
365
+ for (const run of runs) {
366
+ const dateStr = run.startTime.toLocaleString();
367
+ const durationStr = `${(run.duration / 1000).toFixed(1)}s`;
368
+ const statusStr =
369
+ run.summary.failedTasks > 0
370
+ ? `${run.summary.passedTasks} passed, ${run.summary.failedTasks} failed`
371
+ : `${run.summary.passedTasks} passed`;
372
+
373
+ console.log(
374
+ ` ${run.id.substring(0, 8)} - ${dateStr} (${durationStr})`,
375
+ );
376
+ console.log(
377
+ ` ${run.summary.totalFiles} files, ${run.summary.totalTasks} tasks: ${statusStr}`,
378
+ );
379
+ console.log();
380
+ }
381
+ }
382
+ }
383
+
384
+ return 0;
385
+ } catch (error) {
386
+ console.error(
387
+ 'Failed to list history:',
388
+ error instanceof Error ? error.message : String(error),
389
+ );
390
+ return 5;
391
+ }
392
+ };
393
+
394
+ /**
395
+ * Handle the show subcommand
396
+ */
397
+ const handleShowCommand = async (
398
+ context: CliContext,
399
+ options: HistoryOptions,
400
+ ): Promise<number> => {
401
+ try {
402
+ // For show command, ID comes from the args after the subcommand
403
+ const runId = options.args?.[0];
404
+
405
+ if (!runId) {
406
+ console.error('Run ID is required for show command');
407
+ console.error('Usage: modestbench history show <run-id>');
408
+ return 2;
409
+ }
410
+
411
+ const run = await context.historyStorage.loadRun(runId);
412
+
413
+ if (!run) {
414
+ console.error(`Run not found: ${runId}`);
415
+ return 1;
416
+ }
417
+
418
+ if (options.format === 'json') {
419
+ console.log(JSON.stringify(run, null, 2));
420
+ } else {
421
+ // Human format
422
+ console.log(`Benchmark Run: ${run.id}`);
423
+ console.log(`Date: ${run.startTime.toLocaleString()}`);
424
+ console.log(`Duration: ${(run.duration / 1000).toFixed(1)}s`);
425
+ console.log(
426
+ `Environment: Node.js ${run.environment.nodeVersion} on ${run.environment.platform}`,
427
+ );
428
+
429
+ if (run.git) {
430
+ console.log(`Git: ${run.git.branch}@${run.git.commit.substring(0, 8)}`);
431
+ }
432
+
433
+ console.log();
434
+ console.log('Summary:');
435
+ console.log(` Files: ${run.summary.totalFiles}`);
436
+ console.log(` Suites: ${run.summary.totalSuites}`);
437
+ console.log(` Tasks: ${run.summary.totalTasks}`);
438
+ console.log(` Passed: ${run.summary.passedTasks}`);
439
+ console.log(` Failed: ${run.summary.failedTasks}`);
440
+
441
+ // TODO: Show detailed file/suite/task results
442
+ console.log();
443
+ console.log('Detailed results:');
444
+ for (const file of run.files) {
445
+ console.log(` 📁 ${file.filePath}`);
446
+ for (const suite of file.suites) {
447
+ console.log(` 📊 ${suite.name}`);
448
+ for (const task of suite.tasks) {
449
+ const status = task.error ? '❌' : '✅';
450
+ const timing = task.error
451
+ ? 'failed'
452
+ : `${(task.mean / 1000000).toFixed(2)}ms`;
453
+ console.log(` ${status} ${task.name} - ${timing}`);
454
+ }
455
+ }
456
+ }
457
+ }
458
+
459
+ return 0;
460
+ } catch (error) {
461
+ console.error(
462
+ 'Failed to show run:',
463
+ error instanceof Error ? error.message : String(error),
464
+ );
465
+ return 5;
466
+ }
467
+ };
468
+
469
+ /**
470
+ * Handle the trends subcommand
471
+ */
472
+ const handleTrendsCommand = async (
473
+ context: CliContext,
474
+ options: HistoryOptions,
475
+ ): Promise<number> => {
476
+ try {
477
+ // Build query from command line arguments (same as list command)
478
+ let parsedSince: Date | undefined;
479
+ let parsedUntil: Date | undefined;
480
+
481
+ if (options.since) {
482
+ try {
483
+ parsedSince = parseDate(options.since);
484
+ } catch (error) {
485
+ console.error(
486
+ 'Invalid since date:',
487
+ error instanceof Error ? error.message : String(error),
488
+ );
489
+ return 2; // Invalid date format
490
+ }
491
+ }
492
+
493
+ if (options.until) {
494
+ try {
495
+ parsedUntil = parseDate(options.until);
496
+ } catch (error) {
497
+ console.error(
498
+ 'Invalid until date:',
499
+ error instanceof Error ? error.message : String(error),
500
+ );
501
+ return 2; // Invalid date format
502
+ }
503
+ }
504
+
505
+ // Get pattern from args or explicit pattern option
506
+ const pattern = options.args?.[0] || options.pattern;
507
+
508
+ const query = {
509
+ ...(parsedSince && { since: parsedSince }),
510
+ ...(parsedUntil && { until: parsedUntil }),
511
+ ...(pattern && { pattern }),
512
+ ...(options.tags && options.tags.length > 0 && { tags: options.tags }),
513
+ ...(options.limit && { limit: options.limit }),
514
+ } as Partial<HistoryQuery>;
515
+
516
+ // Query historical runs
517
+ const runs = await context.historyStorage.queryRuns(query);
518
+
519
+ if (runs.length === 0) {
520
+ if (!options.quiet) {
521
+ console.log('No historical data found matching criteria');
522
+ }
523
+ return 0; // Success - no data is not an error
524
+ }
525
+
526
+ if (options.format === 'json') {
527
+ // TODO: Generate trends data in JSON format
528
+ const trendsData = {
529
+ runs: runs.length,
530
+ timespan: {
531
+ end: runs[0]?.startTime,
532
+ start: runs[runs.length - 1]?.startTime,
533
+ },
534
+ // TODO: Add actual trend calculations
535
+ };
536
+ console.log(JSON.stringify(trendsData, null, 2));
537
+ } else {
538
+ // Human format trends
539
+ if (!options.quiet) {
540
+ console.log(`Performance trends for ${runs.length} runs:`);
541
+ console.log(
542
+ `Time range: ${runs[runs.length - 1]?.startTime} to ${runs[0]?.startTime}`,
543
+ );
544
+ // TODO: Add trend analysis and visualization
545
+ console.log('(Trend analysis not yet implemented)');
546
+ }
547
+ }
548
+
549
+ return 0; // Success
550
+ } catch (error) {
551
+ console.error('Error showing trends:', error);
552
+ return 3; // Runtime error
553
+ }
554
+ };
555
+
556
+ /**
557
+ * Parse date string (ISO 8601 or relative)
558
+ */
559
+ const parseDate = (dateStr: string): Date => {
560
+ // Try parsing as ISO 8601 first
561
+ const isoDate = new Date(dateStr);
562
+ if (!isNaN(isoDate.getTime())) {
563
+ return isoDate;
564
+ }
565
+
566
+ // TODO: Parse relative dates like "1 week ago", "3 days ago", etc.
567
+ // For now, throw error for invalid dates
568
+ throw new Error(`Invalid date format: "${dateStr}"`);
569
+ };