vigthoria-cli 1.9.2 → 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.
@@ -3,407 +3,476 @@ 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"));
26
23
  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(/\/$/, '');
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
+ }
33
33
  }
34
- function ensureConfigDir() {
35
- fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
34
+ function trimTrailingSlash(value) {
35
+ return value.replace(/\/+$/, '');
36
36
  }
37
- function writeAuthSession(session) {
38
- ensureConfigDir();
39
- fs_1.default.writeFileSync(AUTH_FILE, `${JSON.stringify(session, null, 2)}\n`, { mode: 0o600 });
37
+ function uniqueStrings(values) {
38
+ return [...new Set(values.filter(Boolean))];
40
39
  }
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
- }
45
- return error instanceof Error ? error.message : String(error);
40
+ function getApiUrl() {
41
+ return trimTrailingSlash(process.env.VIGTHORIA_API_URL || DEFAULT_API_URL);
46
42
  }
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 : {};
43
+ function derivePeerHost(baseUrl) {
44
+ if (baseUrl.includes('://api.vigthoria.io')) {
45
+ return baseUrl.replace('://api.vigthoria.io', '://coder.vigthoria.io');
54
46
  }
55
- catch {
56
- return { message: text.slice(0, 300) };
47
+ if (baseUrl.includes('://coder.vigthoria.io')) {
48
+ return baseUrl.replace('://coder.vigthoria.io', '://api.vigthoria.io');
57
49
  }
50
+ return undefined;
58
51
  }
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}`;
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);
73
56
  }
74
- function readAuthSession() {
57
+ function ensureConfigDir() {
58
+ if (!(0, fs_1.existsSync)(CONFIG_DIR)) {
59
+ (0, fs_1.mkdirSync)(CONFIG_DIR, { recursive: true, mode: 0o700 });
60
+ }
75
61
  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;
62
+ (0, fs_1.chmodSync)(CONFIG_DIR, 0o700);
82
63
  }
83
- catch (error) {
84
- console.error('Failed to read saved authentication session:', asErrorMessage(error));
85
- return null;
64
+ catch {
65
+ // Best-effort on non-POSIX filesystems.
86
66
  }
87
67
  }
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');
98
- }
99
- if (typeof storage.clear === 'function')
100
- storage.clear();
101
- }
102
- catch (error) {
103
- console.warn('Failed to clear local auth storage:', asErrorMessage(error));
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.';
104
72
  }
73
+ return raw || 'Unknown authentication error.';
105
74
  }
106
- function resetJwtState(state) {
107
- if (!state)
108
- return;
109
- state.token = null;
110
- state.expiresAt = null;
111
- state.refreshToken = null;
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;
112
81
  }
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 });
82
+ function extractAuthUser(payload, fallbackEmail) {
83
+ if (!payload || typeof payload !== 'object') {
84
+ return fallbackEmail ? { email: fallbackEmail } : undefined;
118
85
  }
119
- catch (error) {
120
- ok = false;
121
- console.error('Failed to remove saved authentication session:', asErrorMessage(error));
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,
94
+ };
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
+ };
122
102
  }
123
- clearBrowserStorage();
124
- resetJwtState(state);
125
- refreshRequests.clear();
126
- return ok;
103
+ return fallbackEmail ? { email: fallbackEmail } : undefined;
127
104
  }
128
- async function postJson(url, body, token) {
129
- let response;
105
+ function loadAuthConfig() {
106
+ if (!(0, fs_1.existsSync)(CONFIG_FILE)) {
107
+ return { apiUrl: getApiUrl() };
108
+ }
130
109
  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),
138
- });
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
+ };
139
116
  }
140
117
  catch (error) {
141
- throw new Error(asErrorMessage(error));
118
+ console.warn(chalk_1.default.yellow(`Warning: could not read auth config: ${humanMessage(error)}`));
119
+ return { apiUrl: getApiUrl() };
142
120
  }
143
- const data = await parseResponseBody(response);
144
- if (!response.ok)
145
- throw new Error(responseErrorMessage(response, data));
146
- return data;
147
121
  }
148
- async function getJson(url, token) {
149
- let response;
122
+ function saveAuthConfig(config) {
123
+ ensureConfigDir();
124
+ (0, fs_1.writeFileSync)(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
150
125
  try {
151
- response = await fetch(url, {
152
- headers: token ? { authorization: `Bearer ${token}` } : undefined,
153
- });
126
+ (0, fs_1.chmodSync)(CONFIG_FILE, 0o600);
154
127
  }
155
- catch (error) {
156
- throw new Error(asErrorMessage(error));
128
+ catch {
129
+ // Best-effort on non-POSIX filesystems.
157
130
  }
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
131
  }
183
- function isExpired(session) {
184
- return typeof session.expiresAt === 'number' && Date.now() >= session.expiresAt - REFRESH_SKEW_MS;
132
+ function clearAuthConfig() {
133
+ if ((0, fs_1.existsSync)(CONFIG_FILE)) {
134
+ (0, fs_1.rmSync)(CONFIG_FILE, { force: true });
135
+ }
185
136
  }
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;
137
+ function getAuthToken() {
138
+ return process.env.VIGTHORIA_TOKEN || loadAuthConfig().token;
192
139
  }
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}`;
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()) {
152
+ try {
153
+ body = JSON.parse(text);
154
+ }
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));
160
+ }
161
+ }
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;
197
171
  }
198
- async function performRefresh(state, session) {
199
- if (!session.refreshToken) {
200
- resetJwtState(state);
201
- return null;
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));
177
+ }
178
+ finally {
179
+ rl.close();
180
+ }
202
181
  }
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);
188
+ }
189
+ else {
190
+ output.write('*');
191
+ }
192
+ };
203
193
  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;
