voyageai-cli 1.1.0 → 1.3.0

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.
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const CONFIG_DIR = path.join(os.homedir(), '.vai');
8
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
9
+
10
+ // Key mapping: CLI key names → internal config keys
11
+ const KEY_MAP = {
12
+ 'api-key': 'apiKey',
13
+ 'mongodb-uri': 'mongodbUri',
14
+ 'default-model': 'defaultModel',
15
+ 'default-dimensions': 'defaultDimensions',
16
+ };
17
+
18
+ // Keys whose values should be masked in output
19
+ const SECRET_KEYS = new Set(['apiKey', 'mongodbUri']);
20
+
21
+ /**
22
+ * Load config from disk. Returns {} if file doesn't exist.
23
+ * @param {string} [configPath] - Override config path (for testing)
24
+ * @returns {object}
25
+ */
26
+ function loadConfig(configPath) {
27
+ const p = configPath || CONFIG_PATH;
28
+ try {
29
+ const raw = fs.readFileSync(p, 'utf-8');
30
+ return JSON.parse(raw);
31
+ } catch (err) {
32
+ if (err.code === 'ENOENT') return {};
33
+ throw err;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Save config to disk. Creates directory if needed, chmod 600 on the file.
39
+ * @param {object} config
40
+ * @param {string} [configPath] - Override config path (for testing)
41
+ */
42
+ function saveConfig(config, configPath) {
43
+ const p = configPath || CONFIG_PATH;
44
+ const dir = path.dirname(p);
45
+ fs.mkdirSync(dir, { recursive: true });
46
+ fs.writeFileSync(p, JSON.stringify(config, null, 2) + '\n', 'utf-8');
47
+ fs.chmodSync(p, 0o600);
48
+ }
49
+
50
+ /**
51
+ * Get a single config value.
52
+ * @param {string} key - Internal config key (e.g. 'apiKey')
53
+ * @param {string} [configPath] - Override config path (for testing)
54
+ * @returns {*}
55
+ */
56
+ function getConfigValue(key, configPath) {
57
+ const config = loadConfig(configPath);
58
+ return config[key];
59
+ }
60
+
61
+ /**
62
+ * Set a single config value.
63
+ * @param {string} key - Internal config key
64
+ * @param {*} value
65
+ * @param {string} [configPath] - Override config path (for testing)
66
+ */
67
+ function setConfigValue(key, value, configPath) {
68
+ const config = loadConfig(configPath);
69
+ config[key] = value;
70
+ saveConfig(config, configPath);
71
+ }
72
+
73
+ /**
74
+ * Delete a single config value.
75
+ * @param {string} key - Internal config key
76
+ * @param {string} [configPath] - Override config path (for testing)
77
+ */
78
+ function deleteConfigValue(key, configPath) {
79
+ const config = loadConfig(configPath);
80
+ delete config[key];
81
+ saveConfig(config, configPath);
82
+ }
83
+
84
+ /**
85
+ * Mask a secret string: show first 4 + '...' + last 4 chars.
86
+ * If < 10 chars, return '****'.
87
+ * @param {string} value
88
+ * @returns {string}
89
+ */
90
+ function maskSecret(value) {
91
+ if (typeof value !== 'string') return String(value);
92
+ if (value.length < 10) return '****';
93
+ return value.slice(0, 4) + '...' + value.slice(-4);
94
+ }
95
+
96
+ module.exports = {
97
+ CONFIG_DIR,
98
+ CONFIG_PATH,
99
+ KEY_MAP,
100
+ SECRET_KEYS,
101
+ loadConfig,
102
+ saveConfig,
103
+ getConfigValue,
104
+ setConfigValue,
105
+ deleteConfigValue,
106
+ maskSecret,
107
+ };
package/src/lib/mongo.js CHANGED
@@ -2,15 +2,17 @@
2
2
 
3
3
  /**
4
4
  * Get MongoDB URI or exit with a helpful error.
5
+ * Checks: env var → config file.
5
6
  * @returns {string}
6
7
  */
7
8
  function requireMongoUri() {
8
- const uri = process.env.MONGODB_URI;
9
+ const { getConfigValue } = require('./config');
10
+ const uri = process.env.MONGODB_URI || getConfigValue('mongodbUri');
9
11
  if (!uri) {
10
- console.error('Error: MONGODB_URI environment variable is not set.');
12
+ console.error('Error: MONGODB_URI is not set.');
11
13
  console.error('');
12
- console.error('Set your Atlas connection string:');
13
- console.error(' export MONGODB_URI="mongodb+srv://user:pass@cluster.mongodb.net/"');
14
+ console.error('Option 1: export MONGODB_URI="mongodb+srv://user:pass@cluster.mongodb.net/"');
15
+ console.error('Option 2: vai config set mongodb-uri "mongodb+srv://user:pass@cluster.mongodb.net/"');
14
16
  process.exit(1);
15
17
  }
16
18
  return uri;
package/src/lib/ui.js ADDED
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const pc = require('picocolors');
4
+ const oraModule = require('ora');
5
+ const ora = oraModule.default || oraModule;
6
+
7
+ // Semantic color helpers
8
+ const ui = {
9
+ // Status indicators
10
+ success: (msg) => `${pc.green('✓')} ${msg}`,
11
+ error: (msg) => `${pc.red('✗')} ${msg}`,
12
+ warn: (msg) => `${pc.yellow('⚠')} ${msg}`,
13
+ info: (msg) => `${pc.cyan('ℹ')} ${msg}`,
14
+
15
+ // Text styling
16
+ bold: pc.bold,
17
+ dim: pc.dim,
18
+ green: pc.green,
19
+ red: pc.red,
20
+ cyan: pc.cyan,
21
+ yellow: pc.yellow,
22
+
23
+ // Labels
24
+ label: (key, value) => ` ${pc.dim(key + ':')} ${value}`,
25
+
26
+ // Score formatting (0-1 scale: green > 0.7, yellow > 0.4, red otherwise)
27
+ score: (val) => {
28
+ const formatted = val.toFixed(6);
29
+ if (val >= 0.7) return pc.green(formatted);
30
+ if (val >= 0.4) return pc.yellow(formatted);
31
+ return pc.red(formatted);
32
+ },
33
+
34
+ // Index status coloring
35
+ status: (s) => {
36
+ const upper = (s || '').toUpperCase();
37
+ if (upper === 'READY') return pc.green(s);
38
+ if (upper === 'BUILDING') return pc.yellow(s);
39
+ if (upper === 'FAILED') return pc.red(s);
40
+ return s;
41
+ },
42
+
43
+ // Spinner
44
+ spinner: (text) => ora({ text, color: 'cyan' }),
45
+ };
46
+
47
+ module.exports = ui;
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ const { describe, it, beforeEach, afterEach } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { Command } = require('commander');
6
+ const { registerConfig } = require('../../src/commands/config');
7
+
8
+ describe('config command', () => {
9
+ it('registers config command with subcommands', () => {
10
+ const program = new Command();
11
+ program.exitOverride();
12
+ registerConfig(program);
13
+
14
+ const configCmd = program.commands.find(c => c.name() === 'config');
15
+ assert.ok(configCmd, 'config command should be registered');
16
+
17
+ const subNames = configCmd.commands.map(c => c.name());
18
+ assert.ok(subNames.includes('set'), 'should have set subcommand');
19
+ assert.ok(subNames.includes('get'), 'should have get subcommand');
20
+ assert.ok(subNames.includes('delete'), 'should have delete subcommand');
21
+ assert.ok(subNames.includes('path'), 'should have path subcommand');
22
+ assert.ok(subNames.includes('reset'), 'should have reset subcommand');
23
+ });
24
+
25
+ it('config set/get/delete round-trip via lib functions', () => {
26
+ // This test exercises the underlying lib (already tested in config.test.js)
27
+ // but validates the key mapping used by the command
28
+ const { KEY_MAP } = require('../../src/lib/config');
29
+
30
+ assert.equal(KEY_MAP['api-key'], 'apiKey');
31
+ assert.equal(KEY_MAP['mongodb-uri'], 'mongodbUri');
32
+ assert.equal(KEY_MAP['default-model'], 'defaultModel');
33
+ assert.equal(KEY_MAP['default-dimensions'], 'defaultDimensions');
34
+ });
35
+ });
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const { describe, it } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { Command } = require('commander');
6
+ const { registerDemo } = require('../../src/commands/demo');
7
+
8
+ describe('demo command', () => {
9
+ it('registers correctly on a program', () => {
10
+ const program = new Command();
11
+ registerDemo(program);
12
+ const demoCmd = program.commands.find(c => c.name() === 'demo');
13
+ assert.ok(demoCmd, 'demo command should be registered');
14
+ });
15
+
16
+ it('has --no-pause option', () => {
17
+ const program = new Command();
18
+ registerDemo(program);
19
+ const demoCmd = program.commands.find(c => c.name() === 'demo');
20
+ const opts = demoCmd.options.map(o => o.long);
21
+ assert.ok(opts.includes('--no-pause'), 'Should have --no-pause option');
22
+ });
23
+
24
+ it('has --skip-pipeline option', () => {
25
+ const program = new Command();
26
+ registerDemo(program);
27
+ const demoCmd = program.commands.find(c => c.name() === 'demo');
28
+ const opts = demoCmd.options.map(o => o.long);
29
+ assert.ok(opts.includes('--skip-pipeline'), 'Should have --skip-pipeline option');
30
+ });
31
+
32
+ it('has --keep option', () => {
33
+ const program = new Command();
34
+ registerDemo(program);
35
+ const demoCmd = program.commands.find(c => c.name() === 'demo');
36
+ const opts = demoCmd.options.map(o => o.long);
37
+ assert.ok(opts.includes('--keep'), 'Should have --keep option');
38
+ });
39
+
40
+ it('has correct description', () => {
41
+ const program = new Command();
42
+ registerDemo(program);
43
+ const demoCmd = program.commands.find(c => c.name() === 'demo');
44
+ assert.ok(demoCmd.description().includes('walkthrough'), 'Should mention walkthrough');
45
+ });
46
+ });
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ const { describe, it, beforeEach, afterEach, mock } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { Command } = require('commander');
6
+ const { registerModels } = require('../../src/commands/models');
7
+
8
+ describe('models command', () => {
9
+ let originalLog;
10
+ let output;
11
+
12
+ beforeEach(() => {
13
+ originalLog = console.log;
14
+ output = [];
15
+ console.log = (...args) => output.push(args.join(' '));
16
+ });
17
+
18
+ afterEach(() => {
19
+ console.log = originalLog;
20
+ mock.restoreAll();
21
+ });
22
+
23
+ it('registers correctly on a program', () => {
24
+ const program = new Command();
25
+ registerModels(program);
26
+ const modelsCmd = program.commands.find(c => c.name() === 'models');
27
+ assert.ok(modelsCmd, 'models command should be registered');
28
+ assert.ok(modelsCmd.description().includes('model'), 'should have a description about models');
29
+ });
30
+
31
+ it('lists all models by default', async () => {
32
+ const program = new Command();
33
+ program.exitOverride();
34
+ registerModels(program);
35
+
36
+ await program.parseAsync(['node', 'test', 'models', '--quiet']);
37
+
38
+ const combined = output.join('\n');
39
+ assert.ok(combined.includes('voyage-4-large'), 'Should include voyage-4-large');
40
+ assert.ok(combined.includes('rerank-2.5'), 'Should include rerank-2.5');
41
+ });
42
+
43
+ it('filters by embedding type', async () => {
44
+ const program = new Command();
45
+ program.exitOverride();
46
+ registerModels(program);
47
+
48
+ await program.parseAsync(['node', 'test', 'models', '--type', 'embedding', '--quiet']);
49
+
50
+ const combined = output.join('\n');
51
+ assert.ok(combined.includes('voyage-4-large'), 'Should include embedding models');
52
+ assert.ok(!combined.includes('rerank-2.5\n'), 'Should not include reranking in data rows');
53
+ // More precise: check that rerank-2.5 doesn't appear as a data row start
54
+ const lines = combined.split('\n');
55
+ const dataLines = lines.filter(l => !l.includes('─') && l.trim().length > 0);
56
+ const hasRerankRow = dataLines.some(l => l.trim().startsWith('rerank-2.5'));
57
+ assert.ok(!hasRerankRow, 'Should not have reranking model rows');
58
+ });
59
+
60
+ it('filters by reranking type', async () => {
61
+ const program = new Command();
62
+ program.exitOverride();
63
+ registerModels(program);
64
+
65
+ await program.parseAsync(['node', 'test', 'models', '--type', 'reranking', '--quiet']);
66
+
67
+ const combined = output.join('\n');
68
+ assert.ok(combined.includes('rerank'), 'Should include reranking models');
69
+ const lines = combined.split('\n');
70
+ const dataLines = lines.filter(l => !l.includes('─') && !l.includes('Model') && l.trim().length > 0);
71
+ const hasEmbedRow = dataLines.some(l => l.includes('voyage-4-large'));
72
+ assert.ok(!hasEmbedRow, 'Should not have embedding model rows');
73
+ });
74
+
75
+ it('outputs JSON when --json flag is used', async () => {
76
+ const program = new Command();
77
+ program.exitOverride();
78
+ registerModels(program);
79
+
80
+ await program.parseAsync(['node', 'test', 'models', '--json']);
81
+
82
+ const combined = output.join('\n');
83
+ const parsed = JSON.parse(combined);
84
+ assert.ok(Array.isArray(parsed));
85
+ assert.ok(parsed.length > 0);
86
+ assert.ok(parsed[0].name);
87
+ assert.ok(parsed[0].type);
88
+ });
89
+ });
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+
3
+ const { describe, it, beforeEach, afterEach, mock } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { Command } = require('commander');
6
+ const { registerPing } = require('../../src/commands/ping');
7
+
8
+ describe('ping command', () => {
9
+ let originalLog;
10
+ let originalError;
11
+ let originalExit;
12
+ let originalKey;
13
+ let originalMongoUri;
14
+ let output;
15
+ let errorOutput;
16
+
17
+ beforeEach(() => {
18
+ originalLog = console.log;
19
+ originalError = console.error;
20
+ originalExit = process.exit;
21
+ originalKey = process.env.VOYAGE_API_KEY;
22
+ originalMongoUri = process.env.MONGODB_URI;
23
+ output = [];
24
+ errorOutput = [];
25
+ console.log = (...args) => output.push(args.join(' '));
26
+ console.error = (...args) => errorOutput.push(args.join(' '));
27
+ // Remove MONGODB_URI by default so we don't accidentally test mongo
28
+ delete process.env.MONGODB_URI;
29
+ });
30
+
31
+ afterEach(() => {
32
+ console.log = originalLog;
33
+ console.error = originalError;
34
+ process.exit = originalExit;
35
+ if (originalKey !== undefined) {
36
+ process.env.VOYAGE_API_KEY = originalKey;
37
+ } else {
38
+ delete process.env.VOYAGE_API_KEY;
39
+ }
40
+ if (originalMongoUri !== undefined) {
41
+ process.env.MONGODB_URI = originalMongoUri;
42
+ } else {
43
+ delete process.env.MONGODB_URI;
44
+ }
45
+ mock.restoreAll();
46
+ });
47
+
48
+ it('registers correctly on a program', () => {
49
+ const program = new Command();
50
+ registerPing(program);
51
+ const pingCmd = program.commands.find(c => c.name() === 'ping');
52
+ assert.ok(pingCmd, 'ping command should be registered');
53
+ });
54
+
55
+ it('prints success on valid API response', async () => {
56
+ process.env.VOYAGE_API_KEY = 'test-key';
57
+
58
+ mock.method(global, 'fetch', async () => ({
59
+ ok: true,
60
+ status: 200,
61
+ json: async () => ({
62
+ data: [{ embedding: new Array(1024).fill(0) }],
63
+ usage: { total_tokens: 1 },
64
+ }),
65
+ }));
66
+
67
+ const program = new Command();
68
+ program.exitOverride();
69
+ registerPing(program);
70
+
71
+ await program.parseAsync(['node', 'test', 'ping']);
72
+
73
+ const combined = output.join('\n');
74
+ assert.ok(combined.includes('✓ Connected to Voyage AI API'), 'Should show success message');
75
+ assert.ok(combined.includes('voyage-4-lite'), 'Should show model name');
76
+ assert.ok(combined.includes('1024'), 'Should show dimensions');
77
+ });
78
+
79
+ it('exits with error on auth failure', async () => {
80
+ process.env.VOYAGE_API_KEY = 'bad-key';
81
+
82
+ mock.method(global, 'fetch', async () => ({
83
+ ok: false,
84
+ status: 401,
85
+ text: async () => 'Unauthorized',
86
+ }));
87
+
88
+ let exitCode = null;
89
+ process.exit = (code) => {
90
+ exitCode = code;
91
+ throw new Error('process.exit called');
92
+ };
93
+
94
+ const program = new Command();
95
+ program.exitOverride();
96
+ registerPing(program);
97
+
98
+ await assert.rejects(
99
+ () => program.parseAsync(['node', 'test', 'ping']),
100
+ /process\.exit called/
101
+ );
102
+
103
+ assert.equal(exitCode, 1);
104
+ const combined = errorOutput.join('\n');
105
+ assert.ok(combined.includes('Authentication failed'), 'Should show auth error');
106
+ });
107
+
108
+ it('exits when VOYAGE_API_KEY is not set', async () => {
109
+ delete process.env.VOYAGE_API_KEY;
110
+
111
+ let exitCode = null;
112
+ process.exit = (code) => {
113
+ exitCode = code;
114
+ throw new Error('process.exit called');
115
+ };
116
+
117
+ const program = new Command();
118
+ program.exitOverride();
119
+ registerPing(program);
120
+
121
+ await assert.rejects(
122
+ () => program.parseAsync(['node', 'test', 'ping']),
123
+ /process\.exit called/
124
+ );
125
+
126
+ assert.equal(exitCode, 1);
127
+ const combined = errorOutput.join('\n');
128
+ assert.ok(combined.includes('VOYAGE_API_KEY'), 'Should mention missing key');
129
+ });
130
+
131
+ it('outputs JSON when --json flag is used', async () => {
132
+ process.env.VOYAGE_API_KEY = 'test-key';
133
+
134
+ mock.method(global, 'fetch', async () => ({
135
+ ok: true,
136
+ status: 200,
137
+ json: async () => ({
138
+ data: [{ embedding: new Array(1024).fill(0) }],
139
+ usage: { total_tokens: 1 },
140
+ }),
141
+ }));
142
+
143
+ const program = new Command();
144
+ program.exitOverride();
145
+ registerPing(program);
146
+
147
+ await program.parseAsync(['node', 'test', 'ping', '--json']);
148
+
149
+ const combined = output.join('\n');
150
+ const parsed = JSON.parse(combined);
151
+ assert.equal(parsed.ok, true);
152
+ assert.ok(parsed.voyage);
153
+ assert.equal(parsed.voyage.ok, true);
154
+ });
155
+ });
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ const { describe, it, beforeEach, afterEach, mock } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+
6
+ describe('api', () => {
7
+ let originalKey;
8
+ let originalExit;
9
+
10
+ beforeEach(() => {
11
+ originalKey = process.env.VOYAGE_API_KEY;
12
+ originalExit = process.exit;
13
+ });
14
+
15
+ afterEach(() => {
16
+ if (originalKey !== undefined) {
17
+ process.env.VOYAGE_API_KEY = originalKey;
18
+ } else {
19
+ delete process.env.VOYAGE_API_KEY;
20
+ }
21
+ process.exit = originalExit;
22
+ mock.restoreAll();
23
+ });
24
+
25
+ describe('requireApiKey', () => {
26
+ it('throws/exits when VOYAGE_API_KEY is not set', () => {
27
+ delete process.env.VOYAGE_API_KEY;
28
+ // Re-require to get fresh module
29
+ delete require.cache[require.resolve('../../src/lib/api')];
30
+ const { requireApiKey } = require('../../src/lib/api');
31
+
32
+ let exitCode = null;
33
+ process.exit = (code) => {
34
+ exitCode = code;
35
+ throw new Error('process.exit called');
36
+ };
37
+
38
+ assert.throws(() => requireApiKey(), /process\.exit called/);
39
+ assert.equal(exitCode, 1);
40
+ });
41
+
42
+ it('returns key when VOYAGE_API_KEY is set', () => {
43
+ process.env.VOYAGE_API_KEY = 'test-key-123';
44
+ delete require.cache[require.resolve('../../src/lib/api')];
45
+ const { requireApiKey } = require('../../src/lib/api');
46
+
47
+ const key = requireApiKey();
48
+ assert.equal(key, 'test-key-123');
49
+ });
50
+ });
51
+
52
+ describe('apiRequest', () => {
53
+ it('returns parsed JSON on success', async () => {
54
+ process.env.VOYAGE_API_KEY = 'test-key';
55
+ delete require.cache[require.resolve('../../src/lib/api')];
56
+ const { apiRequest } = require('../../src/lib/api');
57
+
58
+ const mockResponse = {
59
+ ok: true,
60
+ status: 200,
61
+ json: async () => ({ data: [{ embedding: [1, 2, 3] }] }),
62
+ headers: new Map(),
63
+ };
64
+ mock.method(global, 'fetch', async () => mockResponse);
65
+
66
+ const result = await apiRequest('/embeddings', { input: ['test'], model: 'voyage-4-lite' });
67
+ assert.deepEqual(result, { data: [{ embedding: [1, 2, 3] }] });
68
+ });
69
+
70
+ it('exits on non-200 response', async () => {
71
+ process.env.VOYAGE_API_KEY = 'test-key';
72
+ delete require.cache[require.resolve('../../src/lib/api')];
73
+ const { apiRequest } = require('../../src/lib/api');
74
+
75
+ let exitCode = null;
76
+ process.exit = (code) => {
77
+ exitCode = code;
78
+ throw new Error('process.exit called');
79
+ };
80
+
81
+ const mockResponse = {
82
+ ok: false,
83
+ status: 400,
84
+ json: async () => ({ detail: 'Bad request' }),
85
+ headers: new Map(),
86
+ };
87
+ mock.method(global, 'fetch', async () => mockResponse);
88
+
89
+ await assert.rejects(
90
+ () => apiRequest('/embeddings', { input: ['test'], model: 'voyage-4-lite' }),
91
+ /process\.exit called/
92
+ );
93
+ assert.equal(exitCode, 1);
94
+ });
95
+
96
+ it('retries on 429', async () => {
97
+ process.env.VOYAGE_API_KEY = 'test-key';
98
+ delete require.cache[require.resolve('../../src/lib/api')];
99
+ const { apiRequest } = require('../../src/lib/api');
100
+
101
+ let callCount = 0;
102
+ mock.method(global, 'fetch', async () => {
103
+ callCount++;
104
+ if (callCount === 1) {
105
+ return {
106
+ ok: false,
107
+ status: 429,
108
+ headers: { get: () => '0' },
109
+ json: async () => ({ detail: 'Rate limited' }),
110
+ };
111
+ }
112
+ return {
113
+ ok: true,
114
+ status: 200,
115
+ json: async () => ({ data: 'success' }),
116
+ headers: new Map(),
117
+ };
118
+ });
119
+
120
+ const result = await apiRequest('/embeddings', { input: ['test'], model: 'voyage-4-lite' });
121
+ assert.deepEqual(result, { data: 'success' });
122
+ assert.equal(callCount, 2, 'Should have retried once');
123
+ });
124
+ });
125
+ });
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ const { describe, it, beforeEach, afterEach } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { showBanner, showQuickStart, getVersion } = require('../../src/lib/banner');
6
+
7
+ describe('banner', () => {
8
+ let originalLog;
9
+ let output;
10
+
11
+ beforeEach(() => {
12
+ originalLog = console.log;
13
+ output = [];
14
+ console.log = (...args) => output.push(args.join(' '));
15
+ });
16
+
17
+ afterEach(() => {
18
+ console.log = originalLog;
19
+ });
20
+
21
+ it('getVersion returns a semver string', () => {
22
+ const version = getVersion();
23
+ assert.ok(/^\d+\.\d+\.\d+/.test(version), `Expected semver, got: ${version}`);
24
+ });
25
+
26
+ it('showBanner prints box with vai and Voyage AI CLI', () => {
27
+ showBanner();
28
+ const combined = output.join('\n');
29
+ assert.ok(combined.includes('vai'), 'Should include "vai"');
30
+ assert.ok(combined.includes('Voyage AI CLI'), 'Should include "Voyage AI CLI"');
31
+ assert.ok(combined.includes('╭'), 'Should include top border');
32
+ assert.ok(combined.includes('╰'), 'Should include bottom border');
33
+ });
34
+
35
+ it('showQuickStart prints quick start commands', () => {
36
+ showQuickStart();
37
+ const combined = output.join('\n');
38
+ assert.ok(combined.includes('Quick start'), 'Should include header');
39
+ assert.ok(combined.includes('vai ping'), 'Should include ping');
40
+ assert.ok(combined.includes('vai embed'), 'Should include embed');
41
+ assert.ok(combined.includes('vai models'), 'Should include models');
42
+ assert.ok(combined.includes('vai demo'), 'Should include demo');
43
+ });
44
+ });