@artemiskit/cli 0.1.6 → 0.1.8

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 (44) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/index.js +2653 -4832
  3. package/dist/src/__tests__/helpers/index.d.ts +6 -0
  4. package/dist/src/__tests__/helpers/index.d.ts.map +1 -0
  5. package/dist/src/__tests__/helpers/mock-adapter.d.ts +87 -0
  6. package/dist/src/__tests__/helpers/mock-adapter.d.ts.map +1 -0
  7. package/dist/src/__tests__/helpers/test-utils.d.ts +47 -0
  8. package/dist/src/__tests__/helpers/test-utils.d.ts.map +1 -0
  9. package/dist/src/cli.d.ts.map +1 -1
  10. package/dist/src/commands/compare.d.ts.map +1 -1
  11. package/dist/src/commands/history.d.ts.map +1 -1
  12. package/dist/src/commands/init.d.ts.map +1 -1
  13. package/dist/src/commands/redteam.d.ts.map +1 -1
  14. package/dist/src/commands/report.d.ts.map +1 -1
  15. package/dist/src/commands/run.d.ts.map +1 -1
  16. package/dist/src/ui/errors.d.ts.map +1 -1
  17. package/dist/src/ui/panels.d.ts.map +1 -1
  18. package/dist/src/ui/progress.d.ts.map +1 -1
  19. package/dist/src/ui/utils.d.ts.map +1 -1
  20. package/dist/src/utils/update-checker.d.ts +31 -0
  21. package/dist/src/utils/update-checker.d.ts.map +1 -0
  22. package/package.json +6 -6
  23. package/src/__tests__/helpers/mock-adapter.ts +22 -4
  24. package/src/__tests__/helpers/test-utils.ts +3 -3
  25. package/src/__tests__/integration/compare-command.test.ts +7 -7
  26. package/src/__tests__/integration/config.test.ts +2 -2
  27. package/src/__tests__/integration/history-command.test.ts +2 -2
  28. package/src/__tests__/integration/init-command.test.ts +3 -3
  29. package/src/__tests__/integration/report-command.test.ts +2 -2
  30. package/src/__tests__/integration/ui.test.ts +6 -6
  31. package/src/cli.ts +22 -1
  32. package/src/commands/compare.ts +2 -4
  33. package/src/commands/history.ts +2 -2
  34. package/src/commands/init.ts +52 -12
  35. package/src/commands/redteam.ts +6 -6
  36. package/src/commands/report.ts +3 -3
  37. package/src/commands/run.ts +4 -4
  38. package/src/commands/stress.ts +4 -4
  39. package/src/ui/errors.ts +1 -1
  40. package/src/ui/live-status.ts +1 -1
  41. package/src/ui/panels.ts +2 -2
  42. package/src/ui/progress.ts +1 -1
  43. package/src/ui/utils.ts +4 -3
  44. package/src/utils/update-checker.ts +121 -0
@@ -3,11 +3,12 @@
3
3
  */
4
4
 
5
5
  import { existsSync } from 'node:fs';
6
- import { mkdir, readFile, writeFile, appendFile } from 'node:fs/promises';
6
+ import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
7
7
  import { join } from 'node:path';
8
8
  import chalk from 'chalk';
9
9
  import { Command } from 'commander';
10
10
  import { createSpinner, icons } from '../ui/index.js';
11
+ import { checkForUpdateAndNotify, getCurrentVersion } from '../utils/update-checker.js';
11
12
 
