farion1231/cc-switch
The renderer never writes CLI configs itself. It calls invoke(); command wrappers parse AppType and delegate to services, which lock the rusqlite connection, update SQLite, and atomically rewrite live files under the user home directory. Exclusive apps backfill the outgoing provider from the live file before the new provider is written. Additive apps keep every provider in one native document. When proxy takeover is active, live files point at the local listener and the real upstream config sits in a live backup. Request bodies hit Axum routes, ProviderRouter, format adapters, then reqwest/hyper upstream. Usage scripts are data: QuickJS builds a request descriptor, Rust performs HTTP, QuickJS extracts the result. SQLite mutations wake WebDAV and S3 auto-sync.
- UI switch path is useProviderActions.switchProvider to providersApi.switch, which invokes switch_provider. The command parses AppType, spawn_blocking's onto switch_provider_internal, which calls ProviderService::switch.
- Exclusive switch order in switch_normal: resolve effective current id, sync common-config snippet from live, strip that snippet, save the outgoing provider (backfill), preflight managed Codex OAuth, then write the target live files and update current pointers. Additive apps skip backfill because all providers already coexist. Codex managed OAuth captures a four-file snapshot and restores it if the live commit fails.
- If a live backup row exists or detect_takeover_in_live_config_for_app is true, switch takes proxy_service.lock_switch_for_app and calls hot_switch_provider_inner instead of writing upstream live config. Official providers that fail official_provider_supports_proxy_takeover are rejected with switch.official_blocked_by_proxy. MCP sync is skipped during takeover.
- read_live_settings dispatches per AppType: Codex reads live TOML/auth and merges the cc-switch model catalog projection; Claude reads settings JSON; Gemini joins .env and settings.json; OpenCode, OpenClaw, Hermes, and Grok read their native documents; Mcode returns custom_provider entries; Pi and Claude Desktop refuse generic live import.
- sync_current_to_live walks AppType::all(), skips Pi and Mcode, syncs all providers for additive apps, and for exclusive apps writes only the current provider unless takeover is active (then it updates the restore backup). It then calls McpService::sync_all_enabled and SkillService::sync_to_app, aggregating failures.
- atomic_write creates a sibling temp file (pid, timestamp, counter), flushes, then renames over the target. atomic_write_private forces Unix mode 0600 for credential files. MiniMax write_and_commit rolls the previous YAML bytes back if the database commit fails.
- ProxyServer::start binds listen_address:listen_port, records the actual port for system-proxy detection, and accepts with a manual hyper HTTP/1.1 loop that peeks raw bytes so OriginalHeaderCases can preserve client header casing. ProviderRouter plus FailoverSwitchManager live on ProxyState and survive across requests. GeminiShadowStore and CodexChatHistoryStore hold in-memory protocol bridge state.
- execute_usage_script substitutes template variables, optionally validates base_url, evals the script in rquickjs to extract a request object, performs the HTTP call in Rust, then calls the script extractor on the response. Runtime limits are 5 seconds, 16 MiB, and a 256 KiB stack.
- Every SQLite insert, update, or delete fires register_db_change_hook, which calls webdav_auto_sync::notify_db_changed and s3_auto_sync::notify_db_changed with the table name. AppSettings in settings.json is documented as device-local and not part of that database sync. get_settings_for_frontend clears webdav password and s3 secret_access_key before they reach the UI.
- Startup also pulls models.dev pricing from the renderer via syncModelsDevPricingOnStartup after the React tree mounts, then invalidates usage and models-dev query keys. Failure is logged and does not block boot. Session usage sync runs once immediately (including cost backfill) and then every 60 seconds unless session_auto_sync_enabled is false.
- handle_deeplink_url accepts only ccswitch:// strings, parses them, emits deeplink-import or deeplink-error to the webview, and can focus the main window. The single-instance callback does the same for argv. URL logs go through url_for_log, which strips userinfo, query, and fragment and then replaces known secret substrings of length at least 8.