vigthoria-cli 1.8.19 → 1.9.5

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,367 +1,478 @@
1
1
  "use strict";
2
- /**
3
- * Auth Command - Authentication management
4
- */
5
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
- if (k2 === undefined) k2 = k;
7
- var desc = Object.getOwnPropertyDescriptor(m, k);
8
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
- desc = { enumerable: true, get: function() { return m[k]; } };
10
- }
11
- Object.defineProperty(o, k2, desc);
12
- }) : (function(o, m, k, k2) {
13
- if (k2 === undefined) k2 = k;
14
- o[k2] = m[k];
15
- }));
16
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
- Object.defineProperty(o, "default", { enumerable: true, value: v });
18
- }) : function(o, v) {
19
- o["default"] = v;
20
- });
21
- var __importStar = (this && this.__importStar) || (function () {
22
- var ownKeys = function(o) {
23
- ownKeys = Object.getOwnPropertyNames || function (o) {
24
- var ar = [];
25
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
- return ar;
27
- };
28
- return ownKeys(o);
29
- };
30
- return function (mod) {
31
- if (mod && mod.__esModule) return mod;
32
- var result = {};
33
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
- __setModuleDefault(result, mod);
35
- return result;
36
- };
37
- })();
38
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
39
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
40
4
  };
41
5
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.AuthCommand = void 0;
6
+ exports.loadAuthConfig = loadAuthConfig;
7
+ exports.saveAuthConfig = saveAuthConfig;
8
+ exports.clearAuthConfig = clearAuthConfig;
9
+ exports.getAuthToken = getAuthToken;
10
+ exports.login = login;
11
+ exports.logout = logout;
12
+ exports.whoami = whoami;
13
+ exports.doctor = doctor;
14
+ exports.handleLogin = handleLogin;
15
+ exports.handleLogout = handleLogout;
16
+ exports.statusAction = statusAction;
17
+ exports.registerAuthCommands = registerAuthCommands;
43
18
  const chalk_1 = __importDefault(require("chalk"));
