@bjesuiter/codex-switcher 1.8.4 → 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 +655 -96
  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.4
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 detect `gnome-keyring-daemon` with a full-command `pgrep -f` match, fixing false negatives/errors on systems where `pgrep -x` cannot match process names longer than 15 characters.
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.4";
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,27 +2871,83 @@ 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 checkGnomeKeyringRunning = async () => {
2827
- if (await isCommandAvailable("pgrep")) {
2828
- const pgrepResult = await runCommandCapture("pgrep", ["-f", "(^|/)gnome-keyring-daemon( |$)"]);
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", [
2925
+ "-A",
2926
+ "-o",
2927
+ "args="
2928
+ ]);
2929
+ if (psResult.ok) {
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 };
2931
+ return {
2932
+ ok: false,
2933
+ details: "No gnome-keyring-daemon process found."
2934
+ };
2935
+ }
2936
+ }
2937
+ if (await isCommandAvailable$1("pgrep")) {
2938
+ const pgrepResult = await runCommandCapture$1("pgrep", ["-f", "gnome-keyring-daemon"]);
2829
2939
  if (pgrepResult.ok) return { ok: true };
2830
2940
  return {
2831
2941
  ok: false,
2832
- details: extractCommandFailureDetails(pgrepResult) ?? "No gnome-keyring-daemon process found."
2942
+ details: extractCommandFailureDetails$1(pgrepResult) ?? "No gnome-keyring-daemon process found."
2833
2943
  };
2834
2944
  }
2835
- const psFallback = await runCommandCapture("sh", ["-lc", "ps -A -o comm= | grep -q '^gnome-keyring-daemon$'"]);
2836
- if (psFallback.ok) return { ok: true };
2837
2945
  return {
2838
2946
  ok: false,
2839
- details: extractCommandFailureDetails(psFallback) ?? "No gnome-keyring-daemon process found."
2947
+ details: "Neither ps nor pgrep is available to check gnome-keyring-daemon."
2840
2948
  };
2841
2949
  };
