@artemiskit/cli 0.1.8 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +139 -0
- package/bin/artemis.ts +0 -0
- package/dist/index.js +72343 -34002
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/commands/baseline.d.ts +9 -0
- package/dist/src/commands/baseline.d.ts.map +1 -0
- package/dist/src/commands/compare.d.ts.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/redteam.d.ts.map +1 -1
- package/dist/src/commands/run.d.ts.map +1 -1
- package/dist/src/commands/stress.d.ts.map +1 -1
- package/dist/src/config/loader.d.ts +3 -1
- package/dist/src/config/loader.d.ts.map +1 -1
- package/dist/src/config/schema.d.ts +16 -0
- package/dist/src/config/schema.d.ts.map +1 -1
- package/dist/src/ui/index.d.ts +3 -1
- package/dist/src/ui/index.d.ts.map +1 -1
- package/dist/src/ui/panels.d.ts +21 -0
- package/dist/src/ui/panels.d.ts.map +1 -1
- package/dist/src/ui/prompts.d.ts +92 -0
- package/dist/src/ui/prompts.d.ts.map +1 -0
- package/dist/src/utils/adapter.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/cli.ts +2 -0
- package/src/commands/baseline.ts +473 -0
- package/src/commands/compare.ts +25 -0
- package/src/commands/init.ts +173 -69
- package/src/commands/redteam.ts +63 -10
- package/src/commands/run.ts +863 -141
- package/src/commands/stress.ts +76 -3
- package/src/config/loader.ts +5 -2
- package/src/config/schema.ts +4 -0
- package/src/ui/index.ts +19 -0
- package/src/ui/panels.ts +153 -5
- package/src/ui/prompts.ts +749 -0
- package/src/utils/adapter.ts +15 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline command - Manage baseline runs for regression detection
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { BaselineMetadata, BaselineStorageAdapter } from '@artemiskit/core';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { loadConfig } from '../config/loader.js';
|
|
9
|
+
import { createSpinner, icons, isTTY, padText, renderError } from '../ui/index.js';
|
|
10
|
+
import { createStorage } from '../utils/storage.js';
|
|
11
|
+
|
|
12
|
+
interface BaselineSetOptions {
|
|
13
|
+
scenario?: string;
|
|
14
|
+
tag?: string;
|
|
15
|
+
config?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface BaselineListOptions {
|
|
19
|
+
config?: string;
|
|
20
|
+
json?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface BaselineRemoveOptions {
|
|
24
|
+
config?: string;
|
|
25
|
+
force?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if storage adapter supports baselines
|
|
30
|
+
*/
|
|
31
|
+
function isBaselineStorage(storage: unknown): storage is BaselineStorageAdapter {
|
|
32
|
+
return (
|
|
33
|
+
typeof storage === 'object' &&
|
|
34
|
+
storage !== null &&
|
|
35
|
+
'setBaseline' in storage &&
|
|
36
|
+
'getBaseline' in storage &&
|
|
37
|
+
'listBaselines' in storage &&
|
|
38
|
+
'removeBaseline' in storage
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Render baselines table for TTY
|
|
44
|
+
*/
|
|
45
|
+
function renderBaselinesTable(baselines: BaselineMetadata[]): string {
|
|
46
|
+
const scenarioWidth = 30;
|
|
47
|
+
const runIdWidth = 16;
|
|
48
|
+
const rateWidth = 12;
|
|
49
|
+
const dateWidth = 20;
|
|
50
|
+
const tagWidth = 12;
|
|
51
|
+
|
|
52
|
+
const width =
|
|
53
|
+
2 + scenarioWidth + 1 + runIdWidth + 1 + rateWidth + 1 + dateWidth + 1 + tagWidth + 2;
|
|
54
|
+
const border = '═'.repeat(width - 2);
|
|
55
|
+
|
|
56
|
+
const formatHeaderRow = () => {
|
|
57
|
+
const scenarioPad = padText('Scenario', scenarioWidth);
|
|
58
|
+
const runIdPad = padText('Run ID', runIdWidth);
|
|
59
|
+
const ratePad = padText('Success Rate', rateWidth, 'right');
|
|
60
|
+
const datePad = padText('Created', dateWidth, 'right');
|
|
61
|
+
const tagPad = padText('Tag', tagWidth, 'right');
|
|
62
|
+
return `║ ${scenarioPad} ${runIdPad} ${ratePad} ${datePad} ${tagPad} ║`;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const lines = [
|
|
66
|
+
`╔${border}╗`,
|
|
67
|
+
`║${padText('BASELINES', width - 2, 'center')}║`,
|
|
68
|
+
`╠${border}╣`,
|
|
69
|
+
formatHeaderRow(),
|
|
70
|
+
`╟${'─'.repeat(width - 2)}╢`,
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
for (const baseline of baselines) {
|
|
74
|
+
const rateColor =
|
|
75
|
+
baseline.metrics.successRate >= 0.9
|
|
76
|
+
? chalk.green
|
|
77
|
+
: baseline.metrics.successRate >= 0.7
|
|
78
|
+
? chalk.yellow
|
|
79
|
+
: chalk.red;
|
|
80
|
+
|
|
81
|
+
const truncScenario =
|
|
82
|
+
baseline.scenario.length > scenarioWidth - 2
|
|
83
|
+
? `${baseline.scenario.slice(0, scenarioWidth - 3)}…`
|
|
84
|
+
: baseline.scenario;
|
|
85
|
+
const scenarioPad = padText(truncScenario, scenarioWidth);
|
|
86
|
+
const runIdPad = padText(baseline.runId, runIdWidth);
|
|
87
|
+
|
|
88
|
+
const rateValue = `${(baseline.metrics.successRate * 100).toFixed(1)}%`;
|
|
89
|
+
const ratePad = padText(rateValue, rateWidth, 'right');
|
|
90
|
+
const rateColored = rateColor(ratePad);
|
|
91
|
+
|
|
92
|
+
const dateObj = new Date(baseline.createdAt);
|
|
93
|
+
const dateStr = `${dateObj.toLocaleDateString()} ${dateObj.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
|
94
|
+
const datePad = padText(dateStr, dateWidth, 'right');
|
|
95
|
+
|
|
96
|
+
const tagPad = padText(baseline.tag || '-', tagWidth, 'right');
|
|
97
|
+
|
|
98
|
+
lines.push(`║ ${scenarioPad} ${runIdPad} ${rateColored} ${datePad} ${tagPad} ║`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
lines.push(`╚${border}╝`);
|
|
102
|
+
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Render baselines as plain text for CI/non-TTY
|
|
108
|
+
*/
|
|
109
|
+
function renderBaselinesPlain(baselines: BaselineMetadata[]): string {
|
|
110
|
+
const lines = ['=== BASELINES ===', ''];
|
|
111
|
+
|
|
112
|
+
for (const baseline of baselines) {
|
|
113
|
+
const rate = `${(baseline.metrics.successRate * 100).toFixed(1)}%`;
|
|
114
|
+
const date = new Date(baseline.createdAt).toLocaleString();
|
|
115
|
+
const tag = baseline.tag ? ` [${baseline.tag}]` : '';
|
|
116
|
+
lines.push(`${baseline.scenario} ${baseline.runId} ${rate} ${date}${tag}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return lines.join('\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Create the baseline set subcommand
|
|
124
|
+
*/
|
|
125
|
+
function baselineSetCommand(): Command {
|
|
126
|
+
const cmd = new Command('set');
|
|
127
|
+
|
|
128
|
+
cmd
|
|
129
|
+
.description('Set a run as the baseline for regression comparison')
|
|
130
|
+
.argument('<run-id>', 'Run ID to set as baseline')
|
|
131
|
+
.option('-s, --scenario <name>', 'Override scenario name (defaults to scenario from run)')
|
|
132
|
+
.option('-t, --tag <tag>', 'Optional tag/description for the baseline')
|
|
133
|
+
.option('--config <path>', 'Path to config file')
|
|
134
|
+
.action(async (runId: string, options: BaselineSetOptions) => {
|
|
135
|
+
const spinner = createSpinner('Setting baseline...');
|
|
136
|
+
spinner.start();
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const config = await loadConfig(options.config);
|
|
140
|
+
const storage = createStorage({ fileConfig: config });
|
|
141
|
+
|
|
142
|
+
if (!isBaselineStorage(storage)) {
|
|
143
|
+
spinner.fail('Error');
|
|
144
|
+
console.log();
|
|
145
|
+
console.log(
|
|
146
|
+
renderError({
|
|
147
|
+
title: 'Baselines Not Supported',
|
|
148
|
+
reason: 'Current storage adapter does not support baseline management',
|
|
149
|
+
suggestions: [
|
|
150
|
+
'Use local storage which supports baselines',
|
|
151
|
+
'Check your storage configuration in artemis.config.yaml',
|
|
152
|
+
],
|
|
153
|
+
})
|
|
154
|
+
);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const baseline = await storage.setBaseline(options.scenario || '', runId, options.tag);
|
|
159
|
+
|
|
160
|
+
spinner.succeed('Baseline set successfully');
|
|
161
|
+
console.log();
|
|
162
|
+
console.log(`${icons.passed} ${chalk.bold('Baseline created')}`);
|
|
163
|
+
console.log();
|
|
164
|
+
console.log(` Scenario: ${chalk.cyan(baseline.scenario)}`);
|
|
165
|
+
console.log(` Run ID: ${chalk.dim(baseline.runId)}`);
|
|
166
|
+
console.log(
|
|
167
|
+
` Success Rate: ${chalk.green(`${(baseline.metrics.successRate * 100).toFixed(1)}%`)}`
|
|
168
|
+
);
|
|
169
|
+
console.log(
|
|
170
|
+
` Test Cases: ${baseline.metrics.passedCases}/${baseline.metrics.totalCases} passed`
|
|
171
|
+
);
|
|
172
|
+
if (baseline.tag) {
|
|
173
|
+
console.log(` Tag: ${chalk.dim(baseline.tag)}`);
|
|
174
|
+
}
|
|
175
|
+
console.log();
|
|
176
|
+
console.log(
|
|
177
|
+
chalk.dim('Future runs of this scenario will be compared against this baseline.')
|
|
178
|
+
);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
spinner.fail('Error');
|
|
181
|
+
console.log();
|
|
182
|
+
console.log(
|
|
183
|
+
renderError({
|
|
184
|
+
title: 'Failed to Set Baseline',
|
|
185
|
+
reason: (error as Error).message,
|
|
186
|
+
suggestions: [
|
|
187
|
+
'Check that the run ID exists',
|
|
188
|
+
'Run "artemiskit history" to see available runs',
|
|
189
|
+
'Verify storage configuration',
|
|
190
|
+
],
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return cmd;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Create the baseline list subcommand
|
|
202
|
+
*/
|
|
203
|
+
function baselineListCommand(): Command {
|
|
204
|
+
const cmd = new Command('list');
|
|
205
|
+
|
|
206
|
+
cmd
|
|
207
|
+
.description('List all baselines')
|
|
208
|
+
.option('--config <path>', 'Path to config file')
|
|
209
|
+
.option('--json', 'Output as JSON')
|
|
210
|
+
.action(async (options: BaselineListOptions) => {
|
|
211
|
+
const spinner = createSpinner('Loading baselines...');
|
|
212
|
+
spinner.start();
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const config = await loadConfig(options.config);
|
|
216
|
+
const storage = createStorage({ fileConfig: config });
|
|
217
|
+
|
|
218
|
+
if (!isBaselineStorage(storage)) {
|
|
219
|
+
spinner.fail('Error');
|
|
220
|
+
console.log();
|
|
221
|
+
console.log(
|
|
222
|
+
renderError({
|
|
223
|
+
title: 'Baselines Not Supported',
|
|
224
|
+
reason: 'Current storage adapter does not support baseline management',
|
|
225
|
+
suggestions: ['Use local storage which supports baselines'],
|
|
226
|
+
})
|
|
227
|
+
);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const baselines = await storage.listBaselines();
|
|
232
|
+
spinner.succeed('Loaded baselines');
|
|
233
|
+
console.log();
|
|
234
|
+
|
|
235
|
+
if (baselines.length === 0) {
|
|
236
|
+
console.log(chalk.dim('No baselines set.'));
|
|
237
|
+
console.log();
|
|
238
|
+
console.log(chalk.dim('Set a baseline with:'));
|
|
239
|
+
console.log(chalk.dim(' artemiskit baseline set <run-id>'));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (options.json) {
|
|
244
|
+
console.log(JSON.stringify(baselines, null, 2));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (isTTY) {
|
|
249
|
+
console.log(renderBaselinesTable(baselines));
|
|
250
|
+
} else {
|
|
251
|
+
console.log(renderBaselinesPlain(baselines));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log();
|
|
255
|
+
console.log(
|
|
256
|
+
chalk.dim(`${baselines.length} baseline${baselines.length === 1 ? '' : 's'} configured`)
|
|
257
|
+
);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
spinner.fail('Error');
|
|
260
|
+
console.log();
|
|
261
|
+
console.log(
|
|
262
|
+
renderError({
|
|
263
|
+
title: 'Failed to List Baselines',
|
|
264
|
+
reason: (error as Error).message,
|
|
265
|
+
suggestions: ['Verify storage configuration'],
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
return cmd;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Create the baseline remove subcommand
|
|
277
|
+
*/
|
|
278
|
+
function baselineRemoveCommand(): Command {
|
|
279
|
+
const cmd = new Command('remove');
|
|
280
|
+
|
|
281
|
+
cmd
|
|
282
|
+
.description('Remove a baseline')
|
|
283
|
+
.argument('<identifier>', 'Run ID of the baseline to remove (or scenario name with --scenario)')
|
|
284
|
+
.option('--config <path>', 'Path to config file')
|
|
285
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
286
|
+
.option('-s, --scenario', 'Treat identifier as scenario name instead of run ID')
|
|
287
|
+
.action(async (identifier: string, options: BaselineRemoveOptions & { scenario?: boolean }) => {
|
|
288
|
+
const spinner = createSpinner('Removing baseline...');
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const config = await loadConfig(options.config);
|
|
292
|
+
const storage = createStorage({ fileConfig: config });
|
|
293
|
+
|
|
294
|
+
if (!isBaselineStorage(storage)) {
|
|
295
|
+
console.log(
|
|
296
|
+
renderError({
|
|
297
|
+
title: 'Baselines Not Supported',
|
|
298
|
+
reason: 'Current storage adapter does not support baseline management',
|
|
299
|
+
suggestions: ['Use local storage which supports baselines'],
|
|
300
|
+
})
|
|
301
|
+
);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check if baseline exists first - by run ID or scenario name
|
|
306
|
+
const existing = options.scenario
|
|
307
|
+
? await storage.getBaseline(identifier)
|
|
308
|
+
: await storage.getBaselineByRunId(identifier);
|
|
309
|
+
|
|
310
|
+
if (!existing) {
|
|
311
|
+
console.log();
|
|
312
|
+
console.log(
|
|
313
|
+
chalk.yellow(
|
|
314
|
+
options.scenario
|
|
315
|
+
? `No baseline found for scenario: ${identifier}`
|
|
316
|
+
: `No baseline found with run ID: ${identifier}`
|
|
317
|
+
)
|
|
318
|
+
);
|
|
319
|
+
console.log();
|
|
320
|
+
console.log(chalk.dim('List baselines with:'));
|
|
321
|
+
console.log(chalk.dim(' artemiskit baseline list'));
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Confirm if not forced
|
|
326
|
+
if (!options.force && isTTY) {
|
|
327
|
+
const { promptConfirm } = await import('../ui/index.js');
|
|
328
|
+
const confirmed = await promptConfirm(
|
|
329
|
+
`Remove baseline for "${existing.scenario}"? (Run ID: ${existing.runId})`,
|
|
330
|
+
false
|
|
331
|
+
);
|
|
332
|
+
if (!confirmed) {
|
|
333
|
+
console.log(chalk.dim('Cancelled.'));
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
spinner.start();
|
|
339
|
+
const removed = options.scenario
|
|
340
|
+
? await storage.removeBaseline(identifier)
|
|
341
|
+
: await storage.removeBaselineByRunId(identifier);
|
|
342
|
+
|
|
343
|
+
if (removed) {
|
|
344
|
+
spinner.succeed('Baseline removed');
|
|
345
|
+
console.log();
|
|
346
|
+
console.log(`${icons.passed} Removed baseline for: ${chalk.cyan(existing.scenario)}`);
|
|
347
|
+
} else {
|
|
348
|
+
spinner.fail('Baseline not found');
|
|
349
|
+
}
|
|
350
|
+
} catch (error) {
|
|
351
|
+
spinner.fail('Error');
|
|
352
|
+
console.log();
|
|
353
|
+
console.log(
|
|
354
|
+
renderError({
|
|
355
|
+
title: 'Failed to Remove Baseline',
|
|
356
|
+
reason: (error as Error).message,
|
|
357
|
+
suggestions: [
|
|
358
|
+
'Check the run ID or scenario name',
|
|
359
|
+
'Run "artemiskit baseline list" to see baselines',
|
|
360
|
+
],
|
|
361
|
+
})
|
|
362
|
+
);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
return cmd;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Create the baseline get subcommand
|
|
372
|
+
*/
|
|
373
|
+
function baselineGetCommand(): Command {
|
|
374
|
+
const cmd = new Command('get');
|
|
375
|
+
|
|
376
|
+
cmd
|
|
377
|
+
.description('Get baseline details')
|
|
378
|
+
.argument('<identifier>', 'Run ID of the baseline (or scenario name with --scenario)')
|
|
379
|
+
.option('--config <path>', 'Path to config file')
|
|
380
|
+
.option('--json', 'Output as JSON')
|
|
381
|
+
.option('-s, --scenario', 'Treat identifier as scenario name instead of run ID')
|
|
382
|
+
.action(
|
|
383
|
+
async (
|
|
384
|
+
identifier: string,
|
|
385
|
+
options: { config?: string; json?: boolean; scenario?: boolean }
|
|
386
|
+
) => {
|
|
387
|
+
try {
|
|
388
|
+
const config = await loadConfig(options.config);
|
|
389
|
+
const storage = createStorage({ fileConfig: config });
|
|
390
|
+
|
|
391
|
+
if (!isBaselineStorage(storage)) {
|
|
392
|
+
console.log(
|
|
393
|
+
renderError({
|
|
394
|
+
title: 'Baselines Not Supported',
|
|
395
|
+
reason: 'Current storage adapter does not support baseline management',
|
|
396
|
+
suggestions: ['Use local storage which supports baselines'],
|
|
397
|
+
})
|
|
398
|
+
);
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Look up by run ID or scenario name
|
|
403
|
+
const baseline = options.scenario
|
|
404
|
+
? await storage.getBaseline(identifier)
|
|
405
|
+
: await storage.getBaselineByRunId(identifier);
|
|
406
|
+
|
|
407
|
+
if (!baseline) {
|
|
408
|
+
console.log(
|
|
409
|
+
chalk.yellow(
|
|
410
|
+
options.scenario
|
|
411
|
+
? `No baseline found for scenario: ${identifier}`
|
|
412
|
+
: `No baseline found with run ID: ${identifier}`
|
|
413
|
+
)
|
|
414
|
+
);
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (options.json) {
|
|
419
|
+
console.log(JSON.stringify(baseline, null, 2));
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
console.log();
|
|
424
|
+
console.log(chalk.bold(`Baseline: ${baseline.scenario}`));
|
|
425
|
+
console.log();
|
|
426
|
+
console.log(` Run ID: ${baseline.runId}`);
|
|
427
|
+
console.log(` Created: ${new Date(baseline.createdAt).toLocaleString()}`);
|
|
428
|
+
console.log(
|
|
429
|
+
` Success Rate: ${chalk.green(`${(baseline.metrics.successRate * 100).toFixed(1)}%`)}`
|
|
430
|
+
);
|
|
431
|
+
console.log(
|
|
432
|
+
` Test Cases: ${baseline.metrics.passedCases}/${baseline.metrics.totalCases}`
|
|
433
|
+
);
|
|
434
|
+
console.log(` Latency: ${baseline.metrics.medianLatencyMs}ms (median)`);
|
|
435
|
+
console.log(` Tokens: ${baseline.metrics.totalTokens.toLocaleString()}`);
|
|
436
|
+
if (baseline.tag) {
|
|
437
|
+
console.log(` Tag: ${baseline.tag}`);
|
|
438
|
+
}
|
|
439
|
+
console.log();
|
|
440
|
+
} catch (error) {
|
|
441
|
+
console.log(
|
|
442
|
+
renderError({
|
|
443
|
+
title: 'Failed to Get Baseline',
|
|
444
|
+
reason: (error as Error).message,
|
|
445
|
+
suggestions: [
|
|
446
|
+
'Check the run ID or scenario name',
|
|
447
|
+
'Run "artemiskit baseline list" to see baselines',
|
|
448
|
+
],
|
|
449
|
+
})
|
|
450
|
+
);
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
return cmd;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Create the main baseline command with subcommands
|
|
461
|
+
*/
|
|
462
|
+
export function baselineCommand(): Command {
|
|
463
|
+
const cmd = new Command('baseline');
|
|
464
|
+
|
|
465
|
+
cmd.description('Manage baseline runs for regression detection');
|
|
466
|
+
|
|
467
|
+
cmd.addCommand(baselineSetCommand());
|
|
468
|
+
cmd.addCommand(baselineListCommand());
|
|
469
|
+
cmd.addCommand(baselineRemoveCommand());
|
|
470
|
+
cmd.addCommand(baselineGetCommand());
|
|
471
|
+
|
|
472
|
+
return cmd;
|
|
473
|
+
}
|
package/src/commands/compare.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Compare command - Compare two test runs
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { writeFileSync } from 'node:fs';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { buildComparisonData, generateCompareHTMLReport } from '@artemiskit/reports';
|
|
5
8
|
import chalk from 'chalk';
|
|
6
9
|
import { Command } from 'commander';
|
|
7
10
|
import { loadConfig } from '../config/loader.js';
|
|
@@ -11,6 +14,8 @@ import { createStorage } from '../utils/storage.js';
|
|
|
11
14
|
interface CompareOptions {
|
|
12
15
|
threshold?: number;
|
|
13
16
|
config?: string;
|
|
17
|
+
html?: string;
|
|
18
|
+
json?: string;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
function renderComparisonPanel(
|
|
@@ -135,6 +140,8 @@ export function compareCommand(): Command {
|
|
|
135
140
|
.argument('<current>', 'Current run ID')
|
|
136
141
|
.option('--threshold <number>', 'Regression threshold (0-1)', '0.05')
|
|
137
142
|
.option('--config <path>', 'Path to config file')
|
|
143
|
+
.option('--html <path>', 'Generate HTML comparison report')
|
|
144
|
+
.option('--json <path>', 'Generate JSON comparison report')
|
|
138
145
|
.action(async (baselineId: string, currentId: string, options: CompareOptions) => {
|
|
139
146
|
const spinner = createSpinner('Loading runs...');
|
|
140
147
|
spinner.start();
|
|
@@ -165,6 +172,24 @@ export function compareCommand(): Command {
|
|
|
165
172
|
const comparison = await storage.compare(baselineId, currentId);
|
|
166
173
|
const { baseline, current, delta } = comparison;
|
|
167
174
|
|
|
175
|
+
// Generate HTML report if requested
|
|
176
|
+
if (options.html) {
|
|
177
|
+
const htmlPath = resolve(options.html);
|
|
178
|
+
const html = generateCompareHTMLReport(baseline, current);
|
|
179
|
+
writeFileSync(htmlPath, html, 'utf-8');
|
|
180
|
+
console.log(`${icons.passed} HTML comparison report saved to: ${chalk.cyan(htmlPath)}`);
|
|
181
|
+
console.log();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Generate JSON report if requested
|
|
185
|
+
if (options.json) {
|
|
186
|
+
const jsonPath = resolve(options.json);
|
|
187
|
+
const comparisonData = buildComparisonData(baseline, current);
|
|
188
|
+
writeFileSync(jsonPath, JSON.stringify(comparisonData, null, 2), 'utf-8');
|
|
189
|
+
console.log(`${icons.passed} JSON comparison report saved to: ${chalk.cyan(jsonPath)}`);
|
|
190
|
+
console.log();
|
|
191
|
+
}
|
|
192
|
+
|
|
168
193
|
// Show comparison panel
|
|
169
194
|
if (isTTY) {
|
|
170
195
|
console.log(renderComparisonPanel(baseline, current, delta));
|