@artemiskit/cli 0.2.3 → 0.2.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,wBAAgB,SAAS,IAAI,OAAO,CAwCnC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAapC,wBAAgB,SAAS,IAAI,OAAO,CAyCnC"}
@@ -1 +1 @@
1
- {"version":3,"file":"redteam.d.ts","sourceRoot":"","sources":["../../../src/commands/redteam.ts"],"names":[],"mappings":"AAAA;;GAEG;AAsCH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAoCpC,wBAAgB,cAAc,IAAI,OAAO,CAycxC"}
1
+ {"version":3,"file":"redteam.d.ts","sourceRoot":"","sources":["../../../src/commands/redteam.ts"],"names":[],"mappings":"AAAA;;GAEG;AAuCH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAoCpC,wBAAgB,cAAc,IAAI,OAAO,CAidxC"}
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/commands/run.ts"],"names":[],"mappings":"AAAA;;GAEG;AAiBH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyiBpC,wBAAgB,UAAU,IAAI,OAAO,CAggBpC"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/commands/run.ts"],"names":[],"mappings":"AAAA;;GAEG;AAiBH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyiBpC,wBAAgB,UAAU,IAAI,OAAO,CAwgBpC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Validate command - Validate scenarios without running them
3
+ */
4
+ import { Command } from 'commander';
5
+ export declare function validateCommand(): Command;
6
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/commands/validate.ts"],"names":[],"mappings":"AAAA;;GAEG;AASH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,wBAAgB,eAAe,IAAI,OAAO,CAkHzC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artemiskit/cli",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Command-line interface for ArtemisKit LLM evaluation toolkit",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -45,11 +45,11 @@
45
45
  "test": "bun test"
46
46
  },
