## Context 用户在多家 Claude Code 兼容的 coding plan 间切换,本质是"换一组环境变量再启动 `claude`"。当前痛点: - 不同 provider 用到的 env 变量集合会有差异(例如有的用 `ANTHROPIC_AUTH_TOKEN`,有的用 `ANTHROPIC_API_KEY` + `ANTHROPIC_BASE_URL`,有的还设 `ANTHROPIC_MODEL` / `ANTHROPIC_SMALL_FAST_MODEL`),漏清理会造成"以为切了但没切"的串扰; - 在 shell 里写一堆 alias/函数维护成本高,也不利于多机同步。 该工具目标是做成一个薄封装:配置集中、切换确定性强、启动 `claude` 后行为与直接在 shell 里运行一致(stdio、信号、退出码都透传)。 ## Goals / Non-Goals **Goals:** - 单个 Go 二进制,零外部运行时依赖即可使用。 - 配置文件人类可读可手改,支持多 provider,每 provider 任意条 env 变量。 - 切换流程确定性:清理 → 注入 → 启动;不污染用户当前 shell。 - 子进程退出码、stdio、信号(SIGINT/SIGTERM/SIGHUP)与裸跑 `claude` 等价。 - 非交互 (`cc-switch use `) 与交互 (`cc-switch` 裸跑或 `cc-switch use` 无参) 两种入口。 **Non-Goals:** - 不做 API Key 加密存储(v1 依赖 0600 文件权限;后续可评估接入 OS keychain)。 - 不管理 shell rc 文件、不写 profile、不 export 到父 shell。 - 不内置 provider 模板/预设(v1 要求用户自填 env 键值)。 - 不做多租户/团队共享配置同步(留给用户自己用 dotfiles/Git 同步)。 - 不代理或改写 `claude` 行为,只负责"换环境再启动"。 ## Decisions ### Decision: 配置格式与位置 采用 YAML,路径遵循 XDG:默认 `$XDG_CONFIG_HOME/cc-switch/config.yaml`(未设置时回落到 `~/.config/cc-switch/config.yaml`);`CC_SWITCH_CONFIG` 环境变量可覆盖。 - **Why**: YAML 对多行、嵌套 map 友好;env 变量很适合 `key: value` 展示。用户可能同时维护多个 provider,手改体验优于 TOML/JSON。 - **Alternatives considered**: TOML(层次表达稍弱);JSON(无注释);split file per provider(增加 IO 复杂度,收益不明显)。 ### Decision: 环境变量"清理"的实现方式 **构造子进程的 `Env` 切片**,而非修改 `cc-switch` 自身的 `os.Environ`: 1. 读取 `os.Environ()` 作为基础; 2. 从中剔除 key ∈ `union(all providers' env keys)`; 3. 追加选中 provider 的 key=value 对; 4. 赋给 `exec.Cmd.Env`,启动 `claude`。 这样"清理 → 注入"只作用于子进程环境,`cc-switch` 自身进程和父 shell 完全不受影响;"退出后清理"由进程隔离天然保证(子进程环境随其生命周期结束)。对外我们保留"退出后再清理一次"的语义承诺,在 `cc-switch` 退出前打印一条 trace(可选 `--verbose`)确认,但不做实际 mutation。 - **Why**: 避免对 `os.Environ` 做有状态 mutation 带来的测试复杂度和泄漏风险;读写集中在一处易于单测。 - **Alternatives considered**: 用 `os.Setenv` / `os.Unsetenv` 再 fork 子进程——需要在 defer 中回滚,panic 路径容易漏;对多 goroutine 场景不友好。 ### Decision: `claude` 可执行文件解析 优先使用配置里的 `claude_path`(绝对或相对路径,支持 `~`);未配置时 `exec.LookPath("claude")`;都找不到则以明确错误退出(退出码保留给用户,不伪装成 `claude` 自身错误)。 - **Why**: 不少用户把 `claude` 装在 `~/.claude/local/claude` 这类非 PATH 路径,需要显式覆盖;同时多数用户 PATH 里就有,不应强制配置。 ### Decision: 运行方式——subprocess 而非 exec-replace 使用 `os/exec` 启动 `claude`,`cc-switch` 作为父进程守护: - 透传 stdin/stdout/stderr; - 注册信号处理器,把 SIGINT/SIGTERM/SIGHUP 转发给子进程; - `cmd.Wait()` 完成后提取 `ExitCode()` 作为 `cc-switch` 自身退出码。 不使用 `syscall.Exec`(替换当前进程映像)。 - **Why**: 保留子进程退出后做记账/清理/提示的机会;跨平台兼容(Windows 无 `exec` 语义);便于未来加 `--dry-run`、`--print-env` 等 hook。 - **Trade-off**: 多一层进程,但 `claude` 本身就是长交互进程,进程链深一层对用户无感。 ### Decision: CLI 框架 采用 `github.com/spf13/cobra` 做子命令装配,`gopkg.in/yaml.v3` 做 YAML 序列化。 - **Why**: cobra 是 Go 生态事实标准,子命令/help/flag 等基础设施完善,减少样板;yaml.v3 能保留 key 顺序,用户手改 diff 友好。 - **Alternatives considered**: 标准库 `flag`(子命令层级要自己搭)、`urfave/cli`(也可行但生态小一号)。 ### Decision: Provider 选择的交互 UI 优先使用轻量方案:在 tty 下用简单数字菜单(stdin 读一行),不引入完整 TUI 库。若未来体验不足再评估 `survey`/`bubbletea`。 - **Why**: 二进制体积、依赖面、CI 环境兼容性(非 tty 时自动退化为报错提示用户给出 ``)。 ### Decision: 配置文件权限 创建配置文件与其父目录时使用 `0700`(目录)、`0600`(文件)。读写配置前若发现权限过宽(group/other 可读),打印一条 warning 但不中止——降低误操作阻塞成本。 - **Why**: 配置里存 API key,POSIX 权限是第一道防线;warning 留一条可见线索。 ### Decision: 原子写入 保存配置使用 write-to-temp-then-rename:先写 `config.yaml.tmp`,fsync,再 `os.Rename` 为目标文件。 - **Why**: 防止编辑/并发导致半写坏文件丢掉所有 provider 配置。 ### Decision: Env 值的间接引用语法 支持值形如 `env:VAR_NAME` 从启动时的父进程环境中读取实际值(正则约束 `^env:[A-Za-z_][A-Za-z0-9_]*$`)。解析时机:**启动阶段**,在"并集清理"之前先对 `os.Environ()` 取快照,针对选中 provider 的 env 值逐一解析引用;引用的 VAR_NAME 未在快照中出现则非零退出并打印哪一个 key 引用的哪个 VAR 缺失。引用的变量值本身仍被视为最终 env value,写入子进程环境前不会再做任何 shell 扩展。 - **Why**: 让用户可以把 API key 放在系统 keychain / 1Password CLI / direnv 暴露的 env 中,配置文件里只存"指针",降低明文泄露面。 - **为什么先取快照再清理**: 被引用的 VAR 本身可能恰好落在并集里(例如 provider A 用 key `ANTHROPIC_API_KEY: env:ANTHROPIC_API_KEY`),如果清理后再解析就读不到了。先快照绕过这层次序耦合。 - **Alternatives considered**: `${VAR}` 风格(与"value 字面传递"的承诺冲突、视觉上像 shell 扩展易误解);YAML 显式结构 `{from_env: VAR}`(schema 复杂、增删改 UX 差);`!env` YAML 标签(不易被通用 YAML 工具处理)。 - **局限**: 值字面量恰好以 `env:` 开头且并非引用的情况极少,v1 不支持转义;文档明示"若确实需要字面 `env:xxx`,请改用不同写法或等 v2 转义方案"。 ### Decision: Provider 模板 在二进制中内嵌(`go:embed` 一份 `templates.yaml`)一组常见 provider 的 env key 骨架,作为 `cc-switch add --from-template ` 的起点。v1 的种子模板:`anthropic-official`、`openrouter`、`deepseek`、`moonshot`(Kimi)、`zhipu`(GLM)、`custom-base`(仅 BASE_URL + AUTH_TOKEN,给未列出的 OpenAI-兼容服务用)。每个模板包含:`description`、`env` key 列表(仅键名 + 每项可选的 `hint` 提示文案和可选的 `default` 示例 value,例如 `ANTHROPIC_BASE_URL` 默认填官方 URL)。 - **交互流程**:`add --from-template openrouter` 进入向导,逐个 key 提示输入(显示 hint;允许直接输入 value 或 `env:VAR` 引用;有 default 的回车接受)。提供 `--non-interactive` 时只带出 key 骨架写入(value 为空占位),要求用户后续 `edit` 补齐。 - **扩展途径**:v1 不支持用户自定义模板目录(non-goal);v2 可加 `$XDG_CONFIG_HOME/cc-switch/templates/`。README 会附贡献新模板到 upstream 的 PR 指引。 - **Why**: 让首次上手不用翻各家文档即可拼出 env;限制在"仅键名骨架"避免工具把"该用哪个 model id"这种快速过时的信息硬编码。 - **Alternatives considered**: 纯文档 snippet(用户仍需手抄);远程拉取模板(新增网络依赖、安全面、离线不可用)。 ## Risks / Trade-offs - **[Risk] 明文存储 API key** → 以 0600 权限 + 文档中明示;后续版本评估 keychain 集成;提供 `env:VAR_NAME` 语法支持从外部 env 引用(v1 可先设计字段,实现留到后续)——v1 不实现该语法,但在 schema 中预留扩展位。 - **[Risk] 用户在 shell 里已经 export 了某些 ANTHROPIC_* 变量,和 provider 冲突** → 因为我们对 `union(all providers' keys)` 做清理,即便这些变量来自父 shell 也会被覆盖;但如果用户 export 的某个 key 未出现在**任何** provider 配置里,则会被原样继承(这是预期行为)。文档中明确说明"union"范围,并建议用户把所有相关 key 都登记到至少一个 provider 里以便统一清理。 - **[Risk] 信号转发在 Windows 与 Unix 语义差异** → v1 明确只支持 macOS/Linux(goreleaser/发布时不打 Windows 包);Windows 用户可走 WSL。 - **[Risk] 交互菜单在非 tty(CI、管道)下卡死** → 检测 `stdin.IsTerminal()`,非 tty 时要求必须显式给 ``,否则非零退出并打印提示。 - **[Trade-off] 不做 exec-replace** → 多一层父进程,但换来跨平台、可测试性、未来扩展空间,值得。 - **[Trade-off] 无内置 provider 模板** → 用户第一次上手要自己查各家文档;作为补偿,`cc-switch add` 提供交互式向导,把常见 key 列出来让用户勾选填值。 ## Migration Plan 这是全新仓库、无历史用户: 1. 先落地 module 骨架与基础命令(`add`/`list`/`use`); 2. 实现 env 计算与子进程启动; 3. 补齐 `edit`/`remove`/`config`; 4. 加交互选择; 5. 发布 v0.1.0(Homebrew tap + `go install` 双通道)。 无回滚策略需求(新项目)。 ## Open Questions - 是否在 `add` 时提供常见 provider 的 env key preset(Anthropic 官方、DeepSeek、Kimi、Moonshot、OpenRouter 等兼容层)作为可选引导?—— 倾向 v1 不做,由 README 给出 snippet;v2 观察用户反馈再加。 - 是否支持"一次性覆盖":`cc-switch use foo --set ANTHROPIC_MODEL=claude-opus-4-7`?—— v1 不做,保持配置单一事实来源;v2 可加 `--set/--unset` 作为临时 override。 - 是否需要 `cc-switch doctor` 做健康检查(claude 路径存在、配置权限正确、tty 检测)?—— v1 暂不做,但在 `list -v` 中附带展示 claude 路径解析结果。