1
0
Files

10 KiB
Raw Permalink Blame History

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

5. 安装策略:复制而非符号链接

决策:全局和项目级安装都使用文件复制,不使用符号链接。

理由

  • 避免符号链接在跨平台和跨文件系统时的兼容性问题(尤其是 Windows
  • 项目可以独立于全局安装,避免意外修改影响其他项目
  • 磁盘空间在现代系统中不是瓶颈
  • 替代方案:全局符号链接(复杂度高,跨平台问题)

影响

  • 更新全局安装不会自动影响项目级安装(需显式更新)
  • 多个项目可以独立更新各自的 skills/commands

6. 命令文件组织:命令组概念

决策:将 commands 按目录组织,整个目录作为"命令组"一起安装。

理由

  • 源仓库中 commands 按功能分组(如 commands/lyxy-kb/ 包含 init/ask/ingest/rebuild
  • 命令组内的命令通常有关联,应一起安装
  • 简化用户操作,避免逐个命令安装

命令组到命令的映射

  • Claude Code/lyxy-kb-initcommands/lyxy-kb/init.md
  • OpenCode/lyxy-kb:initcommand/lyxy-kb-init.md

7. 安装记录清理Clean 命令

决策:提供 clean 命令扫描并清理孤立记录install.json 中存在但目标路径已删除)。

理由

  • 用户可能手动删除已安装的目录
  • 避免 install.json 与实际文件系统状态不一致
  • 不自动清理(避免误删),由用户显式触发

实现

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/
  • 环境变量注入是轻量级且侵入性最小的方案
  • 支持并行测试(每个测试独立目录)

实现

// 配置路径注入
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 命令(未来增强)诊断和修复配置