@artemiskit/cli 0.1.4 → 0.1.6
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 +24 -0
- package/README.md +1 -0
- package/dist/index.js +19129 -20009
- package/dist/src/commands/compare.d.ts.map +1 -1
- package/dist/src/commands/history.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/report.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/ui/colors.d.ts +44 -0
- package/dist/src/ui/colors.d.ts.map +1 -0
- package/dist/src/ui/errors.d.ts +39 -0
- package/dist/src/ui/errors.d.ts.map +1 -0
- package/dist/src/ui/index.d.ts +16 -0
- package/dist/src/ui/index.d.ts.map +1 -0
- package/dist/src/ui/live-status.d.ts +82 -0
- package/dist/src/ui/live-status.d.ts.map +1 -0
- package/dist/src/ui/panels.d.ts +49 -0
- package/dist/src/ui/panels.d.ts.map +1 -0
- package/dist/src/ui/progress.d.ts +60 -0
- package/dist/src/ui/progress.d.ts.map +1 -0
- package/dist/src/ui/utils.d.ts +42 -0
- package/dist/src/ui/utils.d.ts.map +1 -0
- package/package.json +6 -6
- package/src/__tests__/helpers/index.ts +6 -0
- package/src/__tests__/helpers/mock-adapter.ts +90 -0
- package/src/__tests__/helpers/test-utils.ts +205 -0
- package/src/__tests__/integration/compare-command.test.ts +236 -0
- package/src/__tests__/integration/config.test.ts +125 -0
- package/src/__tests__/integration/history-command.test.ts +251 -0
- package/src/__tests__/integration/init-command.test.ts +177 -0
- package/src/__tests__/integration/report-command.test.ts +245 -0
- package/src/__tests__/integration/ui.test.ts +230 -0
- package/src/commands/compare.ts +158 -49
- package/src/commands/history.ts +131 -30
- package/src/commands/init.ts +181 -21
- package/src/commands/redteam.ts +118 -75
- package/src/commands/report.ts +29 -14
- package/src/commands/run.ts +86 -66
- package/src/commands/stress.ts +61 -63
- package/src/ui/colors.ts +62 -0
- package/src/ui/errors.ts +248 -0
- package/src/ui/index.ts +42 -0
- package/src/ui/live-status.ts +259 -0
- package/src/ui/panels.ts +216 -0
- package/src/ui/progress.ts +139 -0
- package/src/ui/utils.ts +88 -0
package/src/commands/run.ts
CHANGED
|
@@ -9,16 +9,25 @@ import {
|
|
|
9
9
|
runScenario,
|
|
10
10
|
} from '@artemiskit/core';
|
|
11
11
|
import chalk from 'chalk';
|
|
12
|
-
import Table from 'cli-table3';
|
|
13
12
|
import { Command } from 'commander';
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
13
|
+
import { loadConfig } from '../config/loader.js';
|
|
14
|
+
import {
|
|
15
|
+
createSpinner,
|
|
16
|
+
icons,
|
|
17
|
+
renderError,
|
|
18
|
+
renderProgressBar,
|
|
19
|
+
renderSummaryPanel,
|
|
20
|
+
getProviderErrorContext,
|
|
21
|
+
formatDuration,
|
|
22
|
+
padText,
|
|
23
|
+
isTTY,
|
|
24
|
+
} from '../ui/index.js';
|
|
16
25
|
import {
|
|
17
26
|
buildAdapterConfig,
|
|
18
27
|
resolveModelWithSource,
|
|
19
28
|
resolveProviderWithSource,
|
|
20
|
-
} from '../utils/adapter';
|
|
21
|
-
import { createStorage } from '../utils/storage';
|
|
29
|
+
} from '../utils/adapter.js';
|
|
30
|
+
import { createStorage } from '../utils/storage.js';
|
|
22
31
|
|
|
23
32
|
interface RunOptions {
|
|
24
33
|
provider?: string;
|
|
@@ -57,7 +66,8 @@ export function runCommand(): Command {
|
|
|
57
66
|
'Custom redaction patterns (regex or built-in: email, phone, credit_card, ssn, api_key)'
|
|
58
67
|
)
|
|
59
68
|
.action(async (scenarioPath: string, options: RunOptions) => {
|
|
60
|
-
const spinner =
|
|
69
|
+
const spinner = createSpinner('Loading configuration...');
|
|
70
|
+
spinner.start();
|
|
61
71
|
|
|
62
72
|
try {
|
|
63
73
|
// Load config file if present
|
|
@@ -122,6 +132,15 @@ export function runCommand(): Command {
|
|
|
122
132
|
console.log();
|
|
123
133
|
}
|
|
124
134
|
|
|
135
|
+
// Track progress
|
|
136
|
+
const totalCases = scenario.cases.length;
|
|
137
|
+
let completedCases = 0;
|
|
138
|
+
|
|
139
|
+
// Calculate max widths for alignment
|
|
140
|
+
const maxIdLength = Math.max(...scenario.cases.map((c) => c.id.length));
|
|
141
|
+
const maxScoreLength = 6; // "(100%)"
|
|
142
|
+
const maxDurationLength = 6; // "10.0s" or "999ms"
|
|
143
|
+
|
|
125
144
|
// Run scenario using core runner
|
|
126
145
|
const result = await runScenario({
|
|
127
146
|
scenario,
|
|
@@ -134,9 +153,29 @@ export function runCommand(): Command {
|
|
|
134
153
|
retries: options.retries ? Number.parseInt(String(options.retries)) : undefined,
|
|
135
154
|
redaction,
|
|
136
155
|
onCaseComplete: (caseResult) => {
|
|
137
|
-
|
|
156
|
+
completedCases++;
|
|
157
|
+
|
|
158
|
+
const statusIcon = caseResult.ok ? icons.passed : icons.failed;
|
|
138
159
|
const scoreStr = `(${(caseResult.score * 100).toFixed(0)}%)`;
|
|
139
|
-
|
|
160
|
+
const durationStr = caseResult.latencyMs ? formatDuration(caseResult.latencyMs) : '';
|
|
161
|
+
|
|
162
|
+
// Pad columns for alignment
|
|
163
|
+
const paddedId = padText(caseResult.id, maxIdLength);
|
|
164
|
+
const paddedScore = padText(scoreStr, maxScoreLength, 'right');
|
|
165
|
+
const paddedDuration = padText(durationStr, maxDurationLength, 'right');
|
|
166
|
+
|
|
167
|
+
// Show result - with progress bar in TTY, simple format in CI/CD
|
|
168
|
+
if (isTTY) {
|
|
169
|
+
const progressBar = renderProgressBar(completedCases, totalCases, { width: 15 });
|
|
170
|
+
console.log(
|
|
171
|
+
`${statusIcon} ${paddedId} ${chalk.dim(paddedScore)} ${chalk.dim(paddedDuration)} ${progressBar}`
|
|
172
|
+
);
|
|
173
|
+
} else {
|
|
174
|
+
// CI/CD friendly output - no progress bar, just count
|
|
175
|
+
console.log(
|
|
176
|
+
`${statusIcon} ${paddedId} ${chalk.dim(paddedScore)} ${chalk.dim(paddedDuration)} [${completedCases}/${totalCases}]`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
140
179
|
|
|
141
180
|
if (!caseResult.ok && options.verbose) {
|
|
142
181
|
console.log(chalk.dim(` Reason: ${caseResult.reason}`));
|
|
@@ -149,9 +188,35 @@ export function runCommand(): Command {
|
|
|
149
188
|
},
|
|
150
189
|
});
|
|
151
190
|
|
|
152
|
-
// Display summary
|
|
191
|
+
// Display summary using enhanced panel
|
|
153
192
|
console.log();
|
|
154
|
-
|
|
193
|
+
const summaryData = {
|
|
194
|
+
passed: result.manifest.metrics.passed_cases,
|
|
195
|
+
failed: result.manifest.metrics.failed_cases,
|
|
196
|
+
skipped: 0,
|
|
197
|
+
successRate: result.manifest.metrics.success_rate * 100,
|
|
198
|
+
duration: result.manifest.duration_ms,
|
|
199
|
+
title: 'TEST RESULTS',
|
|
200
|
+
};
|
|
201
|
+
console.log(renderSummaryPanel(summaryData));
|
|
202
|
+
|
|
203
|
+
// Show additional metrics
|
|
204
|
+
console.log();
|
|
205
|
+
console.log(
|
|
206
|
+
chalk.dim(
|
|
207
|
+
`Run ID: ${result.manifest.run_id} | Median Latency: ${result.manifest.metrics.median_latency_ms}ms | Tokens: ${result.manifest.metrics.total_tokens.toLocaleString()}`
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Show redaction info if enabled
|
|
212
|
+
if (result.manifest.redaction?.enabled) {
|
|
213
|
+
const r = result.manifest.redaction;
|
|
214
|
+
console.log(
|
|
215
|
+
chalk.dim(
|
|
216
|
+
`Redactions: ${r.summary.totalRedactions} (${r.summary.promptsRedacted} prompts, ${r.summary.responsesRedacted} responses)`
|
|
217
|
+
)
|
|
218
|
+
);
|
|
219
|
+
}
|
|
155
220
|
|
|
156
221
|
// Save results
|
|
157
222
|
if (options.save) {
|
|
@@ -167,9 +232,18 @@ export function runCommand(): Command {
|
|
|
167
232
|
}
|
|
168
233
|
} catch (error) {
|
|
169
234
|
spinner.fail('Error');
|
|
170
|
-
|
|
235
|
+
|
|
236
|
+
// Get provider from options or default
|
|
237
|
+
const provider = options.provider || 'unknown';
|
|
238
|
+
|
|
239
|
+
// Display enhanced error message
|
|
240
|
+
const errorContext = getProviderErrorContext(provider, error as Error);
|
|
241
|
+
console.log();
|
|
242
|
+
console.log(renderError(errorContext));
|
|
243
|
+
|
|
171
244
|
if (options.verbose) {
|
|
172
|
-
console.
|
|
245
|
+
console.log();
|
|
246
|
+
console.error(chalk.dim((error as Error).stack));
|
|
173
247
|
}
|
|
174
248
|
process.exit(1);
|
|
175
249
|
}
|
|
@@ -177,57 +251,3 @@ export function runCommand(): Command {
|
|
|
177
251
|
|
|
178
252
|
return cmd;
|
|
179
253
|
}
|
|
180
|
-
|
|
181
|
-
function displaySummary(
|
|
182
|
-
metrics: {
|
|
183
|
-
success_rate: number;
|
|
184
|
-
total_cases: number;
|
|
185
|
-
passed_cases: number;
|
|
186
|
-
failed_cases: number;
|
|
187
|
-
median_latency_ms: number;
|
|
188
|
-
total_tokens: number;
|
|
189
|
-
},
|
|
190
|
-
runId: string,
|
|
191
|
-
redaction?: {
|
|
192
|
-
enabled: boolean;
|
|
193
|
-
summary: {
|
|
194
|
-
promptsRedacted: number;
|
|
195
|
-
responsesRedacted: number;
|
|
196
|
-
totalRedactions: number;
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
): void {
|
|
200
|
-
const table = new Table({
|
|
201
|
-
head: [chalk.bold('Metric'), chalk.bold('Value')],
|
|
202
|
-
style: { head: [], border: [] },
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
const successColor =
|
|
206
|
-
metrics.success_rate >= 0.9
|
|
207
|
-
? chalk.green
|
|
208
|
-
: metrics.success_rate >= 0.7
|
|
209
|
-
? chalk.yellow
|
|
210
|
-
: chalk.red;
|
|
211
|
-
|
|
212
|
-
table.push(
|
|
213
|
-
['Run ID', runId],
|
|
214
|
-
['Success Rate', successColor(`${(metrics.success_rate * 100).toFixed(1)}%`)],
|
|
215
|
-
['Passed', chalk.green(metrics.passed_cases.toString())],
|
|
216
|
-
['Failed', metrics.failed_cases > 0 ? chalk.red(metrics.failed_cases.toString()) : '0'],
|
|
217
|
-
['Median Latency', `${metrics.median_latency_ms}ms`],
|
|
218
|
-
['Total Tokens', metrics.total_tokens.toLocaleString()]
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
// Add redaction info if enabled
|
|
222
|
-
if (redaction?.enabled) {
|
|
223
|
-
table.push(
|
|
224
|
-
['Redaction', chalk.yellow('Enabled')],
|
|
225
|
-
[
|
|
226
|
-
'Redactions Made',
|
|
227
|
-
`${redaction.summary.totalRedactions} (${redaction.summary.promptsRedacted} prompts, ${redaction.summary.responsesRedacted} responses)`,
|
|
228
|
-
]
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
console.log(table.toString());
|
|
233
|
-
}
|
package/src/commands/stress.ts
CHANGED
|
@@ -17,17 +17,25 @@ import {
|
|
|
17
17
|
} from '@artemiskit/core';
|
|
18
18
|
import { generateJSONReport, generateStressHTMLReport } from '@artemiskit/reports';
|
|
19
19
|
import chalk from 'chalk';
|
|
20
|
-
import Table from 'cli-table3';
|
|
21
20
|
import { Command } from 'commander';
|
|
22
21
|
import { nanoid } from 'nanoid';
|
|
23
|
-
import
|
|
24
|
-
import {
|
|
22
|
+
import { loadConfig } from '../config/loader.js';
|
|
23
|
+
import {
|
|
24
|
+
createSpinner,
|
|
25
|
+
renderStressSummaryPanel,
|
|
26
|
+
renderError,
|
|
27
|
+
renderInfoBox,
|
|
28
|
+
renderProgressBar,
|
|
29
|
+
getProviderErrorContext,
|
|
30
|
+
isTTY,
|
|
31
|
+
colors,
|
|
32
|
+
} from '../ui/index.js';
|
|
25
33
|
import {
|
|
26
34
|
buildAdapterConfig,
|
|
27
35
|
resolveModelWithSource,
|
|
28
36
|
resolveProviderWithSource,
|
|
29
|
-
} from '../utils/adapter';
|
|
30
|
-
import { createStorage } from '../utils/storage';
|
|
37
|
+
} from '../utils/adapter.js';
|
|
38
|
+
import { createStorage } from '../utils/storage.js';
|
|
31
39
|
|
|
32
40
|
interface StressOptions {
|
|
33
41
|
provider?: string;
|
|
@@ -66,7 +74,8 @@ export function stressCommand(): Command {
|
|
|
66
74
|
'Custom redaction patterns (regex or built-in: email, phone, credit_card, ssn, api_key)'
|
|
67
75
|
)
|
|
68
76
|
.action(async (scenarioPath: string, options: StressOptions) => {
|
|
69
|
-
const spinner =
|
|
77
|
+
const spinner = createSpinner('Loading configuration...');
|
|
78
|
+
spinner.start();
|
|
70
79
|
const startTime = new Date();
|
|
71
80
|
|
|
72
81
|
try {
|
|
@@ -132,21 +141,22 @@ export function stressCommand(): Command {
|
|
|
132
141
|
redactor = new Redactor(redactionConfig);
|
|
133
142
|
}
|
|
134
143
|
|
|
144
|
+
// Display configuration using info box
|
|
135
145
|
console.log();
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
const configLines = [
|
|
147
|
+
`Concurrency: ${concurrency}`,
|
|
148
|
+
`Duration: ${durationSec}s`,
|
|
149
|
+
`Ramp-up: ${rampUpSec}s`,
|
|
150
|
+
];
|
|
140
151
|
if (maxRequests) {
|
|
141
|
-
|
|
152
|
+
configLines.push(`Max requests: ${maxRequests}`);
|
|
142
153
|
}
|
|
143
154
|
if (options.redact) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
`Redaction: enabled${options.redactPatterns ? ` (${options.redactPatterns.join(', ')})` : ' (default patterns)'}`
|
|
147
|
-
)
|
|
155
|
+
configLines.push(
|
|
156
|
+
`Redaction: enabled${options.redactPatterns ? ` (${options.redactPatterns.join(', ')})` : ''}`
|
|
148
157
|
);
|
|
149
158
|
}
|
|
159
|
+
console.log(renderInfoBox('Stress Test Configuration', configLines));
|
|
150
160
|
console.log();
|
|
151
161
|
|
|
152
162
|
// Get test prompts from scenario cases
|
|
@@ -169,8 +179,13 @@ export function stressCommand(): Command {
|
|
|
169
179
|
rampUpMs: rampUpSec * 1000,
|
|
170
180
|
maxRequests,
|
|
171
181
|
temperature: scenario.temperature,
|
|
172
|
-
onProgress: (completed, active) => {
|
|
173
|
-
|
|
182
|
+
onProgress: (completed, active, _total) => {
|
|
183
|
+
if (isTTY) {
|
|
184
|
+
const progress = maxRequests
|
|
185
|
+
? renderProgressBar(completed, maxRequests, { width: 15, showPercentage: true })
|
|
186
|
+
: `${completed} completed`;
|
|
187
|
+
spinner.update(`Running stress test... ${progress}, ${active} active`);
|
|
188
|
+
}
|
|
174
189
|
},
|
|
175
190
|
verbose: options.verbose,
|
|
176
191
|
});
|
|
@@ -232,8 +247,24 @@ export function stressCommand(): Command {
|
|
|
232
247
|
redaction: redactionInfo,
|
|
233
248
|
};
|
|
234
249
|
|
|
235
|
-
// Display
|
|
236
|
-
|
|
250
|
+
// Display summary using enhanced panel
|
|
251
|
+
const summaryData = {
|
|
252
|
+
totalRequests: metrics.total_requests,
|
|
253
|
+
successfulRequests: metrics.successful_requests,
|
|
254
|
+
failedRequests: metrics.failed_requests,
|
|
255
|
+
successRate: metrics.success_rate * 100,
|
|
256
|
+
duration: endTime.getTime() - startTime.getTime(),
|
|
257
|
+
avgLatency: metrics.avg_latency_ms,
|
|
258
|
+
p50Latency: metrics.p50_latency_ms,
|
|
259
|
+
p95Latency: metrics.p95_latency_ms,
|
|
260
|
+
p99Latency: metrics.p99_latency_ms,
|
|
261
|
+
throughput: metrics.requests_per_second,
|
|
262
|
+
};
|
|
263
|
+
console.log(renderStressSummaryPanel(summaryData));
|
|
264
|
+
|
|
265
|
+
// Show run ID
|
|
266
|
+
console.log();
|
|
267
|
+
console.log(chalk.dim(`Run ID: ${runId}`));
|
|
237
268
|
|
|
238
269
|
// Display latency histogram if verbose
|
|
239
270
|
if (options.verbose) {
|
|
@@ -269,7 +300,13 @@ export function stressCommand(): Command {
|
|
|
269
300
|
}
|
|
270
301
|
} catch (error) {
|
|
271
302
|
spinner.fail('Error');
|
|
272
|
-
|
|
303
|
+
|
|
304
|
+
// Display enhanced error message
|
|
305
|
+
const provider = options.provider || 'unknown';
|
|
306
|
+
const errorContext = getProviderErrorContext(provider, error as Error);
|
|
307
|
+
console.log();
|
|
308
|
+
console.log(renderError(errorContext));
|
|
309
|
+
|
|
273
310
|
process.exit(1);
|
|
274
311
|
}
|
|
275
312
|
});
|
|
@@ -290,7 +327,7 @@ interface StressTestOptions {
|
|
|
290
327
|
rampUpMs: number;
|
|
291
328
|
maxRequests?: number;
|
|
292
329
|
temperature?: number;
|
|
293
|
-
onProgress?: (completed: number, active: number) => void;
|
|
330
|
+
onProgress?: (completed: number, active: number, total?: number) => void;
|
|
294
331
|
verbose?: boolean;
|
|
295
332
|
}
|
|
296
333
|
|
|
@@ -343,7 +380,7 @@ async function runStressTest(options: StressTestOptions): Promise<StressRequestR
|
|
|
343
380
|
} finally {
|
|
344
381
|
active--;
|
|
345
382
|
completed++;
|
|
346
|
-
onProgress?.(completed, active);
|
|
383
|
+
onProgress?.(completed, active, maxRequests);
|
|
347
384
|
}
|
|
348
385
|
};
|
|
349
386
|
|
|
@@ -428,45 +465,6 @@ function sampleResults(results: StressRequestResult[], maxSamples: number): Stre
|
|
|
428
465
|
return sampled;
|
|
429
466
|
}
|
|
430
467
|
|
|
431
|
-
function displayStats(metrics: StressMetrics, runId: string): void {
|
|
432
|
-
const table = new Table({
|
|
433
|
-
head: [chalk.bold('Metric'), chalk.bold('Value')],
|
|
434
|
-
style: { head: [], border: [] },
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
table.push(
|
|
438
|
-
['Run ID', runId],
|
|
439
|
-
['Total Requests', metrics.total_requests.toString()],
|
|
440
|
-
['Successful', chalk.green(metrics.successful_requests.toString())],
|
|
441
|
-
['Failed', metrics.failed_requests > 0 ? chalk.red(metrics.failed_requests.toString()) : '0'],
|
|
442
|
-
['', ''],
|
|
443
|
-
['Requests/sec', metrics.requests_per_second.toFixed(2)],
|
|
444
|
-
['', ''],
|
|
445
|
-
['Min Latency', `${metrics.min_latency_ms}ms`],
|
|
446
|
-
['Max Latency', `${metrics.max_latency_ms}ms`],
|
|
447
|
-
['Avg Latency', `${metrics.avg_latency_ms}ms`],
|
|
448
|
-
['p50 Latency', `${metrics.p50_latency_ms}ms`],
|
|
449
|
-
['p90 Latency', `${metrics.p90_latency_ms}ms`],
|
|
450
|
-
['p95 Latency', `${metrics.p95_latency_ms}ms`],
|
|
451
|
-
['p99 Latency', `${metrics.p99_latency_ms}ms`]
|
|
452
|
-
);
|
|
453
|
-
|
|
454
|
-
console.log(chalk.bold('Results'));
|
|
455
|
-
console.log(table.toString());
|
|
456
|
-
|
|
457
|
-
// Success rate
|
|
458
|
-
const successRate = metrics.success_rate * 100;
|
|
459
|
-
|
|
460
|
-
console.log();
|
|
461
|
-
if (successRate >= 99) {
|
|
462
|
-
console.log(chalk.green(`✓ Success rate: ${successRate.toFixed(2)}%`));
|
|
463
|
-
} else if (successRate >= 95) {
|
|
464
|
-
console.log(chalk.yellow(`⚠ Success rate: ${successRate.toFixed(2)}%`));
|
|
465
|
-
} else {
|
|
466
|
-
console.log(chalk.red(`✗ Success rate: ${successRate.toFixed(2)}%`));
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
468
|
function displayHistogram(results: StressRequestResult[]): void {
|
|
471
469
|
const successful = results.filter((r) => r.success);
|
|
472
470
|
if (successful.length === 0) return;
|
|
@@ -492,10 +490,10 @@ function displayHistogram(results: StressRequestResult[]): void {
|
|
|
492
490
|
const rangeEnd = (i + 1) * bucketSize;
|
|
493
491
|
const count = buckets[i];
|
|
494
492
|
const barLength = maxCount > 0 ? Math.round((count / maxCount) * 30) : 0;
|
|
495
|
-
const bar = '█'.repeat(barLength);
|
|
493
|
+
const bar = colors.highlight('█'.repeat(barLength));
|
|
496
494
|
|
|
497
495
|
console.log(
|
|
498
|
-
`${chalk.dim(`${rangeStart.toString().padStart(5)}-${rangeEnd.toString().padStart(5)}ms`)} │ ${
|
|
496
|
+
`${chalk.dim(`${rangeStart.toString().padStart(5)}-${rangeEnd.toString().padStart(5)}ms`)} │ ${bar} ${count}`
|
|
499
497
|
);
|
|
500
498
|
}
|
|
501
499
|
}
|
package/src/ui/colors.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consistent color scheme for CLI output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
export const colors = {
|
|
8
|
+
// Status colors
|
|
9
|
+
success: chalk.green,
|
|
10
|
+
error: chalk.red,
|
|
11
|
+
warning: chalk.yellow,
|
|
12
|
+
info: chalk.blue,
|
|
13
|
+
muted: chalk.gray,
|
|
14
|
+
|
|
15
|
+
// Semantic colors for test results
|
|
16
|
+
passed: chalk.green,
|
|
17
|
+
failed: chalk.red,
|
|
18
|
+
skipped: chalk.gray,
|
|
19
|
+
running: chalk.blue,
|
|
20
|
+
|
|
21
|
+
// UI elements
|
|
22
|
+
border: chalk.gray,
|
|
23
|
+
highlight: chalk.cyan,
|
|
24
|
+
label: chalk.bold,
|
|
25
|
+
value: chalk.white,
|
|
26
|
+
|
|
27
|
+
// Percentage thresholds
|
|
28
|
+
percentGood: chalk.green, // >= 90%
|
|
29
|
+
percentWarn: chalk.yellow, // >= 70%
|
|
30
|
+
percentBad: chalk.red, // < 70%
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns appropriate color function based on percentage value
|
|
35
|
+
*/
|
|
36
|
+
export function colorByPercentage(value: number): typeof chalk {
|
|
37
|
+
if (value >= 90) return colors.percentGood;
|
|
38
|
+
if (value >= 70) return colors.percentWarn;
|
|
39
|
+
return colors.percentBad;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Format a percentage value with appropriate coloring
|
|
44
|
+
*/
|
|
45
|
+
export function formatPercentage(value: number): string {
|
|
46
|
+
const color = colorByPercentage(value);
|
|
47
|
+
return color(`${value.toFixed(1)}%`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Status icons with colors
|
|
52
|
+
*/
|
|
53
|
+
export const icons = {
|
|
54
|
+
passed: chalk.green('✓'),
|
|
55
|
+
failed: chalk.red('✗'),
|
|
56
|
+
skipped: chalk.gray('○'),
|
|
57
|
+
running: chalk.blue('◉'),
|
|
58
|
+
warning: chalk.yellow('⚠'),
|
|
59
|
+
info: chalk.blue('ℹ'),
|
|
60
|
+
arrow: chalk.cyan('→'),
|
|
61
|
+
bullet: chalk.gray('•'),
|
|
62
|
+
};
|