194
+ return await new Promise((resolve) => rl.question(question, resolve));
214
195
  }
215
- catch (error) {
216
- console.error('Failed to refresh authentication session:', asErrorMessage(error));
217
- clearAuthSession(state);
218
- return null;
196
+ finally {
197
+ if (originalWrite) {
198
+ mutableRl._writeToOutput = originalWrite;
199
+ }
200
+ output.write('\n');
201
+ rl.close();
219
202
  }
220
203
  }
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;
204
+ function markSuccessExit() {
205
+ process.exitCode = 0;
240
206
  }
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;
207
+ function markErrorExit() {
208
+ process.exitCode = 1;
209
+ }
210
+ async function login(email, password) {
246
211
  try {
247
- const profile = await getJson(`${apiUrl}/api/auth/me`, token);
248
- user = profile.data || profile.user;
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.');
216
+ }
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
+ }
255
+ }
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(' '));
249
261
  }
250
262
  catch (error) {
251
- throw new Error(`Token validation failed: ${asErrorMessage(error)}`);
263
+ const message = humanMessage(error);
264
+ markErrorExit();
265
+ throw new Error(message);
252
266
  }
253
- const now = new Date().toISOString();
254
- const session = { accessToken: token, user, apiUrl, createdAt: now, updatedAt: now };
255
- writeAuthSession(session);
256
- return session;
257
267
  }
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;
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
+ }
291
+ }
292
+ if (!remoteLogoutDone) {
293
+ console.warn(chalk_1.default.yellow('Remote logout endpoint not reachable; local credentials were still cleared.'));
294
+ }
295
+ }
296
+ clearAuthConfig();
297
+ process.exitCode = 0;
266
298
  }
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);
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
+ }
326
+ }
327
+ const verifyEndpoint = `${normalizedBase}/api/auth/verify`;
328
+ attempted.push(verifyEndpoint);
281
329
  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;
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;
287
339
  }
288
340
  }
289
- catch (error) {
290
- const message = asErrorMessage(error);
291
- if (!/pending|authorization_pending|slow_down/i.test(message))
292
- throw new Error(message);
341
+ catch {
342
+ // continue fallback route probing
293
343
  }
294
344
  }
295
- throw new Error('Timed out waiting for device authentication.');
345
+ throw new Error(`Unable to verify account on known auth endpoints. Tried ${attempted.join(', ')}`);
296
346
  }
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,
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,
313
356
  };
314
- const token = await refreshJwtIfNeeded(state);
315
- return token ? readAuthSession() : null;
357
+ process.exitCode = 0;
358
+ return report;
316
359
  }
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 = {}) {
360
+ async function handleLogin(options = {}) {
325
361
  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);
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;
344
372
  }
345
- catch (error) {
346
- console.warn('Remote logout failed; local session will still be cleared:', asErrorMessage(error));
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.'));
347
377
  }
378
+ if (!email) {
379
+ email = (await ask('Email: ')).trim();
380
+ }
381
+ if (!password) {
382
+ password = await ask('Password: ', true);
383
+ }
384
+ if (!email || !password) {
385
+ throw new Error('Email and password are required. Use --email and --password, or run vigthoria login interactively.');
386
+ }
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}`);
393
+ process.exitCode = 0;
394
+ return config;
348
395
  }
349
- if (!clearAuthSession(state))
396
+ catch (error) {
397
+ console.error(chalk_1.default.red(`Login failed: ${humanMessage(error)}`));
350
398
  process.exitCode = 1;
351
- else
352
- console.log('Logged out.');
399
+ return undefined;
400
+ }
353
401
  }
354
- async function handleLogin(config) {
402
+ async function handleLogout(_options) {
355
403
  try {
356
- await loginAction(config || {});
404
+ await logout();
405
+ console.log(chalk_1.default.green('Logged out successfully.'));
406
+ process.exitCode = 0;
357
407
  }
358
408
  catch (error) {
359
- console.error('Login failed:', asErrorMessage(error));
409
+ console.error(chalk_1.default.red(`Logout failed: ${humanMessage(error)}`));
360
410
  process.exitCode = 1;
361
411
  }
362
412
  }
363
- async function handleLogout(config) {
364
- await logoutAction(config && typeof config === 'object' ? config : null);
365
- }
366
413
  async function statusAction() {
367
- const session = await getValidAuthSession();
368
- if (!session) {
369
- console.log('Not authenticated. Run `vigthoria auth login` to sign in.');
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.
427
+ }
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;
434
+ }
435
+ catch (error) {
436
+ console.error(chalk_1.default.red(`Unable to read status: ${humanMessage(error)}`));
370
437
  process.exitCode = 1;
371
- return;
372
438
  }
373
- printSession(session);
374
439
  }
375
- function createAuthCommand() {
376
- const auth = new commander_1.Command('auth').description('Manage Vigthoria CLI authentication');
440
+ function registerAuthCommands(program) {
441
+ const auth = program.command('auth').description('Manage Vigthoria CLI authentication');
377
442
  auth
378
443
  .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')
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)')
385
449
  .action(async (options) => {
386
450
  await handleLogin(options);
387
451
  });
388
452
  auth
389
453
  .command('logout')
390
- .description('Clear the saved Vigthoria session')
454
+ .description('Sign out and remove saved credentials')
391
455
  .action(async () => {
392
- await handleLogout(null);
456
+ await handleLogout();
393
457
  });
394
458
  auth
395
- .command('status')
396
- .description('Show current authentication status')
459
+ .command('whoami')
460
+ .description('Show the authenticated Vigthoria account')
397
461
  .action(async () => {
398
- await statusAction();
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
+ }
399
477
  });
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
478
  }
408
- exports.authCommand = createAuthCommand();
409
- exports.default = exports.authCommand;