Switching, adding, editing, sync, universal providers and entering or
leaving local routing now write Codex through the write engine. Only
routing, credential, model, reasoning, context-window and compatibility
fields are replaced; MCP servers, [projects], comments and the user's own
provider tables stay byte-for-byte. auth.json, config.toml, the model
catalog, the managed-account marker and a device-local login stash commit
as one operation, so a crash at any point rolls forward on startup.
- Third-party routes are always written as [model_providers.custom] with
the key in experimental_bearer_token. Rows that use another id, a
top-level openai_base_url or a legacy reserved table are normalized on
the way out. Switching to an official card leaves a dormant custom table
(local proxy address plus placeholder credential) so older third-party
sessions can still resume.
- requires_openai_auth follows the route's auth and whether a login is on
disk; keyring and ephemeral credential stores are handled separately.
- A switch is refused before anything is written when the active profile
overrides the route, the selected table is missing or built in, a
keyless row would fall back to the official login, or the row has no
auth.
- Provider tables an older version wrote are removed only when they can
be proven to be CC Switch's. Tables a profile references are kept, and
reserved ids that cannot be proven are renamed.
- Provider-exclusive fields, including web_search and a row's own
model_catalog_json, are removed on the way out only when live still
holds the previous provider's value.
- No more backfill. A native ChatGPT login that CC Switch deletes or
overwrites is kept in ~/.cc-switch/codex-login-stash.json (0600, not
synced) and restored when switching back to an official card; logging
out on an official card discards it. Login snapshots already in
official rows seed the stash once. A leftover API key in auth.json is
deleted only when it matches a third-party row's key.
- Managed accounts fetch their token before taking the lock. If the CLI
refreshes the login during a switch, the switch stops before writing
anything. Once files are published, a later failure no longer rolls the
row back; startup finishes the switch.
- Local routing: third-party routes get a fixed local-proxy table and the
official route uses cc-switch-official plus the client's own login. The
contract covers every field written, so a route switch with the same
contract touches no files.
- The Codex editor shows what config.toml will look like after switching.
On save, key fields go to the row and other edits go to live as global
settings with a three-way check. Row fields that never reach live are
listed read-only and can be copied.
- The Codex common-config snippet is frozen like Claude's; its UI and the
TOML snippet command are removed. New Codex rows keep
commonConfigEnabled: true for older versions that sync them.
- The write engine can delete a file as part of an operation.
- Integration-test mutexes tolerate poisoning so one failure no longer
cascades into unrelated tests.
- Format code in app_config.rs to comply with rustfmt rules
- Remove extra blank line in config.rs
- Format test code in app_config_load.rs for consistency
BREAKING CHANGE: Runtime auto-migration from v1 to v2 config format has been removed.
Changes:
- Remove automatic v1→v2 migration logic from MultiAppConfig::load()
- Improve v1 detection using structural analysis (checks for 'apps' key absence)
- Return clear error with migration instructions when v1 config is detected
- Add comprehensive tests for config loading edge cases
- Fix false positive detection when v1 config contains 'version' or 'mcp' fields
Migration path for users:
1. Install v3.2.x to perform one-time auto-migration, OR
2. Manually edit ~/.cc-switch/config.json to v2 format
Rationale:
- Separates concerns: load() should be read-only, not perform side effects
- Fail-fast principle: unsupported formats should error immediately
- Simplifies code maintenance by removing migration logic from hot path
Tests added:
- load_v1_config_returns_error_and_does_not_write
- load_v1_with_extra_version_still_treated_as_v1
- load_invalid_json_returns_parse_error_and_does_not_write
- load_valid_v2_config_succeeds