@bjesuiter/codex-switcher 1.8.0 → 1.8.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.
Files changed (3) hide show
  1. package/README.md +5 -7
  2. package/cdx.mjs +203 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,19 +6,15 @@ Switch the coding-agents [pi](https://pi.dev/), [codex](https://developers.opena
6
6
 
7
7
  ## Latest Changes
8
8
 
9
- ### 1.8.0
9
+ ### 1.8.2
10
10
 
11
11
  #### Features
12
12
 
13
- - Added manual OAuth URL clipboard assist for login/relogin fallback flow, including an opt-in copy prompt and non-blocking behavior.
14
- - Added cross-platform clipboard copy strategies for auth URLs (local clipboard commands + OSC52 terminal fallback).
15
- - Added tmux/screen OSC52 framing support and fallback copy-command hints when auto-copy is unavailable.
13
+ - Added `update-self` command aliases: `self-update`, `update`, and `updte`.
16
14
 
17
15
  #### Fixes
18
16
 
19
- - Clarified fallback guidance: manual URL copy/paste flow is now the recommended browser-launch fallback, while `--device-flow` may fail on some VPS/server IPs due to Cloudflare challenges.
20
- - Linux secure-store login reliability: treat native Secret Service "no matching entry" responses as missing-entry cases and improve unavailable-store error guidance.
21
- - Added Mosh-specific clipboard heuristics/warnings so OSC52 copy reports are less misleading when clipboard updates are unreliable.
17
+ - Added an interactive Linux troubleshooting checklist in `cdx doctor` when the secure-store probe fails, guiding sequential checks for `gnome-keyring`, `secret-tool`, and running `gnome-keyring-daemon`.
22
18
 
23
19
  see full changelog here: https://github.com/bjesuiter/codex-switcher/blob/main/CHANGELOG.md
24
20
 
@@ -177,6 +173,8 @@ cdx migrate-secrets
177
173
  | `cdx doctor --check-keychain-acl` | Detect macOS keychain ACL/runtime mismatches (`cdx`/Bun vs legacy `security` CLI), warn about prompt-heavy setups, and suggest `cdx migrate-secrets` (slow) |
178
174
  | `cdx usage` | Show usage overview for all accounts |
179
175
  | `cdx usage <account>` | Show detailed usage for a specific account |
176
+ | `cdx update-self` | Update cdx to the latest version |
177
+ | `cdx self-update` / `cdx update` / `cdx updte` | Aliases for `cdx update-self` |
180
178
  | `cdx help [command]` | Show help for all commands or one command |
181
179
  | `cdx complete <shell>` | Generate shell completion script (`zsh`, `bash`, `fish`, `powershell`) |
182
180
  | `cdx version` | Show CLI version |
package/cdx.mjs CHANGED
@@ -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.8.0";
18
+ var version = "1.8.2";
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$1 = (command, platform = process.platform) => {
259
+ const isCommandAvailable$2 = (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$1(launcher.command, platform)
272
+ available: isCommandAvailable$2(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$1;
278
+ const commandAvailable = options.isCommandAvailableImpl ?? isCommandAvailable$2;
279
279
  const launcher = getBrowserLauncher(platform, url);
280
280
  if (!commandAvailable(launcher.command, platform)) return {
281
281
  ok: false,
@@ -306,7 +306,7 @@ const openBrowserUrl = (url, options = {}) => {
306
306
 
307
307
  //#endregion
308
308
  //#region lib/platform/clipboard.ts
309
- const isCommandAvailable = (command, platform = process.platform) => {
309
+ const isCommandAvailable$1 = (command, platform = process.platform) => {
310
310
  const probe = platform === "win32" ? "where" : "which";
311
311
  return Bun.spawnSync({
312
312
  cmd: [probe, command],
@@ -392,7 +392,7 @@ const getLocalClipboardTargets = (platform, env, commandExists) => {
392
392
  });
393
393
  return targets;
394
394
  };
395
- const resolveClipboardTargets = (context = {}, commandExists = isCommandAvailable) => {
395
+ const resolveClipboardTargets = (context = {}, commandExists = isCommandAvailable$1) => {
396
396
  const platform = context.platform ?? process.platform;
397
397
  const env = context.env ?? process.env;
398
398
  const isTTY = context.isTTY ?? (Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY));
@@ -433,7 +433,7 @@ const defaultRunCommand = (command, args, input) => {
433
433
  };
434
434
  };
435
435
  const tryCopyToClipboard = (text, options = {}) => {
436
- const commandExists = options.commandExistsImpl ?? isCommandAvailable;
436
+ const commandExists = options.commandExistsImpl ?? isCommandAvailable$1;
437
437
  const runCommand = options.runCommandImpl ?? defaultRunCommand;
438
438
  const writeStdout = options.writeStdoutImpl ?? ((chunk) => {
439
439
  process.stdout.write(chunk);
@@ -479,7 +479,7 @@ const tryCopyToClipboard = (text, options = {}) => {
479
479
  };
480
480
  };
481
481
  const escapePosixSingleQuoted = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
482
- const buildClipboardHelperCommand = (text, context = {}, commandExists = isCommandAvailable) => {
482
+ const buildClipboardHelperCommand = (text, context = {}, commandExists = isCommandAvailable$1) => {
483
483
  const commandTarget = resolveClipboardTargets(context, commandExists).find((target) => target.kind === "command");
484
484
  if (!commandTarget) return null;
485
485
  if (commandTarget.method === "powershell") {
@@ -663,13 +663,15 @@ const MISSING_ENTRY_MARKERS = [
663
663
  "no matching entry found in secure storage",
664
664
  "password not found",
665
665
  "no stored credentials found",
666
- "credential not found"
666
+ "credential not found",
667
+ "no result found"
667
668
  ];
668
669
  const STORE_UNAVAILABLE_MARKERS = [
669
670
  "unable to initialize linux secure-store backend",
670
671
  "no keyring backend could be initialized",
671
672
  "native keyring module not available",
672
673
  "secret service operation failed",
674
+ "couldn't access platform secure storage",
673
675
  "dbus",
674
676
  "d-bus",
675
677
  "org.freedesktop.secrets",
@@ -1061,14 +1063,16 @@ const windowsCrossKeychainPayloadExists = async (accountId) => withWindowsBacken
1061
1063
  //#endregion
1062
1064
  //#region lib/secrets/store.ts
1063
1065
  const MISSING_SECRET_STORE_ERROR_MARKERS = [
1064
- "No stored credentials found",
1065
- "No Keychain payload found",
1066
- "Password not found",
1067
- "no matching entry found in secure storage"
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"
1068
1071
  ];
1069
1072
  const isMissingSecretStoreEntryError = (error) => {
1070
1073
  if (!(error instanceof Error)) return false;
1071
- return MISSING_SECRET_STORE_ERROR_MARKERS.some((marker) => error.message.includes(marker));
1074
+ const message = error.message.toLowerCase();
1075
+ return MISSING_SECRET_STORE_ERROR_MARKERS.some((marker) => message.includes(marker));
1072
1076
  };
1073
1077
  const createMissingSecretStoreEntryError = (accountId) => /* @__PURE__ */ new Error(`No stored credentials found for account ${accountId}.`);
1074
1078
  const CACHED_ADAPTER_SYMBOL = Symbol.for("cdx.secretStore.cachedAdapter");
@@ -2711,6 +2715,56 @@ const getKeychainDecryptAccessByServiceAsync = async (services) => {
2711
2715
  return parseKeychainDecryptAccessFromDump(dumpResult.output, dedupedServices);
2712
2716
  };
2713
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
+
2714
2768
  //#endregion
2715
2769
  //#region lib/commands/doctor.ts
2716
2770
  const hasRuntimeTrustedApp = (trustedApplications, runtimeExecutablePath) => {
@@ -2720,6 +2774,123 @@ const hasRuntimeTrustedApp = (trustedApplications, runtimeExecutablePath) => {
2720
2774
  return path.basename(trustedApp).toLowerCase() === runtimeBaseName;
2721
2775
  });
2722
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
+ };
2794
+ const isInteractiveTerminal = () => Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
2795
+ const runCommandCapture = async (command, args) => await new Promise((resolve) => {
2796
+ const child = spawn(command, args, { stdio: [
2797
+ "ignore",
2798
+ "pipe",
2799
+ "pipe"
2800
+ ] });
2801
+ let stdout = "";
2802
+ let stderr = "";
2803
+ let spawnError = null;
2804
+ child.stdout?.on("data", (chunk) => {
2805
+ stdout += chunk.toString();
2806
+ });
2807
+ child.stderr?.on("data", (chunk) => {
2808
+ stderr += chunk.toString();
2809
+ });
2810
+ child.once("error", (error) => {
2811
+ spawnError = error.message;
2812
+ });
2813
+ child.once("close", (code) => {
2814
+ resolve({
2815
+ ok: spawnError === null && code === 0,
2816
+ stdout: stdout.trim(),
2817
+ stderr: stderr.trim(),
2818
+ ...spawnError ? { error: spawnError } : {}
2819
+ });
2820
+ });
2821
+ });
2822
+ const extractCommandFailureDetails = (result) => result.error || result.stderr || result.stdout || void 0;
2823
+ const isCommandAvailable = async (commandName) => {
2824
+ return (await runCommandCapture("sh", ["-lc", `command -v ${commandName} >/dev/null 2>&1`])).ok;
2825
+ };
2826
+ const checkGnomeKeyringRunning = async () => {
2827
+ if (await isCommandAvailable("pgrep")) {
2828
+ const pgrepResult = await runCommandCapture("pgrep", ["-x", "gnome-keyring-daemon"]);
2829
+ if (pgrepResult.ok) return { ok: true };
2830
+ return {
2831
+ ok: false,
2832
+ details: extractCommandFailureDetails(pgrepResult) ?? "No gnome-keyring-daemon process found."
2833
+ };
2834
+ }
2835
+ const psFallback = await runCommandCapture("sh", ["-lc", "ps -A -o comm= | grep -q '^gnome-keyring-daemon$'"]);
2836
+ if (psFallback.ok) return { ok: true };
2837
+ return {
2838
+ ok: false,
2839
+ details: extractCommandFailureDetails(psFallback) ?? "No gnome-keyring-daemon process found."
2840
+ };
2841
+ };
2842
+ const runLinuxSecretStoreChecklist = async () => {
2843
+ const gnomeKeyringInstalled = await isCommandAvailable("gnome-keyring-daemon");
2844
+ const secretToolInstalled = await isCommandAvailable("secret-tool");
2845
+ const gnomeKeyringRunning = await checkGnomeKeyringRunning();
2846
+ return [
2847
+ {
2848
+ question: "Is gnome-keyring installed?",
2849
+ ok: gnomeKeyringInstalled,
2850
+ hint: "Install the `gnome-keyring` package, then log out/in (or restart your session)."
2851
+ },
2852
+ {
2853
+ question: "Is secret-tool installed?",
2854
+ ok: secretToolInstalled,
2855
+ hint: "Install the package that provides `secret-tool` (often `libsecret-tools`)."
2856
+ },
2857
+ {
2858
+ question: "Is gnome-keyring running?",
2859
+ ok: gnomeKeyringRunning.ok,
2860
+ details: gnomeKeyringRunning.details,
2861
+ hint: "Start/unlock gnome-keyring-daemon in your session (for a quick test: `gnome-keyring-daemon --start --components=secrets`)."
2862
+ }
2863
+ ];
2864
+ };
2865
+ const maybeRunLinuxSecretStoreChecklist = async () => {
2866
+ if (!isInteractiveTerminal()) {
2867
+ process.stdout.write(" Tip: run `cdx doctor` in an interactive terminal to start guided Linux secret-store checks.\n");
2868
+ return;
2869
+ }
2870
+ const shouldRunChecklist = await p.confirm({
2871
+ message: "Run guided Linux secret-store checks now? (gnome-keyring installed, secret-tool installed, gnome-keyring running)",
2872
+ initialValue: true
2873
+ });
2874
+ if (p.isCancel(shouldRunChecklist) || !shouldRunChecklist) {
2875
+ process.stdout.write(" Guided Linux checks skipped.\n");
2876
+ return;
2877
+ }
2878
+ process.stdout.write(" Guided Linux checks:\n");
2879
+ const checklist = await runLinuxSecretStoreChecklist();
2880
+ let passed = 0;
2881
+ for (let i = 0; i < checklist.length; i++) {
2882
+ const item = checklist[i];
2883
+ if (item.ok) {
2884
+ passed += 1;
2885
+ process.stdout.write(` ${i + 1}/3 ${item.question} yes\n`);
2886
+ continue;
2887
+ }
2888
+ process.stdout.write(` ${i + 1}/3 ${item.question} no\n`);
2889
+ if (item.details) process.stdout.write(` details: ${item.details}\n`);
2890
+ if (item.hint) process.stdout.write(` hint: ${item.hint}\n`);
2891
+ }
2892
+ process.stdout.write(` Guided checklist summary: ${passed}/${checklist.length} checks passed.\n`);
2893
+ };
2723
2894
  const registerDoctorCommand = (program) => {
2724
2895
  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) => {
2725
2896
  try {
@@ -2770,6 +2941,18 @@ const registerDoctorCommand = (program) => {
2770
2941
  process.stdout.write(` Summary: ${okCount}/${status.accounts.length} configured account(s) passed secure-store load checks.\n`);
2771
2942
  }
2772
2943
  }
2944
+ const probeHeading = getSecretStoreProbeHeading(process.platform);
2945
+ if (probeHeading) {
2946
+ process.stdout.write(`\n${probeHeading}:\n`);
2947
+ const probeResult = await runSecretStoreWriteReadProbe(createProbeAdapterForCurrentPlatform());
2948
+ if (probeResult.ok) process.stdout.write(" write/read/delete probe: OK\n");
2949
+ else {
2950
+ process.stdout.write(` ⚠ ${probeResult.stage} failed: ${probeResult.error.message}\n`);
2951
+ const guidance = getSecretStoreProbeGuidance(process.platform);
2952
+ if (guidance) process.stdout.write(` ${guidance}\n`);
2953
+ if (process.platform === "linux") await maybeRunLinuxSecretStoreChecklist();
2954
+ }
2955
+ }
2773
2956
  if (process.platform === "darwin" && !options.checkKeychainAcl) {
2774
2957
  process.stdout.write(" ┌─ Optional keychain ACL check\n");
2775
2958
  process.stdout.write(" │ Run: cdx doctor --check-keychain-acl\n");
@@ -2827,7 +3010,7 @@ const registerDoctorCommand = (program) => {
2827
3010
  const registerHelpCommand = (program) => {
2828
3011
  program.command("help").description("Show available commands and usage information").argument("[command]", "Show help for a specific command").action((commandName) => {
2829
3012
  if (commandName) {
2830
- const command = program.commands.find((entry) => entry.name() === commandName);
3013
+ const command = program.commands.find((entry) => entry.name() === commandName || entry.aliases().includes(commandName));
2831
3014
  if (command) {
2832
3015
  command.outputHelp();
2833
3016
  return;
@@ -3447,7 +3630,11 @@ const executeUpdate = async (command, args) => {
3447
3630
  });
3448
3631
  };
3449
3632
  const registerUpdateSelfCommand = (program) => {
3450
- 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) => {
3633
+ program.command("update-self").aliases([
3634
+ "self-update",
3635
+ "update",
3636
+ "updte"
3637
+ ]).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) => {
3451
3638
  try {
3452
3639
  const requestedManager = options.manager ?? "auto";
3453
3640
  if (![
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bjesuiter/codex-switcher",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "type": "module",
5
5
  "description": "CLI tool to switch between multiple OpenAI accounts for OpenCode",
6
6
  "bin": {