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.
- package/README.md +15 -5
- package/dist/commands/auth.d.ts +28 -38
- package/dist/commands/auth.js +461 -313
- package/dist/commands/bridge.js +3 -8
- package/dist/commands/chat.d.ts +3 -0
- package/dist/commands/chat.js +97 -34
- package/dist/commands/index.js +1 -1
- package/dist/commands/legion.d.ts +22 -19
- package/dist/commands/legion.js +561 -134
- package/dist/commands/preview.js +32 -7
- package/dist/commands/repo.js +19 -13
- package/dist/commands/security.d.ts +20 -0
- package/dist/commands/security.js +98 -0
- package/dist/commands/update.d.ts +9 -0
- package/dist/commands/update.js +235 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +147 -40
- package/dist/utils/api.d.ts +25 -70
- package/dist/utils/api.js +875 -693
- package/dist/utils/config.js +1 -1
- package/dist/utils/tools.d.ts +11 -0
- package/dist/utils/tools.js +251 -5
- package/install.ps1 +322 -0
- package/install.sh +314 -0
- package/package.json +18 -3
- package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +159 -0
- package/scripts/release/publish-cli-release.sh +73 -0
- package/scripts/release/validate-no-go-gates.sh +129 -0
- package/scripts/release/verify-runtime-consistency.mjs +64 -0
package/dist/commands/auth.js
CHANGED
|
@@ -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.
|
|
7
|
-
exports.
|
|
8
|
-
exports.
|
|
9
|
-
exports.
|
|
10
|
-
exports.
|
|
11
|
-
exports.
|
|
12
|
-
exports.
|
|
13
|
-
exports.
|
|
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.
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
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
|
|
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.
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
35
|
-
|
|
35
|
+
function trimTrailingSlash(value) {
|
|
36
|
+
return value.replace(/\/+$/, '');
|
|
36
37
|
}
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
42
|
-
if (
|
|
43
|
-
return '
|
|
44
|
+
function derivePeerHost(baseUrl) {
|
|
45
|
+
if (baseUrl.includes('://api.vigthoria.io')) {
|
|
46
|
+
return baseUrl.replace('://api.vigthoria.io', '://coder.vigthoria.io');
|
|
44
47
|
}
|
|
45
|
-
|
|
48
|
+
if (baseUrl.includes('://coder.vigthoria.io')) {
|
|
49
|
+
return baseUrl.replace('://coder.vigthoria.io', '://api.vigthoria.io');
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
46
52
|
}
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
63
|
+
(0, fs_1.chmodSync)(CONFIG_DIR, 0o700);
|
|
54
64
|
}
|
|
55
65
|
catch {
|
|
56
|
-
|
|
66
|
+
// Best-effort on non-POSIX filesystems.
|
|
57
67
|
}
|
|
58
68
|
}
|
|
59
|
-
function
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
84
|
-
|
|
85
|
-
return null;
|
|
83
|
+
catch {
|
|
84
|
+
// Keep legacy auth flow working even if shared config write fails.
|
|
86
85
|
}
|
|
87
86
|
}
|
|
88
|
-
function
|
|
89
|
-
const storage = globalThis.localStorage;
|
|
90
|
-
if (!storage)
|
|
91
|
-
return;
|
|
87
|
+
function clearSharedConfigAuth() {
|
|
92
88
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
103
|
-
|
|
95
|
+
catch {
|
|
96
|
+
// Ignore shared config clear failures during logout.
|
|
104
97
|
}
|
|
105
98
|
}
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
resetJwtState(state);
|
|
125
|
-
refreshRequests.clear();
|
|
126
|
-
return ok;
|
|
168
|
+
clearSharedConfigAuth();
|
|
127
169
|
}
|
|
128
|
-
|
|
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
|
-
|
|
181
|
+
...init,
|
|
182
|
+
signal: controller.signal,
|
|
133
183
|
headers: {
|
|
184
|
+
accept: 'application/json',
|
|
134
185
|
'content-type': 'application/json',
|
|
135
|
-
...(
|
|
186
|
+
...(init.headers || {}),
|
|
136
187
|
},
|
|
137
|
-
body: JSON.stringify(body),
|
|
138
188
|
});
|
|
139
189
|
}
|
|
140
190
|
catch (error) {
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
247
|
+
finally {
|
|
248
|
+
if (originalWrite) {
|
|
249
|
+
mutableRl._writeToOutput = originalWrite;
|
|
250
|
+
}
|
|
251
|
+
output.write('\n');
|
|
252
|
+
rl.close();
|
|
219
253
|
}
|
|
220
254
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
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
|
|
248
|
-
|
|
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
|
-
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
290
|
-
|
|
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(
|
|
396
|
+
throw new Error(`Unable to verify account on known auth endpoints. Tried ${attempted.join(', ')}`);
|
|
296
397
|
}
|
|
297
|
-
async function
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
315
|
-
return
|
|
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
|
|
411
|
+
async function handleLogin(options = {}) {
|
|
325
412
|
try {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
:
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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
|
-
|
|
447
|
+
catch (error) {
|
|
448
|
+
console.error(chalk_1.default.red(`Login failed: ${humanMessage(error)}`));
|
|
350
449
|
process.exitCode = 1;
|
|
351
|
-
|
|
352
|
-
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
353
452
|
}
|
|
354
|
-
async function
|
|
453
|
+
async function handleLogout(_options) {
|
|
355
454
|
try {
|
|
356
|
-
await
|
|
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(
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
376
|
-
const auth =
|
|
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
|
|
380
|
-
.option('--token <token>', '
|
|
381
|
-
.option('--email <email>', '
|
|
382
|
-
.option('--password <password>', '
|
|
383
|
-
.option('--
|
|
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('
|
|
533
|
+
.description('Sign out and remove saved credentials')
|
|
391
534
|
.action(async () => {
|
|
392
|
-
await handleLogout(
|
|
535
|
+
await handleLogout();
|
|
393
536
|
});
|
|
394
537
|
auth
|
|
395
|
-
.command('
|
|
396
|
-
.description('Show
|
|
538
|
+
.command('whoami')
|
|
539
|
+
.description('Show the authenticated Vigthoria account')
|
|
397
540
|
.action(async () => {
|
|
398
|
-
|
|
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;
|