@bjesuiter/codex-switcher 1.8.1 → 1.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +6 -4
  2. package/cdx.mjs +345 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,12 +6,12 @@ Switch the coding-agents [pi](https://pi.dev/), [codex](https://developers.opena
6
6
 
7
7
  ## Latest Changes
8
8
 
9
- ### 1.8.1
9
+ ### 1.8.3
10
10
 
11
- #### Fixes
11
+ #### Improvements
12
12
 
13
- - Added platform-native secure-store write/read/delete probes to `cdx doctor` on Linux, macOS, and Windows, so runtime secure-store failures are detected directly instead of only reporting adapter capability.
14
- - Linux secure-store error handling now treats Secret Service `no result found` responses as missing-entry cases and classifies generic `Couldn't access platform secure storage` failures as unavailable-store errors with actionable guidance.
13
+ - `cdx update-self` now prints the detected installed version directly after a successful update, so you don't need to run `cdx version` manually.
14
+ - Linux `cdx doctor` guided checks now offer interactive recovery when `gnome-keyring-daemon` is not running: start now, or (when detectable as disabled) enable autostart and start now.
15
15
 
16
16
  see full changelog here: https://github.com/bjesuiter/codex-switcher/blob/main/CHANGELOG.md
17
17
 
@@ -170,6 +170,8 @@ cdx migrate-secrets
170
170
  | `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) |
171
171
  | `cdx usage` | Show usage overview for all accounts |
172
172
  | `cdx usage <account>` | Show detailed usage for a specific account |
173
+ | `cdx update-self` | Update cdx to the latest version |
174
+ | `cdx self-update` / `cdx update` / `cdx updte` | Aliases for `cdx update-self` |
173
175
  | `cdx help [command]` | Show help for all commands or one command |
174
176
  | `cdx complete <shell>` | Generate shell completion script (`zsh`, `bash`, `fish`, `powershell`) |
175
177
  | `cdx version` | Show CLI version |
package/cdx.mjs CHANGED
@@ -15,7 +15,7 @@ import { generatePKCE } from "@openauthjs/openauth/pkce";
15
15
  import http from "node:http";
16
16
 
17
17
  //#region package.json
18
- var version = "1.8.1";
18
+ var version = "1.8.3";
19
19
 
20
20
  //#endregion
21
21
  //#region lib/platform/path-resolver.ts
@@ -256,7 +256,7 @@ const getBrowserLauncher = (platform = process.platform, url) => {
256
256
  label: "xdg-open"
257
257
  };
258
258
  };
259
- const isCommandAvailable$1 = (command, platform = process.platform) => {
259
+ const isCommandAvailable$2 = (command, platform = process.platform) => {
260
260
  const probe = platform === "win32" ? "where" : "which";
261
261
  return Bun.spawnSync({
262
262
  cmd: [probe, command],
@@ -269,13 +269,13 @@ const getBrowserLauncherCapability = (platform = process.platform) => {
269
269
  return {
270
270
  command: launcher.command,
271
271
  label: launcher.label,
272
- available: isCommandAvailable$1(launcher.command, platform)
272
+ available: isCommandAvailable$2(launcher.command, platform)
273
273
  };
274
274
  };
275
275
  const openBrowserUrl = (url, options = {}) => {
276
276
  const platform = options.platform ?? process.platform;
277
277
  const spawnImpl = options.spawnImpl ?? spawn;
278
- const commandAvailable = options.isCommandAvailableImpl ?? isCommandAvailable$1;
278
+ const commandAvailable = options.isCommandAvailableImpl ?? isCommandAvailable$2;
279
279
  const launcher = getBrowserLauncher(platform, url);
280
280
  if (!commandAvailable(launcher.command, platform)) return {
281
281
  ok: false,
@@ -306,7 +306,7 @@ const openBrowserUrl = (url, options = {}) => {
306
306
 
307
307
  //#endregion
308
308
  //#region lib/platform/clipboard.ts
309
- const isCommandAvailable = (command, platform = process.platform) => {
309
+ const isCommandAvailable$1 = (command, platform = process.platform) => {
310
310
  const probe = platform === "win32" ? "where" : "which";
311
311
  return Bun.spawnSync({
312
312
  cmd: [probe, command],
@@ -392,7 +392,7 @@ const getLocalClipboardTargets = (platform, env, commandExists) => {
392
392
  });
393
393
  return targets;
394
394
  };
395
- const resolveClipboardTargets = (context = {}, commandExists = isCommandAvailable) => {
395
+ const resolveClipboardTargets = (context = {}, commandExists = isCommandAvailable$1) => {
396
396
  const platform = context.platform ?? process.platform;
397
397
  const env = context.env ?? process.env;
398
398
  const isTTY = context.isTTY ?? (Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY));
@@ -433,7 +433,7 @@ const defaultRunCommand = (command, args, input) => {
433
433
  };
434
434
  };
435
435
  const tryCopyToClipboard = (text, options = {}) => {
436
- const commandExists = options.commandExistsImpl ?? isCommandAvailable;
436
+ const commandExists = options.commandExistsImpl ?? isCommandAvailable$1;
437
437
  const runCommand = options.runCommandImpl ?? defaultRunCommand;
438
438
  const writeStdout = options.writeStdoutImpl ?? ((chunk) => {
439
439
  process.stdout.write(chunk);
@@ -479,7 +479,7 @@ const tryCopyToClipboard = (text, options = {}) => {
479
479
  };
480
480
  };
481
481
  const escapePosixSingleQuoted = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
482
- const buildClipboardHelperCommand = (text, context = {}, commandExists = isCommandAvailable) => {
482
+ const buildClipboardHelperCommand = (text, context = {}, commandExists = isCommandAvailable$1) => {
483
483
  const commandTarget = resolveClipboardTargets(context, commandExists).find((target) => target.kind === "command");
484
484
  if (!commandTarget) return null;
485
485
  if (commandTarget.method === "powershell") {
@@ -2791,6 +2791,294 @@ const getSecretStoreProbeGuidance = (platform) => {
2791
2791
  if (platform === "win32") return "Suggested fix: ensure Windows Credential Manager is available for this user session, then retry login.";
2792
2792
  return null;
2793
2793
  };
2794
+ const isInteractiveTerminal = () => Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
2795
+ const runCommandCapture = async (command, args) => await new Promise((resolve) => {
2796
+ const child = spawn(command, args, { stdio: [
2797
+ "ignore",
2798
+ "pipe",
2799
+ "pipe"
2800
+ ] });
2801
+ let stdout = "";
2802
+ let stderr = "";
2803
+ let spawnError = null;
2804
+ child.stdout?.on("data", (chunk) => {
2805
+ stdout += chunk.toString();
2806
+ });
2807
+ child.stderr?.on("data", (chunk) => {
2808
+ stderr += chunk.toString();
2809
+ });
2810
+ child.once("error", (error) => {
2811
+ spawnError = error.message;
2812
+ });
2813
+ child.once("close", (code) => {
2814
+ resolve({
2815
+ ok: spawnError === null && code === 0,
2816
+ stdout: stdout.trim(),
2817
+ stderr: stderr.trim(),
2818
+ ...spawnError ? { error: spawnError } : {}
2819
+ });
2820
+ });
2821
+ });
2822
+ const extractCommandFailureDetails = (result) => result.error || result.stderr || result.stdout || void 0;
2823
+ const isCommandAvailable = async (commandName) => {
2824
+ return (await runCommandCapture("sh", ["-lc", `command -v ${commandName} >/dev/null 2>&1`])).ok;
2825
+ };
2826
+ const checkGnomeKeyringRunning = async () => {
2827
+ if (await isCommandAvailable("pgrep")) {
2828
+ const pgrepResult = await runCommandCapture("pgrep", ["-x", "gnome-keyring-daemon"]);
2829
+ if (pgrepResult.ok) return { ok: true };
2830
+ return {
2831
+ ok: false,
2832
+ details: extractCommandFailureDetails(pgrepResult) ?? "No gnome-keyring-daemon process found."
2833
+ };
2834
+ }
2835
+ const psFallback = await runCommandCapture("sh", ["-lc", "ps -A -o comm= | grep -q '^gnome-keyring-daemon$'"]);
2836
+ if (psFallback.ok) return { ok: true };
2837
+ return {
2838
+ ok: false,
2839
+ details: extractCommandFailureDetails(psFallback) ?? "No gnome-keyring-daemon process found."
2840
+ };
2841
+ };
2842
+ const parseSystemctlEnabledState = (output) => {
2843
+ const normalized = output.trim().toLowerCase();
2844
+ if ([
2845
+ "enabled",
2846
+ "enabled-runtime",
2847
+ "static",
2848
+ "indirect",
2849
+ "generated"
2850
+ ].includes(normalized)) return "enabled";
2851
+ if ([
2852
+ "disabled",
2853
+ "masked",
2854
+ "not-found",
2855
+ "linked",
2856
+ "linked-runtime"
2857
+ ].includes(normalized)) return "disabled";
2858
+ return "unknown";
2859
+ };
2860
+ const getLinuxGnomeKeyringAutoStartStatus = async () => {
2861
+ if (!await isCommandAvailable("systemctl")) return {
2862
+ state: "unknown",
2863
+ details: "systemctl is not available; autostart detection depends on your desktop/session config."
2864
+ };
2865
+ const units = ["gnome-keyring-daemon.socket", "gnome-keyring-daemon.service"];
2866
+ let sawDisabled = false;
2867
+ const details = [];
2868
+ for (const unit of units) {
2869
+ const result = await runCommandCapture("systemctl", [
2870
+ "--user",
2871
+ "is-enabled",
2872
+ unit
2873
+ ]);
2874
+ const state = parseSystemctlEnabledState(result.stdout || result.stderr);
2875
+ if (state === "enabled") return {
2876
+ state: "enabled",
2877
+ details: `${unit} is enabled (${result.stdout || "enabled"}).`
2878
+ };
2879
+ if (state === "disabled") {
2880
+ sawDisabled = true;
2881
+ details.push(`${unit}: ${result.stdout || result.stderr || "disabled"}`);
2882
+ continue;
2883
+ }
2884
+ const maybeDetail = extractCommandFailureDetails(result);
2885
+ if (maybeDetail) details.push(`${unit}: ${maybeDetail}`);
2886
+ }
2887
+ if (sawDisabled) return {
2888
+ state: "disabled",
2889
+ details: details.join("; ")
2890
+ };
2891
+ return {
2892
+ state: "unknown",
2893
+ details: details.join("; ") || "Unable to determine gnome-keyring autostart state."
2894
+ };
2895
+ };
2896
+ const applyKeyringEnvAssignments = (raw) => {
2897
+ const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
2898
+ for (const line of lines) {
2899
+ const match = line.match(/^([A-Z0-9_]+)=(.*);?$/);
2900
+ if (!match) continue;
2901
+ const key = match[1];
2902
+ const value = match[2].replace(/;$/, "");
2903
+ if (key) process.env[key] = value;
2904
+ }
2905
+ };
2906
+ const startGnomeKeyringNow = async () => {
2907
+ const directStart = await runCommandCapture("gnome-keyring-daemon", ["--start", "--components=secrets"]);
2908
+ if (directStart.ok) {
2909
+ applyKeyringEnvAssignments(directStart.stdout);
2910
+ const runningCheck = await checkGnomeKeyringRunning();
2911
+ if (runningCheck.ok) return { ok: true };
2912
+ return {
2913
+ ok: false,
2914
+ details: runningCheck.details ?? "gnome-keyring-daemon start command succeeded, but process was not detected afterwards."
2915
+ };
2916
+ }
2917
+ if (await isCommandAvailable("systemctl")) {
2918
+ const serviceStart = await runCommandCapture("systemctl", [
2919
+ "--user",
2920
+ "start",
2921
+ "gnome-keyring-daemon.service"
2922
+ ]);
2923
+ if (serviceStart.ok) {
2924
+ const runningCheck = await checkGnomeKeyringRunning();
2925
+ if (runningCheck.ok) return { ok: true };
2926
+ return {
2927
+ ok: false,
2928
+ details: runningCheck.details ?? "systemctl start succeeded, but gnome-keyring-daemon was not detected afterwards."
2929
+ };
2930
+ }
2931
+ return {
2932
+ ok: false,
2933
+ details: extractCommandFailureDetails(directStart) ?? extractCommandFailureDetails(serviceStart) ?? "Failed to start gnome-keyring-daemon."
2934
+ };
2935
+ }
2936
+ return {
2937
+ ok: false,
2938
+ details: extractCommandFailureDetails(directStart) ?? "Failed to start gnome-keyring-daemon."
2939
+ };
2940
+ };
2941
+ const enableGnomeKeyringAutoStart = async () => {
2942
+ if (!await isCommandAvailable("systemctl")) return {
2943
+ ok: false,
2944
+ details: "systemctl is not available, so cdx cannot automatically enable startup in this session manager."
2945
+ };
2946
+ const units = ["gnome-keyring-daemon.socket", "gnome-keyring-daemon.service"];
2947
+ const failures = [];
2948
+ for (const unit of units) {
2949
+ const result = await runCommandCapture("systemctl", [
2950
+ "--user",
2951
+ "enable",
2952
+ unit
2953
+ ]);
2954
+ if (result.ok) return {
2955
+ ok: true,
2956
+ details: `${unit} enabled.`
2957
+ };
2958
+ const detail = extractCommandFailureDetails(result);
2959
+ failures.push(`${unit}: ${detail ?? "enable failed"}`);
2960
+ }
2961
+ return {
2962
+ ok: false,
2963
+ details: failures.join("; ")
2964
+ };
2965
+ };
2966
+ const maybeOfferToStartGnomeKeyring = async () => {
2967
+ const autoStartStatus = await getLinuxGnomeKeyringAutoStartStatus();
2968
+ if (autoStartStatus.state === "enabled") {
2969
+ const shouldStartNow = await p.confirm({
2970
+ message: "gnome-keyring autostart appears enabled, but it is not running right now. Start it now?",
2971
+ initialValue: true
2972
+ });
2973
+ if (p.isCancel(shouldStartNow) || !shouldStartNow) return false;
2974
+ const startResult = await startGnomeKeyringNow();
2975
+ if (!startResult.ok) {
2976
+ process.stdout.write(` failed to start gnome-keyring-daemon: ${startResult.details ?? "unknown error"}\n`);
2977
+ return false;
2978
+ }
2979
+ process.stdout.write(" started gnome-keyring-daemon for this session.\n");
2980
+ return true;
2981
+ }
2982
+ if (autoStartStatus.details) process.stdout.write(` autostart check: ${autoStartStatus.details}\n`);
2983
+ const action = await p.select({
2984
+ message: autoStartStatus.state === "disabled" ? "gnome-keyring autostart seems disabled. What should cdx do?" : "Could not confirm gnome-keyring autostart. What should cdx do?",
2985
+ options: [
2986
+ {
2987
+ value: "start-now",
2988
+ label: "Start now only"
2989
+ },
2990
+ {
2991
+ value: "enable-and-start",
2992
+ label: "Enable on system start and start now"
2993
+ },
2994
+ {
2995
+ value: "skip",
2996
+ label: "Skip"
2997
+ }
2998
+ ],
2999
+ initialValue: "start-now"
3000
+ });
3001
+ if (p.isCancel(action) || action === "skip") return false;
3002
+ if (action === "enable-and-start") {
3003
+ const enableResult = await enableGnomeKeyringAutoStart();
3004
+ if (!enableResult.ok) {
3005
+ process.stdout.write(` failed to enable autostart: ${enableResult.details ?? "unknown error"}\n`);
3006
+ return false;
3007
+ }
3008
+ process.stdout.write(` autostart enabled${enableResult.details ? ` (${enableResult.details})` : ""}.\n`);
3009
+ }
3010
+ const startResult = await startGnomeKeyringNow();
3011
+ if (!startResult.ok) {
3012
+ process.stdout.write(` failed to start gnome-keyring-daemon: ${startResult.details ?? "unknown error"}\n`);
3013
+ return false;
3014
+ }
3015
+ process.stdout.write(" started gnome-keyring-daemon for this session.\n");
3016
+ return true;
3017
+ };
3018
+ const runLinuxSecretStoreChecklist = async () => {
3019
+ const gnomeKeyringInstalled = await isCommandAvailable("gnome-keyring-daemon");
3020
+ const secretToolInstalled = await isCommandAvailable("secret-tool");
3021
+ const gnomeKeyringRunning = await checkGnomeKeyringRunning();
3022
+ return [
3023
+ {
3024
+ id: "gnome-keyring-installed",
3025
+ question: "Is gnome-keyring installed?",
3026
+ ok: gnomeKeyringInstalled,
3027
+ hint: "Install the `gnome-keyring` package, then log out/in (or restart your session)."
3028
+ },
3029
+ {
3030
+ id: "secret-tool-installed",
3031
+ question: "Is secret-tool installed?",
3032
+ ok: secretToolInstalled,
3033
+ hint: "Install the package that provides `secret-tool` (often `libsecret-tools`)."
3034
+ },
3035
+ {
3036
+ id: "gnome-keyring-running",
3037
+ question: "Is gnome-keyring running?",
3038
+ ok: gnomeKeyringRunning.ok,
3039
+ details: gnomeKeyringRunning.details,
3040
+ hint: "Start/unlock gnome-keyring-daemon in your session (for a quick test: `gnome-keyring-daemon --start --components=secrets`)."
3041
+ }
3042
+ ];
3043
+ };
3044
+ const maybeRunLinuxSecretStoreChecklist = async () => {
3045
+ if (!isInteractiveTerminal()) {
3046
+ process.stdout.write(" Tip: run `cdx doctor` in an interactive terminal to start guided Linux secret-store checks.\n");
3047
+ return;
3048
+ }
3049
+ const shouldRunChecklist = await p.confirm({
3050
+ message: "Run guided Linux secret-store checks now? (gnome-keyring installed, secret-tool installed, gnome-keyring running)",
3051
+ initialValue: true
3052
+ });
3053
+ if (p.isCancel(shouldRunChecklist) || !shouldRunChecklist) {
3054
+ process.stdout.write(" Guided Linux checks skipped.\n");
3055
+ return;
3056
+ }
3057
+ process.stdout.write(" Guided Linux checks:\n");
3058
+ const checklist = await runLinuxSecretStoreChecklist();
3059
+ let passed = 0;
3060
+ for (let i = 0; i < checklist.length; i++) {
3061
+ const item = checklist[i];
3062
+ if (item.ok) {
3063
+ passed += 1;
3064
+ process.stdout.write(` ${i + 1}/3 ${item.question} yes\n`);
3065
+ continue;
3066
+ }
3067
+ process.stdout.write(` ${i + 1}/3 ${item.question} no\n`);
3068
+ if (item.details) process.stdout.write(` details: ${item.details}\n`);
3069
+ if (item.hint) process.stdout.write(` hint: ${item.hint}\n`);
3070
+ if (item.id === "gnome-keyring-running") {
3071
+ if (await maybeOfferToStartGnomeKeyring()) {
3072
+ const runningNow = await checkGnomeKeyringRunning();
3073
+ if (runningNow.ok) {
3074
+ passed += 1;
3075
+ process.stdout.write(" re-check: gnome-keyring-daemon is now running.\n");
3076
+ } else process.stdout.write(` re-check still failing: ${runningNow.details ?? "process not detected"}\n`);
3077
+ }
3078
+ }
3079
+ }
3080
+ process.stdout.write(` Guided checklist summary: ${passed}/${checklist.length} checks passed.\n`);
3081
+ };
2794
3082
  const registerDoctorCommand = (program) => {
2795
3083
  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) => {
2796
3084
  try {
@@ -2850,6 +3138,7 @@ const registerDoctorCommand = (program) => {
2850
3138
  process.stdout.write(` ⚠ ${probeResult.stage} failed: ${probeResult.error.message}\n`);
2851
3139
  const guidance = getSecretStoreProbeGuidance(process.platform);
2852
3140
  if (guidance) process.stdout.write(` ${guidance}\n`);
3141
+ if (process.platform === "linux") await maybeRunLinuxSecretStoreChecklist();
2853
3142
  }
2854
3143
  }
2855
3144
  if (process.platform === "darwin" && !options.checkKeychainAcl) {
@@ -2909,7 +3198,7 @@ const registerDoctorCommand = (program) => {
2909
3198
  const registerHelpCommand = (program) => {
2910
3199
  program.command("help").description("Show available commands and usage information").argument("[command]", "Show help for a specific command").action((commandName) => {
2911
3200
  if (commandName) {
2912
- const command = program.commands.find((entry) => entry.name() === commandName);
3201
+ const command = program.commands.find((entry) => entry.name() === commandName || entry.aliases().includes(commandName));
2913
3202
  if (command) {
2914
3203
  command.outputHelp();
2915
3204
  return;
@@ -3528,8 +3817,52 @@ const executeUpdate = async (command, args) => {
3528
3817
  });
3529
3818
  });
3530
3819
  };
3820
+ const executeCapture = async (command, args) => await new Promise((resolve) => {
3821
+ const child = spawn(command, args, { stdio: [
3822
+ "ignore",
3823
+ "pipe",
3824
+ "pipe"
3825
+ ] });
3826
+ let stdout = "";
3827
+ child.stdout?.on("data", (chunk) => {
3828
+ stdout += chunk.toString();
3829
+ });
3830
+ child.once("error", () => {
3831
+ resolve({
3832
+ ok: false,
3833
+ output: ""
3834
+ });
3835
+ });
3836
+ child.once("close", (code) => {
3837
+ resolve({
3838
+ ok: code === 0,
3839
+ output: stdout.trim()
3840
+ });
3841
+ });
3842
+ });
3843
+ const getInstalledCdxVersion = async () => {
3844
+ const attempts = [{
3845
+ command: "cdx",
3846
+ args: ["--version"]
3847
+ }];
3848
+ if (process.argv[0] && process.argv[1]) attempts.push({
3849
+ command: process.argv[0],
3850
+ args: [process.argv[1], "--version"]
3851
+ });
3852
+ for (const attempt of attempts) {
3853
+ const result = await executeCapture(attempt.command, attempt.args);
3854
+ if (!result.ok || !result.output) continue;
3855
+ const version = result.output.split(/\r?\n/).at(-1)?.trim();
3856
+ if (version) return version;
3857
+ }
3858
+ return null;
3859
+ };
3531
3860
  const registerUpdateSelfCommand = (program) => {
3532
- program.command("update-self").description("Update cdx to the latest version").option("--manager <manager>", "Select update manager (auto|bun|npm|deno)", "auto").option("--dry-run", "Print selected manager and update command without executing").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
3861
+ program.command("update-self").aliases([
3862
+ "self-update",
3863
+ "update",
3864
+ "updte"
3865
+ ]).description("Update cdx to the latest version").option("--manager <manager>", "Select update manager (auto|bun|npm|deno)", "auto").option("--dry-run", "Print selected manager and update command without executing").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
3533
3866
  try {
3534
3867
  const requestedManager = options.manager ?? "auto";
3535
3868
  if (![
@@ -3598,8 +3931,9 @@ const registerUpdateSelfCommand = (program) => {
3598
3931
  }
3599
3932
  }
3600
3933
  await executeUpdate(command.command, command.args);
3934
+ const installedVersion = await getInstalledCdxVersion();
3601
3935
  process.stdout.write("Update completed.\n");
3602
- process.stdout.write("Run `cdx version` to verify the installed version.\n");
3936
+ process.stdout.write(`Installed version: ${installedVersion ?? "unknown"}\n`);
3603
3937
  } catch (error) {
3604
3938
  exitWithCommandError(error);
3605
3939
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bjesuiter/codex-switcher",
3
- "version": "1.8.1",
3
+ "version": "1.8.3",
4
4
  "type": "module",
5
5
  "description": "CLI tool to switch between multiple OpenAI accounts for OpenCode",
6
6
  "bin": {