modestbench 0.9.0 → 0.9.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.
- package/CHANGELOG.md +9 -0
- package/dist/cli/commands/baseline.cjs +8 -8
- package/dist/cli/commands/baseline.cjs.map +1 -1
- package/dist/cli/commands/baseline.js +8 -8
- package/dist/cli/commands/baseline.js.map +1 -1
- package/dist/cli/commands/init.cjs +2 -2
- package/dist/cli/commands/init.cjs.map +1 -1
- package/dist/cli/commands/init.js +2 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/run.cjs +3 -3
- package/dist/cli/commands/run.cjs.map +1 -1
- package/dist/cli/commands/run.d.cts +0 -13
- package/dist/cli/commands/run.d.cts.map +1 -1
- package/dist/cli/commands/run.d.ts +0 -13
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +1 -1
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/test.cjs +1 -1
- package/dist/cli/commands/test.cjs.map +1 -1
- package/dist/cli/commands/test.js +1 -1
- package/dist/cli/commands/test.js.map +1 -1
- package/dist/cli/index.cjs +654 -875
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts +27 -17
- package/dist/cli/index.d.cts.map +1 -1
- package/dist/cli/index.d.ts +27 -17
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +656 -874
- package/dist/cli/index.js.map +1 -1
- package/dist/config/budget-schema.cjs +1 -1
- package/dist/config/budget-schema.cjs.map +1 -1
- package/dist/config/budget-schema.js +1 -1
- package/dist/config/budget-schema.js.map +1 -1
- package/dist/core/engines/accurate-engine.cjs +1 -1
- package/dist/core/engines/accurate-engine.cjs.map +1 -1
- package/dist/core/engines/accurate-engine.d.cts.map +1 -1
- package/dist/core/engines/accurate-engine.d.ts.map +1 -1
- package/dist/core/engines/accurate-engine.js +1 -1
- package/dist/core/engines/accurate-engine.js.map +1 -1
- package/dist/formatters/history/compare.cjs +6 -6
- package/dist/formatters/history/compare.cjs.map +1 -1
- package/dist/formatters/history/compare.js +6 -6
- package/dist/formatters/history/compare.js.map +1 -1
- package/dist/formatters/history/show.cjs +2 -2
- package/dist/formatters/history/show.cjs.map +1 -1
- package/dist/formatters/history/show.js +2 -2
- package/dist/formatters/history/show.js.map +1 -1
- package/dist/formatters/history/trends.cjs +1 -1
- package/dist/formatters/history/trends.cjs.map +1 -1
- package/dist/formatters/history/trends.js +1 -1
- package/dist/formatters/history/trends.js.map +1 -1
- package/dist/reporters/human.cjs +3 -3
- package/dist/reporters/human.cjs.map +1 -1
- package/dist/reporters/human.d.cts.map +1 -1
- package/dist/reporters/human.d.ts.map +1 -1
- package/dist/reporters/human.js +3 -3
- package/dist/reporters/human.js.map +1 -1
- package/dist/reporters/nyan.cjs +1 -1
- package/dist/reporters/nyan.cjs.map +1 -1
- package/dist/reporters/nyan.js +1 -1
- package/dist/reporters/nyan.js.map +1 -1
- package/dist/services/budget-evaluator.cjs +2 -2
- package/dist/services/budget-evaluator.cjs.map +1 -1
- package/dist/services/budget-evaluator.js +2 -2
- package/dist/services/budget-evaluator.js.map +1 -1
- package/dist/services/config-manager.cjs +2 -2
- package/dist/services/config-manager.cjs.map +1 -1
- package/dist/services/config-manager.js +2 -2
- package/dist/services/config-manager.js.map +1 -1
- package/dist/services/reporter-registry.cjs +8 -8
- package/dist/services/reporter-registry.cjs.map +1 -1
- package/dist/services/reporter-registry.js +8 -8
- package/dist/services/reporter-registry.js.map +1 -1
- package/package.json +7 -9
- package/src/cli/commands/baseline.ts +8 -8
- package/src/cli/commands/init.ts +2 -2
- package/src/cli/commands/run.ts +1 -1
- package/src/cli/commands/test.ts +1 -1
- package/src/cli/index.ts +805 -948
- package/src/config/budget-schema.ts +1 -1
- package/src/core/engines/accurate-engine.ts +1 -1
- package/src/formatters/history/compare.ts +6 -6
- package/src/formatters/history/show.ts +2 -2
- package/src/formatters/history/trends.ts +1 -1
- package/src/reporters/human.ts +5 -3
- package/src/reporters/nyan.ts +1 -1
- package/src/services/budget-evaluator.ts +2 -2
- package/src/services/config-manager.ts +2 -2
- package/src/services/reporter-registry.ts +8 -8
package/src/cli/index.ts
CHANGED
|
@@ -3,14 +3,25 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* ModestBench CLI Entry Point
|
|
5
5
|
*
|
|
6
|
-
* Command-line interface using
|
|
6
|
+
* Command-line interface using bargs for command parsing and routing. Provides
|
|
7
7
|
* global options, help generation, and dependency injection setup.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
8
10
|
*/
|
|
9
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';
|
|
10
23
|
import { realpathSync } from 'node:fs';
|
|
11
24
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import yargs from 'yargs';
|
|
13
|
-
import { hideBin } from 'yargs/helpers';
|
|
14
25
|
|
|
15
26
|
import type {
|
|
16
27
|
BenchmarkEngine,
|
|
@@ -24,7 +35,6 @@ import type {
|
|
|
24
35
|
import { bootstrap } from '../bootstrap.js';
|
|
25
36
|
import {
|
|
26
37
|
ABORT_TIMEOUT,
|
|
27
|
-
DEFAULT_BENCHMARK_DIR,
|
|
28
38
|
DEFAULT_ENGINE,
|
|
29
39
|
DEFAULT_REPORTER,
|
|
30
40
|
Engines,
|
|
@@ -63,10 +73,7 @@ import {
|
|
|
63
73
|
handleTrendsCommand,
|
|
64
74
|
} from './commands/history.js';
|
|
65
75
|
import { handleInitCommand as initCommand } from './commands/init.js';
|
|
66
|
-
import {
|
|
67
|
-
RUN_COMMAND_DEFAULTS,
|
|
68
|
-
handleRunCommand as runCommand,
|
|
69
|
-
} from './commands/run.js';
|
|
76
|
+
import { handleRunCommand as runCommand } from './commands/run.js';
|
|
70
77
|
import {
|
|
71
78
|
handleTestCommand as testCommand,
|
|
72
79
|
type TestOptions,
|
|
@@ -80,1026 +87,876 @@ export interface CliContext {
|
|
|
80
87
|
readonly configManager: ConfigurationManager;
|
|
81
88
|
readonly engine: BenchmarkEngine;
|
|
82
89
|
readonly historyStorage: HistoryStorage;
|
|
83
|
-
readonly options:
|
|
90
|
+
readonly options: InferParserValues<typeof globalOptions>;
|
|
84
91
|
readonly progressManager: ProgressManager;
|
|
85
92
|
readonly reporterRegistry: ReporterRegistry;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
interface GlobalOptions {
|
|
92
|
-
/** Configuration file path */
|
|
93
|
-
config?: string | undefined;
|
|
94
|
-
/** Working directory */
|
|
95
|
-
cwd?: string;
|
|
96
|
-
/** JSON output for machine parsing */
|
|
97
|
-
json?: boolean;
|
|
98
|
-
/** Disable colored output */
|
|
99
|
-
noColor?: boolean;
|
|
100
|
-
/** Enable verbose output */
|
|
101
|
-
verbose?: boolean;
|
|
102
|
-
}
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// Global Options Parser
|
|
97
|
+
// ============================================================================
|
|
103
98
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
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
|
+
// ============================================================================
|
|
115
539
|
|
|
116
540
|
/**
|
|
117
|
-
*
|
|
541
|
+
* Additional global options for history and baseline subcommands
|
|
118
542
|
*/
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
const args = argv || hideBin(process.argv);
|
|
543
|
+
const quietOption = opt.options({
|
|
544
|
+
quiet: opt.boolean({
|
|
545
|
+
description: 'Minimal output',
|
|
546
|
+
}),
|
|
547
|
+
});
|
|
125
548
|
|
|
126
|
-
|
|
549
|
+
// ============================================================================
|
|
550
|
+
// Main CLI Builder
|
|
551
|
+
// ============================================================================
|
|
127
552
|
|
|
128
|
-
|
|
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
|
+
};
|
|
129
575
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.wrap(Math.min(120, cli.terminalWidth()))
|
|
179
|
-
.command(
|
|
180
|
-
['$0 [pattern..]', 'run [pattern..]'],
|
|
181
|
-
'Run benchmark files',
|
|
182
|
-
(yargs) =>
|
|
183
|
-
yargs
|
|
184
|
-
.positional('pattern', {
|
|
185
|
-
array: true,
|
|
186
|
-
defaultDescription: `(auto-discovered from ${DEFAULT_BENCHMARK_DIR} directory)`,
|
|
187
|
-
describe:
|
|
188
|
-
'File paths, directory paths, or glob patterns for benchmark files',
|
|
189
|
-
type: 'string',
|
|
190
|
-
})
|
|
191
|
-
.option('config', {
|
|
192
|
-
alias: 'c',
|
|
193
|
-
description: 'Path to configuration file',
|
|
194
|
-
type: 'string',
|
|
195
|
-
})
|
|
196
|
-
.option('reporter', {
|
|
197
|
-
alias: 'r',
|
|
198
|
-
array: true,
|
|
199
|
-
choices: Object.values(Reporters).sort(),
|
|
200
|
-
defaultDescription: DEFAULT_REPORTER,
|
|
201
|
-
description: 'Output reporters to use (human,json,csv)',
|
|
202
|
-
type: 'string',
|
|
203
|
-
})
|
|
204
|
-
.option('output', {
|
|
205
|
-
alias: 'o',
|
|
206
|
-
description: 'Output directory for reports',
|
|
207
|
-
type: 'string',
|
|
208
|
-
})
|
|
209
|
-
.option('output-file', {
|
|
210
|
-
alias: ['of', 'file'],
|
|
211
|
-
description:
|
|
212
|
-
'Custom filename for reporter output (use with single reporter only)',
|
|
213
|
-
requiresArg: true,
|
|
214
|
-
type: 'string',
|
|
215
|
-
})
|
|
216
|
-
.option('iterations', {
|
|
217
|
-
alias: 'i',
|
|
218
|
-
description: 'Number of iterations per benchmark',
|
|
219
|
-
type: 'number',
|
|
220
|
-
})
|
|
221
|
-
.option('time', {
|
|
222
|
-
alias: 't',
|
|
223
|
-
description: 'Time budget per benchmark in milliseconds',
|
|
224
|
-
type: 'number',
|
|
225
|
-
})
|
|
226
|
-
.option('warmup', {
|
|
227
|
-
alias: ['w', 'warm'],
|
|
228
|
-
description: 'Number of warmup iterations',
|
|
229
|
-
type: 'number',
|
|
230
|
-
})
|
|
231
|
-
.option('limit-by', {
|
|
232
|
-
alias: ['l', 'limit'],
|
|
233
|
-
choices: ['time', 'iterations', 'any', 'all'],
|
|
234
|
-
description:
|
|
235
|
-
'How to limit benchmarks: time (time budget), iterations (sample count), any (either threshold), all (both thresholds)',
|
|
236
|
-
type: 'string',
|
|
237
|
-
})
|
|
238
|
-
.option('bail', {
|
|
239
|
-
alias: 'b',
|
|
240
|
-
defaultDescription: String(RUN_COMMAND_DEFAULTS.bail),
|
|
241
|
-
description: 'Stop on first failure',
|
|
242
|
-
type: 'boolean',
|
|
243
|
-
})
|
|
244
|
-
.option('exclude', {
|
|
245
|
-
alias: 'X',
|
|
246
|
-
array: true,
|
|
247
|
-
description: 'Exclude patterns (comma-separated)',
|
|
248
|
-
type: 'string',
|
|
249
|
-
})
|
|
250
|
-
.option('timeout', {
|
|
251
|
-
description: 'Timeout per benchmark in milliseconds',
|
|
252
|
-
type: 'number',
|
|
253
|
-
})
|
|
254
|
-
.option('quiet', {
|
|
255
|
-
alias: 'q',
|
|
256
|
-
defaultDescription: String(RUN_COMMAND_DEFAULTS.quiet),
|
|
257
|
-
description: 'Minimal output',
|
|
258
|
-
type: 'boolean',
|
|
259
|
-
})
|
|
260
|
-
.option('tag', {
|
|
261
|
-
array: true,
|
|
262
|
-
description: 'Include only benchmarks with any of these tags',
|
|
263
|
-
type: 'string',
|
|
264
|
-
})
|
|
265
|
-
.option('exclude-tag', {
|
|
266
|
-
alias: 'T',
|
|
267
|
-
array: true,
|
|
268
|
-
description: 'Exclude benchmarks with any of these tags',
|
|
269
|
-
type: 'string',
|
|
270
|
-
})
|
|
271
|
-
.option('engine', {
|
|
272
|
-
alias: 'e',
|
|
273
|
-
choices: Object.values(Engines),
|
|
274
|
-
defaultDescription: DEFAULT_ENGINE,
|
|
275
|
-
description:
|
|
276
|
-
'Benchmark engine: tinybench (default) or accurate (requires --allow-natives-syntax)',
|
|
277
|
-
type: 'string',
|
|
278
|
-
})
|
|
279
|
-
.option('json-pretty', {
|
|
280
|
-
defaultDescription: 'false',
|
|
281
|
-
description:
|
|
282
|
-
'Pretty-print JSON output (only affects json reporter)',
|
|
283
|
-
type: 'boolean',
|
|
284
|
-
})
|
|
285
|
-
.example([
|
|
286
|
-
['$0 run', 'Run benchmarks in current directory and bench/'],
|
|
287
|
-
['$0 run benchmarks/', 'Run all benchmarks in a directory'],
|
|
288
|
-
['$0 run src/perf/', 'Run benchmarks in specific directory'],
|
|
289
|
-
['$0 run "src/**/*.bench.js"', 'Run specific glob pattern'],
|
|
290
|
-
['$0 run file1.bench.js file2.bench.js', 'Run specific files'],
|
|
291
|
-
['$0 run benchmarks/ tests/perf/', 'Run multiple directories'],
|
|
292
|
-
['$0 run -r json -r csv', 'Use multiple reporters'],
|
|
293
|
-
['$0 run --iterations 1000', 'Set iteration count'],
|
|
294
|
-
['$0 run --engine accurate', 'Use high-accuracy engine'],
|
|
295
|
-
['$0 run --bail', 'Stop on first failure'],
|
|
296
|
-
])
|
|
297
|
-
.check((argv) => {
|
|
298
|
-
if (
|
|
299
|
-
argv.reporter &&
|
|
300
|
-
argv.reporter.length > 1 &&
|
|
301
|
-
argv['output-file']
|
|
302
|
-
) {
|
|
303
|
-
throw new Error(
|
|
304
|
-
'--output-file can only be used with a single reporter. Use --output <dir> for multiple reporters.',
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
return true;
|
|
308
|
-
}),
|
|
309
|
-
async (argv) => {
|
|
310
|
-
const context = await createCliContext(
|
|
311
|
-
argv,
|
|
312
|
-
abortController!,
|
|
313
|
-
argv.engine,
|
|
314
|
-
);
|
|
315
|
-
const exitCode = await runCommand(context, {
|
|
316
|
-
bail: argv.bail,
|
|
317
|
-
config: argv.config,
|
|
318
|
-
cwd: argv.cwd,
|
|
319
|
-
engine: argv.engine,
|
|
320
|
-
exclude: argv.exclude,
|
|
321
|
-
excludeTags: argv['exclude-tag'],
|
|
322
|
-
iterations: argv.iterations,
|
|
323
|
-
json: argv.json,
|
|
324
|
-
jsonPretty: argv['json-pretty'],
|
|
325
|
-
noColor: argv.noColor,
|
|
326
|
-
outputDir: argv.output,
|
|
327
|
-
outputFile: argv['output-file'],
|
|
328
|
-
pattern: argv.pattern,
|
|
329
|
-
progress: argv.progress,
|
|
330
|
-
quiet: argv.quiet,
|
|
331
|
-
reporters: argv.reporter,
|
|
332
|
-
tags: argv.tag,
|
|
333
|
-
time: argv.time,
|
|
334
|
-
timeout: argv.timeout,
|
|
335
|
-
verbose: argv.verbose,
|
|
336
|
-
warmup: argv.warmup,
|
|
337
|
-
});
|
|
338
|
-
process.exit(exitCode);
|
|
339
|
-
},
|
|
340
|
-
)
|
|
341
|
-
.command('history', 'View and manage benchmark history', (yargs) =>
|
|
342
|
-
yargs
|
|
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)
|
|
343
624
|
.command(
|
|
344
625
|
'list',
|
|
345
|
-
|
|
346
|
-
(
|
|
347
|
-
|
|
348
|
-
.option('since', {
|
|
349
|
-
description:
|
|
350
|
-
'Show runs since date (ISO 8601 or relative like "1 week ago")',
|
|
351
|
-
type: 'string',
|
|
352
|
-
})
|
|
353
|
-
.option('until', {
|
|
354
|
-
description:
|
|
355
|
-
'Show runs until date (ISO 8601 or relative like "1 day ago")',
|
|
356
|
-
type: 'string',
|
|
357
|
-
})
|
|
358
|
-
.option('pattern', {
|
|
359
|
-
description: 'Filter by benchmark name pattern',
|
|
360
|
-
type: 'string',
|
|
361
|
-
})
|
|
362
|
-
.option('tag', {
|
|
363
|
-
alias: 't',
|
|
364
|
-
array: true,
|
|
365
|
-
description: 'Filter by tags (comma-separated)',
|
|
366
|
-
type: 'string',
|
|
367
|
-
})
|
|
368
|
-
.option('limit', {
|
|
369
|
-
defaultDescription: '10',
|
|
370
|
-
description: 'Maximum number of results',
|
|
371
|
-
type: 'number',
|
|
372
|
-
})
|
|
373
|
-
.option('format', {
|
|
374
|
-
choices: ['human', 'json', 'csv'] as const,
|
|
375
|
-
defaultDescription: 'human' as const,
|
|
376
|
-
description: 'Output format',
|
|
377
|
-
type: 'string',
|
|
378
|
-
})
|
|
379
|
-
.example([
|
|
380
|
-
['$0 history list', 'List recent benchmark runs'],
|
|
381
|
-
[
|
|
382
|
-
'$0 history list --since "1 week ago"',
|
|
383
|
-
'List runs from last week',
|
|
384
|
-
],
|
|
385
|
-
['$0 history list --limit 20', 'List 20 most recent runs'],
|
|
386
|
-
['$0 history list --format json', 'List runs in JSON format'],
|
|
387
|
-
]),
|
|
388
|
-
async (argv) => {
|
|
389
|
-
const context = await createCliContext(argv, abortController!);
|
|
626
|
+
historyListParser,
|
|
627
|
+
async ({ values }) => {
|
|
628
|
+
const context = await createCliContext(values, abortController);
|
|
390
629
|
const exitCode = await handleListCommand(context, {
|
|
391
|
-
cwd:
|
|
392
|
-
format:
|
|
393
|
-
limit:
|
|
394
|
-
pattern:
|
|
395
|
-
since:
|
|
396
|
-
tags:
|
|
397
|
-
until:
|
|
398
|
-
verbose:
|
|
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,
|
|
399
638
|
});
|
|
400
639
|
process.exit(exitCode);
|
|
401
640
|
},
|
|
641
|
+
'List recent benchmark runs',
|
|
402
642
|
)
|
|
403
643
|
.command(
|
|
404
|
-
'show
|
|
405
|
-
|
|
406
|
-
(
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
demandOption: true,
|
|
410
|
-
describe: 'ID of the benchmark run to show',
|
|
411
|
-
type: 'string',
|
|
412
|
-
})
|
|
413
|
-
.option('format', {
|
|
414
|
-
choices: ['human', 'json', 'csv'] as const,
|
|
415
|
-
defaultDescription: 'human' as const,
|
|
416
|
-
description: 'Output format',
|
|
417
|
-
type: 'string',
|
|
418
|
-
})
|
|
419
|
-
.example([
|
|
420
|
-
[
|
|
421
|
-
'$0 history show abc123',
|
|
422
|
-
'Show detailed results for run abc123',
|
|
423
|
-
],
|
|
424
|
-
[
|
|
425
|
-
'$0 history show abc123 --format json',
|
|
426
|
-
'Show run in JSON format',
|
|
427
|
-
],
|
|
428
|
-
]),
|
|
429
|
-
async (argv) => {
|
|
430
|
-
const context = await createCliContext(argv, abortController!);
|
|
644
|
+
'show',
|
|
645
|
+
historyShowParser,
|
|
646
|
+
async ({ positionals, values }) => {
|
|
647
|
+
const [runId] = positionals;
|
|
648
|
+
const context = await createCliContext(values, abortController);
|
|
431
649
|
const exitCode = await handleShowCommand(context, {
|
|
432
|
-
cwd:
|
|
433
|
-
format:
|
|
434
|
-
runId
|
|
435
|
-
verbose:
|
|
650
|
+
cwd: values.cwd,
|
|
651
|
+
format: values.format,
|
|
652
|
+
runId,
|
|
653
|
+
verbose: values.verbose,
|
|
436
654
|
});
|
|
437
655
|
process.exit(exitCode);
|
|
438
656
|
},
|
|
657
|
+
'Show detailed results for a specific run',
|
|
439
658
|
)
|
|
440
659
|
.command(
|
|
441
|
-
'compare
|
|
442
|
-
|
|
443
|
-
(
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
demandOption: true,
|
|
447
|
-
describe: 'ID of the first benchmark run',
|
|
448
|
-
type: 'string',
|
|
449
|
-
})
|
|
450
|
-
.positional('run-id2', {
|
|
451
|
-
demandOption: true,
|
|
452
|
-
describe: 'ID of the second benchmark run',
|
|
453
|
-
type: 'string',
|
|
454
|
-
})
|
|
455
|
-
.option('format', {
|
|
456
|
-
choices: ['human', 'json'] as const,
|
|
457
|
-
defaultDescription: 'human' as const,
|
|
458
|
-
description: 'Output format',
|
|
459
|
-
type: 'string',
|
|
460
|
-
})
|
|
461
|
-
.example([
|
|
462
|
-
['$0 history compare abc123 def456', 'Compare two runs'],
|
|
463
|
-
[
|
|
464
|
-
'$0 history compare abc123 def456 --format json',
|
|
465
|
-
'Compare in JSON format',
|
|
466
|
-
],
|
|
467
|
-
]),
|
|
468
|
-
async (argv) => {
|
|
469
|
-
const context = await createCliContext(argv, abortController!);
|
|
660
|
+
'compare',
|
|
661
|
+
historyCompareParser,
|
|
662
|
+
async ({ positionals, values }) => {
|
|
663
|
+
const [runId1, runId2] = positionals;
|
|
664
|
+
const context = await createCliContext(values, abortController);
|
|
470
665
|
const exitCode = await handleCompareCommand(context, {
|
|
471
|
-
cwd:
|
|
472
|
-
format:
|
|
473
|
-
runId1
|
|
474
|
-
runId2
|
|
475
|
-
verbose:
|
|
666
|
+
cwd: values.cwd,
|
|
667
|
+
format: values.format,
|
|
668
|
+
runId1,
|
|
669
|
+
runId2,
|
|
670
|
+
verbose: values.verbose,
|
|
476
671
|
});
|
|
477
672
|
process.exit(exitCode);
|
|
478
673
|
},
|
|
674
|
+
'Compare two benchmark runs',
|
|
479
675
|
)
|
|
480
676
|
.command(
|
|
481
|
-
'trends
|
|
482
|
-
|
|
483
|
-
(
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
describe: 'Filter by benchmark name pattern',
|
|
487
|
-
type: 'string',
|
|
488
|
-
})
|
|
489
|
-
.option('since', {
|
|
490
|
-
description:
|
|
491
|
-
'Show trends since date (ISO 8601 or relative like "1 week ago")',
|
|
492
|
-
type: 'string',
|
|
493
|
-
})
|
|
494
|
-
.option('until', {
|
|
495
|
-
description:
|
|
496
|
-
'Show trends until date (ISO 8601 or relative like "1 day ago")',
|
|
497
|
-
type: 'string',
|
|
498
|
-
})
|
|
499
|
-
.option('tag', {
|
|
500
|
-
alias: 't',
|
|
501
|
-
array: true,
|
|
502
|
-
description: 'Filter by tags (comma-separated)',
|
|
503
|
-
type: 'string',
|
|
504
|
-
})
|
|
505
|
-
.option('limit', {
|
|
506
|
-
description: 'Maximum number of runs to analyze',
|
|
507
|
-
type: 'number',
|
|
508
|
-
})
|
|
509
|
-
.option('all', {
|
|
510
|
-
alias: 'a',
|
|
511
|
-
defaultDescription: 'false',
|
|
512
|
-
description: 'Analyze all runs (ignore limit)',
|
|
513
|
-
type: 'boolean',
|
|
514
|
-
})
|
|
515
|
-
.option('format', {
|
|
516
|
-
choices: ['human', 'json'] as const,
|
|
517
|
-
defaultDescription: 'human' as const,
|
|
518
|
-
description: 'Output format',
|
|
519
|
-
type: 'string',
|
|
520
|
-
})
|
|
521
|
-
.example([
|
|
522
|
-
[
|
|
523
|
-
'$0 history trends',
|
|
524
|
-
'Show performance trends for all benchmarks',
|
|
525
|
-
],
|
|
526
|
-
[
|
|
527
|
-
'$0 history trends --since "1 month ago"',
|
|
528
|
-
'Show trends from last month',
|
|
529
|
-
],
|
|
530
|
-
[
|
|
531
|
-
'$0 history trends "array-*"',
|
|
532
|
-
'Show trends for array benchmarks',
|
|
533
|
-
],
|
|
534
|
-
[
|
|
535
|
-
'$0 history trends --format json',
|
|
536
|
-
'Output trends in JSON format',
|
|
537
|
-
],
|
|
538
|
-
]),
|
|
539
|
-
async (argv) => {
|
|
540
|
-
const context = await createCliContext(argv, abortController!);
|
|
677
|
+
'trends',
|
|
678
|
+
historyTrendsParser,
|
|
679
|
+
async ({ positionals, values }) => {
|
|
680
|
+
const [pattern] = positionals;
|
|
681
|
+
const context = await createCliContext(values, abortController);
|
|
541
682
|
const exitCode = await handleTrendsCommand(context, {
|
|
542
|
-
all:
|
|
543
|
-
cwd:
|
|
544
|
-
format:
|
|
545
|
-
limit:
|
|
546
|
-
pattern
|
|
547
|
-
since:
|
|
548
|
-
tags:
|
|
549
|
-
until:
|
|
550
|
-
verbose:
|
|
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,
|
|
551
692
|
});
|
|
552
693
|
process.exit(exitCode);
|
|
553
694
|
},
|
|
695
|
+
'Show performance trends over time',
|
|
554
696
|
)
|
|
555
697
|
.command(
|
|
556
698
|
'clean',
|
|
557
|
-
|
|
558
|
-
(
|
|
559
|
-
|
|
560
|
-
.option('max-age', {
|
|
561
|
-
description: 'Remove runs older than this many days',
|
|
562
|
-
type: 'number',
|
|
563
|
-
})
|
|
564
|
-
.option('max-runs', {
|
|
565
|
-
description: 'Keep only this many most recent runs',
|
|
566
|
-
type: 'number',
|
|
567
|
-
})
|
|
568
|
-
.option('max-size', {
|
|
569
|
-
description: 'Keep history under this size in bytes',
|
|
570
|
-
type: 'number',
|
|
571
|
-
})
|
|
572
|
-
.option('yes', {
|
|
573
|
-
alias: 'y',
|
|
574
|
-
description: 'Confirm cleanup without prompting',
|
|
575
|
-
type: 'boolean',
|
|
576
|
-
})
|
|
577
|
-
.option('quiet', {
|
|
578
|
-
default: false,
|
|
579
|
-
description: 'Minimal output',
|
|
580
|
-
type: 'boolean',
|
|
581
|
-
})
|
|
582
|
-
.check((argv) => {
|
|
583
|
-
if (
|
|
584
|
-
!argv['max-age'] &&
|
|
585
|
-
!argv['max-runs'] &&
|
|
586
|
-
!argv['max-size']
|
|
587
|
-
) {
|
|
588
|
-
throw new Error(
|
|
589
|
-
'At least one cleanup criterion must be specified (--max-age, --max-runs, or --max-size)',
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
return true;
|
|
593
|
-
})
|
|
594
|
-
.example([
|
|
595
|
-
[
|
|
596
|
-
'$0 history clean --max-runs 50 --yes',
|
|
597
|
-
'Keep only latest 50 runs',
|
|
598
|
-
],
|
|
599
|
-
[
|
|
600
|
-
'$0 history clean --max-age 30',
|
|
601
|
-
'Preview removing runs older than 30 days',
|
|
602
|
-
],
|
|
603
|
-
[
|
|
604
|
-
'$0 history clean --max-size 10485760',
|
|
605
|
-
'Keep history under 10MB',
|
|
606
|
-
],
|
|
607
|
-
]),
|
|
608
|
-
async (argv) => {
|
|
609
|
-
const context = await createCliContext(argv, abortController!);
|
|
699
|
+
historyCleanParser,
|
|
700
|
+
async ({ values }) => {
|
|
701
|
+
const context = await createCliContext(values, abortController);
|
|
610
702
|
const exitCode = await handleCleanCommand(context, {
|
|
611
|
-
confirm:
|
|
612
|
-
cwd:
|
|
613
|
-
maxAge:
|
|
614
|
-
maxRuns:
|
|
615
|
-
maxSize:
|
|
616
|
-
quiet:
|
|
617
|
-
verbose:
|
|
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,
|
|
618
710
|
});
|
|
619
711
|
process.exit(exitCode);
|
|
620
712
|
},
|
|
713
|
+
'Clean up old benchmark history',
|
|
621
714
|
)
|
|
622
715
|
.command(
|
|
623
716
|
'export',
|
|
624
|
-
|
|
625
|
-
(
|
|
626
|
-
|
|
627
|
-
.option('format', {
|
|
628
|
-
choices: ['json', 'csv'] as const,
|
|
629
|
-
defaultDescription: 'json' as const,
|
|
630
|
-
description: 'Export format',
|
|
631
|
-
type: 'string',
|
|
632
|
-
})
|
|
633
|
-
.option('output', {
|
|
634
|
-
alias: 'o',
|
|
635
|
-
demandOption: true,
|
|
636
|
-
description: 'Output file path',
|
|
637
|
-
type: 'string',
|
|
638
|
-
})
|
|
639
|
-
.option('since', {
|
|
640
|
-
description: 'Export runs since date',
|
|
641
|
-
type: 'string',
|
|
642
|
-
})
|
|
643
|
-
.option('until', {
|
|
644
|
-
description: 'Export runs until date',
|
|
645
|
-
type: 'string',
|
|
646
|
-
})
|
|
647
|
-
.example([
|
|
648
|
-
[
|
|
649
|
-
'$0 history export -o history.json',
|
|
650
|
-
'Export all history to JSON',
|
|
651
|
-
],
|
|
652
|
-
[
|
|
653
|
-
'$0 history export -o history.csv --format csv',
|
|
654
|
-
'Export to CSV',
|
|
655
|
-
],
|
|
656
|
-
[
|
|
657
|
-
'$0 history export -o recent.json --since "1 week ago"',
|
|
658
|
-
'Export recent runs',
|
|
659
|
-
],
|
|
660
|
-
]),
|
|
661
|
-
async (argv) => {
|
|
662
|
-
const context = await createCliContext(argv, abortController!);
|
|
717
|
+
historyExportParser,
|
|
718
|
+
async ({ values }) => {
|
|
719
|
+
const context = await createCliContext(values, abortController);
|
|
663
720
|
const exitCode = await handleExportCommand(context, {
|
|
664
|
-
cwd:
|
|
665
|
-
format:
|
|
666
|
-
outputPath:
|
|
667
|
-
quiet: Boolean(
|
|
668
|
-
since:
|
|
669
|
-
until:
|
|
670
|
-
verbose:
|
|
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,
|
|
671
728
|
});
|
|
672
729
|
process.exitCode = exitCode;
|
|
673
730
|
},
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
['$0 history export -o data.json', 'Export history'],
|
|
684
|
-
]),
|
|
685
|
-
)
|
|
686
|
-
.command('baseline', 'Manage performance baselines', (yargs) => {
|
|
687
|
-
return yargs
|
|
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)
|
|
688
740
|
.command(
|
|
689
|
-
'set
|
|
690
|
-
|
|
691
|
-
(
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
describe: 'Name for the baseline',
|
|
695
|
-
type: 'string',
|
|
696
|
-
})
|
|
697
|
-
.option('run-id', {
|
|
698
|
-
description: 'Specific run ID to save (default: most recent)',
|
|
699
|
-
type: 'string',
|
|
700
|
-
})
|
|
701
|
-
.option('commit', {
|
|
702
|
-
description: 'Git commit SHA (40 characters)',
|
|
703
|
-
type: 'string',
|
|
704
|
-
})
|
|
705
|
-
.option('branch', {
|
|
706
|
-
description: 'Git branch name',
|
|
707
|
-
type: 'string',
|
|
708
|
-
})
|
|
709
|
-
.option('default', {
|
|
710
|
-
defaultDescription: 'false',
|
|
711
|
-
description: 'Set as default baseline',
|
|
712
|
-
type: 'boolean',
|
|
713
|
-
})
|
|
714
|
-
.example([
|
|
715
|
-
[
|
|
716
|
-
'$0 baseline set production-v1.0',
|
|
717
|
-
'Save most recent run as baseline',
|
|
718
|
-
],
|
|
719
|
-
['$0 baseline set v1.0 --default', 'Save and set as default'],
|
|
720
|
-
[
|
|
721
|
-
'$0 baseline set v1.0 --commit abc123...',
|
|
722
|
-
'Save with commit info',
|
|
723
|
-
],
|
|
724
|
-
]);
|
|
725
|
-
},
|
|
726
|
-
async (argv) => {
|
|
727
|
-
const context = await createCliContext(argv, abortController!);
|
|
741
|
+
'set',
|
|
742
|
+
baselineSetParser,
|
|
743
|
+
async ({ positionals, values }) => {
|
|
744
|
+
const [name] = positionals;
|
|
745
|
+
const context = await createCliContext(values, abortController);
|
|
728
746
|
const exitCode = await handleBaselineSetCommand(context, {
|
|
729
|
-
branch:
|
|
730
|
-
commit:
|
|
731
|
-
cwd:
|
|
732
|
-
default:
|
|
733
|
-
name
|
|
734
|
-
quiet: Boolean(
|
|
735
|
-
runId:
|
|
736
|
-
verbose:
|
|
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,
|
|
737
755
|
});
|
|
738
756
|
process.exit(exitCode);
|
|
739
757
|
},
|
|
758
|
+
'Save a benchmark run as a baseline',
|
|
740
759
|
)
|
|
741
760
|
.command(
|
|
742
761
|
'list',
|
|
743
|
-
|
|
744
|
-
(
|
|
745
|
-
|
|
746
|
-
.option('format', {
|
|
747
|
-
choices: ['human', 'json'] as const,
|
|
748
|
-
defaultDescription: 'human' as const,
|
|
749
|
-
description: 'Output format',
|
|
750
|
-
type: 'string',
|
|
751
|
-
})
|
|
752
|
-
.example([
|
|
753
|
-
['$0 baseline list', 'List all baselines'],
|
|
754
|
-
['$0 baseline list --format json', 'List in JSON format'],
|
|
755
|
-
]);
|
|
756
|
-
},
|
|
757
|
-
async (argv) => {
|
|
758
|
-
const context = await createCliContext(argv, abortController!);
|
|
762
|
+
baselineListParser,
|
|
763
|
+
async ({ values }) => {
|
|
764
|
+
const context = await createCliContext(values, abortController);
|
|
759
765
|
const exitCode = await handleBaselineListCommand(context, {
|
|
760
|
-
cwd:
|
|
761
|
-
format:
|
|
762
|
-
quiet: Boolean(
|
|
763
|
-
verbose:
|
|
766
|
+
cwd: values.cwd,
|
|
767
|
+
format: values.format,
|
|
768
|
+
quiet: Boolean(values.quiet),
|
|
769
|
+
verbose: values.verbose,
|
|
764
770
|
});
|
|
765
771
|
process.exit(exitCode);
|
|
766
772
|
},
|
|
773
|
+
'List all saved baselines',
|
|
767
774
|
)
|
|
768
775
|
.command(
|
|
769
|
-
'show
|
|
770
|
-
|
|
771
|
-
(
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
describe: 'Baseline name to show',
|
|
775
|
-
type: 'string',
|
|
776
|
-
})
|
|
777
|
-
.option('format', {
|
|
778
|
-
choices: ['human', 'json'] as const,
|
|
779
|
-
defaultDescription: 'human' as const,
|
|
780
|
-
description: 'Output format',
|
|
781
|
-
type: 'string',
|
|
782
|
-
})
|
|
783
|
-
.example([
|
|
784
|
-
['$0 baseline show production-v1.0', 'Show baseline details'],
|
|
785
|
-
[
|
|
786
|
-
'$0 baseline show v1.0 --format json',
|
|
787
|
-
'Show in JSON format',
|
|
788
|
-
],
|
|
789
|
-
]);
|
|
790
|
-
},
|
|
791
|
-
async (argv) => {
|
|
792
|
-
const context = await createCliContext(argv, abortController!);
|
|
776
|
+
'show',
|
|
777
|
+
baselineShowParser,
|
|
778
|
+
async ({ positionals, values }) => {
|
|
779
|
+
const [name] = positionals;
|
|
780
|
+
const context = await createCliContext(values, abortController);
|
|
793
781
|
const exitCode = await handleBaselineShowCommand(context, {
|
|
794
|
-
cwd:
|
|
795
|
-
format:
|
|
796
|
-
name
|
|
797
|
-
quiet: Boolean(
|
|
798
|
-
verbose:
|
|
782
|
+
cwd: values.cwd,
|
|
783
|
+
format: values.format,
|
|
784
|
+
name,
|
|
785
|
+
quiet: Boolean(values.quiet),
|
|
786
|
+
verbose: values.verbose,
|
|
799
787
|
});
|
|
800
788
|
process.exit(exitCode);
|
|
801
789
|
},
|
|
790
|
+
'Show baseline details',
|
|
802
791
|
)
|
|
803
792
|
.command(
|
|
804
|
-
'delete
|
|
805
|
-
|
|
806
|
-
(
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
describe: 'Baseline name to delete',
|
|
810
|
-
type: 'string',
|
|
811
|
-
})
|
|
812
|
-
.example([
|
|
813
|
-
['$0 baseline delete old-baseline', 'Delete a baseline'],
|
|
814
|
-
]);
|
|
815
|
-
},
|
|
816
|
-
async (argv) => {
|
|
817
|
-
const context = await createCliContext(argv, abortController!);
|
|
793
|
+
'delete',
|
|
794
|
+
baselineDeleteParser,
|
|
795
|
+
async ({ positionals, values }) => {
|
|
796
|
+
const [name] = positionals;
|
|
797
|
+
const context = await createCliContext(values, abortController);
|
|
818
798
|
const exitCode = await handleBaselineDeleteCommand(context, {
|
|
819
|
-
cwd:
|
|
820
|
-
name
|
|
821
|
-
quiet: Boolean(
|
|
822
|
-
verbose:
|
|
799
|
+
cwd: values.cwd,
|
|
800
|
+
name,
|
|
801
|
+
quiet: Boolean(values.quiet),
|
|
802
|
+
verbose: values.verbose,
|
|
823
803
|
});
|
|
824
804
|
process.exit(exitCode);
|
|
825
805
|
},
|
|
806
|
+
'Delete a baseline',
|
|
826
807
|
)
|
|
827
808
|
.command(
|
|
828
809
|
'analyze',
|
|
829
|
-
|
|
830
|
-
(
|
|
831
|
-
|
|
832
|
-
.option('runs', {
|
|
833
|
-
defaultDescription: '10',
|
|
834
|
-
description: 'Number of recent runs to analyze',
|
|
835
|
-
type: 'number',
|
|
836
|
-
})
|
|
837
|
-
.option('confidence', {
|
|
838
|
-
defaultDescription: '0.95',
|
|
839
|
-
description: 'Confidence level (0.5-0.999, default 0.95)',
|
|
840
|
-
type: 'number',
|
|
841
|
-
})
|
|
842
|
-
.example([
|
|
843
|
-
[
|
|
844
|
-
'$0 baseline analyze',
|
|
845
|
-
'Analyze last 10 runs with 95% confidence',
|
|
846
|
-
],
|
|
847
|
-
['$0 baseline analyze --runs 20', 'Analyze last 20 runs'],
|
|
848
|
-
[
|
|
849
|
-
'$0 baseline analyze --confidence 0.90',
|
|
850
|
-
'Use 90% confidence level',
|
|
851
|
-
],
|
|
852
|
-
]);
|
|
853
|
-
},
|
|
854
|
-
async (argv) => {
|
|
855
|
-
const context = await createCliContext(argv, abortController!);
|
|
810
|
+
baselineAnalyzeParser,
|
|
811
|
+
async ({ values }) => {
|
|
812
|
+
const context = await createCliContext(values, abortController);
|
|
856
813
|
const exitCode = await handleBaselineAnalyzeCommand(context, {
|
|
857
|
-
confidence:
|
|
858
|
-
cwd:
|
|
859
|
-
quiet: Boolean(
|
|
860
|
-
runs:
|
|
861
|
-
verbose:
|
|
814
|
+
confidence: values.confidence,
|
|
815
|
+
cwd: values.cwd,
|
|
816
|
+
quiet: Boolean(values.quiet),
|
|
817
|
+
runs: values.runs,
|
|
818
|
+
verbose: values.verbose,
|
|
862
819
|
});
|
|
863
820
|
process.exit(exitCode);
|
|
864
821
|
},
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
type: 'string',
|
|
897
|
-
})
|
|
898
|
-
.option('force', {
|
|
899
|
-
defaultDescription: 'false',
|
|
900
|
-
description: 'Overwrite existing files',
|
|
901
|
-
type: 'boolean',
|
|
902
|
-
})
|
|
903
|
-
.option('yes', {
|
|
904
|
-
alias: 'y',
|
|
905
|
-
defaultDescription: 'false',
|
|
906
|
-
description: 'Accept all prompts automatically',
|
|
907
|
-
type: 'boolean',
|
|
908
|
-
})
|
|
909
|
-
.option('quiet', {
|
|
910
|
-
alias: 'q',
|
|
911
|
-
defaultDescription: 'false',
|
|
912
|
-
description: 'Minimal output',
|
|
913
|
-
type: 'boolean',
|
|
914
|
-
})
|
|
915
|
-
.example([
|
|
916
|
-
['$0 init', 'Initialize a basic project'],
|
|
917
|
-
[
|
|
918
|
-
'$0 init advanced --config-type ts',
|
|
919
|
-
'Initialize advanced project with TypeScript config',
|
|
920
|
-
],
|
|
921
|
-
[
|
|
922
|
-
'$0 init library --no-examples',
|
|
923
|
-
'Initialize library project without examples',
|
|
924
|
-
],
|
|
925
|
-
]);
|
|
926
|
-
},
|
|
927
|
-
async (argv) => {
|
|
928
|
-
const context = await createCliContext(argv, abortController!);
|
|
929
|
-
const exitCode = await initCommand(context, {
|
|
930
|
-
configType: argv['config-type'],
|
|
931
|
-
cwd: argv.cwd,
|
|
932
|
-
examples: argv.examples,
|
|
933
|
-
force: argv.force,
|
|
934
|
-
quiet: argv.quiet,
|
|
935
|
-
type: argv.type,
|
|
936
|
-
verbose: argv.verbose,
|
|
937
|
-
yes: argv.yes,
|
|
938
|
-
});
|
|
939
|
-
process.exitCode = exitCode;
|
|
940
|
-
},
|
|
941
|
-
)
|
|
942
|
-
.command(
|
|
943
|
-
['analyze [command]', 'profile [command]'],
|
|
944
|
-
'Analyze code execution and identify benchmark candidates',
|
|
945
|
-
(yargs) => {
|
|
946
|
-
return yargs
|
|
947
|
-
.positional('command', {
|
|
948
|
-
description: 'Command to analyze (e.g., "npm test")',
|
|
949
|
-
type: 'string',
|
|
950
|
-
})
|
|
951
|
-
.option('input', {
|
|
952
|
-
alias: 'i',
|
|
953
|
-
description: 'Path to existing *.cpuprofile file',
|
|
954
|
-
type: 'string',
|
|
955
|
-
})
|
|
956
|
-
.option('filter-file', {
|
|
957
|
-
description: 'Filter functions by file glob pattern',
|
|
958
|
-
type: 'string',
|
|
959
|
-
})
|
|
960
|
-
.option('min-percent', {
|
|
961
|
-
alias: ['m', 'min'],
|
|
962
|
-
default: 0.5,
|
|
963
|
-
description: 'Minimum execution percentage to show',
|
|
964
|
-
type: 'number',
|
|
965
|
-
})
|
|
966
|
-
.option('top', {
|
|
967
|
-
alias: 'n',
|
|
968
|
-
default: 25,
|
|
969
|
-
description: 'Number of top functions to show',
|
|
970
|
-
type: 'number',
|
|
971
|
-
})
|
|
972
|
-
.option('group-by-file', {
|
|
973
|
-
default: false,
|
|
974
|
-
description: 'Group results by file',
|
|
975
|
-
type: 'boolean',
|
|
976
|
-
})
|
|
977
|
-
.check((argv) => {
|
|
978
|
-
if (!argv.command && !argv.input) {
|
|
979
|
-
throw new Error('Either [command] or --input must be provided');
|
|
980
|
-
}
|
|
981
|
-
return true;
|
|
982
|
-
});
|
|
983
|
-
},
|
|
984
|
-
async (argv) => {
|
|
985
|
-
// Context not needed for analyze command currently
|
|
986
|
-
const context = {} as CliContext;
|
|
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;
|
|
987
853
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
+
};
|
|
998
897
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
type: 'number',
|
|
1024
|
-
})
|
|
1025
|
-
.option('warmup', {
|
|
1026
|
-
alias: 'w',
|
|
1027
|
-
default: 5,
|
|
1028
|
-
description: 'Number of warmup iterations',
|
|
1029
|
-
type: 'number',
|
|
1030
|
-
})
|
|
1031
|
-
.option('bail', {
|
|
1032
|
-
alias: 'b',
|
|
1033
|
-
default: false,
|
|
1034
|
-
description: 'Stop on first failure',
|
|
1035
|
-
type: 'boolean',
|
|
1036
|
-
})
|
|
1037
|
-
.option('quiet', {
|
|
1038
|
-
alias: 'q',
|
|
1039
|
-
default: false,
|
|
1040
|
-
description: 'Minimal output',
|
|
1041
|
-
type: 'boolean',
|
|
1042
|
-
})
|
|
1043
|
-
.example([
|
|
1044
|
-
['$0 test mocha test/*.spec.js', 'Run Mocha tests as benchmarks'],
|
|
1045
|
-
[
|
|
1046
|
-
'$0 test node-test test/*.test.js',
|
|
1047
|
-
'Run node:test tests as benchmarks',
|
|
1048
|
-
],
|
|
1049
|
-
[
|
|
1050
|
-
'$0 test ava test/*.js --iterations 500',
|
|
1051
|
-
'Run AVA tests with 500 iterations',
|
|
1052
|
-
],
|
|
1053
|
-
[
|
|
1054
|
-
'$0 test mocha test/unit.spec.js --json',
|
|
1055
|
-
'Output results as JSON',
|
|
1056
|
-
],
|
|
1057
|
-
]);
|
|
1058
|
-
},
|
|
1059
|
-
async (argv) => {
|
|
1060
|
-
const context = await createCliContext(argv, abortController!);
|
|
1061
|
-
const options: TestOptions = {
|
|
1062
|
-
bail: argv.bail,
|
|
1063
|
-
cwd: argv.cwd,
|
|
1064
|
-
framework: argv.framework as TestOptions['framework'],
|
|
1065
|
-
iterations: argv.iterations,
|
|
1066
|
-
json: argv.json,
|
|
1067
|
-
noColor: argv.noColor,
|
|
1068
|
-
pattern: argv.files,
|
|
1069
|
-
quiet: argv.quiet,
|
|
1070
|
-
verbose: argv.verbose,
|
|
1071
|
-
warmup: argv.warmup,
|
|
1072
|
-
};
|
|
1073
|
-
const exitCode = await testCommand(context, options);
|
|
1074
|
-
process.exit(exitCode);
|
|
1075
|
-
},
|
|
1076
|
-
)
|
|
1077
|
-
.fail((msg, err, yargs) => {
|
|
1078
|
-
if (err) {
|
|
1079
|
-
console.error('Error:', err.message);
|
|
1080
|
-
if (process.env.DEBUG) {
|
|
1081
|
-
console.error(err.stack);
|
|
1082
|
-
}
|
|
1083
|
-
// Show help for file discovery errors (similar to usage errors)
|
|
1084
|
-
if (
|
|
1085
|
-
isModestBenchError(err) &&
|
|
1086
|
-
err.code === ErrorCodes.FILE_DISCOVERY_FAILED
|
|
1087
|
-
) {
|
|
1088
|
-
console.error();
|
|
1089
|
-
yargs.showHelp();
|
|
1090
|
-
process.exit(ExitCodes.DISCOVERY_ERROR);
|
|
1091
|
-
}
|
|
1092
|
-
process.exit(ExitCodes.RUNTIME_ERROR);
|
|
1093
|
-
} else {
|
|
1094
|
-
// Show help for usage errors (unknown arguments, etc.)
|
|
1095
|
-
console.error(msg);
|
|
1096
|
-
console.error();
|
|
1097
|
-
yargs.showHelp();
|
|
1098
|
-
process.exit(ExitCodes.CONFIG_ERROR);
|
|
1099
|
-
}
|
|
1100
|
-
})
|
|
1101
|
-
.parse();
|
|
898
|
+
/**
|
|
899
|
+
* Initialize and run the CLI
|
|
900
|
+
*/
|
|
901
|
+
export const cli = (argv?: string[]): void => {
|
|
902
|
+
const abortController = new AbortController();
|
|
903
|
+
setupSignalHandlers(abortController);
|
|
904
|
+
main(argv, abortController).catch((error) => {
|
|
905
|
+
console.error('CLI error:', error);
|
|
906
|
+
process.exit(ExitCodes.UNKNOWN_ERROR);
|
|
907
|
+
});
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Main CLI entry point
|
|
912
|
+
*/
|
|
913
|
+
export const main = async (
|
|
914
|
+
argv?: string[],
|
|
915
|
+
abortController?: AbortController,
|
|
916
|
+
): Promise<void> => {
|
|
917
|
+
const controller = abortController ?? new AbortController();
|
|
918
|
+
|
|
919
|
+
try {
|
|
920
|
+
const cliBuilder = createCli(controller);
|
|
921
|
+
await cliBuilder.parseAsync(argv);
|
|
1102
922
|
} catch (error) {
|
|
923
|
+
// Handle bargs errors
|
|
924
|
+
if (error instanceof HelpError) {
|
|
925
|
+
// Help was requested or invalid args - message already printed
|
|
926
|
+
process.exit(ExitCodes.CONFIG_ERROR);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (error instanceof BargsError) {
|
|
930
|
+
console.error('Error:', error.message);
|
|
931
|
+
process.exit(ExitCodes.CONFIG_ERROR);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Handle bargs validation errors (thrown as plain Error, not BargsError)
|
|
935
|
+
if (
|
|
936
|
+
error instanceof Error &&
|
|
937
|
+
(error.message.startsWith('Invalid value for --') ||
|
|
938
|
+
error.message.startsWith('Missing required'))
|
|
939
|
+
) {
|
|
940
|
+
console.error('Error:', error.message);
|
|
941
|
+
process.exit(ExitCodes.CONFIG_ERROR);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Handle ModestBench errors
|
|
945
|
+
if (isModestBenchError(error)) {
|
|
946
|
+
console.error('Error:', error.message);
|
|
947
|
+
if (process.env.DEBUG) {
|
|
948
|
+
console.error(error.stack);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Show help for file discovery errors
|
|
952
|
+
if (error.code === ErrorCodes.FILE_DISCOVERY_FAILED) {
|
|
953
|
+
process.exit(ExitCodes.DISCOVERY_ERROR);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
process.exit(ExitCodes.RUNTIME_ERROR);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Unexpected error
|
|
1103
960
|
console.error(
|
|
1104
961
|
'Unexpected error:',
|
|
1105
962
|
error instanceof Error ? error.message : String(error),
|
|
@@ -1115,7 +972,7 @@ export const main = async (
|
|
|
1115
972
|
* Create CLI context with dependency injection
|
|
1116
973
|
*/
|
|
1117
974
|
const createCliContext = async (
|
|
1118
|
-
options:
|
|
975
|
+
options: InferParserValues<typeof globalOptions>,
|
|
1119
976
|
abortController: AbortController,
|
|
1120
977
|
engineType: Engine = DEFAULT_ENGINE,
|
|
1121
978
|
): Promise<CliContext> => {
|
|
@@ -1132,7 +989,7 @@ const createCliContext = async (
|
|
|
1132
989
|
engine.registerReporter(
|
|
1133
990
|
Reporters.HUMAN,
|
|
1134
991
|
new HumanReporter({
|
|
1135
|
-
color: !options
|
|
992
|
+
color: !options['no-color'],
|
|
1136
993
|
verbose: options.verbose,
|
|
1137
994
|
}),
|
|
1138
995
|
);
|
|
@@ -1162,7 +1019,7 @@ const createCliContext = async (
|
|
|
1162
1019
|
engine.registerReporter(
|
|
1163
1020
|
'nyan',
|
|
1164
1021
|
new NyanReporter({
|
|
1165
|
-
color: !options
|
|
1022
|
+
color: !options['no-color'],
|
|
1166
1023
|
}),
|
|
1167
1024
|
);
|
|
1168
1025
|
|