@bjesuiter/codex-switcher 1.7.4 → 1.8.1
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 +9 -7
- package/cdx.mjs +602 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,12 +6,12 @@ Switch the coding-agents [pi](https://pi.dev/), [codex](https://developers.opena
|
|
|
6
6
|
|
|
7
7
|
## Latest Changes
|
|
8
8
|
|
|
9
|
-
### 1.
|
|
9
|
+
### 1.8.1
|
|
10
10
|
|
|
11
11
|
#### Fixes
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
13
|
+
- Added platform-native secure-store write/read/delete probes to `cdx doctor` on Linux, macOS, and Windows, so runtime secure-store failures are detected directly instead of only reporting adapter capability.
|
|
14
|
+
- Linux secure-store error handling now treats Secret Service `no result found` responses as missing-entry cases and classifies generic `Couldn't access platform secure storage` failures as unavailable-store errors with actionable guidance.
|
|
15
15
|
|
|
16
16
|
see full changelog here: https://github.com/bjesuiter/codex-switcher/blob/main/CHANGELOG.md
|
|
17
17
|
|
|
@@ -153,12 +153,12 @@ cdx migrate-secrets
|
|
|
153
153
|
| Command | Description |
|
|
154
154
|
|---------|-------------|
|
|
155
155
|
| `cdx` | Interactive mode |
|
|
156
|
-
| `cdx login` | Add a new OpenAI account via OAuth |
|
|
157
|
-
| `cdx login --device-flow` | Add account using OAuth device flow (
|
|
156
|
+
| `cdx login` | Add a new OpenAI account via OAuth (recommended default flow; supports manual URL copy/paste callback completion with optional clipboard copy assist) |
|
|
157
|
+
| `cdx login --device-flow` | Add account using OAuth device flow (may fail on some VPS/server IPs due to Cloudflare challenges) |
|
|
158
158
|
| `cdx relogin` | Re-authenticate an existing account via OAuth |
|
|
159
|
-
| `cdx relogin --device-flow` | Re-authenticate interactively using OAuth device flow |
|
|
159
|
+
| `cdx relogin --device-flow` | Re-authenticate interactively using OAuth device flow (may fail on some VPS/server IPs due to Cloudflare challenges) |
|
|
160
160
|
| `cdx relogin <account>` | Re-authenticate a specific account by ID or label |
|
|
161
|
-
| `cdx relogin <account> --device-flow` | Re-authenticate specific account using OAuth device flow |
|
|
161
|
+
| `cdx relogin <account> --device-flow` | Re-authenticate specific account using OAuth device flow (may fail on some VPS/server IPs due to Cloudflare challenges) |
|
|
162
162
|
| `cdx switch` | Switch account (interactive picker) |
|
|
163
163
|
| `cdx switch --next` | Cycle to next account |
|
|
164
164
|
| `cdx switch <id>` | Switch to specific account |
|
|
@@ -177,6 +177,8 @@ cdx migrate-secrets
|
|
|
177
177
|
| `cdx --version` | Show version |
|
|
178
178
|
| `cdx --secret-store legacy-keychain <command>` | Override configured backend for this run (macOS legacy keychain) |
|
|
179
179
|
|
|
180
|
+
> Tip: On SSH/VPS, prefer `cdx login` (without `--device-flow`) and complete login via manual callback URL/code copy-paste. The manual flow can offer clipboard copy assist (including OSC52 on compatible terminals) to avoid selecting long URLs. In Mosh sessions, OSC52 clipboard updates may be unreliable; if needed, use the printed fallback copy command or manual copy. Device flow can be blocked by Cloudflare challenge pages.
|
|
181
|
+
|
|
180
182
|
### Shell completion
|
|
181
183
|
|
|
182
184
|
Generate and source completion scripts:
|
package/cdx.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import * as p from "@clack/prompts";
|
|
9
|
-
import { spawn } from "node:child_process";
|
|
9
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
10
10
|
import { deletePassword, getPassword, listBackends, setPassword, useBackend } from "@bjesuiter/cross-keychain";
|
|
11
11
|
import { createInterface } from "node:readline/promises";
|
|
12
12
|
import { randomBytes } from "node:crypto";
|
|
@@ -15,7 +15,7 @@ import { generatePKCE } from "@openauthjs/openauth/pkce";
|
|
|
15
15
|
import http from "node:http";
|
|
16
16
|
|
|
17
17
|
//#region package.json
|
|
18
|
-
var version = "1.
|
|
18
|
+
var version = "1.8.1";
|
|
19
19
|
|
|
20
20
|
//#endregion
|
|
21
21
|
//#region lib/platform/path-resolver.ts
|
|
@@ -256,7 +256,7 @@ const getBrowserLauncher = (platform = process.platform, url) => {
|
|
|
256
256
|
label: "xdg-open"
|
|
257
257
|
};
|
|
258
258
|
};
|
|
259
|
-
const isCommandAvailable = (command, platform = process.platform) => {
|
|
259
|
+
const isCommandAvailable$1 = (command, platform = process.platform) => {
|
|
260
260
|
const probe = platform === "win32" ? "where" : "which";
|
|
261
261
|
return Bun.spawnSync({
|
|
262
262
|
cmd: [probe, command],
|
|
@@ -269,13 +269,13 @@ const getBrowserLauncherCapability = (platform = process.platform) => {
|
|
|
269
269
|
return {
|
|
270
270
|
command: launcher.command,
|
|
271
271
|
label: launcher.label,
|
|
272
|
-
available: isCommandAvailable(launcher.command, platform)
|
|
272
|
+
available: isCommandAvailable$1(launcher.command, platform)
|
|
273
273
|
};
|
|
274
274
|
};
|
|
275
275
|
const openBrowserUrl = (url, options = {}) => {
|
|
276
276
|
const platform = options.platform ?? process.platform;
|
|
277
277
|
const spawnImpl = options.spawnImpl ?? spawn;
|
|
278
|
-
const commandAvailable = options.isCommandAvailableImpl ?? isCommandAvailable;
|
|
278
|
+
const commandAvailable = options.isCommandAvailableImpl ?? isCommandAvailable$1;
|
|
279
279
|
const launcher = getBrowserLauncher(platform, url);
|
|
280
280
|
if (!commandAvailable(launcher.command, platform)) return {
|
|
281
281
|
ok: false,
|
|
@@ -304,6 +304,192 @@ const openBrowserUrl = (url, options = {}) => {
|
|
|
304
304
|
}
|
|
305
305
|
};
|
|
306
306
|
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region lib/platform/clipboard.ts
|
|
309
|
+
const isCommandAvailable = (command, platform = process.platform) => {
|
|
310
|
+
const probe = platform === "win32" ? "where" : "which";
|
|
311
|
+
return Bun.spawnSync({
|
|
312
|
+
cmd: [probe, command],
|
|
313
|
+
stdout: "pipe",
|
|
314
|
+
stderr: "pipe"
|
|
315
|
+
}).exitCode === 0;
|
|
316
|
+
};
|
|
317
|
+
const isLikelyMoshSession = (env = process.env) => Boolean(env.MOSH_IP || env.MOSH_KEY || env.MOSH_PREDICTION_DISPLAY);
|
|
318
|
+
const isLikelyRemoteSession = (env = process.env) => {
|
|
319
|
+
if (env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY) return true;
|
|
320
|
+
return isLikelyMoshSession(env);
|
|
321
|
+
};
|
|
322
|
+
const hasDisplayServer = (env) => Boolean(env.WAYLAND_DISPLAY || env.DISPLAY);
|
|
323
|
+
const supportsOsc52 = (isTTY, env) => {
|
|
324
|
+
if (!isTTY) return false;
|
|
325
|
+
if ((env.TERM ?? "").toLowerCase() === "dumb") return false;
|
|
326
|
+
return true;
|
|
327
|
+
};
|
|
328
|
+
const getLocalClipboardTargets = (platform, env, commandExists) => {
|
|
329
|
+
if (platform === "darwin") return commandExists("pbcopy", platform) ? [{
|
|
330
|
+
kind: "command",
|
|
331
|
+
method: "pbcopy",
|
|
332
|
+
command: "pbcopy",
|
|
333
|
+
args: []
|
|
334
|
+
}] : [];
|
|
335
|
+
if (platform === "win32") {
|
|
336
|
+
const targets = [];
|
|
337
|
+
if (commandExists("clip", platform)) targets.push({
|
|
338
|
+
kind: "command",
|
|
339
|
+
method: "clip",
|
|
340
|
+
command: "cmd",
|
|
341
|
+
args: ["/c", "clip"]
|
|
342
|
+
});
|
|
343
|
+
if (commandExists("powershell", platform)) targets.push({
|
|
344
|
+
kind: "command",
|
|
345
|
+
method: "powershell",
|
|
346
|
+
command: "powershell",
|
|
347
|
+
args: [
|
|
348
|
+
"-NoProfile",
|
|
349
|
+
"-Command",
|
|
350
|
+
"$v=[Console]::In.ReadToEnd(); Set-Clipboard -Value $v"
|
|
351
|
+
]
|
|
352
|
+
});
|
|
353
|
+
if (commandExists("pwsh", platform)) targets.push({
|
|
354
|
+
kind: "command",
|
|
355
|
+
method: "powershell",
|
|
356
|
+
command: "pwsh",
|
|
357
|
+
args: [
|
|
358
|
+
"-NoProfile",
|
|
359
|
+
"-Command",
|
|
360
|
+
"$v=[Console]::In.ReadToEnd(); Set-Clipboard -Value $v"
|
|
361
|
+
]
|
|
362
|
+
});
|
|
363
|
+
return targets;
|
|
364
|
+
}
|
|
365
|
+
const targets = [];
|
|
366
|
+
const wayland = Boolean(env.WAYLAND_DISPLAY);
|
|
367
|
+
const x11 = Boolean(env.DISPLAY);
|
|
368
|
+
if (isLikelyRemoteSession(env) && !hasDisplayServer(env)) return targets;
|
|
369
|
+
if (wayland && commandExists("wl-copy", platform)) targets.push({
|
|
370
|
+
kind: "command",
|
|
371
|
+
method: "wl-copy",
|
|
372
|
+
command: "wl-copy",
|
|
373
|
+
args: []
|
|
374
|
+
});
|
|
375
|
+
if ((x11 || !wayland) && commandExists("xclip", platform)) targets.push({
|
|
376
|
+
kind: "command",
|
|
377
|
+
method: "xclip",
|
|
378
|
+
command: "xclip",
|
|
379
|
+
args: ["-selection", "clipboard"]
|
|
380
|
+
});
|
|
381
|
+
if ((x11 || !wayland) && commandExists("xsel", platform)) targets.push({
|
|
382
|
+
kind: "command",
|
|
383
|
+
method: "xsel",
|
|
384
|
+
command: "xsel",
|
|
385
|
+
args: ["--clipboard", "--input"]
|
|
386
|
+
});
|
|
387
|
+
if (!wayland && commandExists("wl-copy", platform)) targets.push({
|
|
388
|
+
kind: "command",
|
|
389
|
+
method: "wl-copy",
|
|
390
|
+
command: "wl-copy",
|
|
391
|
+
args: []
|
|
392
|
+
});
|
|
393
|
+
return targets;
|
|
394
|
+
};
|
|
395
|
+
const resolveClipboardTargets = (context = {}, commandExists = isCommandAvailable) => {
|
|
396
|
+
const platform = context.platform ?? process.platform;
|
|
397
|
+
const env = context.env ?? process.env;
|
|
398
|
+
const isTTY = context.isTTY ?? (Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY));
|
|
399
|
+
const localTargets = getLocalClipboardTargets(platform, env, commandExists);
|
|
400
|
+
const remote = isLikelyRemoteSession(env);
|
|
401
|
+
const allowOsc52 = supportsOsc52(isTTY, env);
|
|
402
|
+
const result = [];
|
|
403
|
+
if (remote && allowOsc52) result.push({
|
|
404
|
+
kind: "osc52",
|
|
405
|
+
method: "osc52"
|
|
406
|
+
});
|
|
407
|
+
result.push(...localTargets);
|
|
408
|
+
if (!remote && allowOsc52) result.push({
|
|
409
|
+
kind: "osc52",
|
|
410
|
+
method: "osc52"
|
|
411
|
+
});
|
|
412
|
+
return result;
|
|
413
|
+
};
|
|
414
|
+
const buildOsc52Sequence = (text, env = process.env) => {
|
|
415
|
+
const osc = `\u001b]52;c;${Buffer.from(text, "utf8").toString("base64")}\u0007`;
|
|
416
|
+
if (env.TMUX) return `\u001bPtmux;\u001b${osc}\u001b\\`;
|
|
417
|
+
const term = (env.TERM ?? "").toLowerCase();
|
|
418
|
+
if (env.STY || term.startsWith("screen")) return `\u001bP${osc}\u001b\\`;
|
|
419
|
+
return osc;
|
|
420
|
+
};
|
|
421
|
+
const defaultRunCommand = (command, args, input) => {
|
|
422
|
+
const result = spawnSync(command, args, {
|
|
423
|
+
input,
|
|
424
|
+
encoding: "utf8",
|
|
425
|
+
windowsHide: true
|
|
426
|
+
});
|
|
427
|
+
if (result.status === 0) return { ok: true };
|
|
428
|
+
const stderr = typeof result.stderr === "string" ? result.stderr.trim() : "";
|
|
429
|
+
const stdout = typeof result.stdout === "string" ? result.stdout.trim() : "";
|
|
430
|
+
return {
|
|
431
|
+
ok: false,
|
|
432
|
+
error: stderr || stdout || `exit status ${result.status ?? "unknown"}`
|
|
433
|
+
};
|
|
434
|
+
};
|
|
435
|
+
const tryCopyToClipboard = (text, options = {}) => {
|
|
436
|
+
const commandExists = options.commandExistsImpl ?? isCommandAvailable;
|
|
437
|
+
const runCommand = options.runCommandImpl ?? defaultRunCommand;
|
|
438
|
+
const writeStdout = options.writeStdoutImpl ?? ((chunk) => {
|
|
439
|
+
process.stdout.write(chunk);
|
|
440
|
+
});
|
|
441
|
+
const targets = resolveClipboardTargets(options, commandExists);
|
|
442
|
+
if (targets.length === 0) return {
|
|
443
|
+
ok: false,
|
|
444
|
+
method: "none",
|
|
445
|
+
error: "No clipboard method available in this environment"
|
|
446
|
+
};
|
|
447
|
+
const errors = [];
|
|
448
|
+
for (const target of targets) {
|
|
449
|
+
if (target.kind === "osc52") {
|
|
450
|
+
const env = options.env ?? process.env;
|
|
451
|
+
try {
|
|
452
|
+
writeStdout(buildOsc52Sequence(text, env));
|
|
453
|
+
if (isLikelyMoshSession(env)) return {
|
|
454
|
+
ok: true,
|
|
455
|
+
method: "osc52",
|
|
456
|
+
warning: "Mosh session detected: OSC52 clipboard updates may not work reliably. If clipboard did not update, copy the URL manually from above."
|
|
457
|
+
};
|
|
458
|
+
return {
|
|
459
|
+
ok: true,
|
|
460
|
+
method: "osc52"
|
|
461
|
+
};
|
|
462
|
+
} catch (error) {
|
|
463
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
464
|
+
errors.push(`osc52: ${message}`);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const commandResult = runCommand(target.command, target.args, text);
|
|
469
|
+
if (commandResult.ok) return {
|
|
470
|
+
ok: true,
|
|
471
|
+
method: target.method
|
|
472
|
+
};
|
|
473
|
+
errors.push(`${target.method}: ${commandResult.error ?? "unknown error"}`);
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
ok: false,
|
|
477
|
+
method: "none",
|
|
478
|
+
error: errors.join("; ")
|
|
479
|
+
};
|
|
480
|
+
};
|
|
481
|
+
const escapePosixSingleQuoted = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
482
|
+
const buildClipboardHelperCommand = (text, context = {}, commandExists = isCommandAvailable) => {
|
|
483
|
+
const commandTarget = resolveClipboardTargets(context, commandExists).find((target) => target.kind === "command");
|
|
484
|
+
if (!commandTarget) return null;
|
|
485
|
+
if (commandTarget.method === "powershell") {
|
|
486
|
+
const escaped = text.replace(/'/g, "''");
|
|
487
|
+
return `${commandTarget.command} -NoProfile -Command "Set-Clipboard -Value '${escaped}'"`;
|
|
488
|
+
}
|
|
489
|
+
if (commandTarget.method === "clip") return `printf '%s' ${escapePosixSingleQuoted(text)} | cmd /c clip`;
|
|
490
|
+
return `printf '%s' ${escapePosixSingleQuoted(text)} | ${commandTarget.command} ${commandTarget.args.join(" ")}`.trim();
|
|
491
|
+
};
|
|
492
|
+
|
|
307
493
|
//#endregion
|
|
308
494
|
//#region lib/keychain.ts
|
|
309
495
|
const SERVICE_PREFIX$3 = "cdx-openai-";
|
|
@@ -473,8 +659,38 @@ const ensureFallbackConsent = async (scope, warningMessage) => {
|
|
|
473
659
|
//#region lib/secrets/linux-cross-keychain.ts
|
|
474
660
|
const SERVICE_PREFIX$2 = "cdx-openai-";
|
|
475
661
|
const LINUX_FALLBACK_SCOPE = "linux:cross-keychain:secret-service";
|
|
662
|
+
const MISSING_ENTRY_MARKERS = [
|
|
663
|
+
"no matching entry found in secure storage",
|
|
664
|
+
"password not found",
|
|
665
|
+
"no stored credentials found",
|
|
666
|
+
"credential not found",
|
|
667
|
+
"no result found"
|
|
668
|
+
];
|
|
669
|
+
const STORE_UNAVAILABLE_MARKERS = [
|
|
670
|
+
"unable to initialize linux secure-store backend",
|
|
671
|
+
"no keyring backend could be initialized",
|
|
672
|
+
"native keyring module not available",
|
|
673
|
+
"secret service operation failed",
|
|
674
|
+
"couldn't access platform secure storage",
|
|
675
|
+
"dbus",
|
|
676
|
+
"d-bus",
|
|
677
|
+
"org.freedesktop.secrets",
|
|
678
|
+
"service unavailable"
|
|
679
|
+
];
|
|
476
680
|
let backendInitPromise$2 = null;
|
|
477
681
|
let selectedBackend$2 = null;
|
|
682
|
+
const getErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
683
|
+
const classifyLinuxSecureStoreError = (error) => {
|
|
684
|
+
const message = getErrorMessage(error).toLowerCase();
|
|
685
|
+
if (MISSING_ENTRY_MARKERS.some((marker) => message.includes(marker))) return "missing_entry";
|
|
686
|
+
if (STORE_UNAVAILABLE_MARKERS.some((marker) => message.includes(marker))) return "store_unavailable";
|
|
687
|
+
return "other";
|
|
688
|
+
};
|
|
689
|
+
const createLinuxSecureStoreUnavailableError = (details) => {
|
|
690
|
+
const guidance = "Linux secure store is unavailable. Ensure Secret Service is installed/running (for example gnome-keyring with secret-tool), then retry login.";
|
|
691
|
+
if (!details) return new Error(guidance);
|
|
692
|
+
return /* @__PURE__ */ new Error(`${guidance} Technical details: ${details}`);
|
|
693
|
+
};
|
|
478
694
|
const tryUseBackend$2 = async (backendId) => {
|
|
479
695
|
try {
|
|
480
696
|
await useBackend(backendId, getCrossKeychainBackendOverrides());
|
|
@@ -492,6 +708,20 @@ const selectBackend$2 = async () => {
|
|
|
492
708
|
if (await tryUseBackend$2("secret-service")) return "secret-service";
|
|
493
709
|
throw new Error("Unable to initialize Linux secure-store backend via cross-keychain.");
|
|
494
710
|
};
|
|
711
|
+
const setActiveBackend = (backendId) => {
|
|
712
|
+
selectedBackend$2 = backendId;
|
|
713
|
+
backendInitPromise$2 = Promise.resolve();
|
|
714
|
+
};
|
|
715
|
+
const trySwitchBackend = async (backendId, options = {}) => {
|
|
716
|
+
if (options.forWrite && backendId === "secret-service") await ensureFallbackConsent(LINUX_FALLBACK_SCOPE, "⚠ Security warning: only the cross-keychain Linux fallback backend is available.\nThis path relies on shell-based `secret-tool` operations for Secret Service access.\nCompared to native bindings, secrets may be more exposed to process inspection/logging while helper commands run.");
|
|
717
|
+
try {
|
|
718
|
+
await useBackend(backendId, getCrossKeychainBackendOverrides());
|
|
719
|
+
setActiveBackend(backendId);
|
|
720
|
+
return true;
|
|
721
|
+
} catch {
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
};
|
|
495
725
|
const ensureLinuxBackend = async (options = {}) => {
|
|
496
726
|
if (!backendInitPromise$2) backendInitPromise$2 = (async () => {
|
|
497
727
|
selectedBackend$2 = await selectBackend$2();
|
|
@@ -517,17 +747,75 @@ const parsePayload$2 = (accountId, raw) => {
|
|
|
517
747
|
return parsed;
|
|
518
748
|
};
|
|
519
749
|
const withService$1 = async (accountId, run, options = {}) => {
|
|
520
|
-
|
|
521
|
-
|
|
750
|
+
try {
|
|
751
|
+
await ensureLinuxBackend(options);
|
|
752
|
+
} catch (error) {
|
|
753
|
+
throw createLinuxSecureStoreUnavailableError(getErrorMessage(error));
|
|
754
|
+
}
|
|
755
|
+
try {
|
|
756
|
+
return await run(getLinuxCrossKeychainService(accountId));
|
|
757
|
+
} catch (error) {
|
|
758
|
+
if (classifyLinuxSecureStoreError(error) === "store_unavailable") throw createLinuxSecureStoreUnavailableError(getErrorMessage(error));
|
|
759
|
+
throw error;
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
const trySaveWithSecretServiceFallback = async (accountId, serializedPayload) => {
|
|
763
|
+
if (!await trySwitchBackend("secret-service", { forWrite: true })) return {
|
|
764
|
+
ok: false,
|
|
765
|
+
error: /* @__PURE__ */ new Error("Unable to switch Linux secure-store backend to secret-service fallback.")
|
|
766
|
+
};
|
|
767
|
+
try {
|
|
768
|
+
await setPassword(getLinuxCrossKeychainService(accountId), accountId, serializedPayload);
|
|
769
|
+
return { ok: true };
|
|
770
|
+
} catch (error) {
|
|
771
|
+
return {
|
|
772
|
+
ok: false,
|
|
773
|
+
error
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
const saveLinuxCrossKeychainPayload = async (accountId, payload) => {
|
|
778
|
+
const serialized = JSON.stringify(payload);
|
|
779
|
+
try {
|
|
780
|
+
await withService$1(accountId, (service) => setPassword(service, accountId, serialized), { forWrite: true });
|
|
781
|
+
return;
|
|
782
|
+
} catch (error) {
|
|
783
|
+
const kind = classifyLinuxSecureStoreError(error);
|
|
784
|
+
if (kind === "missing_entry" && selectedBackend$2 === "native-linux") {
|
|
785
|
+
const fallbackResult = await trySaveWithSecretServiceFallback(accountId, serialized);
|
|
786
|
+
if (fallbackResult.ok) return;
|
|
787
|
+
throw createLinuxSecureStoreUnavailableError(`Native backend could not create the credential entry (${getErrorMessage(error)}). Fallback secret-service backend also failed (${getErrorMessage(fallbackResult.error)}).`);
|
|
788
|
+
}
|
|
789
|
+
if (kind === "store_unavailable") throw createLinuxSecureStoreUnavailableError(getErrorMessage(error));
|
|
790
|
+
throw error;
|
|
791
|
+
}
|
|
522
792
|
};
|
|
523
|
-
const saveLinuxCrossKeychainPayload = async (accountId, payload) => withService$1(accountId, (service) => setPassword(service, accountId, JSON.stringify(payload)), { forWrite: true });
|
|
524
793
|
const loadLinuxCrossKeychainPayload = async (accountId) => {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
794
|
+
try {
|
|
795
|
+
const raw = await withService$1(accountId, (service) => getPassword(service, accountId));
|
|
796
|
+
if (raw === null) throw new Error(`No stored credentials found for account ${accountId}.`);
|
|
797
|
+
return parsePayload$2(accountId, raw);
|
|
798
|
+
} catch (error) {
|
|
799
|
+
if (classifyLinuxSecureStoreError(error) === "missing_entry") throw new Error(`No stored credentials found for account ${accountId}.`);
|
|
800
|
+
throw error;
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
const deleteLinuxCrossKeychainPayload = async (accountId) => {
|
|
804
|
+
try {
|
|
805
|
+
await withService$1(accountId, (service) => deletePassword(service, accountId));
|
|
806
|
+
} catch (error) {
|
|
807
|
+
if (classifyLinuxSecureStoreError(error) === "missing_entry") return;
|
|
808
|
+
throw error;
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
const linuxCrossKeychainPayloadExists = async (accountId) => {
|
|
812
|
+
try {
|
|
813
|
+
return await withService$1(accountId, async (service) => await getPassword(service, accountId) !== null);
|
|
814
|
+
} catch (error) {
|
|
815
|
+
if (classifyLinuxSecureStoreError(error) === "missing_entry") return false;
|
|
816
|
+
throw error;
|
|
817
|
+
}
|
|
528
818
|
};
|
|
529
|
-
const deleteLinuxCrossKeychainPayload = async (accountId) => withService$1(accountId, (service) => deletePassword(service, accountId));
|
|
530
|
-
const linuxCrossKeychainPayloadExists = async (accountId) => withService$1(accountId, async (service) => await getPassword(service, accountId) !== null);
|
|
531
819
|
|
|
532
820
|
//#endregion
|
|
533
821
|
//#region lib/secrets/macos-cross-keychain.ts
|
|
@@ -775,13 +1063,16 @@ const windowsCrossKeychainPayloadExists = async (accountId) => withWindowsBacken
|
|
|
775
1063
|
//#endregion
|
|
776
1064
|
//#region lib/secrets/store.ts
|
|
777
1065
|
const MISSING_SECRET_STORE_ERROR_MARKERS = [
|
|
778
|
-
"
|
|
779
|
-
"
|
|
780
|
-
"
|
|
1066
|
+
"no stored credentials found",
|
|
1067
|
+
"no keychain payload found",
|
|
1068
|
+
"password not found",
|
|
1069
|
+
"no matching entry found in secure storage",
|
|
1070
|
+
"no result found"
|
|
781
1071
|
];
|
|
782
1072
|
const isMissingSecretStoreEntryError = (error) => {
|
|
783
1073
|
if (!(error instanceof Error)) return false;
|
|
784
|
-
|
|
1074
|
+
const message = error.message.toLowerCase();
|
|
1075
|
+
return MISSING_SECRET_STORE_ERROR_MARKERS.some((marker) => message.includes(marker));
|
|
785
1076
|
};
|
|
786
1077
|
const createMissingSecretStoreEntryError = (accountId) => /* @__PURE__ */ new Error(`No stored credentials found for account ${accountId}.`);
|
|
787
1078
|
const CACHED_ADAPTER_SYMBOL = Symbol.for("cdx.secretStore.cachedAdapter");
|
|
@@ -1467,33 +1758,19 @@ const promptBrowserFallbackChoice = async () => {
|
|
|
1467
1758
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1468
1759
|
const selected = remoteHint ? "device" : "manual";
|
|
1469
1760
|
p.log.info(`Non-interactive terminal detected. Falling back to ${selected === "device" ? "device OAuth flow" : "manual URL copy/paste flow"}.`);
|
|
1761
|
+
if (selected === "device") p.log.info("When interactive, manual URL copy/paste flow is recommended because device flow may be blocked by Cloudflare on some VPS/server IPs.");
|
|
1470
1762
|
return selected;
|
|
1471
1763
|
}
|
|
1472
|
-
const options =
|
|
1473
|
-
{
|
|
1474
|
-
value: "device",
|
|
1475
|
-
label: "Use device OAuth flow",
|
|
1476
|
-
hint: "Recommended on SSH/remote servers"
|
|
1477
|
-
},
|
|
1478
|
-
{
|
|
1479
|
-
value: "manual",
|
|
1480
|
-
label: "Finish manually by copying URL",
|
|
1481
|
-
hint: "Open URL on any machine and paste callback URL/code back here"
|
|
1482
|
-
},
|
|
1483
|
-
{
|
|
1484
|
-
value: "cancel",
|
|
1485
|
-
label: "Cancel login"
|
|
1486
|
-
}
|
|
1487
|
-
] : [
|
|
1764
|
+
const options = [
|
|
1488
1765
|
{
|
|
1489
1766
|
value: "manual",
|
|
1490
|
-
label: "Finish manually by copying URL",
|
|
1491
|
-
hint: "Open URL on any machine and paste callback URL/code back here"
|
|
1767
|
+
label: "Finish manually by copying URL (recommended)",
|
|
1768
|
+
hint: remoteHint ? "Best on SSH/VPS: open URL on any machine and paste callback URL/code back here" : "Open URL on any machine and paste callback URL/code back here"
|
|
1492
1769
|
},
|
|
1493
1770
|
{
|
|
1494
1771
|
value: "device",
|
|
1495
1772
|
label: "Use device OAuth flow",
|
|
1496
|
-
hint: "
|
|
1773
|
+
hint: "May fail on some VPS/servers due to Cloudflare challenge"
|
|
1497
1774
|
},
|
|
1498
1775
|
{
|
|
1499
1776
|
value: "cancel",
|
|
@@ -1535,10 +1812,33 @@ const promptPortConflictChoice = async (listeningProcess) => {
|
|
|
1535
1812
|
if (p.isCancel(selection)) return "cancel";
|
|
1536
1813
|
return selection;
|
|
1537
1814
|
};
|
|
1815
|
+
const maybeCopyAuthorizationUrlToClipboard = async (authorizationUrl) => {
|
|
1816
|
+
if (Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY)) {
|
|
1817
|
+
const shouldCopy = await p.confirm({
|
|
1818
|
+
message: "Copy login URL to clipboard now?",
|
|
1819
|
+
initialValue: true
|
|
1820
|
+
});
|
|
1821
|
+
if (p.isCancel(shouldCopy) || !shouldCopy) return;
|
|
1822
|
+
}
|
|
1823
|
+
const copyResult = tryCopyToClipboard(authorizationUrl);
|
|
1824
|
+
if (copyResult.ok) {
|
|
1825
|
+
p.log.success(`Copied login URL to clipboard via ${copyResult.method}.`);
|
|
1826
|
+
if (copyResult.warning) {
|
|
1827
|
+
p.log.warning(copyResult.warning);
|
|
1828
|
+
const helper = buildClipboardHelperCommand(authorizationUrl);
|
|
1829
|
+
if (helper) p.log.message(`If needed, run this copy command instead:\n${helper}`);
|
|
1830
|
+
}
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
p.log.warning(`Could not copy login URL automatically${copyResult.error ? ` (${copyResult.error})` : ""}.`);
|
|
1834
|
+
const helper = buildClipboardHelperCommand(authorizationUrl);
|
|
1835
|
+
if (helper) p.log.message(`Try this copy command:\n${helper}`);
|
|
1836
|
+
};
|
|
1538
1837
|
const promptManualAuthorizationCode = async (authorizationUrl, expectedState) => {
|
|
1539
1838
|
p.log.info("Manual login selected.");
|
|
1540
1839
|
p.log.message(`Open this URL in a browser:\n${authorizationUrl}`);
|
|
1541
1840
|
p.log.message("After approving, copy the full callback URL (or just the 'code' value) and paste it below.");
|
|
1841
|
+
await maybeCopyAuthorizationUrlToClipboard(authorizationUrl);
|
|
1542
1842
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
1543
1843
|
const response = await p.text({
|
|
1544
1844
|
message: "Paste callback URL or authorization code:",
|
|
@@ -1563,6 +1863,8 @@ const promptManualAuthorizationCode = async (authorizationUrl, expectedState) =>
|
|
|
1563
1863
|
return null;
|
|
1564
1864
|
};
|
|
1565
1865
|
const runDeviceOAuthFlow = async (useSpinner) => {
|
|
1866
|
+
p.log.info("Device OAuth flow may fail on some VPS/servers because auth.openai.com can return a Cloudflare challenge.");
|
|
1867
|
+
p.log.info("Recommended alternative: run login without --device-flow and use manual URL copy/paste callback completion.");
|
|
1566
1868
|
const deviceFlowResult = await startDeviceAuthorizationFlow();
|
|
1567
1869
|
if (deviceFlowResult.type !== "success") {
|
|
1568
1870
|
p.log.error("Device OAuth flow is not available right now.");
|
|
@@ -2413,6 +2715,56 @@ const getKeychainDecryptAccessByServiceAsync = async (services) => {
|
|
|
2413
2715
|
return parseKeychainDecryptAccessFromDump(dumpResult.output, dedupedServices);
|
|
2414
2716
|
};
|
|
2415
2717
|
|
|
2718
|
+
//#endregion
|
|
2719
|
+
//#region lib/secrets/probe.ts
|
|
2720
|
+
const toError = (error) => error instanceof Error ? error : new Error(String(error));
|
|
2721
|
+
const createProbePayload = (accountId, now) => ({
|
|
2722
|
+
refresh: `probe-refresh-${accountId}`,
|
|
2723
|
+
access: `probe-access-${accountId}`,
|
|
2724
|
+
expires: now + 6e4,
|
|
2725
|
+
accountId
|
|
2726
|
+
});
|
|
2727
|
+
const payloadMatches = (expected, actual) => expected.accountId === actual.accountId && expected.refresh === actual.refresh && expected.access === actual.access && expected.expires === actual.expires;
|
|
2728
|
+
const runSecretStoreWriteReadProbe = async (secretStore, options = {}) => {
|
|
2729
|
+
const probeAccountId = options.probeAccountId ?? `cdx-doctor-probe-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2730
|
+
const payload = createProbePayload(probeAccountId, options.now ?? Date.now());
|
|
2731
|
+
let saveSucceeded = false;
|
|
2732
|
+
let result = { ok: true };
|
|
2733
|
+
try {
|
|
2734
|
+
await secretStore.save(probeAccountId, payload);
|
|
2735
|
+
saveSucceeded = true;
|
|
2736
|
+
} catch (error) {
|
|
2737
|
+
result = {
|
|
2738
|
+
ok: false,
|
|
2739
|
+
stage: "save",
|
|
2740
|
+
error: toError(error)
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
if (result.ok) try {
|
|
2744
|
+
if (!payloadMatches(payload, await secretStore.load(probeAccountId))) result = {
|
|
2745
|
+
ok: false,
|
|
2746
|
+
stage: "verify",
|
|
2747
|
+
error: /* @__PURE__ */ new Error("Secure-store probe loaded payload does not match the saved payload.")
|
|
2748
|
+
};
|
|
2749
|
+
} catch (error) {
|
|
2750
|
+
result = {
|
|
2751
|
+
ok: false,
|
|
2752
|
+
stage: "load",
|
|
2753
|
+
error: toError(error)
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
if (saveSucceeded) try {
|
|
2757
|
+
await secretStore.delete(probeAccountId);
|
|
2758
|
+
} catch (error) {
|
|
2759
|
+
if (result.ok) result = {
|
|
2760
|
+
ok: false,
|
|
2761
|
+
stage: "delete",
|
|
2762
|
+
error: toError(error)
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
return result;
|
|
2766
|
+
};
|
|
2767
|
+
|
|
2416
2768
|
//#endregion
|
|
2417
2769
|
//#region lib/commands/doctor.ts
|
|
2418
2770
|
const hasRuntimeTrustedApp = (trustedApplications, runtimeExecutablePath) => {
|
|
@@ -2422,6 +2774,23 @@ const hasRuntimeTrustedApp = (trustedApplications, runtimeExecutablePath) => {
|
|
|
2422
2774
|
return path.basename(trustedApp).toLowerCase() === runtimeBaseName;
|
|
2423
2775
|
});
|
|
2424
2776
|
};
|
|
2777
|
+
const getSecretStoreProbeHeading = (platform) => {
|
|
2778
|
+
if (platform === "linux") return "Linux secure-store probe";
|
|
2779
|
+
if (platform === "darwin") return "macOS secure-store probe";
|
|
2780
|
+
if (platform === "win32") return "Windows secure-store probe";
|
|
2781
|
+
return null;
|
|
2782
|
+
};
|
|
2783
|
+
const createProbeAdapterForCurrentPlatform = () => {
|
|
2784
|
+
const currentAdapter = getSecretStoreAdapter();
|
|
2785
|
+
if (process.platform === "darwin" && currentAdapter.id === "macos-legacy-keychain") return createSecretStoreAdapterFromSelection("legacy-keychain", "darwin");
|
|
2786
|
+
return createRuntimeSecretStoreAdapter(process.platform);
|
|
2787
|
+
};
|
|
2788
|
+
const getSecretStoreProbeGuidance = (platform) => {
|
|
2789
|
+
if (platform === "linux") return "Suggested fix: ensure Secret Service is running/unlocked (for example gnome-keyring + secret-tool), then retry login.";
|
|
2790
|
+
if (platform === "darwin") return "Suggested fix: ensure Keychain Access is unlocked and allows this runtime/toolchain to store/read passwords, then retry login.";
|
|
2791
|
+
if (platform === "win32") return "Suggested fix: ensure Windows Credential Manager is available for this user session, then retry login.";
|
|
2792
|
+
return null;
|
|
2793
|
+
};
|
|
2425
2794
|
const registerDoctorCommand = (program) => {
|
|
2426
2795
|
program.command("doctor").description("Show auth file paths and runtime capabilities").option("--check-keychain-acl", "Run keychain trusted-app/ACL checks on macOS (can be slow)").action(async (options) => {
|
|
2427
2796
|
try {
|
|
@@ -2472,6 +2841,17 @@ const registerDoctorCommand = (program) => {
|
|
|
2472
2841
|
process.stdout.write(` Summary: ${okCount}/${status.accounts.length} configured account(s) passed secure-store load checks.\n`);
|
|
2473
2842
|
}
|
|
2474
2843
|
}
|
|
2844
|
+
const probeHeading = getSecretStoreProbeHeading(process.platform);
|
|
2845
|
+
if (probeHeading) {
|
|
2846
|
+
process.stdout.write(`\n${probeHeading}:\n`);
|
|
2847
|
+
const probeResult = await runSecretStoreWriteReadProbe(createProbeAdapterForCurrentPlatform());
|
|
2848
|
+
if (probeResult.ok) process.stdout.write(" write/read/delete probe: OK\n");
|
|
2849
|
+
else {
|
|
2850
|
+
process.stdout.write(` ⚠ ${probeResult.stage} failed: ${probeResult.error.message}\n`);
|
|
2851
|
+
const guidance = getSecretStoreProbeGuidance(process.platform);
|
|
2852
|
+
if (guidance) process.stdout.write(` ${guidance}\n`);
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2475
2855
|
if (process.platform === "darwin" && !options.checkKeychainAcl) {
|
|
2476
2856
|
process.stdout.write(" ┌─ Optional keychain ACL check\n");
|
|
2477
2857
|
process.stdout.write(" │ Run: cdx doctor --check-keychain-acl\n");
|
|
@@ -2567,7 +2947,7 @@ const registerLabelCommand = (program) => {
|
|
|
2567
2947
|
//#region lib/commands/login.ts
|
|
2568
2948
|
const registerLoginCommand = (program, deps = {}) => {
|
|
2569
2949
|
const runLogin = deps.performLogin ?? performLogin;
|
|
2570
|
-
program.command("login").description("Add a new OpenAI account via OAuth").option("--device-flow", "Use OAuth device flow instead of browser callback flow").action(async (options) => {
|
|
2950
|
+
program.command("login").description("Add a new OpenAI account via OAuth").option("--device-flow", "Use OAuth device flow instead of browser callback flow (manual/browser flow is recommended; device flow may fail on some VPS due to Cloudflare)").action(async (options) => {
|
|
2571
2951
|
try {
|
|
2572
2952
|
if (!await runLogin({ authFlow: options.deviceFlow ? "device" : "auto" })) {
|
|
2573
2953
|
process.stderr.write("Login failed.\n");
|
|
@@ -2719,7 +3099,7 @@ const writeUpdatedAuthSummary = (result) => {
|
|
|
2719
3099
|
//#endregion
|
|
2720
3100
|
//#region lib/commands/refresh.ts
|
|
2721
3101
|
const registerReloginCommand = (program) => {
|
|
2722
|
-
program.command("relogin").description("Re-authenticate an existing account with full OAuth login (no duplicate account)").option("--device-flow", "Use OAuth device flow instead of browser callback flow").argument("[account]", "Account ID or label to re-login").action(async (account, options) => {
|
|
3102
|
+
program.command("relogin").description("Re-authenticate an existing account with full OAuth login (no duplicate account)").option("--device-flow", "Use OAuth device flow instead of browser callback flow (manual/browser flow is recommended; device flow may fail on some VPS due to Cloudflare)").argument("[account]", "Account ID or label to re-login").action(async (account, options) => {
|
|
2723
3103
|
try {
|
|
2724
3104
|
const authFlow = options.deviceFlow ? "device" : "auto";
|
|
2725
3105
|
if (account) {
|
|
@@ -3043,6 +3423,189 @@ const registerUsageCommand = (program) => {
|
|
|
3043
3423
|
});
|
|
3044
3424
|
};
|
|
3045
3425
|
|
|
3426
|
+
//#endregion
|
|
3427
|
+
//#region lib/runtime/update-manager.ts
|
|
3428
|
+
const detectRuntime = (input = {}) => {
|
|
3429
|
+
const hasDenoGlobal = input.hasDenoGlobal ?? ("Deno" in globalThis && typeof globalThis.Deno !== "undefined");
|
|
3430
|
+
const versions = input.versions ?? process.versions;
|
|
3431
|
+
if (hasDenoGlobal) return "deno";
|
|
3432
|
+
if (typeof versions.bun === "string" && versions.bun.length > 0) return "bun";
|
|
3433
|
+
if (typeof versions.node === "string" && versions.node.length > 0) return "node";
|
|
3434
|
+
return "unknown";
|
|
3435
|
+
};
|
|
3436
|
+
const normalizePath = (value) => value.replaceAll("\\", "/").toLowerCase();
|
|
3437
|
+
const detectInstallManagerFromPath = (executablePath) => {
|
|
3438
|
+
if (!executablePath) return "unknown";
|
|
3439
|
+
const normalizedPath = normalizePath(executablePath);
|
|
3440
|
+
if (normalizedPath.includes("/.bun/install/global/node_modules/")) return "bun";
|
|
3441
|
+
if (normalizedPath.includes("/lib/node_modules/") || normalizedPath.includes("/npm/node_modules/")) return "npm";
|
|
3442
|
+
if (normalizedPath.includes("/.deno/") || normalizedPath.includes("/deno/bin/")) return "deno";
|
|
3443
|
+
return "unknown";
|
|
3444
|
+
};
|
|
3445
|
+
const classifyInstallContextFromPath = (executablePath) => {
|
|
3446
|
+
if (!executablePath) return "unknown";
|
|
3447
|
+
if (detectInstallManagerFromPath(executablePath) !== "unknown") return "global";
|
|
3448
|
+
const normalizedPath = normalizePath(executablePath);
|
|
3449
|
+
if (normalizedPath.endsWith("/cdx.ts") || normalizedPath.includes("/codex-switcher/") || normalizedPath.includes("/node_modules/.bin/")) return "local-or-dev";
|
|
3450
|
+
return "unknown";
|
|
3451
|
+
};
|
|
3452
|
+
const resolveFromRuntime = (runtime) => {
|
|
3453
|
+
if (runtime === "bun") return "bun";
|
|
3454
|
+
if (runtime === "node") return "npm";
|
|
3455
|
+
if (runtime === "deno") return "deno";
|
|
3456
|
+
return null;
|
|
3457
|
+
};
|
|
3458
|
+
const resolveUpdateManager = (input) => {
|
|
3459
|
+
if (input.requestedManager !== "auto") return {
|
|
3460
|
+
ok: true,
|
|
3461
|
+
manager: input.requestedManager,
|
|
3462
|
+
source: "explicit"
|
|
3463
|
+
};
|
|
3464
|
+
if (input.installManager !== "unknown") return {
|
|
3465
|
+
ok: true,
|
|
3466
|
+
manager: input.installManager,
|
|
3467
|
+
source: "install-manager"
|
|
3468
|
+
};
|
|
3469
|
+
const fromRuntime = resolveFromRuntime(input.runtime);
|
|
3470
|
+
if (fromRuntime) return {
|
|
3471
|
+
ok: true,
|
|
3472
|
+
manager: fromRuntime,
|
|
3473
|
+
source: "runtime"
|
|
3474
|
+
};
|
|
3475
|
+
return { ok: false };
|
|
3476
|
+
};
|
|
3477
|
+
const buildUpdateInstallCommand = (manager, packageName) => {
|
|
3478
|
+
if (manager === "bun") return {
|
|
3479
|
+
command: "bun",
|
|
3480
|
+
args: [
|
|
3481
|
+
"add",
|
|
3482
|
+
"-g",
|
|
3483
|
+
`${packageName}@latest`
|
|
3484
|
+
]
|
|
3485
|
+
};
|
|
3486
|
+
if (manager === "npm") return {
|
|
3487
|
+
command: "npm",
|
|
3488
|
+
args: [
|
|
3489
|
+
"i",
|
|
3490
|
+
"-g",
|
|
3491
|
+
`${packageName}@latest`
|
|
3492
|
+
]
|
|
3493
|
+
};
|
|
3494
|
+
return {
|
|
3495
|
+
command: "deno",
|
|
3496
|
+
args: [
|
|
3497
|
+
"install",
|
|
3498
|
+
"-g",
|
|
3499
|
+
"-f",
|
|
3500
|
+
"-A",
|
|
3501
|
+
"-n",
|
|
3502
|
+
"cdx",
|
|
3503
|
+
`npm:${packageName}@latest`
|
|
3504
|
+
]
|
|
3505
|
+
};
|
|
3506
|
+
};
|
|
3507
|
+
|
|
3508
|
+
//#endregion
|
|
3509
|
+
//#region lib/commands/update-self.ts
|
|
3510
|
+
const PACKAGE_NAME = "@bjesuiter/codex-switcher";
|
|
3511
|
+
const quoteIfNeeded = (value) => {
|
|
3512
|
+
if (value.includes(" ")) return JSON.stringify(value);
|
|
3513
|
+
return value;
|
|
3514
|
+
};
|
|
3515
|
+
const formatShellCommand = (command, args) => [command, ...args].map(quoteIfNeeded).join(" ");
|
|
3516
|
+
const executeUpdate = async (command, args) => {
|
|
3517
|
+
await new Promise((resolve, reject) => {
|
|
3518
|
+
const child = spawn(command, args, { stdio: "inherit" });
|
|
3519
|
+
child.once("error", (error) => {
|
|
3520
|
+
reject(error);
|
|
3521
|
+
});
|
|
3522
|
+
child.once("close", (code) => {
|
|
3523
|
+
if (code === 0) {
|
|
3524
|
+
resolve();
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
reject(/* @__PURE__ */ new Error(`Update command failed with exit code ${code ?? "unknown"}.`));
|
|
3528
|
+
});
|
|
3529
|
+
});
|
|
3530
|
+
};
|
|
3531
|
+
const registerUpdateSelfCommand = (program) => {
|
|
3532
|
+
program.command("update-self").description("Update cdx to the latest version").option("--manager <manager>", "Select update manager (auto|bun|npm|deno)", "auto").option("--dry-run", "Print selected manager and update command without executing").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
|
|
3533
|
+
try {
|
|
3534
|
+
const requestedManager = options.manager ?? "auto";
|
|
3535
|
+
if (![
|
|
3536
|
+
"auto",
|
|
3537
|
+
"bun",
|
|
3538
|
+
"npm",
|
|
3539
|
+
"deno"
|
|
3540
|
+
].includes(requestedManager)) {
|
|
3541
|
+
process.stderr.write(`Invalid value '${requestedManager}' for --manager. Allowed values: auto, bun, npm, deno.\n`);
|
|
3542
|
+
process.exit(1);
|
|
3543
|
+
}
|
|
3544
|
+
const runtime = detectRuntime();
|
|
3545
|
+
const executablePath = process.argv[1];
|
|
3546
|
+
const installManager = detectInstallManagerFromPath(executablePath);
|
|
3547
|
+
const installContext = classifyInstallContextFromPath(executablePath);
|
|
3548
|
+
if (requestedManager === "auto" && installManager === "unknown" && installContext === "local-or-dev") {
|
|
3549
|
+
process.stderr.write("Refusing to auto-update from a local/dev checkout. Re-run with --manager bun|npm|deno.\n");
|
|
3550
|
+
process.exit(1);
|
|
3551
|
+
}
|
|
3552
|
+
const resolvedManager = resolveUpdateManager({
|
|
3553
|
+
requestedManager,
|
|
3554
|
+
runtime,
|
|
3555
|
+
installManager
|
|
3556
|
+
});
|
|
3557
|
+
process.stdout.write(`Runtime: ${runtime}\n`);
|
|
3558
|
+
process.stdout.write(`Detected install manager: ${installManager}\n`);
|
|
3559
|
+
if (!resolvedManager.ok) {
|
|
3560
|
+
process.stderr.write("Could not determine update manager automatically. Re-run with --manager bun|npm|deno.\n");
|
|
3561
|
+
process.stderr.write("Manual update commands:\n");
|
|
3562
|
+
process.stderr.write(` bun: ${formatShellCommand("bun", [
|
|
3563
|
+
"add",
|
|
3564
|
+
"-g",
|
|
3565
|
+
`${PACKAGE_NAME}@latest`
|
|
3566
|
+
])}\n`);
|
|
3567
|
+
process.stderr.write(` npm: ${formatShellCommand("npm", [
|
|
3568
|
+
"i",
|
|
3569
|
+
"-g",
|
|
3570
|
+
`${PACKAGE_NAME}@latest`
|
|
3571
|
+
])}\n`);
|
|
3572
|
+
process.stderr.write(` deno: ${formatShellCommand("deno", [
|
|
3573
|
+
"install",
|
|
3574
|
+
"-g",
|
|
3575
|
+
"-f",
|
|
3576
|
+
"-A",
|
|
3577
|
+
"-n",
|
|
3578
|
+
"cdx",
|
|
3579
|
+
`npm:${PACKAGE_NAME}@latest`
|
|
3580
|
+
])}\n`);
|
|
3581
|
+
process.exit(1);
|
|
3582
|
+
}
|
|
3583
|
+
const selectedManager = resolvedManager.manager;
|
|
3584
|
+
const command = buildUpdateInstallCommand(selectedManager, PACKAGE_NAME);
|
|
3585
|
+
const printableCommand = formatShellCommand(command.command, command.args);
|
|
3586
|
+
process.stdout.write(`Selected manager: ${selectedManager}\n`);
|
|
3587
|
+
process.stdout.write(`Source: ${resolvedManager.source}\n`);
|
|
3588
|
+
process.stdout.write(`Command: ${printableCommand}\n`);
|
|
3589
|
+
if (options.dryRun) return;
|
|
3590
|
+
if (!options.yes) {
|
|
3591
|
+
const confirmed = await p.confirm({
|
|
3592
|
+
message: `Run update command now?\n${printableCommand}`,
|
|
3593
|
+
initialValue: true
|
|
3594
|
+
});
|
|
3595
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
3596
|
+
process.stderr.write("Update cancelled.\n");
|
|
3597
|
+
process.exit(1);
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
await executeUpdate(command.command, command.args);
|
|
3601
|
+
process.stdout.write("Update completed.\n");
|
|
3602
|
+
process.stdout.write("Run `cdx version` to verify the installed version.\n");
|
|
3603
|
+
} catch (error) {
|
|
3604
|
+
exitWithCommandError(error);
|
|
3605
|
+
}
|
|
3606
|
+
});
|
|
3607
|
+
};
|
|
3608
|
+
|
|
3046
3609
|
//#endregion
|
|
3047
3610
|
//#region lib/commands/version.ts
|
|
3048
3611
|
const registerVersionCommand = (program, version) => {
|
|
@@ -3162,6 +3725,7 @@ const createProgram = (deps = {}) => {
|
|
|
3162
3725
|
registerStatusCommand(program);
|
|
3163
3726
|
registerDoctorCommand(program);
|
|
3164
3727
|
registerUsageCommand(program);
|
|
3728
|
+
registerUpdateSelfCommand(program);
|
|
3165
3729
|
registerHelpCommand(program);
|
|
3166
3730
|
registerVersionCommand(program, version);
|
|
3167
3731
|
registerDefaultInteractiveAction(program);
|