tasks.md 9.9 KB

1. 项目脚手架

  • 1.1 初始化 Go module:go mod init github.com/kotoyuuko/cc-switch-cli,设 go 1.22+
  • 1.2 添加依赖:github.com/spf13/cobragopkg.in/yaml.v3
  • 1.3 建立目录结构:cmd/cc-switch/main.gointernal/config/internal/provider/internal/runner/internal/cli/
  • 1.4 在 cmd/cc-switch/main.go 写空的 cobra root command,可运行 go run ./cmd/cc-switch --help
  • 1.5 加入 Makefilejustfilebuildtestvetfmt 常用 target
  • 1.6 添加 .gitignore(忽略二进制、dist/、本地 config.yaml

2. 配置层(internal/config)

  • 2.1 定义 Config / Provider 结构体,字段与 specs/provider-config/spec.md 一致(claude_pathdefault_providerprovidersenvdescription
  • 2.2 实现配置路径解析:CC_SWITCH_CONFIG > XDG_CONFIG_HOME > ~/.config/cc-switch/config.yaml;支持 ~ 展开
  • 2.3 实现 Load():文件不存在返回空 Config,存在则 yaml 反序列化;对权限 > 0600 的文件打印 warning
  • 2.4 实现 Save():写 config.yaml.tmp → fsync → os.Rename;父目录缺失则以 0700 创建;新建文件以 0600 权限
  • 2.5 实现 provider 名校验(正则 ^[A-Za-z0-9_-]+$)与 env 非空校验
  • 2.6 实现增删改查方法:AddProviderRemoveProviderUpdateProviderListProvidersSetDefaultSetClaudePath(后者校验路径存在且可执行)
  • 2.7 实现 env value 引用识别辅助:IsEnvRef(s string) (varName string, ok bool) 基于正则 ^env:[A-Za-z_][A-Za-z0-9_]*$;展示层(list -v、config show)遇到引用 value 原样输出、不脱敏
  • 2.8 覆盖单元测试:路径解析、load/save 原子性、权限 warning、CRUD、非法名、删除 default 清空逻辑、IsEnvRef 的正反例(包括 env:env:1xenv:FOO 等)

3. Provider 运行时计算(internal/provider)

  • 3.1 实现 UnionEnvKeys(providers) []string:遍历所有 provider 的 env key 去重
  • 3.2 实现 ResolveEnvRefs(selected Provider, parentSnapshot map[string]string) (resolved map[string]string, err error):对 selected.env 中匹配 env:VAR 的 value 逐项从 parentSnapshot 查找;缺失则返回 error 指出哪个 key 引用哪个 VAR 缺失;只解析一次不做链式
  • 3.3 实现 BuildChildEnv(parent []string, union []string, resolved map[string]string) []string:从 parent 剔除 union 中的 key,再 append resolved 的 key=value(resolved 的值覆盖同名)
  • 3.4 单元测试:
    • 父环境覆盖、并集剔除、value 不做 shell 展开、同名 key 以 selected 为准
    • ResolveEnvRefs 正常解析、VAR 缺失报错、被引用 VAR 恰好在并集中(快照先于清理)、不做链式解析

4. Runner 子进程层(internal/runner)

  • 4.1 实现 ResolveClaudePath(cfg Config) (string, error):优先 claude_path(带 ~ 展开 + 可执行位校验),否则 exec.LookPath("claude");都失败则返回带引导信息的错误
  • 4.2 实现 Run(ctx, claudePath string, childEnv []string, args []string) (exitCode int, err error)
    • cmd.Env = childEnvcmd.Stdin/Stdout/Stderr = os.Std*
    • 使用 os/signal.Notify 捕获 SIGINT/SIGTERM/SIGHUP 并 cmd.Process.Signal(sig) 转发
    • cmd.Wait() 后返回 ExitCode();信号终止时返回 128 + signum
  • 4.3 单元测试:用 exec.Command("/bin/sh", "-c", "echo $FOO") 作为替身验证 env 注入;用 short-lived 子进程验证 exit code 透传;用 sleep + SIGINT 验证信号转发
  • 4.4 在 Unix 专属文件 runner_unix.go 中实现信号转发;预留 runner_other.go 返回"unsupported platform"错误(为 Windows/Plan9 兜底)

5. CLI 装配(internal/cli)

  • 5.1 在 internal/cli/root.go 定义 root cmd 与全局 flag:-v/--verbose--config
  • 5.2 在 root.goPersistentPreRunE 中加载配置(若文件不存在则持有空配置)
  • 5.3 实现 cc-switch add:flag 模式 --env KEY=VALUE(可重复)、--description--from-template <tpl>--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 拒绝写入
  • 5.4 实现 cc-switch list / ls:普通模式只列名称 + default 标记;-v 列出 env key 与脱敏 value(*** 或首 4 字符 + ***
  • 5.5 实现 cc-switch edit <name>:支持 --env--description--remove-env KEY 等 flag
  • 5.6 实现 cc-switch remove <name> / rm <name>:删除后若等于 default_provider 则清空该字段
  • 5.7 实现 cc-switch config show|set|getset claude-pathset defaultget <key>show(脱敏)
  • 5.8 实现 cc-switch use [name]
    • tty + 无 name → 交互菜单(编号/名称输入,空输入选 default,最多 3 次非法重试)
    • 非 tty + 无 name → 使用 default_provider;无 default 则报错
    • 有 name → 直接选中;不存在则报错
    • 选中后流程:取 os.Environ() 快照 → ResolveEnvRefs(缺失即退出、不启动)→ UnionEnvKeysBuildChildEnv → runner 启动 → 等待 → 退出码透传
  • 5.9 实现 cc-switch(root 裸跑)→ 转发到 use 子命令
  • 5.10 实现 cc-switch version:注入构建时的 version/commit/date(通过 ldflags)
  • 5.11 verbose 时在 stderr 打印 trace:解析到的 config 路径、claude 路径、并集 key、选中 provider、cleanup complete
  • 5.12 实现 cc-switch templates list / templates show <tpl>,只读内嵌模板数据
  • 5.13 覆盖 cli 集成测试(用 cobraSetArgs + SetOut/Err 捕获):add/list/remove/use/templates 的各 scenario,包含 --from-templateenv:VAR 写入、引用解析失败的 use 路径

6. 脱敏与日志

  • 6.1 实现 value 脱敏函数:长度 ≤ 4 全部 ***;否则前 4 字符 + ***
  • 6.2 在所有打印 provider env value 的地方统一走脱敏函数(list -v、config show、config get),但引用 value(env:VAR)原样输出不脱敏
  • 6.3 verbose trace 中永不打印 env value,仅 key 列表

7. 内嵌模板(internal/templates)

  • 7.1 设计模板数据结构:Template{Name, Description string; Env []TemplateEnvKey{Name, Hint, Default string}}
  • 7.2 创建 internal/templates/templates.yamlgo:embed),填充 v1 种子模板:anthropic-officialopenrouterdeepseekmoonshotzhipucustom-base只填键名 + hint + 可选 default(官方 base URL 等),绝不预置 token
  • 7.3 实现 Load() []TemplateGet(name string) (Template, bool)List() []Template
  • 7.4 单元测试:加载完整性(所有种子模板非空、env key 列表非空)、Get 正反例、yaml 解析错误路径

8. 端到端测试

  • 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 42cc-switch 也 42
  • 8.2 信号 e2e:fake claude = 长 sleep + 捕获 SIGINT 并 exit 130cc-switch 收 SIGINT 后也 130 退出(覆盖于 internal/runner 单测)
  • 8.3 非 tty 行为:用 stdin</dev/null 运行 cc-switch use,无 default 时报错;有 default 时正确启动
  • 8.4 权限 warning:chmod 0644 后运行任一命令,stderr 有 warning 且功能正常
  • 8.5 env 引用 e2e:
    • 引用成功:父 shell 设 MY_KEY=sk-ok,provider env.ANTHROPIC_API_KEY=env:MY_KEY,fake claude 收到 ANTHROPIC_API_KEY=sk-ok
    • 引用缺失:父 shell 不设 MY_KEYcc-switch use 非零退出,fake claude 未被启动(可通过 claude 端写入一个 sentinel 文件检测"没执行")
    • 快照顺序:provider B env.ANTHROPIC_API_KEY=env:ANTHROPIC_API_KEY,父 shell 设该 var,fake claude 收到父 shell 的值(验证先快照后清理)
  • 8.6 模板 e2e:cc-switch add x --from-template openrouter --non-interactive --env ANTHROPIC_AUTH_TOKEN=sk-test --env ANTHROPIC_MODEL=anthropic/claude-opus-4(其他 key 由模板带出)能成功落盘;cc-switch templates list 输出含全部种子模板

9. 打包与发布准备

  • 9.1 编写最小可用的 README.md:安装、快速上手(add → use)、配置文件示例(Anthropic 官方 / OpenRouter / 其他兼容层 snippet)、env:VAR 引用用法(配合 1Password CLI / direnv 的示例)、--from-template 用法
  • 9.2 添加 GoReleaser 配置(.goreleaser.yaml):darwin/linux 的 amd64+arm64 二进制;SHA256 校验;可选 Homebrew tap
  • 9.3 在 GitHub Actions 添加 CI:vet + test + build(PR & main)
  • 9.4 打 tag v0.1.0,触发发布流水线(验收通过后) — 留给用户

10. 文档与示例配置

  • 10.1 在 README 中明确"并集清理"语义,配合示例解释"为什么把所有可能用到的 key 都登记"
  • 10.2 附一个 examples/config.example.yaml,含两三个 provider 示例(占位 key + 至少一条 env:VAR 引用示例)
  • 10.3 记录 Non-Goal:不改父 shell、不加密存储、暂不支持 Windows、v1 不支持用户自定义模板目录、不做 env:VAR 链式解析、不做字面 env: 转义