vcode-cli 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vynthen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # V Code CLI
2
+
3
+ > Connect your local machine to [Vynthen](https://vynthen.com) V Code — a powerful AI code agent with full system control, secure keychain auth, and human-in-the-loop permissions.
4
+
5
+ ```
6
+ ╔═══════════════════════════════════════════════════════╗
7
+ ║ ██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗ ║
8
+ ║ ██║ ██║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ║
9
+ ║ ██║ ██║ ██║ ██║ ██║██║ ██║█████╗ ║
10
+ ║ ╚██╗ ██╔╝ ██║ ██║ ██║██║ ██║██╔══╝ ║
11
+ ║ ╚████╔╝ ╚██████╗╚██████╔╝██████╔╝███████╗ ║
12
+ ║ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ║
13
+ ║ ┌──────────────────────┐ ║
14
+ ║ │ ◉ ◉ │ ║
15
+ ║ │ ─── │ ║
16
+ ║ └──────────────────────┘ ║
17
+ ╚═══════════════════════════════════════════════════════╝
18
+ ```
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install -g vcode-cli
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```bash
29
+ # 1. Login with your Vynthen account
30
+ vcode login
31
+
32
+ # 2. Navigate to your project
33
+ cd /path/to/your/project
34
+
35
+ # 3. Start the bridge
36
+ vcode start
37
+ ```
38
+
39
+ Then open [vynthen.com](https://vynthen.com) and use V Code — it will operate on your local machine through the secure bridge.
40
+
41
+ ## Commands
42
+
43
+ | Command | Description |
44
+ |---------|-------------|
45
+ | `vcode login` | Authenticate with your Vynthen email and password |
46
+ | `vcode start` | Start the V Code bridge and connect to Vynthen |
47
+ | `vcode start --approve-all` | Start with all operations auto-approved |
48
+ | `vcode logout` | Clear stored authentication token |
49
+ | `vcode status` | Show connection status and logged-in user |
50
+ | `vcode --version` | Show version |
51
+
52
+ ## What Can V Code Do?
53
+
54
+ Once connected, V Code can perform operations on your local machine — **every single operation requires your explicit approval** in the terminal.
55
+
56
+ ### File System
57
+ - 📖 Read files and directories
58
+ - ✏️ Create, edit, and write files
59
+ - 📁 Create and delete directories
60
+ - 🔄 Rename and move files
61
+
62
+ ### Terminal
63
+ - ⚡ Execute any shell command
64
+ - 📦 Run scripts and install packages
65
+
66
+ ### Desktop Control
67
+ - 🖱️ Mouse: move, click, right-click, double-click, drag
68
+ - ⌨️ Keyboard: type text, press key combinations
69
+ - 📸 Screenshot: capture full screen or specific window
70
+
71
+ ### System
72
+ - 📂 Open folders in file manager
73
+ - 🌐 Open URLs in browser
74
+ - 📋 Read and write clipboard
75
+ - 🔍 List and manage running processes
76
+
77
+ ## Permission System
78
+
79
+ Every operation is color-coded by risk level:
80
+
81
+ - 🔴 **Red** — Destructive operations (delete, kill process)
82
+ - 🟡 **Yellow** — Read operations (read file, screenshot)
83
+ - 🔵 **Blue** — Write operations (create, edit file)
84
+ - 🟣 **Purple** — Execute operations (run command, open URL)
85
+
86
+ When V Code requests an operation, you'll see:
87
+
88
+ ```
89
+ 🔵 WRITE V Code wants to write file
90
+ Details: /home/user/project/index.js
91
+
92
+ Allow this operation? (Y/n/a)
93
+ y — Yes, approve this operation
94
+ n — No, deny this operation
95
+ a — Approve all for this session
96
+ ```
97
+
98
+ ## Security
99
+
100
+ - 🔐 **OS Keychain** — JWT stored in your system's native credential store (libsecret on Linux, Keychain on macOS, Credential Manager on Windows). Never stored in plain text.
101
+ - ⏱️ **8-Hour JWT** — Tokens expire automatically. Run `vcode login` to refresh.
102
+ - 🛑 **Human-in-the-Loop** — Zero automatic operations. Every action needs your explicit `y` approval.
103
+ - 🔒 **Encrypted Transport** — All traffic over WSS (encrypted WebSocket).
104
+ - 📝 **Audit Log** — Every approved and denied operation is logged to `~/.vcode/audit.log`.
105
+ - 🚦 **Rate Limited** — Login is rate-limited to 5 attempts per 15 minutes.
106
+
107
+ ## Audit Log
108
+
109
+ All operations are logged locally at `~/.vcode/audit.log`:
110
+
111
+ ```
112
+ [2026-04-04T10:30:15.123Z] APPROVED READ_FILE /home/user/project/src/app.tsx
113
+ [2026-04-04T10:30:18.456Z] APPROVED WRITE_FILE /home/user/project/src/app.tsx
114
+ [2026-04-04T10:30:22.789Z] DENIED DELETE_FILE /home/user/project/.env
115
+ [2026-04-04T10:30:25.012Z] APPROVED EXEC_COMMAND `npm run build` in /home/user/project
116
+ ```
117
+
118
+ ## System Requirements
119
+
120
+ - **Node.js** ≥ 18.0.0
121
+ - **OS**: Linux, macOS, or Windows
122
+ - For mouse/keyboard control: `@nut-tree/nut-js` (optional, installed separately)
123
+
124
+ ## Troubleshooting
125
+
126
+ ### "Not authenticated" error
127
+ Run `vcode login` to authenticate with your Vynthen account.
128
+
129
+ ### "Token expired" error
130
+ Your JWT has expired (8-hour lifetime). Run `vcode login` again.
131
+
132
+ ### Mouse/keyboard features not working
133
+ Install the optional native module:
134
+ ```bash
135
+ npm install -g @nut-tree/nut-js
136
+ ```
137
+
138
+ ### Connection issues
139
+ Check your internet connection and ensure `wss://vynthen.com` is accessible. The CLI will auto-reconnect with exponential backoff.
140
+
141
+ ## License
142
+
143
+ MIT © [Vynthen](https://vynthen.com)
package/bin/vcode.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * V Code CLI — Vynthen Local Bridge
5
+ * Entry point for all CLI commands.
6
+ */
7
+
8
+ import { Command } from 'commander';
9
+ import { readFileSync } from 'fs';
10
+ import { fileURLToPath } from 'url';
11
+ import { dirname, join } from 'path';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
16
+
17
+ const program = new Command();
18
+
19
+ program
20
+ .name('vcode')
21
+ .description('Connect your local machine to Vynthen V Code')
22
+ .version(pkg.version, '-v, --version');
23
+
24
+ program
25
+ .command('login')
26
+ .description('Authenticate with your Vynthen account')
27
+ .action(async () => {
28
+ const { login } = await import('../lib/commands/login.js');
29
+ await login();
30
+ });
31
+
32
+ program
33
+ .command('start')
34
+ .description('Start the V Code bridge and connect to Vynthen')
35
+ .option('--approve-all', 'Auto-approve all operations for this session')
36
+ .action(async (opts) => {
37
+ const { start } = await import('../lib/commands/start.js');
38
+ await start(opts);
39
+ });
40
+
41
+ program
42
+ .command('logout')
43
+ .description('Clear stored authentication token')
44
+ .action(async () => {
45
+ const { logout } = await import('../lib/commands/logout.js');
46
+ await logout();
47
+ });
48
+
49
+ program
50
+ .command('status')
51
+ .description('Show connection status and logged-in user')
52
+ .action(async () => {
53
+ const { status } = await import('../lib/commands/status.js');
54
+ await status();
55
+ });
56
+
57
+ program.parse(process.argv);
58
+
59
+ // Show help if no command provided
60
+ if (!process.argv.slice(2).length) {
61
+ const { showLogo } = await import('../lib/logo.js');
62
+ await showLogo(false);
63
+ program.outputHelp();
64
+ }
package/lib/audit.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * V Code — Audit Logger.
3
+ * Logs every approved operation to ~/.vcode/audit.log
4
+ */
5
+
6
+ import { appendFileSync, mkdirSync, existsSync, readFileSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { homedir } from 'os';
9
+
10
+ const VCODE_DIR = join(homedir(), '.vcode');
11
+ const AUDIT_FILE = join(VCODE_DIR, 'audit.log');
12
+
13
+ function ensureDir() {
14
+ if (!existsSync(VCODE_DIR)) {
15
+ mkdirSync(VCODE_DIR, { recursive: true });
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Append an entry to the audit log.
21
+ * @param {string} action — e.g. 'FILE_READ', 'FILE_WRITE', 'EXEC_COMMAND'
22
+ * @param {string} detail — human-readable description
23
+ * @param {'approved'|'denied'} decision
24
+ */
25
+ export function logAudit(action, detail, decision) {
26
+ ensureDir();
27
+ const ts = new Date().toISOString();
28
+ const line = `[${ts}] ${decision.toUpperCase().padEnd(8)} ${action.padEnd(20)} ${detail}\n`;
29
+ appendFileSync(AUDIT_FILE, line, 'utf8');
30
+ }
31
+
32
+ /**
33
+ * Read audit log contents (last N lines).
34
+ */
35
+ export function readAuditLog(lines = 50) {
36
+ ensureDir();
37
+ if (!existsSync(AUDIT_FILE)) return '(no audit log yet)';
38
+ const content = readFileSync(AUDIT_FILE, 'utf8');
39
+ const allLines = content.trim().split('\n');
40
+ return allLines.slice(-lines).join('\n');
41
+ }
42
+
43
+ /**
44
+ * Get the path to the audit log.
45
+ */
46
+ export function getAuditPath() {
47
+ return AUDIT_FILE;
48
+ }
package/lib/auth.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * V Code — Authentication module.
3
+ * Handles login against Vynthen API with rate limiting awareness.
4
+ */
5
+
6
+ const API_URL = 'https://vynthen.com/api/auth/cli-login';
7
+
8
+ /**
9
+ * Authenticate with email and password.
10
+ * Returns { token, user } on success.
11
+ * Throws on failure with descriptive message.
12
+ */
13
+ export async function authenticate(email, password) {
14
+ const res = await fetch(API_URL, {
15
+ method: 'POST',
16
+ headers: { 'Content-Type': 'application/json' },
17
+ body: JSON.stringify({ email, password }),
18
+ });
19
+
20
+ if (res.status === 429) {
21
+ throw new Error('Rate limited — too many login attempts. Try again in 15 minutes.');
22
+ }
23
+
24
+ if (res.status === 401) {
25
+ throw new Error('Invalid email or password.');
26
+ }
27
+
28
+ if (!res.ok) {
29
+ const body = await res.text().catch(() => '');
30
+ throw new Error(`Authentication failed (${res.status}): ${body || res.statusText}`);
31
+ }
32
+
33
+ const data = await res.json();
34
+
35
+ if (!data.token) {
36
+ throw new Error('Server returned no token. Please try again.');
37
+ }
38
+
39
+ return {
40
+ token: data.token,
41
+ user: data.user || { email },
42
+ };
43
+ }
package/lib/bridge.js ADDED
@@ -0,0 +1,245 @@
1
+ /**
2
+ * V Code — WebSocket Bridge.
3
+ * Connects to Vynthen V Code backend over encrypted WSS.
4
+ * Receives operation requests, dispatches them locally, sends results back.
5
+ */
6
+
7
+ import WebSocket from 'ws';
8
+ import chalk from 'chalk';
9
+ import ora from 'ora';
10
+ import { dispatch } from './dispatcher.js';
11
+ import { getToken, isTokenExpired } from './keychain.js';
12
+
13
+ const WS_URL = 'wss://vynthen.com/api/vcode-ws';
14
+ const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 15000, 30000]; // exponential backoff
15
+
16
+ export class Bridge {
17
+ constructor({ onStatusChange, onOperation, onError }) {
18
+ this.ws = null;
19
+ this.token = null;
20
+ this.connected = false;
21
+ this.reconnectAttempt = 0;
22
+ this.intentionalClose = false;
23
+ this.spinner = null;
24
+ this.onStatusChange = onStatusChange || (() => {});
25
+ this.onOperation = onOperation || (() => {});
26
+ this.onError = onError || (() => {});
27
+ this.operationCount = 0;
28
+ this.startTime = null;
29
+ }
30
+
31
+ /**
32
+ * Start the bridge connection.
33
+ */
34
+ async connect() {
35
+ this.token = await getToken();
36
+
37
+ if (!this.token) {
38
+ throw new Error('Not authenticated. Run `vcode login` first.');
39
+ }
40
+
41
+ if (isTokenExpired(this.token)) {
42
+ throw new Error('Token expired. Run `vcode login` to re-authenticate.');
43
+ }
44
+
45
+ this.startTime = Date.now();
46
+ this.intentionalClose = false;
47
+ this._connect();
48
+ }
49
+
50
+ /**
51
+ * Internal connect with reconnection logic.
52
+ */
53
+ _connect() {
54
+ if (this.intentionalClose) return;
55
+
56
+ this.spinner = ora({
57
+ text: chalk.gray('Connecting to Vynthen V Code...'),
58
+ spinner: 'dots12',
59
+ color: 'magenta',
60
+ }).start();
61
+
62
+ this.ws = new WebSocket(WS_URL, {
63
+ headers: {
64
+ Authorization: `Bearer ${this.token}`,
65
+ },
66
+ });
67
+
68
+ this.ws.on('open', () => {
69
+ this.connected = true;
70
+ this.reconnectAttempt = 0;
71
+
72
+ if (this.spinner) this.spinner.succeed(chalk.green('Connected to Vynthen V Code'));
73
+
74
+ this.onStatusChange('connected');
75
+
76
+ // Send system info
77
+ import('os').then(os => {
78
+ this.send({
79
+ type: 'CLIENT_INFO',
80
+ data: {
81
+ platform: process.platform,
82
+ arch: process.arch,
83
+ nodeVersion: process.version,
84
+ cwd: process.cwd(),
85
+ hostname: os.hostname(),
86
+ username: os.userInfo().username,
87
+ homedir: os.homedir(),
88
+ },
89
+ });
90
+ });
91
+
92
+ console.log(chalk.gray(' Waiting for operations from Vynthen...\n'));
93
+ });
94
+
95
+ this.ws.on('message', async (raw) => {
96
+ try {
97
+ const message = JSON.parse(raw.toString());
98
+ await this._handleMessage(message);
99
+ } catch (err) {
100
+ console.error(chalk.red(` ✗ Failed to parse message: ${err.message}`));
101
+ }
102
+ });
103
+
104
+ this.ws.on('close', (code, reason) => {
105
+ this.connected = false;
106
+
107
+ if (this.intentionalClose) {
108
+ console.log(chalk.gray('\n Bridge disconnected.'));
109
+ this.onStatusChange('disconnected');
110
+ return;
111
+ }
112
+
113
+ const reasonStr = reason?.toString() || '';
114
+
115
+ if (code === 4001) {
116
+ console.log(chalk.red('\n ✗ Authentication failed. Run `vcode login` again.'));
117
+ this.onStatusChange('auth_failed');
118
+ return;
119
+ }
120
+
121
+ if (code === 4002) {
122
+ console.log(chalk.red('\n ✗ Token expired. Run `vcode login` to re-authenticate.'));
123
+ this.onStatusChange('token_expired');
124
+ return;
125
+ }
126
+
127
+ this.onStatusChange('reconnecting');
128
+ this._reconnect();
129
+ });
130
+
131
+ this.ws.on('error', (err) => {
132
+ if (this.spinner) this.spinner.fail(chalk.red(`Connection error: ${err.message}`));
133
+ this.onError(err);
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Reconnect with exponential backoff.
139
+ */
140
+ _reconnect() {
141
+ if (this.intentionalClose) return;
142
+
143
+ const delay = RECONNECT_DELAYS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS.length - 1)];
144
+ this.reconnectAttempt++;
145
+
146
+ console.log(chalk.yellow(` ↻ Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempt})...`));
147
+
148
+ setTimeout(() => {
149
+ if (!this.intentionalClose) {
150
+ this._connect();
151
+ }
152
+ }, delay);
153
+ }
154
+
155
+ /**
156
+ * Handle incoming message from server.
157
+ */
158
+ async _handleMessage(message) {
159
+ const { type, id, data } = message;
160
+
161
+ switch (type) {
162
+ case 'OPERATION': {
163
+ this.operationCount++;
164
+ const opType = data?.operation;
165
+ const params = data?.params || {};
166
+
167
+ const ts = new Date().toLocaleTimeString();
168
+ console.log(chalk.gray(` [${ts}] `) + chalk.cyan(`Operation #${this.operationCount}: `) + chalk.white(opType));
169
+
170
+ this.onOperation(opType, params);
171
+
172
+ try {
173
+ const result = await dispatch(opType, params);
174
+
175
+ this.send({
176
+ type: 'OPERATION_RESULT',
177
+ id,
178
+ data: result,
179
+ });
180
+
181
+ if (result.success) {
182
+ console.log(chalk.green(` ✓ ${opType} completed successfully\n`));
183
+ } else {
184
+ console.log(chalk.red(` ✗ ${opType} failed: ${result.error}\n`));
185
+ }
186
+ } catch (err) {
187
+ this.send({
188
+ type: 'OPERATION_RESULT',
189
+ id,
190
+ data: { success: false, error: err.message },
191
+ });
192
+ console.log(chalk.red(` ✗ ${opType} threw error: ${err.message}\n`));
193
+ }
194
+ break;
195
+ }
196
+
197
+ case 'PING':
198
+ this.send({ type: 'PONG' });
199
+ break;
200
+
201
+ case 'SERVER_MESSAGE':
202
+ console.log(chalk.hex('#a855f7')(` ⓘ Server: ${data?.message || ''}`));
203
+ break;
204
+
205
+ default:
206
+ // Unknown message type — ignore
207
+ break;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Send a message to the server.
213
+ */
214
+ send(message) {
215
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
216
+ this.ws.send(JSON.stringify(message));
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Gracefully disconnect.
222
+ */
223
+ disconnect() {
224
+ this.intentionalClose = true;
225
+ if (this.ws) {
226
+ this.ws.close(1000, 'Client disconnecting');
227
+ this.ws = null;
228
+ }
229
+ this.connected = false;
230
+ }
231
+
232
+ /**
233
+ * Get uptime in human-readable format.
234
+ */
235
+ getUptime() {
236
+ if (!this.startTime) return '0s';
237
+ const diff = Date.now() - this.startTime;
238
+ const hours = Math.floor(diff / 3600000);
239
+ const mins = Math.floor((diff % 3600000) / 60000);
240
+ const secs = Math.floor((diff % 60000) / 1000);
241
+ if (hours > 0) return `${hours}h ${mins}m`;
242
+ if (mins > 0) return `${mins}m ${secs}s`;
243
+ return `${secs}s`;
244
+ }
245
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * V Code — Login Command.
3
+ * Prompts for Vynthen credentials and stores JWT in OS keychain.
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import inquirer from 'inquirer';
8
+ import ora from 'ora';
9
+ import { authenticate } from '../auth.js';
10
+ import { storeToken, storeUser, getToken, isTokenExpired } from '../keychain.js';
11
+ import { showLogo } from '../logo.js';
12
+
13
+ export async function login() {
14
+ await showLogo();
15
+
16
+ // Check if already logged in with valid token
17
+ const existing = await getToken();
18
+ if (existing && !isTokenExpired(existing)) {
19
+ console.log(chalk.green(' ✓ You are already logged in with a valid token.\n'));
20
+ const { relogin } = await inquirer.prompt([
21
+ {
22
+ type: 'confirm',
23
+ name: 'relogin',
24
+ message: 'Do you want to login again?',
25
+ default: false,
26
+ },
27
+ ]);
28
+ if (!relogin) {
29
+ console.log(chalk.gray(' Keeping existing session.\n'));
30
+ return;
31
+ }
32
+ }
33
+
34
+ console.log(chalk.hex('#a855f7')(' Log in with your Vynthen account\n'));
35
+
36
+ const answers = await inquirer.prompt([
37
+ {
38
+ type: 'input',
39
+ name: 'email',
40
+ message: 'Email:',
41
+ validate: (val) => {
42
+ if (!val || !val.includes('@')) return 'Please enter a valid email address.';
43
+ return true;
44
+ },
45
+ },
46
+ {
47
+ type: 'password',
48
+ name: 'password',
49
+ message: 'Password:',
50
+ mask: '●',
51
+ validate: (val) => {
52
+ if (!val || val.length < 1) return 'Password cannot be empty.';
53
+ return true;
54
+ },
55
+ },
56
+ ]);
57
+
58
+ const spinner = ora({
59
+ text: chalk.gray('Authenticating...'),
60
+ spinner: 'dots12',
61
+ color: 'magenta',
62
+ }).start();
63
+
64
+ try {
65
+ const { token, user } = await authenticate(answers.email, answers.password);
66
+
67
+ await storeToken(token);
68
+ await storeUser(answers.email);
69
+
70
+ spinner.succeed(chalk.green('Authenticated successfully!'));
71
+
72
+ console.log('');
73
+ console.log(chalk.white(' Account: ') + chalk.cyan(answers.email));
74
+ console.log(chalk.white(' Token: ') + chalk.gray('Stored securely in OS keychain'));
75
+ console.log(chalk.white(' Expires: ') + chalk.gray('8 hours from now'));
76
+ console.log('');
77
+ console.log(chalk.hex('#a855f7')(' Run ') + chalk.white.bold('vcode start') + chalk.hex('#a855f7')(' to connect to Vynthen V Code.\n'));
78
+ } catch (err) {
79
+ spinner.fail(chalk.red('Authentication failed'));
80
+ console.log('');
81
+
82
+ if (err.message.includes('Rate limited')) {
83
+ console.log(chalk.red(' ⚠ Too many login attempts. Please wait 15 minutes.\n'));
84
+ } else if (err.message.includes('Invalid email')) {
85
+ console.log(chalk.red(' ✗ Invalid email or password. Please try again.\n'));
86
+ } else {
87
+ console.log(chalk.red(` ✗ ${err.message}\n`));
88
+ }
89
+
90
+ process.exit(1);
91
+ }
92
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * V Code — Logout Command.
3
+ * Clears stored JWT token and user info from OS keychain.
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import { deleteToken, deleteUser, getToken } from '../keychain.js';
9
+
10
+ export async function logout() {
11
+ const existing = await getToken();
12
+
13
+ if (!existing) {
14
+ console.log(chalk.yellow('\n ⚠ No active session found. You are already logged out.\n'));
15
+ return;
16
+ }
17
+
18
+ const spinner = ora({
19
+ text: chalk.gray('Clearing credentials from keychain...'),
20
+ spinner: 'dots12',
21
+ color: 'magenta',
22
+ }).start();
23
+
24
+ try {
25
+ await deleteToken();
26
+ await deleteUser();
27
+ spinner.succeed(chalk.green('Logged out successfully'));
28
+ console.log(chalk.gray(' Token and credentials removed from OS keychain.\n'));
29
+ } catch (err) {
30
+ spinner.fail(chalk.red('Failed to clear credentials'));
31
+ console.log(chalk.red(` ✗ ${err.message}\n`));
32
+ process.exit(1);
33
+ }
34
+ }