Skip to content
Merged
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
48 changes: 45 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ tar = "0.4"
zip = { version = "9", default-features = false, features = ["deflate"] }
libc = "0.2"
rpassword = "7"
# Export bundles: AES-256-GCM/HMAC/CSPRNG from ring (already in the lockfile via
# rustls) plus Argon2id for the passphrase KDF.
ring = "0.17"
argon2 = { version = "0.5", default-features = false, features = ["alloc"] }
zeroize = "1"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "=0.61.2", features = [
Expand All @@ -81,6 +86,13 @@ windows-sys = { version = "=0.61.2", features = [
axum = "0.8"
chrono = { version = "0.4", features = ["clock"] }

# The 64 MiB Argon2id KDF is unbearably slow unoptimised; keep debug tests fast.
[profile.dev.package.argon2]
opt-level = 3

[profile.dev.package.blake2]
opt-level = 3

[profile.release]
strip = true
lto = true
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Run `codex-switch doctor` to inspect engine selection; `--desktop-codex <path>`
## What it does

- Saves, imports, renames, switches, and recoverably deletes Codex profiles.
- Moves everything to another machine: `codex-switch export` writes all profiles, providers and `config.toml` into one passphrase-encrypted bundle, and `codex-switch import <bundle>` restores it offline with a dry-run preview and `skip` / `overwrite` / `merge` conflict strategies. Refresh tokens are single-use, so use each account on one machine only.
- **Beta — custom API providers:** save a Responses-compatible endpoint and API key, configure multiple models with per-model reasoning and `web_search`, fetch the gateway's model catalog, and launch Codex through native per-run profiles. Compatibility depends on the gateway and model. Providers do not show ChatGPT quota or participate in ChatGPT automatic account selection:

```bash
Expand Down
1 change: 1 addition & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ codex-switch launch # 用最佳账号启动 Codex
## 功能一览

- 保存、导入、重命名、切换和可恢复地删除 Codex 账号。
- 整体迁移到另一台机器:`codex-switch export` 把全部账号、提供方和 `config.toml` 打包成一个用口令加密的文件,`codex-switch import <导出包>` 离线导入,支持 `--dry-run` 预览和 `skip` / `overwrite` / `merge` 冲突策略。refresh token 只能使用一次,同一账号请只在一台机器上使用。
- **Beta:自定义 API 提供方。** 保存兼容 Responses 协议的端点和 API 密钥,为每个端点配置多个模型及各自的思考等级、`web_search`,获取网关模型目录并启动 Codex。兼容性取决于网关和具体模型;提供方不展示 ChatGPT 额度,也不参与自动选号。TUI Providers 页可用同一张表单新增/编辑(`a` / `e`),按 `Enter` / `o` 启动 Codex;启动使用 Codex 原生的每次运行独立 profile(`$CODEX_HOME` 中的 `cs-*.config.toml`),共享认证与 ChatGPT 配置保持不变:

```bash
Expand Down
8 changes: 8 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## Unreleased

- **整机导出/导入。** `codex-switch export [FILE]` 把全部账号、提供方(含 API 密钥和模型目录)与 `config.toml` 写入一个 `.csbundle`,用 Argon2id(64 MiB、t=3、p=1)派生密钥、AES-256-GCM 加密,文件权限 `0600`;`--plaintext` 需要确认。`codex-switch import <导出包>` 按内容识别导出包并离线导入(仍执行管理员策略和身份检查),支持 `--dry-run`、`--strategy skip|overwrite|merge`、`--activate`,被替换的文件备份到 `import-backups/`,出错时回滚。原有的 `auth.json` 导入不变。refresh token 只能用一次,两条命令都会提示同一账号不要在两台机器上同时使用。
- 导入只在当前账号的 live `auth.json` 属于同一账号、且能证明比导入内容旧时才替换它:`merge` 遇到更新或无法比较的 live 会跳过,`overwrite` 需要再加 `--replace-live`;live 属于其他账号(如未跟踪的 `codex login`)时不会被改动,`--activate` 也不会在无确认时覆盖它。计划和 `--json` 中新增 `replaces_live`。
- 同一导出包里同一账号出现多次时只写入能证明最新的一份,无法判断时整组跳过;本地有多个同账号 profile 时标为 `ambiguous`。
- 导入未加密的导出包、或会新增带 `codex_config` 覆盖项的提供方时,也需要交互确认或 `--yes`;计划中列出提供方的 `base_url` 和覆盖项数量。
- 中断的导入会在日志中记录每个被替换文件与其备份的对应关系,下次导入给出逐条恢复步骤;`import --dry-run`、`use`、`launch`、`export` 会提示存在未完成的导入。KDF 参数上限收紧为 256 MiB/t=6;`--json` 下的参数用法错误也输出 JSON。

## v20261015.2.0 — 2026-10-15

- TUI 主面板和账号菜单隐藏已确认无 Daybreak Blue/Red 资格的详情行;未知状态、查询失败和已有资格仍保留展示。
Expand Down
1 change: 1 addition & 0 deletions docs/wiki/Architecture-Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ If periodic refresh or warmup is desired, a user-managed cron, systemd user time
| `$CODEX_SWITCH_HOME/providers/<alias>/models.json` | Generated Codex model catalog for `/model` |
| `$CODEX_SWITCH_HOME/current` | Current alias marker |
| `$CODEX_SWITCH_HOME/deleted-profiles/` | Recoverable profile archives |
| `$CODEX_SWITCH_HOME/import-backups/<timestamp>/` | Bundle-import backups of replaced files plus `journal.json` (contains secrets; `0700`/`0600`) |
| `$CODEX_SWITCH_HOME/cache.json` | Usage, workspace metadata, and rejected-credential cache |
| `$CODEX_SWITCH_HOME/config.toml` | Application configuration |
| `$CODEX_SWITCH_HOME/logs/` | Rotated diagnostic logs |
Expand Down
25 changes: 25 additions & 0 deletions docs/wiki/Chinese-Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,31 @@ ChatGPT 文件登录需要 `cli_auth_credentials_store = "file"`。登录、导
codex-switch import ~/auth-backups
```

### 迁移到另一台机器(导出 / 导入)

```bash
# 机器 A:导出全部账号、提供方(含 API 密钥和模型目录)与 config.toml
codex-switch export ~/codex-switch.csbundle # 交互输入两次口令
codex-switch export out.csbundle --no-config # 不带 config.toml
codex-switch export out.csbundle --no-providers # 不带提供方

# 机器 B:先预览,再导入
codex-switch import ~/codex-switch.csbundle --dry-run
codex-switch import ~/codex-switch.csbundle # 默认 --strategy skip
codex-switch import ~/codex-switch.csbundle --strategy merge --yes --activate
```

- 导出包默认用 Argon2id(64 MiB、t=3、p=1)派生密钥,AES-256-GCM 加密,整个文件头参与认证;文件权限 0600,已存在的目标文件需 `--force` 才会覆盖。脚本中可用 `--passphrase-stdin` 从标准输入第一行读取口令,口令不接受命令行参数或环境变量。
- `--plaintext` 写出未加密的导出包,需要交互确认或 `--yes`;文件里是可直接使用的 token 和 API 密钥,导入后请立即删除。
- **refresh token 只能使用一次。** 导出不会让源机器上的账号失效;两台机器同时使用同一账号时,先刷新的一方会让另一方报 `refresh_token_reused`,需要重新 `codex-switch login <别名>`。
- 导入离线进行,不调用 Usage API,但仍执行管理员工作区策略和身份检查;任何条目校验失败都会在写入前整体中止。口令错误、文件被改动、版本过新分别给出不同错误(`--json` 下的 `code` 为 `wrong_passphrase`、`bundle_tampered`、`unsupported_bundle_version`)。
- 冲突策略:`skip`(默认,保留本地)、`overwrite`(用导入内容替换,旧文件备份到 `import-backups/`)、`merge`(账号保留 `last_refresh` 更新的一份;`config.toml` 只补充本地缺少的键;提供方暂按 `skip` 处理)。别名被其他账号或提供方占用时(大小写不敏感)总是改名导入为 `<别名>_2`,绝不覆盖其他账号。同一导出包中同一账号出现多次时只写入 `last_refresh` 可证明最新的一份,无法判断时整组跳过;本地有多个同账号 profile 时标为 `ambiguous`。`overwrite` / `merge`、替换 live `auth.json`、新增带 `codex_config` 覆盖项的提供方,以及未加密导出包的任何写入都需要确认,非交互或 `--json` 时需要 `--yes`;预览中会列出提供方的 `base_url` 和覆盖项数量。
- live `~/.codex/auth.json` 和 profile 同等对待:导入更新当前账号时,只有 live 属于同一账号、且可证明比导入内容旧(`refresh_token` 相同,或 `last_refresh` 更早)才会替换。Codex 刚刷新过、更新或无法比较时,`merge` 跳过该账号,`overwrite` 需要再加 `--replace-live`;live 属于其他账号时绝不改动。`--json` 每项都有 `replaces_live`。
- `--dry-run` 会把 `config.toml` 中的 `[proxy]` 标为“machine-specific, review”,代理通常与机器相关,导入后请检查。
- 导入默认不切换当前账号,只提示源机器当前账号;加 `--activate[=别名]` 才会切换。与 `use` 一样,live `auth.json` 不属于任何已保存账号时,未经交互确认不会被覆盖。
- 写入中途出错会按日志反向回滚;进程崩溃留下 `in-progress` 日志时,日志里记录了每个被替换文件对应的备份,下次导入会拒绝执行并逐条列出“备份 -> 原路径”的恢复步骤;`import --dry-run`、`use`、`launch`、`export` 会提示存在未完成的导入。
- 缓存、日志、已删除账号、`recovery/` 和 Codex 自身文件不导出。

## 日常操作(ChatGPT 账号)

| 目的 | 命令 / 操作 |
Expand Down
4 changes: 3 additions & 1 deletion docs/wiki/Command-Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ The installed binary remains authoritative: use `codex-switch --help` and `codex
|---|---|
| `login [--device] [alias]` | Add or reauthorize a profile through browser PKCE or device-code login. If the alias already exists, it is reauthorized; otherwise a new profile is created. |
| `import <path> [alias]` | Validate and import one `auth.json`, or recursively scan a directory for JSON files. The alias applies to single-file imports only; directories auto-assign aliases. An account that is already saved (same file, or same `account_id` and email) is skipped instead of duplicated, so its single-use refresh token is not spent. |
| `export [FILE] [--plaintext [--yes]] [--force] [--passphrase-stdin] [--no-providers] [--no-config]` | Write every saved profile, custom provider (API key and `models.json` included) and `config.toml` into one bundle, default `./codex-switch-<YYYYMMDD-HHMMSS>.csbundle`, mode `0600`. The bundle is encrypted with AES-256-GCM under an Argon2id key (m = 64 MiB, t = 3, p = 1); the whole header is authenticated. The passphrase is prompted twice, or read from the first stdin line with `--passphrase-stdin`; it is never taken from argv or the environment. An existing file is refused unless `--force`. `--plaintext` writes an unencrypted bundle and needs interactive confirmation or `--yes` (always `--yes` under `--json` or without a TTY). Caches, logs, deleted profiles, `recovery/` and Codex-owned files are not exported. Refresh tokens are single-use: the exported accounts stay valid on the source, and using one account on both machines makes one of them log in again. |
| `import <bundle> [--strategy skip\|overwrite\|merge] [--dry-run] [--yes] [--activate[=ALIAS]] [--passphrase-stdin] [--replace-live]` | Import an export bundle, detected by content (`"format": "codex-switch-export"`), not extension. Offline: no Usage API call, but the managed workspace policy, the `account_id` requirement and the identity rules still apply, and any invalid entry aborts before a write. Same account (`account_id` + email) or same provider (alias + `identity_id`) with different content, or a different `config.toml`, is a conflict handled by `--strategy` (default `skip`; `overwrite` replaces after a backup; `merge` keeps the newer credential by `last_refresh` and adds only missing `config.toml` keys; provider merge is treated as `skip`). A different account under a taken alias (case-insensitive) is always imported under `<alias>_2`. Email-only matches, and accounts saved under several local aliases, are skipped as `ambiguous`; an account that appears more than once in the bundle is written once, from the entry with the provably newest `last_refresh` (none if that cannot be shown). `overwrite`/`merge`, replacing the live `auth.json`, providers that add `codex_config` overrides, and any write from an unencrypted bundle need confirmation or `--yes`; the plan lists each provider's `base_url` and override count. `--dry-run` prints the plan and writes nothing; `[proxy]` in `config.toml` is flagged "machine-specific, review". Replaced files are backed up under `import-backups/<UTC timestamp>/` with a `journal.json`; an error rolls back. An import interrupted by a crash leaves an `in-progress` journal that maps every replaced path to its backup; the next import refuses and prints those pairs, and `import --dry-run`, `use`, `launch` and `export` warn. `current` changes only with `--activate` (default alias: the source machine's current account), which, like `use`, will not overwrite a live `auth.json` that belongs to no saved profile without interactive confirmation. When the local current profile is updated, its live `auth.json` is replaced too only if it belongs to the same account and is provably older than the imported credentials (same `refresh_token`, or an earlier `last_refresh`); otherwise `merge` skips the profile and `overwrite` skips it unless `--replace-live` is given. A live file of another account is never touched. JSON items carry `replaces_live`. Bundle-only flags on an `auth.json` path, or an alias with a bundle, are usage errors (exit 2). |
| `doctor [--desktop-codex <path>]` | Check the selected Codex runtime and, optionally, a desktop app's explicitly supplied bundled engine. Runtime selection uses `CODEX_SWITCH_CODEX`, then PATH, then known macOS desktop installations. A missing PATH CLI alone does not fail a healthy desktop-only setup. Invalid selected or explicitly supplied engines produce a nonzero exit. |
| `list [-f]` | Show profiles, usage, and availability; `-f` / `--force` bypasses the cache. |
| `use [alias] [--consume-card]` | Switch explicitly, or omit the alias to auto-select with the unified scoring algorithm. When the pool is exhausted, `--consume-card` consumes the earliest-expiring reset card to revive an account (auto-select only; ignored when an alias is given). |
Expand All @@ -33,7 +35,7 @@ The installed binary remains authoritative: use `codex-switch --help` and `codex

| Option | Environment variable | Behavior |
|---|---|---|
| `--json` | — | Compact structured output (supported by `list`, `use`, `launch`, `warmup`, `reset-card`, `rename`, `delete`, `login`, `import`, `self-update`, `doctor`, `provider add`, `provider list`, `provider show`, `provider rename`, `provider remove`, `provider fetch-models`, `provider probe`). `doctor --json` reports `ok`, `minimum_version`, `aligned_version`, required `runtime_note`, `core_account_commands_available`, `launch_runtime_available`, `path_cli`, `selected_runtime`, `selected_runtime_source`, `desktop_codex`, and (when both versions parse) `versions_match` plus `version_relation`; each executable report has `executable`, `version`, `status`, and optional `note`. `launch --json` prints one envelope after Codex exits; each captured Codex stream is limited to 1 MiB and has a `*_truncated` flag. |
| `--json` | — | Compact structured output (supported by `list`, `use`, `launch`, `warmup`, `reset-card`, `rename`, `delete`, `login`, `import`, `export`, `self-update`, `doctor`, `provider add`, `provider list`, `provider show`, `provider rename`, `provider remove`, `provider fetch-models`, `provider probe`). `doctor --json` reports `ok`, `minimum_version`, `aligned_version`, required `runtime_note`, `core_account_commands_available`, `launch_runtime_available`, `path_cli`, `selected_runtime`, `selected_runtime_source`, `desktop_codex`, and (when both versions parse) `versions_match` plus `version_relation`; each executable report has `executable`, `version`, `status`, and optional `note`. `launch --json` prints one envelope after Codex exits; each captured Codex stream is limited to 1 MiB and has a `*_truncated` flag. |
| `--json-pretty` | — | Indented structured output. |
| `--proxy <URL>` | `CS_PROXY` | Override proxy configuration for this process; supports `http(s)://`, `socks4://`, `socks5://`, and `socks5h://` (remote DNS). |
| `--color <auto\|always\|never>` | `CS_COLOR` | Control CLI terminal color. `NO_COLOR` disables CLI color regardless of this option. The TUI still paints its designed palette. |
Expand Down
Loading
Loading