vigthoria-cli 1.8.15 → 1.9.2

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,409 @@
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;
43
- 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));
6
+ exports.authCommand = void 0;
7
+ exports.readAuthSession = readAuthSession;
8
+ exports.clearAuthSession = clearAuthSession;
9
+ exports.refreshJwtIfNeeded = refreshJwtIfNeeded;
10
+ exports.loginWithToken = loginWithToken;
11
+ exports.loginWithCredentials = loginWithCredentials;
12
+ exports.loginWithDeviceCode = loginWithDeviceCode;
13
+ exports.getValidAuthSession = getValidAuthSession;
14
+ exports.loginAction = loginAction;
15
+ exports.logoutAction = logoutAction;
16
+ exports.handleLogin = handleLogin;
17
+ exports.handleLogout = handleLogout;
18
+ exports.statusAction = statusAction;
19
+ exports.createAuthCommand = createAuthCommand;
20
+ exports.registerAuthCommand = registerAuthCommand;
21
+ const commander_1 = require("commander");
22
+ const fs_1 = __importDefault(require("fs"));
23
+ const os_1 = __importDefault(require("os"));
24
+ const path_1 = __importDefault(require("path"));
25
+ const promises_1 = require("timers/promises");
26
+ const DEFAULT_API_URL = 'https://coder.vigthoria.io';
27
+ const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.vigthoria');
28
+ const AUTH_FILE = path_1.default.join(CONFIG_DIR, 'auth.json');
29
+ const REFRESH_SKEW_MS = 30_000;
30
+ const refreshRequests = new Map();
31
+ function getApiUrl(explicit) {
32
+ return (explicit || process.env.VIGTHORIA_API_URL || DEFAULT_API_URL).replace(/\/$/, '');
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
- });
34
+ function ensureConfigDir() {
35
+ fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
36
+ }
37
+ function writeAuthSession(session) {
38
+ ensureConfigDir();
39
+ fs_1.default.writeFileSync(AUTH_FILE, `${JSON.stringify(session, null, 2)}\n`, { mode: 0o600 });
40
+ }
41
+ function asErrorMessage(error) {
42
+ if (error instanceof TypeError && /fetch|network|failed|ECONN|ENOTFOUND|ETIMEDOUT/i.test(error.message)) {
43
+ return 'Network error while contacting Vigthoria. Check your connection and API URL.';
70
44
  }
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
- }
102
- };
103
- process.stdin.on('data', onData);
104
- });
45
+ return error instanceof Error ? error.message : String(error);
105
46
  }
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);
47
+ async function parseResponseBody(response) {
48
+ const text = await response.text();
49
+ if (!text)
50
+ return {};
51
+ try {
52
+ const parsed = JSON.parse(text);
53
+ return parsed && typeof parsed === 'object' ? parsed : {};
114
54
  }
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 });
128
- 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
- }
174
- }
175
- catch (err) {
176
- rl.close();
177
- throw err;
178
- }
55
+ catch {
56
+ return { message: text.slice(0, 300) };
179
57
  }
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();
186
- }
187
- else {
188
- this.logger.error('Login failed. Please check your credentials.');
189
- }
58
+ }
59
+ function responseErrorMessage(response, data) {
60
+ const serverMessage = typeof data.error === 'string'
61
+ ? data.error
62
+ : typeof data.message === 'string'
63
+ ? data.message
64
+ : undefined;
65
+ if (response.status === 400 || response.status === 401 || response.status === 403) {
66
+ return serverMessage || 'Invalid credentials. Check your email, password, or token and try again.';
190
67
  }
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();
197
- }
198
- else {
199
- this.logger.error('Invalid token. Please check and try again.');
200
- }
68
+ if (response.status === 404)
69
+ return serverMessage || 'Authentication endpoint was not found for the configured API URL.';
70
+ if (response.status >= 500)
71
+ return serverMessage || 'Vigthoria authentication service is temporarily unavailable. Try again later.';
72
+ return serverMessage || `Request failed with HTTP ${response.status}`;
73
+ }
74
+ function readAuthSession() {
75
+ try {
76
+ if (!fs_1.default.existsSync(AUTH_FILE))
77
+ return null;
78
+ const parsed = JSON.parse(fs_1.default.readFileSync(AUTH_FILE, 'utf8'));
79
+ if (!parsed || typeof parsed.accessToken !== 'string' || !parsed.accessToken)
80
+ return null;
81
+ return parsed;
201
82
  }
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();
83
+ catch (error) {
84
+ console.error('Failed to read saved authentication session:', asErrorMessage(error));
85
+ return null;
211
86
  }
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');
87
+ }
88
+ function clearBrowserStorage() {
89
+ const storage = globalThis.localStorage;
90
+ if (!storage)
91
+ return;
92
+ try {
93
+ if (typeof storage.removeItem === 'function') {
94
+ storage.removeItem('vigthoria.auth');
95
+ storage.removeItem('vigthoria.jwt');
96
+ storage.removeItem('auth');
97
+ storage.removeItem('jwt');
220
98
  }
99
+ if (typeof storage.clear === 'function')
100
+ storage.clear();
221
101
  }
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;
232
- }
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)`));
253
- }
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})`));
102
+ catch (error) {
103
+ console.warn('Failed to clear local auth storage:', asErrorMessage(error));
104
+ }
105
+ }
106
+ function resetJwtState(state) {
107
+ if (!state)
108
+ return;
109
+ state.token = null;
110
+ state.expiresAt = null;
111
+ state.refreshToken = null;
112
+ }
113
+ function clearAuthSession(state) {
114
+ let ok = true;
115
+ try {
116
+ if (fs_1.default.existsSync(AUTH_FILE))
117
+ fs_1.default.rmSync(AUTH_FILE, { force: true });
118
+ }
119
+ catch (error) {
120
+ ok = false;
121
+ console.error('Failed to remove saved authentication session:', asErrorMessage(error));
122
+ }
123
+ clearBrowserStorage();
124
+ resetJwtState(state);
125
+ refreshRequests.clear();
126
+ return ok;
127
+ }
128
+ async function postJson(url, body, token) {
129
+ let response;
130
+ try {
131
+ response = await fetch(url, {
132
+ method: 'POST',
133
+ headers: {
134
+ 'content-type': 'application/json',
135
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
136
+ },
137
+ body: JSON.stringify(body),
263
138
  });
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));
297
- }
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)));
301
- }
302
- if (apiStatus.models.error) {
303
- console.log(chalk_1.default.gray(' Error: ') + chalk_1.default.red(apiStatus.models.error));
304
- }
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));
139
+ }
140
+ catch (error) {
141
+ throw new Error(asErrorMessage(error));
142
+ }
143
+ const data = await parseResponseBody(response);
144
+ if (!response.ok)
145
+ throw new Error(responseErrorMessage(response, data));
146
+ return data;
147
+ }
148
+ async function getJson(url, token) {
149
+ let response;
150
+ try {
151
+ response = await fetch(url, {
152
+ headers: token ? { authorization: `Bearer ${token}` } : undefined,
153
+ });
154
+ }
155
+ catch (error) {
156
+ throw new Error(asErrorMessage(error));
157
+ }
158
+ const data = await parseResponseBody(response);
159
+ if (!response.ok)
160
+ throw new Error(responseErrorMessage(response, data));
161
+ return data;
162
+ }
163
+ function normalizeSession(data, apiUrl) {
164
+ const accessToken = data.accessToken || data.token;
165
+ if (!accessToken)
166
+ throw new Error('Authentication response did not include an access token.');
167
+ const now = new Date().toISOString();
168
+ const expiresAt = typeof data.expiresAt === 'number'
169
+ ? data.expiresAt
170
+ : typeof data.expiresIn === 'number'
171
+ ? Date.now() + data.expiresIn * 1000
172
+ : undefined;
173
+ return {
174
+ accessToken,
175
+ refreshToken: data.refreshToken,
176
+ user: data.user,
177
+ expiresAt,
178
+ apiUrl,
179
+ createdAt: now,
180
+ updatedAt: now,
181
+ };
182
+ }
183
+ function isExpired(session) {
184
+ return typeof session.expiresAt === 'number' && Date.now() >= session.expiresAt - REFRESH_SKEW_MS;
185
+ }
186
+ function jwtNeedsRefresh(state) {
187
+ if (!state.token)
188
+ return false;
189
+ if (typeof state.isExpired === 'function')
190
+ return state.isExpired();
191
+ return typeof state.expiresAt === 'number' && Date.now() >= state.expiresAt - REFRESH_SKEW_MS;
192
+ }
193
+ function refreshKey(state, session) {
194
+ const apiUrl = getApiUrl(state.apiUrl || session?.apiUrl);
195
+ const refreshToken = state.refreshToken || session?.refreshToken || 'no-refresh-token';
196
+ return `${apiUrl}:${refreshToken}`;
197
+ }
198
+ async function performRefresh(state, session) {
199
+ if (!session.refreshToken) {
200
+ resetJwtState(state);
201
+ return null;
202
+ }
203
+ try {
204
+ const data = await postJson(`${session.apiUrl}/api/auth/refresh`, { refreshToken: session.refreshToken, client: 'vigthoria-cli' });
205
+ const refreshed = normalizeSession({ ...data, refreshToken: data.refreshToken || session.refreshToken }, session.apiUrl);
206
+ refreshed.createdAt = session.createdAt;
207
+ refreshed.updatedAt = new Date().toISOString();
208
+ writeAuthSession(refreshed);
209
+ state.token = refreshed.accessToken;
210
+ state.expiresAt = refreshed.expiresAt ?? null;
211
+ state.refreshToken = refreshed.refreshToken ?? session.refreshToken;
212
+ state.apiUrl = refreshed.apiUrl;
213
+ return refreshed.accessToken;
214
+ }
215
+ catch (error) {
216
+ console.error('Failed to refresh authentication session:', asErrorMessage(error));
217
+ clearAuthSession(state);
218
+ return null;
219
+ }
220
+ }
221
+ async function refreshJwtIfNeeded(state) {
222
+ if (!state || typeof state !== 'object')
223
+ throw new Error('JWT state is required.');
224
+ if (!jwtNeedsRefresh(state))
225
+ return state.token;
226
+ const session = readAuthSession();
227
+ if (!session) {
228
+ resetJwtState(state);
229
+ return null;
230
+ }
231
+ const key = refreshKey(state, session);
232
+ const existing = refreshRequests.get(key);
233
+ if (existing)
234
+ return existing;
235
+ const request = performRefresh(state, session).finally(() => {
236
+ refreshRequests.delete(key);
237
+ });
238
+ refreshRequests.set(key, request);
239
+ return request;
240
+ }
241
+ async function loginWithToken(token, options = {}) {
242
+ if (!token || typeof token !== 'string')
243
+ throw new Error('A token is required.');
244
+ const apiUrl = getApiUrl(options.apiUrl);
245
+ let user;
246
+ try {
247
+ const profile = await getJson(`${apiUrl}/api/auth/me`, token);
248
+ user = profile.data || profile.user;
249
+ }
250
+ catch (error) {
251
+ throw new Error(`Token validation failed: ${asErrorMessage(error)}`);
252
+ }
253
+ const now = new Date().toISOString();
254
+ const session = { accessToken: token, user, apiUrl, createdAt: now, updatedAt: now };
255
+ writeAuthSession(session);
256
+ return session;
257
+ }
258
+ async function loginWithCredentials(email, password, options = {}) {
259
+ if (!email || !password)
260
+ throw new Error('Email and password are required.');
261
+ const apiUrl = getApiUrl(options.apiUrl);
262
+ const data = await postJson(`${apiUrl}/api/auth/login`, { email, password, client: 'vigthoria-cli' });
263
+ const session = normalizeSession(data, apiUrl);
264
+ writeAuthSession(session);
265
+ return session;
266
+ }
267
+ async function loginWithDeviceCode(options = {}) {
268
+ const apiUrl = getApiUrl(options.apiUrl);
269
+ const start = await postJson(`${apiUrl}/api/auth/device`, { client: 'vigthoria-cli' });
270
+ const verificationUri = start.verificationUri || start.verification_uri;
271
+ const userCode = start.userCode || start.user_code;
272
+ const deviceCode = start.deviceCode || start.device_code;
273
+ if (!verificationUri || !userCode || !deviceCode)
274
+ throw new Error('Device authorization endpoint returned an incomplete response.');
275
+ console.log(`Open this URL to authenticate: ${verificationUri}`);
276
+ console.log(`Enter code: ${userCode}`);
277
+ const timeoutAt = Date.now() + (options.pollTimeoutMs ?? 10 * 60 * 1000);
278
+ const intervalMs = Math.max(1000, (start.interval ?? 5) * 1000);
279
+ while (Date.now() < timeoutAt) {
280
+ await (0, promises_1.setTimeout)(intervalMs);
281
+ try {
282
+ const result = await postJson(`${apiUrl}/api/auth/device/complete`, { deviceCode, device_code: deviceCode, client: 'vigthoria-cli' });
283
+ if (result.accessToken || result.token) {
284
+ const session = normalizeSession(result, apiUrl);
285
+ writeAuthSession(session);
286
+ return session;
309
287
  }
310
288
  }
311
- else {
312
- console.log(chalk_1.default.gray(' Self-hosted Models: ') + chalk_1.default.gray('disabled'));
289
+ catch (error) {
290
+ const message = asErrorMessage(error);
291
+ if (!/pending|authorization_pending|slow_down/i.test(message))
292
+ throw new Error(message);
313
293
  }
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
- }
326
- }
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`));
294
+ }
295
+ throw new Error('Timed out waiting for device authentication.');
296
+ }
297
+ async function getValidAuthSession() {
298
+ const session = readAuthSession();
299
+ if (!session)
300
+ return null;
301
+ if (!isExpired(session))
302
+ return session;
303
+ if (!session.refreshToken) {
304
+ clearAuthSession();
305
+ return null;
306
+ }
307
+ const state = {
308
+ token: session.accessToken,
309
+ expiresAt: session.expiresAt ?? null,
310
+ refreshToken: session.refreshToken,
311
+ apiUrl: session.apiUrl,
312
+ isExpired: () => true,
313
+ };
314
+ const token = await refreshJwtIfNeeded(state);
315
+ return token ? readAuthSession() : null;
316
+ }
317
+ function printSession(session) {
318
+ const user = session.user?.email || session.user?.name || session.user?.id || 'authenticated user';
319
+ console.log(`Authenticated as ${user}`);
320
+ console.log(`API: ${session.apiUrl}`);
321
+ if (session.expiresAt)
322
+ console.log(`Expires: ${new Date(session.expiresAt).toLocaleString()}`);
323
+ }
324
+ async function loginAction(options = {}) {
325
+ try {
326
+ const session = options.token
327
+ ? await loginWithToken(options.token, options)
328
+ : options.email && options.password
329
+ ? await loginWithCredentials(options.email, options.password, options)
330
+ : await loginWithDeviceCode(options);
331
+ console.log('Login successful.');
332
+ printSession(session);
333
+ }
334
+ catch (error) {
335
+ clearAuthSession();
336
+ throw new Error(asErrorMessage(error));
337
+ }
338
+ }
339
+ async function logoutAction(state) {
340
+ const session = readAuthSession();
341
+ if (session) {
342
+ try {
343
+ await postJson(`${session.apiUrl}/api/auth/logout`, { client: 'vigthoria-cli' }, session.accessToken);
333
344
  }
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));
345
+ catch (error) {
346
+ console.warn('Remote logout failed; local session will still be cleared:', asErrorMessage(error));
342
347
  }
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();
351
- process.exitCode = 0;
352
348
  }
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())}`));
360
- }
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();
349
+ if (!clearAuthSession(state))
350
+ process.exitCode = 1;
351
+ else
352
+ console.log('Logged out.');
353
+ }
354
+ async function handleLogin(config) {
355
+ try {
356
+ await loginAction(config || {});
357
+ }
358
+ catch (error) {
359
+ console.error('Login failed:', asErrorMessage(error));
360
+ process.exitCode = 1;
361
+ }
362
+ }
363
+ async function handleLogout(config) {
364
+ await logoutAction(config && typeof config === 'object' ? config : null);
365
+ }
366
+ async function statusAction() {
367
+ const session = await getValidAuthSession();
368
+ if (!session) {
369
+ console.log('Not authenticated. Run `vigthoria auth login` to sign in.');
370
+ process.exitCode = 1;
371
+ return;
365
372
  }
373
+ printSession(session);
374
+ }
375
+ function createAuthCommand() {
376
+ const auth = new commander_1.Command('auth').description('Manage Vigthoria CLI authentication');
377
+ auth
378
+ .command('login')
379
+ .description('Sign in to Vigthoria')
380
+ .option('--token <token>', 'use an existing access token')
381
+ .option('--email <email>', 'account email for password login')
382
+ .option('--password <password>', 'account password for password login')
383
+ .option('--api-url <url>', 'Vigthoria API URL')
384
+ .option('--device', 'force browser/device-code login')
385
+ .action(async (options) => {
386
+ await handleLogin(options);
387
+ });
388
+ auth
389
+ .command('logout')
390
+ .description('Clear the saved Vigthoria session')
391
+ .action(async () => {
392
+ await handleLogout(null);
393
+ });
394
+ auth
395
+ .command('status')
396
+ .description('Show current authentication status')
397
+ .action(async () => {
398
+ await statusAction();
399
+ });
400
+ auth.action(() => auth.help());
401
+ return auth;
402
+ }
403
+ function registerAuthCommand(program) {
404
+ const command = createAuthCommand();
405
+ program.addCommand(command);
406
+ return command;
366
407
  }
367
- exports.AuthCommand = AuthCommand;
408
+ exports.authCommand = createAuthCommand();
409
+ exports.default = exports.authCommand;