## Context 当前手动管理 AI 编程平台的 skills/commands 面临以下挑战: - **平台差异**:Claude Code 使用目录结构,OpenCode 使用扁平化文件命名(如 `lyxy-kb-init.md`) - **手动操作**:需要手动理解每个平台的规范、复制文件、重命名 - **追踪困难**:无法知道哪些 skills/commands 已安装、来自哪个源、何时安装 - **更新风险**:手动更新容易遗漏文件或破坏已有配置 目标用户是作者本人,工具需要简单直接,优先实现核心功能而非过度设计。 **约束条件**: - 单用户场景,不考虑多用户协作 - 仅支持最新版本,不处理多版本共存 - 不解析依赖关系,用户手动管理依赖 - 平台支持范围:Claude Code、OpenCode(未来可扩展) ## Goals / Non-Goals **Goals:** - 自动化从 git 源仓库到目标平台的完整安装流程 - 支持全局和项目级两种安装作用域 - 内置 Claude Code 和 OpenCode 的平台适配规则 - 记录所有安装操作,支持查询、更新、卸载、清理 - 事务性安装机制,避免部分失败导致的不一致状态 - 用户友好的交互体验(确认覆盖、清晰的错误提示) **Non-Goals:** - ❌ 多版本管理(语义化版本、版本锁定、版本冲突解决) - ❌ 依赖解析和自动安装依赖 - ❌ 插件化的平台适配器系统 - ❌ 复杂的仓库注册中心或包索引服务 - ❌ 跨平台迁移或批量同步 ## Decisions ### 1. 技术栈选择:Go + Cobra **决策**:使用 Go 语言开发,CLI 框架选择 Cobra。 **理由**: - **Go**:编译为单一可执行文件,无运行时依赖,跨平台分发简单 - **Cobra**:业界标准 CLI 框架(kubectl、docker 都用它),支持子命令、自动帮助生成、参数验证 - **替代方案**:标准库 `flag` 包功能过于简单,不适合多子命令场景 **影响**:开发者需要熟悉 Go 和 Cobra 的基本用法。 --- ### 2. 配置文件格式:JSON **决策**:使用 JSON 格式存储配置(repository.json、install.json)。 **理由**: - Go 标准库原生支持,无需第三方依赖 - 结构清晰,易于程序读写和人工检查 - **替代方案**:YAML(需要第三方库)、TOML(生态较小) **配置结构**: ``` ~/.skillmgr/ ├── repository.json # 源仓库列表 ├── install.json # 安装记录 └── cache/ # git 仓库缓存 ``` --- ### 3. 事务性安装:Tmp Staging **决策**:采用三阶段事务性安装(Stage → Commit → Rollback)。 **理由**: - 避免部分文件复制失败导致目标目录处于不一致状态 - 先在系统临时目录(`os.TempDir()`)组装完整的目标文件树 - 验证成功后一次性移动到最终位置 - 失败时自动清理临时目录,不污染目标 **流程**: ``` 1. 创建 staging 目录(/tmp/skillmgr-xxxxx/) 2. 复制所有文件到 staging(应用平台适配规则) 3. 验证 staging 目录完整性 4. 移动 staging 到目标位置(原子操作) 5. 失败则删除 staging,不影响目标 ``` **替代方案**:直接复制到目标(风险高)、使用 Git worktree(过于复杂)。 --- ### 4. 平台适配器:内置而非插件化 **决策**:将 Claude Code 和 OpenCode 的适配规则硬编码在程序内,不支持用户自定义适配器。 **理由**: - 目标平台数量少且稳定(2 个),插件系统收益低 - 硬编码保证规则的正确性和一致性 - 简化实现和维护成本 - **替代方案**:配置文件定义适配规则(增加复杂度)、插件系统(过度设计) **适配器接口**: ```go type PlatformAdapter interface { GetSkillInstallPath(scope, name) (string, error) GetCommandInstallPath(scope, group) (string, error) AdaptSkill(sourcePath, destPath) (map[string]string, error) AdaptCommand(sourcePath, destPath, group) (map[string]string, error) } ``` **差异处理**: - **Skills**:两个平台都保持目录结构,直接复制 - **Commands**: - Claude Code 保持目录结构(`commands/lyxy-kb/init.md`) - OpenCode 扁平化文件名(`command/lyxy-kb-init.md`) --- ### 5. 安装策略:复制而非符号链接 **决策**:全局和项目级安装都使用文件复制,不使用符号链接。 **理由**: - 避免符号链接在跨平台和跨文件系统时的兼容性问题(尤其是 Windows) - 项目可以独立于全局安装,避免意外修改影响其他项目 - 磁盘空间在现代系统中不是瓶颈 - **替代方案**:全局符号链接(复杂度高,跨平台问题) **影响**: - 更新全局安装不会自动影响项目级安装(需显式更新) - 多个项目可以独立更新各自的 skills/commands --- ### 6. 命令文件组织:命令组概念 **决策**:将 commands 按目录组织,整个目录作为"命令组"一起安装。 **理由**: - 源仓库中 commands 按功能分组(如 `commands/lyxy-kb/` 包含 init/ask/ingest/rebuild) - 命令组内的命令通常有关联,应一起安装 - 简化用户操作,避免逐个命令安装 **命令组到命令的映射**: - Claude Code:`/lyxy-kb-init` → `commands/lyxy-kb/init.md` - OpenCode:`/lyxy-kb:init` → `command/lyxy-kb-init.md` --- ### 7. 安装记录清理:Clean 命令 **决策**:提供 `clean` 命令扫描并清理孤立记录(install.json 中存在但目标路径已删除)。 **理由**: - 用户可能手动删除已安装的目录 - 避免 install.json 与实际文件系统状态不一致 - 不自动清理(避免误删),由用户显式触发 **实现**: ```bash skillmgr clean # 扫描 install.json 中所有记录 # 检查 install_path 是否存在 # 列出孤立记录并确认删除 ``` --- ### 8. 目录冲突处理:用户决策 **决策**:安装前检查目标目录是否存在,存在时由用户决定是否覆盖。 **场景**: 1. **install.json 有记录 + 目录存在**:已安装,询问是否覆盖 2. **install.json 无记录 + 目录存在**:未被 skillmgr 管理的目录,询问是否覆盖 3. **install.json 有记录 + 目录不存在**:孤立记录,清理记录后继续安装 **用户交互**: ``` Skill 'lyxy-kb' is already installed. Overwrite? [y/N]: ``` --- ### 9. 项目结构:独立 Go 项目 **决策**:在 `manager/` 目录下创建独立的 Go 项目,与现有 skills 仓库分离。 **目录结构**: ``` manager/ ├── cmd/skillmgr/ # CLI 命令实现 ├── internal/ # 内部包(不对外暴露) │ ├── config/ # 配置文件读写 │ ├── repo/ # Git 仓库管理 │ ├── adapter/ # 平台适配器 │ ├── installer/ # 安装逻辑 │ └── prompt/ # 用户交互 ├── pkg/ # 可对外暴露的包 │ └── fileutil/ # 文件工具 ├── go.mod └── README.md ``` **理由**: - 不污染现有 skills 仓库结构 - 工具本身可以独立开发、测试、发布 - 清晰的模块边界 --- ### 10. 测试隔离:环境变量注入 **决策**:通过环境变量覆盖配置和目标路径,实现零污染测试。 **理由**: - 测试不应影响用户的实际配置(`~/.skillmgr/`)和安装目录(`~/.claude/`) - 环境变量注入是轻量级且侵入性最小的方案 - 支持并行测试(每个测试独立目录) **实现**: ```go // 配置路径注入 func GetConfigRoot() (string, error) { if testRoot := os.Getenv("SKILLMGR_TEST_ROOT"); testRoot != "" { return testRoot, nil } // 生产模式... } // 目标路径注入 func getBasePath(scope Scope) (string, error) { if testBase := os.Getenv("SKILLMGR_TEST_BASE"); testBase != "" { return testBase, nil } // 生产模式... } ``` **测试使用**: ```go func TestInstall(t *testing.T) { testRoot := t.TempDir() testBase := t.TempDir() os.Setenv("SKILLMGR_TEST_ROOT", testRoot) os.Setenv("SKILLMGR_TEST_BASE", testBase) defer os.Unsetenv("SKILLMGR_TEST_ROOT") defer os.Unsetenv("SKILLMGR_TEST_BASE") // 测试代码... } ``` **替代方案**: - **依赖注入**(将路径作为参数传递):侵入性强,需要重构所有函数签名 - **Mock 文件系统**(如 afero):复杂度高,且无法测试真实文件系统行为 - **专用测试模式标志**:需要额外的全局状态管理 ## Risks / Trade-offs ### 1. 无版本管理 **风险**:用户无法回退到旧版本的 skill/command,更新可能引入破坏性变更。 **缓解**: - 文档中建议用户在重要项目中使用 git 管理项目配置目录(如 `.claude/`) - 工具记录 `updated_at` 时间,方便追溯 --- ### 2. 无依赖解析 **风险**:安装 command 时,依赖的 skill 可能未安装(如 `lyxy-kb` 命令依赖 `lyxy-reader-office` skill)。 **缓解**: - 在 skill 的 SKILL.md 中明确记录依赖关系(如 `compatibility` 字段) - 错误提示中建议用户检查依赖 - 未来可选增强:扫描 SKILL.md 提示缺失依赖 --- ### 3. Git 依赖 **风险**:工具依赖系统中已安装 Git 客户端,无 Git 则无法拉取仓库。 **缓解**: - 在 README 中明确前置条件 - 首次运行时检测 Git 是否可用,提示安装 - 错误消息中包含 Git 安装指引 --- ### 4. 跨文件系统移动失败 **风险**:`os.Rename()` 在跨文件系统时会失败(如 tmp 在 tmpfs,目标在 ext4)。 **缓解**: - 捕获 Rename 错误,fallback 到递归复制 + 删除 staging - 在事务实现中明确处理两种路径 --- ### 5. 平台适配规则变化 **风险**:Claude Code 或 OpenCode 未来修改目录结构规范,导致工具失效。 **缓解**: - 将适配规则集中在 `internal/adapter/` 包中,便于修改 - 提供版本号,用户可锁定工具版本以保证稳定性 - 文档中建议关注平台更新公告 --- ### 6. 手动修改配置文件 **风险**:用户手动编辑 repository.json 或 install.json 可能破坏格式,导致解析失败。 **缓解**: - JSON 解析错误时提示备份并重建配置文件 - 提供 `doctor` 命令(未来增强)诊断和修复配置