@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/ui/errors.ts
ADDED
|
@@ -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
|
+
}
|
package/src/ui/index.ts
ADDED
|
@@ -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
|
+
}
|