@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.
- package/README.md +14 -2
- package/cdx.mjs +655 -96
- 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,27 +2871,83 @@ 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
|
-
|
|
2828
|
-
|
|
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:
|
|
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 (
|
|
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.
|
|
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"
|