@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -0
  3. package/dist/index.js +19129 -20009
  4. package/dist/src/commands/compare.d.ts.map +1 -1
  5. package/dist/src/commands/history.d.ts.map +1 -1
  6. package/dist/src/commands/init.d.ts.map +1 -1
  7. package/dist/src/commands/redteam.d.ts.map +1 -1
  8. package/dist/src/commands/report.d.ts.map +1 -1
  9. package/dist/src/commands/run.d.ts.map +1 -1
  10. package/dist/src/commands/stress.d.ts.map +1 -1
  11. package/dist/src/ui/colors.d.ts +44 -0
  12. package/dist/src/ui/colors.d.ts.map +1 -0
  13. package/dist/src/ui/errors.d.ts +39 -0
  14. package/dist/src/ui/errors.d.ts.map +1 -0
  15. package/dist/src/ui/index.d.ts +16 -0
  16. package/dist/src/ui/index.d.ts.map +1 -0
  17. package/dist/src/ui/live-status.d.ts +82 -0
  18. package/dist/src/ui/live-status.d.ts.map +1 -0
  19. package/dist/src/ui/panels.d.ts +49 -0
  20. package/dist/src/ui/panels.d.ts.map +1 -0
  21. package/dist/src/ui/progress.d.ts +60 -0
  22. package/dist/src/ui/progress.d.ts.map +1 -0
  23. package/dist/src/ui/utils.d.ts +42 -0
  24. package/dist/src/ui/utils.d.ts.map +1 -0
  25. package/package.json +6 -6
  26. package/src/__tests__/helpers/index.ts +6 -0
  27. package/src/__tests__/helpers/mock-adapter.ts +90 -0
  28. package/src/__tests__/helpers/test-utils.ts +205 -0
  29. package/src/__tests__/integration/compare-command.test.ts +236 -0
  30. package/src/__tests__/integration/config.test.ts +125 -0
  31. package/src/__tests__/integration/history-command.test.ts +251 -0
  32. package/src/__tests__/integration/init-command.test.ts +177 -0
  33. package/src/__tests__/integration/report-command.test.ts +245 -0
  34. package/src/__tests__/integration/ui.test.ts +230 -0
  35. package/src/commands/compare.ts +158 -49
  36. package/src/commands/history.ts +131 -30
  37. package/src/commands/init.ts +181 -21
  38. package/src/commands/redteam.ts +118 -75
  39. package/src/commands/report.ts +29 -14
  40. package/src/commands/run.ts +86 -66
  41. package/src/commands/stress.ts +61 -63
  42. package/src/ui/colors.ts +62 -0
  43. package/src/ui/errors.ts +248 -0
  44. package/src/ui/index.ts +42 -0
  45. package/src/ui/live-status.ts +259 -0
  46. package/src/ui/panels.ts +216 -0
  47. package/src/ui/progress.ts +139 -0
  48. package/src/ui/utils.ts +88 -0
@@ -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 ora from 'ora';
15
- import { loadConfig } from '../config/loader';
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 = ora('Loading configuration...').start();
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
- const statusIcon = caseResult.ok ? chalk.green('✓') : chalk.red('✗');
156
+ completedCases++;
157
+
158
+ const statusIcon = caseResult.ok ? icons.passed : icons.failed;
138
159
  const scoreStr = `(${(caseResult.score * 100).toFixed(0)}%)`;
139
- console.log(`${statusIcon} ${caseResult.id} ${chalk.dim(scoreStr)}`);
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
- displaySummary(result.manifest.metrics, result.manifest.run_id, result.manifest.redaction);
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
- console.error(chalk.red('Error:'), (error as Error).message);
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.error((error as Error).stack);
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
- }
@@ -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 ora from 'ora';
24
- import { loadConfig } from '../config/loader';
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 = ora('Loading configuration...').start();
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
- console.log(chalk.bold('Stress Test Configuration'));
137
- console.log(chalk.dim(`Concurrency: ${concurrency}`));
138
- console.log(chalk.dim(`Duration: ${durationSec}s`));
139
- console.log(chalk.dim(`Ramp-up: ${rampUpSec}s`));
146
+ const configLines = [
147
+ `Concurrency: ${concurrency}`,
148
+ `Duration: ${durationSec}s`,
149
+ `Ramp-up: ${rampUpSec}s`,
150
+ ];
140
151
  if (maxRequests) {
141
- console.log(chalk.dim(`Max requests: ${maxRequests}`));
152
+ configLines.push(`Max requests: ${maxRequests}`);
142
153
  }
143
154
  if (options.redact) {
144
- console.log(
145
- chalk.dim(
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
- spinner.text = `Running stress test... ${completed} completed, ${active} active`;
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 stats
236
- displayStats(metrics, runId);
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
- console.error(chalk.red('Error:'), (error as Error).message);
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`)} │ ${chalk.cyan(bar)} ${count}`
496
+ `${chalk.dim(`${rangeStart.toString().padStart(5)}-${rangeEnd.toString().padStart(5)}ms`)} │ ${bar} ${count}`
499
497
  );
500
498
  }
501
499
  }
@@ -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
+ };