overlord-cli 3.5.0 → 3.5.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 +1 -0
- package/bin/_cli/auth.mjs +181 -62
- package/bin/_cli/index.mjs +7 -0
- package/bin/_cli/version.mjs +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
package/bin/_cli/auth.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/* global console, fetch, process, setTimeout, URL, URLSearchParams */
|
|
2
3
|
|
|
3
4
|
import { execFileSync } from 'node:child_process';
|
|
4
5
|
import crypto from 'node:crypto';
|
|
@@ -8,6 +9,7 @@ import { buildAuthHeaders, clearCredentials, loadCredentials, loadRuntime, saveC
|
|
|
8
9
|
|
|
9
10
|
const DEFAULT_OVERLORD_URL = process.env.OVERLORD_URL ?? 'http://localhost:3000';
|
|
10
11
|
const DEFAULT_CLI_REDIRECT_URI = 'http://127.0.0.1:45619/callback';
|
|
12
|
+
const DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5;
|
|
11
13
|
|
|
12
14
|
// ---------------------------------------------------------------------------
|
|
13
15
|
// PKCE helpers
|
|
@@ -170,6 +172,45 @@ async function fetchAuthConfig(platformUrl, localSecret) {
|
|
|
170
172
|
};
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
async function requestDeviceAuthorization(platformUrl, localSecret) {
|
|
176
|
+
const res = await fetch(`${platformUrl}/api/auth/device/request`, {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: buildAuthHeaders('', localSecret)
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!res.ok) {
|
|
182
|
+
const text = await res.text();
|
|
183
|
+
throw new Error(`Device authorization request failed (${res.status}): ${snippet(text)}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return readJsonOrThrow(res, 'Device authorization request', platformUrl);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function pollDeviceAuthorization(platformUrl, deviceCode, localSecret) {
|
|
190
|
+
const res = await fetch(`${platformUrl}/api/auth/device/poll`, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
headers: {
|
|
193
|
+
...buildAuthHeaders('', localSecret),
|
|
194
|
+
'Content-Type': 'application/json'
|
|
195
|
+
},
|
|
196
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const body = await readJsonOrThrow(res, 'Device authorization poll', platformUrl);
|
|
200
|
+
|
|
201
|
+
if (res.ok) return body;
|
|
202
|
+
|
|
203
|
+
if (res.status === 400 || res.status === 404 || res.status === 429) {
|
|
204
|
+
return body;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
throw new Error(`Device authorization poll failed (${res.status}): ${snippet(JSON.stringify(body))}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function sleep(ms) {
|
|
211
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
212
|
+
}
|
|
213
|
+
|
|
173
214
|
async function exchangeCodeForSupabaseTokens(supabaseUrl, clientId, code, codeVerifier, redirectUri) {
|
|
174
215
|
const res = await fetch(`${supabaseUrl}/auth/v1/oauth/token`, {
|
|
175
216
|
method: 'POST',
|
|
@@ -219,35 +260,98 @@ function openBrowser(url) {
|
|
|
219
260
|
}
|
|
220
261
|
}
|
|
221
262
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
263
|
+
function printDeviceAuthorizationInstructions(verificationUri, userCode, logger = console) {
|
|
264
|
+
logger.log(' Verification URL:', verificationUri);
|
|
265
|
+
logger.log(' Authorization code:', userCode);
|
|
266
|
+
logger.log('\nOpen the verification URL in any browser to approve this CLI login.\n');
|
|
267
|
+
}
|
|
225
268
|
|
|
226
|
-
export async function
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
269
|
+
export async function authLoginViaDeviceFlow(
|
|
270
|
+
platformUrl,
|
|
271
|
+
localSecret,
|
|
272
|
+
{
|
|
273
|
+
browserOpener = openBrowser,
|
|
274
|
+
logger = console,
|
|
275
|
+
sleepFn = sleep,
|
|
276
|
+
stdout = process.stdout
|
|
277
|
+
} = {}
|
|
278
|
+
) {
|
|
279
|
+
const deviceAuth = await requestDeviceAuthorization(platformUrl, localSecret);
|
|
280
|
+
const verificationUri = String(deviceAuth.verification_uri ?? '').trim();
|
|
281
|
+
const userCode = String(deviceAuth.user_code ?? '').trim();
|
|
282
|
+
const deviceCode = String(deviceAuth.device_code ?? '').trim();
|
|
283
|
+
|
|
284
|
+
if (!verificationUri || !userCode || !deviceCode) {
|
|
285
|
+
throw new Error('Device authorization response was missing required fields.');
|
|
286
|
+
}
|
|
230
287
|
|
|
231
|
-
|
|
288
|
+
const initialPollIntervalSeconds = Number(deviceAuth.interval);
|
|
289
|
+
let pollIntervalSeconds =
|
|
290
|
+
Number.isFinite(initialPollIntervalSeconds) && initialPollIntervalSeconds > 0
|
|
291
|
+
? initialPollIntervalSeconds
|
|
292
|
+
: DEFAULT_DEVICE_POLL_INTERVAL_SECONDS;
|
|
293
|
+
|
|
294
|
+
printDeviceAuthorizationInstructions(verificationUri, userCode, logger);
|
|
295
|
+
logger.log('Opening browser...\n');
|
|
296
|
+
browserOpener(verificationUri);
|
|
297
|
+
|
|
298
|
+
stdout.write('Waiting for browser authorization');
|
|
232
299
|
|
|
300
|
+
for (;;) {
|
|
301
|
+
await sleepFn(pollIntervalSeconds * 1000);
|
|
302
|
+
|
|
303
|
+
const result = await pollDeviceAuthorization(platformUrl, deviceCode, localSecret);
|
|
304
|
+
const status = String(result?.status ?? '');
|
|
305
|
+
|
|
306
|
+
if (status === 'pending') {
|
|
307
|
+
stdout.write('.');
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (status === 'slow_down') {
|
|
312
|
+
stdout.write('.');
|
|
313
|
+
const nextInterval = Number(result?.interval);
|
|
314
|
+
if (Number.isFinite(nextInterval) && nextInterval > 0) {
|
|
315
|
+
pollIntervalSeconds = nextInterval;
|
|
316
|
+
} else {
|
|
317
|
+
pollIntervalSeconds += 1;
|
|
318
|
+
}
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (status === 'authorized') {
|
|
323
|
+
logger.log('\n');
|
|
324
|
+
return {
|
|
325
|
+
access_token: result.access_token,
|
|
326
|
+
platform_url: result.platform_url ?? platformUrl
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (status === 'expired') {
|
|
331
|
+
throw new Error('Authorization request expired. Please run `ovld auth login` again.');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (result?.error) {
|
|
335
|
+
throw new Error(String(result.error));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
throw new Error(`Unexpected device authorization status: ${status || 'unknown'}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export async function authLoginViaOAuthLoopback(platformUrl, localSecret) {
|
|
233
343
|
// 1. Discover OAuth config from the platform
|
|
234
344
|
let supabaseUrl, cliClientId, cliRedirectUri, resolvedPlatformUrl;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
resolvedPlatformUrl = config.platform_url ?? platformUrl;
|
|
241
|
-
} catch (err) {
|
|
242
|
-
console.error(`\nError: ${err.message}`);
|
|
243
|
-
process.exit(1);
|
|
244
|
-
}
|
|
345
|
+
const config = await fetchAuthConfig(platformUrl, localSecret);
|
|
346
|
+
supabaseUrl = config.supabase_url;
|
|
347
|
+
cliClientId = config.cli_client_id;
|
|
348
|
+
cliRedirectUri = config.cli_redirect_uri;
|
|
349
|
+
resolvedPlatformUrl = config.platform_url ?? platformUrl;
|
|
245
350
|
|
|
246
351
|
if (!supabaseUrl || !cliClientId) {
|
|
247
|
-
|
|
248
|
-
'
|
|
352
|
+
throw new Error(
|
|
353
|
+
'OAuth is not configured for CLI login. Set SUPABASE_OAUTH_CLI_CLIENT_ID on the Overlord server.'
|
|
249
354
|
);
|
|
250
|
-
process.exit(1);
|
|
251
355
|
}
|
|
252
356
|
|
|
253
357
|
// 2. PKCE parameters + state
|
|
@@ -256,14 +360,7 @@ export async function authLogin() {
|
|
|
256
360
|
const state = generateState();
|
|
257
361
|
|
|
258
362
|
// 3. Use exact loopback redirect URI (Supabase does not support wildcard callback URLs)
|
|
259
|
-
|
|
260
|
-
try {
|
|
261
|
-
redirectTarget = parseLoopbackRedirectUri(cliRedirectUri ?? DEFAULT_CLI_REDIRECT_URI);
|
|
262
|
-
} catch (err) {
|
|
263
|
-
console.error(`\nError: ${err.message}`);
|
|
264
|
-
process.exit(1);
|
|
265
|
-
}
|
|
266
|
-
|
|
363
|
+
const redirectTarget = parseLoopbackRedirectUri(cliRedirectUri ?? DEFAULT_CLI_REDIRECT_URI);
|
|
267
364
|
const { host, port, callbackPath, redirectUri } = redirectTarget;
|
|
268
365
|
|
|
269
366
|
// 4. Build the Supabase OAuth authorization URL
|
|
@@ -285,47 +382,69 @@ export async function authLogin() {
|
|
|
285
382
|
|
|
286
383
|
// 6. Wait for the auth code
|
|
287
384
|
let authCode;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
console.log('\n');
|
|
292
|
-
} catch (err) {
|
|
293
|
-
console.error(`\n\nAuthorization failed: ${err.message}`);
|
|
294
|
-
process.exit(1);
|
|
295
|
-
}
|
|
385
|
+
process.stdout.write('Waiting for browser authorization');
|
|
386
|
+
authCode = await callbackPromise;
|
|
387
|
+
console.log('\n');
|
|
296
388
|
|
|
297
389
|
// 7. Exchange auth code → Supabase tokens
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
redirectUri
|
|
306
|
-
);
|
|
307
|
-
} catch (err) {
|
|
308
|
-
console.error(`\nError exchanging code for tokens: ${err.message}`);
|
|
309
|
-
process.exit(1);
|
|
310
|
-
}
|
|
390
|
+
const supabaseTokens = await exchangeCodeForSupabaseTokens(
|
|
391
|
+
supabaseUrl,
|
|
392
|
+
cliClientId,
|
|
393
|
+
authCode,
|
|
394
|
+
codeVerifier,
|
|
395
|
+
redirectUri
|
|
396
|
+
);
|
|
311
397
|
|
|
312
398
|
// 8. Exchange Supabase access token → Overlord agent_token
|
|
313
|
-
|
|
399
|
+
const agentTokenData = await exchangeForAgentToken(
|
|
400
|
+
resolvedPlatformUrl,
|
|
401
|
+
supabaseTokens.access_token,
|
|
402
|
+
localSecret
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
access_token: agentTokenData.access_token,
|
|
407
|
+
platform_url: agentTokenData.platform_url ?? resolvedPlatformUrl
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
// Public auth commands
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
export async function authLogin() {
|
|
416
|
+
const runtime = loadRuntime();
|
|
417
|
+
const platformUrl = process.env.OVERLORD_URL ?? runtime?.platform_url ?? DEFAULT_OVERLORD_URL;
|
|
418
|
+
const localSecret = runtime?.local_secret ?? process.env.OVERLORD_LOCAL_SECRET ?? '';
|
|
419
|
+
|
|
420
|
+
console.log('Starting Overlord CLI authorization...\n');
|
|
421
|
+
let credentials;
|
|
314
422
|
try {
|
|
315
|
-
|
|
316
|
-
resolvedPlatformUrl,
|
|
317
|
-
supabaseTokens.access_token,
|
|
318
|
-
localSecret
|
|
319
|
-
);
|
|
423
|
+
credentials = await authLoginViaDeviceFlow(platformUrl, localSecret);
|
|
320
424
|
} catch (err) {
|
|
321
|
-
|
|
322
|
-
|
|
425
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
426
|
+
const canFallbackToLoopback =
|
|
427
|
+
message.includes('Device authorization request failed (404)') ||
|
|
428
|
+
message.includes('Device authorization request failed (405)');
|
|
429
|
+
|
|
430
|
+
if (!canFallbackToLoopback) {
|
|
431
|
+
console.error(`\nAuthorization failed: ${message}`);
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
console.log('Device authorization is unavailable on this server. Falling back to loopback OAuth.\n');
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
credentials = await authLoginViaOAuthLoopback(platformUrl, localSecret);
|
|
439
|
+
} catch (fallbackErr) {
|
|
440
|
+
console.error(`\nAuthorization failed: ${fallbackErr.message}`);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
323
443
|
}
|
|
324
444
|
|
|
325
|
-
// 9. Persist credentials (same format — backward-compatible)
|
|
326
445
|
saveCredentials({
|
|
327
|
-
access_token:
|
|
328
|
-
platform_url:
|
|
446
|
+
access_token: credentials.access_token,
|
|
447
|
+
platform_url: credentials.platform_url ?? platformUrl
|
|
329
448
|
});
|
|
330
449
|
|
|
331
450
|
console.log('Logged in successfully!');
|
|
@@ -354,7 +473,7 @@ export async function runAuthCommand(subcommand) {
|
|
|
354
473
|
console.log(`ovld auth <subcommand>
|
|
355
474
|
|
|
356
475
|
Subcommands:
|
|
357
|
-
login Authorize the CLI via browser (
|
|
476
|
+
login Authorize the CLI via browser (works locally or over SSH)
|
|
358
477
|
status Show current login status
|
|
359
478
|
logout Remove stored credentials
|
|
360
479
|
`);
|
package/bin/_cli/index.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { runProtocolCommand } from './protocol.mjs';
|
|
|
8
8
|
import { runDoctorCommand, runSetupCommand } from './setup.mjs';
|
|
9
9
|
import { runTicketCommand } from './ticket.mjs';
|
|
10
10
|
import { runTicketsCommand } from './tickets.mjs';
|
|
11
|
+
import { runVersionCommand } from './version.mjs';
|
|
11
12
|
|
|
12
13
|
function printHelp(primaryCommand) {
|
|
13
14
|
console.log(`Overlord CLI
|
|
@@ -27,6 +28,7 @@ Usage:
|
|
|
27
28
|
${primaryCommand} context Print ticket context (requires TICKET_ID)
|
|
28
29
|
${primaryCommand} setup <agent|all> Install Overlord agent connector
|
|
29
30
|
${primaryCommand} doctor Validate installed agent connectors
|
|
31
|
+
${primaryCommand} version Show the installed CLI version
|
|
30
32
|
${primaryCommand} help Show this help message
|
|
31
33
|
|
|
32
34
|
Agents:
|
|
@@ -108,6 +110,11 @@ export async function runCli({ primaryCommand }) {
|
|
|
108
110
|
return;
|
|
109
111
|
}
|
|
110
112
|
|
|
113
|
+
if (command === 'version') {
|
|
114
|
+
runVersionCommand();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
111
118
|
// Launcher commands (`run` / `resume` kept as legacy aliases)
|
|
112
119
|
if (
|
|
113
120
|
command === 'connect' ||
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const { version } = require('../../package.json');
|
|
7
|
+
|
|
8
|
+
export function runVersionCommand() {
|
|
9
|
+
console.log(`Overlord CLI ${version}`);
|
|
10
|
+
}
|