Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ Choose the file for your platform:

- **macOS Apple Silicon:** `Codex.Switcher_*_aarch64.dmg`
- **macOS Intel:** `Codex.Switcher_*_x64.dmg`
- **Windows:** `Codex.Switcher_*_x64-setup.exe` or `Codex.Switcher_*_x64_en-US.msi`
- **Windows (installer):** `Codex.Switcher_*_x64-setup.exe` or `Codex.Switcher_*_x64_en-US.msi`
- **Windows (portable):** `codex-switcher.exe` — single executable, no installation required
- **Linux Debian/Ubuntu:** `Codex.Switcher_*_amd64.deb`
- **Linux AppImage:** `Codex.Switcher_*_amd64.AppImage`
- **Linux RPM:** `Codex.Switcher-*-1.x86_64.rpm`
Expand All @@ -47,6 +48,28 @@ Choose the file for your platform:
> open "/Applications/Codex Switcher.app"
> ```

### Windows Portable EXE

The Windows `codex-switcher.exe` is a **self-contained executable** — no installer, no runtime, no
registry entries. Just download and run:

1. Download `codex-switcher.exe` from the latest release
2. Place it anywhere you like (e.g. `Desktop` or `C:\Tools`)
3. Double-click to launch

All account data (tokens, settings) is stored in `%USERPROFILE%\.codex-switcher\` and
`%USERPROFILE%\.codex\auth.json` — completely separate from the executable file.

### Updating

To update to a newer version:

1. Right-click the Codex Switcher tray icon → **Quit**
2. Replace the old `codex-switcher.exe` with the new one
3. Launch the new executable

Your accounts and settings are preserved — no re-login needed.

### Auto Updates

Codex Switcher checks the latest GitHub release on startup. When a newer signed
Expand Down
12 changes: 12 additions & 0 deletions src-tauri/src/api/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ pub async fn fetch_chatgpt_account_metadata(
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();

// When Cloudflare throws a managed challenge (403 + JS challenge page) the
// accounts/check endpoint becomes unavailable for non-browser clients.
// Return a readable error instead of dumping the entire HTML page.
if status.as_u16() == 403
&& (body.contains("_cf_chl_opt") || body.contains("challenge-platform"))
{
anyhow::bail!(
"Cloudflare rate-limit (403); plan info temporarily unavailable"
);
}

anyhow::bail!("Accounts check API error: {status} - {body}");
}

Expand Down
40 changes: 27 additions & 13 deletions src/hooks/useAccounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,24 @@ export function useAccounts() {
}

if (options?.refreshMetadata) {
await runWithConcurrency(
list,
async (account) => {
await invokeBackend<AccountInfo>("refresh_account_metadata", {
accountId: account.id,
});
},
maxConcurrentUsageRequests
);

list = await loadAccounts(true);
try {
await runWithConcurrency(
list,
async (account) => {
await invokeBackend<AccountInfo>("refresh_account_metadata", {
accountId: account.id,
});
},
maxConcurrentUsageRequests
);

list = await loadAccounts(true);
} catch (err) {
console.warn(
"Failed to refresh account metadata, continuing with usage refresh:",
err
);
}
}

const accountIds = list.map((account) => account.id);
Expand Down Expand Up @@ -178,8 +185,15 @@ export function useAccounts() {
) => {
try {
if (options?.refreshMetadata) {
await invokeBackend<AccountInfo>("refresh_account_metadata", { accountId });
await loadAccounts(true);
try {
await invokeBackend<AccountInfo>("refresh_account_metadata", { accountId });
await loadAccounts(true);
} catch (err) {
console.warn(
"Failed to refresh account metadata, continuing with usage refresh:",
err
);
}
}

setAccounts((prev) =>
Expand Down