@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
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Panel rendering for summary displays
3
+ */
4
+
5
+ import chalk from 'chalk';
6
+ import { formatPercentage, icons } from './colors.js';
7
+ import { isTTY, centerText, padText, formatDuration } from './utils.js';
8
+
9
+ export interface SummaryData {
10
+ passed: number;
11
+ failed: number;
12
+ skipped: number;
13
+ successRate: number;
14
+ duration: number;
15
+ title?: string;
16
+ }
17
+
18
+ export interface StressSummaryData {
19
+ totalRequests: number;
20
+ successfulRequests: number;
21
+ failedRequests: number;
22
+ successRate: number;
23
+ duration: number;
24
+ avgLatency: number;
25
+ p50Latency: number;
26
+ p95Latency: number;
27
+ p99Latency: number;
28
+ throughput: number;
29
+ }
30
+
31
+ export interface RedteamSummaryData {
32
+ totalCases: number;
33
+ safeResponses: number;
34
+ unsafeResponses: number;
35
+ blockedResponses: number;
36
+ errorResponses: number;
37
+ defenseRate: number;
38
+ severityBreakdown?: Record<string, number>;
39
+ }
40
+
41
+ /**
42
+ * Render a test results summary panel
43
+ */
44
+ export function renderSummaryPanel(data: SummaryData): string {
45
+ const title = data.title ?? 'TEST RESULTS';
46
+ const width = 55;
47
+
48
+ if (!isTTY) {
49
+ // Plain text format for CI/CD
50
+ return [
51
+ `=== ${title} ===`,
52
+ `Passed: ${data.passed} Failed: ${data.failed} Skipped: ${data.skipped}`,
53
+ `Success Rate: ${data.successRate.toFixed(1)}%`,
54
+ `Duration: ${formatDuration(data.duration)}`,
55
+ ].join('\n');
56
+ }
57
+
58
+ const border = '═'.repeat(width - 2);
59
+
60
+ const statsLine = ` ${icons.passed} Passed: ${data.passed} ${icons.failed} Failed: ${data.failed} ${icons.skipped} Skipped: ${data.skipped}`;
61
+
62
+ const lines = [
63
+ chalk.cyan(`╔${border}╗`),
64
+ chalk.cyan('║') + centerText(chalk.bold(title), width - 2) + chalk.cyan('║'),
65
+ chalk.cyan(`╠${border}╣`),
66
+ chalk.cyan('║') + padText(statsLine, width - 2) + chalk.cyan('║'),
67
+ chalk.cyan('║') +
68
+ padText(` Success Rate: ${formatPercentage(data.successRate)}`, width - 2) +
69
+ chalk.cyan('║'),
70
+ chalk.cyan('║') +
71
+ padText(` Duration: ${formatDuration(data.duration)}`, width - 2) +
72
+ chalk.cyan('║'),
73
+ chalk.cyan(`╚${border}╝`),
74
+ ];
75
+
76
+ return lines.join('\n');
77
+ }
78
+
79
+ /**
80
+ * Render a stress test summary panel
81
+ */
82
+ export function renderStressSummaryPanel(data: StressSummaryData): string {
83
+ const width = 55;
84
+
85
+ if (!isTTY) {
86
+ return [
87
+ '=== STRESS TEST RESULTS ===',
88
+ `Total Requests: ${data.totalRequests}`,
89
+ `Successful: ${data.successfulRequests} (${data.successRate.toFixed(1)}%)`,
90
+ `Failed: ${data.failedRequests}`,
91
+ `Duration: ${formatDuration(data.duration)}`,
92
+ `Throughput: ${data.throughput.toFixed(1)} req/s`,
93
+ `Latency: avg=${data.avgLatency.toFixed(0)}ms p50=${data.p50Latency.toFixed(0)}ms p95=${data.p95Latency.toFixed(0)}ms p99=${data.p99Latency.toFixed(0)}ms`,
94
+ ].join('\n');
95
+ }
96
+
97
+ const border = '═'.repeat(width - 2);
98
+
99
+ const lines = [
100
+ chalk.cyan(`╔${border}╗`),
101
+ chalk.cyan('║') + centerText(chalk.bold('STRESS TEST RESULTS'), width - 2) + chalk.cyan('║'),
102
+ chalk.cyan(`╠${border}╣`),
103
+ chalk.cyan('║') +
104
+ padText(` Total Requests: ${data.totalRequests}`, width - 2) +
105
+ chalk.cyan('║'),
106
+ chalk.cyan('║') +
107
+ padText(
108
+ ` ${icons.passed} Successful: ${data.successfulRequests} ${icons.failed} Failed: ${data.failedRequests}`,
109
+ width - 2
110
+ ) +
111
+ chalk.cyan('║'),
112
+ chalk.cyan('║') +
113
+ padText(` Success Rate: ${formatPercentage(data.successRate)}`, width - 2) +
114
+ chalk.cyan('║'),
115
+ chalk.cyan(`╠${border}╣`),
116
+ chalk.cyan('║') + centerText(chalk.dim('Performance Metrics'), width - 2) + chalk.cyan('║'),
117
+ chalk.cyan(`╠${border}╣`),
118
+ chalk.cyan('║') +
119
+ padText(` Throughput: ${chalk.bold(data.throughput.toFixed(1))} req/s`, width - 2) +
120
+ chalk.cyan('║'),
121
+ chalk.cyan('║') +
122
+ padText(` Duration: ${formatDuration(data.duration)}`, width - 2) +
123
+ chalk.cyan('║'),
124
+ chalk.cyan(`╠${border}╣`),
125
+ chalk.cyan('║') + centerText(chalk.dim('Latency'), width - 2) + chalk.cyan('║'),
126
+ chalk.cyan(`╠${border}╣`),
127
+ chalk.cyan('║') +
128
+ padText(` Average: ${data.avgLatency.toFixed(0)}ms`, width - 2) +
129
+ chalk.cyan('║'),
130
+ chalk.cyan('║') +
131
+ padText(
132
+ ` p50: ${data.p50Latency.toFixed(0)}ms p95: ${data.p95Latency.toFixed(0)}ms p99: ${data.p99Latency.toFixed(0)}ms`,
133
+ width - 2
134
+ ) +
135
+ chalk.cyan('║'),
136
+ chalk.cyan(`╚${border}╝`),
137
+ ];
138
+
139
+ return lines.join('\n');
140
+ }
141
+
142
+ /**
143
+ * Render a red team summary panel
144
+ */
145
+ export function renderRedteamSummaryPanel(data: RedteamSummaryData): string {
146
+ const width = 55;
147
+
148
+ if (!isTTY) {
149
+ return [
150
+ '=== RED TEAM RESULTS ===',
151
+ `Total Cases: ${data.totalCases}`,
152
+ `Safe: ${data.safeResponses} Unsafe: ${data.unsafeResponses} Blocked: ${data.blockedResponses} Errors: ${data.errorResponses}`,
153
+ `Defense Rate: ${data.defenseRate.toFixed(1)}%`,
154
+ ].join('\n');
155
+ }
156
+
157
+ const border = '═'.repeat(width - 2);
158
+
159
+ const defenseColor =
160
+ data.defenseRate >= 90 ? chalk.green : data.defenseRate >= 70 ? chalk.yellow : chalk.red;
161
+
162
+ const lines = [
163
+ chalk.magenta(`╔${border}╗`),
164
+ chalk.magenta('║') + centerText(chalk.bold('RED TEAM RESULTS'), width - 2) + chalk.magenta('║'),
165
+ chalk.magenta(`╠${border}╣`),
166
+ chalk.magenta('║') +
167
+ padText(` Total Cases: ${data.totalCases}`, width - 2) +
168
+ chalk.magenta('║'),
169
+ chalk.magenta('║') +
170
+ padText(
171
+ ` ${chalk.green('✓')} Safe: ${data.safeResponses} ${chalk.red('✗')} Unsafe: ${data.unsafeResponses}`,
172
+ width - 2
173
+ ) +
174
+ chalk.magenta('║'),
175
+ chalk.magenta('║') +
176
+ padText(
177
+ ` ${chalk.cyan('⊘')} Blocked: ${data.blockedResponses} ${chalk.yellow('!')} Errors: ${data.errorResponses}`,
178
+ width - 2
179
+ ) +
180
+ chalk.magenta('║'),
181
+ chalk.magenta(`╠${border}╣`),
182
+ chalk.magenta('║') +
183
+ padText(` Defense Rate: ${defenseColor(data.defenseRate.toFixed(1) + '%')}`, width - 2) +
184
+ chalk.magenta('║'),
185
+ chalk.magenta(`╚${border}╝`),
186
+ ];
187
+
188
+ return lines.join('\n');
189
+ }
190
+
191
+ /**
192
+ * Render a simple info box
193
+ */
194
+ export function renderInfoBox(title: string, lines: string[]): string {
195
+ const width = 55;
196
+
197
+ if (!isTTY) {
198
+ return [`=== ${title} ===`, ...lines].join('\n');
199
+ }
200
+
201
+ const border = '─'.repeat(width - 2);
202
+
203
+ const result = [
204
+ chalk.gray(`┌─ ${chalk.bold(title)} ${border.slice(title.length + 3)}┐`),
205
+ chalk.gray('│') + ' '.repeat(width - 2) + chalk.gray('│'),
206
+ ];
207
+
208
+ for (const line of lines) {
209
+ result.push(chalk.gray('│') + padText(` ${line}`, width - 2) + chalk.gray('│'));
210
+ }
211
+
212
+ result.push(chalk.gray('│') + ' '.repeat(width - 2) + chalk.gray('│'));
213
+ result.push(chalk.gray(`└${border}┘`));
214
+
215
+ return result.join('\n');
216
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Progress bar rendering utilities
3
+ */
4
+
5
+ import chalk from 'chalk';
6
+ import { isTTY, stripAnsi } from './utils.js';
7
+
8
+ export interface ProgressBarOptions {
9
+ /** Width of the progress bar in characters */
10
+ width?: number;
11
+ /** Character for filled portion */
12
+ filledChar?: string;
13
+ /** Character for empty portion */
14
+ emptyChar?: string;
15
+ /** Show percentage */
16
+ showPercentage?: boolean;
17
+ /** Show count (current/total) */
18
+ showCount?: boolean;
19
+ /** Label to show before the bar */
20
+ label?: string;
21
+ }
22
+
23
+ const defaultOptions: Required<ProgressBarOptions> = {
24
+ width: 20,
25
+ filledChar: '█',
26
+ emptyChar: '░',
27
+ showPercentage: true,
28
+ showCount: true,
29
+ label: '',
30
+ };
31
+
32
+ /**
33
+ * Render a progress bar
34
+ *
35
+ * @example
36
+ * renderProgressBar(3, 10)
37
+ * // "████████░░░░░░░░░░░░ 3/10 (30%)"
38
+ *
39
+ * renderProgressBar(3, 10, { label: 'Running tests' })
40
+ * // "Running tests ████████░░░░░░░░░░░░ 3/10 (30%)"
41
+ */
42
+ export function renderProgressBar(
43
+ current: number,
44
+ total: number,
45
+ options: ProgressBarOptions = {}
46
+ ): string {
47
+ const opts = { ...defaultOptions, ...options };
48
+ const percentage = total > 0 ? Math.round((current / total) * 100) : 0;
49
+ const filled = total > 0 ? Math.round((current / total) * opts.width) : 0;
50
+ const empty = opts.width - filled;
51
+
52
+ // For non-TTY, use simple text format
53
+ if (!isTTY) {
54
+ const parts: string[] = [];
55
+ if (opts.label) parts.push(opts.label);
56
+ if (opts.showCount) parts.push(`${current}/${total}`);
57
+ if (opts.showPercentage) parts.push(`(${percentage}%)`);
58
+ return parts.join(' ');
59
+ }
60
+
61
+ const bar =
62
+ chalk.green(opts.filledChar.repeat(filled)) + chalk.gray(opts.emptyChar.repeat(empty));
63
+
64
+ const parts: string[] = [];
65
+ if (opts.label) parts.push(opts.label);
66
+ parts.push(bar);
67
+ if (opts.showCount) parts.push(`${current}/${total}`);
68
+ if (opts.showPercentage) parts.push(`(${percentage}%)`);
69
+
70
+ return parts.join(' ');
71
+ }
72
+
73
+ /**
74
+ * Create a progress bar manager for updating in-place
75
+ */
76
+ export class ProgressBar {
77
+ private current = 0;
78
+ private total: number;
79
+ private options: Required<ProgressBarOptions>;
80
+ private lastOutput = '';
81
+
82
+ constructor(total: number, options: ProgressBarOptions = {}) {
83
+ this.total = total;
84
+ this.options = { ...defaultOptions, ...options };
85
+ }
86
+
87
+ /**
88
+ * Update progress and render
89
+ */
90
+ update(current: number): void {
91
+ this.current = current;
92
+ this.render();
93
+ }
94
+
95
+ /**
96
+ * Increment progress by one
97
+ */
98
+ increment(): void {
99
+ this.update(this.current + 1);
100
+ }
101
+
102
+ /**
103
+ * Render the progress bar
104
+ */
105
+ private render(): void {
106
+ const output = renderProgressBar(this.current, this.total, this.options);
107
+
108
+ if (isTTY) {
109
+ // Clear previous line and write new output
110
+ const clearLength = stripAnsi(this.lastOutput).length;
111
+ process.stdout.write('\r' + ' '.repeat(clearLength) + '\r');
112
+ process.stdout.write(output);
113
+ this.lastOutput = output;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Complete the progress bar and move to next line
119
+ */
120
+ complete(): void {
121
+ this.update(this.total);
122
+ if (isTTY) {
123
+ process.stdout.write('\n');
124
+ }
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Render a compact inline progress indicator
130
+ * Useful for spinners or status lines
131
+ */
132
+ export function renderInlineProgress(current: number, total: number, label?: string): string {
133
+ const percentage = total > 0 ? Math.round((current / total) * 100) : 0;
134
+
135
+ if (label) {
136
+ return `${label}: ${current}/${total} (${percentage}%)`;
137
+ }
138
+ return `${current}/${total} (${percentage}%)`;
139
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * UI utility functions for terminal handling
3
+ */
4
+
5
+ /**
6
+ * Check if stdout is a TTY (interactive terminal)
7
+ */
8
+ export const isTTY = process.stdout.isTTY ?? false;
9
+
10
+ /**
11
+ * Get terminal width, with fallback for non-TTY environments
12
+ */
13
+ export function getTerminalWidth(): number {
14
+ return process.stdout.columns ?? 80;
15
+ }
16
+
17
+ /**
18
+ * Render different output based on TTY availability
19
+ * @param ttyOutput Output for interactive terminals
20
+ * @param plainOutput Output for non-TTY (CI/CD) environments
21
+ */
22
+ export function renderConditional(ttyOutput: string, plainOutput: string): string {
23
+ return isTTY ? ttyOutput : plainOutput;
24
+ }
25
+
26
+ /**
27
+ * Center text within a given width
28
+ */
29
+ export function centerText(text: string, width: number): string {
30
+ const visibleLength = stripAnsi(text).length;
31
+ const padding = Math.max(0, Math.floor((width - visibleLength) / 2));
32
+ const rightPadding = width - padding - visibleLength;
33
+ return ' '.repeat(padding) + text + ' '.repeat(Math.max(0, rightPadding));
34
+ }
35
+
36
+ /**
37
+ * Pad text to a specific width (accounting for ANSI codes)
38
+ */
39
+ export function padText(
40
+ text: string,
41
+ width: number,
42
+ align: 'left' | 'right' | 'center' = 'left'
43
+ ): string {
44
+ const visibleLength = stripAnsi(text).length;
45
+ const paddingNeeded = Math.max(0, width - visibleLength);
46
+
47
+ if (align === 'center') {
48
+ return centerText(text, width);
49
+ } else if (align === 'right') {
50
+ return ' '.repeat(paddingNeeded) + text;
51
+ }
52
+ return text + ' '.repeat(paddingNeeded);
53
+ }
54
+
55
+ /**
56
+ * Strip ANSI escape codes from string (for length calculations)
57
+ */
58
+ export function stripAnsi(str: string): string {
59
+ // eslint-disable-next-line no-control-regex
60
+ return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
61
+ }
62
+
63
+ /**
64
+ * Truncate text to fit within a width, adding ellipsis if needed
65
+ */
66
+ export function truncate(text: string, maxWidth: number): string {
67
+ const visibleLength = stripAnsi(text).length;
68
+ if (visibleLength <= maxWidth) return text;
69
+ return text.slice(0, maxWidth - 1) + '…';
70
+ }
71
+
72
+ /**
73
+ * Format duration in a human-readable way
74
+ */
75
+ export function formatDuration(ms: number): string {
76
+ if (ms < 1000) return `${ms.toFixed(0)}ms`;
77
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
78
+ const minutes = Math.floor(ms / 60000);
79
+ const seconds = ((ms % 60000) / 1000).toFixed(0);
80
+ return `${minutes}m ${seconds}s`;
81
+ }
82
+
83
+ /**
84
+ * Format a number with commas for readability
85
+ */
86
+ export function formatNumber(num: number): string {
87
+ return num.toLocaleString();
88
+ }