2842
- const parseSystemctlEnabledState = (output) => {
2950
+ const parseSystemctlEnabledState$1 = (output) => {
2843
2951
  const normalized = output.trim().toLowerCase();
2844
2952
  if ([
2845
2953
  "enabled",
@@ -2857,8 +2965,8 @@ const parseSystemctlEnabledState = (output) => {
2857
2965
  ].includes(normalized)) return "disabled";
2858
2966
  return "unknown";
2859
2967
  };
2860
- const getLinuxGnomeKeyringAutoStartStatus = async () => {
2861
- if (!await isCommandAvailable("systemctl")) return {
2968
+ const getLinuxGnomeKeyringAutoStartStatus$1 = async () => {
2969
+ if (!await isCommandAvailable$1("systemctl")) return {
2862
2970
  state: "unknown",
2863
2971
  details: "systemctl is not available; autostart detection depends on your desktop/session config."
2864
2972
  };
@@ -2866,12 +2974,12 @@ const getLinuxGnomeKeyringAutoStartStatus = async () => {
2866
2974
  let sawDisabled = false;
2867
2975
  const details = [];
2868
2976
  for (const unit of units) {
2869
- const result = await runCommandCapture("systemctl", [
2977
+ const result = await runCommandCapture$1("systemctl", [
2870
2978
  "--user",
2871
2979
  "is-enabled",
2872
2980
  unit
2873
2981
  ]);
2874
- const state = parseSystemctlEnabledState(result.stdout || result.stderr);
2982
+ const state = parseSystemctlEnabledState$1(result.stdout || result.stderr);
2875
2983
  if (state === "enabled") return {
2876
2984
  state: "enabled",
2877
2985
  details: `${unit} is enabled (${result.stdout || "enabled"}).`
@@ -2881,7 +2989,7 @@ const getLinuxGnomeKeyringAutoStartStatus = async () => {
2881
2989
  details.push(`${unit}: ${result.stdout || result.stderr || "disabled"}`);
2882
2990
  continue;
2883
2991
  }
2884
- const maybeDetail = extractCommandFailureDetails(result);
2992
+ const maybeDetail = extractCommandFailureDetails$1(result);
2885
2993
  if (maybeDetail) details.push(`${unit}: ${maybeDetail}`);
2886
2994
  }
2887
2995
  if (sawDisabled) return {
@@ -2903,25 +3011,25 @@ const applyKeyringEnvAssignments = (raw) => {
2903
3011
  if (key) process.env[key] = value;
2904
3012
  }
2905
3013
  };
2906
- const startGnomeKeyringNow = async () => {
2907
- 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"]);
2908
3016
  if (directStart.ok) {
2909
3017
  applyKeyringEnvAssignments(directStart.stdout);
2910
- const runningCheck = await checkGnomeKeyringRunning();
3018
+ const runningCheck = await checkGnomeKeyringRunning$1();
2911
3019
  if (runningCheck.ok) return { ok: true };
2912
3020
  return {
2913
3021
  ok: false,
2914
3022
  details: runningCheck.details ?? "gnome-keyring-daemon start command succeeded, but process was not detected afterwards."
2915
3023
  };
2916
3024
  }
2917
- if (await isCommandAvailable("systemctl")) {
2918
- const serviceStart = await runCommandCapture("systemctl", [
3025
+ if (await isCommandAvailable$1("systemctl")) {
3026
+ const serviceStart = await runCommandCapture$1("systemctl", [
2919
3027
  "--user",
2920
3028
  "start",
2921
3029
  "gnome-keyring-daemon.service"
2922
3030
  ]);
2923
3031
  if (serviceStart.ok) {
2924
- const runningCheck = await checkGnomeKeyringRunning();
3032
+ const runningCheck = await checkGnomeKeyringRunning$1();
2925
3033
  if (runningCheck.ok) return { ok: true };
2926
3034
  return {
2927
3035
  ok: false,
@@ -2930,23 +3038,23 @@ const startGnomeKeyringNow = async () => {
2930
3038
  }
2931
3039
  return {
2932
3040
  ok: false,
2933
- 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."
2934
3042
  };
2935
3043
  }
2936
3044
  return {
2937
3045
  ok: false,
2938
- details: extractCommandFailureDetails(directStart) ?? "Failed to start gnome-keyring-daemon."
3046
+ details: extractCommandFailureDetails$1(directStart) ?? "Failed to start gnome-keyring-daemon."
2939
3047
  };
2940
3048
  };
2941
3049
  const enableGnomeKeyringAutoStart = async () => {
2942
- if (!await isCommandAvailable("systemctl")) return {
3050
+ if (!await isCommandAvailable$1("systemctl")) return {
2943
3051
  ok: false,
2944
3052
  details: "systemctl is not available, so cdx cannot automatically enable startup in this session manager."
2945
3053
  };
2946
3054
  const units = ["gnome-keyring-daemon.socket", "gnome-keyring-daemon.service"];
2947
3055
  const failures = [];
2948
3056
  for (const unit of units) {
2949
- const result = await runCommandCapture("systemctl", [
3057
+ const result = await runCommandCapture$1("systemctl", [
2950
3058
  "--user",
2951
3059
  "enable",
2952
3060
  unit
@@ -2955,7 +3063,7 @@ const enableGnomeKeyringAutoStart = async () => {
2955
3063
  ok: true,
2956
3064
  details: `${unit} enabled.`
2957
3065
  };
2958
- const detail = extractCommandFailureDetails(result);
3066
+ const detail = extractCommandFailureDetails$1(result);
2959
3067
  failures.push(`${unit}: ${detail ?? "enable failed"}`);
2960
3068
  }
2961
3069
  return {
@@ -2964,14 +3072,14 @@ const enableGnomeKeyringAutoStart = async () => {
2964
3072
  };
2965
3073
  };
2966
3074
  const maybeOfferToStartGnomeKeyring = async () => {
2967
- const autoStartStatus = await getLinuxGnomeKeyringAutoStartStatus();
3075
+ const autoStartStatus = await getLinuxGnomeKeyringAutoStartStatus$1();
2968
3076
  if (autoStartStatus.state === "enabled") {
2969
3077
  const shouldStartNow = await p.confirm({
2970
3078
  message: "gnome-keyring autostart appears enabled, but it is not running right now. Start it now?",
2971
3079
  initialValue: true
2972
3080
  });
2973
3081
  if (p.isCancel(shouldStartNow) || !shouldStartNow) return false;
2974
- const startResult = await startGnomeKeyringNow();
3082
+ const startResult = await startGnomeKeyringNow$1();
2975
3083
  if (!startResult.ok) {
2976
3084
  process.stdout.write(` failed to start gnome-keyring-daemon: ${startResult.details ?? "unknown error"}\n`);
2977
3085
  return false;
@@ -3007,7 +3115,7 @@ const maybeOfferToStartGnomeKeyring = async () => {
3007
3115
  }
3008
3116
  process.stdout.write(` autostart enabled${enableResult.details ? ` (${enableResult.details})` : ""}.\n`);
3009
3117
  }
3010
- const startResult = await startGnomeKeyringNow();
3118
+ const startResult = await startGnomeKeyringNow$1();
3011
3119
  if (!startResult.ok) {
3012
3120
  process.stdout.write(` failed to start gnome-keyring-daemon: ${startResult.details ?? "unknown error"}\n`);
3013
3121
  return false;
@@ -3016,9 +3124,9 @@ const maybeOfferToStartGnomeKeyring = async () => {
3016
3124
  return true;
3017
3125
  };
3018
3126
  const runLinuxSecretStoreChecklist = async () => {
3019
- const gnomeKeyringInstalled = await isCommandAvailable("gnome-keyring-daemon");
3020
- const secretToolInstalled = await isCommandAvailable("secret-tool");
3021
- 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();
3022
3130
  return [
3023
3131
  {
3024
3132
  id: "gnome-keyring-installed",
@@ -3037,12 +3145,118 @@ const runLinuxSecretStoreChecklist = async () => {
3037
3145
  question: "Is gnome-keyring running?",
3038
3146
  ok: gnomeKeyringRunning.ok,
3039
3147
  details: gnomeKeyringRunning.details,
3040
- 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)."
3041
3149
  }
3042
3150
  ];
3043
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
+ };
3044
3258
  const maybeRunLinuxSecretStoreChecklist = async () => {
3045
- if (!isInteractiveTerminal()) {
3259
+ if (!isInteractiveTerminal$1()) {
3046
3260
  process.stdout.write(" Tip: run `cdx doctor` in an interactive terminal to start guided Linux secret-store checks.\n");
3047
3261
  return;
3048
3262
  }
@@ -3069,7 +3283,7 @@ const maybeRunLinuxSecretStoreChecklist = async () => {
3069
3283
  if (item.hint) process.stdout.write(` hint: ${item.hint}\n`);
3070
3284
  if (item.id === "gnome-keyring-running") {
3071
3285
  if (await maybeOfferToStartGnomeKeyring()) {
3072
- const runningNow = await checkGnomeKeyringRunning();
3286
+ const runningNow = await checkGnomeKeyringRunning$1();
3073
3287
  if (runningNow.ok) {
3074
3288
  passed += 1;
3075
3289
  process.stdout.write(" re-check: gnome-keyring-daemon is now running.\n");
@@ -3078,6 +3292,10 @@ const maybeRunLinuxSecretStoreChecklist = async () => {
3078
3292
  }
3079
3293
  }
3080
3294
  process.stdout.write(` Guided checklist summary: ${passed}/${checklist.length} checks passed.\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
+ }
3081
3299
  };
3082
3300
  const registerDoctorCommand = (program) => {
3083
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) => {
@@ -3192,7 +3410,6 @@ const registerDoctorCommand = (program) => {
3192
3410
  }
3193
3411
  });
3194
3412
  };
3195
-
3196
3413
  //#endregion
3197
3414
  //#region lib/commands/help.ts
3198
3415
  const registerHelpCommand = (program) => {
@@ -3210,7 +3427,362 @@ const registerHelpCommand = (program) => {
3210
3427
  program.outputHelp();
3211
3428
  });
3212
3429
  };
3213
-
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
+ };
3214
3786
  //#endregion
3215
3787
  //#region lib/commands/label.ts
3216
3788
  const registerLabelCommand = (program) => {
@@ -3231,7 +3803,6 @@ const registerLabelCommand = (program) => {
3231
3803
  }
3232
3804
  });
3233
3805
  };
3234
-
3235
3806
  //#endregion
3236
3807
  //#region lib/commands/login.ts
3237
3808
  const registerLoginCommand = (program, deps = {}) => {
@@ -3247,7 +3818,6 @@ const registerLoginCommand = (program, deps = {}) => {
3247
3818
  }
3248
3819
  });
3249
3820
  };
3250
-
3251
3821
  //#endregion
3252
3822
  //#region lib/secrets/migrate.ts
3253
3823
  const asErrorMessage = (error) => error instanceof Error ? error.message : String(error);
@@ -3330,7 +3900,6 @@ const migrateLegacyMacOSSecrets = async (options = {}) => {
3330
3900
  accountResults
3331
3901
  };
3332
3902
  };
3333
-
3334
3903
  //#endregion
3335
3904
  //#region lib/commands/migrate-secrets.ts
3336
3905
  const statusPrefix = (result) => {
@@ -3360,7 +3929,6 @@ const registerMigrateSecretsCommand = (program) => {
3360
3929
  }
3361
3930
  });
3362
3931
  };
3363
-
3364
3932
  //#endregion
3365
3933
  //#region lib/commands/output.ts
3366
3934
  const formatCodexMark = (result) => {
@@ -3384,7 +3952,6 @@ const writeUpdatedAuthSummary = (result) => {
3384
3952
  process.stdout.write(` Pi Agent: ${piMark}\n`);
3385
3953
  process.stdout.write(` Codex CLI: ${codexMark}\n`);
3386
3954
  };
3387
-
3388
3955
  //#endregion
3389
3956
  //#region lib/commands/refresh.ts
3390
3957
  const registerReloginCommand = (program) => {
@@ -3420,7 +3987,6 @@ const registerReloginCommand = (program) => {
3420
3987
  }
3421
3988
  });
3422
3989
  };
3423
-
3424
3990
  //#endregion
3425
3991
  //#region lib/usage.ts
3426
3992
  const USAGE_ENDPOINT = "https://chatgpt.com/backend-api/wham/usage";
@@ -3577,7 +4143,6 @@ const formatUsageOverview = (entries) => {
3577
4143
  }
3578
4144
  return lines.join("\n");
3579
4145
  };
3580
-
3581
4146
  //#endregion
3582
4147
  //#region lib/commands/status.ts
3583
4148
  const registerStatusCommand = (program) => {
@@ -3636,7 +4201,6 @@ const registerStatusCommand = (program) => {
3636
4201
  }
3637
4202
  });
3638
4203
  };
3639
-
3640
4204
  //#endregion
3641
4205
  //#region lib/commands/switch.ts
3642
4206
  const switchNext = async () => {
@@ -3671,7 +4235,6 @@ const registerSwitchCommand = (program) => {
3671
4235
  }
3672
4236
  });
3673
4237
  };
3674
-
3675
4238
  //#endregion
3676
4239
  //#region lib/commands/usage.ts
3677
4240
  const registerUsageCommand = (program) => {
@@ -3711,7 +4274,6 @@ const registerUsageCommand = (program) => {
3711
4274
  }
3712
4275
  });
3713
4276
  };
3714
-
3715
4277
  //#endregion
3716
4278
  //#region lib/runtime/update-manager.ts
3717
4279
  const detectRuntime = (input = {}) => {
@@ -3793,7 +4355,6 @@ const buildUpdateInstallCommand = (manager, packageName) => {
3793
4355
  ]
3794
4356
  };
3795
4357
  };
3796
-
3797
4358
  //#endregion
3798
4359
  //#region lib/commands/update-self.ts
3799
4360
  const PACKAGE_NAME = "@bjesuiter/codex-switcher";
@@ -3939,7 +4500,6 @@ const registerUpdateSelfCommand = (program) => {
3939
4500
  }
3940
4501
  });
3941
4502
  };
3942
-
3943
4503
  //#endregion
3944
4504
  //#region lib/commands/version.ts
3945
4505
  const registerVersionCommand = (program, version) => {
@@ -3947,7 +4507,6 @@ const registerVersionCommand = (program, version) => {
3947
4507
  process.stdout.write(`${version}\n`);
3948
4508
  });
3949
4509
  };
3950
-
3951
4510
  //#endregion
3952
4511
  //#region cdx.ts
3953
4512
  const interactiveMode = runInteractiveMode;
@@ -4056,6 +4615,7 @@ const createProgram = (deps = {}) => {
4056
4615
  registerSwitchCommand(program);
4057
4616
  registerLabelCommand(program);
4058
4617
  registerMigrateSecretsCommand(program);
4618
+ registerKeyringCommand(program);
4059
4619
  registerStatusCommand(program);
4060
4620
  registerDoctorCommand(program);
4061
4621
  registerUsageCommand(program);
@@ -4079,6 +4639,5 @@ const main = async () => {
4079
4639
  if (import.meta.main) main().catch((error) => {
4080
4640
  exitWithCommandError(error);
4081
4641
  });
4082
-
4083
4642
  //#endregion
4084
- 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.4",
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"