vigthoria-cli 1.9.2 → 1.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,407 +3,555 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
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;
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;
16
14
  exports.handleLogin = handleLogin;
17
15
  exports.handleLogout = handleLogout;
18
16
  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"));
17
+ exports.registerAuthCommands = registerAuthCommands;
18
+ const chalk_1 = __importDefault(require("chalk"));
19
+ const fs_1 = require("fs");
20
+ const os_1 = require("os");
24
21
  const path_1 = __importDefault(require("path"));
25
- const promises_1 = require("timers/promises");
22
+ const readline_1 = __importDefault(require("readline"));
23
+ const config_js_1 = require("../utils/config.js");
26
24
  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(/\/$/, '');
25
+ const CONFIG_DIR = path_1.default.join((0, os_1.homedir)(), '.vigthoria');
26
+ const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
27
+ const KNOWN_AUTH_BASE_URLS = ['https://coder.vigthoria.io', 'https://api.vigthoria.io'];
28
+ class HttpError extends Error {
29
+ status;
30
+ constructor(status, message) {
31
+ super(message);
32
+ this.status = status;
33
+ }
33
34
  }
34
- function ensureConfigDir() {
35
- fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
35
+ function trimTrailingSlash(value) {
36
+ return value.replace(/\/+$/, '');
36
37
  }
37
- function writeAuthSession(session) {
38
- ensureConfigDir();
39
- fs_1.default.writeFileSync(AUTH_FILE, `${JSON.stringify(session, null, 2)}\n`, { mode: 0o600 });
38
+ function uniqueStrings(values) {
39
+ return [...new Set(values.filter(Boolean))];
40
+ }
41
+ function getApiUrl() {
42
+ return trimTrailingSlash(process.env.VIGTHORIA_API_URL || DEFAULT_API_URL);
40
43
  }
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.';
44
+ function derivePeerHost(baseUrl) {
45
+ if (baseUrl.includes('://api.vigthoria.io')) {
46
+ return baseUrl.replace('://api.vigthoria.io', '://coder.vigthoria.io');
44
47
  }
45
- return error instanceof Error ? error.message : String(error);
48
+ if (baseUrl.includes('://coder.vigthoria.io')) {
49
+ return baseUrl.replace('://coder.vigthoria.io', '://api.vigthoria.io');
50
+ }
51
+ return undefined;
46
52
  }
47
- async function parseResponseBody(response) {
48
- const text = await response.text();
49
- if (!text)
50
- return {};
53
+ function getAuthBaseCandidates(seedBaseUrl) {
54
+ const seed = trimTrailingSlash(seedBaseUrl || getApiUrl());
55
+ const peer = derivePeerHost(seed);
56
+ return uniqueStrings([seed, ...(peer ? [peer] : []), ...KNOWN_AUTH_BASE_URLS]).map(trimTrailingSlash);
57
+ }
58
+ function ensureConfigDir() {
59
+ if (!(0, fs_1.existsSync)(CONFIG_DIR)) {
60
+ (0, fs_1.mkdirSync)(CONFIG_DIR, { recursive: true, mode: 0o700 });
61
+ }
51
62
  try {
52
- const parsed = JSON.parse(text);
53
- return parsed && typeof parsed === 'object' ? parsed : {};
63
+ (0, fs_1.chmodSync)(CONFIG_DIR, 0o700);
54
64
  }
55
65
  catch {
56
- return { message: text.slice(0, 300) };
66
+ // Best-effort on non-POSIX filesystems.
57
67
  }
58
68
  }
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.';
67
- }
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() {
69
+ function syncSharedConfig(config) {
75
70
  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;
71
+ const shared = new config_js_1.Config();
72
+ shared.set('apiUrl', trimTrailingSlash(config.apiUrl || getApiUrl()));
73
+ if (config.token) {
74
+ shared.set('authToken', config.token);
75
+ }
76
+ if (config.user?.id) {
77
+ shared.set('userId', String(config.user.id));
78
+ }
79
+ if (config.user?.email) {
80
+ shared.set('email', String(config.user.email));
81
+ }
82
82
  }
83
- catch (error) {
84
- console.error('Failed to read saved authentication session:', asErrorMessage(error));
85
- return null;
83
+ catch {
84
+ // Keep legacy auth flow working even if shared config write fails.
86
85
  }
87
86
  }
88
- function clearBrowserStorage() {
89
- const storage = globalThis.localStorage;
90
- if (!storage)
91
- return;
87
+ function clearSharedConfigAuth() {
92
88
  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');
98
- }
99
- if (typeof storage.clear === 'function')
100
- storage.clear();
89
+ const shared = new config_js_1.Config();
90
+ shared.set('authToken', null);
91
+ shared.set('refreshToken', null);
92
+ shared.set('userId', null);
93
+ shared.set('email', null);
101
94
  }
102
- catch (error) {
103
- console.warn('Failed to clear local auth storage:', asErrorMessage(error));
95
+ catch {
96
+ // Ignore shared config clear failures during logout.
104
97
  }
105
98
  }
106
- function resetJwtState(state) {
107
- if (!state)
108
- return;
109
- state.token = null;
110
- state.expiresAt = null;
111
- state.refreshToken = null;
99
+ function humanMessage(error) {
100
+ const raw = error instanceof Error ? error.message : String(error);
101
+ if (/^\s*</.test(raw) || /<!doctype html/i.test(raw)) {
102
+ return 'The Vigthoria service returned an unexpected HTML response. This usually means auth routes are misconfigured server-side.';
103
+ }
104
+ return raw || 'Unknown authentication error.';
105
+ }
106
+ function extractAuthToken(payload) {
107
+ if (!payload || typeof payload !== 'object') {
108
+ return undefined;
109
+ }
110
+ const body = payload;
111
+ return body.token || body.accessToken || body.tokens?.access_token;
112
112
  }
113
- function clearAuthSession(state) {
114
- let ok = true;
113
+ function extractAuthUser(payload, fallbackEmail) {
114
+ if (!payload || typeof payload !== 'object') {
115
+ return fallbackEmail ? { email: fallbackEmail } : undefined;
116
+ }
117
+ const body = payload;
118
+ const user = body.user;
119
+ if (user && typeof user === 'object') {
120
+ const typed = user;
121
+ return {
122
+ id: typed.id ? String(typed.id) : undefined,
123
+ email: typed.email ? String(typed.email) : fallbackEmail,
124
+ name: typed.name ? String(typed.name) : typed.username ? String(typed.username) : undefined,
125
+ };
126
+ }
127
+ if (body.email || body.id || body.username) {
128
+ return {
129
+ id: body.id ? String(body.id) : undefined,
130
+ email: body.email ? String(body.email) : fallbackEmail,
131
+ name: body.username ? String(body.username) : body.name ? String(body.name) : undefined,
132
+ };
133
+ }
134
+ return fallbackEmail ? { email: fallbackEmail } : undefined;
135
+ }
136
+ function loadAuthConfig() {
137
+ if (!(0, fs_1.existsSync)(CONFIG_FILE)) {
138
+ return { apiUrl: getApiUrl() };
139
+ }
115
140
  try {
116
- if (fs_1.default.existsSync(AUTH_FILE))
117
- fs_1.default.rmSync(AUTH_FILE, { force: true });
141
+ const parsed = JSON.parse((0, fs_1.readFileSync)(CONFIG_FILE, 'utf8'));
142
+ return {
143
+ apiUrl: trimTrailingSlash(parsed.apiUrl || getApiUrl()),
144
+ token: parsed.token || parsed.authToken,
145
+ user: parsed.user,
146
+ };
118
147
  }
119
148
  catch (error) {
120
- ok = false;
121
- console.error('Failed to remove saved authentication session:', asErrorMessage(error));
149
+ console.warn(chalk_1.default.yellow(`Warning: could not read auth config: ${humanMessage(error)}`));
150
+ return { apiUrl: getApiUrl() };
151
+ }
152
+ }
153
+ function saveAuthConfig(config) {
154
+ ensureConfigDir();
155
+ (0, fs_1.writeFileSync)(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
156
+ try {
157
+ (0, fs_1.chmodSync)(CONFIG_FILE, 0o600);
158
+ }
159
+ catch {
160
+ // Best-effort on non-POSIX filesystems.
161
+ }
162
+ syncSharedConfig(config);
163
+ }
164
+ function clearAuthConfig() {
165
+ if ((0, fs_1.existsSync)(CONFIG_FILE)) {
166
+ (0, fs_1.rmSync)(CONFIG_FILE, { force: true });
122
167
  }
123
- clearBrowserStorage();
124
- resetJwtState(state);
125
- refreshRequests.clear();
126
- return ok;
168
+ clearSharedConfigAuth();
127
169
  }
128
- async function postJson(url, body, token) {
170
+ function getAuthToken() {
171
+ return process.env.VIGTHORIA_TOKEN || loadAuthConfig().token;
172
+ }
173
+ async function requestJson(url, init) {
174
+ const timeoutMsRaw = Number(process.env.VIGTHORIA_AUTH_TIMEOUT_MS || 8000);
175
+ const timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? timeoutMsRaw : 8000;
176
+ const controller = new AbortController();
177
+ const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
129
178
  let response;
130
179
  try {
131
180
  response = await fetch(url, {
132
- method: 'POST',
181
+ ...init,
182
+ signal: controller.signal,
133
183
  headers: {
184
+ accept: 'application/json',
134
185
  'content-type': 'application/json',
135
- ...(token ? { authorization: `Bearer ${token}` } : {}),
186
+ ...(init.headers || {}),
136
187
  },
137
- body: JSON.stringify(body),
138
188
  });
139
189
  }
140
190
  catch (error) {
141
- throw new Error(asErrorMessage(error));
191
+ const name = error?.name;
192
+ if (name === 'AbortError') {
193
+ throw new Error(`Authentication request timed out after ${timeoutMs}ms`);
194
+ }
195
+ throw error;
142
196
  }
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
- });
197
+ finally {
198
+ clearTimeout(timeoutHandle);
154
199
  }
155
- catch (error) {
156
- throw new Error(asErrorMessage(error));
200
+ const text = await response.text();
201
+ let body = {};
202
+ if (text.trim()) {
203
+ try {
204
+ body = JSON.parse(text);
205
+ }
206
+ catch {
207
+ if (/^\s*</.test(text) || /<!doctype html/i.test(text)) {
208
+ throw new HttpError(response.status, 'The Vigthoria service returned an unexpected HTML response. Please verify auth routes.');
209
+ }
210
+ throw new HttpError(response.status, text.slice(0, 240));
211
+ }
157
212
  }
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}`;
213
+ if (!response.ok) {
214
+ const message = typeof body === 'object' && body && 'error' in body
215
+ ? String(body.error)
216
+ : typeof body === 'object' && body && 'message' in body
217
+ ? String(body.message)
218
+ : `Request failed with status ${response.status}`;
219
+ throw new HttpError(response.status, message);
220
+ }
221
+ return body;
197
222
  }
198
- async function performRefresh(state, session) {
199
- if (!session.refreshToken) {
200
- resetJwtState(state);
201
- return null;
223
+ async function ask(question, hidden = false) {
224
+ const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
225
+ if (!hidden) {
226
+ try {
227
+ return await new Promise((resolve) => rl.question(question, resolve));
228
+ }
229
+ finally {
230
+ rl.close();
231
+ }
202
232
  }
233
+ const output = process.stdout;
234
+ const mutableRl = rl;
235
+ const originalWrite = mutableRl._writeToOutput;
236
+ mutableRl._writeToOutput = function writeMasked(value) {
237
+ if (value.includes('\n') || value.includes('\r')) {
238
+ output.write(value);
239
+ }
240
+ else {
241
+ output.write('*');
242
+ }
243
+ };
203
244
  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;
245
+ return await new Promise((resolve) => rl.question(question, resolve));
214
246
  }
215
- catch (error) {
216
- console.error('Failed to refresh authentication session:', asErrorMessage(error));
217
- clearAuthSession(state);
218
- return null;
247
+ finally {
248
+ if (originalWrite) {
249
+ mutableRl._writeToOutput = originalWrite;
250
+ }
251
+ output.write('\n');
252
+ rl.close();
219
253
  }
220
254
  }
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;
255
+ function markSuccessExit() {
256
+ process.exitCode = 0;
257
+ }
258
+ function markErrorExit() {
259
+ process.exitCode = 1;
240
260
  }
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;
261
+ async function login(email, password) {
246
262
  try {
247
- const profile = await getJson(`${apiUrl}/api/auth/me`, token);
248
- user = profile.data || profile.user;
263
+ const resolvedEmail = (email || '').trim();
264
+ const resolvedPassword = password || '';
265
+ if (!resolvedEmail || !resolvedPassword) {
266
+ throw new Error('Email and password are required. Pass --email and --password or run login interactively.');
267
+ }
268
+ const authBases = getAuthBaseCandidates(getApiUrl());
269
+ const endpointVariants = [
270
+ { path: '/auth/login', body: { email: resolvedEmail, password: resolvedPassword } },
271
+ { path: '/api/auth/login', body: { email: resolvedEmail, password: resolvedPassword } },
272
+ { path: '/api/login-json', body: { identifier: resolvedEmail, password: resolvedPassword } },
273
+ { path: '/api/login', body: { email: resolvedEmail, password: resolvedPassword } },
274
+ ];
275
+ const attempted = [];
276
+ const failures = [];
277
+ for (const base of authBases) {
278
+ for (const variant of endpointVariants) {
279
+ const endpoint = `${trimTrailingSlash(base)}${variant.path}`;
280
+ attempted.push(endpoint);
281
+ try {
282
+ const result = await requestJson(endpoint, {
283
+ method: 'POST',
284
+ body: JSON.stringify(variant.body),
285
+ });
286
+ const token = extractAuthToken(result);
287
+ if (!token) {
288
+ failures.push(`${endpoint} -> success without token`);
289
+ continue;
290
+ }
291
+ const config = {
292
+ apiUrl: trimTrailingSlash(base),
293
+ token,
294
+ user: extractAuthUser(result, resolvedEmail),
295
+ success: true,
296
+ };
297
+ saveAuthConfig(config);
298
+ markSuccessExit();
299
+ return config;
300
+ }
301
+ catch (error) {
302
+ const message = humanMessage(error);
303
+ failures.push(`${endpoint} -> ${message}`);
304
+ }
305
+ }
306
+ }
307
+ throw new Error([
308
+ 'Login failed on all known auth endpoints.',
309
+ `Tried: ${attempted.join(', ')}`,
310
+ `Last errors: ${failures.slice(-4).join(' | ')}`,
311
+ ].join(' '));
249
312
  }
250
313
  catch (error) {
251
- throw new Error(`Token validation failed: ${asErrorMessage(error)}`);
314
+ const message = humanMessage(error);
315
+ markErrorExit();
316
+ throw new Error(message);
252
317
  }
253
- const now = new Date().toISOString();
254
- const session = { accessToken: token, user, apiUrl, createdAt: now, updatedAt: now };
255
- writeAuthSession(session);
256
- return session;
257
318
  }
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;
319
+ async function logout() {
320
+ const config = loadAuthConfig();
321
+ if (config.token) {
322
+ const bases = getAuthBaseCandidates(config.apiUrl || getApiUrl());
323
+ const logoutPaths = ['/auth/logout', '/api/auth/logout', '/api/logout'];
324
+ let remoteLogoutDone = false;
325
+ for (const base of bases) {
326
+ for (const route of logoutPaths) {
327
+ try {
328
+ await requestJson(`${trimTrailingSlash(base)}${route}`, {
329
+ method: 'POST',
330
+ headers: { authorization: `Bearer ${config.token}` },
331
+ });
332
+ remoteLogoutDone = true;
333
+ break;
334
+ }
335
+ catch {
336
+ // Continue trying known routes.
337
+ }
338
+ }
339
+ if (remoteLogoutDone) {
340
+ break;
341
+ }
342
+ }
343
+ if (!remoteLogoutDone) {
344
+ console.warn(chalk_1.default.yellow('Remote logout endpoint not reachable; local credentials were still cleared.'));
345
+ }
346
+ }
347
+ clearAuthConfig();
348
+ process.exitCode = 0;
266
349
  }
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);
350
+ async function whoami() {
351
+ const config = loadAuthConfig();
352
+ if (!config.token) {
353
+ return undefined;
354
+ }
355
+ const bases = getAuthBaseCandidates(config.apiUrl || getApiUrl());
356
+ const attempted = [];
357
+ for (const base of bases) {
358
+ const normalizedBase = trimTrailingSlash(base);
359
+ const getRoutes = ['/auth/me', '/api/auth/me', '/api/user/info', '/api/profile'];
360
+ for (const route of getRoutes) {
361
+ const endpoint = `${normalizedBase}${route}`;
362
+ attempted.push(endpoint);
363
+ try {
364
+ const result = await requestJson(endpoint, {
365
+ method: 'GET',
366
+ headers: { authorization: `Bearer ${config.token}` },
367
+ });
368
+ const user = extractAuthUser(result) || extractAuthUser(result.user);
369
+ if (user) {
370
+ saveAuthConfig({ ...config, apiUrl: normalizedBase, user });
371
+ return user;
372
+ }
373
+ }
374
+ catch {
375
+ // continue fallback route probing
376
+ }
377
+ }
378
+ const verifyEndpoint = `${normalizedBase}/api/auth/verify`;
379
+ attempted.push(verifyEndpoint);
281
380
  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;
381
+ const result = await requestJson(verifyEndpoint, {
382
+ method: 'POST',
383
+ headers: { authorization: `Bearer ${config.token}` },
384
+ body: JSON.stringify({ token: config.token }),
385
+ });
386
+ const user = extractAuthUser(result) || extractAuthUser(result.user);
387
+ if (user) {
388
+ saveAuthConfig({ ...config, apiUrl: normalizedBase, user });
389
+ return user;
287
390
  }
288
391
  }
289
- catch (error) {
290
- const message = asErrorMessage(error);
291
- if (!/pending|authorization_pending|slow_down/i.test(message))
292
- throw new Error(message);
392
+ catch {
393
+ // continue fallback route probing
293
394
  }
294
395
  }
295
- throw new Error('Timed out waiting for device authentication.');
396
+ throw new Error(`Unable to verify account on known auth endpoints. Tried ${attempted.join(', ')}`);
296
397
  }
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,
398
+ async function doctor() {
399
+ const config = loadAuthConfig();
400
+ const report = {
401
+ nodeVersion: process.version,
402
+ platform: process.platform,
403
+ arch: process.arch,
404
+ configFile: CONFIG_FILE,
405
+ loggedIn: Boolean(config.token),
406
+ apiUrl: config.apiUrl,
313
407
  };
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()}`);
408
+ process.exitCode = 0;
409
+ return report;
323
410
  }
324
- async function loginAction(options = {}) {
411
+ async function handleLogin(options = {}) {
325
412
  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);
413
+ if (options.token) {
414
+ const config = {
415
+ apiUrl: getApiUrl(),
416
+ token: options.token,
417
+ success: true,
418
+ };
419
+ saveAuthConfig(config);
420
+ console.log(chalk_1.default.green('Logged in successfully with API token.'));
421
+ process.exitCode = 0;
422
+ return config;
344
423
  }
345
- catch (error) {
346
- console.warn('Remote logout failed; local session will still be cleared:', asErrorMessage(error));
424
+ let email = options.email?.trim();
425
+ let password = options.password;
426
+ if (options.device) {
427
+ console.log(chalk_1.default.yellow('Device-code login is not enabled by this Vigthoria service. Falling back to email and password authentication.'));
428
+ }
429
+ if (!email) {
430
+ email = (await ask('Email: ')).trim();
431
+ }
432
+ if (!password) {
433
+ password = await ask('Password: ', true);
347
434
  }
435
+ if (!email || !password) {
436
+ throw new Error('Email and password are required. Use --email and --password, or run vigthoria login interactively.');
437
+ }
438
+ const config = await login(email, password);
439
+ console.log(chalk_1.default.green('Logged in successfully.'));
440
+ if (config.user?.email) {
441
+ console.log(`Account: ${config.user.email}`);
442
+ }
443
+ console.log(`API: ${config.apiUrl}`);
444
+ process.exitCode = 0;
445
+ return config;
348
446
  }
349
- if (!clearAuthSession(state))
447
+ catch (error) {
448
+ console.error(chalk_1.default.red(`Login failed: ${humanMessage(error)}`));
350
449
  process.exitCode = 1;
351
- else
352
- console.log('Logged out.');
450
+ return undefined;
451
+ }
353
452
  }
354
- async function handleLogin(config) {
453
+ async function handleLogout(_options) {
355
454
  try {
356
- await loginAction(config || {});
455
+ await logout();
456
+ console.log(chalk_1.default.green('Logged out successfully.'));
457
+ process.exitCode = 0;
357
458
  }
358
459
  catch (error) {
359
- console.error('Login failed:', asErrorMessage(error));
460
+ console.error(chalk_1.default.red(`Logout failed: ${humanMessage(error)}`));
360
461
  process.exitCode = 1;
361
462
  }
362
463
  }
363
- async function handleLogout(config) {
364
- await logoutAction(config && typeof config === 'object' ? config : null);
365
- }
366
464
  async function statusAction() {
367
- const session = await getValidAuthSession();
368
- if (!session) {
369
- console.log('Not authenticated. Run `vigthoria auth login` to sign in.');
465
+ try {
466
+ const config = loadAuthConfig();
467
+ if (!config.token) {
468
+ console.log(chalk_1.default.yellow('Not logged in.'));
469
+ process.exitCode = 0;
470
+ return;
471
+ }
472
+ let user = config.user;
473
+ try {
474
+ user = await whoami();
475
+ }
476
+ catch {
477
+ // Keep local status available even if remote whoami endpoint is down.
478
+ }
479
+ const sharedConfig = new config_js_1.Config();
480
+ let overallStatus = 'Unknown';
481
+ let coderStatus = 'Unknown';
482
+ let modelsStatus = 'Unknown';
483
+ try {
484
+ const [coderResponse, modelsResponse] = await Promise.all([
485
+ fetch(`${trimTrailingSlash(config.apiUrl)}/api/health`),
486
+ fetch('https://api.vigthoria.io/health'),
487
+ ]);
488
+ const coderJson = await coderResponse.json().catch(() => ({}));
489
+ const modelsJson = await modelsResponse.json().catch(() => ({}));
490
+ const coderOk = coderResponse.ok && (coderJson.status === 'ok' || coderJson.healthy === true);
491
+ const modelsOk = modelsResponse.ok && (modelsJson.status === 'ok' || modelsJson.status === 'healthy' || modelsJson.healthy === true);
492
+ coderStatus = coderOk ? 'Online' : 'Offline';
493
+ modelsStatus = modelsOk ? 'Online' : 'Offline';
494
+ overallStatus = coderOk && modelsOk ? 'Healthy' : 'Degraded';
495
+ }
496
+ catch {
497
+ // Status should still render even if health probes fail.
498
+ }
499
+ const plan = String(sharedConfig.get('subscription')?.plan || '').trim();
500
+ console.log(chalk_1.default.white('Account Status'));
501
+ console.log(chalk_1.default.green('Logged in.'));
502
+ if (user?.email) {
503
+ console.log(`Account: ${user.email}`);
504
+ }
505
+ if (plan) {
506
+ console.log(`Plan: ${plan.toUpperCase()}`);
507
+ }
508
+ console.log(`Overall: ${overallStatus}`);
509
+ console.log(`Coder API: ${coderStatus}`);
510
+ console.log(`Models API: ${modelsStatus}`);
511
+ console.log(`API: ${config.apiUrl}`);
512
+ process.exitCode = 0;
513
+ }
514
+ catch (error) {
515
+ console.error(chalk_1.default.red(`Unable to read status: ${humanMessage(error)}`));
370
516
  process.exitCode = 1;
371
- return;
372
517
  }
373
- printSession(session);
374
518
  }
375
- function createAuthCommand() {
376
- const auth = new commander_1.Command('auth').description('Manage Vigthoria CLI authentication');
519
+ function registerAuthCommands(program) {
520
+ const auth = program.command('auth').description('Manage Vigthoria CLI authentication');
377
521
  auth
378
522
  .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')
523
+ .description('Sign in with your Vigthoria account')
524
+ .option('-t, --token <token>', 'API token for token-based authentication')
525
+ .option('-e, --email <email>', 'Account email address')
526
+ .option('-p, --password <password>', 'Account password')
527
+ .option('--device', 'Use OAuth device flow (requires server support)')
385
528
  .action(async (options) => {
386
529
  await handleLogin(options);
387
530
  });
388
531
  auth
389
532
  .command('logout')
390
- .description('Clear the saved Vigthoria session')
533
+ .description('Sign out and remove saved credentials')
391
534
  .action(async () => {
392
- await handleLogout(null);
535
+ await handleLogout();
393
536
  });
394
537
  auth
395
- .command('status')
396
- .description('Show current authentication status')
538
+ .command('whoami')
539
+ .description('Show the authenticated Vigthoria account')
397
540
  .action(async () => {
398
- await statusAction();
541
+ try {
542
+ const user = await whoami();
543
+ if (!user) {
544
+ console.log(chalk_1.default.yellow('Not logged in.'));
545
+ process.exitCode = 0;
546
+ return;
547
+ }
548
+ console.log(chalk_1.default.green('Logged in.'));
549
+ console.log(user.email || user.name || user.id || 'Authenticated user');
550
+ process.exitCode = 0;
551
+ }
552
+ catch (error) {
553
+ console.error(chalk_1.default.red(`Unable to fetch account: ${humanMessage(error)}`));
554
+ process.exitCode = 1;
555
+ }
399
556
  });
400
- auth.action(() => auth.help());
401
- return auth;
402
- }
403
- function registerAuthCommand(program) {
404
- const command = createAuthCommand();
405
- program.addCommand(command);
406
- return command;
407
557
  }
408
- exports.authCommand = createAuthCommand();
409
- exports.default = exports.authCommand;