@bjesuiter/codex-switcher 1.8.5 → 1.8.6

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 +14 -2
  2. package/cdx.mjs +643 -98
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -6,11 +6,21 @@ Switch the coding-agents [pi](https://pi.dev/), [codex](https://developers.opena
6
6
 
7
7
  ## Latest Changes
8
8
 
9
- ### 1.8.5
9
+ ### 1.8.6
10
+
11
+ #### Features
12
+
13
+ - Add Linux `cdx keyring check` for focused `gnome-keyring` dependency/runtime checks plus a secure-store probe.
14
+ - Add Linux `cdx keyring install` to install required keyring packages on Debian/Ubuntu/Mint, with `--yes` and `--skip-check` support.
15
+ - Expand Linux `cdx doctor` secure-store remediation with deeper guidance and step-by-step follow-up checks when Secret Service/keyring access fails.
10
16
 
11
17
  #### Fixes
12
18
 
13
- - Linux `cdx doctor` guided checks now avoid false-positive `gnome-keyring-daemon` running detection by preferring `ps` command-line matching (with a `pgrep` fallback), and provide clearer guidance when basic checks pass but the secure-store probe still fails (for example locked keyring/default collection/session bus issues).
19
+ - When Linux secure-store access fails because the keyring is locked, `cdx` now prompts you to unlock it and retry instead of only surfacing a generic error.
20
+
21
+ #### Internal
22
+
23
+ - Update dependencies and tooling: `@bomb.sh/tab`, `@clack/prompts`, `@types/bun`, `@types/node`, and `tsdown`.
14
24
 
15
25
  see full changelog here: https://github.com/bjesuiter/codex-switcher/blob/main/CHANGELOG.md
16
26
 
@@ -167,6 +177,8 @@ cdx migrate-secrets
167
177
  | `cdx migrate-secrets` | Migrate macOS legacy keychain entries to cross-keychain and switch config to `auto` |
168
178
  | `cdx doctor` | Show auth file paths/state and runtime capabilities |
169
179
  | `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) |
180
+ | `cdx keyring check` | Run focused Linux gnome-keyring dependency/runtime checks and secure-store probe |
181
+ | `cdx keyring install` | Install Linux keyring dependencies on Debian/Ubuntu/Mint (`--yes`, `--skip-check`) |
170
182
  | `cdx usage` | Show usage overview for all accounts |
171
183
  | `cdx usage <account>` | Show detailed usage for a specific account |
172
184
  | `cdx update-self` | Update cdx to the latest version |
package/cdx.mjs CHANGED
@@ -13,10 +13,8 @@ import { randomBytes } from "node:crypto";
13
13
  import { Decrypter, Encrypter } from "age-encryption";
14
14
  import { generatePKCE } from "@openauthjs/openauth/pkce";
15
15
  import http from "node:http";
16
-
17
16
  //#region package.json
18
- var version = "1.8.5";
19
-
17
+ var version = "1.8.6";
20
18
  //#endregion
21
19
  //#region lib/platform/path-resolver.ts
22
20
  const envValue = (env, key) => {
@@ -61,7 +59,6 @@ const resolveRuntimePaths = (input) => {
61
59
  if (input.platform === "win32") return resolveWindowsPaths(input.env, input.homeDir);
62
60
  return resolveXdgPaths(input.env, input.homeDir, input.platform);
63
61
  };
64
-
65
62
  //#endregion
66
63
  //#region lib/paths.ts
67
64
  const toPathConfig = (paths) => ({
@@ -109,7 +106,6 @@ const createTestPaths = (testDir) => ({
109
106
  codexAuthPath: path.join(testDir, "codex", "auth.json"),
110
107
  piAuthPath: path.join(testDir, "pi", "auth.json")
111
108
  });
112
-
113
109
  //#endregion
114
110
  //#region lib/config.ts
115
111
  const isSecretStoreSelection = (value) => value === "auto" || value === "legacy-keychain";
@@ -143,7 +139,6 @@ const configExists = () => {
143
139
  const { configPath } = getPaths();
144
140
  return existsSync(configPath);
145
141
  };
146
-
147
142
  //#endregion
148
143
  //#region lib/commands/errors.ts
149
144
  const exitWithCommandError = (error) => {
@@ -151,7 +146,6 @@ const exitWithCommandError = (error) => {
151
146
  process.stderr.write(`${message}\n`);
152
147
  process.exit(1);
153
148
  };
154
-
155
149
  //#endregion
156
150
  //#region lib/auth.ts
157
151
  const readExistingJson = async (filePath) => {
@@ -231,7 +225,6 @@ const writeAllAuthFiles = async (payload) => {
231
225
  codexCleared
232
226
  };
233
227
  };
234
-
235
228
  //#endregion
236
229
  //#region lib/platform/browser.ts
237
230
  const getBrowserLauncher = (platform = process.platform, url) => {
@@ -256,7 +249,7 @@ const getBrowserLauncher = (platform = process.platform, url) => {
256
249
  label: "xdg-open"
257
250
  };
258
251
  };
259
- const isCommandAvailable$2 = (command, platform = process.platform) => {
252
+ const isCommandAvailable$3 = (command, platform = process.platform) => {
260
253
  const probe = platform === "win32" ? "where" : "which";
261
254
  return Bun.spawnSync({
262
255
  cmd: [probe, command],
@@ -269,13 +262,13 @@ const getBrowserLauncherCapability = (platform = process.platform) => {
269
262
  return {
270
263
  command: launcher.command,
271
264
  label: launcher.label,
272
- available: isCommandAvailable$2(launcher.command, platform)
265
+ available: isCommandAvailable$3(launcher.command, platform)
273
266
  };
274
267
  };
275
268
  const openBrowserUrl = (url, options = {}) => {
276
269
  const platform = options.platform ?? process.platform;
277
270
  const spawnImpl = options.spawnImpl ?? spawn;
278
- const commandAvailable = options.isCommandAvailableImpl ?? isCommandAvailable$2;
271
+ const commandAvailable = options.isCommandAvailableImpl ?? isCommandAvailable$3;
279
272
  const launcher = getBrowserLauncher(platform, url);
280
273
  if (!commandAvailable(launcher.command, platform)) return {
281
274
  ok: false,
@@ -303,10 +296,9 @@ const openBrowserUrl = (url, options = {}) => {
303
296
  };
304
297
  }
305
298
  };
306
-
307
299
  //#endregion
308
300
  //#region lib/platform/clipboard.ts
309
- const isCommandAvailable$1 = (command, platform = process.platform) => {
301
+ const isCommandAvailable$2 = (command, platform = process.platform) => {
310
302
  const probe = platform === "win32" ? "where" : "which";
311
303
  return Bun.spawnSync({
312
304
  cmd: [probe, command],
@@ -392,7 +384,7 @@ const getLocalClipboardTargets = (platform, env, commandExists) => {
392
384
  });
393
385
  return targets;
394
386
  };
395
- const resolveClipboardTargets = (context = {}, commandExists = isCommandAvailable$1) => {
387
+ const resolveClipboardTargets = (context = {}, commandExists = isCommandAvailable$2) => {
396
388
  const platform = context.platform ?? process.platform;
397
389
  const env = context.env ?? process.env;
398
390
  const isTTY = context.isTTY ?? (Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY));
@@ -433,7 +425,7 @@ const defaultRunCommand = (command, args, input) => {
433
425
  };
434
426
  };
435
427
  const tryCopyToClipboard = (text, options = {}) => {
436
- const commandExists = options.commandExistsImpl ?? isCommandAvailable$1;
428
+ const commandExists = options.commandExistsImpl ?? isCommandAvailable$2;
437
429
  const runCommand = options.runCommandImpl ?? defaultRunCommand;
438
430
  const writeStdout = options.writeStdoutImpl ?? ((chunk) => {
439
431
  process.stdout.write(chunk);
@@ -479,7 +471,7 @@ const tryCopyToClipboard = (text, options = {}) => {
479
471
  };
480
472
  };
481
473
  const escapePosixSingleQuoted = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
482
- const buildClipboardHelperCommand = (text, context = {}, commandExists = isCommandAvailable$1) => {
474
+ const buildClipboardHelperCommand = (text, context = {}, commandExists = isCommandAvailable$2) => {
483
475
  const commandTarget = resolveClipboardTargets(context, commandExists).find((target) => target.kind === "command");
484
476
  if (!commandTarget) return null;
485
477
  if (commandTarget.method === "powershell") {
@@ -489,7 +481,6 @@ const buildClipboardHelperCommand = (text, context = {}, commandExists = isComma
489
481
  if (commandTarget.method === "clip") return `printf '%s' ${escapePosixSingleQuoted(text)} | cmd /c clip`;
490
482
  return `printf '%s' ${escapePosixSingleQuoted(text)} | ${commandTarget.command} ${commandTarget.args.join(" ")}`.trim();
491
483
  };
492
-
493
484
  //#endregion
494
485
  //#region lib/keychain.ts
495
486
  const SERVICE_PREFIX$3 = "cdx-openai-";
@@ -588,11 +579,9 @@ const listKeychainAccounts = () => {
588
579
  while ((match = serviceRegex.exec(output)) !== null) if (match[1]) accounts.push(match[1]);
589
580
  return [...new Set(accounts)];
590
581
  };
591
-
592
582
  //#endregion
593
583
  //#region lib/secrets/cross-keychain-overrides.ts
594
584
  const LEGACY_MAX_PASSWORD_LENGTH = 4096;
595
- const DEFAULT_CROSS_KEYCHAIN_MAX_PASSWORD_LENGTH = 16384;
596
585
  const parseMaxPasswordLength = (value) => {
597
586
  if (!value) return null;
598
587
  const parsed = Number.parseInt(value, 10);
@@ -600,9 +589,8 @@ const parseMaxPasswordLength = (value) => {
600
589
  return parsed;
601
590
  };
602
591
  const getCrossKeychainBackendOverrides = () => {
603
- return { max_password_length: parseMaxPasswordLength(process.env.CDX_CROSS_KEYCHAIN_MAX_PASSWORD_LENGTH) ?? DEFAULT_CROSS_KEYCHAIN_MAX_PASSWORD_LENGTH };
592
+ return { max_password_length: parseMaxPasswordLength(process.env.CDX_CROSS_KEYCHAIN_MAX_PASSWORD_LENGTH) ?? 16384 };
604
593
  };
605
-
606
594
  //#endregion
607
595
  //#region lib/secrets/fallback-consent.ts
608
596
  const CONSENT_FILE = "secure-store-fallback-consent.json";
@@ -654,7 +642,6 @@ const ensureFallbackConsent = async (scope, warningMessage) => {
654
642
  accepted[scope] = { acceptedAt: (/* @__PURE__ */ new Date()).toISOString() };
655
643
  await saveConsentMap(accepted);
656
644
  };
657
-
658
645
  //#endregion
659
646
  //#region lib/secrets/linux-cross-keychain.ts
660
647
  const SERVICE_PREFIX$2 = "cdx-openai-";
@@ -670,6 +657,7 @@ const STORE_UNAVAILABLE_MARKERS = [
670
657
  "unable to initialize linux secure-store backend",
671
658
  "no keyring backend could be initialized",
672
659
  "native keyring module not available",
660
+ "linux secure store is unavailable",
673
661
  "secret service operation failed",
674
662
  "couldn't access platform secure storage",
675
663
  "dbus",
@@ -759,6 +747,82 @@ const withService$1 = async (accountId, run, options = {}) => {
759
747
  throw error;
760
748
  }
761
749
  };
750
+ const isInteractiveTerminal$2 = () => Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
751
+ const applyEnvAssignments$1 = (raw) => {
752
+ const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
753
+ for (const line of lines) {
754
+ const match = line.match(/^([A-Z0-9_]+)=(.*);?$/);
755
+ if (!match) continue;
756
+ const key = match[1];
757
+ const value = match[2].replace(/;$/, "");
758
+ if (key) process.env[key] = value;
759
+ }
760
+ };
761
+ const runCommandWithInput = async (command, args, input) => await new Promise((resolve) => {
762
+ const child = spawn(command, args, { stdio: [
763
+ "pipe",
764
+ "pipe",
765
+ "pipe"
766
+ ] });
767
+ let stdout = "";
768
+ let stderr = "";
769
+ let spawnError = null;
770
+ child.stdout?.on("data", (chunk) => {
771
+ stdout += chunk.toString();
772
+ });
773
+ child.stderr?.on("data", (chunk) => {
774
+ stderr += chunk.toString();
775
+ });
776
+ child.once("error", (error) => {
777
+ spawnError = error.message;
778
+ });
779
+ child.once("close", (code) => {
780
+ resolve({
781
+ ok: spawnError === null && code === 0,
782
+ stdout: stdout.trim(),
783
+ stderr: stderr.trim(),
784
+ ...spawnError ? { error: spawnError } : {}
785
+ });
786
+ });
787
+ child.stdin?.write(input);
788
+ child.stdin?.end();
789
+ });
790
+ const attemptInteractiveLinuxKeyringUnlock = async () => {
791
+ if (!isInteractiveTerminal$2()) return false;
792
+ const shouldUnlock = await p.confirm({
793
+ message: "Linux keyring appears locked. Unlock it now?",
794
+ initialValue: true
795
+ });
796
+ if (p.isCancel(shouldUnlock) || !shouldUnlock) return false;
797
+ const passphrase = await p.password({
798
+ message: "Enter Linux keyring password:",
799
+ validate: (value) => {
800
+ if (!value || !value.trim()) return "Password is required to unlock keyring.";
801
+ }
802
+ });
803
+ if (p.isCancel(passphrase) || !passphrase) return false;
804
+ const result = await runCommandWithInput("gnome-keyring-daemon", ["--unlock", "--components=secrets"], `${passphrase}\n`);
805
+ if (!result.ok) {
806
+ const details = result.error || result.stderr || result.stdout;
807
+ if (details) process.stderr.write(`cdx: keyring unlock failed (${details})\n`);
808
+ return false;
809
+ }
810
+ applyEnvAssignments$1(result.stdout);
811
+ return true;
812
+ };
813
+ const withLinuxUnlockRetry = async (run, options = {}) => {
814
+ let unlockAttempted = false;
815
+ while (true) try {
816
+ return await run();
817
+ } catch (error) {
818
+ const kind = classifyLinuxSecureStoreError(error);
819
+ if (!(!unlockAttempted && (kind === "store_unavailable" || kind === "missing_entry" && options.forWrite && options.retryOnMissingEntryForNativeWrite && selectedBackend$2 === "native-linux"))) throw error;
820
+ unlockAttempted = true;
821
+ if (!await attemptInteractiveLinuxKeyringUnlock()) throw error;
822
+ backendInitPromise$2 = null;
823
+ selectedBackend$2 = null;
824
+ }
825
+ };
762
826
  const trySaveWithSecretServiceFallback = async (accountId, serializedPayload) => {
763
827
  if (!await trySwitchBackend("secret-service", { forWrite: true })) return {
764
828
  ok: false,
@@ -777,7 +841,10 @@ const trySaveWithSecretServiceFallback = async (accountId, serializedPayload) =>
777
841
  const saveLinuxCrossKeychainPayload = async (accountId, payload) => {
778
842
  const serialized = JSON.stringify(payload);
779
843
  try {
780
- await withService$1(accountId, (service) => setPassword(service, accountId, serialized), { forWrite: true });
844
+ await withLinuxUnlockRetry(() => withService$1(accountId, (service) => setPassword(service, accountId, serialized), { forWrite: true }), {
845
+ forWrite: true,
846
+ retryOnMissingEntryForNativeWrite: true
847
+ });
781
848
  return;
782
849
  } catch (error) {
783
850
  const kind = classifyLinuxSecureStoreError(error);
@@ -792,7 +859,7 @@ const saveLinuxCrossKeychainPayload = async (accountId, payload) => {
792
859
  };
793
860
  const loadLinuxCrossKeychainPayload = async (accountId) => {
794
861
  try {
795
- const raw = await withService$1(accountId, (service) => getPassword(service, accountId));
862
+ const raw = await withLinuxUnlockRetry(() => withService$1(accountId, (service) => getPassword(service, accountId)));
796
863
  if (raw === null) throw new Error(`No stored credentials found for account ${accountId}.`);
797
864
  return parsePayload$2(accountId, raw);
798
865
  } catch (error) {
@@ -802,7 +869,7 @@ const loadLinuxCrossKeychainPayload = async (accountId) => {
802
869
  };
803
870
  const deleteLinuxCrossKeychainPayload = async (accountId) => {
804
871
  try {
805
- await withService$1(accountId, (service) => deletePassword(service, accountId));
872
+ await withLinuxUnlockRetry(() => withService$1(accountId, (service) => deletePassword(service, accountId)));
806
873
  } catch (error) {
807
874
  if (classifyLinuxSecureStoreError(error) === "missing_entry") return;
808
875
  throw error;
@@ -810,13 +877,12 @@ const deleteLinuxCrossKeychainPayload = async (accountId) => {
810
877
  };
811
878
  const linuxCrossKeychainPayloadExists = async (accountId) => {
812
879
  try {
813
- return await withService$1(accountId, async (service) => await getPassword(service, accountId) !== null);
880
+ return await withLinuxUnlockRetry(() => withService$1(accountId, async (service) => await getPassword(service, accountId) !== null));
814
881
  } catch (error) {
815
882
  if (classifyLinuxSecureStoreError(error) === "missing_entry") return false;
816
883
  throw error;
817
884
  }
818
885
  };
819
-
820
886
  //#endregion
821
887
  //#region lib/secrets/macos-cross-keychain.ts
822
888
  const SERVICE_PREFIX$1 = "cdx-openai-";
@@ -881,7 +947,6 @@ const loadMacOSCrossKeychainPayload = async (accountId) => {
881
947
  };
882
948
  const deleteMacOSCrossKeychainPayload = async (accountId) => withService(accountId, (service) => deletePassword(service, accountId));
883
949
  const macosCrossKeychainPayloadExists = async (accountId) => withService(accountId, async (service) => await getPassword(service, accountId) !== null);
884
-
885
950
  //#endregion
886
951
  //#region lib/secrets/windows-cross-keychain.ts
887
952
  const SERVICE_PREFIX = "cdx-openai-";
@@ -1059,7 +1124,6 @@ const windowsCrossKeychainPayloadExists = async (accountId) => withWindowsBacken
1059
1124
  }
1060
1125
  return await loadLegacyPayload(accountId) !== null;
1061
1126
  });
1062
-
1063
1127
  //#endregion
1064
1128
  //#region lib/secrets/store.ts
1065
1129
  const MISSING_SECRET_STORE_ERROR_MARKERS = [
@@ -1287,7 +1351,6 @@ const getSecretStoreCapability = () => {
1287
1351
  ...capability.reason ? { reason: capability.reason } : {}
1288
1352
  };
1289
1353
  };
1290
-
1291
1354
  //#endregion
1292
1355
  //#region lib/oauth/constants.ts
1293
1356
  const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
@@ -1297,7 +1360,6 @@ const TOKEN_URL = "https://auth.openai.com/oauth/token";
1297
1360
  const REDIRECT_URI = "http://localhost:1455/auth/callback";
1298
1361
  const SCOPE = "openid profile email offline_access";
1299
1362
  const CALLBACK_PORT = 1455;
1300
-
1301
1363
  //#endregion
1302
1364
  //#region lib/oauth/auth.ts
1303
1365
  const createState = () => {
@@ -1530,7 +1592,6 @@ const extractAccountId = (accessToken) => {
1530
1592
  if (authClaim?.user_id) return authClaim.user_id;
1531
1593
  return payload.sub ?? null;
1532
1594
  };
1533
-
1534
1595
  //#endregion
1535
1596
  //#region lib/oauth/server.ts
1536
1597
  const AUTH_TIMEOUT_MS = 300 * 1e3;
@@ -1628,7 +1689,6 @@ const startOAuthServer = (state) => {
1628
1689
  });
1629
1690
  });
1630
1691
  };
1631
-
1632
1692
  //#endregion
1633
1693
  //#region lib/oauth/login.ts
1634
1694
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -2149,7 +2209,6 @@ const performLogin = async (options = {}) => {
2149
2209
  p.outro("You can now use 'cdx switch' to activate this account.");
2150
2210
  return { accountId };
2151
2211
  };
2152
-
2153
2212
  //#endregion
2154
2213
  //#region lib/refresh.ts
2155
2214
  const writeActiveAuthFilesIfCurrent = async (accountId) => {
@@ -2159,7 +2218,6 @@ const writeActiveAuthFilesIfCurrent = async (accountId) => {
2159
2218
  if (!current || current.accountId !== accountId) return null;
2160
2219
  return writeAllAuthFiles(await getSecretStoreAdapter().load(accountId));
2161
2220
  };
2162
-
2163
2221
  //#endregion
2164
2222
  //#region lib/platform/capabilities.ts
2165
2223
  const getRuntimeCapabilities = () => {
@@ -2171,7 +2229,6 @@ const getRuntimeCapabilities = () => {
2171
2229
  browserLauncher: getBrowserLauncherCapability(process.platform)
2172
2230
  };
2173
2231
  };
2174
-
2175
2232
  //#endregion
2176
2233
  //#region lib/status.ts
2177
2234
  const formatDuration = (ms) => {
@@ -2299,7 +2356,6 @@ const getStatus = async () => {
2299
2356
  capabilities: getRuntimeCapabilities()
2300
2357
  };
2301
2358
  };
2302
-
2303
2359
  //#endregion
2304
2360
  //#region lib/interactive.ts
2305
2361
  const getAccountDisplay = (accountId, isCurrent, label) => {
@@ -2630,7 +2686,6 @@ const runInteractiveMode = async () => {
2630
2686
  }
2631
2687
  p.outro("Goodbye!");
2632
2688
  };
2633
-
2634
2689
  //#endregion
2635
2690
  //#region lib/commands/interactive.ts
2636
2691
  const registerDefaultInteractiveAction = (program) => {
@@ -2642,7 +2697,6 @@ const registerDefaultInteractiveAction = (program) => {
2642
2697
  }
2643
2698
  });
2644
2699
  };
2645
-
2646
2700
  //#endregion
2647
2701
  //#region lib/keychain-acl.ts
2648
2702
  const getDefaultMap = (services) => {
@@ -2714,7 +2768,6 @@ const getKeychainDecryptAccessByServiceAsync = async (services) => {
2714
2768
  if (!dumpResult.success) return defaultResult;
2715
2769
  return parseKeychainDecryptAccessFromDump(dumpResult.output, dedupedServices);
2716
2770
  };
2717
-
2718
2771
  //#endregion
2719
2772
  //#region lib/secrets/probe.ts
2720
2773
  const toError = (error) => error instanceof Error ? error : new Error(String(error));
@@ -2764,7 +2817,6 @@ const runSecretStoreWriteReadProbe = async (secretStore, options = {}) => {
2764
2817
  }
2765
2818
  return result;
2766
2819
  };
2767
-
2768
2820
  //#endregion
2769
2821
  //#region lib/commands/doctor.ts
2770
2822
  const hasRuntimeTrustedApp = (trustedApplications, runtimeExecutablePath) => {
@@ -2791,8 +2843,8 @@ const getSecretStoreProbeGuidance = (platform) => {
2791
2843
  if (platform === "win32") return "Suggested fix: ensure Windows Credential Manager is available for this user session, then retry login.";
2792
2844
  return null;
2793
2845
  };
2794
- const isInteractiveTerminal = () => Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
2795
- const runCommandCapture = async (command, args) => await new Promise((resolve) => {
2846
+ const isInteractiveTerminal$1 = () => Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
2847
+ const runCommandCapture$1 = async (command, args) => await new Promise((resolve) => {
2796
2848
  const child = spawn(command, args, { stdio: [
2797
2849
  "ignore",
2798
2850
  "pipe",
@@ -2819,32 +2871,75 @@ const runCommandCapture = async (command, args) => await new Promise((resolve) =
2819
2871
  });
2820
2872
  });
2821
2873
  });
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;
2874
+ const runCommandCaptureWithInput = async (command, args, input) => await new Promise((resolve) => {
2875
+ const child = spawn(command, args, { stdio: [
2876
+ "pipe",
2877
+ "pipe",
2878
+ "pipe"
2879
+ ] });
2880
+ let stdout = "";
2881
+ let stderr = "";
2882
+ let spawnError = null;
2883
+ child.stdout?.on("data", (chunk) => {
2884
+ stdout += chunk.toString();
2885
+ });
2886
+ child.stderr?.on("data", (chunk) => {
2887
+ stderr += chunk.toString();
2888
+ });
2889
+ child.once("error", (error) => {
2890
+ spawnError = error.message;
2891
+ });
2892
+ child.stdin?.write(input);
2893
+ child.stdin?.end();
2894
+ child.once("close", (code) => {
2895
+ resolve({
2896
+ ok: spawnError === null && code === 0,
2897
+ stdout: stdout.trim(),
2898
+ stderr: stderr.trim(),
2899
+ ...spawnError ? { error: spawnError } : {}
2900
+ });
2901
+ });
2902
+ });
2903
+ const runCommandDetached = async (command, args) => {
2904
+ try {
2905
+ spawn(command, args, {
2906
+ detached: true,
2907
+ stdio: "ignore"
2908
+ }).unref();
2909
+ return { ok: true };
2910
+ } catch (error) {
2911
+ return {
2912
+ ok: false,
2913
+ error: error instanceof Error ? error.message : String(error)
2914
+ };
2915
+ }
2825
2916
  };
2826
- const GNOME_KEYRING_CMDLINE_PATTERN = /(^|\/)gnome-keyring-daemon(\s|$)/;
2827
- const checkGnomeKeyringRunning = async () => {
2828
- if (await isCommandAvailable("ps")) {
2829
- const psResult = await runCommandCapture("ps", [
2917
+ const extractCommandFailureDetails$1 = (result) => result.error || result.stderr || result.stdout || void 0;
2918
+ const isCommandAvailable$1 = async (commandName) => {
2919
+ return (await runCommandCapture$1("sh", ["-lc", `command -v ${commandName} >/dev/null 2>&1`])).ok;
2920
+ };
2921
+ const GNOME_KEYRING_CMDLINE_PATTERN$1 = /(^|\/)gnome-keyring-daemon(\s|$)/;
2922
+ const checkGnomeKeyringRunning$1 = async () => {
2923
+ if (await isCommandAvailable$1("ps")) {
2924
+ const psResult = await runCommandCapture$1("ps", [
2830
2925
  "-A",
2831
2926
  "-o",
2832
2927
  "args="
2833
2928
  ]);
2834
2929
  if (psResult.ok) {
2835
- if (psResult.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).some((line) => GNOME_KEYRING_CMDLINE_PATTERN.test(line))) return { ok: true };
2930
+ if (psResult.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).some((line) => GNOME_KEYRING_CMDLINE_PATTERN$1.test(line))) return { ok: true };
2836
2931
  return {
2837
2932
  ok: false,
2838
2933
  details: "No gnome-keyring-daemon process found."
2839
2934
  };
2840
2935
  }
2841
2936
  }
2842
- if (await isCommandAvailable("pgrep")) {
2843
- const pgrepResult = await runCommandCapture("pgrep", ["-f", "gnome-keyring-daemon"]);
2937
+ if (await isCommandAvailable$1("pgrep")) {
2938
+ const pgrepResult = await runCommandCapture$1("pgrep", ["-f", "gnome-keyring-daemon"]);
2844
2939
  if (pgrepResult.ok) return { ok: true };
2845
2940
  return {
2846
2941
  ok: false,
2847
- details: extractCommandFailureDetails(pgrepResult) ?? "No gnome-keyring-daemon process found."
2942
+ details: extractCommandFailureDetails$1(pgrepResult) ?? "No gnome-keyring-daemon process found."
2848
2943
  };
2849
2944
  }
2850
2945
  return {
@@ -2852,7 +2947,7 @@ const checkGnomeKeyringRunning = async () => {
2852
2947
  details: "Neither ps nor pgrep is available to check gnome-keyring-daemon."
2853
2948
  };
2854
2949
  };
2855
- const parseSystemctlEnabledState = (output) => {
2950
+ const parseSystemctlEnabledState$1 = (output) => {
2856
2951
  const normalized = output.trim().toLowerCase();
2857
2952
  if ([
2858
2953
  "enabled",
@@ -2870,8 +2965,8 @@ const parseSystemctlEnabledState = (output) => {
2870
2965
  ].includes(normalized)) return "disabled";
2871
2966
  return "unknown";
2872
2967
  };
2873
- const getLinuxGnomeKeyringAutoStartStatus = async () => {
2874
- if (!await isCommandAvailable("systemctl")) return {
2968
+ const getLinuxGnomeKeyringAutoStartStatus$1 = async () => {
2969
+ if (!await isCommandAvailable$1("systemctl")) return {
2875
2970
  state: "unknown",
2876
2971
  details: "systemctl is not available; autostart detection depends on your desktop/session config."
2877
2972
  };
@@ -2879,12 +2974,12 @@ const getLinuxGnomeKeyringAutoStartStatus = async () => {
2879
2974
  let sawDisabled = false;
2880
2975
  const details = [];
2881
2976
  for (const unit of units) {
2882
- const result = await runCommandCapture("systemctl", [
2977
+ const result = await runCommandCapture$1("systemctl", [
2883
2978
  "--user",
2884
2979
  "is-enabled",
2885
2980
  unit
2886
2981
  ]);
2887
- const state = parseSystemctlEnabledState(result.stdout || result.stderr);
2982
+ const state = parseSystemctlEnabledState$1(result.stdout || result.stderr);
2888
2983
  if (state === "enabled") return {
2889
2984
  state: "enabled",
2890
2985
  details: `${unit} is enabled (${result.stdout || "enabled"}).`
@@ -2894,7 +2989,7 @@ const getLinuxGnomeKeyringAutoStartStatus = async () => {
2894
2989
  details.push(`${unit}: ${result.stdout || result.stderr || "disabled"}`);
2895
2990
  continue;
2896
2991
  }
2897
- const maybeDetail = extractCommandFailureDetails(result);
2992
+ const maybeDetail = extractCommandFailureDetails$1(result);
2898
2993
  if (maybeDetail) details.push(`${unit}: ${maybeDetail}`);
2899
2994
  }
2900
2995
  if (sawDisabled) return {
@@ -2916,25 +3011,25 @@ const applyKeyringEnvAssignments = (raw) => {
2916
3011
  if (key) process.env[key] = value;
2917
3012
  }
2918
3013
  };
2919
- const startGnomeKeyringNow = async () => {
2920
- const directStart = await runCommandCapture("gnome-keyring-daemon", ["--start", "--components=secrets"]);
3014
+ const startGnomeKeyringNow$1 = async () => {
3015
+ const directStart = await runCommandCapture$1("gnome-keyring-daemon", ["--start", "--components=secrets"]);
2921
3016
  if (directStart.ok) {
2922
3017
  applyKeyringEnvAssignments(directStart.stdout);
2923
- const runningCheck = await checkGnomeKeyringRunning();
3018
+ const runningCheck = await checkGnomeKeyringRunning$1();
2924
3019
  if (runningCheck.ok) return { ok: true };
2925
3020
  return {
2926
3021
  ok: false,
2927
3022
  details: runningCheck.details ?? "gnome-keyring-daemon start command succeeded, but process was not detected afterwards."
2928
3023
  };
2929
3024
  }
2930
- if (await isCommandAvailable("systemctl")) {
2931
- const serviceStart = await runCommandCapture("systemctl", [
3025
+ if (await isCommandAvailable$1("systemctl")) {
3026
+ const serviceStart = await runCommandCapture$1("systemctl", [
2932
3027
  "--user",
2933
3028
  "start",
2934
3029
  "gnome-keyring-daemon.service"
2935
3030
  ]);
2936
3031
  if (serviceStart.ok) {
2937
- const runningCheck = await checkGnomeKeyringRunning();
3032
+ const runningCheck = await checkGnomeKeyringRunning$1();
2938
3033
  if (runningCheck.ok) return { ok: true };
2939
3034
  return {
2940
3035
  ok: false,
@@ -2943,23 +3038,23 @@ const startGnomeKeyringNow = async () => {
2943
3038
  }
2944
3039
  return {
2945
3040
  ok: false,
2946
- details: extractCommandFailureDetails(directStart) ?? extractCommandFailureDetails(serviceStart) ?? "Failed to start gnome-keyring-daemon."
3041
+ details: extractCommandFailureDetails$1(directStart) ?? extractCommandFailureDetails$1(serviceStart) ?? "Failed to start gnome-keyring-daemon."
2947
3042
  };
2948
3043
  }
2949
3044
  return {
2950
3045
  ok: false,
2951
- details: extractCommandFailureDetails(directStart) ?? "Failed to start gnome-keyring-daemon."
3046
+ details: extractCommandFailureDetails$1(directStart) ?? "Failed to start gnome-keyring-daemon."
2952
3047
  };
2953
3048
  };
2954
3049
  const enableGnomeKeyringAutoStart = async () => {
2955
- if (!await isCommandAvailable("systemctl")) return {
3050
+ if (!await isCommandAvailable$1("systemctl")) return {
2956
3051
  ok: false,
2957
3052
  details: "systemctl is not available, so cdx cannot automatically enable startup in this session manager."
2958
3053
  };
2959
3054
  const units = ["gnome-keyring-daemon.socket", "gnome-keyring-daemon.service"];
2960
3055
  const failures = [];
2961
3056
  for (const unit of units) {
2962
- const result = await runCommandCapture("systemctl", [
3057
+ const result = await runCommandCapture$1("systemctl", [
2963
3058
  "--user",
2964
3059
  "enable",
2965
3060
  unit
@@ -2968,7 +3063,7 @@ const enableGnomeKeyringAutoStart = async () => {
2968
3063
  ok: true,
2969
3064
  details: `${unit} enabled.`
2970
3065
  };
2971
- const detail = extractCommandFailureDetails(result);
3066
+ const detail = extractCommandFailureDetails$1(result);
2972
3067
  failures.push(`${unit}: ${detail ?? "enable failed"}`);
2973
3068
  }
2974
3069
  return {
@@ -2977,14 +3072,14 @@ const enableGnomeKeyringAutoStart = async () => {
2977
3072
  };
2978
3073
  };
2979
3074
  const maybeOfferToStartGnomeKeyring = async () => {
2980
- const autoStartStatus = await getLinuxGnomeKeyringAutoStartStatus();
3075
+ const autoStartStatus = await getLinuxGnomeKeyringAutoStartStatus$1();
2981
3076
  if (autoStartStatus.state === "enabled") {
2982
3077
  const shouldStartNow = await p.confirm({
2983
3078
  message: "gnome-keyring autostart appears enabled, but it is not running right now. Start it now?",
2984
3079
  initialValue: true
2985
3080
  });
2986
3081
  if (p.isCancel(shouldStartNow) || !shouldStartNow) return false;
2987
- const startResult = await startGnomeKeyringNow();
3082
+ const startResult = await startGnomeKeyringNow$1();
2988
3083
  if (!startResult.ok) {
2989
3084
  process.stdout.write(` failed to start gnome-keyring-daemon: ${startResult.details ?? "unknown error"}\n`);
2990
3085
  return false;
@@ -3020,7 +3115,7 @@ const maybeOfferToStartGnomeKeyring = async () => {
3020
3115
  }
3021
3116
  process.stdout.write(` autostart enabled${enableResult.details ? ` (${enableResult.details})` : ""}.\n`);
3022
3117
  }
3023
- const startResult = await startGnomeKeyringNow();
3118
+ const startResult = await startGnomeKeyringNow$1();
3024
3119
  if (!startResult.ok) {
3025
3120
  process.stdout.write(` failed to start gnome-keyring-daemon: ${startResult.details ?? "unknown error"}\n`);
3026
3121
  return false;
@@ -3029,9 +3124,9 @@ const maybeOfferToStartGnomeKeyring = async () => {
3029
3124
  return true;
3030
3125
  };
3031
3126
  const runLinuxSecretStoreChecklist = async () => {
3032
- const gnomeKeyringInstalled = await isCommandAvailable("gnome-keyring-daemon");
3033
- const secretToolInstalled = await isCommandAvailable("secret-tool");
3034
- const gnomeKeyringRunning = await checkGnomeKeyringRunning();
3127
+ const gnomeKeyringInstalled = await isCommandAvailable$1("gnome-keyring-daemon");
3128
+ const secretToolInstalled = await isCommandAvailable$1("secret-tool");
3129
+ const gnomeKeyringRunning = await checkGnomeKeyringRunning$1();
3035
3130
  return [
3036
3131
  {
3037
3132
  id: "gnome-keyring-installed",
@@ -3050,12 +3145,118 @@ const runLinuxSecretStoreChecklist = async () => {
3050
3145
  question: "Is gnome-keyring running?",
3051
3146
  ok: gnomeKeyringRunning.ok,
3052
3147
  details: gnomeKeyringRunning.details,
3053
- hint: "Start/unlock gnome-keyring-daemon in your session (for a quick test: `gnome-keyring-daemon --start --components=secrets`)."
3148
+ hint: "Start/unlock gnome-keyring-daemon in your session (cdx can do this interactively)."
3054
3149
  }
3055
3150
  ];
3056
3151
  };
3152
+ const runSecretToolRoundTripCheck = async () => {
3153
+ const id = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
3154
+ const service = `cdx-doctor-secret-service-${id}`;
3155
+ const account = `cdx-doctor-account-${id}`;
3156
+ const value = `cdx-doctor-value-${id}`;
3157
+ const storeResult = await runCommandCaptureWithInput("secret-tool", [
3158
+ "store",
3159
+ "--label=cdx doctor Secret Service probe",
3160
+ "service",
3161
+ service,
3162
+ "account",
3163
+ account
3164
+ ], `${value}\n`);
3165
+ if (!storeResult.ok) return {
3166
+ ok: false,
3167
+ details: extractCommandFailureDetails$1(storeResult) ?? "secret-tool store failed."
3168
+ };
3169
+ const lookupResult = await runCommandCapture$1("secret-tool", [
3170
+ "lookup",
3171
+ "service",
3172
+ service,
3173
+ "account",
3174
+ account
3175
+ ]);
3176
+ if (!lookupResult.ok) return {
3177
+ ok: false,
3178
+ details: extractCommandFailureDetails$1(lookupResult) ?? "secret-tool lookup failed."
3179
+ };
3180
+ if (lookupResult.stdout.trim() !== value) return {
3181
+ ok: false,
3182
+ details: "secret-tool lookup returned an unexpected value after store."
3183
+ };
3184
+ const clearResult = await runCommandCapture$1("secret-tool", [
3185
+ "clear",
3186
+ "service",
3187
+ service,
3188
+ "account",
3189
+ account
3190
+ ]);
3191
+ if (!clearResult.ok) return {
3192
+ ok: false,
3193
+ details: extractCommandFailureDetails$1(clearResult) ?? "secret-tool clear failed."
3194
+ };
3195
+ return { ok: true };
3196
+ };
3197
+ const runLinuxSecretStoreDeepRemediation = async () => {
3198
+ if (!isInteractiveTerminal$1()) return;
3199
+ const hasSecretTool = await isCommandAvailable$1("secret-tool");
3200
+ const hasSeahorse = await isCommandAvailable$1("seahorse");
3201
+ const shouldStart = await p.confirm({
3202
+ message: "Run deeper Linux Secret Service remediation now? (interactive actions, no copy/paste commands)",
3203
+ initialValue: true
3204
+ });
3205
+ if (p.isCancel(shouldStart) || !shouldStart) return;
3206
+ while (true) {
3207
+ const action = await p.select({
3208
+ message: "Choose next remediation action:",
3209
+ options: [
3210
+ {
3211
+ value: "secret-tool-test",
3212
+ label: hasSecretTool ? "Run Secret Service write/read/clear test now" : "Run Secret Service write/read/clear test now (secret-tool not found)"
3213
+ },
3214
+ {
3215
+ value: "open-keyring-manager",
3216
+ label: hasSeahorse ? "Open keyring manager now" : "Open keyring manager now (seahorse not found)"
3217
+ },
3218
+ {
3219
+ value: "retry-probe",
3220
+ label: "Retry cdx secure-store probe now"
3221
+ },
3222
+ {
3223
+ value: "done",
3224
+ label: "Done"
3225
+ }
3226
+ ],
3227
+ initialValue: "secret-tool-test"
3228
+ });
3229
+ if (p.isCancel(action) || action === "done") return;
3230
+ if (action === "secret-tool-test") {
3231
+ if (!hasSecretTool) {
3232
+ process.stdout.write(" secret-tool is not installed; install it first, then rerun this remediation action.\n");
3233
+ continue;
3234
+ }
3235
+ const result = await runSecretToolRoundTripCheck();
3236
+ if (result.ok) process.stdout.write(" secret-tool roundtrip test passed.\n");
3237
+ else process.stdout.write(` secret-tool roundtrip test failed: ${result.details ?? "unknown error"}\n`);
3238
+ continue;
3239
+ }
3240
+ if (action === "open-keyring-manager") {
3241
+ if (!hasSeahorse) {
3242
+ process.stdout.write(" keyring manager app was not found (seahorse). Install it to manage collections/locks interactively.\n");
3243
+ continue;
3244
+ }
3245
+ const opened = await runCommandDetached("seahorse", []);
3246
+ if (!opened.ok) process.stdout.write(` failed to open keyring manager: ${opened.error ?? "unknown error"}\n`);
3247
+ else process.stdout.write(" opened keyring manager. Unlock/create the default keyring, then return here and retry probe.\n");
3248
+ continue;
3249
+ }
3250
+ const probeResult = await runSecretStoreWriteReadProbe(createProbeAdapterForCurrentPlatform());
3251
+ if (probeResult.ok) {
3252
+ process.stdout.write(" cdx secure-store probe now passes.\n");
3253
+ return;
3254
+ }
3255
+ process.stdout.write(` probe still failing (${probeResult.stage}): ${probeResult.error.message}\n`);
3256
+ }
3257
+ };
3057
3258
  const maybeRunLinuxSecretStoreChecklist = async () => {
3058
- if (!isInteractiveTerminal()) {
3259
+ if (!isInteractiveTerminal$1()) {
3059
3260
  process.stdout.write(" Tip: run `cdx doctor` in an interactive terminal to start guided Linux secret-store checks.\n");
3060
3261
  return;
3061
3262
  }
@@ -3082,7 +3283,7 @@ const maybeRunLinuxSecretStoreChecklist = async () => {
3082
3283
  if (item.hint) process.stdout.write(` hint: ${item.hint}\n`);
3083
3284
  if (item.id === "gnome-keyring-running") {
3084
3285
  if (await maybeOfferToStartGnomeKeyring()) {
3085
- const runningNow = await checkGnomeKeyringRunning();
3286
+ const runningNow = await checkGnomeKeyringRunning$1();
3086
3287
  if (runningNow.ok) {
3087
3288
  passed += 1;
3088
3289
  process.stdout.write(" re-check: gnome-keyring-daemon is now running.\n");
@@ -3091,7 +3292,10 @@ const maybeRunLinuxSecretStoreChecklist = async () => {
3091
3292
  }
3092
3293
  }
3093
3294
  process.stdout.write(` Guided checklist summary: ${passed}/${checklist.length} checks passed.\n`);
3094
- if (passed === checklist.length) process.stdout.write(" Note: basic checks passed, but secure-store probe still failed. This can happen when the keyring is locked, has no default collection, or your D-Bus/session setup prevents Secret Service writes.\n");
3295
+ if (passed === checklist.length) {
3296
+ process.stdout.write(" Note: basic checks passed, but secure-store probe still failed. This can happen when the keyring is locked, has no default collection, or your D-Bus/session setup prevents Secret Service writes.\n");
3297
+ await runLinuxSecretStoreDeepRemediation();
3298
+ }
3095
3299
  };
3096
3300
  const registerDoctorCommand = (program) => {
3097
3301
  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) => {
@@ -3206,7 +3410,6 @@ const registerDoctorCommand = (program) => {
3206
3410
  }
3207
3411
  });
3208
3412
  };
3209
-
3210
3413
  //#endregion
3211
3414
  //#region lib/commands/help.ts
3212
3415
  const registerHelpCommand = (program) => {
@@ -3224,7 +3427,362 @@ const registerHelpCommand = (program) => {
3224
3427
  program.outputHelp();
3225
3428
  });
3226
3429
  };
3227
-
3430
+ //#endregion
3431
+ //#region lib/commands/keyring.ts
3432
+ const APT_INSTALL_COMMAND = "sudo apt-get update && sudo apt-get install -y gnome-keyring libsecret-tools dbus-user-session xdg-utils libpam-gnome-keyring";
3433
+ const GNOME_KEYRING_CMDLINE_PATTERN = /(^|\/)gnome-keyring-daemon(\s|$)/;
3434
+ const isInteractiveTerminal = () => Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
3435
+ const runCommandCapture = async (command, args) => await new Promise((resolve) => {
3436
+ const child = spawn(command, args, { stdio: [
3437
+ "ignore",
3438
+ "pipe",
3439
+ "pipe"
3440
+ ] });
3441
+ let stdout = "";
3442
+ let stderr = "";
3443
+ let spawnError = null;
3444
+ child.stdout?.on("data", (chunk) => {
3445
+ stdout += chunk.toString();
3446
+ });
3447
+ child.stderr?.on("data", (chunk) => {
3448
+ stderr += chunk.toString();
3449
+ });
3450
+ child.once("error", (error) => {
3451
+ spawnError = error.message;
3452
+ });
3453
+ child.once("close", (code) => {
3454
+ resolve({
3455
+ ok: spawnError === null && code === 0,
3456
+ stdout: stdout.trim(),
3457
+ stderr: stderr.trim(),
3458
+ ...spawnError ? { error: spawnError } : {}
3459
+ });
3460
+ });
3461
+ });
3462
+ const runCommandInherit = async (command, args) => await new Promise((resolve) => {
3463
+ const child = spawn(command, args, { stdio: "inherit" });
3464
+ child.once("error", () => resolve(1));
3465
+ child.once("close", (code) => resolve(code ?? 1));
3466
+ });
3467
+ const extractCommandFailureDetails = (result) => result.error || result.stderr || result.stdout || void 0;
3468
+ const isCommandAvailable = async (commandName) => {
3469
+ return (await runCommandCapture("sh", ["-lc", `command -v ${commandName} >/dev/null 2>&1`])).ok;
3470
+ };
3471
+ const parseSystemctlEnabledState = (output) => {
3472
+ const normalized = output.trim().toLowerCase();
3473
+ if ([
3474
+ "enabled",
3475
+ "enabled-runtime",
3476
+ "static",
3477
+ "indirect",
3478
+ "generated"
3479
+ ].includes(normalized)) return "enabled";
3480
+ if ([
3481
+ "disabled",
3482
+ "masked",
3483
+ "not-found",
3484
+ "linked",
3485
+ "linked-runtime"
3486
+ ].includes(normalized)) return "disabled";
3487
+ return "unknown";
3488
+ };
3489
+ const applyEnvAssignments = (raw) => {
3490
+ const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
3491
+ for (const line of lines) {
3492
+ const match = line.match(/^([A-Z0-9_]+)=(.*);?$/);
3493
+ if (!match) continue;
3494
+ const key = match[1];
3495
+ const value = match[2].replace(/;$/, "");
3496
+ if (key) process.env[key] = value;
3497
+ }
3498
+ };
3499
+ const checkGnomeKeyringRunning = async () => {
3500
+ if (await isCommandAvailable("ps")) {
3501
+ const psResult = await runCommandCapture("ps", [
3502
+ "-A",
3503
+ "-o",
3504
+ "args="
3505
+ ]);
3506
+ if (psResult.ok) {
3507
+ if (psResult.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).some((line) => GNOME_KEYRING_CMDLINE_PATTERN.test(line))) return { ok: true };
3508
+ return {
3509
+ ok: false,
3510
+ details: "No gnome-keyring-daemon process found."
3511
+ };
3512
+ }
3513
+ }
3514
+ if (await isCommandAvailable("pgrep")) {
3515
+ const pgrepResult = await runCommandCapture("pgrep", ["-f", "gnome-keyring-daemon"]);
3516
+ if (pgrepResult.ok) return { ok: true };
3517
+ return {
3518
+ ok: false,
3519
+ details: extractCommandFailureDetails(pgrepResult) ?? "No gnome-keyring-daemon process found."
3520
+ };
3521
+ }
3522
+ return {
3523
+ ok: false,
3524
+ details: "Neither ps nor pgrep is available to check gnome-keyring-daemon."
3525
+ };
3526
+ };
3527
+ const getLinuxGnomeKeyringAutoStartStatus = async () => {
3528
+ if (!await isCommandAvailable("systemctl")) return {
3529
+ state: "unknown",
3530
+ details: "systemctl is not available; autostart detection depends on your desktop/session config."
3531
+ };
3532
+ const units = ["gnome-keyring-daemon.socket", "gnome-keyring-daemon.service"];
3533
+ let sawDisabled = false;
3534
+ const details = [];
3535
+ for (const unit of units) {
3536
+ const result = await runCommandCapture("systemctl", [
3537
+ "--user",
3538
+ "is-enabled",
3539
+ unit
3540
+ ]);
3541
+ const state = parseSystemctlEnabledState(result.stdout || result.stderr);
3542
+ if (state === "enabled") return {
3543
+ state: "enabled",
3544
+ details: `${unit} is enabled (${result.stdout || "enabled"}).`
3545
+ };
3546
+ if (state === "disabled") {
3547
+ sawDisabled = true;
3548
+ details.push(`${unit}: ${result.stdout || result.stderr || "disabled"}`);
3549
+ continue;
3550
+ }
3551
+ const maybeDetail = extractCommandFailureDetails(result);
3552
+ if (maybeDetail) details.push(`${unit}: ${maybeDetail}`);
3553
+ }
3554
+ if (sawDisabled) return {
3555
+ state: "disabled",
3556
+ details: details.join("; ")
3557
+ };
3558
+ return {
3559
+ state: "unknown",
3560
+ details: details.join("; ") || "Unable to determine gnome-keyring autostart state."
3561
+ };
3562
+ };
3563
+ const startGnomeKeyringNow = async () => {
3564
+ const directStart = await runCommandCapture("gnome-keyring-daemon", ["--start", "--components=secrets"]);
3565
+ if (directStart.ok) {
3566
+ applyEnvAssignments(directStart.stdout);
3567
+ const runningCheck = await checkGnomeKeyringRunning();
3568
+ if (runningCheck.ok) return { ok: true };
3569
+ return {
3570
+ ok: false,
3571
+ details: runningCheck.details ?? "gnome-keyring-daemon start command succeeded, but process was not detected afterwards."
3572
+ };
3573
+ }
3574
+ if (await isCommandAvailable("systemctl")) {
3575
+ const serviceStart = await runCommandCapture("systemctl", [
3576
+ "--user",
3577
+ "start",
3578
+ "gnome-keyring-daemon.service"
3579
+ ]);
3580
+ if (serviceStart.ok) {
3581
+ const runningCheck = await checkGnomeKeyringRunning();
3582
+ if (runningCheck.ok) return { ok: true };
3583
+ return {
3584
+ ok: false,
3585
+ details: runningCheck.details ?? "systemctl start succeeded, but gnome-keyring-daemon was not detected afterwards."
3586
+ };
3587
+ }
3588
+ return {
3589
+ ok: false,
3590
+ details: extractCommandFailureDetails(directStart) ?? extractCommandFailureDetails(serviceStart) ?? "Failed to start gnome-keyring-daemon."
3591
+ };
3592
+ }
3593
+ return {
3594
+ ok: false,
3595
+ details: extractCommandFailureDetails(directStart) ?? "Failed to start gnome-keyring-daemon."
3596
+ };
3597
+ };
3598
+ const detectLinuxDistro = async () => {
3599
+ try {
3600
+ const pairs = (await readFile("/etc/os-release", "utf8")).split(/\r?\n/).map((line) => line.trim()).filter(Boolean).filter((line) => !line.startsWith("#")).map((line) => {
3601
+ const idx = line.indexOf("=");
3602
+ if (idx === -1) return null;
3603
+ const key = line.slice(0, idx);
3604
+ let value = line.slice(idx + 1);
3605
+ value = value.replace(/^"|"$/g, "");
3606
+ return {
3607
+ key,
3608
+ value
3609
+ };
3610
+ }).filter((entry) => Boolean(entry));
3611
+ const map = new Map(pairs.map((entry) => [entry.key, entry.value]));
3612
+ const id = (map.get("ID") ?? "unknown").toLowerCase();
3613
+ return {
3614
+ id,
3615
+ idLike: (map.get("ID_LIKE") ?? "").toLowerCase().split(/\s+/).map((item) => item.trim()).filter(Boolean),
3616
+ prettyName: map.get("PRETTY_NAME") ?? map.get("NAME") ?? id
3617
+ };
3618
+ } catch {
3619
+ return {
3620
+ id: "unknown",
3621
+ idLike: [],
3622
+ prettyName: "Unknown Linux"
3623
+ };
3624
+ }
3625
+ };
3626
+ const isDebianUbuntuMint = (distro) => {
3627
+ if ([
3628
+ "debian",
3629
+ "ubuntu",
3630
+ "linuxmint",
3631
+ "mint"
3632
+ ].includes(distro.id)) return true;
3633
+ return distro.idLike.some((item) => [
3634
+ "debian",
3635
+ "ubuntu",
3636
+ "linuxmint",
3637
+ "mint"
3638
+ ].includes(item));
3639
+ };
3640
+ const printChecklist = (title, items) => {
3641
+ process.stdout.write(`${title}:\n`);
3642
+ let passed = 0;
3643
+ items.forEach((item, index) => {
3644
+ if (item.ok) {
3645
+ passed += 1;
3646
+ process.stdout.write(` ${index + 1}. ${item.question}: yes\n`);
3647
+ return;
3648
+ }
3649
+ process.stdout.write(` ${index + 1}. ${item.question}: no\n`);
3650
+ if (item.details) process.stdout.write(` details: ${item.details}\n`);
3651
+ if (item.hint) process.stdout.write(` hint: ${item.hint}\n`);
3652
+ });
3653
+ process.stdout.write(` Summary: ${passed}/${items.length} checks passed.\n`);
3654
+ return passed;
3655
+ };
3656
+ const runLinuxKeyringCheck = async (options = {}) => {
3657
+ const distro = await detectLinuxDistro();
3658
+ process.stdout.write("\nLinux keyring diagnostics:\n");
3659
+ process.stdout.write(` Distro: ${distro.prettyName} (id=${distro.id})\n`);
3660
+ process.stdout.write(` DBUS_SESSION_BUS_ADDRESS: ${process.env.DBUS_SESSION_BUS_ADDRESS ?? "<unset>"}\n`);
3661
+ process.stdout.write(` XDG_RUNTIME_DIR: ${process.env.XDG_RUNTIME_DIR ?? "<unset>"}\n`);
3662
+ const gnomeKeyringInstalled = await isCommandAvailable("gnome-keyring-daemon");
3663
+ const secretToolInstalled = await isCommandAvailable("secret-tool");
3664
+ const xdgOpenInstalled = await isCommandAvailable("xdg-open");
3665
+ const dbusSendInstalled = await isCommandAvailable("dbus-send");
3666
+ const running = await checkGnomeKeyringRunning();
3667
+ const autostart = await getLinuxGnomeKeyringAutoStartStatus();
3668
+ const checklistPassed = printChecklist("\nDependency and runtime checks", [
3669
+ {
3670
+ question: "gnome-keyring-daemon installed",
3671
+ ok: gnomeKeyringInstalled,
3672
+ hint: "Install package: gnome-keyring"
3673
+ },
3674
+ {
3675
+ question: "secret-tool installed",
3676
+ ok: secretToolInstalled,
3677
+ hint: "Install package: libsecret-tools"
3678
+ },
3679
+ {
3680
+ question: "dbus-send installed",
3681
+ ok: dbusSendInstalled,
3682
+ hint: "Install package: dbus"
3683
+ },
3684
+ {
3685
+ question: "xdg-open installed",
3686
+ ok: xdgOpenInstalled,
3687
+ hint: "Install package: xdg-utils"
3688
+ },
3689
+ {
3690
+ question: "gnome-keyring-daemon running",
3691
+ ok: running.ok,
3692
+ details: running.details,
3693
+ hint: "Start daemon: gnome-keyring-daemon --start --components=secrets"
3694
+ }
3695
+ ]);
3696
+ process.stdout.write("\nAutostart status:\n");
3697
+ process.stdout.write(` gnome-keyring autostart: ${autostart.state}`);
3698
+ if (autostart.details) process.stdout.write(` (${autostart.details})`);
3699
+ process.stdout.write("\n");
3700
+ if (options.allowInteractiveStart !== false && isInteractiveTerminal() && gnomeKeyringInstalled && !running.ok) {
3701
+ const shouldStart = await p.confirm({
3702
+ message: "gnome-keyring-daemon is not running. Start it now for this session?",
3703
+ initialValue: true
3704
+ });
3705
+ if (!p.isCancel(shouldStart) && shouldStart) {
3706
+ const started = await startGnomeKeyringNow();
3707
+ if (started.ok) process.stdout.write(" Started gnome-keyring-daemon for this session.\n");
3708
+ else process.stdout.write(` Failed to start gnome-keyring-daemon (${started.details ?? "unknown error"}).\n`);
3709
+ }
3710
+ }
3711
+ process.stdout.write("\nSecret-store write/read/delete probe:\n");
3712
+ const probeResult = await runSecretStoreWriteReadProbe(createRuntimeSecretStoreAdapter("linux"));
3713
+ if (probeResult.ok) process.stdout.write(" write/read/delete probe: OK\n");
3714
+ else {
3715
+ process.stdout.write(` ${probeResult.stage} failed: ${probeResult.error.message}\n`);
3716
+ process.stdout.write(" Suggested fix: ensure Secret Service is running and unlocked (gnome-keyring + secret-tool), then run `cdx keyring check` again.\n");
3717
+ }
3718
+ const checksOk = checklistPassed === 5 && probeResult.ok;
3719
+ process.stdout.write(`\nResult: ${checksOk ? "OK" : "NOT READY"}\n\n`);
3720
+ return checksOk;
3721
+ };
3722
+ const runLinuxKeyringInstall = async (options) => {
3723
+ const distro = await detectLinuxDistro();
3724
+ process.stdout.write("\nLinux keyring setup:\n");
3725
+ process.stdout.write(` Distro: ${distro.prettyName} (id=${distro.id})\n`);
3726
+ if (!isDebianUbuntuMint(distro)) {
3727
+ process.stdout.write("\nAutomatic install is currently only implemented for Debian/Ubuntu/Mint.\n");
3728
+ process.stdout.write("Install these packages manually, then run `cdx keyring check`:\n");
3729
+ process.stdout.write(" - gnome-keyring\n");
3730
+ process.stdout.write(" - libsecret-tools\n");
3731
+ process.stdout.write(" - dbus-user-session\n");
3732
+ process.stdout.write(" - xdg-utils\n");
3733
+ process.stdout.write(" - libpam-gnome-keyring (optional, for PAM auto-unlock)\n\n");
3734
+ return;
3735
+ }
3736
+ process.stdout.write("\nInstall command:\n");
3737
+ process.stdout.write(` ${APT_INSTALL_COMMAND}\n`);
3738
+ let shouldRun = true;
3739
+ if (!options.yes) {
3740
+ if (!isInteractiveTerminal()) {
3741
+ process.stdout.write("\nNon-interactive terminal detected. Re-run with --yes to execute install automatically.\n\n");
3742
+ return;
3743
+ }
3744
+ const confirmed = await p.confirm({
3745
+ message: "Run this install command now?",
3746
+ initialValue: true
3747
+ });
3748
+ shouldRun = !p.isCancel(confirmed) && confirmed;
3749
+ }
3750
+ if (!shouldRun) {
3751
+ process.stdout.write("\nInstall skipped.\n\n");
3752
+ return;
3753
+ }
3754
+ const exitCode = await runCommandInherit("sh", ["-lc", APT_INSTALL_COMMAND]);
3755
+ if (exitCode !== 0) throw new Error(`Install command failed with exit code ${exitCode}.`);
3756
+ process.stdout.write("\nInstall completed.\n");
3757
+ if (!options.skipCheck) {
3758
+ if (!await runLinuxKeyringCheck({ allowInteractiveStart: true })) process.exitCode = 1;
3759
+ } else process.stdout.write("Run `cdx keyring check` to verify setup.\n\n");
3760
+ };
3761
+ const registerKeyringCommand = (program) => {
3762
+ const keyring = program.command("keyring").description("Setup and diagnose Linux gnome-keyring/Secret Service support");
3763
+ keyring.command("check").description("Run focused Linux keyring dependency and probe checks").action(async () => {
3764
+ try {
3765
+ if (process.platform !== "linux") {
3766
+ process.stdout.write("This command currently targets Linux only.\n\n");
3767
+ return;
3768
+ }
3769
+ if (!await runLinuxKeyringCheck({ allowInteractiveStart: true })) process.exitCode = 1;
3770
+ } catch (error) {
3771
+ exitWithCommandError(error);
3772
+ }
3773
+ });
3774
+ keyring.command("install").description("Install gnome-keyring dependencies on Debian/Ubuntu/Mint").option("--yes", "Run installation without interactive confirmation").option("--skip-check", "Skip automatic post-install verification").action(async (options) => {
3775
+ try {
3776
+ if (process.platform !== "linux") {
3777
+ process.stdout.write("This command currently targets Linux only.\n\n");
3778
+ return;
3779
+ }
3780
+ await runLinuxKeyringInstall(options);
3781
+ } catch (error) {
3782
+ exitWithCommandError(error);
3783
+ }
3784
+ });
3785
+ };
3228
3786
  //#endregion
3229
3787
  //#region lib/commands/label.ts
3230
3788
  const registerLabelCommand = (program) => {
@@ -3245,7 +3803,6 @@ const registerLabelCommand = (program) => {
3245
3803
  }
3246
3804
  });
3247
3805
  };
3248
-
3249
3806
  //#endregion
3250
3807
  //#region lib/commands/login.ts
3251
3808
  const registerLoginCommand = (program, deps = {}) => {
@@ -3261,7 +3818,6 @@ const registerLoginCommand = (program, deps = {}) => {
3261
3818
  }
3262
3819
  });
3263
3820
  };
3264
-
3265
3821
  //#endregion
3266
3822
  //#region lib/secrets/migrate.ts
3267
3823
  const asErrorMessage = (error) => error instanceof Error ? error.message : String(error);
@@ -3344,7 +3900,6 @@ const migrateLegacyMacOSSecrets = async (options = {}) => {
3344
3900
  accountResults
3345
3901
  };
3346
3902
  };
3347
-
3348
3903
  //#endregion
3349
3904
  //#region lib/commands/migrate-secrets.ts
3350
3905
  const statusPrefix = (result) => {
@@ -3374,7 +3929,6 @@ const registerMigrateSecretsCommand = (program) => {
3374
3929
  }
3375
3930
  });
3376
3931
  };
3377
-
3378
3932
  //#endregion
3379
3933
  //#region lib/commands/output.ts
3380
3934
  const formatCodexMark = (result) => {
@@ -3398,7 +3952,6 @@ const writeUpdatedAuthSummary = (result) => {
3398
3952
  process.stdout.write(` Pi Agent: ${piMark}\n`);
3399
3953
  process.stdout.write(` Codex CLI: ${codexMark}\n`);
3400
3954
  };
3401
-
3402
3955
  //#endregion
3403
3956
  //#region lib/commands/refresh.ts
3404
3957
  const registerReloginCommand = (program) => {
@@ -3434,7 +3987,6 @@ const registerReloginCommand = (program) => {
3434
3987
  }
3435
3988
  });
3436
3989
  };
3437
-
3438
3990
  //#endregion
3439
3991
  //#region lib/usage.ts
3440
3992
  const USAGE_ENDPOINT = "https://chatgpt.com/backend-api/wham/usage";
@@ -3591,7 +4143,6 @@ const formatUsageOverview = (entries) => {
3591
4143
  }
3592
4144
  return lines.join("\n");
3593
4145
  };
3594
-
3595
4146
  //#endregion
3596
4147
  //#region lib/commands/status.ts
3597
4148
  const registerStatusCommand = (program) => {
@@ -3650,7 +4201,6 @@ const registerStatusCommand = (program) => {
3650
4201
  }
3651
4202
  });
3652
4203
  };
3653
-
3654
4204
  //#endregion
3655
4205
  //#region lib/commands/switch.ts
3656
4206
  const switchNext = async () => {
@@ -3685,7 +4235,6 @@ const registerSwitchCommand = (program) => {
3685
4235
  }
3686
4236
  });
3687
4237
  };
3688
-
3689
4238
  //#endregion
3690
4239
  //#region lib/commands/usage.ts
3691
4240
  const registerUsageCommand = (program) => {
@@ -3725,7 +4274,6 @@ const registerUsageCommand = (program) => {
3725
4274
  }
3726
4275
  });
3727
4276
  };
3728
-
3729
4277
  //#endregion
3730
4278
  //#region lib/runtime/update-manager.ts
3731
4279
  const detectRuntime = (input = {}) => {
@@ -3807,7 +4355,6 @@ const buildUpdateInstallCommand = (manager, packageName) => {
3807
4355
  ]
3808
4356
  };
3809
4357
  };
3810
-
3811
4358
  //#endregion
3812
4359
  //#region lib/commands/update-self.ts
3813
4360
  const PACKAGE_NAME = "@bjesuiter/codex-switcher";
@@ -3953,7 +4500,6 @@ const registerUpdateSelfCommand = (program) => {
3953
4500
  }
3954
4501
  });
3955
4502
  };
3956
-
3957
4503
  //#endregion
3958
4504
  //#region lib/commands/version.ts
3959
4505
  const registerVersionCommand = (program, version) => {
@@ -3961,7 +4507,6 @@ const registerVersionCommand = (program, version) => {
3961
4507
  process.stdout.write(`${version}\n`);
3962
4508
  });
3963
4509
  };
3964
-
3965
4510
  //#endregion
3966
4511
  //#region cdx.ts
3967
4512
  const interactiveMode = runInteractiveMode;
@@ -4070,6 +4615,7 @@ const createProgram = (deps = {}) => {
4070
4615
  registerSwitchCommand(program);
4071
4616
  registerLabelCommand(program);
4072
4617
  registerMigrateSecretsCommand(program);
4618
+ registerKeyringCommand(program);
4073
4619
  registerStatusCommand(program);
4074
4620
  registerDoctorCommand(program);
4075
4621
  registerUsageCommand(program);
@@ -4093,6 +4639,5 @@ const main = async () => {
4093
4639
  if (import.meta.main) main().catch((error) => {
4094
4640
  exitWithCommandError(error);
4095
4641
  });
4096
-
4097
4642
  //#endregion
4098
- export { createProgram, createRuntimeSecretStoreAdapter, createSecretStoreAdapterFromSelection, createTestPaths, getMacOSKeychainPromptWarning, getPaths, getSecretStoreAdapter, interactiveMode, loadConfig, resetPaths, resetSecretStoreAdapter, resolveMacOSCrossKeychainBackendId, runInteractiveMode, saveConfig, setPaths, setSecretStoreAdapter, switchNext, switchToAccount, writeAllAuthFiles, writeAuthFile, writeCodexAuthFile, writePiAuthFile };
4643
+ export { createProgram, createRuntimeSecretStoreAdapter, createSecretStoreAdapterFromSelection, createTestPaths, getMacOSKeychainPromptWarning, getPaths, getSecretStoreAdapter, interactiveMode, loadConfig, resetPaths, resetSecretStoreAdapter, resolveMacOSCrossKeychainBackendId, runInteractiveMode, saveConfig, setPaths, setSecretStoreAdapter, switchNext, switchToAccount, writeAllAuthFiles, writeAuthFile, writeCodexAuthFile, writePiAuthFile };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bjesuiter/codex-switcher",
3
- "version": "1.8.5",
3
+ "version": "1.8.6",
4
4
  "type": "module",
5
5
  "description": "CLI tool to switch between multiple OpenAI accounts for OpenCode",
6
6
  "bin": {
@@ -21,8 +21,8 @@
21
21
  "author": "bjesuiter",
22
22
  "dependencies": {
23
23
  "@bjesuiter/cross-keychain": "1.1.0-jb.0",
24
- "@bomb.sh/tab": "^0.0.13",
25
- "@clack/prompts": "^1.0.0",
24
+ "@bomb.sh/tab": "^0.0.14",
25
+ "@clack/prompts": "^1.1.0",
26
26
  "@openauthjs/openauth": "^0.4.3",
27
27
  "age-encryption": "^0.3.0",
28
28
  "commander": "^14.0.3"