47
47
  "dependencies": {
48
- "@artemiskit/adapter-openai": "0.1.10",
49
- "@artemiskit/adapter-vercel-ai": "0.1.10",
50
- "@artemiskit/core": "0.2.3",
51
- "@artemiskit/redteam": "0.2.3",
52
- "@artemiskit/reports": "0.2.3",
48
+ "@artemiskit/adapter-openai": "0.1.11",
49
+ "@artemiskit/adapter-vercel-ai": "0.1.11",
50
+ "@artemiskit/core": "0.2.4",
51
+ "@artemiskit/redteam": "0.2.4",
52
+ "@artemiskit/reports": "0.2.4",
53
53
  "chalk": "^5.3.0",
54
54
  "cli-table3": "^0.6.3",
55
55
  "commander": "^12.0.0",
package/src/cli.ts CHANGED
@@ -12,6 +12,7 @@ import { redteamCommand } from './commands/redteam';
12
12
  import { reportCommand } from './commands/report';
13
13
  import { runCommand } from './commands/run';
14
14
  import { stressCommand } from './commands/stress';
15
+ import { validateCommand } from './commands/validate';
15
16
  import { checkForUpdate, formatUpdateMessage, formatVersionDisplay } from './utils/update-checker';
16
17
 
17
18
  export function createCLI(): Command {
@@ -46,6 +47,7 @@ export function createCLI(): Command {
46
47
 
47
48
  program.addCommand(initCommand());
48
49
  program.addCommand(runCommand());
50
+ program.addCommand(validateCommand());
49
51
  program.addCommand(baselineCommand());
50
52
  program.addCommand(compareCommand());
51
53
  program.addCommand(historyCommand());
@@ -35,6 +35,7 @@ import {
35
35
  import {
36
36
  generateJSONReport,
37
37
  generateRedTeamHTMLReport,
38
+ generateRedTeamJUnitReport,
38
39
  generateRedTeamMarkdownReport,
39
40
  } from '@artemiskit/reports';
40
41
  import chalk from 'chalk';
@@ -70,7 +71,7 @@ interface RedteamOptions {
70
71
  config?: string;
71
72
  redact?: boolean;
72
73
  redactPatterns?: string[];
73
- export?: 'markdown';
74
+ export?: 'markdown' | 'junit';
74
75
  exportOutput?: string;
75
76
  }
76
77
 
@@ -97,7 +98,7 @@ export function redteamCommand(): Command {
97
98
  '--redact-patterns <patterns...>',
98
99
  'Custom redaction patterns (regex or built-in: email, phone, credit_card, ssn, api_key)'
99
100
  )
100
- .option('--export <format>', 'Export results to format (markdown)')
101
+ .option('--export <format>', 'Export results to format (markdown or junit)')
101
102
  .option('--export-output <dir>', 'Output directory for exports (default: ./artemis-exports)')
102
103
  .action(async (scenarioPath: string, options: RedteamOptions) => {
103
104
  const spinner = createSpinner('Loading configuration...');
@@ -503,14 +504,22 @@ export function redteamCommand(): Command {
503
504
  console.log(chalk.dim(` JSON: ${jsonPath}`));
504
505
  }
505
506
 
506
- // Export to markdown if requested
507
- if (options.export === 'markdown') {
507
+ // Export if requested
508
+ if (options.export) {
508
509
  const exportDir = options.exportOutput || './artemis-exports';
509
510
  await mkdir(exportDir, { recursive: true });
510
- const markdown = generateRedTeamMarkdownReport(manifest);
511
- const mdPath = join(exportDir, `${runId}.md`);
512
- await writeFile(mdPath, markdown);
513
- console.log(chalk.dim(`Exported: ${mdPath}`));
511
+
512
+ if (options.export === 'markdown') {
513
+ const markdown = generateRedTeamMarkdownReport(manifest);
514
+ const mdPath = join(exportDir, `${runId}.md`);
515
+ await writeFile(mdPath, markdown);
516
+ console.log(chalk.dim(`Exported: ${mdPath}`));
517
+ } else if (options.export === 'junit') {
518
+ const junit = generateRedTeamJUnitReport(manifest);
519
+ const junitPath = join(exportDir, `${runId}.xml`);
520
+ await writeFile(junitPath, junit);
521
+ console.log(chalk.dim(`Exported: ${junitPath}`));
522
+ }
514
523
  }
515
524
 
516
525
  // Exit with error if there were unsafe responses
@@ -15,7 +15,7 @@ import {
15
15
  resolveScenarioPaths,
16
16
  runScenario,
17
17
  } from '@artemiskit/core';
18
- import { generateMarkdownReport } from '@artemiskit/reports';
18
+ import { generateJUnitReport, generateMarkdownReport } from '@artemiskit/reports';
19
19
  import chalk from 'chalk';
20
20
  import { Command } from 'commander';
21
21
  import { loadConfig } from '../config/loader.js';
@@ -68,8 +68,8 @@ interface RunOptions {
68
68
  threshold?: number;
69
69
  /** Budget limit in USD - fail if cost exceeds this */
70
70
  budget?: number;
71
- /** Export format: markdown */
72
- export?: 'markdown';
71
+ /** Export format: markdown or junit */
72
+ export?: 'markdown' | 'junit';
73
73
  /** Output directory for exports */
74
74
  exportOutput?: string;
75
75
  }
@@ -607,7 +607,7 @@ export function runCommand(): Command {
607
607
  .option('--baseline', 'Compare against baseline and detect regression')
608
608
  .option('--threshold <number>', 'Regression threshold (0-1), e.g., 0.05 for 5%', '0.05')
609
609
  .option('--budget <amount>', 'Maximum budget in USD - fail if estimated cost exceeds this')
610
- .option('--export <format>', 'Export format: markdown')
610
+ .option('--export <format>', 'Export format: markdown or junit (for CI integration)')
611
611
  .option('--export-output <dir>', 'Output directory for exports (default: ./artemis-exports)')
612
612
  .action(async (scenarioPath: string | undefined, options: RunOptions) => {
613
613
  // Determine CI mode: explicit flag, environment variable, or summary format that implies CI
@@ -819,14 +819,22 @@ export function runCommand(): Command {
819
819
  console.log(chalk.dim(`Saved: ${savedPath}`));
820
820
  }
821
821
 
822
- // Export to markdown if requested
823
- if (options.export === 'markdown') {
822
+ // Export if requested
823
+ if (options.export) {
824
824
  const exportDir = options.exportOutput || './artemis-exports';
825
825
  await mkdir(exportDir, { recursive: true });
826
- const markdown = generateMarkdownReport(result.manifest);
827
- const mdPath = join(exportDir, `${result.manifest.run_id}.md`);
828
- await writeFile(mdPath, markdown);
829
- console.log(chalk.dim(`Exported: ${mdPath}`));
826
+
827
+ if (options.export === 'markdown') {
828
+ const markdown = generateMarkdownReport(result.manifest);
829
+ const mdPath = join(exportDir, `${result.manifest.run_id}.md`);
830
+ await writeFile(mdPath, markdown);
831
+ console.log(chalk.dim(`Exported: ${mdPath}`));
832
+ } else if (options.export === 'junit') {
833
+ const junit = generateJUnitReport(result.manifest);
834
+ const junitPath = join(exportDir, `${result.manifest.run_id}.xml`);
835
+ await writeFile(junitPath, junit);
836
+ console.log(chalk.dim(`Exported: ${junitPath}`));
837
+ }
830
838
  }
831
839
  } catch (error) {
832
840
  // Record failed scenario
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Validate command - Validate scenarios without running them
3
+ */
4
+
5
+ import { readdirSync, statSync } from 'node:fs';
6
+ import { mkdir, writeFile } from 'node:fs/promises';
7
+ import { basename, join, resolve } from 'node:path';
8
+ import { ScenarioValidator, type ValidationResult, type ValidationSummary } from '@artemiskit/core';
9
+ import { generateValidationJUnitReport } from '@artemiskit/reports';
10
+ import { Glob } from 'bun';
11
+ import chalk from 'chalk';
12
+ import { Command } from 'commander';
13
+ import { icons } from '../ui/index.js';
14
+
15
+ interface ValidateOptions {
16
+ json?: boolean;
17
+ strict?: boolean;
18
+ quiet?: boolean;
19
+ export?: 'junit';
20
+ exportOutput?: string;
21
+ }
22
+
23
+ export function validateCommand(): Command {
24
+ const cmd = new Command('validate');
25
+
26
+ cmd
27
+ .description('Validate scenario files without running them')
28
+ .argument('<path>', 'Path to scenario file, directory, or glob pattern')
29
+ .option('--json', 'Output as JSON')
30
+ .option('--strict', 'Treat warnings as errors')
31
+ .option('-q, --quiet', 'Only output errors (no success messages)')
32
+ .option('--export <format>', 'Export results to format (junit for CI integration)')
33
+ .option('--export-output <dir>', 'Output directory for exports (default: ./artemis-exports)')
34
+ .action(async (pathArg: string, options: ValidateOptions) => {
35
+ const validator = new ScenarioValidator({ strict: options.strict });
36
+
37
+ // Resolve files to validate
38
+ const files = resolveFiles(pathArg);
39
+
40
+ if (files.length === 0) {
41
+ if (options.json) {
42
+ console.log(
43
+ JSON.stringify(
44
+ {
45
+ valid: false,
46
+ error: `No scenario files found matching: ${pathArg}`,
47
+ results: [],
48
+ summary: { total: 0, passed: 0, failed: 0, withWarnings: 0 },
49
+ },
50
+ null,
51
+ 2
52
+ )
53
+ );
54
+ } else {
55
+ console.log(chalk.red(`${icons.failed} No scenario files found matching: ${pathArg}`));
56
+ }
57
+ process.exit(2);
58
+ }
59
+
60
+ // Validate all files
61
+ const results: ValidationResult[] = [];
62
+
63
+ if (!options.json && !options.quiet) {
64
+ console.log(chalk.bold('Validating scenarios...\n'));
65
+ }
66
+
67
+ for (const file of files) {
68
+ const result = validator.validate(file);
69
+ results.push(result);
70
+
71
+ // In strict mode, warnings become errors
72
+ if (options.strict && result.warnings.length > 0) {
73
+ result.valid = false;
74
+ result.errors.push(
75
+ ...result.warnings.map((w: ValidationResult['warnings'][0]) => ({
76
+ ...w,
77
+ severity: 'error' as const,
78
+ }))
79
+ );
80
+ }
81
+
82
+ if (!options.json) {
83
+ printFileResult(result, options);
84
+ }
85
+ }
86
+
87
+ // Calculate summary
88
+ const summary: ValidationSummary = {
89
+ total: results.length,
90
+ passed: results.filter((r) => r.valid && r.warnings.length === 0).length,
91
+ failed: results.filter((r) => !r.valid).length,
92
+ withWarnings: results.filter((r) => r.valid && r.warnings.length > 0).length,
93
+ };
94
+
95
+ // Output results
96
+ if (options.json) {
97
+ console.log(
98
+ JSON.stringify(
99
+ {
100
+ valid: summary.failed === 0,
101
+ results: results.map((r) => ({
102
+ file: r.file,
103
+ valid: r.valid,
104
+ errors: r.errors,
105
+ warnings: r.warnings,
106
+ })),
107
+ summary,
108
+ },
109
+ null,
110
+ 2
111
+ )
112
+ );
113
+ } else if (!options.quiet) {
114
+ console.log();
115
+ printSummary(summary, options.strict);
116
+ }
117
+
118
+ // Export to JUnit if requested
119
+ if (options.export === 'junit') {
120
+ const exportDir = options.exportOutput || './artemis-exports';
121
+ await mkdir(exportDir, { recursive: true });
122
+ const junit = generateValidationJUnitReport(results);
123
+ const junitPath = join(exportDir, `validation-${Date.now()}.xml`);
124
+ await writeFile(junitPath, junit);
125
+ if (!options.quiet) {
126
+ console.log(chalk.dim(`Exported: ${junitPath}`));
127
+ }
128
+ }
129
+
130
+ // Exit with appropriate code
131
+ if (summary.failed > 0) {
132
+ process.exit(1);
133
+ }
134
+ });
135
+
136
+ return cmd;
137
+ }
138
+
139
+ /**
140
+ * Resolve files from path argument (file, directory, or glob)
141
+ */
142
+ function resolveFiles(pathArg: string): string[] {
143
+ const resolved = resolve(pathArg);
144
+
145
+ try {
146
+ const stat = statSync(resolved);
147
+
148
+ if (stat.isFile()) {
149
+ // Single file
150
+ return [resolved];
151
+ }
152
+
153
+ if (stat.isDirectory()) {
154
+ // Directory - find all yaml files recursively
155
+ return findYamlFiles(resolved);
156
+ }
157
+ } catch {
158
+ // Path doesn't exist as file/directory - try as glob
159
+ }
160
+
161
+ // Try as glob pattern using Bun's Glob
162
+ const glob = new Glob(pathArg);
163
+ const matches: string[] = [];
164
+ for (const file of glob.scanSync({ absolute: true, onlyFiles: true })) {
165
+ if (file.endsWith('.yaml') || file.endsWith('.yml')) {
166
+ matches.push(file);
167
+ }
168
+ }
169
+
170
+ return matches;
171
+ }
172
+
173
+ /**
174
+ * Find all YAML files in a directory recursively
175
+ */
176
+ function findYamlFiles(dir: string): string[] {
177
+ const files: string[] = [];
178
+
179
+ const entries = readdirSync(dir, { withFileTypes: true });
180
+
181
+ for (const entry of entries) {
182
+ const fullPath = join(dir, entry.name);
183
+
184
+ if (entry.isDirectory()) {
185
+ files.push(...findYamlFiles(fullPath));
186
+ } else if (entry.isFile() && (entry.name.endsWith('.yaml') || entry.name.endsWith('.yml'))) {
187
+ files.push(fullPath);
188
+ }
189
+ }
190
+
191
+ return files;
192
+ }
193
+
194
+ /**
195
+ * Print result for a single file
196
+ */
197
+ function printFileResult(result: ValidationResult, options: ValidateOptions): void {
198
+ const fileName = basename(result.file);
199
+
200
+ if (result.valid && result.warnings.length === 0) {
201
+ if (!options.quiet) {
202
+ console.log(`${icons.passed} ${chalk.green(fileName)}`);
203
+ }
204
+ } else if (result.valid && result.warnings.length > 0) {
205
+ console.log(`${icons.warning} ${chalk.yellow(fileName)}`);
206
+ for (const warning of result.warnings) {
207
+ const location = warning.column
208
+ ? `Line ${warning.line}:${warning.column}`
209
+ : `Line ${warning.line}`;
210
+ console.log(chalk.yellow(` ${location}: ${warning.message}`));
211
+ if (warning.suggestion) {
212
+ console.log(chalk.dim(` Suggestion: ${warning.suggestion}`));
213
+ }
214
+ }
215
+ } else {
216
+ console.log(`${icons.failed} ${chalk.red(fileName)}`);
217
+ for (const error of result.errors) {
218
+ const location = error.column ? `Line ${error.line}:${error.column}` : `Line ${error.line}`;
219
+ console.log(chalk.red(` ${location}: ${error.message}`));
220
+ if (error.suggestion) {
221
+ console.log(chalk.dim(` Suggestion: ${error.suggestion}`));
222
+ }
223
+ }
224
+ for (const warning of result.warnings) {
225
+ const location = warning.column
226
+ ? `Line ${warning.line}:${warning.column}`
227
+ : `Line ${warning.line}`;
228
+ console.log(chalk.yellow(` ${location}: ${warning.message}`));
229
+ }
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Print validation summary
235
+ */
236
+ function printSummary(summary: ValidationSummary, strict?: boolean): void {
237
+ const parts: string[] = [];
238
+
239
+ if (summary.passed > 0) {
240
+ parts.push(chalk.green(`${summary.passed} passed`));
241
+ }
242
+ if (summary.failed > 0) {
243
+ parts.push(chalk.red(`${summary.failed} failed`));
244
+ }
245
+ if (summary.withWarnings > 0 && !strict) {
246
+ parts.push(chalk.yellow(`${summary.withWarnings} with warnings`));
247
+ }
248
+
249
+ const statusIcon = summary.failed > 0 ? icons.failed : icons.passed;
250
+ const statusColor = summary.failed > 0 ? chalk.red : chalk.green;
251
+
252
+ console.log(statusColor(`${statusIcon} ${parts.join(', ')}`));
253
+ console.log(chalk.dim(`${summary.total} scenario(s) validated`));
254
+ }