@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.
- package/README.md +14 -2
- package/cdx.mjs +643 -98
- 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.
|
|
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
|
|
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.
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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) ??
|
|
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
|
|
2823
|
-
const
|
|
2824
|
-
|
|
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
|
|
2827
|
-
const
|
|
2828
|
-
|
|
2829
|
-
|
|
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 (
|
|
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)
|
|
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.
|
|
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.
|
|
25
|
-
"@clack/prompts": "^1.
|
|
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"
|