vigthoria-cli 1.8.15 → 1.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -6
- package/dist/commands/auth.d.ts +49 -21
- package/dist/commands/auth.js +385 -343
- package/dist/commands/chat.d.ts +10 -2
- package/dist/commands/chat.js +328 -93
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +40 -20
- package/dist/commands/index.d.ts +12 -0
- package/dist/commands/index.js +182 -0
- package/dist/commands/legion.d.ts +39 -0
- package/dist/commands/legion.js +999 -71
- package/dist/index.d.ts +3 -1
- package/dist/index.js +506 -28
- package/dist/utils/api.d.ts +74 -18
- package/dist/utils/api.js +701 -805
- package/dist/utils/config.js +9 -10
- package/dist/utils/context-ranker.d.ts +24 -0
- package/dist/utils/context-ranker.js +147 -0
- package/dist/utils/post-write-validator.d.ts +25 -0
- package/dist/utils/post-write-validator.js +138 -0
- package/dist/utils/session.d.ts +19 -0
- package/dist/utils/session.js +91 -6
- package/dist/utils/task-display.d.ts +31 -0
- package/dist/utils/task-display.js +115 -0
- package/dist/utils/tools.d.ts +15 -0
- package/dist/utils/tools.js +341 -58
- package/dist/utils/workspace-cache.d.ts +31 -0
- package/dist/utils/workspace-cache.js +96 -0
- package/package.json +7 -3
package/dist/commands/auth.js
CHANGED
|
@@ -1,367 +1,409 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Auth Command - Authentication management
|
|
4
|
-
*/
|
|
5
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
-
if (k2 === undefined) k2 = k;
|
|
7
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
-
}
|
|
11
|
-
Object.defineProperty(o, k2, desc);
|
|
12
|
-
}) : (function(o, m, k, k2) {
|
|
13
|
-
if (k2 === undefined) k2 = k;
|
|
14
|
-
o[k2] = m[k];
|
|
15
|
-
}));
|
|
16
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
-
}) : function(o, v) {
|
|
19
|
-
o["default"] = v;
|
|
20
|
-
});
|
|
21
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
-
var ownKeys = function(o) {
|
|
23
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
-
var ar = [];
|
|
25
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
-
return ar;
|
|
27
|
-
};
|
|
28
|
-
return ownKeys(o);
|
|
29
|
-
};
|
|
30
|
-
return function (mod) {
|
|
31
|
-
if (mod && mod.__esModule) return mod;
|
|
32
|
-
var result = {};
|
|
33
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
-
__setModuleDefault(result, mod);
|
|
35
|
-
return result;
|
|
36
|
-
};
|
|
37
|
-
})();
|
|
38
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
4
|
};
|
|
41
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
-
exports.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
6
|
+
exports.authCommand = void 0;
|
|
7
|
+
exports.readAuthSession = readAuthSession;
|
|
8
|
+
exports.clearAuthSession = clearAuthSession;
|
|
9
|
+
exports.refreshJwtIfNeeded = refreshJwtIfNeeded;
|
|
10
|
+
exports.loginWithToken = loginWithToken;
|
|
11
|
+
exports.loginWithCredentials = loginWithCredentials;
|
|
12
|
+
exports.loginWithDeviceCode = loginWithDeviceCode;
|
|
13
|
+
exports.getValidAuthSession = getValidAuthSession;
|
|
14
|
+
exports.loginAction = loginAction;
|
|
15
|
+
exports.logoutAction = logoutAction;
|
|
16
|
+
exports.handleLogin = handleLogin;
|
|
17
|
+
exports.handleLogout = handleLogout;
|
|
18
|
+
exports.statusAction = statusAction;
|
|
19
|
+
exports.createAuthCommand = createAuthCommand;
|
|
20
|
+
exports.registerAuthCommand = registerAuthCommand;
|
|
21
|
+
const commander_1 = require("commander");
|
|
22
|
+
const fs_1 = __importDefault(require("fs"));
|
|
23
|
+
const os_1 = __importDefault(require("os"));
|
|
24
|
+
const path_1 = __importDefault(require("path"));
|
|
25
|
+
const promises_1 = require("timers/promises");
|
|
26
|
+
const DEFAULT_API_URL = 'https://coder.vigthoria.io';
|
|
27
|
+
const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.vigthoria');
|
|
28
|
+
const AUTH_FILE = path_1.default.join(CONFIG_DIR, 'auth.json');
|
|
29
|
+
const REFRESH_SKEW_MS = 30_000;
|
|
30
|
+
const refreshRequests = new Map();
|
|
31
|
+
function getApiUrl(explicit) {
|
|
32
|
+
return (explicit || process.env.VIGTHORIA_API_URL || DEFAULT_API_URL).replace(/\/$/, '');
|
|
60
33
|
}
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
34
|
+
function ensureConfigDir() {
|
|
35
|
+
fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
36
|
+
}
|
|
37
|
+
function writeAuthSession(session) {
|
|
38
|
+
ensureConfigDir();
|
|
39
|
+
fs_1.default.writeFileSync(AUTH_FILE, `${JSON.stringify(session, null, 2)}\n`, { mode: 0o600 });
|
|
40
|
+
}
|
|
41
|
+
function asErrorMessage(error) {
|
|
42
|
+
if (error instanceof TypeError && /fetch|network|failed|ECONN|ENOTFOUND|ETIMEDOUT/i.test(error.message)) {
|
|
43
|
+
return 'Network error while contacting Vigthoria. Check your connection and API URL.';
|
|
70
44
|
}
|
|
71
|
-
return
|
|
72
|
-
const stdout = process.stdout;
|
|
73
|
-
let answer = '';
|
|
74
|
-
// Pause rl so it doesn't compete for stdin data events
|
|
75
|
-
rl.pause();
|
|
76
|
-
stdout.write(question);
|
|
77
|
-
process.stdin.setRawMode(true);
|
|
78
|
-
process.stdin.resume();
|
|
79
|
-
const onData = (ch) => {
|
|
80
|
-
const c = ch.toString('utf8');
|
|
81
|
-
if (c === '\n' || c === '\r' || c === '\u0004') {
|
|
82
|
-
process.stdin.setRawMode(false);
|
|
83
|
-
process.stdin.removeListener('data', onData);
|
|
84
|
-
stdout.write('\n');
|
|
85
|
-
rl.resume();
|
|
86
|
-
resolve(answer);
|
|
87
|
-
}
|
|
88
|
-
else if (c === '\u007f' || c === '\b') {
|
|
89
|
-
if (answer.length > 0) {
|
|
90
|
-
answer = answer.slice(0, -1);
|
|
91
|
-
stdout.write('\b \b');
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
else if (c === '\u0003') {
|
|
95
|
-
rl.close();
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
answer += c;
|
|
100
|
-
stdout.write('*');
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
process.stdin.on('data', onData);
|
|
104
|
-
});
|
|
45
|
+
return error instanceof Error ? error.message : String(error);
|
|
105
46
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
this.api = new api_js_1.APIClient(config, logger);
|
|
47
|
+
async function parseResponseBody(response) {
|
|
48
|
+
const text = await response.text();
|
|
49
|
+
if (!text)
|
|
50
|
+
return {};
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(text);
|
|
53
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
114
54
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (options.token) {
|
|
118
|
-
await this.loginWithToken(options.token);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
console.log();
|
|
122
|
-
console.log(chalk_1.default.cyan(`${logger_js_1.CH.hDouble.repeat(3)} Vigthoria Login ${logger_js_1.CH.hDouble.repeat(3)}`));
|
|
123
|
-
console.log();
|
|
124
|
-
// Use Node's built-in readline instead of inquirer.
|
|
125
|
-
// inquirer 9.x destroys and recreates readline interfaces per prompt
|
|
126
|
-
// which causes ERR_USE_AFTER_CLOSE on Node 20 Windows TTY.
|
|
127
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
128
|
-
try {
|
|
129
|
-
console.log(chalk_1.default.white(' 1) Email & Password'));
|
|
130
|
-
console.log(chalk_1.default.white(' 2) API Token'));
|
|
131
|
-
console.log(chalk_1.default.white(' 3) Browser Login'));
|
|
132
|
-
console.log();
|
|
133
|
-
const choice = (await ask(rl, chalk_1.default.cyan('? ') + 'Choose login method (1/2/3): ')).trim();
|
|
134
|
-
switch (choice) {
|
|
135
|
-
case '1':
|
|
136
|
-
case 'credentials': {
|
|
137
|
-
const email = (await ask(rl, chalk_1.default.cyan('? ') + 'Email: ')).trim();
|
|
138
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
139
|
-
if (!emailRegex.test(email)) {
|
|
140
|
-
this.logger.error('Please enter a valid email');
|
|
141
|
-
rl.close();
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const password = await askHidden(rl, chalk_1.default.cyan('? ') + 'Password: ');
|
|
145
|
-
rl.close();
|
|
146
|
-
if (password.length < 6) {
|
|
147
|
-
this.logger.error('Password must be at least 6 characters');
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
await this.doCredentialLogin(email, password);
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
case '2':
|
|
154
|
-
case 'token': {
|
|
155
|
-
const token = await askHidden(rl, chalk_1.default.cyan('? ') + 'API Token: ');
|
|
156
|
-
rl.close();
|
|
157
|
-
if (!token) {
|
|
158
|
-
this.logger.error('Please enter your API token');
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
await this.loginWithToken(token);
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
case '3':
|
|
165
|
-
case 'browser':
|
|
166
|
-
rl.close();
|
|
167
|
-
await this.loginWithBrowser();
|
|
168
|
-
break;
|
|
169
|
-
default:
|
|
170
|
-
rl.close();
|
|
171
|
-
this.logger.error('Invalid choice. Please enter 1, 2, or 3.');
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
catch (err) {
|
|
176
|
-
rl.close();
|
|
177
|
-
throw err;
|
|
178
|
-
}
|
|
55
|
+
catch {
|
|
56
|
+
return { message: text.slice(0, 300) };
|
|
179
57
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
58
|
+
}
|
|
59
|
+
function responseErrorMessage(response, data) {
|
|
60
|
+
const serverMessage = typeof data.error === 'string'
|
|
61
|
+
? data.error
|
|
62
|
+
: typeof data.message === 'string'
|
|
63
|
+
? data.message
|
|
64
|
+
: undefined;
|
|
65
|
+
if (response.status === 400 || response.status === 401 || response.status === 403) {
|
|
66
|
+
return serverMessage || 'Invalid credentials. Check your email, password, or token and try again.';
|
|
190
67
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
68
|
+
if (response.status === 404)
|
|
69
|
+
return serverMessage || 'Authentication endpoint was not found for the configured API URL.';
|
|
70
|
+
if (response.status >= 500)
|
|
71
|
+
return serverMessage || 'Vigthoria authentication service is temporarily unavailable. Try again later.';
|
|
72
|
+
return serverMessage || `Request failed with HTTP ${response.status}`;
|
|
73
|
+
}
|
|
74
|
+
function readAuthSession() {
|
|
75
|
+
try {
|
|
76
|
+
if (!fs_1.default.existsSync(AUTH_FILE))
|
|
77
|
+
return null;
|
|
78
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(AUTH_FILE, 'utf8'));
|
|
79
|
+
if (!parsed || typeof parsed.accessToken !== 'string' || !parsed.accessToken)
|
|
80
|
+
return null;
|
|
81
|
+
return parsed;
|
|
201
82
|
}
|
|
202
|
-
|
|
203
|
-
console.
|
|
204
|
-
|
|
205
|
-
console.log();
|
|
206
|
-
console.log(chalk_1.default.cyan('Visit: https://coder.vigthoria.io/cli-auth'));
|
|
207
|
-
console.log();
|
|
208
|
-
console.log(chalk_1.default.gray('After authenticating, copy the token and run:'));
|
|
209
|
-
console.log(chalk_1.default.white(' vigthoria login --token YOUR_TOKEN'));
|
|
210
|
-
console.log();
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error('Failed to read saved authentication session:', asErrorMessage(error));
|
|
85
|
+
return null;
|
|
211
86
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
87
|
+
}
|
|
88
|
+
function clearBrowserStorage() {
|
|
89
|
+
const storage = globalThis.localStorage;
|
|
90
|
+
if (!storage)
|
|
91
|
+
return;
|
|
92
|
+
try {
|
|
93
|
+
if (typeof storage.removeItem === 'function') {
|
|
94
|
+
storage.removeItem('vigthoria.auth');
|
|
95
|
+
storage.removeItem('vigthoria.jwt');
|
|
96
|
+
storage.removeItem('auth');
|
|
97
|
+
storage.removeItem('jwt');
|
|
220
98
|
}
|
|
99
|
+
if (typeof storage.clear === 'function')
|
|
100
|
+
storage.clear();
|
|
221
101
|
}
|
|
222
|
-
|
|
223
|
-
console.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
console.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
models.forEach(m => {
|
|
259
|
-
console.log(chalk_1.default.gray(` ${logger_js_1.CH.bullet} `) + chalk_1.default.cyan(m.id) + chalk_1.default.gray(' -> ') + chalk_1.default.white(m.name));
|
|
260
|
-
const runtimeLabel = m.tier === 'cloud' ? 'cloud' : 'blackwell';
|
|
261
|
-
console.log(chalk_1.default.gray(` ${m.tier === 'cloud' ? logger_js_1.CH.cloud : logger_js_1.CH.home} ${m.description}`));
|
|
262
|
-
console.log(chalk_1.default.gray(` Backend: ${m.backendModel} (${runtimeLabel})`));
|
|
102
|
+
catch (error) {
|
|
103
|
+
console.warn('Failed to clear local auth storage:', asErrorMessage(error));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function resetJwtState(state) {
|
|
107
|
+
if (!state)
|
|
108
|
+
return;
|
|
109
|
+
state.token = null;
|
|
110
|
+
state.expiresAt = null;
|
|
111
|
+
state.refreshToken = null;
|
|
112
|
+
}
|
|
113
|
+
function clearAuthSession(state) {
|
|
114
|
+
let ok = true;
|
|
115
|
+
try {
|
|
116
|
+
if (fs_1.default.existsSync(AUTH_FILE))
|
|
117
|
+
fs_1.default.rmSync(AUTH_FILE, { force: true });
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
ok = false;
|
|
121
|
+
console.error('Failed to remove saved authentication session:', asErrorMessage(error));
|
|
122
|
+
}
|
|
123
|
+
clearBrowserStorage();
|
|
124
|
+
resetJwtState(state);
|
|
125
|
+
refreshRequests.clear();
|
|
126
|
+
return ok;
|
|
127
|
+
}
|
|
128
|
+
async function postJson(url, body, token) {
|
|
129
|
+
let response;
|
|
130
|
+
try {
|
|
131
|
+
response = await fetch(url, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: {
|
|
134
|
+
'content-type': 'application/json',
|
|
135
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify(body),
|
|
263
138
|
});
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
throw new Error(asErrorMessage(error));
|
|
142
|
+
}
|
|
143
|
+
const data = await parseResponseBody(response);
|
|
144
|
+
if (!response.ok)
|
|
145
|
+
throw new Error(responseErrorMessage(response, data));
|
|
146
|
+
return data;
|
|
147
|
+
}
|
|
148
|
+
async function getJson(url, token) {
|
|
149
|
+
let response;
|
|
150
|
+
try {
|
|
151
|
+
response = await fetch(url, {
|
|
152
|
+
headers: token ? { authorization: `Bearer ${token}` } : undefined,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
throw new Error(asErrorMessage(error));
|
|
157
|
+
}
|
|
158
|
+
const data = await parseResponseBody(response);
|
|
159
|
+
if (!response.ok)
|
|
160
|
+
throw new Error(responseErrorMessage(response, data));
|
|
161
|
+
return data;
|
|
162
|
+
}
|
|
163
|
+
function normalizeSession(data, apiUrl) {
|
|
164
|
+
const accessToken = data.accessToken || data.token;
|
|
165
|
+
if (!accessToken)
|
|
166
|
+
throw new Error('Authentication response did not include an access token.');
|
|
167
|
+
const now = new Date().toISOString();
|
|
168
|
+
const expiresAt = typeof data.expiresAt === 'number'
|
|
169
|
+
? data.expiresAt
|
|
170
|
+
: typeof data.expiresIn === 'number'
|
|
171
|
+
? Date.now() + data.expiresIn * 1000
|
|
172
|
+
: undefined;
|
|
173
|
+
return {
|
|
174
|
+
accessToken,
|
|
175
|
+
refreshToken: data.refreshToken,
|
|
176
|
+
user: data.user,
|
|
177
|
+
expiresAt,
|
|
178
|
+
apiUrl,
|
|
179
|
+
createdAt: now,
|
|
180
|
+
updatedAt: now,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function isExpired(session) {
|
|
184
|
+
return typeof session.expiresAt === 'number' && Date.now() >= session.expiresAt - REFRESH_SKEW_MS;
|
|
185
|
+
}
|
|
186
|
+
function jwtNeedsRefresh(state) {
|
|
187
|
+
if (!state.token)
|
|
188
|
+
return false;
|
|
189
|
+
if (typeof state.isExpired === 'function')
|
|
190
|
+
return state.isExpired();
|
|
191
|
+
return typeof state.expiresAt === 'number' && Date.now() >= state.expiresAt - REFRESH_SKEW_MS;
|
|
192
|
+
}
|
|
193
|
+
function refreshKey(state, session) {
|
|
194
|
+
const apiUrl = getApiUrl(state.apiUrl || session?.apiUrl);
|
|
195
|
+
const refreshToken = state.refreshToken || session?.refreshToken || 'no-refresh-token';
|
|
196
|
+
return `${apiUrl}:${refreshToken}`;
|
|
197
|
+
}
|
|
198
|
+
async function performRefresh(state, session) {
|
|
199
|
+
if (!session.refreshToken) {
|
|
200
|
+
resetJwtState(state);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const data = await postJson(`${session.apiUrl}/api/auth/refresh`, { refreshToken: session.refreshToken, client: 'vigthoria-cli' });
|
|
205
|
+
const refreshed = normalizeSession({ ...data, refreshToken: data.refreshToken || session.refreshToken }, session.apiUrl);
|
|
206
|
+
refreshed.createdAt = session.createdAt;
|
|
207
|
+
refreshed.updatedAt = new Date().toISOString();
|
|
208
|
+
writeAuthSession(refreshed);
|
|
209
|
+
state.token = refreshed.accessToken;
|
|
210
|
+
state.expiresAt = refreshed.expiresAt ?? null;
|
|
211
|
+
state.refreshToken = refreshed.refreshToken ?? session.refreshToken;
|
|
212
|
+
state.apiUrl = refreshed.apiUrl;
|
|
213
|
+
return refreshed.accessToken;
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
console.error('Failed to refresh authentication session:', asErrorMessage(error));
|
|
217
|
+
clearAuthSession(state);
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function refreshJwtIfNeeded(state) {
|
|
222
|
+
if (!state || typeof state !== 'object')
|
|
223
|
+
throw new Error('JWT state is required.');
|
|
224
|
+
if (!jwtNeedsRefresh(state))
|
|
225
|
+
return state.token;
|
|
226
|
+
const session = readAuthSession();
|
|
227
|
+
if (!session) {
|
|
228
|
+
resetJwtState(state);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const key = refreshKey(state, session);
|
|
232
|
+
const existing = refreshRequests.get(key);
|
|
233
|
+
if (existing)
|
|
234
|
+
return existing;
|
|
235
|
+
const request = performRefresh(state, session).finally(() => {
|
|
236
|
+
refreshRequests.delete(key);
|
|
237
|
+
});
|
|
238
|
+
refreshRequests.set(key, request);
|
|
239
|
+
return request;
|
|
240
|
+
}
|
|
241
|
+
async function loginWithToken(token, options = {}) {
|
|
242
|
+
if (!token || typeof token !== 'string')
|
|
243
|
+
throw new Error('A token is required.');
|
|
244
|
+
const apiUrl = getApiUrl(options.apiUrl);
|
|
245
|
+
let user;
|
|
246
|
+
try {
|
|
247
|
+
const profile = await getJson(`${apiUrl}/api/auth/me`, token);
|
|
248
|
+
user = profile.data || profile.user;
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
throw new Error(`Token validation failed: ${asErrorMessage(error)}`);
|
|
252
|
+
}
|
|
253
|
+
const now = new Date().toISOString();
|
|
254
|
+
const session = { accessToken: token, user, apiUrl, createdAt: now, updatedAt: now };
|
|
255
|
+
writeAuthSession(session);
|
|
256
|
+
return session;
|
|
257
|
+
}
|
|
258
|
+
async function loginWithCredentials(email, password, options = {}) {
|
|
259
|
+
if (!email || !password)
|
|
260
|
+
throw new Error('Email and password are required.');
|
|
261
|
+
const apiUrl = getApiUrl(options.apiUrl);
|
|
262
|
+
const data = await postJson(`${apiUrl}/api/auth/login`, { email, password, client: 'vigthoria-cli' });
|
|
263
|
+
const session = normalizeSession(data, apiUrl);
|
|
264
|
+
writeAuthSession(session);
|
|
265
|
+
return session;
|
|
266
|
+
}
|
|
267
|
+
async function loginWithDeviceCode(options = {}) {
|
|
268
|
+
const apiUrl = getApiUrl(options.apiUrl);
|
|
269
|
+
const start = await postJson(`${apiUrl}/api/auth/device`, { client: 'vigthoria-cli' });
|
|
270
|
+
const verificationUri = start.verificationUri || start.verification_uri;
|
|
271
|
+
const userCode = start.userCode || start.user_code;
|
|
272
|
+
const deviceCode = start.deviceCode || start.device_code;
|
|
273
|
+
if (!verificationUri || !userCode || !deviceCode)
|
|
274
|
+
throw new Error('Device authorization endpoint returned an incomplete response.');
|
|
275
|
+
console.log(`Open this URL to authenticate: ${verificationUri}`);
|
|
276
|
+
console.log(`Enter code: ${userCode}`);
|
|
277
|
+
const timeoutAt = Date.now() + (options.pollTimeoutMs ?? 10 * 60 * 1000);
|
|
278
|
+
const intervalMs = Math.max(1000, (start.interval ?? 5) * 1000);
|
|
279
|
+
while (Date.now() < timeoutAt) {
|
|
280
|
+
await (0, promises_1.setTimeout)(intervalMs);
|
|
281
|
+
try {
|
|
282
|
+
const result = await postJson(`${apiUrl}/api/auth/device/complete`, { deviceCode, device_code: deviceCode, client: 'vigthoria-cli' });
|
|
283
|
+
if (result.accessToken || result.token) {
|
|
284
|
+
const session = normalizeSession(result, apiUrl);
|
|
285
|
+
writeAuthSession(session);
|
|
286
|
+
return session;
|
|
309
287
|
}
|
|
310
288
|
}
|
|
311
|
-
|
|
312
|
-
|
|
289
|
+
catch (error) {
|
|
290
|
+
const message = asErrorMessage(error);
|
|
291
|
+
if (!/pending|authorization_pending|slow_down/i.test(message))
|
|
292
|
+
throw new Error(message);
|
|
313
293
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
294
|
+
}
|
|
295
|
+
throw new Error('Timed out waiting for device authentication.');
|
|
296
|
+
}
|
|
297
|
+
async function getValidAuthSession() {
|
|
298
|
+
const session = readAuthSession();
|
|
299
|
+
if (!session)
|
|
300
|
+
return null;
|
|
301
|
+
if (!isExpired(session))
|
|
302
|
+
return session;
|
|
303
|
+
if (!session.refreshToken) {
|
|
304
|
+
clearAuthSession();
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
const state = {
|
|
308
|
+
token: session.accessToken,
|
|
309
|
+
expiresAt: session.expiresAt ?? null,
|
|
310
|
+
refreshToken: session.refreshToken,
|
|
311
|
+
apiUrl: session.apiUrl,
|
|
312
|
+
isExpired: () => true,
|
|
313
|
+
};
|
|
314
|
+
const token = await refreshJwtIfNeeded(state);
|
|
315
|
+
return token ? readAuthSession() : null;
|
|
316
|
+
}
|
|
317
|
+
function printSession(session) {
|
|
318
|
+
const user = session.user?.email || session.user?.name || session.user?.id || 'authenticated user';
|
|
319
|
+
console.log(`Authenticated as ${user}`);
|
|
320
|
+
console.log(`API: ${session.apiUrl}`);
|
|
321
|
+
if (session.expiresAt)
|
|
322
|
+
console.log(`Expires: ${new Date(session.expiresAt).toLocaleString()}`);
|
|
323
|
+
}
|
|
324
|
+
async function loginAction(options = {}) {
|
|
325
|
+
try {
|
|
326
|
+
const session = options.token
|
|
327
|
+
? await loginWithToken(options.token, options)
|
|
328
|
+
: options.email && options.password
|
|
329
|
+
? await loginWithCredentials(options.email, options.password, options)
|
|
330
|
+
: await loginWithDeviceCode(options);
|
|
331
|
+
console.log('Login successful.');
|
|
332
|
+
printSession(session);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
clearAuthSession();
|
|
336
|
+
throw new Error(asErrorMessage(error));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async function logoutAction(state) {
|
|
340
|
+
const session = readAuthSession();
|
|
341
|
+
if (session) {
|
|
342
|
+
try {
|
|
343
|
+
await postJson(`${session.apiUrl}/api/auth/logout`, { client: 'vigthoria-cli' }, session.accessToken);
|
|
333
344
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
// --- Display Auth Scopes ---
|
|
337
|
-
console.log();
|
|
338
|
-
console.log(chalk_1.default.white('Auth Scopes:'));
|
|
339
|
-
console.log(chalk_1.default.gray(' Model Auth: ') + (tokenValidation.valid ? chalk_1.default.green('Valid') : chalk_1.default.red('Invalid')) + chalk_1.default.gray(' (used by chat, agent, review, explain, generate, fix)'));
|
|
340
|
-
if (!tokenValidation.valid && tokenValidation.error) {
|
|
341
|
-
console.log(chalk_1.default.gray(' ') + chalk_1.default.red(tokenValidation.error));
|
|
345
|
+
catch (error) {
|
|
346
|
+
console.warn('Remote logout failed; local session will still be cleared:', asErrorMessage(error));
|
|
342
347
|
}
|
|
343
|
-
console.log(chalk_1.default.gray(' Repo Auth: ') + (capabilityStatus.repoMemory.ok ? chalk_1.default.green('Active') : chalk_1.default.gray('N/A — repo memory not deployed')) + chalk_1.default.gray(' (used by repo push/pull/list only)'));
|
|
344
|
-
console.log(chalk_1.default.gray(' Bridge Auth: ') + (capabilityStatus.devtoolsBridge.ok ? chalk_1.default.green('Connected') : chalk_1.default.gray('N/A — bridge not running')) + chalk_1.default.gray(' (used by --bridge flag only)'));
|
|
345
|
-
console.log();
|
|
346
|
-
// Graceful exit — destroy keep-alive agents so the event loop drains
|
|
347
|
-
// naturally. Setting exitCode (not calling process.exit()) avoids the
|
|
348
|
-
// libuv UV_HANDLE_CLOSING assertion on Windows that occurs when
|
|
349
|
-
// process.exit() is invoked while socket handles are mid-close.
|
|
350
|
-
this.api.destroy();
|
|
351
|
-
process.exitCode = 0;
|
|
352
348
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
console.log();
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
console.
|
|
364
|
-
|
|
349
|
+
if (!clearAuthSession(state))
|
|
350
|
+
process.exitCode = 1;
|
|
351
|
+
else
|
|
352
|
+
console.log('Logged out.');
|
|
353
|
+
}
|
|
354
|
+
async function handleLogin(config) {
|
|
355
|
+
try {
|
|
356
|
+
await loginAction(config || {});
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
console.error('Login failed:', asErrorMessage(error));
|
|
360
|
+
process.exitCode = 1;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function handleLogout(config) {
|
|
364
|
+
await logoutAction(config && typeof config === 'object' ? config : null);
|
|
365
|
+
}
|
|
366
|
+
async function statusAction() {
|
|
367
|
+
const session = await getValidAuthSession();
|
|
368
|
+
if (!session) {
|
|
369
|
+
console.log('Not authenticated. Run `vigthoria auth login` to sign in.');
|
|
370
|
+
process.exitCode = 1;
|
|
371
|
+
return;
|
|
365
372
|
}
|
|
373
|
+
printSession(session);
|
|
374
|
+
}
|
|
375
|
+
function createAuthCommand() {
|
|
376
|
+
const auth = new commander_1.Command('auth').description('Manage Vigthoria CLI authentication');
|
|
377
|
+
auth
|
|
378
|
+
.command('login')
|
|
379
|
+
.description('Sign in to Vigthoria')
|
|
380
|
+
.option('--token <token>', 'use an existing access token')
|
|
381
|
+
.option('--email <email>', 'account email for password login')
|
|
382
|
+
.option('--password <password>', 'account password for password login')
|
|
383
|
+
.option('--api-url <url>', 'Vigthoria API URL')
|
|
384
|
+
.option('--device', 'force browser/device-code login')
|
|
385
|
+
.action(async (options) => {
|
|
386
|
+
await handleLogin(options);
|
|
387
|
+
});
|
|
388
|
+
auth
|
|
389
|
+
.command('logout')
|
|
390
|
+
.description('Clear the saved Vigthoria session')
|
|
391
|
+
.action(async () => {
|
|
392
|
+
await handleLogout(null);
|
|
393
|
+
});
|
|
394
|
+
auth
|
|
395
|
+
.command('status')
|
|
396
|
+
.description('Show current authentication status')
|
|
397
|
+
.action(async () => {
|
|
398
|
+
await statusAction();
|
|
399
|
+
});
|
|
400
|
+
auth.action(() => auth.help());
|
|
401
|
+
return auth;
|
|
402
|
+
}
|
|
403
|
+
function registerAuthCommand(program) {
|
|
404
|
+
const command = createAuthCommand();
|
|
405
|
+
program.addCommand(command);
|
|
406
|
+
return command;
|
|
366
407
|
}
|
|
367
|
-
exports.
|
|
408
|
+
exports.authCommand = createAuthCommand();
|
|
409
|
+
exports.default = exports.authCommand;
|