44
- const readline = __importStar(require("readline"));
45
- const logger_js_1 = require("../utils/logger.js");
46
- const api_js_1 = require("../utils/api.js");
47
- /**
48
- * Prompt helpers using Node's built-in readline.
49
- * inquirer 9.x destroys the readline interface after each prompt() call
50
- * which triggers ERR_USE_AFTER_CLOSE on Node 20 Windows TTY when a second
51
- * prompt is attempted on the same process.stdin. Using raw readline avoids
52
- * this entirely.
53
- *
54
- * IMPORTANT: We reuse a single readline.Interface for all prompts in the
55
- * login flow to avoid the triple-rl stdin contention that causes stalls
56
- * on Windows TTY and piped terminals.
57
- */
58
- function ask(rl, question) {
59
- return new Promise((resolve) => rl.question(question, resolve));
19
+ const fs_1 = require("fs");
20
+ const os_1 = require("os");
21
+ const path_1 = __importDefault(require("path"));
22
+ const readline_1 = __importDefault(require("readline"));
23
+ const DEFAULT_API_URL = 'https://coder.vigthoria.io';
24
+ const CONFIG_DIR = path_1.default.join((0, os_1.homedir)(), '.vigthoria');
25
+ const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
26
+ const KNOWN_AUTH_BASE_URLS = ['https://coder.vigthoria.io', 'https://api.vigthoria.io'];
27
+ class HttpError extends Error {
28
+ status;
29
+ constructor(status, message) {
30
+ super(message);
31
+ this.status = status;
32
+ }
60
33
  }
61
- function askHidden(rl, question) {
62
- // In non-interactive (piped) terminals, fall back to plain rl.question
63
- // because raw data events won't fire after stdin reaches EOF.
64
- if (!process.stdin.isTTY) {
65
- return new Promise((resolve) => {
66
- rl.question(question, (answer) => {
67
- resolve(answer.trim());
68
- });
69
- });
70
- }
71
- return new Promise((resolve) => {
72
- const stdout = process.stdout;
73
- let answer = '';
74
- // Pause rl so it doesn't compete for stdin data events
75
- rl.pause();
76
- stdout.write(question);
77
- process.stdin.setRawMode(true);
78
- process.stdin.resume();
79
- const onData = (ch) => {
80
- const c = ch.toString('utf8');
81
- if (c === '\n' || c === '\r' || c === '\u0004') {
82
- process.stdin.setRawMode(false);
83
- process.stdin.removeListener('data', onData);
84
- stdout.write('\n');
85
- rl.resume();
86
- resolve(answer);
87
- }
88
- else if (c === '\u007f' || c === '\b') {
89
- if (answer.length > 0) {
90
- answer = answer.slice(0, -1);
91
- stdout.write('\b \b');
92
- }
93
- }
94
- else if (c === '\u0003') {
95
- rl.close();
96
- process.exit(1);
97
- }
98
- else {
99
- answer += c;
100
- stdout.write('*');
101
- }
34
+ function trimTrailingSlash(value) {
35
+ return value.replace(/\/+$/, '');
36
+ }
37
+ function uniqueStrings(values) {
38
+ return [...new Set(values.filter(Boolean))];
39
+ }
40
+ function getApiUrl() {
41
+ return trimTrailingSlash(process.env.VIGTHORIA_API_URL || DEFAULT_API_URL);
42
+ }
43
+ function derivePeerHost(baseUrl) {
44
+ if (baseUrl.includes('://api.vigthoria.io')) {
45
+ return baseUrl.replace('://api.vigthoria.io', '://coder.vigthoria.io');
46
+ }
47
+ if (baseUrl.includes('://coder.vigthoria.io')) {
48
+ return baseUrl.replace('://coder.vigthoria.io', '://api.vigthoria.io');
49
+ }
50
+ return undefined;
51
+ }
52
+ function getAuthBaseCandidates(seedBaseUrl) {
53
+ const seed = trimTrailingSlash(seedBaseUrl || getApiUrl());
54
+ const peer = derivePeerHost(seed);
55
+ return uniqueStrings([seed, ...(peer ? [peer] : []), ...KNOWN_AUTH_BASE_URLS]).map(trimTrailingSlash);
56
+ }
57
+ function ensureConfigDir() {
58
+ if (!(0, fs_1.existsSync)(CONFIG_DIR)) {
59
+ (0, fs_1.mkdirSync)(CONFIG_DIR, { recursive: true, mode: 0o700 });
60
+ }
61
+ try {
62
+ (0, fs_1.chmodSync)(CONFIG_DIR, 0o700);
63
+ }
64
+ catch {
65
+ // Best-effort on non-POSIX filesystems.
66
+ }
67
+ }
68
+ function humanMessage(error) {
69
+ const raw = error instanceof Error ? error.message : String(error);
70
+ if (/^\s*</.test(raw) || /<!doctype html/i.test(raw)) {
71
+ return 'The Vigthoria service returned an unexpected HTML response. This usually means auth routes are misconfigured server-side.';
72
+ }
73
+ return raw || 'Unknown authentication error.';
74
+ }
75
+ function extractAuthToken(payload) {
76
+ if (!payload || typeof payload !== 'object') {
77
+ return undefined;
78
+ }
79
+ const body = payload;
80
+ return body.token || body.accessToken || body.tokens?.access_token;
81
+ }
82
+ function extractAuthUser(payload, fallbackEmail) {
83
+ if (!payload || typeof payload !== 'object') {
84
+ return fallbackEmail ? { email: fallbackEmail } : undefined;
85
+ }
86
+ const body = payload;
87
+ const user = body.user;
88
+ if (user && typeof user === 'object') {
89
+ const typed = user;
90
+ return {
91
+ id: typed.id ? String(typed.id) : undefined,
92
+ email: typed.email ? String(typed.email) : fallbackEmail,
93
+ name: typed.name ? String(typed.name) : typed.username ? String(typed.username) : undefined,
102
94
  };
103
- process.stdin.on('data', onData);
104
- });
95
+ }
96
+ if (body.email || body.id || body.username) {
97
+ return {
98
+ id: body.id ? String(body.id) : undefined,
99
+ email: body.email ? String(body.email) : fallbackEmail,
100
+ name: body.username ? String(body.username) : body.name ? String(body.name) : undefined,
101
+ };
102
+ }
103
+ return fallbackEmail ? { email: fallbackEmail } : undefined;
105
104
  }
106
- class AuthCommand {
107
- config;
108
- logger;
109
- api;
110
- constructor(config, logger) {
111
- this.config = config;
112
- this.logger = logger;
113
- this.api = new api_js_1.APIClient(config, logger);
114
- }
115
- async login(options) {
116
- // If token provided, use token auth
117
- if (options.token) {
118
- await this.loginWithToken(options.token);
119
- return;
120
- }
121
- console.log();
122
- console.log(chalk_1.default.cyan(`${logger_js_1.CH.hDouble.repeat(3)} Vigthoria Login ${logger_js_1.CH.hDouble.repeat(3)}`));
123
- console.log();
124
- // Use Node's built-in readline instead of inquirer.
125
- // inquirer 9.x destroys and recreates readline interfaces per prompt
126
- // which causes ERR_USE_AFTER_CLOSE on Node 20 Windows TTY.
127
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
105
+ function loadAuthConfig() {
106
+ if (!(0, fs_1.existsSync)(CONFIG_FILE)) {
107
+ return { apiUrl: getApiUrl() };
108
+ }
109
+ try {
110
+ const parsed = JSON.parse((0, fs_1.readFileSync)(CONFIG_FILE, 'utf8'));
111
+ return {
112
+ apiUrl: trimTrailingSlash(parsed.apiUrl || getApiUrl()),
113
+ token: parsed.token || parsed.authToken,
114
+ user: parsed.user,
115
+ };
116
+ }
117
+ catch (error) {
118
+ console.warn(chalk_1.default.yellow(`Warning: could not read auth config: ${humanMessage(error)}`));
119
+ return { apiUrl: getApiUrl() };
120
+ }
121
+ }
122
+ function saveAuthConfig(config) {
123
+ ensureConfigDir();
124
+ (0, fs_1.writeFileSync)(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
125
+ try {
126
+ (0, fs_1.chmodSync)(CONFIG_FILE, 0o600);
127
+ }
128
+ catch {
129
+ // Best-effort on non-POSIX filesystems.
130
+ }
131
+ }
132
+ function clearAuthConfig() {
133
+ if ((0, fs_1.existsSync)(CONFIG_FILE)) {
134
+ (0, fs_1.rmSync)(CONFIG_FILE, { force: true });
135
+ }
136
+ }
137
+ function getAuthToken() {
138
+ return process.env.VIGTHORIA_TOKEN || loadAuthConfig().token;
139
+ }
140
+ async function requestJson(url, init) {
141
+ const response = await fetch(url, {
142
+ ...init,
143
+ headers: {
144
+ accept: 'application/json',
145
+ 'content-type': 'application/json',
146
+ ...(init.headers || {}),
147
+ },
148
+ });
149
+ const text = await response.text();
150
+ let body = {};
151
+ if (text.trim()) {
128
152
  try {
129
- console.log(chalk_1.default.white(' 1) Email & Password'));
130
- console.log(chalk_1.default.white(' 2) API Token'));
131
- console.log(chalk_1.default.white(' 3) Browser Login'));
132
- console.log();
133
- const choice = (await ask(rl, chalk_1.default.cyan('? ') + 'Choose login method (1/2/3): ')).trim();
134
- switch (choice) {
135
- case '1':
136
- case 'credentials': {
137
- const email = (await ask(rl, chalk_1.default.cyan('? ') + 'Email: ')).trim();
138
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
139
- if (!emailRegex.test(email)) {
140
- this.logger.error('Please enter a valid email');
141
- rl.close();
142
- return;
143
- }
144
- const password = await askHidden(rl, chalk_1.default.cyan('? ') + 'Password: ');
145
- rl.close();
146
- if (password.length < 6) {
147
- this.logger.error('Password must be at least 6 characters');
148
- return;
149
- }
150
- await this.doCredentialLogin(email, password);
151
- break;
152
- }
153
- case '2':
154
- case 'token': {
155
- const token = await askHidden(rl, chalk_1.default.cyan('? ') + 'API Token: ');
156
- rl.close();
157
- if (!token) {
158
- this.logger.error('Please enter your API token');
159
- return;
160
- }
161
- await this.loginWithToken(token);
162
- break;
163
- }
164
- case '3':
165
- case 'browser':
166
- rl.close();
167
- await this.loginWithBrowser();
168
- break;
169
- default:
170
- rl.close();
171
- this.logger.error('Invalid choice. Please enter 1, 2, or 3.');
172
- break;
173
- }
153
+ body = JSON.parse(text);
174
154
  }
175
- catch (err) {
176
- rl.close();
177
- throw err;
155
+ catch {
156
+ if (/^\s*</.test(text) || /<!doctype html/i.test(text)) {
157
+ throw new HttpError(response.status, 'The Vigthoria service returned an unexpected HTML response. Please verify auth routes.');
158
+ }
159
+ throw new HttpError(response.status, text.slice(0, 240));
178
160
  }
179
161
  }
180
- async doCredentialLogin(email, password) {
181
- const spinner = (0, logger_js_1.createSpinner)('Logging in...').start();
182
- const success = await this.api.login(email, password);
183
- spinner.stop();
184
- if (success) {
185
- this.printLoginSuccess();
162
+ if (!response.ok) {
163
+ const message = typeof body === 'object' && body && 'error' in body
164
+ ? String(body.error)
165
+ : typeof body === 'object' && body && 'message' in body
166
+ ? String(body.message)
167
+ : `Request failed with status ${response.status}`;
168
+ throw new HttpError(response.status, message);
169
+ }
170
+ return body;
171
+ }
172
+ async function ask(question, hidden = false) {
173
+ const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
174
+ if (!hidden) {
175
+ try {
176
+ return await new Promise((resolve) => rl.question(question, resolve));
186
177
  }
187
- else {
188
- this.logger.error('Login failed. Please check your credentials.');
178
+ finally {
179
+ rl.close();
189
180
  }
190
181
  }
191
- async loginWithToken(token) {
192
- const spinner = (0, logger_js_1.createSpinner)('Validating token...').start();
193
- const success = await this.api.loginWithToken(token);
194
- spinner.stop();
195
- if (success) {
196
- this.printLoginSuccess();
182
+ const output = process.stdout;
183
+ const mutableRl = rl;
184
+ const originalWrite = mutableRl._writeToOutput;
185
+ mutableRl._writeToOutput = function writeMasked(value) {
186
+ if (value.includes('\n') || value.includes('\r')) {
187
+ output.write(value);
197
188
  }
198
189
  else {
199
- this.logger.error('Invalid token. Please check and try again.');
190
+ output.write('*');
200
191
  }
192
+ };
193
+ try {
194
+ return await new Promise((resolve) => rl.question(question, resolve));
201
195
  }
202
- async loginWithBrowser() {
203
- console.log();
204
- console.log(chalk_1.default.gray('Opening browser for authentication...'));
205
- console.log();
206
- console.log(chalk_1.default.cyan('Visit: https://coder.vigthoria.io/cli-auth'));
207
- console.log();
208
- console.log(chalk_1.default.gray('After authenticating, copy the token and run:'));
209
- console.log(chalk_1.default.white(' vigthoria login --token YOUR_TOKEN'));
210
- console.log();
211
- }
212
- async logout() {
213
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
214
- const answer = await ask(rl, chalk_1.default.cyan('? ') + 'Are you sure you want to logout? (y/N): ');
215
- rl.close();
216
- const confirm = /^y(es)?$/i.test(answer.trim());
217
- if (confirm) {
218
- this.config.clearAuth();
219
- this.logger.success('Logged out successfully');
196
+ finally {
197
+ if (originalWrite) {
198
+ mutableRl._writeToOutput = originalWrite;
220
199
  }
200
+ output.write('\n');
201
+ rl.close();
221
202
  }
222
- async status() {
223
- console.log();
224
- console.log(chalk_1.default.cyan(`${logger_js_1.CH.hDouble.repeat(3)} Account Status ${logger_js_1.CH.hDouble.repeat(3)}`));
225
- console.log();
226
- if (!this.config.isAuthenticated()) {
227
- this.logger.warn('Not logged in');
228
- console.log();
229
- console.log(chalk_1.default.gray('Run `vigthoria login` to authenticate'));
230
- this.api.destroy();
231
- return;
203
+ }
204
+ function markSuccessExit() {
205
+ process.exitCode = 0;
206
+ }
207
+ function markErrorExit() {
208
+ process.exitCode = 1;
209
+ }
210
+ async function login(email, password) {
211
+ try {
212
+ const resolvedEmail = (email || '').trim();
213
+ const resolvedPassword = password || '';
214
+ if (!resolvedEmail || !resolvedPassword) {
215
+ throw new Error('Email and password are required. Pass --email and --password or run login interactively.');
232
216
  }
233
- const email = this.config.get('email');
234
- const sub = this.config.get('subscription');
235
- // Account info
236
- console.log(chalk_1.default.white('Account:'));
237
- console.log(chalk_1.default.gray(' Email: ') + chalk_1.default.cyan(email));
238
- console.log(chalk_1.default.gray(' User ID: ') + chalk_1.default.gray(this.config.get('userId')));
239
- console.log();
240
- // Subscription info
241
- console.log(chalk_1.default.white('Subscription:'));
242
- const planDisplay = sub.plan ? chalk_1.default.green(sub.plan.toUpperCase()) : chalk_1.default.yellow('FREE');
243
- const statusDisplay = sub.status === 'active' ? chalk_1.default.green('Active') : chalk_1.default.red(sub.status || 'N/A');
244
- console.log(chalk_1.default.gray(' Plan: ') + planDisplay);
245
- console.log(chalk_1.default.gray(' Status: ') + statusDisplay);
246
- console.log(chalk_1.default.gray(' Cloud Models: ') + (this.config.hasCloudAccess() ? chalk_1.default.green('Enabled') : chalk_1.default.yellow('Local only')));
247
- console.log(chalk_1.default.gray(' Operator Mode: ') + (this.config.hasOperatorAccess() ? chalk_1.default.green('Enabled') : chalk_1.default.yellow('Enterprise/Admin only')));
248
- if (sub.expiresAt) {
249
- const expiresDate = new Date(sub.expiresAt);
250
- const daysLeft = Math.ceil((expiresDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
251
- const expiresColor = daysLeft > 7 ? chalk_1.default.green : daysLeft > 0 ? chalk_1.default.yellow : chalk_1.default.red;
252
- console.log(chalk_1.default.gray(' Expires: ') + expiresColor(`${expiresDate.toLocaleDateString()} (${daysLeft} days)`));
217
+ const authBases = getAuthBaseCandidates(getApiUrl());
218
+ const endpointVariants = [
219
+ { path: '/auth/login', body: { email: resolvedEmail, password: resolvedPassword } },
220
+ { path: '/api/auth/login', body: { email: resolvedEmail, password: resolvedPassword } },
221
+ { path: '/api/login-json', body: { identifier: resolvedEmail, password: resolvedPassword } },
222
+ { path: '/api/login', body: { email: resolvedEmail, password: resolvedPassword } },
223
+ ];
224
+ const attempted = [];
225
+ const failures = [];
226
+ for (const base of authBases) {
227
+ for (const variant of endpointVariants) {
228
+ const endpoint = `${trimTrailingSlash(base)}${variant.path}`;
229
+ attempted.push(endpoint);
230
+ try {
231
+ const result = await requestJson(endpoint, {
232
+ method: 'POST',
233
+ body: JSON.stringify(variant.body),
234
+ });
235
+ const token = extractAuthToken(result);
236
+ if (!token) {
237
+ failures.push(`${endpoint} -> success without token`);
238
+ continue;
239
+ }
240
+ const config = {
241
+ apiUrl: trimTrailingSlash(base),
242
+ token,
243
+ user: extractAuthUser(result, resolvedEmail),
244
+ success: true,
245
+ };
246
+ saveAuthConfig(config);
247
+ markSuccessExit();
248
+ return config;
249
+ }
250
+ catch (error) {
251
+ const message = humanMessage(error);
252
+ failures.push(`${endpoint} -> ${message}`);
253
+ }
254
+ }
253
255
  }
254
- console.log();
255
- // Available models
256
- console.log(chalk_1.default.white('Available Models:'));
257
- const models = this.config.getAvailableModels();
258
- models.forEach(m => {
259
- console.log(chalk_1.default.gray(` ${logger_js_1.CH.bullet} `) + chalk_1.default.cyan(m.id) + chalk_1.default.gray(' -> ') + chalk_1.default.white(m.name));
260
- const runtimeLabel = m.tier === 'cloud' ? 'cloud' : 'blackwell';
261
- console.log(chalk_1.default.gray(` ${m.tier === 'cloud' ? logger_js_1.CH.cloud : logger_js_1.CH.home} ${m.description}`));
262
- console.log(chalk_1.default.gray(` Backend: ${m.backendModel} (${runtimeLabel})`));
263
- });
264
- console.log();
265
- // Run ALL three probes in parallel — they are independent.
266
- const spinner = (0, logger_js_1.createSpinner)('Checking API, capabilities and auth...').start();
267
- let capabilityTimeout = null;
268
- const [apiStatus, capabilityStatus, tokenValidation] = await Promise.all([
269
- this.api.getHealthStatus(),
270
- Promise.race([
271
- this.api.getCapabilityTruthStatus({
272
- workspacePath: process.cwd(),
273
- projectPath: process.cwd(),
274
- targetPath: process.cwd(),
275
- }),
276
- new Promise(resolve => {
277
- capabilityTimeout = setTimeout(() => resolve({
278
- overallOk: false,
279
- v3Agent: { name: 'V3 Agent', endpoint: '', ok: false, error: 'Timed out (8s)' },
280
- hyperLoop: { name: 'Hyper Loop', endpoint: '', ok: false, error: 'Timed out (8s)' },
281
- repoMemory: { name: 'Repo Memory', endpoint: '', ok: false, error: 'Timed out (8s)' },
282
- devtoolsBridge: { name: 'DevTools Bridge', endpoint: '', ok: false, error: 'Timed out (8s)' },
283
- }), 8000);
284
- }),
285
- ]),
286
- this.api.validateToken(),
287
- ]);
288
- if (capabilityTimeout)
289
- clearTimeout(capabilityTimeout);
290
- spinner.stop();
291
- // --- Display API Status ---
292
- console.log(chalk_1.default.white('API Status:'));
293
- console.log(chalk_1.default.gray(' Overall: ') + (apiStatus.overallOk ? chalk_1.default.green('Healthy') : chalk_1.default.yellow('Degraded')));
294
- console.log(chalk_1.default.gray(' Coder API: ') + (apiStatus.coder.ok ? chalk_1.default.green('Online') : chalk_1.default.red('Offline')) + chalk_1.default.gray(` (${apiStatus.coder.endpoint})`));
295
- if (apiStatus.coder.error) {
296
- console.log(chalk_1.default.gray(' Error: ') + chalk_1.default.red(apiStatus.coder.error));
256
+ throw new Error([
257
+ 'Login failed on all known auth endpoints.',
258
+ `Tried: ${attempted.join(', ')}`,
259
+ `Last errors: ${failures.slice(-4).join(' | ')}`,
260
+ ].join(' '));
261
+ }
262
+ catch (error) {
263
+ const message = humanMessage(error);
264
+ markErrorExit();
265
+ throw new Error(message);
266
+ }
267
+ }
268
+ async function logout() {
269
+ const config = loadAuthConfig();
270
+ if (config.token) {
271
+ const bases = getAuthBaseCandidates(config.apiUrl || getApiUrl());
272
+ const logoutPaths = ['/auth/logout', '/api/auth/logout', '/api/logout'];
273
+ let remoteLogoutDone = false;
274
+ for (const base of bases) {
275
+ for (const route of logoutPaths) {
276
+ try {
277
+ await requestJson(`${trimTrailingSlash(base)}${route}`, {
278
+ method: 'POST',
279
+ headers: { authorization: `Bearer ${config.token}` },
280
+ });
281
+ remoteLogoutDone = true;
282
+ break;
283
+ }
284
+ catch {
285
+ // Continue trying known routes.
286
+ }
287
+ }
288
+ if (remoteLogoutDone) {
289
+ break;
290
+ }
297
291
  }
298
- console.log(chalk_1.default.gray(' Models API: ') + (apiStatus.models.ok ? chalk_1.default.green('Online') : chalk_1.default.red('Offline')) + chalk_1.default.gray(` (${apiStatus.models.endpoint})`));
299
- if (apiStatus.models.details?.modelCount !== undefined) {
300
- console.log(chalk_1.default.gray(' Models Available: ') + chalk_1.default.cyan(String(apiStatus.models.details.modelCount)));
292
+ if (!remoteLogoutDone) {
293
+ console.warn(chalk_1.default.yellow('Remote logout endpoint not reachable; local credentials were still cleared.'));
301
294
  }
302
- if (apiStatus.models.error) {
303
- console.log(chalk_1.default.gray(' Error: ') + chalk_1.default.red(apiStatus.models.error));
295
+ }
296
+ clearAuthConfig();
297
+ process.exitCode = 0;
298
+ }
299
+ async function whoami() {
300
+ const config = loadAuthConfig();
301
+ if (!config.token) {
302
+ return undefined;
303
+ }
304
+ const bases = getAuthBaseCandidates(config.apiUrl || getApiUrl());
305
+ const attempted = [];
306
+ for (const base of bases) {
307
+ const normalizedBase = trimTrailingSlash(base);
308
+ const getRoutes = ['/auth/me', '/api/auth/me', '/api/user/info', '/api/profile'];
309
+ for (const route of getRoutes) {
310
+ const endpoint = `${normalizedBase}${route}`;
311
+ attempted.push(endpoint);
312
+ try {
313
+ const result = await requestJson(endpoint, {
314
+ method: 'GET',
315
+ headers: { authorization: `Bearer ${config.token}` },
316
+ });
317
+ const user = extractAuthUser(result) || extractAuthUser(result.user);
318
+ if (user) {
319
+ saveAuthConfig({ ...config, apiUrl: normalizedBase, user });
320
+ return user;
321
+ }
322
+ }
323
+ catch {
324
+ // continue fallback route probing
325
+ }
304
326
  }
305
- if (apiStatus.selfHosted) {
306
- console.log(chalk_1.default.gray(' Self-hosted Models: ') + (apiStatus.selfHosted.ok ? chalk_1.default.green('Online') : chalk_1.default.yellow('Unavailable')) + chalk_1.default.gray(` (${apiStatus.selfHosted.endpoint})`));
307
- if (apiStatus.selfHosted.error) {
308
- console.log(chalk_1.default.gray(' Error: ') + chalk_1.default.yellow(apiStatus.selfHosted.error));
327
+ const verifyEndpoint = `${normalizedBase}/api/auth/verify`;
328
+ attempted.push(verifyEndpoint);
329
+ try {
330
+ const result = await requestJson(verifyEndpoint, {
331
+ method: 'POST',
332
+ headers: { authorization: `Bearer ${config.token}` },
333
+ body: JSON.stringify({ token: config.token }),
334
+ });
335
+ const user = extractAuthUser(result) || extractAuthUser(result.user);
336
+ if (user) {
337
+ saveAuthConfig({ ...config, apiUrl: normalizedBase, user });
338
+ return user;
309
339
  }
310
340
  }
311
- else {
312
- console.log(chalk_1.default.gray(' Self-hosted Models: ') + chalk_1.default.gray('disabled'));
341
+ catch {
342
+ // continue fallback route probing
313
343
  }
314
- // --- Display Capability Truth ---
315
- console.log();
316
- console.log(chalk_1.default.white('Capability Truth:'));
317
- const coreOk = apiStatus.overallOk;
318
- console.log(chalk_1.default.gray(' Overall: ') + (coreOk ? chalk_1.default.green('Verified') : chalk_1.default.yellow('Partial')));
319
- // V3 Agent — used by agent/chat commands (routed through model API)
320
- console.log(chalk_1.default.gray(' V3 Agent: ') + (capabilityStatus.v3Agent.ok ? chalk_1.default.green('Reachable') : chalk_1.default.gray('Not deployed (routed through model API)')));
321
- if (capabilityStatus.v3Agent.error && capabilityStatus.v3Agent.ok === false) {
322
- const err = capabilityStatus.v3Agent.error;
323
- if (!/timed? out|not reachable|No V3 agent/i.test(err)) {
324
- console.log(chalk_1.default.gray(' Error: ') + chalk_1.default.red(err));
325
- }
344
+ }
345
+ throw new Error(`Unable to verify account on known auth endpoints. Tried ${attempted.join(', ')}`);
346
+ }
347
+ async function doctor() {
348
+ const config = loadAuthConfig();
349
+ const report = {
350
+ nodeVersion: process.version,
351
+ platform: process.platform,
352
+ arch: process.arch,
353
+ configFile: CONFIG_FILE,
354
+ loggedIn: Boolean(config.token),
355
+ apiUrl: config.apiUrl,
356
+ };
357
+ process.exitCode = 0;
358
+ return report;
359
+ }
360
+ async function handleLogin(options = {}) {
361
+ try {
362
+ if (options.token) {
363
+ const config = {
364
+ apiUrl: getApiUrl(),
365
+ token: options.token,
366
+ success: true,
367
+ };
368
+ saveAuthConfig(config);
369
+ console.log(chalk_1.default.green('Logged in successfully with API token.'));
370
+ process.exitCode = 0;
371
+ return config;
372
+ }
373
+ let email = options.email?.trim();
374
+ let password = options.password;
375
+ if (options.device) {
376
+ console.log(chalk_1.default.yellow('Device-code login is not enabled by this Vigthoria service. Falling back to email and password authentication.'));
377
+ }
378
+ if (!email) {
379
+ email = (await ask('Email: ')).trim();
326
380
  }
327
- // Hyper Loop — optional orchestration layer
328
- console.log(chalk_1.default.gray(' Hyper Loop: ') + (capabilityStatus.hyperLoop.ok ? chalk_1.default.green('Reachable') : chalk_1.default.gray('Not deployed (optional)')));
329
- // Repo Memory — separate auth scope, only affects repo commands
330
- console.log(chalk_1.default.gray(' Repo Memory: ') + (capabilityStatus.repoMemory.ok ? chalk_1.default.green('Active') : chalk_1.default.gray('Not deployed (optional)')));
331
- if (capabilityStatus.repoMemory.details?.compactContextLength !== undefined) {
332
- console.log(chalk_1.default.gray(' Compact Context: ') + chalk_1.default.cyan(`${capabilityStatus.repoMemory.details.compactContextLength} chars`));
381
+ if (!password) {
382
+ password = await ask('Password: ', true);
333
383
  }
334
- // DevTools Bridge local service, not required
335
- console.log(chalk_1.default.gray(' DevTools Bridge: ') + (capabilityStatus.devtoolsBridge.ok ? chalk_1.default.green('Connected') : chalk_1.default.gray('Not running (optional)')));
336
- // --- Display Auth Scopes ---
337
- console.log();
338
- console.log(chalk_1.default.white('Auth Scopes:'));
339
- console.log(chalk_1.default.gray(' Model Auth: ') + (tokenValidation.valid ? chalk_1.default.green('Valid') : chalk_1.default.red('Invalid')) + chalk_1.default.gray(' (used by chat, agent, review, explain, generate, fix)'));
340
- if (!tokenValidation.valid && tokenValidation.error) {
341
- console.log(chalk_1.default.gray(' ') + chalk_1.default.red(tokenValidation.error));
384
+ if (!email || !password) {
385
+ throw new Error('Email and password are required. Use --email and --password, or run vigthoria login interactively.');
342
386
  }
343
- console.log(chalk_1.default.gray(' Repo Auth: ') + (capabilityStatus.repoMemory.ok ? chalk_1.default.green('Active') : chalk_1.default.gray('N/A — repo memory not deployed')) + chalk_1.default.gray(' (used by repo push/pull/list only)'));
344
- console.log(chalk_1.default.gray(' Bridge Auth: ') + (capabilityStatus.devtoolsBridge.ok ? chalk_1.default.green('Connected') : chalk_1.default.gray('N/A — bridge not running')) + chalk_1.default.gray(' (used by --bridge flag only)'));
345
- console.log();
346
- // Graceful exit — destroy keep-alive agents so the event loop drains
347
- // naturally. Setting exitCode (not calling process.exit()) avoids the
348
- // libuv UV_HANDLE_CLOSING assertion on Windows that occurs when
349
- // process.exit() is invoked while socket handles are mid-close.
350
- this.api.destroy();
387
+ const config = await login(email, password);
388
+ console.log(chalk_1.default.green('Logged in successfully.'));
389
+ if (config.user?.email) {
390
+ console.log(`Account: ${config.user.email}`);
391
+ }
392
+ console.log(`API: ${config.apiUrl}`);
351
393
  process.exitCode = 0;
394
+ return config;
395
+ }
396
+ catch (error) {
397
+ console.error(chalk_1.default.red(`Login failed: ${humanMessage(error)}`));
398
+ process.exitCode = 1;
399
+ return undefined;
352
400
  }
353
- printLoginSuccess() {
354
- const email = this.config.get('email');
355
- const sub = this.config.get('subscription');
356
- console.log();
357
- this.logger.success(`Logged in as ${chalk_1.default.cyan(email)}`);
358
- if (sub.plan) {
359
- console.log(chalk_1.default.gray(` Plan: ${chalk_1.default.green(sub.plan.toUpperCase())}`));
401
+ }
402
+ async function handleLogout(_options) {
403
+ try {
404
+ await logout();
405
+ console.log(chalk_1.default.green('Logged out successfully.'));
406
+ process.exitCode = 0;
407
+ }
408
+ catch (error) {
409
+ console.error(chalk_1.default.red(`Logout failed: ${humanMessage(error)}`));
410
+ process.exitCode = 1;
411
+ }
412
+ }
413
+ async function statusAction() {
414
+ try {
415
+ const config = loadAuthConfig();
416
+ if (!config.token) {
417
+ console.log(chalk_1.default.yellow('Not logged in.'));
418
+ process.exitCode = 0;
419
+ return;
420
+ }
421
+ let user = config.user;
422
+ try {
423
+ user = await whoami();
424
+ }
425
+ catch {
426
+ // Keep local status available even if remote whoami endpoint is down.
360
427
  }
361
- console.log();
362
- console.log(chalk_1.default.gray('You can now use all Vigthoria CLI features.'));
363
- console.log(chalk_1.default.gray('Run `vigthoria chat` or `npx vigthoria-chat` to start coding with AI!'));
364
- console.log();
428
+ console.log(chalk_1.default.green('Logged in.'));
429
+ if (user?.email) {
430
+ console.log(`Account: ${user.email}`);
431
+ }
432
+ console.log(`API: ${config.apiUrl}`);
433
+ process.exitCode = 0;
365
434
  }
435
+ catch (error) {
436
+ console.error(chalk_1.default.red(`Unable to read status: ${humanMessage(error)}`));
437
+ process.exitCode = 1;
438
+ }
439
+ }
440
+ function registerAuthCommands(program) {
441
+ const auth = program.command('auth').description('Manage Vigthoria CLI authentication');
442
+ auth
443
+ .command('login')
444
+ .description('Sign in with your Vigthoria account')
445
+ .option('-t, --token <token>', 'API token for token-based authentication')
446
+ .option('-e, --email <email>', 'Account email address')
447
+ .option('-p, --password <password>', 'Account password')
448
+ .option('--device', 'Use OAuth device flow (requires server support)')
449
+ .action(async (options) => {
450
+ await handleLogin(options);
451
+ });
452
+ auth
453
+ .command('logout')
454
+ .description('Sign out and remove saved credentials')
455
+ .action(async () => {
456
+ await handleLogout();
457
+ });
458
+ auth
459
+ .command('whoami')
460
+ .description('Show the authenticated Vigthoria account')
461
+ .action(async () => {
462
+ try {
463
+ const user = await whoami();
464
+ if (!user) {
465
+ console.log(chalk_1.default.yellow('Not logged in.'));
466
+ process.exitCode = 0;
467
+ return;
468
+ }
469
+ console.log(chalk_1.default.green('Logged in.'));
470
+ console.log(user.email || user.name || user.id || 'Authenticated user');
471
+ process.exitCode = 0;
472
+ }
473
+ catch (error) {
474
+ console.error(chalk_1.default.red(`Unable to fetch account: ${humanMessage(error)}`));
475
+ process.exitCode = 1;
476
+ }
477
+ });
366
478
  }
367
- exports.AuthCommand = AuthCommand;