@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,248 @@
1
+ /**
2
+ * Enhanced error display formatting
3
+ */
4
+
5
+ import chalk from 'chalk';
6
+ import { isTTY, padText, truncate } from './utils.js';
7
+
8
+ export interface ErrorContext {
9
+ /** Main error title */
10
+ title: string;
11
+ /** Reason for the error */
12
+ reason: string;
13
+ /** Actionable suggestions for the user */
14
+ suggestions?: string[];
15
+ /** Additional context details */
16
+ details?: Record<string, string>;
17
+ /** Error code (if applicable) */
18
+ code?: string;
19
+ }
20
+
21
+ /**
22
+ * Render an enhanced error panel with context and suggestions
23
+ *
24
+ * @example
25
+ * renderError({
26
+ * title: 'Failed to connect to Azure OpenAI',
27
+ * reason: 'Invalid API key or resource not found',
28
+ * suggestions: [
29
+ * 'Check AZURE_OPENAI_API_KEY is set correctly',
30
+ * 'Verify resource name: my-openai-resource',
31
+ * "Run 'artemiskit init' to reconfigure"
32
+ * ]
33
+ * })
34
+ */
35
+ export function renderError(ctx: ErrorContext): string {
36
+ const width = 60;
37
+
38
+ if (!isTTY) {
39
+ // Plain text format for CI/CD
40
+ const lines = [`ERROR: ${ctx.title}`, `Reason: ${ctx.reason}`];
41
+
42
+ if (ctx.code) {
43
+ lines.push(`Code: ${ctx.code}`);
44
+ }
45
+
46
+ if (ctx.details) {
47
+ for (const [key, value] of Object.entries(ctx.details)) {
48
+ lines.push(`${key}: ${value}`);
49
+ }
50
+ }
51
+
52
+ if (ctx.suggestions?.length) {
53
+ lines.push('Suggestions:');
54
+ for (const suggestion of ctx.suggestions) {
55
+ lines.push(` - ${suggestion}`);
56
+ }
57
+ }
58
+
59
+ return lines.join('\n');
60
+ }
61
+
62
+ const innerWidth = width - 4;
63
+ const lines: string[] = [];
64
+
65
+ // Top border with ERROR label
66
+ const topBorder = '─'.repeat(width - 10);
67
+ lines.push(chalk.red(`┌─ ERROR ${topBorder}┐`));
68
+ lines.push(chalk.red('│') + ' '.repeat(width - 2) + chalk.red('│'));
69
+
70
+ // Title
71
+ lines.push(chalk.red('│') + padText(` ${chalk.bold(ctx.title)}`, width - 2) + chalk.red('│'));
72
+ lines.push(chalk.red('│') + ' '.repeat(width - 2) + chalk.red('│'));
73
+
74
+ // Reason
75
+ const reasonText = truncate(ctx.reason, innerWidth - 10);
76
+ lines.push(chalk.red('│') + padText(` Reason: ${reasonText}`, width - 2) + chalk.red('│'));
77
+
78
+ // Code (if present)
79
+ if (ctx.code) {
80
+ lines.push(
81
+ chalk.red('│') + padText(` Code: ${chalk.dim(ctx.code)}`, width - 2) + chalk.red('│')
82
+ );
83
+ }
84
+
85
+ // Details (if present)
86
+ if (ctx.details && Object.keys(ctx.details).length > 0) {
87
+ lines.push(chalk.red('│') + ' '.repeat(width - 2) + chalk.red('│'));
88
+ for (const [key, value] of Object.entries(ctx.details)) {
89
+ const detailText = truncate(`${key}: ${value}`, innerWidth);
90
+ lines.push(
91
+ chalk.red('│') + padText(` ${chalk.dim(detailText)}`, width - 2) + chalk.red('│')
92
+ );
93
+ }
94
+ }
95
+
96
+ // Suggestions
97
+ if (ctx.suggestions?.length) {
98
+ lines.push(chalk.red('│') + ' '.repeat(width - 2) + chalk.red('│'));
99
+ lines.push(chalk.red('│') + padText(' Suggestions:', width - 2) + chalk.red('│'));
100
+ for (const suggestion of ctx.suggestions) {
101
+ const suggestionText = truncate(suggestion, innerWidth - 4);
102
+ lines.push(
103
+ chalk.red('│') +
104
+ padText(` ${chalk.yellow('•')} ${suggestionText}`, width - 2) +
105
+ chalk.red('│')
106
+ );
107
+ }
108
+ }
109
+
110
+ // Bottom border
111
+ lines.push(chalk.red('│') + ' '.repeat(width - 2) + chalk.red('│'));
112
+ lines.push(chalk.red(`└${'─'.repeat(width - 2)}┘`));
113
+
114
+ return lines.join('\n');
115
+ }
116
+
117
+ /**
118
+ * Render a warning panel (less severe than error)
119
+ */
120
+ export function renderWarning(title: string, message: string, suggestions?: string[]): string {
121
+ const width = 60;
122
+
123
+ if (!isTTY) {
124
+ const lines = [`WARNING: ${title}`, message];
125
+ if (suggestions?.length) {
126
+ lines.push('Suggestions:');
127
+ for (const suggestion of suggestions) {
128
+ lines.push(` - ${suggestion}`);
129
+ }
130
+ }
131
+ return lines.join('\n');
132
+ }
133
+
134
+ const innerWidth = width - 4;
135
+ const lines: string[] = [];
136
+
137
+ const topBorder = '─'.repeat(width - 12);
138
+ lines.push(chalk.yellow(`┌─ WARNING ${topBorder}┐`));
139
+ lines.push(chalk.yellow('│') + ' '.repeat(width - 2) + chalk.yellow('│'));
140
+ lines.push(chalk.yellow('│') + padText(` ${chalk.bold(title)}`, width - 2) + chalk.yellow('│'));
141
+ lines.push(chalk.yellow('│') + ' '.repeat(width - 2) + chalk.yellow('│'));
142
+
143
+ // Word wrap message if needed
144
+ const words = message.split(' ');
145
+ let currentLine = ' ';
146
+ for (const word of words) {
147
+ if (currentLine.length + word.length + 1 <= innerWidth) {
148
+ currentLine += (currentLine.length > 2 ? ' ' : '') + word;
149
+ } else {
150
+ lines.push(chalk.yellow('│') + padText(currentLine, width - 2) + chalk.yellow('│'));
151
+ currentLine = ' ' + word;
152
+ }
153
+ }
154
+ if (currentLine.length > 2) {
155
+ lines.push(chalk.yellow('│') + padText(currentLine, width - 2) + chalk.yellow('│'));
156
+ }
157
+
158
+ if (suggestions?.length) {
159
+ lines.push(chalk.yellow('│') + ' '.repeat(width - 2) + chalk.yellow('│'));
160
+ for (const suggestion of suggestions) {
161
+ const suggestionText = truncate(suggestion, innerWidth - 4);
162
+ lines.push(
163
+ chalk.yellow('│') +
164
+ padText(` ${chalk.dim('•')} ${suggestionText}`, width - 2) +
165
+ chalk.yellow('│')
166
+ );
167
+ }
168
+ }
169
+
170
+ lines.push(chalk.yellow('│') + ' '.repeat(width - 2) + chalk.yellow('│'));
171
+ lines.push(chalk.yellow(`└${'─'.repeat(width - 2)}┘`));
172
+
173
+ return lines.join('\n');
174
+ }
175
+
176
+ /**
177
+ * Format provider-specific error messages with suggestions
178
+ */
179
+ export function getProviderErrorContext(provider: string, error: Error): ErrorContext {
180
+ const errorMessage = error.message.toLowerCase();
181
+
182
+ // OpenAI errors
183
+ if (provider === 'openai') {
184
+ if (errorMessage.includes('api key') || errorMessage.includes('authentication')) {
185
+ return {
186
+ title: 'OpenAI Authentication Failed',
187
+ reason: error.message,
188
+ suggestions: [
189
+ 'Check that OPENAI_API_KEY is set correctly',
190
+ 'Verify the API key is valid and not expired',
191
+ "Run 'artemiskit init' to reconfigure",
192
+ ],
193
+ };
194
+ }
195
+ if (errorMessage.includes('rate limit')) {
196
+ return {
197
+ title: 'OpenAI Rate Limit Exceeded',
198
+ reason: error.message,
199
+ suggestions: [
200
+ 'Wait a moment and try again',
201
+ 'Reduce concurrency with --concurrency flag',
202
+ 'Consider upgrading your OpenAI plan',
203
+ ],
204
+ };
205
+ }
206
+ }
207
+
208
+ // Azure OpenAI errors
209
+ if (provider === 'azure-openai') {
210
+ if (errorMessage.includes('resource') || errorMessage.includes('deployment')) {
211
+ return {
212
+ title: 'Azure OpenAI Resource Error',
213
+ reason: error.message,
214
+ suggestions: [
215
+ 'Verify AZURE_OPENAI_RESOURCE_NAME is correct',
216
+ 'Check AZURE_OPENAI_DEPLOYMENT_NAME matches your deployment',
217
+ 'Ensure the deployment exists in your Azure portal',
218
+ ],
219
+ };
220
+ }
221
+ }
222
+
223
+ // Anthropic errors
224
+ if (provider === 'anthropic') {
225
+ if (errorMessage.includes('api key') || errorMessage.includes('authentication')) {
226
+ return {
227
+ title: 'Anthropic Authentication Failed',
228
+ reason: error.message,
229
+ suggestions: [
230
+ 'Check that ANTHROPIC_API_KEY is set correctly',
231
+ 'Verify the API key is valid',
232
+ "Run 'artemiskit init' to reconfigure",
233
+ ],
234
+ };
235
+ }
236
+ }
237
+
238
+ // Generic fallback
239
+ return {
240
+ title: `${provider} Error`,
241
+ reason: error.message,
242
+ suggestions: [
243
+ 'Check your provider configuration',
244
+ 'Verify environment variables are set correctly',
245
+ "Run 'artemiskit init' to reconfigure",
246
+ ],
247
+ };
248
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * CLI UI Components
3
+ *
4
+ * Centralized UI utilities for consistent CLI output formatting.
5
+ */
6
+
7
+ // Color scheme and icons
8
+ export { colors, icons, colorByPercentage, formatPercentage } from './colors.js';
9
+
10
+ // Utility functions
11
+ export {
12
+ isTTY,
13
+ getTerminalWidth,
14
+ renderConditional,
15
+ centerText,
16
+ padText,
17
+ stripAnsi,
18
+ truncate,
19
+ formatDuration,
20
+ formatNumber,
21
+ } from './utils.js';
22
+
23
+ // Progress bars
24
+ export { renderProgressBar, ProgressBar, renderInlineProgress } from './progress.js';
25
+ export type { ProgressBarOptions } from './progress.js';
26
+
27
+ // Summary panels
28
+ export {
29
+ renderSummaryPanel,
30
+ renderStressSummaryPanel,
31
+ renderRedteamSummaryPanel,
32
+ renderInfoBox,
33
+ } from './panels.js';
34
+ export type { SummaryData, StressSummaryData, RedteamSummaryData } from './panels.js';
35
+
36
+ // Error display
37
+ export { renderError, renderWarning, getProviderErrorContext } from './errors.js';
38
+ export type { ErrorContext } from './errors.js';
39
+
40
+ // Live status tracking
41
+ export { LiveTestStatus, Spinner, createSpinner } from './live-status.js';
42
+ export type { TestStatus } from './live-status.js';
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Real-time test status updates
3
+ */
4
+
5
+ import chalk from 'chalk';
6
+ import ora, { type Ora } from 'ora';
7
+ import { isTTY } from './utils.js';
8
+ import { icons } from './colors.js';
9
+ import { renderProgressBar } from './progress.js';
10
+
11
+ export type TestStatus = 'pending' | 'running' | 'passed' | 'failed' | 'skipped';
12
+
13
+ interface TestResult {
14
+ name: string;
15
+ status: TestStatus;
16
+ duration?: number;
17
+ error?: string;
18
+ }
19
+
20
+ /**
21
+ * Live test status tracker with real-time updates
22
+ */
23
+ export class LiveTestStatus {
24
+ private spinner: Ora;
25
+ private results: Map<string, TestResult> = new Map();
26
+ private total: number;
27
+ private startTime: number = Date.now();
28
+
29
+ constructor(total: number) {
30
+ this.total = total;
31
+ this.spinner = ora({
32
+ spinner: 'dots',
33
+ color: 'cyan',
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Start tracking a test
39
+ */
40
+ start(testId: string, testName: string): void {
41
+ this.results.set(testId, { name: testName, status: 'running' });
42
+
43
+ if (isTTY) {
44
+ this.spinner.start(this.formatStatus());
45
+ } else {
46
+ console.log(`Running: ${testName}`);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Mark a test as passed
52
+ */
53
+ pass(testId: string, duration?: number): void {
54
+ const result = this.results.get(testId);
55
+ if (result) {
56
+ result.status = 'passed';
57
+ result.duration = duration;
58
+ }
59
+ this.updateSpinner();
60
+ }
61
+
62
+ /**
63
+ * Mark a test as failed
64
+ */
65
+ fail(testId: string, error?: string, duration?: number): void {
66
+ const result = this.results.get(testId);
67
+ if (result) {
68
+ result.status = 'failed';
69
+ result.error = error;
70
+ result.duration = duration;
71
+ }
72
+ this.updateSpinner();
73
+ }
74
+
75
+ /**
76
+ * Mark a test as skipped
77
+ */
78
+ skip(testId: string): void {
79
+ const result = this.results.get(testId);
80
+ if (result) {
81
+ result.status = 'skipped';
82
+ }
83
+ this.updateSpinner();
84
+ }
85
+
86
+ /**
87
+ * Update spinner text
88
+ */
89
+ private updateSpinner(): void {
90
+ if (isTTY) {
91
+ this.spinner.text = this.formatStatus();
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Format current status line
97
+ */
98
+ private formatStatus(): string {
99
+ const passed = this.countByStatus('passed');
100
+ const failed = this.countByStatus('failed');
101
+ const running = this.countByStatus('running');
102
+ const completed = passed + failed + this.countByStatus('skipped');
103
+
104
+ const progressBar = renderProgressBar(completed, this.total, {
105
+ width: 15,
106
+ showPercentage: false,
107
+ showCount: false,
108
+ });
109
+
110
+ return `${progressBar} ${chalk.green(passed)} passed, ${chalk.red(failed)} failed, ${chalk.blue(running)} running`;
111
+ }
112
+
113
+ /**
114
+ * Count results by status
115
+ */
116
+ private countByStatus(status: TestStatus): number {
117
+ return [...this.results.values()].filter((r) => r.status === status).length;
118
+ }
119
+
120
+ /**
121
+ * Complete tracking and print final status
122
+ */
123
+ complete(): void {
124
+ if (isTTY) {
125
+ this.spinner.stop();
126
+ }
127
+ this.printFinalStatus();
128
+ }
129
+
130
+ /**
131
+ * Print final status for all tests
132
+ */
133
+ private printFinalStatus(): void {
134
+ console.log('');
135
+
136
+ for (const [_, result] of this.results) {
137
+ const icon = this.getStatusIcon(result.status);
138
+ const duration = result.duration ? chalk.dim(` (${result.duration.toFixed(0)}ms)`) : '';
139
+ console.log(`${icon} ${result.name}${duration}`);
140
+
141
+ if (result.status === 'failed' && result.error) {
142
+ console.log(chalk.red(` ${result.error}`));
143
+ }
144
+ }
145
+
146
+ console.log('');
147
+ }
148
+
149
+ /**
150
+ * Get icon for status
151
+ */
152
+ private getStatusIcon(status: TestStatus): string {
153
+ switch (status) {
154
+ case 'passed':
155
+ return icons.passed;
156
+ case 'failed':
157
+ return icons.failed;
158
+ case 'skipped':
159
+ return icons.skipped;
160
+ case 'running':
161
+ return icons.running;
162
+ default:
163
+ return chalk.gray('○');
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Get summary statistics
169
+ */
170
+ getSummary(): { passed: number; failed: number; skipped: number; duration: number } {
171
+ return {
172
+ passed: this.countByStatus('passed'),
173
+ failed: this.countByStatus('failed'),
174
+ skipped: this.countByStatus('skipped'),
175
+ duration: Date.now() - this.startTime,
176
+ };
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Simple spinner wrapper with consistent styling
182
+ */
183
+ export class Spinner {
184
+ private spinner: Ora;
185
+
186
+ constructor(text?: string) {
187
+ this.spinner = ora({
188
+ text,
189
+ spinner: 'dots',
190
+ color: 'cyan',
191
+ });
192
+ }
193
+
194
+ start(text?: string): this {
195
+ if (isTTY) {
196
+ this.spinner.start(text);
197
+ } else if (text) {
198
+ console.log(text);
199
+ }
200
+ return this;
201
+ }
202
+
203
+ update(text: string): this {
204
+ if (isTTY) {
205
+ this.spinner.text = text;
206
+ }
207
+ return this;
208
+ }
209
+
210
+ succeed(text?: string): this {
211
+ if (isTTY) {
212
+ this.spinner.succeed(text);
213
+ } else if (text) {
214
+ console.log(`${icons.passed} ${text}`);
215
+ }
216
+ return this;
217
+ }
218
+
219
+ fail(text?: string): this {
220
+ if (isTTY) {
221
+ this.spinner.fail(text);
222
+ } else if (text) {
223
+ console.log(`${icons.failed} ${text}`);
224
+ }
225
+ return this;
226
+ }
227
+
228
+ warn(text?: string): this {
229
+ if (isTTY) {
230
+ this.spinner.warn(text);
231
+ } else if (text) {
232
+ console.log(`${icons.warning} ${text}`);
233
+ }
234
+ return this;
235
+ }
236
+
237
+ info(text?: string): this {
238
+ if (isTTY) {
239
+ this.spinner.info(text);
240
+ } else if (text) {
241
+ console.log(`${icons.info} ${text}`);
242
+ }
243
+ return this;
244
+ }
245
+
246
+ stop(): this {
247
+ if (isTTY) {
248
+ this.spinner.stop();
249
+ }
250
+ return this;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Create a new spinner instance
256
+ */
257
+ export function createSpinner(text?: string): Spinner {
258
+ return new Spinner(text);
259
+ }