12
13
  const DEFAULT_CONFIG = `# ArtemisKit Configuration
13
14
  project: my-project
@@ -86,18 +87,54 @@ const ENV_KEYS = [
86
87
  ];
87
88
 
88
89
  function renderWelcomeBanner(): string {
90
+ // Brand color for "KIT" portion: #fb923c (orange)
91
+ const brandColor = chalk.hex('#fb923c');
92
+ const version = getCurrentVersion();
93
+
94
+ // Randomly color each border character white or brand color
95
+ const colorBorderChar = (char: string): string => {
96
+ return Math.random() > 0.5 ? chalk.white(char) : brandColor(char);
97
+ };
98
+
99
+ const colorBorder = (str: string): string => {
100
+ return str.split('').map(colorBorderChar).join('');
101
+ };
102
+
103
+ // All lines are exactly 52 chars inside the borders for perfect alignment
104
+ const topBorder = `╭${'─'.repeat(52)}╮`;
105
+ const bottomBorder = `╰${'─'.repeat(52)}╯`;
106
+ const sideBorderLeft = '│';
107
+ const sideBorderRight = '│';
108
+ const emptyContent = ' '.repeat(52);
109
+
110
+ // Version line: "v0.1.7" centered in brand color
111
+ const versionText = `v${version}`;
112
+ const versionPadding = Math.floor((52 - versionText.length) / 2);
113
+ const versionLine =
114
+ ' '.repeat(versionPadding) +
115
+ brandColor(versionText) +
116
+ ' '.repeat(52 - versionPadding - versionText.length);
117
+
118
+ // Tagline centered
119
+ const tagline = 'Open-source testing toolkit for LLM applications';
120
+ const taglinePadding = Math.floor((52 - tagline.length) / 2);
121
+ const taglineLine =
122
+ ' '.repeat(taglinePadding) +
123
+ chalk.gray(tagline) +
124
+ ' '.repeat(52 - taglinePadding - tagline.length);
125
+
89
126
  const lines = [
90
127
  '',
91
- chalk.cyan(' ╔═══════════════════════════════════════════════════════╗'),
92
- chalk.cyan(' ║ ║'),
93
- chalk.cyan('') +
94
- chalk.bold.white(' 🎯 Welcome to ArtemisKit ') +
95
- chalk.cyan('║'),
96
- chalk.cyan(' ║') +
97
- chalk.gray(' LLM Testing & Evaluation Toolkit ') +
98
- chalk.cyan('║'),
99
- chalk.cyan(' ║ ║'),
100
- chalk.cyan(' ╚═══════════════════════════════════════════════════════╝'),
128
+ ` ${colorBorder(topBorder)}`,
129
+ ` ${colorBorderChar(sideBorderLeft)}${emptyContent}${colorBorderChar(sideBorderRight)}`,
130
+ ` ${colorBorderChar(sideBorderLeft)} ${chalk.bold.white('▄▀█ █▀█ ▀█▀ █▀▀ █▀▄▀█ █ █▀ ')}${brandColor.bold('█▄▀ █ ▀█▀')} ${colorBorderChar(sideBorderRight)}`,
131
+ ` ${colorBorderChar(sideBorderLeft)} ${chalk.bold.white('█▀█ █▀▄ █ ██▄ ▀ █ █ ▄█ ')}${brandColor.bold('█ █ █ █ ')} ${colorBorderChar(sideBorderRight)}`,
132
+ ` ${colorBorderChar(sideBorderLeft)}${emptyContent}${colorBorderChar(sideBorderRight)}`,
133
+ ` ${colorBorderChar(sideBorderLeft)}${versionLine}${colorBorderChar(sideBorderRight)}`,
134
+ ` ${colorBorderChar(sideBorderLeft)}${emptyContent}${colorBorderChar(sideBorderRight)}`,
135
+ ` ${colorBorderChar(sideBorderLeft)}${taglineLine}${colorBorderChar(sideBorderRight)}`,
136
+ ` ${colorBorderChar(sideBorderLeft)}${emptyContent}${colorBorderChar(sideBorderRight)}`,
137
+ ` ${colorBorder(bottomBorder)}`,
101
138
  '',
102
139
  ];
103
140
  return lines.join('\n');
@@ -180,7 +217,7 @@ async function appendEnvKeys(cwd: string): Promise<{ added: string[]; skipped: s
180
217
  // Add newline before our content if file exists and doesn't end with newline
181
218
  const prefix =
182
219
  existingContent && !existingContent.endsWith('\n') ? '\n\n' : existingContent ? '\n' : '';
183
- await appendFile(envPath, prefix + linesToAdd.join('\n') + '\n');
220
+ await appendFile(envPath, `${prefix + linesToAdd.join('\n')}\n`);
184
221
  }
185
222
 
186
223
  return { added, skipped };
@@ -260,6 +297,9 @@ export function initCommand(): Command {
260
297
 
261
298
  // Show success panel
262
299
  console.log(renderSuccessPanel());
300
+
301
+ // Non-blocking update check (fire and forget)
302
+ checkForUpdateAndNotify();
263
303
  } catch (error) {
264
304
  spinner.fail('Error');
265
305
  console.error(chalk.red(`\n${icons.failed} ${(error as Error).message}`));
@@ -35,13 +35,13 @@ import { nanoid } from 'nanoid';
35
35
  import { loadConfig } from '../config/loader.js';
36
36
  import {
37
37
  createSpinner,
38
- renderRedteamSummaryPanel,
38
+ getProviderErrorContext,
39
+ icons,
40
+ isTTY,
39
41
  renderError,
40
42
  renderInfoBox,
41
43
  renderProgressBar,
42
- getProviderErrorContext,
43
- isTTY,
44
- icons,
44
+ renderRedteamSummaryPanel,
45
45
  } from '../ui/index.js';
46
46
  import {
47
47
  buildAdapterConfig,
@@ -213,7 +213,7 @@ export function redteamCommand(): Command {
213
213
 
214
214
  // Clear progress line
215
215
  if (isTTY) {
216
- process.stdout.write('\r' + ' '.repeat(60) + '\r');
216
+ process.stdout.write(`\r${' '.repeat(60)}\r`);
217
217
  }
218
218
 
219
219
  // Display status with appropriate icon
@@ -267,7 +267,7 @@ export function redteamCommand(): Command {
267
267
 
268
268
  // Clear progress line
269
269
  if (isTTY) {
270
- process.stdout.write('\r' + ' '.repeat(60) + '\r');
270
+ process.stdout.write(`\r${' '.repeat(60)}\r`);
271
271
  }
272
272
 
273
273
  // Apply redaction to prompt even for errors/blocked
@@ -13,7 +13,7 @@ import {
13
13
  } from '@artemiskit/reports';
14
14
  import { Command } from 'commander';
15
15
  import { loadConfig } from '../config/loader.js';
16
- import { createSpinner, renderError, renderInfoBox, icons } from '../ui/index.js';
16
+ import { createSpinner, icons, renderError, renderInfoBox } from '../ui/index.js';
17
17
  import { createStorage } from '../utils/storage.js';
18
18
 
19
19
  interface ReportOptions {
@@ -96,7 +96,7 @@ export function reportCommand(): Command {
96
96
  const htmlPath = join(outputDir, `${runId}.html`);
97
97
  await writeFile(htmlPath, html);
98
98
  generatedFiles.push(htmlPath);
99
- spinner.succeed(`Generated HTML report`);
99
+ spinner.succeed('Generated HTML report');
100
100
  }
101
101
 
102
102
  if (format === 'json' || format === 'both') {
@@ -105,7 +105,7 @@ export function reportCommand(): Command {
105
105
  const jsonPath = join(outputDir, `${runId}.json`);
106
106
  await writeFile(jsonPath, json);
107
107
  generatedFiles.push(jsonPath);
108
- spinner.succeed(`Generated JSON report`);
108
+ spinner.succeed('Generated JSON report');
109
109
  }
110
110
 
111
111
  // Show success panel
@@ -13,14 +13,14 @@ import { Command } from 'commander';
13
13
  import { loadConfig } from '../config/loader.js';
14
14
  import {
15
15
  createSpinner,
16
+ formatDuration,
17
+ getProviderErrorContext,
16
18
  icons,
19
+ isTTY,
20
+ padText,
17
21
  renderError,
18
22
  renderProgressBar,
19
23
  renderSummaryPanel,
20
- getProviderErrorContext,
21
- formatDuration,
22
- padText,
23
- isTTY,
24
24
  } from '../ui/index.js';
25
25
  import {
26
26
  buildAdapterConfig,
@@ -21,14 +21,14 @@ import { Command } from 'commander';
21
21
  import { nanoid } from 'nanoid';
22
22
  import { loadConfig } from '../config/loader.js';
23
23
  import {
24
+ colors,
24
25
  createSpinner,
25
- renderStressSummaryPanel,
26
+ getProviderErrorContext,
27
+ isTTY,
26
28
  renderError,
27
29
  renderInfoBox,
28
30
  renderProgressBar,
29
- getProviderErrorContext,
30
- isTTY,
31
- colors,
31
+ renderStressSummaryPanel,
32
32
  } from '../ui/index.js';
33
33
  import {
34
34
  buildAdapterConfig,
package/src/ui/errors.ts CHANGED
@@ -148,7 +148,7 @@ export function renderWarning(title: string, message: string, suggestions?: stri
148
148
  currentLine += (currentLine.length > 2 ? ' ' : '') + word;
149
149
  } else {
150
150
  lines.push(chalk.yellow('│') + padText(currentLine, width - 2) + chalk.yellow('│'));
151
- currentLine = ' ' + word;
151
+ currentLine = ` ${word}`;
152
152
  }
153
153
  }
154
154
  if (currentLine.length > 2) {
@@ -4,9 +4,9 @@
4
4
 
5
5
  import chalk from 'chalk';
6
6
  import ora, { type Ora } from 'ora';
7
- import { isTTY } from './utils.js';
8
7
  import { icons } from './colors.js';
9
8
  import { renderProgressBar } from './progress.js';
9
+ import { isTTY } from './utils.js';
10
10
 
11
11
  export type TestStatus = 'pending' | 'running' | 'passed' | 'failed' | 'skipped';
12
12
 
package/src/ui/panels.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import chalk from 'chalk';
6
6
  import { formatPercentage, icons } from './colors.js';
7
- import { isTTY, centerText, padText, formatDuration } from './utils.js';
7
+ import { centerText, formatDuration, isTTY, padText } from './utils.js';
8
8
 
9
9
  export interface SummaryData {
10
10
  passed: number;
@@ -180,7 +180,7 @@ export function renderRedteamSummaryPanel(data: RedteamSummaryData): string {
180
180
  chalk.magenta('║'),
181
181
  chalk.magenta(`╠${border}╣`),
182
182
  chalk.magenta('║') +
183
- padText(` Defense Rate: ${defenseColor(data.defenseRate.toFixed(1) + '%')}`, width - 2) +
183
+ padText(` Defense Rate: ${defenseColor(`${data.defenseRate.toFixed(1)}%`)}`, width - 2) +
184
184
  chalk.magenta('║'),
185
185
  chalk.magenta(`╚${border}╝`),
186
186
  ];
@@ -108,7 +108,7 @@ export class ProgressBar {
108
108
  if (isTTY) {
109
109
  // Clear previous line and write new output
110
110
  const clearLength = stripAnsi(this.lastOutput).length;
111
- process.stdout.write('\r' + ' '.repeat(clearLength) + '\r');
111
+ process.stdout.write(`\r${' '.repeat(clearLength)}\r`);
112
112
  process.stdout.write(output);
113
113
  this.lastOutput = output;
114
114
  }
package/src/ui/utils.ts CHANGED
@@ -46,7 +46,8 @@ export function padText(
46
46
 
47
47
  if (align === 'center') {
48
48
  return centerText(text, width);
49
- } else if (align === 'right') {
49
+ }
50
+ if (align === 'right') {
50
51
  return ' '.repeat(paddingNeeded) + text;
51
52
  }
52
53
  return text + ' '.repeat(paddingNeeded);
@@ -56,7 +57,7 @@ export function padText(
56
57
  * Strip ANSI escape codes from string (for length calculations)
57
58
  */
58
59
  export function stripAnsi(str: string): string {
59
- // eslint-disable-next-line no-control-regex
60
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape codes require control characters
60
61
  return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
61
62
  }
62
63
 
@@ -66,7 +67,7 @@ export function stripAnsi(str: string): string {
66
67
  export function truncate(text: string, maxWidth: number): string {
67
68
  const visibleLength = stripAnsi(text).length;
68
69
  if (visibleLength <= maxWidth) return text;
69
- return text.slice(0, maxWidth - 1) + '…';
70
+ return `${text.slice(0, maxWidth - 1)}…`;
70
71
  }
71
72
 
72
73
  /**
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Non-blocking update checker for ArtemisKit CLI
3
+ */
4
+
5
+ import chalk from 'chalk';
6
+ import { version as currentVersion } from '../../package.json';
7
+
8
+ const PACKAGE_NAME = '@artemiskit/cli';
9
+ const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
10
+ const FETCH_TIMEOUT_MS = 3000; // 3 second timeout to avoid blocking
11
+
12
+ // Brand color
13
+ const brandColor = chalk.hex('#fb923c');
14
+
15
+ export interface UpdateInfo {
16
+ currentVersion: string;
17
+ latestVersion: string;
18
+ updateAvailable: boolean;
19
+ }
20
+
21
+ /**
22
+ * Fetches the latest version from npm registry with a timeout
23
+ */
24
+ async function fetchLatestVersion(): Promise<string | null> {
25
+ const controller = new AbortController();
26
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
27
+
28
+ try {
29
+ const response = await fetch(NPM_REGISTRY_URL, {
30
+ signal: controller.signal,
31
+ headers: {
32
+ Accept: 'application/json',
33
+ },
34
+ });
35
+
36
+ if (!response.ok) {
37
+ return null;
38
+ }
39
+
40
+ const data = (await response.json()) as { version?: string };
41
+ return data.version || null;
42
+ } catch {
43
+ // Silently fail - network issues shouldn't block CLI usage
44
+ return null;
45
+ } finally {
46
+ clearTimeout(timeoutId);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Compares two semver versions
52
+ * Returns true if latest > current
53
+ */
54
+ function isNewerVersion(current: string, latest: string): boolean {
55
+ const currentParts = current.replace(/^v/, '').split('.').map(Number);
56
+ const latestParts = latest.replace(/^v/, '').split('.').map(Number);
57
+
58
+ for (let i = 0; i < 3; i++) {
59
+ const c = currentParts[i] || 0;
60
+ const l = latestParts[i] || 0;
61
+ if (l > c) return true;
62
+ if (l < c) return false;
63
+ }
64
+ return false;
65
+ }
66
+
67
+ /**
68
+ * Check for updates (non-blocking)
69
+ * Returns update info or null if check fails
70
+ */
71
+ export async function checkForUpdate(): Promise<UpdateInfo | null> {
72
+ const latestVersion = await fetchLatestVersion();
73
+
74
+ if (!latestVersion) {
75
+ return null;
76
+ }
77
+
78
+ return {
79
+ currentVersion,
80
+ latestVersion,
81
+ updateAvailable: isNewerVersion(currentVersion, latestVersion),
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Get the current CLI version
87
+ */
88
+ export function getCurrentVersion(): string {
89
+ return currentVersion;
90
+ }
91
+
92
+ /**
93
+ * Format version display string
94
+ */
95
+ export function formatVersionDisplay(version: string): string {
96
+ return `${chalk.bold('ArtemisKit CLI')} ${brandColor(`v${version}`)}`;
97
+ }
98
+
99
+ /**
100
+ * Format update available message
101
+ */
102
+ export function formatUpdateMessage(current: string, latest: string): string {
103
+ return `\n${chalk.yellow('╭─────────────────────────────────────────────────────╮')}\n${chalk.yellow('│')}${chalk.yellow(' Update available! ')}${chalk.gray(`${current}`)}${chalk.yellow(' → ')}${brandColor.bold(`${latest}`)}${' '.repeat(24 - current.length - latest.length)}${chalk.yellow('│')}\n${chalk.yellow('│')}${chalk.white(' Run ')}${chalk.cyan('npm install -g @artemiskit/cli')}${chalk.white(' to update ')}${chalk.yellow('│')}\n${chalk.yellow('╰─────────────────────────────────────────────────────╯')}`;
104
+ }
105
+
106
+ /**
107
+ * Non-blocking update check that prints message if update available
108
+ * Use this to fire-and-forget an update check
109
+ */
110
+ export function checkForUpdateAndNotify(): void {
111
+ // Fire and forget - don't await
112
+ checkForUpdate()
113
+ .then((info) => {
114
+ if (info?.updateAvailable) {
115
+ console.log(formatUpdateMessage(info.currentVersion, info.latestVersion));
116
+ }
117
+ })
118
+ .catch(() => {
119
+ // Silently ignore errors
120
+ });
121
+ }