## 1. 项目脚手架 - [x] 1.1 初始化 Go module:`go mod init github.com/kotoyuuko/cc-switch-cli`,设 `go 1.22+` - [x] 1.2 添加依赖:`github.com/spf13/cobra`、`gopkg.in/yaml.v3` - [x] 1.3 建立目录结构:`cmd/cc-switch/main.go`、`internal/config/`、`internal/provider/`、`internal/runner/`、`internal/cli/` - [x] 1.4 在 `cmd/cc-switch/main.go` 写空的 cobra root command,可运行 `go run ./cmd/cc-switch --help` - [x] 1.5 加入 `Makefile` 或 `justfile`:`build`、`test`、`vet`、`fmt` 常用 target - [x] 1.6 添加 `.gitignore`(忽略二进制、`dist/`、本地 `config.yaml`) ## 2. 配置层(internal/config) - [x] 2.1 定义 `Config` / `Provider` 结构体,字段与 `specs/provider-config/spec.md` 一致(`claude_path`、`default_provider`、`providers`、`env`、`description`) - [x] 2.2 实现配置路径解析:`CC_SWITCH_CONFIG` > `XDG_CONFIG_HOME` > `~/.config/cc-switch/config.yaml`;支持 `~` 展开 - [x] 2.3 实现 `Load()`:文件不存在返回空 `Config`,存在则 yaml 反序列化;对权限 > 0600 的文件打印 warning - [x] 2.4 实现 `Save()`:写 `config.yaml.tmp` → fsync → `os.Rename`;父目录缺失则以 0700 创建;新建文件以 0600 权限 - [x] 2.5 实现 provider 名校验(正则 `^[A-Za-z0-9_-]+$`)与 env 非空校验 - [x] 2.6 实现增删改查方法:`AddProvider`、`RemoveProvider`、`UpdateProvider`、`ListProviders`、`SetDefault`、`SetClaudePath`(后者校验路径存在且可执行) - [x] 2.7 实现 env value 引用识别辅助:`IsEnvRef(s string) (varName string, ok bool)` 基于正则 `^env:[A-Za-z_][A-Za-z0-9_]*$`;展示层(list -v、config show)遇到引用 value 原样输出、不脱敏 - [x] 2.8 覆盖单元测试:路径解析、load/save 原子性、权限 warning、CRUD、非法名、删除 default 清空逻辑、`IsEnvRef` 的正反例(包括 `env:`、`env:1x`、`env:FOO` 等) ## 3. Provider 运行时计算(internal/provider) - [x] 3.1 实现 `UnionEnvKeys(providers) []string`:遍历所有 provider 的 env key 去重 - [x] 3.2 实现 `ResolveEnvRefs(selected Provider, parentSnapshot map[string]string) (resolved map[string]string, err error)`:对 `selected.env` 中匹配 `env:VAR` 的 value 逐项从 parentSnapshot 查找;缺失则返回 error 指出哪个 key 引用哪个 VAR 缺失;只解析一次不做链式 - [x] 3.3 实现 `BuildChildEnv(parent []string, union []string, resolved map[string]string) []string`:从 parent 剔除 union 中的 key,再 append resolved 的 key=value(resolved 的值覆盖同名) - [x] 3.4 单元测试: - 父环境覆盖、并集剔除、value 不做 shell 展开、同名 key 以 selected 为准 - `ResolveEnvRefs` 正常解析、VAR 缺失报错、被引用 VAR 恰好在并集中(快照先于清理)、不做链式解析 ## 4. Runner 子进程层(internal/runner) - [x] 4.1 实现 `ResolveClaudePath(cfg Config) (string, error)`:优先 `claude_path`(带 `~` 展开 + 可执行位校验),否则 `exec.LookPath("claude")`;都失败则返回带引导信息的错误 - [x] 4.2 实现 `Run(ctx, claudePath string, childEnv []string, args []string) (exitCode int, err error)`: - `cmd.Env = childEnv`;`cmd.Stdin/Stdout/Stderr = os.Std*` - 使用 `os/signal.Notify` 捕获 SIGINT/SIGTERM/SIGHUP 并 `cmd.Process.Signal(sig)` 转发 - `cmd.Wait()` 后返回 `ExitCode()`;信号终止时返回 `128 + signum` - [x] 4.3 单元测试:用 `exec.Command("/bin/sh", "-c", "echo $FOO")` 作为替身验证 env 注入;用 short-lived 子进程验证 exit code 透传;用 `sleep` + SIGINT 验证信号转发 - [x] 4.4 在 Unix 专属文件 `runner_unix.go` 中实现信号转发;预留 `runner_other.go` 返回"unsupported platform"错误(为 Windows/Plan9 兜底) ## 5. CLI 装配(internal/cli) - [x] 5.1 在 `internal/cli/root.go` 定义 root cmd 与全局 flag:`-v/--verbose`、`--config` - [x] 5.2 在 `root.go` 的 `PersistentPreRunE` 中加载配置(若文件不存在则持有空配置) - [x] 5.3 实现 `cc-switch add`:flag 模式 `--env KEY=VALUE`(可重复)、`--description`、`--from-template `、`--non-interactive`; - 非 tty 且无 `--env` 也无 `--from-template` 时报错 - tty 且无 `--env`/`--from-template` 时进入自由向导(空 key 结束) - `--from-template` 带出模板 key 骨架,tty 下逐 key 提示 value(显示 hint、回车接受 default、允许 `env:VAR` 引用),`--non-interactive` + 模板时为占位空字符串 - 组合规则:先模板、后 `--env` 覆盖/追加 - 校验:落盘前任何 value 为空字符串的 key 拒绝写入 - [x] 5.4 实现 `cc-switch list` / `ls`:普通模式只列名称 + default 标记;`-v` 列出 env key 与脱敏 value(`***` 或首 4 字符 + `***`) - [x] 5.5 实现 `cc-switch edit `:支持 `--env`、`--description`、`--remove-env KEY` 等 flag - [x] 5.6 实现 `cc-switch remove ` / `rm `:删除后若等于 default_provider 则清空该字段 - [x] 5.7 实现 `cc-switch config show|set|get`:`set claude-path`、`set default`、`get `、`show`(脱敏) - [x] 5.8 实现 `cc-switch use [name]`: - tty + 无 name → 交互菜单(编号/名称输入,空输入选 default,最多 3 次非法重试) - 非 tty + 无 name → 使用 `default_provider`;无 default 则报错 - 有 name → 直接选中;不存在则报错 - 选中后流程:取 `os.Environ()` 快照 → `ResolveEnvRefs`(缺失即退出、不启动)→ `UnionEnvKeys` → `BuildChildEnv` → runner 启动 → 等待 → 退出码透传 - [x] 5.9 实现 `cc-switch`(root 裸跑)→ 转发到 `use` 子命令 - [x] 5.10 实现 `cc-switch version`:注入构建时的 version/commit/date(通过 ldflags) - [x] 5.11 `verbose` 时在 stderr 打印 trace:解析到的 config 路径、claude 路径、并集 key、选中 provider、cleanup complete - [x] 5.12 实现 `cc-switch templates list` / `templates show `,只读内嵌模板数据 - [x] 5.13 覆盖 cli 集成测试(用 `cobra` 的 `SetArgs` + `SetOut/Err` 捕获):add/list/remove/use/templates 的各 scenario,包含 `--from-template`、`env:VAR` 写入、引用解析失败的 use 路径 ## 6. 脱敏与日志 - [x] 6.1 实现 value 脱敏函数:长度 ≤ 4 全部 `***`;否则前 4 字符 + `***` - [x] 6.2 在所有打印 provider env value 的地方统一走脱敏函数(list -v、config show、config get),但**引用 value(`env:VAR`)原样输出不脱敏** - [x] 6.3 verbose trace 中永不打印 env value,仅 key 列表 ## 7. 内嵌模板(internal/templates) - [x] 7.1 设计模板数据结构:`Template{Name, Description string; Env []TemplateEnvKey{Name, Hint, Default string}}` - [x] 7.2 创建 `internal/templates/templates.yaml`(`go:embed`),填充 v1 种子模板:`anthropic-official`、`openrouter`、`deepseek`、`moonshot`、`zhipu`、`custom-base`,**只填键名 + hint + 可选 default(官方 base URL 等),绝不预置 token** - [x] 7.3 实现 `Load() []Template`、`Get(name string) (Template, bool)`、`List() []Template` - [x] 7.4 单元测试:加载完整性(所有种子模板非空、env key 列表非空)、`Get` 正反例、yaml 解析错误路径 ## 8. 端到端测试 - [x] 8.1 黑盒 e2e:用 `go test` 启动 `cc-switch`,用一个简短 shell 脚本充当"fake claude"(打印所有 ANTHROPIC_* 变量后退出),验证: - 并集清理:父 shell 设置 `ANTHROPIC_API_KEY=old`、provider 未设置该 key,fake claude 输出中不包含该 key - 注入覆盖:provider 设置 `ANTHROPIC_API_KEY=new`,fake claude 输出中值为 `new` - value 字面传递:值为 `$HOME` 时,fake claude 输出字面 `$HOME` - 退出码透传:fake claude `exit 42`,`cc-switch` 也 42 - [x] 8.2 信号 e2e:fake claude = 长 sleep + 捕获 SIGINT 并 `exit 130`,`cc-switch` 收 SIGINT 后也 130 退出(覆盖于 `internal/runner` 单测) - [x] 8.3 非 tty 行为:用 `stdin