10 KiB
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 个),插件系统收益低
- 硬编码保证规则的正确性和一致性
- 简化实现和维护成本
- 替代方案:配置文件定义适配规则(增加复杂度)、插件系统(过度设计)
适配器接口:
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)
- Claude Code 保持目录结构(
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 与实际文件系统状态不一致
- 不自动清理(避免误删),由用户显式触发
实现:
skillmgr clean
# 扫描 install.json 中所有记录
# 检查 install_path 是否存在
# 列出孤立记录并确认删除
8. 目录冲突处理:用户决策
决策:安装前检查目标目录是否存在,存在时由用户决定是否覆盖。
场景:
- install.json 有记录 + 目录存在:已安装,询问是否覆盖
- install.json 无记录 + 目录存在:未被 skillmgr 管理的目录,询问是否覆盖
- 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/) - 环境变量注入是轻量级且侵入性最小的方案
- 支持并行测试(每个测试独立目录)
实现:
// 配置路径注入
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
}
// 生产模式...
}
测试使用:
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命令(未来增强)诊断和修复配置