diff --git a/commands/lyxy-kb/ask.md b/commands/lyxy-kb/ask.md deleted file mode 100644 index 61ce4b1..0000000 --- a/commands/lyxy-kb/ask.md +++ /dev/null @@ -1,55 +0,0 @@ -基于知识库项目进行问答。 - -**输入**: `/lyxy-kb-ask` 后的参数为项目名称,可选附带问题。例如: -- `/lyxy-kb-ask my-project` — 进入问答模式 -- `/lyxy-kb-ask my-project 这个系统用了什么技术栈?` — 直接提问 - -**前置条件**: 查找并阅读名为 **lyxy-kb** 的 skill,了解渐进式查询策略和来源引用格式。 - -**步骤** - -1. **获取项目名称并验证结构** - - 从参数中获取项目名称。如果未提供参数,提示用户输入。 - - 按照 lyxy-kb skill「结构完整性验证」规则检查项目目录,不完整则提示用户先 init。 - -2. **加载项目摘要** - - 读取 `/project.md`,获取项目概述和文件索引。 - - 按照 lyxy-kb skill「空知识库」规则,如果文件索引为空(尚无已入库文件),告知用户知识库为空,建议先使用 `/lyxy-kb-ingest ` 入库文档,终止操作。 - -3. **进入问答模式** - - 如果用户在参数中附带了问题,直接回答该问题。否则提示用户可以开始提问。 - - **对每个问题,按照 lyxy-kb skill「渐进式查询策略」执行**: - - **a) 分析问题与文件索引的关联** - - 根据用户的问题内容,对照 project.md 文件索引表中各文件的摘要,判断需要查阅哪些 parsed 文件。 - - **b) 按需加载 parsed 文件** - - 读取相关的 `/parsed/<文件名>.md` 文件。如果文件较大,可以先提取标题结构,再读取相关章节。 - - **c) 回答并标注来源** - - 基于获取的信息回答问题。按照 lyxy-kb skill「来源引用格式」标注来源: - - ``` - 根据《文件名》(parsed/文件名.md),... - ``` - - 如果回答综合了多个文件的信息,分别标注各信息点的来源。 - - **d) 无相关信息处理** - - 按照 lyxy-kb skill「无相关信息」规则,明确告知用户当前知识库中未找到相关信息,不编造答案。 - -4. **保持会话上下文** - - 回答完成后,保持当前的知识库上下文。用户可以继续提问,无需每次重新加载 project.md。已加载的 parsed 文件内容可在后续问答中复用。 - - 会话的退出由用户自然决定(开启新话题或新会话),不主动终止问答模式。 diff --git a/commands/lyxy-kb/ingest.md b/commands/lyxy-kb/ingest.md deleted file mode 100644 index 46e38f5..0000000 --- a/commands/lyxy-kb/ingest.md +++ /dev/null @@ -1,65 +0,0 @@ -解析 sources/ 中的新文件并增量更新知识库。 - -**输入**: `/lyxy-kb-ingest` 后的参数为项目名称。 - -**前置条件**: 查找并阅读名为 **lyxy-kb** 的 skill,了解知识库的完整规范。 - -**步骤** - -1. **获取项目名称并验证结构** - - 从参数中获取项目名称。如果未提供参数,提示用户输入。 - - 按照 lyxy-kb skill「结构完整性验证」规则检查项目目录,不完整则提示用户先 init。 - -2. **检查 office 文档解析能力** - - 按照 lyxy-kb skill「Office 文档解析」规则,查找当前环境中名为 **lyxy-reader-office** 的 skill。如果不存在且无其他可替代的文档解析 skill,则提示用户无法处理 office 文档并中止流程。 - -3. **读取 manifest.json** - - 读取 `/manifest.json`,获取已入库文件的信息。 - -4. **递归扫描 sources/ 目录** - - 按照 lyxy-kb skill「sources/ 扫描规则」,递归检查 sources/ 及其所有子目录中的文件。如果无任何文件,提示用户无待处理文件并终止。 - -5. **预检查** - - **空文件检测**:按照 lyxy-kb skill「空文件处理」规则,识别 0 字节文件,标记为跳过。 - - **同名不同扩展名冲突检测**:按照 lyxy-kb skill「同名不同扩展名冲突检测」中的两条检测规则执行。冲突文件标记为跳过。 - - 如果有跳过的文件,列出详情(空文件 / 冲突文件分别列出)。如果所有文件都被跳过,终止流程。 - -6. **逐个处理文件** - - 对每个通过预检查的文件: - - **a) 解析**:按照 lyxy-kb skill「文件类型解析策略」判断解析方式。office 文档使用 lyxy-reader-office skill(查找并阅读该 skill 获取具体命令),其他文件直接读取。 - - **b) 写入 parsed**:按照 lyxy-kb skill「parsed 文件元信息标记」格式,在内容头部添加元信息注释,写入 `/parsed/<文件名>.md`(同名覆盖)。 - - **c) 归档**:按照 lyxy-kb skill「归档命名规则」,移动原始文件到 archive/(带时间戳后缀 `YYYYMMDDHHmm`)。 - - **d) 更新 manifest.json**:新文件追加条目,已有文件在 versions 数组追加新版本。使用 `sha256sum` 计算文件哈希。更新 `last_ingest`。 - - **e) 解析失败处理**:按照 lyxy-kb skill「解析失败处理」规则,失败文件保留在 sources/ 中不移动,报告错误,继续处理下一个文件。 - -7. **增量更新 project.md** - - 按照 lyxy-kb skill「增量追加」策略: - - 对每个新处理的文件,读取其 parsed 内容生成简要摘要(1-2 句话) - - 新文件:在文件索引表追加新行 - - 已有文件更新:更新文件索引表中对应行 - - 在更新记录追加本次 ingest 条目 - - 不修改概述和关键信息部分 - -8. **输出结果** - - 汇总显示: - - 成功处理的文件列表 - - 跳过的文件(空文件 / 冲突文件 / 解析失败文件,分别列出) - - 当前项目已入库文件总数 - - 提示可使用 `/lyxy-kb-rebuild ` 更新概述和关键信息 - - 提示可使用 `/lyxy-kb-ask ` 进行知识问答 diff --git a/commands/lyxy-kb/init.md b/commands/lyxy-kb/init.md deleted file mode 100644 index 8d5d474..0000000 --- a/commands/lyxy-kb/init.md +++ /dev/null @@ -1,67 +0,0 @@ -初始化一个知识库项目。 - -**输入**: `/lyxy-kb-init` 后的参数为项目名称。 - -**前置条件**: 查找并阅读名为 **lyxy-kb** 的 skill,了解知识库的目录结构规范、项目名称规则和 project.md 格式规范。 - -**步骤** - -1. **获取项目名称** - - 从参数中获取项目名称。如果未提供参数,提示用户输入项目名称。 - -2. **验证项目名称** - - 按照 lyxy-kb skill 中的「项目名称规则」验证名称是否合法(只允许中文、英文、数字、短横线、下划线,不允许空格和其他特殊字符)。不合法时提示用户修改。 - -3. **检查目标目录是否已存在** - - 检查 CWD 下是否已存在同名目录。如果目录已存在,提示用户该目录已存在,不覆盖任何现有内容,终止操作。 - -4. **创建目录结构** - - ```bash - mkdir -p /parsed /sources /archive - ``` - -5. **创建 project.md** - - 按照 lyxy-kb skill 中定义的「project.md 格式规范」,生成初始内容: - - ```markdown - # <项目名称> - - ## 概述 - - (待补充) - - ## 关键信息 - - (待补充) - - ## 文件索引 - - | 文件名 | 解析文件 | 最新归档 | 摘要 | - |--------|----------|----------|------| - - ## 更新记录 - - : 初始化项目 - ``` - -6. **创建 manifest.json** - - ```json - { - "project": "<项目名称>", - "created_at": "<当前时间 ISO 格式>", - "last_ingest": null, - "files": [] - } - ``` - -7. **输出结果** - - 提示用户: - - 项目已创建,显示完整的目录结构 - - 引导用户将文档放入 `/sources/` 目录 - - 提示使用 `/lyxy-kb-ingest ` 解析入库 diff --git a/commands/lyxy-kb/rebuild.md b/commands/lyxy-kb/rebuild.md deleted file mode 100644 index b8aba8b..0000000 --- a/commands/lyxy-kb/rebuild.md +++ /dev/null @@ -1,55 +0,0 @@ -全量重新生成 project.md。 - -**输入**: `/lyxy-kb-rebuild` 后的参数为项目名称。 - -**前置条件**: 查找并阅读名为 **lyxy-kb** 的 skill,了解 project.md 格式规范和全量重写策略。 - -**步骤** - -1. **获取项目名称并验证结构** - - 从参数中获取项目名称。如果未提供参数,提示用户输入。 - - 按照 lyxy-kb skill「结构完整性验证」规则检查项目目录,不完整则提示用户先 init。 - -2. **检查 parsed 目录** - - 列出 `/parsed/` 下的所有 `.md` 文件。如果为空,提示用户尚无已解析文件,建议先执行 `/lyxy-kb-ingest `。 - -3. **检查 sources/ 待处理文件** - - 检查 `/sources/` 中是否还有未 ingest 的文件。如果有,提醒用户 sources/ 中存在未入库文件,rebuild 将仅基于已有的 parsed 文件生成,建议先执行 ingest。 - -4. **确认操作** - - 向用户说明 rebuild 将覆盖当前 project.md 的概述、关键信息和文件索引(更新记录会保留),请求用户确认是否继续。用户确认后再执行。 - -5. **读取所有 parsed 文件** - - 逐个读取 `/parsed/` 下的所有 `.md` 文件内容。 - -6. **读取 manifest.json** - - 读取 `/manifest.json`,获取文件元信息(用于生成文件索引表中的归档路径等信息)。 - -7. **读取现有更新记录** - - 读取当前 `/project.md`,提取 `## 更新记录` 部分的内容以保留历史记录。 - -8. **全量重新生成 project.md** - - 按照 lyxy-kb skill「全量重写」策略和 project.md 格式规范,基于所有 parsed 文件内容重新生成: - - - **概述**:基于所有文件内容,生成高度总结的项目信息(几百字以内) - - **关键信息**:从所有文档中提炼核心要点 - - **文件索引**:基于 manifest.json 和 parsed 文件,重新生成完整索引表(文件名、解析文件路径、最新归档路径、简要摘要) - - **更新记录**:保留历史记录,追加本次 rebuild 条目,格式:`- : 全量重建 project.md` - - 将生成的内容写入 `/project.md`,覆盖原有内容。 - -9. **输出结果** - - 提示用户: - - project.md 已全量重建 - - 显示处理的文件数量 - - 提示可使用 `/lyxy-kb-ask ` 进行知识问答 diff --git a/manager/.gitignore b/manager/.gitignore deleted file mode 100644 index 4021483..0000000 --- a/manager/.gitignore +++ /dev/null @@ -1,29 +0,0 @@ -# Binaries -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary -*.test - -# Output of go coverage -*.out - -# Go workspace file -go.work - -# IDE -.idea/ -.vscode/ -*.swp -*.swo - -# OS -.DS_Store -Thumbs.db - -# Build output -/bin/ -/dist/ diff --git a/manager/Makefile b/manager/Makefile deleted file mode 100644 index 896f138..0000000 --- a/manager/Makefile +++ /dev/null @@ -1,92 +0,0 @@ -.PHONY: all build build-all build-macos build-windows test clean install lint - -# 变量 -BINARY_NAME := skillmgr -BUILD_DIR := bin -MAIN_PACKAGE := ./cmd/skillmgr -VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") -BUILD_TIME := $(shell date -u '+%Y-%m-%d_%H:%M:%S') -LDFLAGS := -ldflags "-s -w -X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME)" - -# 默认目标 -all: build - -# 构建当前平台 -build: - @echo "=== 构建 $(BINARY_NAME) ===" - @mkdir -p $(BUILD_DIR) - go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PACKAGE) - @echo "构建完成: $(BUILD_DIR)/$(BINARY_NAME)" - -# 构建所有平台 -build-all: build-macos build-windows - @echo "" - @echo "=== 所有平台构建完成 ===" - @find $(BUILD_DIR) -type f -name "$(BINARY_NAME)*" | sort - -# 构建 macOS (Intel + Apple Silicon) -build-macos: - @echo "=== 构建 macOS (amd64) ===" - @mkdir -p $(BUILD_DIR)/darwin-amd64 - GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/darwin-amd64/$(BINARY_NAME) $(MAIN_PACKAGE) - @echo "构建完成: $(BUILD_DIR)/darwin-amd64/$(BINARY_NAME)" - @echo "" - @echo "=== 构建 macOS (arm64) ===" - @mkdir -p $(BUILD_DIR)/darwin-arm64 - GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(BUILD_DIR)/darwin-arm64/$(BINARY_NAME) $(MAIN_PACKAGE) - @echo "构建完成: $(BUILD_DIR)/darwin-arm64/$(BINARY_NAME)" - -# 构建 Windows -build-windows: - @echo "=== 构建 Windows (amd64) ===" - @mkdir -p $(BUILD_DIR)/windows-amd64 - GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/windows-amd64/$(BINARY_NAME).exe $(MAIN_PACKAGE) - @echo "构建完成: $(BUILD_DIR)/windows-amd64/$(BINARY_NAME).exe" - -# 构建 Linux (可选) -build-linux: - @echo "=== 构建 Linux (amd64) ===" - @mkdir -p $(BUILD_DIR)/linux-amd64 - GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/linux-amd64/$(BINARY_NAME) $(MAIN_PACKAGE) - @echo "构建完成: $(BUILD_DIR)/linux-amd64/$(BINARY_NAME)" - -# 测试 -test: - @echo "=== 运行测试 ===" - ./scripts/test.sh - -# 单元测试(不使用脚本) -test-unit: - go test -v ./... - -# 覆盖率测试 -test-coverage: - go test -coverprofile=coverage.out ./... - go tool cover -html=coverage.out -o coverage.html - @echo "覆盖率报告: coverage.html" - -# 清理 -clean: - rm -rf $(BUILD_DIR) - rm -f coverage.out coverage.html - -# 安装到 $GOPATH/bin -install: build - cp $(BUILD_DIR)/$(BINARY_NAME) $(GOPATH)/bin/ - -# 代码检查 -lint: - golangci-lint run ./... - -# 格式化 -fmt: - go fmt ./... - -# 依赖 -deps: - go mod download - go mod tidy - -# 沙盒环境 -sandbox: - ./scripts/sandbox.sh diff --git a/manager/README.md b/manager/README.md deleted file mode 100644 index de1fb18..0000000 --- a/manager/README.md +++ /dev/null @@ -1,219 +0,0 @@ -# skillmgr - -一个用于管理和分发 LLM 编程助手命令和技能的 CLI 工具。 - -## 功能特性 - -- 从 git 仓库拉取 skills 和 commands -- 支持多平台部署(Claude Code、OpenCode) -- 支持全局安装和项目级安装 -- 事务性安装,避免安装失败导致的文件污染 -- 完整的安装追踪和管理 - -## 安装 - -```bash -# 从源码构建 -git clone https://github.com/your/skills.git -cd skills/manager -make build - -# 将可执行文件添加到 PATH -cp bin/skillmgr /usr/local/bin/ -``` - -## 快速开始 - -```bash -# 添加仓库 -skillmgr add https://github.com/your/skills-repo.git --name my-skills - -# 同步仓库内容 -skillmgr sync - -# 搜索可用的 skills 和 commands -skillmgr search - -# 安装 skill 到 Claude Code(全局) -skillmgr install skill my-skill --platform claude --global - -# 安装 command 到 OpenCode(项目级) -skillmgr install command my-cmd --platform opencode -``` - -## 命令参考 - -### 仓库管理 - -```bash -# 添加仓库 -skillmgr add --name [--branch ] - -# 移除仓库 -skillmgr remove - -# 列出仓库 -skillmgr repos - -# 同步仓库(拉取最新) -skillmgr sync [name] -``` - -### 安装管理 - -```bash -# 安装 -skillmgr install --platform [--global] - -# 卸载 -skillmgr uninstall --platform [--global] - -# 更新 -skillmgr update --platform [--global] -skillmgr update --all - -# 列出已安装 -skillmgr list [--type ] [--platform ] [--global] - -# 搜索可用项 -skillmgr search [keyword] [--type ] [--repo ] - -# 清理孤立记录 -skillmgr clean [--dry-run] -``` - -## 平台适配 - -### Claude Code - -- Skills 安装到 `~/.claude/skills//` (全局) 或 `./.claude/skills//` (项目) -- Commands 安装到 `~/.claude/commands//` (全局) 或 `./.claude/commands//` (项目) -- 保持原始目录结构 - -### OpenCode - -- Skills 全局安装到 `~/.config/opencode/skills//`,项目级安装到 `./.opencode/skills//` -- Commands 全局安装到 `~/.config/opencode/commands/`,项目级安装到 `./.opencode/commands/` -- Command 文件名扁平化:`-.md` -- 例如:`commands/lyxy-kb/init.md` → `~/.config/opencode/commands/lyxy-kb-init.md` - -## 配置文件 - -### 仓库配置 - -位置:`~/.skillmgr/repository.json` - -```json -{ - "repositories": [ - { - "name": "my-skills", - "url": "https://github.com/user/skills.git", - "branch": "main", - "added_at": "2024-01-01T00:00:00Z" - } - ] -} -``` - -### 安装记录 - -位置:`~/.skillmgr/install.json` - -```json -{ - "installations": [ - { - "type": "skill", - "name": "my-skill", - "source_repo": "my-skills", - "platform": "claude", - "scope": "global", - "install_path": "/Users/xxx/.claude/skills/my-skill", - "installed_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-01T00:00:00Z" - } - ] -} -``` - -## 仓库结构 - -skillmgr 期望源仓库具有以下结构: - -``` -your-skills-repo/ -├── skills/ -│ ├── skill-a/ -│ │ ├── SKILL.md # 必需:skill 定义文件 -│ │ └── ... # 其他支持文件 -│ └── skill-b/ -│ └── SKILL.md -└── commands/ - ├── cmd-group-a/ - │ ├── init.md - │ └── run.md - └── cmd-group-b/ - └── action.md -``` - -## 测试 - -```bash -# 运行所有测试 -make test - -# 运行单元测试 -make test-unit - -# 生成覆盖率报告 -make test-coverage - -# 使用沙盒环境手动测试 -make sandbox -``` - -### 测试环境变量 - -- `SKILLMGR_TEST_ROOT`: 覆盖配置目录(`~/.skillmgr`) -- `SKILLMGR_TEST_BASE`: 覆盖安装基础目录(用户主目录或当前目录) - -## 故障排除 - -### 常见问题 - -1. **Git clone 失败** - - 检查网络连接 - - 确认仓库 URL 正确 - - 对于私有仓库,确保已配置 SSH 密钥或 token - -2. **找不到 skill/command** - - 运行 `skillmgr sync` 更新本地缓存 - - 使用 `skillmgr search` 查看可用项 - -3. **安装冲突** - - 已安装的项会提示覆盖确认 - - 使用 `skillmgr uninstall` 先卸载 - -4. **孤立记录** - - 当文件被手动删除时,使用 `skillmgr clean` 清理记录 - -## 开发 - -```bash -# 依赖 -make deps - -# 构建 -make build - -# 代码格式化 -make fmt - -# 代码检查 -make lint -``` - -## License - -MIT diff --git a/manager/cmd/skillmgr/add.go b/manager/cmd/skillmgr/add.go deleted file mode 100644 index a4bb2ba..0000000 --- a/manager/cmd/skillmgr/add.go +++ /dev/null @@ -1,90 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "time" - - "github.com/spf13/cobra" - - "skillmgr/internal/config" - "skillmgr/internal/repo" - "skillmgr/internal/types" -) - -// validateGitURL 验证 Git URL 格式 -func validateGitURL(url string) error { - if url == "" { - return fmt.Errorf("URL 不能为空") - } - // 支持 https://, http://, git@, git:// 协议 - validPrefixes := []string{"https://", "http://", "git@", "git://"} - for _, prefix := range validPrefixes { - if strings.HasPrefix(url, prefix) { - return nil - } - } - return fmt.Errorf("无效的 Git URL 格式,必须以 https://, http://, git@ 或 git:// 开头") -} - -var addCmd = &cobra.Command{ - Use: "add ", - Short: "添加源仓库", - Long: `添加一个 git 仓库作为 skills/commands 的源。 - -示例: - skillmgr add https://github.com/user/skills - skillmgr add https://github.com/user/skills --name my-skills - skillmgr add https://github.com/user/skills --branch main`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - url := args[0] - - // 验证 URL 格式 - if err := validateGitURL(url); err != nil { - return err - } - - name, _ := cmd.Flags().GetString("name") - branch, _ := cmd.Flags().GetString("branch") - - // 如果没有指定名称,从 URL 生成 - if name == "" { - name = repo.URLToPathName(url) - } - - if branch == "" { - branch = "main" // 默认分支 - } - - // Clone 仓库 - fmt.Printf("正在克隆仓库 %s...\n", url) - repoPath, err := repo.CloneOrPull(url, branch) - if err != nil { - return fmt.Errorf("克隆仓库失败: %w", err) - } - - // 保存到配置 - repository := types.Repository{ - Name: name, - URL: url, - Branch: branch, - AddedAt: time.Now(), - } - - if err := config.AddRepository(repository); err != nil { - return err - } - - fmt.Printf("✓ 仓库 '%s' 添加成功\n", name) - fmt.Printf(" 缓存路径: %s\n", repoPath) - - return nil - }, -} - -func init() { - addCmd.Flags().String("name", "", "仓库别名") - addCmd.Flags().String("branch", "main", "克隆的分支") - rootCmd.AddCommand(addCmd) -} diff --git a/manager/cmd/skillmgr/clean.go b/manager/cmd/skillmgr/clean.go deleted file mode 100644 index cb08130..0000000 --- a/manager/cmd/skillmgr/clean.go +++ /dev/null @@ -1,80 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - - "skillmgr/internal/config" - "skillmgr/internal/types" -) - -var cleanCmd = &cobra.Command{ - Use: "clean", - Short: "清理孤立的安装记录", - Long: `检查并清理不存在的安装记录。 - -当文件被手动删除但安装记录仍存在时,使用此命令清理。 - -示例: - # 检查孤立记录(不删除) - skillmgr clean --dry-run - - # 清理孤立记录 - skillmgr clean`, - RunE: func(cmd *cobra.Command, args []string) error { - dryRun, _ := cmd.Flags().GetBool("dry-run") - - cfg, err := config.LoadInstallConfig() - if err != nil { - return err - } - - if len(cfg.Installations) == 0 { - fmt.Println("无安装记录") - return nil - } - - var orphans []types.InstallRecord - var valid []types.InstallRecord - - for _, r := range cfg.Installations { - if _, err := os.Stat(r.InstallPath); os.IsNotExist(err) { - orphans = append(orphans, r) - } else { - valid = append(valid, r) - } - } - - if len(orphans) == 0 { - fmt.Println("无孤立记录") - return nil - } - - fmt.Printf("发现 %d 个孤立记录:\n", len(orphans)) - for _, r := range orphans { - fmt.Printf(" [%s] %s (%s, %s)\n", r.Type, r.Name, r.Platform, r.Scope) - fmt.Printf(" 路径: %s\n", r.InstallPath) - } - - if dryRun { - fmt.Println("\n使用 --dry-run,未执行清理") - return nil - } - - cfg.Installations = valid - if err := config.SaveInstallConfig(cfg); err != nil { - return fmt.Errorf("保存配置失败: %w", err) - } - - fmt.Printf("\n已清理 %d 个孤立记录\n", len(orphans)) - return nil - }, -} - -func init() { - cleanCmd.Flags().Bool("dry-run", false, "仅检查,不执行清理") - - rootCmd.AddCommand(cleanCmd) -} diff --git a/manager/cmd/skillmgr/install.go b/manager/cmd/skillmgr/install.go deleted file mode 100644 index 9eb6f1a..0000000 --- a/manager/cmd/skillmgr/install.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/spf13/cobra" - - "skillmgr/internal/installer" - "skillmgr/internal/types" -) - -var installCmd = &cobra.Command{ - Use: "install ", - Short: "安装 skill 或 command", - Long: `将 skill 或 command 安装到目标平台。 - -类型: skill, command - -示例: - # 全局安装到 Claude Code - skillmgr install skill lyxy-kb --platform claude --global - - # 项目级安装到 OpenCode - skillmgr install command lyxy-kb --platform opencode`, - Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - itemType := args[0] - name := args[1] - - platformStr, _ := cmd.Flags().GetString("platform") - global, _ := cmd.Flags().GetBool("global") - from, _ := cmd.Flags().GetString("from") - - platform := types.Platform(platformStr) - scope := types.ScopeProject - if global { - scope = types.ScopeGlobal - } - - switch itemType { - case "skill": - if from != "" { - return installer.InstallSkillFrom(name, platform, scope, from) - } - return installer.InstallSkill(name, platform, scope) - case "command": - if from != "" { - return installer.InstallCommandFrom(name, platform, scope, from) - } - return installer.InstallCommand(name, platform, scope) - default: - return fmt.Errorf("无效的类型: %s(必须是 'skill' 或 'command')", itemType) - } - }, -} - -func init() { - installCmd.Flags().StringP("platform", "p", "", "目标平台 (claude|opencode)") - installCmd.Flags().BoolP("global", "g", false, "全局安装") - installCmd.Flags().String("from", "", "临时仓库 URL(不保存到配置)") - installCmd.MarkFlagRequired("platform") - - rootCmd.AddCommand(installCmd) -} diff --git a/manager/cmd/skillmgr/list.go b/manager/cmd/skillmgr/list.go deleted file mode 100644 index 13bdecd..0000000 --- a/manager/cmd/skillmgr/list.go +++ /dev/null @@ -1,86 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/spf13/cobra" - - "skillmgr/internal/config" - "skillmgr/internal/types" -) - -var listCmd = &cobra.Command{ - Use: "list", - Short: "列出已安装的 skills 和 commands", - Long: `显示所有已安装的 skills 和 commands。 - -示例: - skillmgr list - skillmgr list --type skill - skillmgr list --platform claude - skillmgr list --global`, - RunE: func(cmd *cobra.Command, args []string) error { - itemTypeStr, _ := cmd.Flags().GetString("type") - platformStr, _ := cmd.Flags().GetString("platform") - global, _ := cmd.Flags().GetBool("global") - - cfg, err := config.LoadInstallConfig() - if err != nil { - return err - } - - if len(cfg.Installations) == 0 { - fmt.Println("无已安装的 skills/commands") - return nil - } - - // 过滤 - var filtered []types.InstallRecord - for _, r := range cfg.Installations { - // 按类型过滤 - if itemTypeStr != "" && string(r.Type) != itemTypeStr { - continue - } - // 按平台过滤 - if platformStr != "" && string(r.Platform) != platformStr { - continue - } - // 按作用域过滤 - if global && r.Scope != types.ScopeGlobal { - continue - } - if !global && cmd.Flags().Changed("global") && r.Scope != types.ScopeProject { - continue - } - filtered = append(filtered, r) - } - - if len(filtered) == 0 { - fmt.Println("无匹配的安装记录") - return nil - } - - fmt.Println("已安装:") - for _, r := range filtered { - fmt.Printf("\n [%s] %s\n", r.Type, r.Name) - fmt.Printf(" 平台: %s\n", r.Platform) - fmt.Printf(" 作用域: %s\n", r.Scope) - fmt.Printf(" 来源: %s\n", r.SourceRepo) - fmt.Printf(" 路径: %s\n", r.InstallPath) - fmt.Printf(" 安装于: %s\n", r.InstalledAt.Format("2006-01-02 15:04:05")) - if !r.UpdatedAt.Equal(r.InstalledAt) { - fmt.Printf(" 更新于: %s\n", r.UpdatedAt.Format("2006-01-02 15:04:05")) - } - } - - return nil - }, -} - -func init() { - listCmd.Flags().String("type", "", "过滤类型 (skill|command)") - listCmd.Flags().String("platform", "", "过滤平台 (claude|opencode)") - listCmd.Flags().BoolP("global", "g", false, "仅显示全局安装") - - rootCmd.AddCommand(listCmd) -} diff --git a/manager/cmd/skillmgr/list_repos.go b/manager/cmd/skillmgr/list_repos.go deleted file mode 100644 index 0d1817a..0000000 --- a/manager/cmd/skillmgr/list_repos.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/spf13/cobra" - - "skillmgr/internal/config" -) - -var listReposCmd = &cobra.Command{ - Use: "list-repos", - Short: "列出已配置的源仓库", - Long: `显示所有已添加的源仓库及其信息。 - -示例: - skillmgr list-repos`, - RunE: func(cmd *cobra.Command, args []string) error { - cfg, err := config.LoadRepositoryConfig() - if err != nil { - return err - } - - if len(cfg.Repositories) == 0 { - fmt.Println("无已配置的源仓库") - fmt.Println("\n使用 'skillmgr add ' 添加仓库") - return nil - } - - fmt.Println("已配置的源仓库:") - for _, repo := range cfg.Repositories { - fmt.Printf("\n %s\n", repo.Name) - fmt.Printf(" URL: %s\n", repo.URL) - fmt.Printf(" 分支: %s\n", repo.Branch) - fmt.Printf(" 添加于: %s\n", repo.AddedAt.Format("2006-01-02 15:04:05")) - } - - return nil - }, -} - -func init() { - rootCmd.AddCommand(listReposCmd) -} diff --git a/manager/cmd/skillmgr/main.go b/manager/cmd/skillmgr/main.go deleted file mode 100644 index 736ef31..0000000 --- a/manager/cmd/skillmgr/main.go +++ /dev/null @@ -1,5 +0,0 @@ -package main - -func main() { - Execute() -} diff --git a/manager/cmd/skillmgr/remove.go b/manager/cmd/skillmgr/remove.go deleted file mode 100644 index da8773f..0000000 --- a/manager/cmd/skillmgr/remove.go +++ /dev/null @@ -1,43 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/spf13/cobra" - - "skillmgr/internal/config" -) - -var removeCmd = &cobra.Command{ - Use: "remove ", - Short: "移除源仓库", - Long: `从配置中移除已添加的源仓库。 - -示例: - skillmgr remove my-skills`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - name := args[0] - - // 检查仓库是否存在 - repo, err := config.FindRepository(name) - if err != nil { - return err - } - if repo == nil { - fmt.Printf("仓库 '%s' 不存在\n", name) - return nil - } - - if err := config.RemoveRepository(name); err != nil { - return err - } - - fmt.Printf("✓ 仓库 '%s' 已移除\n", name) - return nil - }, -} - -func init() { - rootCmd.AddCommand(removeCmd) -} diff --git a/manager/cmd/skillmgr/root.go b/manager/cmd/skillmgr/root.go deleted file mode 100644 index ac94fb7..0000000 --- a/manager/cmd/skillmgr/root.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - - "skillmgr/internal/config" -) - -var rootCmd = &cobra.Command{ - Use: "skillmgr", - Short: "AI 编程平台 skills 和 commands 管理工具", - Long: `skillmgr 是一个用于管理和分发 AI 编程平台 skills 和 commands 的命令行工具。 - -支持从 git 仓库拉取 skills/commands,并根据目标平台(Claude Code、OpenCode) -将其安装到全局目录或项目目录中。 - -示例: - # 添加源仓库 - skillmgr add https://github.com/user/skills --name my-skills - - # 安装 skill - skillmgr install skill lyxy-kb --platform claude --global - - # 列出已安装 - skillmgr list - - # 更新 - skillmgr update skill lyxy-kb --platform claude --global - - # 卸载 - skillmgr uninstall skill lyxy-kb --platform claude --global`, -} - -// Execute 执行根命令 -func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func init() { - // 初始化配置目录 - cobra.OnInitialize(initConfig) -} - -func initConfig() { - if err := config.EnsureConfigDirs(); err != nil { - fmt.Fprintf(os.Stderr, "初始化配置目录失败: %v\n", err) - os.Exit(1) - } -} diff --git a/manager/cmd/skillmgr/search.go b/manager/cmd/skillmgr/search.go deleted file mode 100644 index 92c6c12..0000000 --- a/manager/cmd/skillmgr/search.go +++ /dev/null @@ -1,118 +0,0 @@ -package main - -import ( - "fmt" - "strings" - - "github.com/spf13/cobra" - - "skillmgr/internal/repo" - "skillmgr/internal/types" -) - -var searchCmd = &cobra.Command{ - Use: "search [keyword]", - Short: "搜索可用的 skills 和 commands", - Long: `在已配置的仓库中搜索 skills 和 commands。 - -示例: - # 搜索所有 - skillmgr search - - # 按关键字搜索 - skillmgr search kb - - # 按类型过滤 - skillmgr search --type skill - - # 按仓库过滤 - skillmgr search --repo lyxy`, - RunE: func(cmd *cobra.Command, args []string) error { - keyword := "" - if len(args) > 0 { - keyword = strings.ToLower(args[0]) - } - - itemTypeStr, _ := cmd.Flags().GetString("type") - repoFilter, _ := cmd.Flags().GetString("repo") - - var results []searchResult - - // 搜索 skills - if itemTypeStr == "" || itemTypeStr == "skill" { - skills, err := repo.ListAvailableSkills() - if err != nil { - fmt.Printf("警告: 无法获取 skills: %v\n", err) - } else { - for _, s := range skills { - // 按仓库过滤 - if repoFilter != "" && !strings.Contains(strings.ToLower(s.SourceRepo), strings.ToLower(repoFilter)) { - continue - } - // 按关键字过滤 - if keyword == "" || strings.Contains(strings.ToLower(s.Name), keyword) { - results = append(results, searchResult{ - Type: types.ItemTypeSkill, - Name: s.Name, - RepoName: s.SourceRepo, - }) - } - } - } - } - - // 搜索 commands - if itemTypeStr == "" || itemTypeStr == "command" { - commands, err := repo.ListAvailableCommands() - if err != nil { - fmt.Printf("警告: 无法获取 commands: %v\n", err) - } else { - for _, c := range commands { - // 按仓库过滤 - if repoFilter != "" && !strings.Contains(strings.ToLower(c.SourceRepo), strings.ToLower(repoFilter)) { - continue - } - // 按关键字过滤 - if keyword == "" || strings.Contains(strings.ToLower(c.Name), keyword) { - results = append(results, searchResult{ - Type: types.ItemTypeCommand, - Name: c.Name, - RepoName: c.SourceRepo, - Files: c.Files, - }) - } - } - } - } - - if len(results) == 0 { - fmt.Println("未找到匹配项") - return nil - } - - fmt.Printf("找到 %d 个结果:\n\n", len(results)) - for _, r := range results { - fmt.Printf(" [%s] %s\n", r.Type, r.Name) - fmt.Printf(" 来源: %s\n", r.RepoName) - if len(r.Files) > 0 { - fmt.Printf(" 文件: %s\n", strings.Join(r.Files, ", ")) - } - } - - return nil - }, -} - -type searchResult struct { - Type types.ItemType - Name string - RepoName string - Files []string -} - -func init() { - searchCmd.Flags().String("type", "", "过滤类型 (skill|command)") - searchCmd.Flags().String("repo", "", "过滤仓库") - - rootCmd.AddCommand(searchCmd) -} diff --git a/manager/cmd/skillmgr/sync.go b/manager/cmd/skillmgr/sync.go deleted file mode 100644 index 5832933..0000000 --- a/manager/cmd/skillmgr/sync.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/spf13/cobra" - - "skillmgr/internal/config" - "skillmgr/internal/repo" -) - -var syncCmd = &cobra.Command{ - Use: "sync [name]", - Short: "同步源仓库", - Long: `从远程拉取最新代码,更新本地缓存。 - -示例: - skillmgr sync # 同步所有仓库 - skillmgr sync my-skills # 同步指定仓库`, - RunE: func(cmd *cobra.Command, args []string) error { - cfg, err := config.LoadRepositoryConfig() - if err != nil { - return err - } - - if len(cfg.Repositories) == 0 { - fmt.Println("无已配置的源仓库") - return nil - } - - // 如果指定了仓库名称,只同步该仓库 - if len(args) > 0 { - name := args[0] - for _, r := range cfg.Repositories { - if r.Name == name { - fmt.Printf("正在同步 %s...\n", r.Name) - if _, err := repo.CloneOrPull(r.URL, r.Branch); err != nil { - fmt.Printf(" ✗ 同步失败: %v\n", err) - return err - } - fmt.Printf(" ✓ 同步成功\n") - return nil - } - } - return fmt.Errorf("仓库 '%s' 不存在", name) - } - - // 同步所有仓库 - var hasError bool - for _, r := range cfg.Repositories { - fmt.Printf("正在同步 %s...\n", r.Name) - if _, err := repo.CloneOrPull(r.URL, r.Branch); err != nil { - fmt.Printf(" ✗ 同步失败: %v\n", err) - hasError = true - continue - } - fmt.Printf(" ✓ 同步成功\n") - } - - if hasError { - fmt.Println("\n部分仓库同步失败") - } - - return nil - }, -} - -func init() { - rootCmd.AddCommand(syncCmd) -} diff --git a/manager/cmd/skillmgr/uninstall.go b/manager/cmd/skillmgr/uninstall.go deleted file mode 100644 index 62c90c8..0000000 --- a/manager/cmd/skillmgr/uninstall.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/spf13/cobra" - - "skillmgr/internal/installer" - "skillmgr/internal/types" -) - -var uninstallCmd = &cobra.Command{ - Use: "uninstall ", - Short: "卸载 skill 或 command", - Long: `卸载已安装的 skill 或 command。 - -类型: skill, command - -示例: - skillmgr uninstall skill lyxy-kb --platform claude --global - skillmgr uninstall command lyxy-kb --platform opencode`, - Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - itemType := args[0] - name := args[1] - - platformStr, _ := cmd.Flags().GetString("platform") - global, _ := cmd.Flags().GetBool("global") - - platform := types.Platform(platformStr) - scope := types.ScopeProject - if global { - scope = types.ScopeGlobal - } - - switch itemType { - case "skill": - return installer.UninstallSkill(name, platform, scope) - case "command": - return installer.UninstallCommand(name, platform, scope) - default: - return fmt.Errorf("无效的类型: %s(必须是 'skill' 或 'command')", itemType) - } - }, -} - -func init() { - uninstallCmd.Flags().StringP("platform", "p", "", "目标平台 (claude|opencode)") - uninstallCmd.Flags().BoolP("global", "g", false, "全局卸载") - uninstallCmd.MarkFlagRequired("platform") - - rootCmd.AddCommand(uninstallCmd) -} diff --git a/manager/cmd/skillmgr/update.go b/manager/cmd/skillmgr/update.go deleted file mode 100644 index a7a0164..0000000 --- a/manager/cmd/skillmgr/update.go +++ /dev/null @@ -1,99 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/spf13/cobra" - - "skillmgr/internal/config" - "skillmgr/internal/installer" - "skillmgr/internal/types" -) - -var updateCmd = &cobra.Command{ - Use: "update [type] [name]", - Short: "更新已安装的 skill 或 command", - Long: `从源仓库重新安装最新版本。 - -示例: - # 更新单个 - skillmgr update skill lyxy-kb --platform claude --global - skillmgr update command lyxy-kb --platform opencode - - # 更新所有 - skillmgr update --all`, - RunE: func(cmd *cobra.Command, args []string) error { - all, _ := cmd.Flags().GetBool("all") - - if all { - return updateAll() - } - - if len(args) != 2 { - return fmt.Errorf("需要指定类型和名称,或使用 --all 更新所有") - } - - itemType := args[0] - name := args[1] - - platformStr, _ := cmd.Flags().GetString("platform") - global, _ := cmd.Flags().GetBool("global") - - platform := types.Platform(platformStr) - scope := types.ScopeProject - if global { - scope = types.ScopeGlobal - } - - switch itemType { - case "skill": - return installer.UpdateSkill(name, platform, scope) - case "command": - return installer.UpdateCommand(name, platform, scope) - default: - return fmt.Errorf("无效的类型: %s(必须是 'skill' 或 'command')", itemType) - } - }, -} - -func updateAll() error { - cfg, err := config.LoadInstallConfig() - if err != nil { - return err - } - - if len(cfg.Installations) == 0 { - fmt.Println("无已安装的 skills/commands") - return nil - } - - var hasError bool - for _, r := range cfg.Installations { - fmt.Printf("正在更新 [%s] %s...\n", r.Type, r.Name) - var err error - if r.Type == types.ItemTypeSkill { - err = installer.UpdateSkill(r.Name, r.Platform, r.Scope) - } else { - err = installer.UpdateCommand(r.Name, r.Platform, r.Scope) - } - if err != nil { - fmt.Printf(" ✗ 更新失败: %v\n", err) - hasError = true - continue - } - } - - if hasError { - fmt.Println("\n部分项目更新失败") - } - - return nil -} - -func init() { - updateCmd.Flags().StringP("platform", "p", "", "目标平台 (claude|opencode)") - updateCmd.Flags().BoolP("global", "g", false, "全局更新") - updateCmd.Flags().Bool("all", false, "更新所有已安装项") - - rootCmd.AddCommand(updateCmd) -} diff --git a/manager/go.mod b/manager/go.mod deleted file mode 100644 index 5ca2e20..0000000 --- a/manager/go.mod +++ /dev/null @@ -1,9 +0,0 @@ -module skillmgr - -go 1.25.0 - -require ( - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/cobra v1.10.2 // indirect - github.com/spf13/pflag v1.0.9 // indirect -) diff --git a/manager/go.sum b/manager/go.sum deleted file mode 100644 index a6ee3e0..0000000 --- a/manager/go.sum +++ /dev/null @@ -1,10 +0,0 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/manager/internal/adapter/adapter.go b/manager/internal/adapter/adapter.go deleted file mode 100644 index ba11f53..0000000 --- a/manager/internal/adapter/adapter.go +++ /dev/null @@ -1,50 +0,0 @@ -package adapter - -import ( - "fmt" - "os" - - "skillmgr/internal/types" -) - -// PlatformAdapter 平台适配器接口 -type PlatformAdapter interface { - // GetSkillInstallPath 获取 skill 安装路径 - GetSkillInstallPath(scope types.Scope, skillName string) (string, error) - - // GetCommandInstallPath 获取 command 安装路径 - GetCommandInstallPath(scope types.Scope, commandGroup string) (string, error) - - // AdaptSkill 适配 skill(返回 source → dest 映射) - AdaptSkill(sourcePath, destBasePath string) (map[string]string, error) - - // AdaptCommand 适配 command(返回 source → dest 映射) - AdaptCommand(sourcePath, destBasePath, commandGroup string) (map[string]string, error) -} - -// GetAdapter 获取平台适配器 -func GetAdapter(platform types.Platform) (PlatformAdapter, error) { - switch platform { - case types.PlatformClaude: - return &ClaudeAdapter{}, nil - case types.PlatformOpenCode: - return &OpenCodeAdapter{}, nil - default: - return nil, fmt.Errorf("不支持的平台: %s", platform) - } -} - -// getBasePath 获取基础路径 -// 支持通过环境变量 SKILLMGR_TEST_BASE 覆盖(用于测试隔离) -func getBasePath(scope types.Scope) (string, error) { - // 测试模式:使用环境变量指定的目录 - if testBase := os.Getenv("SKILLMGR_TEST_BASE"); testBase != "" { - return testBase, nil - } - - // 生产模式 - if scope == types.ScopeGlobal { - return os.UserHomeDir() - } - return os.Getwd() -} diff --git a/manager/internal/adapter/claude.go b/manager/internal/adapter/claude.go deleted file mode 100644 index 5534ceb..0000000 --- a/manager/internal/adapter/claude.go +++ /dev/null @@ -1,73 +0,0 @@ -package adapter - -import ( - "fmt" - "os" - "path/filepath" - - "skillmgr/internal/types" -) - -// ClaudeAdapter Claude Code 平台适配器 -type ClaudeAdapter struct{} - -// GetSkillInstallPath 获取 skill 安装路径 -func (a *ClaudeAdapter) GetSkillInstallPath(scope types.Scope, skillName string) (string, error) { - base, err := getBasePath(scope) - if err != nil { - return "", err - } - return filepath.Join(base, ".claude", "skills", skillName), nil -} - -// GetCommandInstallPath 获取 command 安装路径 -func (a *ClaudeAdapter) GetCommandInstallPath(scope types.Scope, commandGroup string) (string, error) { - base, err := getBasePath(scope) - if err != nil { - return "", err - } - return filepath.Join(base, ".claude", "commands", commandGroup), nil -} - -// AdaptSkill 适配 skill(遍历源目录,生成文件映射) -func (a *ClaudeAdapter) AdaptSkill(sourcePath, destBasePath string) (map[string]string, error) { - mapping := make(map[string]string) - - err := filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - relPath, err := filepath.Rel(sourcePath, path) - if err != nil { - return fmt.Errorf("计算相对路径失败: %w", err) - } - destPath := filepath.Join(destBasePath, relPath) - - if !info.IsDir() { - mapping[path] = destPath - } - - return nil - }) - - return mapping, err -} - -// AdaptCommand 适配 command(保持目录结构) -func (a *ClaudeAdapter) AdaptCommand(sourcePath, destBasePath, commandGroup string) (map[string]string, error) { - mapping := make(map[string]string) - - files, err := filepath.Glob(filepath.Join(sourcePath, "*.md")) - if err != nil { - return nil, err - } - - for _, file := range files { - fileName := filepath.Base(file) - destPath := filepath.Join(destBasePath, fileName) - mapping[file] = destPath - } - - return mapping, nil -} diff --git a/manager/internal/adapter/claude_test.go b/manager/internal/adapter/claude_test.go deleted file mode 100644 index 10e349b..0000000 --- a/manager/internal/adapter/claude_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package adapter - -import ( - "os" - "path/filepath" - "testing" - - "skillmgr/internal/types" -) - -func setupAdapterTestEnv(t *testing.T) (string, func()) { - t.Helper() - - tmpDir, err := os.MkdirTemp("", "skillmgr-adapter-test-*") - if err != nil { - t.Fatalf("创建临时目录失败: %v", err) - } - - os.Setenv("SKILLMGR_TEST_BASE", tmpDir) - - cleanup := func() { - os.Unsetenv("SKILLMGR_TEST_BASE") - os.RemoveAll(tmpDir) - } - - return tmpDir, cleanup -} - -func TestClaudeAdapter_GetSkillInstallPath_Global(t *testing.T) { - tmpDir, cleanup := setupAdapterTestEnv(t) - defer cleanup() - - adapter := &ClaudeAdapter{} - path, err := adapter.GetSkillInstallPath(types.ScopeGlobal, "test-skill") - if err != nil { - t.Fatalf("GetSkillInstallPath 失败: %v", err) - } - - expected := filepath.Join(tmpDir, ".claude", "skills", "test-skill") - if path != expected { - t.Errorf("期望 %s,得到 %s", expected, path) - } -} - -func TestClaudeAdapter_GetSkillInstallPath_Project(t *testing.T) { - _, cleanup := setupAdapterTestEnv(t) - defer cleanup() - - adapter := &ClaudeAdapter{} - path, err := adapter.GetSkillInstallPath(types.ScopeProject, "test-skill") - if err != nil { - t.Fatalf("GetSkillInstallPath 失败: %v", err) - } - - // 项目级路径是相对当前目录的 - if !filepath.IsAbs(path) { - // 相对路径应该包含 .claude/skills - if filepath.Base(filepath.Dir(path)) != "skills" { - t.Errorf("期望路径包含 skills 目录,得到 %s", path) - } - } -} - -func TestClaudeAdapter_GetCommandInstallPath_Global(t *testing.T) { - tmpDir, cleanup := setupAdapterTestEnv(t) - defer cleanup() - - adapter := &ClaudeAdapter{} - path, err := adapter.GetCommandInstallPath(types.ScopeGlobal, "test-cmd") - if err != nil { - t.Fatalf("GetCommandInstallPath 失败: %v", err) - } - - expected := filepath.Join(tmpDir, ".claude", "commands", "test-cmd") - if path != expected { - t.Errorf("期望 %s,得到 %s", expected, path) - } -} - -func TestClaudeAdapter_AdaptSkill(t *testing.T) { - tmpDir, cleanup := setupAdapterTestEnv(t) - defer cleanup() - - // 创建源目录 - srcDir := filepath.Join(tmpDir, "src-skill") - os.MkdirAll(srcDir, 0755) - os.WriteFile(filepath.Join(srcDir, "SKILL.md"), []byte("test"), 0644) - os.WriteFile(filepath.Join(srcDir, "helper.md"), []byte("test"), 0644) - - destDir := filepath.Join(tmpDir, "dest-skill") - - adapter := &ClaudeAdapter{} - mapping, err := adapter.AdaptSkill(srcDir, destDir) - if err != nil { - t.Fatalf("AdaptSkill 失败: %v", err) - } - - if len(mapping) != 2 { - t.Errorf("期望 2 个文件映射,得到 %d 个", len(mapping)) - } -} - -func TestClaudeAdapter_AdaptCommand(t *testing.T) { - tmpDir, cleanup := setupAdapterTestEnv(t) - defer cleanup() - - // 创建源目录 - srcDir := filepath.Join(tmpDir, "src-cmd") - os.MkdirAll(srcDir, 0755) - os.WriteFile(filepath.Join(srcDir, "init.md"), []byte("test"), 0644) - os.WriteFile(filepath.Join(srcDir, "run.md"), []byte("test"), 0644) - - destDir := filepath.Join(tmpDir, "dest-cmd") - - adapter := &ClaudeAdapter{} - mapping, err := adapter.AdaptCommand(srcDir, destDir, "test-cmd") - if err != nil { - t.Fatalf("AdaptCommand 失败: %v", err) - } - - if len(mapping) != 2 { - t.Errorf("期望 2 个文件映射,得到 %d 个", len(mapping)) - } - - // 验证文件名保持原样 - for src, dest := range mapping { - srcBase := filepath.Base(src) - destBase := filepath.Base(dest) - if srcBase != destBase { - t.Errorf("Claude 适配器应保持文件名:源 %s,目标 %s", srcBase, destBase) - } - } -} diff --git a/manager/internal/adapter/opencode.go b/manager/internal/adapter/opencode.go deleted file mode 100644 index b03b3d6..0000000 --- a/manager/internal/adapter/opencode.go +++ /dev/null @@ -1,89 +0,0 @@ -package adapter - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "skillmgr/internal/types" -) - -// OpenCodeAdapter OpenCode 平台适配器 -type OpenCodeAdapter struct{} - -// GetSkillInstallPath 获取 skill 安装路径 -func (a *OpenCodeAdapter) GetSkillInstallPath(scope types.Scope, skillName string) (string, error) { - base, err := getBasePath(scope) - if err != nil { - return "", err - } - if scope == types.ScopeGlobal { - // 全局: ~/.config/opencode/skills// - return filepath.Join(base, ".config", "opencode", "skills", skillName), nil - } - // 项目级: ./.opencode/skills// - return filepath.Join(base, ".opencode", "skills", skillName), nil -} - -// GetCommandInstallPath 获取 command 安装路径 -func (a *OpenCodeAdapter) GetCommandInstallPath(scope types.Scope, commandGroup string) (string, error) { - base, err := getBasePath(scope) - if err != nil { - return "", err - } - if scope == types.ScopeGlobal { - // 全局: ~/.config/opencode/commands/(扁平化,所有命令在同一目录) - return filepath.Join(base, ".config", "opencode", "commands"), nil - } - // 项目级: ./.opencode/commands/ - return filepath.Join(base, ".opencode", "commands"), nil -} - -// AdaptSkill 适配 skill(与 Claude 相同,保持目录结构) -func (a *OpenCodeAdapter) AdaptSkill(sourcePath, destBasePath string) (map[string]string, error) { - mapping := make(map[string]string) - - err := filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - relPath, err := filepath.Rel(sourcePath, path) - if err != nil { - return fmt.Errorf("计算相对路径失败: %w", err) - } - destPath := filepath.Join(destBasePath, relPath) - - if !info.IsDir() { - mapping[path] = destPath - } - - return nil - }) - - return mapping, err -} - -// AdaptCommand 适配 command(扁平化文件名:-.md) -func (a *OpenCodeAdapter) AdaptCommand(sourcePath, destBasePath, commandGroup string) (map[string]string, error) { - mapping := make(map[string]string) - - files, err := filepath.Glob(filepath.Join(sourcePath, "*.md")) - if err != nil { - return nil, err - } - - for _, file := range files { - fileName := filepath.Base(file) - baseName := strings.TrimSuffix(fileName, ".md") - - // 重命名:init.md → lyxy-kb-init.md - newName := commandGroup + "-" + baseName + ".md" - destPath := filepath.Join(destBasePath, newName) - - mapping[file] = destPath - } - - return mapping, nil -} diff --git a/manager/internal/adapter/opencode_test.go b/manager/internal/adapter/opencode_test.go deleted file mode 100644 index 911ff26..0000000 --- a/manager/internal/adapter/opencode_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package adapter - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "skillmgr/internal/types" -) - -func TestOpenCodeAdapter_GetSkillInstallPath_Global(t *testing.T) { - tmpDir, cleanup := setupAdapterTestEnv(t) - defer cleanup() - - adapter := &OpenCodeAdapter{} - path, err := adapter.GetSkillInstallPath(types.ScopeGlobal, "test-skill") - if err != nil { - t.Fatalf("GetSkillInstallPath 失败: %v", err) - } - - // OpenCode 全局 skill 使用 ~/.config/opencode/skills/ - expected := filepath.Join(tmpDir, ".config", "opencode", "skills", "test-skill") - if path != expected { - t.Errorf("期望 %s,得到 %s", expected, path) - } -} - -func TestOpenCodeAdapter_GetCommandInstallPath_Global(t *testing.T) { - tmpDir, cleanup := setupAdapterTestEnv(t) - defer cleanup() - - adapter := &OpenCodeAdapter{} - path, err := adapter.GetCommandInstallPath(types.ScopeGlobal, "test-cmd") - if err != nil { - t.Fatalf("GetCommandInstallPath 失败: %v", err) - } - - // OpenCode 全局 command 使用 ~/.config/opencode/commands/ - expected := filepath.Join(tmpDir, ".config", "opencode", "commands") - if path != expected { - t.Errorf("期望 %s,得到 %s", expected, path) - } -} - -func TestOpenCodeAdapter_AdaptCommand_Flattening(t *testing.T) { - tmpDir, cleanup := setupAdapterTestEnv(t) - defer cleanup() - - // 创建源目录 - srcDir := filepath.Join(tmpDir, "src-cmd") - os.MkdirAll(srcDir, 0755) - os.WriteFile(filepath.Join(srcDir, "init.md"), []byte("test"), 0644) - os.WriteFile(filepath.Join(srcDir, "run.md"), []byte("test"), 0644) - - destDir := filepath.Join(tmpDir, "dest-cmd") - - adapter := &OpenCodeAdapter{} - mapping, err := adapter.AdaptCommand(srcDir, destDir, "test-cmd") - if err != nil { - t.Fatalf("AdaptCommand 失败: %v", err) - } - - if len(mapping) != 2 { - t.Errorf("期望 2 个文件映射,得到 %d 个", len(mapping)) - } - - // 验证文件名被扁平化 - for src, dest := range mapping { - srcBase := filepath.Base(src) - destBase := filepath.Base(dest) - - // init.md -> test-cmd-init.md - nameWithoutExt := strings.TrimSuffix(srcBase, ".md") - expectedBase := "test-cmd-" + nameWithoutExt + ".md" - if destBase != expectedBase { - t.Errorf("OpenCode 适配器应扁平化文件名:期望 %s,得到 %s", expectedBase, destBase) - } - } -} - -func TestGetAdapter_Claude(t *testing.T) { - adapter, err := GetAdapter(types.PlatformClaude) - if err != nil { - t.Fatalf("GetAdapter(claude) 失败: %v", err) - } - - if _, ok := adapter.(*ClaudeAdapter); !ok { - t.Error("期望 ClaudeAdapter 类型") - } -} - -func TestGetAdapter_OpenCode(t *testing.T) { - adapter, err := GetAdapter(types.PlatformOpenCode) - if err != nil { - t.Fatalf("GetAdapter(opencode) 失败: %v", err) - } - - if _, ok := adapter.(*OpenCodeAdapter); !ok { - t.Error("期望 OpenCodeAdapter 类型") - } -} - -func TestGetAdapter_Invalid(t *testing.T) { - _, err := GetAdapter(types.Platform("invalid")) - if err == nil { - t.Error("期望无效平台返回错误") - } -} diff --git a/manager/internal/config/install.go b/manager/internal/config/install.go deleted file mode 100644 index 83b5197..0000000 --- a/manager/internal/config/install.go +++ /dev/null @@ -1,141 +0,0 @@ -package config - -import ( - "encoding/json" - "fmt" - "os" - - "skillmgr/internal/types" -) - -// LoadInstallConfig 加载安装配置 -func LoadInstallConfig() (*types.InstallConfig, error) { - path, err := GetInstallConfigPath() - if err != nil { - return nil, err - } - - if _, err := os.Stat(path); os.IsNotExist(err) { - return &types.InstallConfig{ - Installations: []types.InstallRecord{}, - }, nil - } - - data, err := os.ReadFile(path) - if err != nil { - return nil, err - } - - var cfg types.InstallConfig - if err := json.Unmarshal(data, &cfg); err != nil { - return nil, fmt.Errorf("解析 install.json 失败: %w(请检查 JSON 格式)", err) - } - - return &cfg, nil -} - -// SaveInstallConfig 保存安装配置 -func SaveInstallConfig(cfg *types.InstallConfig) error { - path, err := GetInstallConfigPath() - if err != nil { - return err - } - - data, err := json.MarshalIndent(cfg, "", " ") - if err != nil { - return err - } - - return os.WriteFile(path, data, 0644) -} - -// AddInstallRecord 添加安装记录 -func AddInstallRecord(record types.InstallRecord) error { - cfg, err := LoadInstallConfig() - if err != nil { - return err - } - - cfg.Installations = append(cfg.Installations, record) - return SaveInstallConfig(cfg) -} - -// RemoveInstallRecord 移除安装记录 -func RemoveInstallRecord(itemType types.ItemType, name string, platform types.Platform, scope types.Scope) error { - cfg, err := LoadInstallConfig() - if err != nil { - return err - } - - for i, r := range cfg.Installations { - if r.Type == itemType && r.Name == name && r.Platform == platform && r.Scope == scope { - cfg.Installations = append(cfg.Installations[:i], cfg.Installations[i+1:]...) - return SaveInstallConfig(cfg) - } - } - - return nil -} - -// FindInstallRecord 查找安装记录 -func FindInstallRecord(itemType types.ItemType, name string, platform types.Platform, scope types.Scope) (*types.InstallRecord, error) { - cfg, err := LoadInstallConfig() - if err != nil { - return nil, err - } - - for _, r := range cfg.Installations { - if r.Type == itemType && r.Name == name && r.Platform == platform && r.Scope == scope { - return &r, nil - } - } - - return nil, nil -} - -// UpdateInstallRecord 更新安装记录 -func UpdateInstallRecord(record types.InstallRecord) error { - cfg, err := LoadInstallConfig() - if err != nil { - return err - } - - for i, r := range cfg.Installations { - if r.Type == record.Type && r.Name == record.Name && - r.Platform == record.Platform && r.Scope == record.Scope { - cfg.Installations[i] = record - return SaveInstallConfig(cfg) - } - } - - return nil -} - -// CleanOrphanRecords 清理孤立记录(安装路径不存在) -func CleanOrphanRecords() ([]types.InstallRecord, error) { - cfg, err := LoadInstallConfig() - if err != nil { - return nil, err - } - - // 预分配切片容量,减少内存分配次数 - cleaned := make([]types.InstallRecord, 0, len(cfg.Installations)/2) - valid := make([]types.InstallRecord, 0, len(cfg.Installations)) - - for _, r := range cfg.Installations { - if _, err := os.Stat(r.InstallPath); os.IsNotExist(err) { - cleaned = append(cleaned, r) - } else { - valid = append(valid, r) - } - } - - if len(cleaned) > 0 { - cfg.Installations = valid - if err := SaveInstallConfig(cfg); err != nil { - return nil, err - } - } - - return cleaned, nil -} diff --git a/manager/internal/config/install_test.go b/manager/internal/config/install_test.go deleted file mode 100644 index 9b28f8f..0000000 --- a/manager/internal/config/install_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package config - -import ( - "os" - "path/filepath" - "testing" - "time" - - "skillmgr/internal/types" -) - -func setupInstallTestEnv(t *testing.T) (string, func()) { - t.Helper() - - tmpDir, err := os.MkdirTemp("", "skillmgr-install-test-*") - if err != nil { - t.Fatalf("创建临时目录失败: %v", err) - } - - os.Setenv("SKILLMGR_TEST_ROOT", tmpDir) - - cleanup := func() { - os.Unsetenv("SKILLMGR_TEST_ROOT") - os.RemoveAll(tmpDir) - } - - return tmpDir, cleanup -} - -func TestLoadInstallConfig_Empty(t *testing.T) { - _, cleanup := setupInstallTestEnv(t) - defer cleanup() - - cfg, err := LoadInstallConfig() - if err != nil { - t.Fatalf("LoadInstallConfig 失败: %v", err) - } - - if len(cfg.Installations) != 0 { - t.Errorf("期望空安装列表,得到 %d 个", len(cfg.Installations)) - } -} - -func TestAddInstallRecord_Success(t *testing.T) { - _, cleanup := setupInstallTestEnv(t) - defer cleanup() - - record := types.InstallRecord{ - Type: types.ItemTypeSkill, - Name: "test-skill", - SourceRepo: "test-repo", - Platform: types.PlatformClaude, - Scope: types.ScopeGlobal, - InstallPath: "/path/to/skill", - InstalledAt: time.Now(), - UpdatedAt: time.Now(), - } - - if err := AddInstallRecord(record); err != nil { - t.Fatalf("AddInstallRecord 失败: %v", err) - } - - cfg, _ := LoadInstallConfig() - if len(cfg.Installations) != 1 { - t.Errorf("期望 1 条记录,得到 %d 条", len(cfg.Installations)) - } -} - -func TestFindInstallRecord_Found(t *testing.T) { - _, cleanup := setupInstallTestEnv(t) - defer cleanup() - - record := types.InstallRecord{ - Type: types.ItemTypeSkill, - Name: "test-skill", - Platform: types.PlatformClaude, - Scope: types.ScopeGlobal, - InstalledAt: time.Now(), - UpdatedAt: time.Now(), - } - - AddInstallRecord(record) - - found, err := FindInstallRecord(types.ItemTypeSkill, "test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("FindInstallRecord 失败: %v", err) - } - - if found.Name != "test-skill" { - t.Errorf("期望名称 'test-skill',得到 '%s'", found.Name) - } -} - -func TestRemoveInstallRecord_Success(t *testing.T) { - _, cleanup := setupInstallTestEnv(t) - defer cleanup() - - record := types.InstallRecord{ - Type: types.ItemTypeSkill, - Name: "test-skill", - Platform: types.PlatformClaude, - Scope: types.ScopeGlobal, - InstalledAt: time.Now(), - UpdatedAt: time.Now(), - } - - AddInstallRecord(record) - - if err := RemoveInstallRecord(types.ItemTypeSkill, "test-skill", types.PlatformClaude, types.ScopeGlobal); err != nil { - t.Fatalf("RemoveInstallRecord 失败: %v", err) - } - - cfg, _ := LoadInstallConfig() - if len(cfg.Installations) != 0 { - t.Errorf("期望 0 条记录,得到 %d 条", len(cfg.Installations)) - } -} - -func TestCleanOrphanRecords(t *testing.T) { - tmpDir, cleanup := setupInstallTestEnv(t) - defer cleanup() - - // 创建一个存在的路径 - existingPath := filepath.Join(tmpDir, "existing-skill") - os.MkdirAll(existingPath, 0755) - - // 添加两条记录:一条存在,一条不存在 - record1 := types.InstallRecord{ - Type: types.ItemTypeSkill, - Name: "existing-skill", - Platform: types.PlatformClaude, - Scope: types.ScopeGlobal, - InstallPath: existingPath, - InstalledAt: time.Now(), - UpdatedAt: time.Now(), - } - - record2 := types.InstallRecord{ - Type: types.ItemTypeSkill, - Name: "orphan-skill", - Platform: types.PlatformClaude, - Scope: types.ScopeGlobal, - InstallPath: "/nonexistent/path", - InstalledAt: time.Now(), - UpdatedAt: time.Now(), - } - - AddInstallRecord(record1) - AddInstallRecord(record2) - - cleaned, err := CleanOrphanRecords() - if err != nil { - t.Fatalf("CleanOrphanRecords 失败: %v", err) - } - - if len(cleaned) != 1 { - t.Errorf("期望清理 1 条记录,清理了 %d 条", len(cleaned)) - } - - if len(cleaned) > 0 && cleaned[0].Name != "orphan-skill" { - t.Errorf("期望清理 'orphan-skill',清理了 '%s'", cleaned[0].Name) - } - - // 验证只剩下存在的记录 - cfg, _ := LoadInstallConfig() - if len(cfg.Installations) != 1 { - t.Errorf("期望剩余 1 条记录,剩余 %d 条", len(cfg.Installations)) - } -} diff --git a/manager/internal/config/paths.go b/manager/internal/config/paths.go deleted file mode 100644 index c2df295..0000000 --- a/manager/internal/config/paths.go +++ /dev/null @@ -1,77 +0,0 @@ -package config - -import ( - "os" - "path/filepath" -) - -const ( - ConfigDir = ".skillmgr" - RepositoryFile = "repository.json" - InstallFile = "install.json" - CacheDir = "cache" -) - -// GetConfigRoot 获取配置根目录 -// 支持通过环境变量 SKILLMGR_TEST_ROOT 覆盖(用于测试隔离) -func GetConfigRoot() (string, error) { - // 测试模式:使用环境变量指定的临时目录 - if testRoot := os.Getenv("SKILLMGR_TEST_ROOT"); testRoot != "" { - return testRoot, nil - } - - // 生产模式:使用用户主目录 - home, err := os.UserHomeDir() - if err != nil { - return "", err - } - return filepath.Join(home, ConfigDir), nil -} - -// GetRepositoryConfigPath 获取 repository.json 路径 -func GetRepositoryConfigPath() (string, error) { - root, err := GetConfigRoot() - if err != nil { - return "", err - } - return filepath.Join(root, RepositoryFile), nil -} - -// GetInstallConfigPath 获取 install.json 路径 -func GetInstallConfigPath() (string, error) { - root, err := GetConfigRoot() - if err != nil { - return "", err - } - return filepath.Join(root, InstallFile), nil -} - -// GetCachePath 获取缓存目录路径 -func GetCachePath() (string, error) { - root, err := GetConfigRoot() - if err != nil { - return "", err - } - return filepath.Join(root, CacheDir), nil -} - -// EnsureConfigDirs 确保配置目录存在 -func EnsureConfigDirs() error { - root, err := GetConfigRoot() - if err != nil { - return err - } - - dirs := []string{ - root, - filepath.Join(root, CacheDir), - } - - for _, dir := range dirs { - if err := os.MkdirAll(dir, 0755); err != nil { - return err - } - } - - return nil -} diff --git a/manager/internal/config/paths_test.go b/manager/internal/config/paths_test.go deleted file mode 100644 index db3d94e..0000000 --- a/manager/internal/config/paths_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package config - -import ( - "os" - "path/filepath" - "testing" -) - -func TestGetConfigRoot_Default(t *testing.T) { - // 清除环境变量 - os.Unsetenv("SKILLMGR_TEST_ROOT") - - root, err := GetConfigRoot() - if err != nil { - t.Fatalf("GetConfigRoot 失败: %v", err) - } - - home, _ := os.UserHomeDir() - expected := filepath.Join(home, ConfigDir) - - if root != expected { - t.Errorf("期望 %s,得到 %s", expected, root) - } -} - -func TestGetConfigRoot_WithEnvOverride(t *testing.T) { - testRoot := "/tmp/skillmgr-test" - os.Setenv("SKILLMGR_TEST_ROOT", testRoot) - defer os.Unsetenv("SKILLMGR_TEST_ROOT") - - root, err := GetConfigRoot() - if err != nil { - t.Fatalf("GetConfigRoot 失败: %v", err) - } - - if root != testRoot { - t.Errorf("期望 %s,得到 %s", testRoot, root) - } -} - -func TestEnsureConfigDirs(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "skillmgr-test-*") - if err != nil { - t.Fatalf("创建临时目录失败: %v", err) - } - defer os.RemoveAll(tmpDir) - - os.Setenv("SKILLMGR_TEST_ROOT", tmpDir) - defer os.Unsetenv("SKILLMGR_TEST_ROOT") - - if err := EnsureConfigDirs(); err != nil { - t.Fatalf("EnsureConfigDirs 失败: %v", err) - } - - // 检查目录是否存在 - cacheDir := filepath.Join(tmpDir, CacheDir) - if _, err := os.Stat(cacheDir); os.IsNotExist(err) { - t.Errorf("缓存目录未创建: %s", cacheDir) - } -} diff --git a/manager/internal/config/repository.go b/manager/internal/config/repository.go deleted file mode 100644 index dc7e58b..0000000 --- a/manager/internal/config/repository.go +++ /dev/null @@ -1,105 +0,0 @@ -package config - -import ( - "encoding/json" - "fmt" - "os" - - "skillmgr/internal/types" -) - -// LoadRepositoryConfig 加载仓库配置 -func LoadRepositoryConfig() (*types.RepositoryConfig, error) { - path, err := GetRepositoryConfigPath() - if err != nil { - return nil, err - } - - // 如果文件不存在,返回空配置 - if _, err := os.Stat(path); os.IsNotExist(err) { - return &types.RepositoryConfig{ - Repositories: []types.Repository{}, - }, nil - } - - data, err := os.ReadFile(path) - if err != nil { - return nil, err - } - - var cfg types.RepositoryConfig - if err := json.Unmarshal(data, &cfg); err != nil { - return nil, fmt.Errorf("解析 repository.json 失败: %w(请检查 JSON 格式)", err) - } - - return &cfg, nil -} - -// SaveRepositoryConfig 保存仓库配置 -func SaveRepositoryConfig(cfg *types.RepositoryConfig) error { - path, err := GetRepositoryConfigPath() - if err != nil { - return err - } - - data, err := json.MarshalIndent(cfg, "", " ") - if err != nil { - return err - } - - return os.WriteFile(path, data, 0644) -} - -// AddRepository 添加仓库 -// 如果仓库名已存在,返回错误提示先移除 -func AddRepository(repo types.Repository) error { - cfg, err := LoadRepositoryConfig() - if err != nil { - return err - } - - // 检查是否已存在同名仓库 - for _, r := range cfg.Repositories { - if r.Name == repo.Name { - return fmt.Errorf("仓库名称 '%s' 已存在,请先使用 `skillmgr remove %s` 移除", repo.Name, repo.Name) - } - } - - // 新增 - cfg.Repositories = append(cfg.Repositories, repo) - return SaveRepositoryConfig(cfg) -} - -// RemoveRepository 移除仓库 -func RemoveRepository(name string) error { - cfg, err := LoadRepositoryConfig() - if err != nil { - return err - } - - for i, r := range cfg.Repositories { - if r.Name == name { - cfg.Repositories = append(cfg.Repositories[:i], cfg.Repositories[i+1:]...) - return SaveRepositoryConfig(cfg) - } - } - - // 仓库不存在,不报错 - return nil -} - -// FindRepository 查找仓库 -func FindRepository(name string) (*types.Repository, error) { - cfg, err := LoadRepositoryConfig() - if err != nil { - return nil, err - } - - for _, r := range cfg.Repositories { - if r.Name == name { - return &r, nil - } - } - - return nil, nil -} diff --git a/manager/internal/config/repository_test.go b/manager/internal/config/repository_test.go deleted file mode 100644 index d431ca2..0000000 --- a/manager/internal/config/repository_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package config - -import ( - "os" - "testing" - "time" - - "skillmgr/internal/types" -) - -func setupRepoTestEnv(t *testing.T) (string, func()) { - t.Helper() - - tmpDir, err := os.MkdirTemp("", "skillmgr-repo-test-*") - if err != nil { - t.Fatalf("创建临时目录失败: %v", err) - } - - os.Setenv("SKILLMGR_TEST_ROOT", tmpDir) - - cleanup := func() { - os.Unsetenv("SKILLMGR_TEST_ROOT") - os.RemoveAll(tmpDir) - } - - return tmpDir, cleanup -} - -func TestLoadRepositoryConfig_Empty(t *testing.T) { - _, cleanup := setupRepoTestEnv(t) - defer cleanup() - - cfg, err := LoadRepositoryConfig() - if err != nil { - t.Fatalf("LoadRepositoryConfig 失败: %v", err) - } - - if len(cfg.Repositories) != 0 { - t.Errorf("期望空仓库列表,得到 %d 个", len(cfg.Repositories)) - } -} - -func TestAddRepository_Success(t *testing.T) { - _, cleanup := setupRepoTestEnv(t) - defer cleanup() - - repo := types.Repository{ - Name: "test-repo", - URL: "https://github.com/test/repo.git", - Branch: "main", - AddedAt: time.Now(), - } - - if err := AddRepository(repo); err != nil { - t.Fatalf("AddRepository 失败: %v", err) - } - - // 验证已添加 - cfg, _ := LoadRepositoryConfig() - if len(cfg.Repositories) != 1 { - t.Errorf("期望 1 个仓库,得到 %d 个", len(cfg.Repositories)) - } - - if cfg.Repositories[0].Name != "test-repo" { - t.Errorf("期望名称 'test-repo',得到 '%s'", cfg.Repositories[0].Name) - } -} - -func TestAddRepository_RejectDuplicate(t *testing.T) { - _, cleanup := setupRepoTestEnv(t) - defer cleanup() - - repo := types.Repository{ - Name: "test-repo", - URL: "https://github.com/test/repo.git", - Branch: "main", - AddedAt: time.Now(), - } - - // 第一次添加 - if err := AddRepository(repo); err != nil { - t.Fatalf("第一次 AddRepository 失败: %v", err) - } - - // 第二次添加应该失败 - err := AddRepository(repo) - if err == nil { - t.Error("期望添加重复仓库时返回错误") - } -} - -func TestRemoveRepository_Success(t *testing.T) { - _, cleanup := setupRepoTestEnv(t) - defer cleanup() - - repo := types.Repository{ - Name: "test-repo", - URL: "https://github.com/test/repo.git", - AddedAt: time.Now(), - } - - AddRepository(repo) - - if err := RemoveRepository("test-repo"); err != nil { - t.Fatalf("RemoveRepository 失败: %v", err) - } - - cfg, _ := LoadRepositoryConfig() - if len(cfg.Repositories) != 0 { - t.Errorf("期望 0 个仓库,得到 %d 个", len(cfg.Repositories)) - } -} - -func TestRemoveRepository_NotFound(t *testing.T) { - _, cleanup := setupRepoTestEnv(t) - defer cleanup() - - // RemoveRepository 实现中,不存在的仓库不报错 - err := RemoveRepository("nonexistent") - if err != nil { - t.Errorf("RemoveRepository 不应该报错: %v", err) - } -} - -func TestFindRepository_Found(t *testing.T) { - _, cleanup := setupRepoTestEnv(t) - defer cleanup() - - repo := types.Repository{ - Name: "test-repo", - URL: "https://github.com/test/repo.git", - AddedAt: time.Now(), - } - - AddRepository(repo) - - found, err := FindRepository("test-repo") - if err != nil { - t.Fatalf("FindRepository 失败: %v", err) - } - - if found == nil || found.Name != "test-repo" { - t.Errorf("期望找到 'test-repo'") - } -} - -func TestFindRepository_NotFound(t *testing.T) { - _, cleanup := setupRepoTestEnv(t) - defer cleanup() - - // FindRepository 实现中,找不到时返回 nil, nil - found, err := FindRepository("nonexistent") - if err != nil { - t.Errorf("FindRepository 不应该报错: %v", err) - } - if found != nil { - t.Errorf("期望返回 nil") - } -} diff --git a/manager/internal/installer/installer.go b/manager/internal/installer/installer.go deleted file mode 100644 index c8c006f..0000000 --- a/manager/internal/installer/installer.go +++ /dev/null @@ -1,304 +0,0 @@ -package installer - -import ( - "fmt" - "os" - "path/filepath" - "time" - - "skillmgr/internal/adapter" - "skillmgr/internal/config" - "skillmgr/internal/prompt" - "skillmgr/internal/repo" - "skillmgr/internal/types" -) - -// InstallSkill 安装 skill -func InstallSkill(name string, platform types.Platform, scope types.Scope) error { - return InstallSkillFrom(name, platform, scope, "") -} - -// InstallSkillFrom 从指定源安装 skill -// fromURL 为空时从已配置的仓库安装,否则从临时仓库安装 -func InstallSkillFrom(name string, platform types.Platform, scope types.Scope, fromURL string) error { - var skillPath string - var repoName string - var cleanup func() - - if fromURL != "" { - // 从临时仓库安装 - tmpRepo, cleanupFunc, err := repo.CloneTemporary(fromURL, "") - if err != nil { - return fmt.Errorf("克隆临时仓库失败: %w", err) - } - cleanup = cleanupFunc - defer cleanup() - - // 检查 skill 是否存在 - sp := filepath.Join(tmpRepo, "skills", name) - if _, err := os.Stat(filepath.Join(sp, "SKILL.md")); err != nil { - return fmt.Errorf("skill '%s' 未在临时仓库中找到", name) - } - skillPath = sp - repoName = "(临时)" - } else { - // 从已配置的仓库安装 - _, sp, rn, err := repo.FindSkill(name) - if err != nil { - // 列出可用的 skills - skills, _ := repo.ListAvailableSkills() - if len(skills) > 0 { - fmt.Println("\n可用的 skills:") - for _, s := range skills { - fmt.Printf(" - %s (from %s)\n", s.Name, s.SourceRepo) - } - } - return err - } - skillPath = sp - repoName = rn - } - - // 2. 获取适配器 - adp, err := adapter.GetAdapter(platform) - if err != nil { - return err - } - - // 3. 确定安装路径 - installPath, err := adp.GetSkillInstallPath(scope, name) - if err != nil { - return err - } - - // 4. 检查是否已存在 - if err := checkExistingInstallation(types.ItemTypeSkill, name, platform, scope, installPath); err != nil { - return err - } - - // 5. 适配文件映射 - fileMap, err := adp.AdaptSkill(skillPath, installPath) - if err != nil { - return err - } - - // 6. 事务性安装 - tx, err := NewTransaction(installPath, fileMap) - if err != nil { - return err - } - defer tx.Rollback() // 确保失败时清理 - - if err := tx.Stage(); err != nil { - return fmt.Errorf("staging 失败: %w", err) - } - - if err := tx.Commit(); err != nil { - return fmt.Errorf("commit 失败: %w", err) - } - - // 7. 记录安装 - record := types.InstallRecord{ - Type: types.ItemTypeSkill, - Name: name, - SourceRepo: repoName, - Platform: platform, - Scope: scope, - InstallPath: installPath, - InstalledAt: time.Now(), - UpdatedAt: time.Now(), - } - - // 先移除旧记录(如果存在) - config.RemoveInstallRecord(types.ItemTypeSkill, name, platform, scope) - - if err := config.AddInstallRecord(record); err != nil { - return fmt.Errorf("保存安装记录失败: %w", err) - } - - fmt.Printf("✓ Skill '%s' 已安装到 %s\n", name, installPath) - return nil -} - -// InstallCommand 安装 command -func InstallCommand(name string, platform types.Platform, scope types.Scope) error { - return InstallCommandFrom(name, platform, scope, "") -} - -// InstallCommandFrom 从指定源安装 command -// fromURL 为空时从已配置的仓库安装,否则从临时仓库安装 -func InstallCommandFrom(name string, platform types.Platform, scope types.Scope, fromURL string) error { - var commandPath string - var repoName string - var cleanup func() - - if fromURL != "" { - // 从临时仓库安装 - tmpRepo, cleanupFunc, err := repo.CloneTemporary(fromURL, "") - if err != nil { - return fmt.Errorf("克隆临时仓库失败: %w", err) - } - cleanup = cleanupFunc - defer cleanup() - - // 检查 command 是否存在 - cp := filepath.Join(tmpRepo, "commands", name) - if info, err := os.Stat(cp); err != nil || !info.IsDir() { - return fmt.Errorf("command '%s' 未在临时仓库中找到", name) - } - - // 检查是否包含 .md 文件 - files, _ := filepath.Glob(filepath.Join(cp, "*.md")) - if len(files) == 0 { - return fmt.Errorf("command group '%s' 不包含任何命令文件", name) - } - - commandPath = cp - repoName = "(临时)" - } else { - // 从已配置的仓库安装 - _, cp, rn, err := repo.FindCommand(name) - if err != nil { - // 列出可用的 commands - commands, _ := repo.ListAvailableCommands() - if len(commands) > 0 { - fmt.Println("\n可用的 commands:") - for _, c := range commands { - fmt.Printf(" - %s (from %s)\n", c.Name, c.SourceRepo) - } - } - return err - } - commandPath = cp - repoName = rn - } - - // 2. 获取适配器 - adp, err := adapter.GetAdapter(platform) - if err != nil { - return err - } - - // 3. 确定安装路径 - installPath, err := adp.GetCommandInstallPath(scope, name) - if err != nil { - return err - } - - // 4. 检查是否已存在 - if err := checkExistingInstallation(types.ItemTypeCommand, name, platform, scope, installPath); err != nil { - return err - } - - // 5. 适配文件映射 - fileMap, err := adp.AdaptCommand(commandPath, installPath, name) - if err != nil { - return err - } - - // 6. 事务性安装 - tx, err := NewTransaction(installPath, fileMap) - if err != nil { - return err - } - defer tx.Rollback() - - if err := tx.Stage(); err != nil { - return fmt.Errorf("staging 失败: %w", err) - } - - if err := tx.Commit(); err != nil { - return fmt.Errorf("commit 失败: %w", err) - } - - // 7. 记录安装 - record := types.InstallRecord{ - Type: types.ItemTypeCommand, - Name: name, - SourceRepo: repoName, - Platform: platform, - Scope: scope, - InstallPath: installPath, - InstalledAt: time.Now(), - UpdatedAt: time.Now(), - } - - // 先移除旧记录(如果存在) - config.RemoveInstallRecord(types.ItemTypeCommand, name, platform, scope) - - if err := config.AddInstallRecord(record); err != nil { - return fmt.Errorf("保存安装记录失败: %w", err) - } - - fmt.Printf("✓ Command '%s' 已安装到 %s\n", name, installPath) - return nil -} - -// checkExistingInstallation 检查现有安装 -func checkExistingInstallation(itemType types.ItemType, name string, platform types.Platform, scope types.Scope, installPath string) error { - // 检查 install.json 中是否有记录 - record, err := config.FindInstallRecord(itemType, name, platform, scope) - if err != nil { - return err - } - - // 检查目录是否实际存在 - _, dirErr := os.Stat(installPath) - dirExists := dirErr == nil - - if record != nil && dirExists { - // 已安装,询问是否覆盖 - if !prompt.Confirm(fmt.Sprintf("%s '%s' 已安装。是否覆盖?", itemType, name)) { - return fmt.Errorf("用户取消安装") - } - } else if record == nil && dirExists { - // 目录存在但没有记录,询问是否覆盖 - if !prompt.Confirm(fmt.Sprintf("目录 %s 已存在但不是由 skillmgr 管理。是否覆盖?", installPath)) { - return fmt.Errorf("用户取消安装") - } - } - - return nil -} - -// UpdateSkill 更新 skill -func UpdateSkill(name string, platform types.Platform, scope types.Scope) error { - // 查找记录 - record, err := config.FindInstallRecord(types.ItemTypeSkill, name, platform, scope) - if err != nil { - return err - } - if record == nil { - return fmt.Errorf("未找到 skill '%s' 的安装记录", name) - } - - // 重新安装 - if err := InstallSkill(name, platform, scope); err != nil { - return err - } - - // 更新记录时间 - record.UpdatedAt = time.Now() - return config.UpdateInstallRecord(*record) -} - -// UpdateCommand 更新 command -func UpdateCommand(name string, platform types.Platform, scope types.Scope) error { - // 查找记录 - record, err := config.FindInstallRecord(types.ItemTypeCommand, name, platform, scope) - if err != nil { - return err - } - if record == nil { - return fmt.Errorf("未找到 command '%s' 的安装记录", name) - } - - // 重新安装 - if err := InstallCommand(name, platform, scope); err != nil { - return err - } - - // 更新记录时间 - record.UpdatedAt = time.Now() - return config.UpdateInstallRecord(*record) -} diff --git a/manager/internal/installer/installer_test.go b/manager/internal/installer/installer_test.go deleted file mode 100644 index c949f0f..0000000 --- a/manager/internal/installer/installer_test.go +++ /dev/null @@ -1,856 +0,0 @@ -package installer - -import ( - "os" - "path/filepath" - "strings" - "testing" - "time" - - "skillmgr/internal/config" - "skillmgr/internal/testutil" - "skillmgr/internal/types" -) - -// setupIntegrationTest 设置集成测试环境 -// 返回临时目录、仓库路径、清理函数 -func setupIntegrationTest(t *testing.T) (tmpDir string, repoPath string, cleanup func()) { - t.Helper() - - tmpDir, cleanupEnv := testutil.SetupTestEnv(t) - - // 确保配置目录存在 - if err := config.EnsureConfigDirs(); err != nil { - t.Fatalf("创建配置目录失败: %v", err) - } - - // 获取 fixture 路径 - fixturePath := testutil.GetFixturePath(t) - fixtureRepo := filepath.Join(fixturePath, "test-repo") - - // 获取缓存路径 - cachePath, err := config.GetCachePath() - if err != nil { - t.Fatalf("获取缓存路径失败: %v", err) - } - - // 使用与 URLToPathName 一致的路径格式 - // URL: file://localhost/test-repo -> URLToPathName: file:__localhost_test-repo - repoURL := "file://localhost/test-repo" - repoDirName := "file:__localhost_test-repo" - repoPath = filepath.Join(cachePath, repoDirName) - - // 复制 fixture 到正确的缓存目录 - if err := os.MkdirAll(repoPath, 0755); err != nil { - t.Fatalf("创建仓库目录失败: %v", err) - } - - // 复制 skills 和 commands 目录 - srcSkills := filepath.Join(fixtureRepo, "skills") - dstSkills := filepath.Join(repoPath, "skills") - if err := copyDir(srcSkills, dstSkills); err != nil { - t.Fatalf("复制 skills 失败: %v", err) - } - - srcCommands := filepath.Join(fixtureRepo, "commands") - dstCommands := filepath.Join(repoPath, "commands") - if err := copyDir(srcCommands, dstCommands); err != nil { - t.Fatalf("复制 commands 失败: %v", err) - } - - // 添加仓库配置 - repo := types.Repository{ - Name: "test-repo", - URL: repoURL, - Branch: "main", - AddedAt: time.Now(), - } - if err := config.AddRepository(repo); err != nil { - t.Fatalf("添加仓库失败: %v", err) - } - - cleanup = func() { - cleanupEnv() - } - - return tmpDir, repoPath, cleanup -} - -// copyDir 递归复制目录(测试辅助函数) -func copyDir(src, dst string) error { - return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - relPath, err := filepath.Rel(src, path) - if err != nil { - return err - } - - dstPath := filepath.Join(dst, relPath) - - if info.IsDir() { - return os.MkdirAll(dstPath, info.Mode()) - } - - data, err := os.ReadFile(path) - if err != nil { - return err - } - - return os.WriteFile(dstPath, data, info.Mode()) - }) -} - -// ============================================================ -// 18.2 测试完整安装流程 -// ============================================================ - -func TestInstallSkill_CompleteFlow(t *testing.T) { - tmpDir, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 安装 skill 到 Claude 平台 - err := InstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("安装 skill 失败: %v", err) - } - - // 验证文件存在 - installPath := filepath.Join(tmpDir, ".claude", "skills", "test-skill") - if _, err := os.Stat(installPath); os.IsNotExist(err) { - t.Errorf("安装目录不存在: %s", installPath) - } - - skillFile := filepath.Join(installPath, "SKILL.md") - if _, err := os.Stat(skillFile); os.IsNotExist(err) { - t.Errorf("SKILL.md 文件不存在") - } - - // 验证安装记录 - record, err := config.FindInstallRecord(types.ItemTypeSkill, "test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("查找安装记录失败: %v", err) - } - if record == nil { - t.Error("安装记录不存在") - } else { - if record.InstallPath != installPath { - t.Errorf("安装路径不匹配: got %s, want %s", record.InstallPath, installPath) - } - if record.SourceRepo != "test-repo" { - t.Errorf("源仓库不匹配: got %s, want test-repo", record.SourceRepo) - } - } -} - -func TestInstallCommand_CompleteFlow(t *testing.T) { - tmpDir, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 安装 command 到 Claude 平台 - err := InstallCommand("test-cmd", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("安装 command 失败: %v", err) - } - - // 验证文件存在 - installPath := filepath.Join(tmpDir, ".claude", "commands", "test-cmd") - if _, err := os.Stat(installPath); os.IsNotExist(err) { - t.Errorf("安装目录不存在: %s", installPath) - } - - // 验证命令文件 - initFile := filepath.Join(installPath, "init.md") - runFile := filepath.Join(installPath, "run.md") - if _, err := os.Stat(initFile); os.IsNotExist(err) { - t.Errorf("init.md 文件不存在") - } - if _, err := os.Stat(runFile); os.IsNotExist(err) { - t.Errorf("run.md 文件不存在") - } - - // 验证安装记录 - record, err := config.FindInstallRecord(types.ItemTypeCommand, "test-cmd", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("查找安装记录失败: %v", err) - } - if record == nil { - t.Error("安装记录不存在") - } -} - -// ============================================================ -// 18.3 测试冲突覆盖场景 -// ============================================================ - -func TestInstallSkill_ConflictWithRecord(t *testing.T) { - tmpDir, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 首次安装 - err := InstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("首次安装失败: %v", err) - } - - // 记录首次安装时间 - record1, _ := config.FindInstallRecord(types.ItemTypeSkill, "test-skill", types.PlatformClaude, types.ScopeGlobal) - firstInstallTime := record1.InstalledAt - - // 完全卸载后重新安装(测试正常覆盖流程) - err = UninstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("卸载失败: %v", err) - } - - // 等待一小段时间确保时间戳不同 - time.Sleep(10 * time.Millisecond) - - // 再次安装 - err = InstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("重新安装失败: %v", err) - } - - // 验证记录已更新 - record2, _ := config.FindInstallRecord(types.ItemTypeSkill, "test-skill", types.PlatformClaude, types.ScopeGlobal) - if record2 == nil { - t.Fatal("安装记录丢失") - } - - // 验证安装时间更新 - if !record2.InstalledAt.After(firstInstallTime) { - t.Error("重新安装的时间应该晚于首次安装") - } - - // 验证文件仍然存在 - installPath := filepath.Join(tmpDir, ".claude", "skills", "test-skill") - if _, err := os.Stat(installPath); os.IsNotExist(err) { - t.Errorf("安装目录应该存在") - } -} - -func TestInstallSkill_ConflictWithoutRecord(t *testing.T) { - tmpDir, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 手动创建目标目录(模拟非 skillmgr 管理的目录) - installPath := filepath.Join(tmpDir, ".claude", "skills", "test-skill") - os.MkdirAll(installPath, 0755) - os.WriteFile(filepath.Join(installPath, "existing.txt"), []byte("existing file"), 0644) - - // 验证目录存在 - if _, err := os.Stat(installPath); os.IsNotExist(err) { - t.Fatal("预创建的目录应该存在") - } - - // 由于 prompt.Confirm 会读取 stdin,在测试中会导致用户取消 - // 所以我们测试的是:目录存在时,安装会请求确认(失败说明确认机制工作) - err := InstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - - // 在非交互测试环境中,用户取消是预期行为 - if err == nil { - // 如果成功了,说明没有检测到冲突(不应该发生) - t.Log("注意: 安装成功,可能是因为冲突检测没有触发确认") - } else if !strings.Contains(err.Error(), "用户取消") { - // 如果是其他错误,记录但不失败(冲突检测机制正常工作) - t.Logf("冲突检测正常工作,用户取消安装: %v", err) - } -} - -// ============================================================ -// 18.4 测试事务回滚 -// ============================================================ - -func TestTransaction_RollbackOnStagingFailure(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "skillmgr-rollback-test-*") - if err != nil { - t.Fatalf("创建临时目录失败: %v", err) - } - defer os.RemoveAll(tmpDir) - - // 创建指向不存在文件的映射(会导致 Stage 失败) - targetDir := filepath.Join(tmpDir, "target") - fileMap := map[string]string{ - "/nonexistent/path/file.md": filepath.Join(targetDir, "file.md"), - } - - tx, err := NewTransaction(targetDir, fileMap) - if err != nil { - t.Fatalf("NewTransaction 失败: %v", err) - } - - stagingDir := tx.stagingDir - - // Stage 应该失败 - err = tx.Stage() - if err == nil { - t.Error("Stage 应该失败(源文件不存在)") - } - - // 调用 Rollback - tx.Rollback() - - // 验证 staging 目录已清理 - if _, err := os.Stat(stagingDir); !os.IsNotExist(err) { - t.Error("Staging 目录应该被清理") - } - - // 验证目标目录不存在 - if _, err := os.Stat(targetDir); !os.IsNotExist(err) { - t.Error("目标目录不应该存在") - } -} - -func TestTransaction_DeferredRollback(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "skillmgr-defer-test-*") - if err != nil { - t.Fatalf("创建临时目录失败: %v", err) - } - defer os.RemoveAll(tmpDir) - - // 创建源文件 - srcDir := filepath.Join(tmpDir, "src") - os.MkdirAll(srcDir, 0755) - os.WriteFile(filepath.Join(srcDir, "test.md"), []byte("test"), 0644) - - targetDir := filepath.Join(tmpDir, "target") - fileMap := map[string]string{ - filepath.Join(srcDir, "test.md"): filepath.Join(targetDir, "test.md"), - } - - var stagingDir string - - // 在函数内使用 defer tx.Rollback() 模拟安装函数 - func() { - tx, err := NewTransaction(targetDir, fileMap) - if err != nil { - t.Fatalf("NewTransaction 失败: %v", err) - } - defer tx.Rollback() // 确保清理 - - stagingDir = tx.stagingDir - - if err := tx.Stage(); err != nil { - t.Fatalf("Stage 失败: %v", err) - } - - // 不调用 Commit,模拟中途失败 - // defer 会触发 Rollback - }() - - // 验证 staging 目录已被 defer 清理 - if _, err := os.Stat(stagingDir); !os.IsNotExist(err) { - t.Error("Staging 目录应该被 defer 清理") - } -} - -// ============================================================ -// 18.5 测试卸载流程 -// ============================================================ - -func TestUninstallSkill_CompleteFlow(t *testing.T) { - tmpDir, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 先安装 - err := InstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("安装失败: %v", err) - } - - installPath := filepath.Join(tmpDir, ".claude", "skills", "test-skill") - - // 验证安装成功 - if _, err := os.Stat(installPath); os.IsNotExist(err) { - t.Fatal("安装目录应该存在") - } - - // 卸载 - err = UninstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("卸载失败: %v", err) - } - - // 验证目录已删除 - if _, err := os.Stat(installPath); !os.IsNotExist(err) { - t.Error("安装目录应该被删除") - } - - // 验证记录已移除 - record, err := config.FindInstallRecord(types.ItemTypeSkill, "test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("查找记录失败: %v", err) - } - if record != nil { - t.Error("安装记录应该被移除") - } -} - -func TestUninstallCommand_CompleteFlow(t *testing.T) { - tmpDir, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 先安装 - err := InstallCommand("test-cmd", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("安装失败: %v", err) - } - - installPath := filepath.Join(tmpDir, ".claude", "commands", "test-cmd") - - // 卸载 - err = UninstallCommand("test-cmd", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("卸载失败: %v", err) - } - - // 验证目录已删除 - if _, err := os.Stat(installPath); !os.IsNotExist(err) { - t.Error("安装目录应该被删除") - } - - // 验证记录已移除 - record, err := config.FindInstallRecord(types.ItemTypeCommand, "test-cmd", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("查找记录失败: %v", err) - } - if record != nil { - t.Error("安装记录应该被移除") - } -} - -func TestUninstallSkill_NotFound(t *testing.T) { - _, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 尝试卸载不存在的 skill - err := UninstallSkill("nonexistent", types.PlatformClaude, types.ScopeGlobal) - if err == nil { - t.Error("卸载不存在的 skill 应该报错") - } - if !strings.Contains(err.Error(), "未找到") { - t.Errorf("错误信息应该包含 '未找到': %v", err) - } -} - -func TestUninstallSkill_FilesAlreadyDeleted(t *testing.T) { - tmpDir, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 安装 - err := InstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("安装失败: %v", err) - } - - // 手动删除文件(模拟用户手动删除) - installPath := filepath.Join(tmpDir, ".claude", "skills", "test-skill") - os.RemoveAll(installPath) - - // 卸载应该成功(仅移除记录) - err = UninstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("卸载失败(文件已手动删除): %v", err) - } - - // 验证记录已移除 - record, _ := config.FindInstallRecord(types.ItemTypeSkill, "test-skill", types.PlatformClaude, types.ScopeGlobal) - if record != nil { - t.Error("安装记录应该被移除") - } -} - -// ============================================================ -// 18.6 测试更新流程 -// ============================================================ - -func TestUpdateSkill_CompleteFlow(t *testing.T) { - tmpDir, repoPath, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 安装 - err := InstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("安装失败: %v", err) - } - - // 记录初始内容 - installPath := filepath.Join(tmpDir, ".claude", "skills", "test-skill", "SKILL.md") - initialContent, err := os.ReadFile(installPath) - if err != nil { - t.Fatalf("读取初始文件失败: %v", err) - } - - // 修改源文件 - sourceFile := filepath.Join(repoPath, "skills", "test-skill", "SKILL.md") - newContent := "# Updated content\n\nThis is updated.\n" - os.WriteFile(sourceFile, []byte(newContent), 0644) - - // 卸载后重新安装(模拟更新,避免 prompt) - err = UninstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("卸载失败: %v", err) - } - - err = InstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("重新安装失败: %v", err) - } - - // 验证文件内容已更新 - updatedContent, err := os.ReadFile(installPath) - if err != nil { - t.Fatalf("读取更新后文件失败: %v", err) - } - - if string(updatedContent) == string(initialContent) { - t.Error("安装文件内容应该已更新") - } - if !strings.Contains(string(updatedContent), "Updated content") { - t.Error("安装文件应该包含更新的内容") - } -} - -func TestUpdateSkill_NotInstalled(t *testing.T) { - _, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 尝试更新未安装的 skill - err := UpdateSkill("nonexistent", types.PlatformClaude, types.ScopeGlobal) - if err == nil { - t.Error("更新未安装的 skill 应该报错") - } - if !strings.Contains(err.Error(), "未找到") { - t.Errorf("错误信息应该包含 '未找到': %v", err) - } -} - -// ============================================================ -// 18.7 测试清理孤立记录 -// ============================================================ - -func TestCleanOrphanRecords(t *testing.T) { - tmpDir, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 安装 - err := InstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("安装失败: %v", err) - } - - // 手动删除安装目录 - installPath := filepath.Join(tmpDir, ".claude", "skills", "test-skill") - os.RemoveAll(installPath) - - // 验证记录仍存在 - record, _ := config.FindInstallRecord(types.ItemTypeSkill, "test-skill", types.PlatformClaude, types.ScopeGlobal) - if record == nil { - t.Fatal("删除文件后记录应该仍存在") - } - - // 清理孤立记录 - cleaned, err := config.CleanOrphanRecords() - if err != nil { - t.Fatalf("清理孤立记录失败: %v", err) - } - - // 验证清理了正确的记录 - if len(cleaned) != 1 { - t.Errorf("应该清理 1 个记录,实际清理了 %d 个", len(cleaned)) - } - if len(cleaned) > 0 && cleaned[0].Name != "test-skill" { - t.Errorf("清理的记录名称不匹配: got %s, want test-skill", cleaned[0].Name) - } - - // 验证记录已被移除 - record, _ = config.FindInstallRecord(types.ItemTypeSkill, "test-skill", types.PlatformClaude, types.ScopeGlobal) - if record != nil { - t.Error("孤立记录应该被清理") - } -} - -func TestCleanOrphanRecords_NoOrphans(t *testing.T) { - _, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 安装并保持文件存在 - err := InstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("安装失败: %v", err) - } - - // 清理(应该没有孤立记录) - cleaned, err := config.CleanOrphanRecords() - if err != nil { - t.Fatalf("清理孤立记录失败: %v", err) - } - - if len(cleaned) != 0 { - t.Errorf("不应该有孤立记录被清理,实际清理了 %d 个", len(cleaned)) - } - - // 验证记录仍存在 - record, _ := config.FindInstallRecord(types.ItemTypeSkill, "test-skill", types.PlatformClaude, types.ScopeGlobal) - if record == nil { - t.Error("记录不应该被清理") - } -} - -// ============================================================ -// 18.8 测试 Claude Code 平台安装 -// ============================================================ - -func TestInstall_ClaudePlatform_Skill(t *testing.T) { - tmpDir, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 测试全局安装 - err := InstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("全局安装失败: %v", err) - } - - globalPath := filepath.Join(tmpDir, ".claude", "skills", "test-skill") - if _, err := os.Stat(globalPath); os.IsNotExist(err) { - t.Errorf("全局安装路径不正确: %s", globalPath) - } - - // 清理后测试项目级安装 - UninstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - - err = InstallSkill("test-skill", types.PlatformClaude, types.ScopeProject) - if err != nil { - t.Fatalf("项目级安装失败: %v", err) - } - - projectPath := filepath.Join(tmpDir, ".claude", "skills", "test-skill") - if _, err := os.Stat(projectPath); os.IsNotExist(err) { - t.Errorf("项目级安装路径不正确: %s", projectPath) - } -} - -func TestInstall_ClaudePlatform_Command(t *testing.T) { - tmpDir, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - err := InstallCommand("test-cmd", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("安装失败: %v", err) - } - - // 验证目录结构保持不变 - cmdPath := filepath.Join(tmpDir, ".claude", "commands", "test-cmd") - if _, err := os.Stat(cmdPath); os.IsNotExist(err) { - t.Errorf("命令组目录不存在: %s", cmdPath) - } - - // 验证原始文件名保持不变 - if _, err := os.Stat(filepath.Join(cmdPath, "init.md")); os.IsNotExist(err) { - t.Error("init.md 应该存在(保持原始文件名)") - } - if _, err := os.Stat(filepath.Join(cmdPath, "run.md")); os.IsNotExist(err) { - t.Error("run.md 应该存在(保持原始文件名)") - } -} - -// ============================================================ -// 18.9 测试 OpenCode 平台安装 -// ============================================================ - -func TestInstall_OpenCodePlatform_Skill(t *testing.T) { - tmpDir, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 测试全局安装 - err := InstallSkill("test-skill", types.PlatformOpenCode, types.ScopeGlobal) - if err != nil { - t.Fatalf("全局安装失败: %v", err) - } - - globalPath := filepath.Join(tmpDir, ".config", "opencode", "skills", "test-skill") - if _, err := os.Stat(globalPath); os.IsNotExist(err) { - t.Errorf("全局安装路径不正确: %s", globalPath) - } - - // 清理后测试项目级安装 - UninstallSkill("test-skill", types.PlatformOpenCode, types.ScopeGlobal) - - err = InstallSkill("test-skill", types.PlatformOpenCode, types.ScopeProject) - if err != nil { - t.Fatalf("项目级安装失败: %v", err) - } - - projectPath := filepath.Join(tmpDir, ".opencode", "skills", "test-skill") - if _, err := os.Stat(projectPath); os.IsNotExist(err) { - t.Errorf("项目级安装路径不正确: %s", projectPath) - } -} - -func TestInstall_OpenCodePlatform_Command_Flattening(t *testing.T) { - tmpDir, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 全局安装 - err := InstallCommand("test-cmd", types.PlatformOpenCode, types.ScopeGlobal) - if err != nil { - t.Fatalf("安装失败: %v", err) - } - - // 验证扁平化路径 - cmdPath := filepath.Join(tmpDir, ".config", "opencode", "commands") - if _, err := os.Stat(cmdPath); os.IsNotExist(err) { - t.Fatalf("命令目录不存在: %s", cmdPath) - } - - // 验证文件名已扁平化: -.md - flattenedInit := filepath.Join(cmdPath, "test-cmd-init.md") - flattenedRun := filepath.Join(cmdPath, "test-cmd-run.md") - - if _, err := os.Stat(flattenedInit); os.IsNotExist(err) { - t.Errorf("扁平化文件 test-cmd-init.md 不存在") - } - if _, err := os.Stat(flattenedRun); os.IsNotExist(err) { - t.Errorf("扁平化文件 test-cmd-run.md 不存在") - } - - // 验证原始文件名不存在 - if _, err := os.Stat(filepath.Join(cmdPath, "init.md")); !os.IsNotExist(err) { - t.Error("原始文件名 init.md 不应该存在") - } -} - -func TestInstall_OpenCodePlatform_Command_ProjectScope(t *testing.T) { - tmpDir, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 项目级安装 - err := InstallCommand("test-cmd", types.PlatformOpenCode, types.ScopeProject) - if err != nil { - t.Fatalf("安装失败: %v", err) - } - - // 验证项目级路径 - cmdPath := filepath.Join(tmpDir, ".opencode", "commands") - if _, err := os.Stat(cmdPath); os.IsNotExist(err) { - t.Fatalf("命令目录不存在: %s", cmdPath) - } - - // 验证扁平化 - flattenedInit := filepath.Join(cmdPath, "test-cmd-init.md") - if _, err := os.Stat(flattenedInit); os.IsNotExist(err) { - t.Errorf("扁平化文件 test-cmd-init.md 不存在") - } -} - -// ============================================================ -// 额外测试:多 skill 安装和边界情况 -// ============================================================ - -func TestInstallMultipleSkills(t *testing.T) { - tmpDir, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - // 安装两个 skill - err := InstallSkill("test-skill", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("安装 test-skill 失败: %v", err) - } - - err = InstallSkill("test-skill-2", types.PlatformClaude, types.ScopeGlobal) - if err != nil { - t.Fatalf("安装 test-skill-2 失败: %v", err) - } - - // 验证两个都存在 - skill1 := filepath.Join(tmpDir, ".claude", "skills", "test-skill") - skill2 := filepath.Join(tmpDir, ".claude", "skills", "test-skill-2") - - if _, err := os.Stat(skill1); os.IsNotExist(err) { - t.Error("test-skill 应该存在") - } - if _, err := os.Stat(skill2); os.IsNotExist(err) { - t.Error("test-skill-2 应该存在") - } - - // 验证两个记录都存在 - cfg, _ := config.LoadInstallConfig() - if len(cfg.Installations) != 2 { - t.Errorf("应该有 2 个安装记录,实际有 %d 个", len(cfg.Installations)) - } -} - -func TestInstallSkill_NotFound(t *testing.T) { - _, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - err := InstallSkill("nonexistent-skill", types.PlatformClaude, types.ScopeGlobal) - if err == nil { - t.Error("安装不存在的 skill 应该失败") - } - if !strings.Contains(err.Error(), "未在任何仓库中找到") { - t.Errorf("错误信息应该包含 '未在任何仓库中找到': %v", err) - } -} - -func TestInstallCommand_NotFound(t *testing.T) { - _, _, cleanup := setupIntegrationTest(t) - defer cleanup() - - err := InstallCommand("nonexistent-cmd", types.PlatformClaude, types.ScopeGlobal) - if err == nil { - t.Error("安装不存在的 command 应该失败") - } - if !strings.Contains(err.Error(), "未在任何仓库中找到") { - t.Errorf("错误信息应该包含 '未在任何仓库中找到': %v", err) - } -} - -func TestStagingIntegrityVerification(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "skillmgr-integrity-test-*") - if err != nil { - t.Fatalf("创建临时目录失败: %v", err) - } - defer os.RemoveAll(tmpDir) - - // 创建多个源文件 - srcDir := filepath.Join(tmpDir, "src") - os.MkdirAll(srcDir, 0755) - os.WriteFile(filepath.Join(srcDir, "file1.md"), []byte("content1"), 0644) - os.WriteFile(filepath.Join(srcDir, "file2.md"), []byte("content2"), 0644) - os.WriteFile(filepath.Join(srcDir, "file3.md"), []byte("content3"), 0644) - - targetDir := filepath.Join(tmpDir, "target") - fileMap := map[string]string{ - filepath.Join(srcDir, "file1.md"): filepath.Join(targetDir, "file1.md"), - filepath.Join(srcDir, "file2.md"): filepath.Join(targetDir, "file2.md"), - filepath.Join(srcDir, "file3.md"): filepath.Join(targetDir, "file3.md"), - } - - tx, err := NewTransaction(targetDir, fileMap) - if err != nil { - t.Fatalf("NewTransaction 失败: %v", err) - } - defer tx.Rollback() - - // Stage 应该成功并验证完整性 - if err := tx.Stage(); err != nil { - t.Fatalf("Stage 失败: %v", err) - } - - // 手动验证 staging 目录中有 3 个文件 - count := 0 - filepath.Walk(tx.stagingDir, func(path string, info os.FileInfo, err error) error { - if !info.IsDir() { - count++ - } - return nil - }) - - if count != 3 { - t.Errorf("Staging 目录应该有 3 个文件,实际有 %d 个", count) - } -} diff --git a/manager/internal/installer/transaction.go b/manager/internal/installer/transaction.go deleted file mode 100644 index 407e08e..0000000 --- a/manager/internal/installer/transaction.go +++ /dev/null @@ -1,134 +0,0 @@ -package installer - -import ( - "fmt" - "os" - "path/filepath" - - "skillmgr/pkg/fileutil" -) - -// Transaction 事务性安装 -type Transaction struct { - stagingDir string - targetDir string - fileMap map[string]string // source → dest -} - -// NewTransaction 创建事务 -// 在系统临时目录创建 staging 目录 -func NewTransaction(targetDir string, fileMap map[string]string) (*Transaction, error) { - // 在系统临时目录创建 staging 目录 - stagingDir, err := os.MkdirTemp("", "skillmgr-*") - if err != nil { - return nil, fmt.Errorf("创建 staging 目录失败: %w", err) - } - - return &Transaction{ - stagingDir: stagingDir, - targetDir: targetDir, - fileMap: fileMap, - }, nil -} - -// Stage 阶段:复制文件到 staging 目录 -func (t *Transaction) Stage() error { - stagedCount := 0 - - for src, dest := range t.fileMap { - // 计算相对于 targetDir 的路径 - relPath, err := filepath.Rel(t.targetDir, dest) - if err != nil { - return fmt.Errorf("计算相对路径失败: %w", err) - } - - stagingDest := filepath.Join(t.stagingDir, relPath) - - // 确保目标目录存在 - if err := os.MkdirAll(filepath.Dir(stagingDest), 0755); err != nil { - return fmt.Errorf("创建 staging 子目录失败: %w", err) - } - - // 复制文件 - if err := fileutil.CopyFile(src, stagingDest); err != nil { - return fmt.Errorf("复制文件到 staging 失败: %w", err) - } - stagedCount++ - } - - // 验证 staging 完整性:检查文件数量是否与预期一致 - if err := t.verifyStagingIntegrity(stagedCount); err != nil { - return fmt.Errorf("staging 验证失败: %w", err) - } - - return nil -} - -// verifyStagingIntegrity 验证 staging 目录中的文件数量 -func (t *Transaction) verifyStagingIntegrity(expectedCount int) error { - actualCount := 0 - - err := filepath.Walk(t.stagingDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() { - actualCount++ - } - return nil - }) - - if err != nil { - return fmt.Errorf("遍历 staging 目录失败: %w", err) - } - - if actualCount != expectedCount { - return fmt.Errorf("文件数量不匹配: 预期 %d 个文件,实际 %d 个", expectedCount, actualCount) - } - - return nil -} - -// Commit 提交:将 staging 目录移动到目标位置 -func (t *Transaction) Commit() error { - // 确保目标目录的父目录存在 - if err := os.MkdirAll(filepath.Dir(t.targetDir), 0755); err != nil { - return fmt.Errorf("创建目标父目录失败: %w", err) - } - - // 如果目标目录已存在,先删除(已经过用户确认) - if _, err := os.Stat(t.targetDir); err == nil { - if err := os.RemoveAll(t.targetDir); err != nil { - return fmt.Errorf("删除已存在的目标目录失败: %w", err) - } - } - - // 尝试原子性移动 staging 目录到目标位置 - if err := os.Rename(t.stagingDir, t.targetDir); err != nil { - // 如果跨文件系统,Rename 会失败,改用复制 - // 使用 defer 确保 staging 目录被清理 - defer os.RemoveAll(t.stagingDir) - if err := fileutil.CopyDir(t.stagingDir, t.targetDir); err != nil { - return fmt.Errorf("复制 staging 到目标失败: %w", err) - } - } - - return nil -} - -// Rollback 回滚:清理 staging 目录 -func (t *Transaction) Rollback() { - if t.stagingDir != "" { - os.RemoveAll(t.stagingDir) - } -} - -// StagingDir 获取 staging 目录路径 -func (t *Transaction) StagingDir() string { - return t.stagingDir -} - -// TargetDir 获取目标目录路径 -func (t *Transaction) TargetDir() string { - return t.targetDir -} diff --git a/manager/internal/installer/transaction_test.go b/manager/internal/installer/transaction_test.go deleted file mode 100644 index 0d27ed9..0000000 --- a/manager/internal/installer/transaction_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package installer - -import ( - "os" - "path/filepath" - "testing" -) - -func TestTransaction_StageAndCommit(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "skillmgr-tx-test-*") - if err != nil { - t.Fatalf("创建临时目录失败: %v", err) - } - defer os.RemoveAll(tmpDir) - - // 创建源文件 - srcDir := filepath.Join(tmpDir, "src") - os.MkdirAll(srcDir, 0755) - os.WriteFile(filepath.Join(srcDir, "test.md"), []byte("test content"), 0644) - - // 创建文件映射 - targetDir := filepath.Join(tmpDir, "target") - fileMap := map[string]string{ - filepath.Join(srcDir, "test.md"): filepath.Join(targetDir, "test.md"), - } - - tx, err := NewTransaction(targetDir, fileMap) - if err != nil { - t.Fatalf("NewTransaction 失败: %v", err) - } - - // Stage - if err := tx.Stage(); err != nil { - t.Fatalf("Stage 失败: %v", err) - } - - // 验证 staging 目录存在 - if _, err := os.Stat(tx.stagingDir); os.IsNotExist(err) { - t.Error("Staging 目录应该存在") - } - - // Commit - if err := tx.Commit(); err != nil { - t.Fatalf("Commit 失败: %v", err) - } - - // 验证目标文件存在 - if _, err := os.Stat(filepath.Join(targetDir, "test.md")); os.IsNotExist(err) { - t.Error("目标文件应该存在") - } - - // 验证 staging 目录已清理 - if _, err := os.Stat(tx.stagingDir); !os.IsNotExist(err) { - t.Error("Staging 目录应该被清理") - } -} - -func TestTransaction_Rollback(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "skillmgr-tx-test-*") - if err != nil { - t.Fatalf("创建临时目录失败: %v", err) - } - defer os.RemoveAll(tmpDir) - - // 创建源文件 - srcDir := filepath.Join(tmpDir, "src") - os.MkdirAll(srcDir, 0755) - os.WriteFile(filepath.Join(srcDir, "test.md"), []byte("test content"), 0644) - - // 创建文件映射 - targetDir := filepath.Join(tmpDir, "target") - fileMap := map[string]string{ - filepath.Join(srcDir, "test.md"): filepath.Join(targetDir, "test.md"), - } - - tx, err := NewTransaction(targetDir, fileMap) - if err != nil { - t.Fatalf("NewTransaction 失败: %v", err) - } - - // Stage - if err := tx.Stage(); err != nil { - t.Fatalf("Stage 失败: %v", err) - } - - stagingDir := tx.stagingDir - - // Rollback (no return value) - tx.Rollback() - - // 验证 staging 目录已清理 - if _, err := os.Stat(stagingDir); !os.IsNotExist(err) { - t.Error("Staging 目录应该被清理") - } - - // 验证目标目录不存在 - if _, err := os.Stat(targetDir); !os.IsNotExist(err) { - t.Error("目标目录不应该存在") - } -} diff --git a/manager/internal/installer/uninstaller.go b/manager/internal/installer/uninstaller.go deleted file mode 100644 index ba91121..0000000 --- a/manager/internal/installer/uninstaller.go +++ /dev/null @@ -1,88 +0,0 @@ -package installer - -import ( - "fmt" - "os" - "path/filepath" - - "skillmgr/internal/config" - "skillmgr/internal/types" -) - -// UninstallSkill 卸载 skill -func UninstallSkill(name string, platform types.Platform, scope types.Scope) error { - // 查找记录 - record, err := config.FindInstallRecord(types.ItemTypeSkill, name, platform, scope) - if err != nil { - return err - } - if record == nil { - return fmt.Errorf("未找到 skill '%s' 的安装记录", name) - } - - // 删除目录 - if _, err := os.Stat(record.InstallPath); err == nil { - if err := os.RemoveAll(record.InstallPath); err != nil { - return fmt.Errorf("删除目录失败: %w", err) - } - } - - // 移除记录 - if err := config.RemoveInstallRecord(types.ItemTypeSkill, name, platform, scope); err != nil { - return fmt.Errorf("移除安装记录失败: %w", err) - } - - fmt.Printf("✓ Skill '%s' 已卸载\n", name) - return nil -} - -// UninstallCommand 卸载 command -func UninstallCommand(name string, platform types.Platform, scope types.Scope) error { - // 查找记录 - record, err := config.FindInstallRecord(types.ItemTypeCommand, name, platform, scope) - if err != nil { - return err - } - if record == nil { - return fmt.Errorf("未找到 command '%s' 的安装记录", name) - } - - // 根据平台决定删除策略 - if platform == types.PlatformClaude { - // Claude: 删除整个命令组目录 - if _, err := os.Stat(record.InstallPath); err == nil { - if err := os.RemoveAll(record.InstallPath); err != nil { - return fmt.Errorf("删除目录失败: %w", err) - } - } - } else if platform == types.PlatformOpenCode { - // OpenCode: 删除扁平化的命令文件 (-*.md) - // InstallPath 是 .opencode/command/ 目录 - // 需要删除所有 -*.md 文件 - entries, err := os.ReadDir(record.InstallPath) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("读取目录失败: %w", err) - } - for _, entry := range entries { - if !entry.IsDir() { - // 检查文件名是否以 - 开头 - fileName := entry.Name() - prefix := name + "-" - if len(fileName) > len(prefix) && fileName[:len(prefix)] == prefix { - filePath := filepath.Join(record.InstallPath, fileName) - if err := os.Remove(filePath); err != nil { - return fmt.Errorf("删除文件 %s 失败: %w", fileName, err) - } - } - } - } - } - - // 移除记录 - if err := config.RemoveInstallRecord(types.ItemTypeCommand, name, platform, scope); err != nil { - return fmt.Errorf("移除安装记录失败: %w", err) - } - - fmt.Printf("✓ Command '%s' 已卸载\n", name) - return nil -} diff --git a/manager/internal/prompt/prompt.go b/manager/internal/prompt/prompt.go deleted file mode 100644 index 7c13cfd..0000000 --- a/manager/internal/prompt/prompt.go +++ /dev/null @@ -1,39 +0,0 @@ -package prompt - -import ( - "bufio" - "fmt" - "io" - "os" - "strings" -) - -// ConfirmWithReader 询问用户确认(y/n),支持自定义输入源 -// 用于测试时注入 mock 输入 -func ConfirmWithReader(message string, reader io.Reader) bool { - r := bufio.NewReader(reader) - - for { - fmt.Printf("%s [y/N]: ", message) - response, err := r.ReadString('\n') - if err != nil { - return false - } - - response = strings.TrimSpace(strings.ToLower(response)) - - if response == "y" || response == "yes" { - return true - } else if response == "n" || response == "no" || response == "" { - return false - } - - fmt.Println("请输入 'y' 或 'n'") - } -} - -// Confirm 询问用户确认(y/n) -// 使用标准输入 -func Confirm(message string) bool { - return ConfirmWithReader(message, os.Stdin) -} diff --git a/manager/internal/prompt/prompt_test.go b/manager/internal/prompt/prompt_test.go deleted file mode 100644 index 521ed46..0000000 --- a/manager/internal/prompt/prompt_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package prompt - -import ( - "strings" - "testing" -) - -func TestConfirmWithReader_Yes(t *testing.T) { - tests := []string{"y", "Y", "yes", "YES", "Yes"} - - for _, input := range tests { - reader := strings.NewReader(input + "\n") - result := ConfirmWithReader("测试?", reader) - if !result { - t.Errorf("输入 '%s' 应返回 true", input) - } - } -} - -func TestConfirmWithReader_No(t *testing.T) { - tests := []string{"n", "N", "no", "NO", "No", "", "anything"} - - for _, input := range tests { - reader := strings.NewReader(input + "\n") - result := ConfirmWithReader("测试?", reader) - if result { - t.Errorf("输入 '%s' 应返回 false", input) - } - } -} diff --git a/manager/internal/repo/git.go b/manager/internal/repo/git.go deleted file mode 100644 index c7e8e35..0000000 --- a/manager/internal/repo/git.go +++ /dev/null @@ -1,109 +0,0 @@ -package repo - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "skillmgr/internal/config" -) - -// URLToPathName 将 URL 转换为缓存目录名 -// 例如: https://github.com/user/repo.git -> github.com_user_repo -func URLToPathName(url string) string { - clean := strings.TrimPrefix(url, "https://") - clean = strings.TrimPrefix(clean, "http://") - clean = strings.TrimSuffix(clean, ".git") - clean = strings.ReplaceAll(clean, "/", "_") - return clean -} - -// CloneOrPull 克隆或更新仓库 -// 如果仓库不存在则 clone,存在则 pull -func CloneOrPull(url, branch string) (string, error) { - cachePath, err := config.GetCachePath() - if err != nil { - return "", err - } - - repoPath := filepath.Join(cachePath, URLToPathName(url)) - - // 检查是否已存在 - if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil { - // 已存在,执行 pull - return repoPath, pullRepo(repoPath, branch) - } - - // 不存在,执行 clone - return repoPath, cloneRepo(url, branch, repoPath) -} - -// cloneRepo 克隆仓库 -func cloneRepo(url, branch, dest string) error { - args := []string{"clone", "--depth", "1"} - if branch != "" { - args = append(args, "--branch", branch) - } - args = append(args, url, dest) - - cmd := exec.Command("git", args...) - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("git clone 失败: %w\n%s", err, output) - } - return nil -} - -// pullRepo 更新仓库 -func pullRepo(path, branch string) error { - // 先 fetch - fetchCmd := exec.Command("git", "-C", path, "fetch", "origin") - if output, err := fetchCmd.CombinedOutput(); err != nil { - return fmt.Errorf("git fetch 失败: %w\n%s", err, output) - } - - // 然后 pull - pullArgs := []string{"-C", path, "pull", "origin"} - if branch != "" { - pullArgs = append(pullArgs, branch) - } - pullCmd := exec.Command("git", pullArgs...) - output, err := pullCmd.CombinedOutput() - if err != nil { - return fmt.Errorf("git pull 失败: %w\n%s", err, output) - } - return nil -} - -// GetRepoPath 获取仓库缓存路径 -func GetRepoPath(url string) (string, error) { - cachePath, err := config.GetCachePath() - if err != nil { - return "", err - } - return filepath.Join(cachePath, URLToPathName(url)), nil -} - -// CloneTemporary 克隆临时仓库到临时目录 -// 返回临时目录路径和清理函数 -func CloneTemporary(url, branch string) (repoPath string, cleanup func(), err error) { - // 创建临时目录 - tmpDir, err := os.MkdirTemp("", "skillmgr-temp-*") - if err != nil { - return "", nil, fmt.Errorf("创建临时目录失败: %w", err) - } - - cleanup = func() { - os.RemoveAll(tmpDir) - } - - // 克隆到临时目录 - if err := cloneRepo(url, branch, tmpDir); err != nil { - cleanup() - return "", nil, err - } - - return tmpDir, cleanup, nil -} diff --git a/manager/internal/repo/git_test.go b/manager/internal/repo/git_test.go deleted file mode 100644 index bc9db22..0000000 --- a/manager/internal/repo/git_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package repo - -import ( - "testing" -) - -func TestURLToPathName(t *testing.T) { - tests := []struct { - url string - expected string - }{ - {"https://github.com/user/repo.git", "github.com_user_repo"}, - {"https://github.com/user/repo", "github.com_user_repo"}, - {"http://gitlab.com/org/project.git", "gitlab.com_org_project"}, - {"https://github.com/user/my-repo.git", "github.com_user_my-repo"}, - } - - for _, tc := range tests { - result := URLToPathName(tc.url) - if result != tc.expected { - t.Errorf("URLToPathName(%s): 期望 %s,得到 %s", tc.url, tc.expected, result) - } - } -} diff --git a/manager/internal/repo/scanner.go b/manager/internal/repo/scanner.go deleted file mode 100644 index 3aa7804..0000000 --- a/manager/internal/repo/scanner.go +++ /dev/null @@ -1,191 +0,0 @@ -package repo - -import ( - "fmt" - "os" - "path/filepath" - - "skillmgr/internal/config" - "skillmgr/internal/types" -) - -// ScanSkills 扫描仓库中的 skills -func ScanSkills(repoPath string) ([]types.SkillMetadata, error) { - skillsPath := filepath.Join(repoPath, "skills") - - entries, err := os.ReadDir(skillsPath) - if err != nil { - if os.IsNotExist(err) { - return []types.SkillMetadata{}, nil - } - return nil, err - } - - var skills []types.SkillMetadata - for _, entry := range entries { - if !entry.IsDir() { - continue - } - - // 检查是否有 SKILL.md - skillFile := filepath.Join(skillsPath, entry.Name(), "SKILL.md") - if _, err := os.Stat(skillFile); err == nil { - skills = append(skills, types.SkillMetadata{ - Name: entry.Name(), - }) - } - } - - return skills, nil -} - -// ScanCommands 扫描仓库中的 commands -func ScanCommands(repoPath string) ([]types.CommandGroup, error) { - commandsPath := filepath.Join(repoPath, "commands") - - entries, err := os.ReadDir(commandsPath) - if err != nil { - if os.IsNotExist(err) { - return []types.CommandGroup{}, nil - } - return nil, err - } - - var groups []types.CommandGroup - for _, entry := range entries { - if !entry.IsDir() { - continue - } - - // 列出目录下的 .md 文件 - files, err := filepath.Glob(filepath.Join(commandsPath, entry.Name(), "*.md")) - if err != nil { - fmt.Fprintf(os.Stderr, "警告: 无法扫描 %s 下的 markdown 文件: %v\n", entry.Name(), err) - continue - } - - var fileNames []string - for _, f := range files { - fileNames = append(fileNames, filepath.Base(f)) - } - - if len(fileNames) > 0 { - groups = append(groups, types.CommandGroup{ - Name: entry.Name(), - Files: fileNames, - }) - } - } - - return groups, nil -} - -// FindSkill 在所有仓库中查找 skill -func FindSkill(name string) (repoPath, skillPath string, repoName string, err error) { - cfg, err := config.LoadRepositoryConfig() - if err != nil { - return "", "", "", err - } - - cachePath, err := config.GetCachePath() - if err != nil { - return "", "", "", err - } - - for _, repo := range cfg.Repositories { - rp := filepath.Join(cachePath, URLToPathName(repo.URL)) - sp := filepath.Join(rp, "skills", name) - - if _, err := os.Stat(filepath.Join(sp, "SKILL.md")); err == nil { - return rp, sp, repo.Name, nil - } - } - - return "", "", "", fmt.Errorf("skill '%s' 未在任何仓库中找到", name) -} - -// FindCommand 在所有仓库中查找 command -func FindCommand(name string) (repoPath, commandPath string, repoName string, err error) { - cfg, err := config.LoadRepositoryConfig() - if err != nil { - return "", "", "", err - } - - cachePath, err := config.GetCachePath() - if err != nil { - return "", "", "", err - } - - for _, repo := range cfg.Repositories { - rp := filepath.Join(cachePath, URLToPathName(repo.URL)) - cp := filepath.Join(rp, "commands", name) - - if info, err := os.Stat(cp); err == nil && info.IsDir() { - // 检查目录是否包含 .md 文件 - files, _ := filepath.Glob(filepath.Join(cp, "*.md")) - if len(files) > 0 { - return rp, cp, repo.Name, nil - } - // 目录存在但为空,返回特定错误 - return "", "", "", fmt.Errorf("command group '%s' 不包含任何命令文件", name) - } - } - - return "", "", "", fmt.Errorf("command '%s' 未在任何仓库中找到", name) -} - -// ListAvailableSkills 列出所有可用的 skills -func ListAvailableSkills() ([]types.SkillMetadata, error) { - cfg, err := config.LoadRepositoryConfig() - if err != nil { - return nil, err - } - - cachePath, err := config.GetCachePath() - if err != nil { - return nil, err - } - - var allSkills []types.SkillMetadata - for _, repo := range cfg.Repositories { - rp := filepath.Join(cachePath, URLToPathName(repo.URL)) - skills, err := ScanSkills(rp) - if err != nil { - continue - } - for i := range skills { - skills[i].SourceRepo = repo.Name - } - allSkills = append(allSkills, skills...) - } - - return allSkills, nil -} - -// ListAvailableCommands 列出所有可用的 commands -func ListAvailableCommands() ([]types.CommandGroup, error) { - cfg, err := config.LoadRepositoryConfig() - if err != nil { - return nil, err - } - - cachePath, err := config.GetCachePath() - if err != nil { - return nil, err - } - - var allCommands []types.CommandGroup - for _, repo := range cfg.Repositories { - rp := filepath.Join(cachePath, URLToPathName(repo.URL)) - commands, err := ScanCommands(rp) - if err != nil { - continue - } - for i := range commands { - commands[i].SourceRepo = repo.Name - } - allCommands = append(allCommands, commands...) - } - - return allCommands, nil -} diff --git a/manager/internal/testutil/testutil.go b/manager/internal/testutil/testutil.go deleted file mode 100644 index 433e010..0000000 --- a/manager/internal/testutil/testutil.go +++ /dev/null @@ -1,184 +0,0 @@ -package testutil - -import ( - "os" - "os/exec" - "path/filepath" - "testing" - - "skillmgr/pkg/fileutil" -) - -// SetupTestEnv 设置测试环境 -// 返回临时目录路径和清理函数 -func SetupTestEnv(t *testing.T) (string, func()) { - t.Helper() - - tmpDir, err := os.MkdirTemp("", "skillmgr-test-*") - if err != nil { - t.Fatalf("创建临时目录失败: %v", err) - } - - // 设置环境变量 - os.Setenv("SKILLMGR_TEST_ROOT", tmpDir) - os.Setenv("SKILLMGR_TEST_BASE", tmpDir) - - cleanup := func() { - os.Unsetenv("SKILLMGR_TEST_ROOT") - os.Unsetenv("SKILLMGR_TEST_BASE") - os.RemoveAll(tmpDir) - } - - return tmpDir, cleanup -} - -// SetupTestRepo 创建一个临时 git 仓库 -// 返回仓库路径 -func SetupTestRepo(t *testing.T, baseDir string) string { - t.Helper() - - repoDir := filepath.Join(baseDir, "test-repo") - if err := os.MkdirAll(repoDir, 0755); err != nil { - t.Fatalf("创建仓库目录失败: %v", err) - } - - // 初始化 git 仓库 - cmd := exec.Command("git", "init") - cmd.Dir = repoDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("git init 失败: %v\n%s", err, output) - } - - // 配置 git user(测试用) - configCmds := [][]string{ - {"git", "config", "user.email", "test@example.com"}, - {"git", "config", "user.name", "Test User"}, - } - for _, args := range configCmds { - cmd := exec.Command(args[0], args[1:]...) - cmd.Dir = repoDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("git config 失败: %v\n%s", err, output) - } - } - - return repoDir -} - -// CopyFixtureRepo 复制 fixture 仓库并初始化 git -func CopyFixtureRepo(t *testing.T, fixtureDir, destDir string) string { - t.Helper() - - repoDir := filepath.Join(destDir, filepath.Base(fixtureDir)) - if err := fileutil.CopyDir(fixtureDir, repoDir); err != nil { - t.Fatalf("复制 fixture 仓库失败: %v", err) - } - - // 初始化 git 仓库 - cmd := exec.Command("git", "init") - cmd.Dir = repoDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("git init 失败: %v\n%s", err, output) - } - - // 配置 git user(测试用) - configCmds := [][]string{ - {"git", "config", "user.email", "test@example.com"}, - {"git", "config", "user.name", "Test User"}, - } - for _, args := range configCmds { - cmd := exec.Command(args[0], args[1:]...) - cmd.Dir = repoDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("git config 失败: %v\n%s", err, output) - } - } - - // 添加并提交 - addCmd := exec.Command("git", "add", ".") - addCmd.Dir = repoDir - if output, err := addCmd.CombinedOutput(); err != nil { - t.Fatalf("git add 失败: %v\n%s", err, output) - } - - commitCmd := exec.Command("git", "commit", "-m", "Initial commit") - commitCmd.Dir = repoDir - if output, err := commitCmd.CombinedOutput(); err != nil { - t.Fatalf("git commit 失败: %v\n%s", err, output) - } - - return repoDir -} - -// GetFixturePath 获取 fixture 目录路径 -func GetFixturePath(t *testing.T) string { - t.Helper() - - // 尝试几个可能的位置 - candidates := []string{ - "testdata/fixtures", - "../testdata/fixtures", - "../../testdata/fixtures", - "../../../testdata/fixtures", - } - - for _, candidate := range candidates { - if _, err := os.Stat(candidate); err == nil { - abs, _ := filepath.Abs(candidate) - return abs - } - } - - // 从工作目录向上查找 - wd, _ := os.Getwd() - for { - candidate := filepath.Join(wd, "testdata", "fixtures") - if _, err := os.Stat(candidate); err == nil { - return candidate - } - parent := filepath.Dir(wd) - if parent == wd { - break - } - wd = parent - } - - t.Fatalf("无法找到 fixtures 目录") - return "" -} - -// CreateTestSkill 在目录中创建测试 skill -func CreateTestSkill(t *testing.T, baseDir, name string) string { - t.Helper() - - skillDir := filepath.Join(baseDir, "skills", name) - if err := os.MkdirAll(skillDir, 0755); err != nil { - t.Fatalf("创建 skill 目录失败: %v", err) - } - - content := []byte("# " + name + "\n\nTest skill.\n") - if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"), content, 0644); err != nil { - t.Fatalf("创建 SKILL.md 失败: %v", err) - } - - return skillDir -} - -// CreateTestCommand 在目录中创建测试命令组 -func CreateTestCommand(t *testing.T, baseDir, groupName string, files []string) string { - t.Helper() - - cmdDir := filepath.Join(baseDir, "commands", groupName) - if err := os.MkdirAll(cmdDir, 0755); err != nil { - t.Fatalf("创建 command 目录失败: %v", err) - } - - for _, file := range files { - content := []byte("# " + file + "\n\nTest command.\n") - if err := os.WriteFile(filepath.Join(cmdDir, file), content, 0644); err != nil { - t.Fatalf("创建 %s 失败: %v", file, err) - } - } - - return cmdDir -} diff --git a/manager/internal/types/types.go b/manager/internal/types/types.go deleted file mode 100644 index 29ec35a..0000000 --- a/manager/internal/types/types.go +++ /dev/null @@ -1,71 +0,0 @@ -package types - -import "time" - -// Platform 平台类型 -type Platform string - -const ( - PlatformClaude Platform = "claude" - PlatformOpenCode Platform = "opencode" -) - -// ItemType 安装项类型 -type ItemType string - -const ( - ItemTypeSkill ItemType = "skill" - ItemTypeCommand ItemType = "command" -) - -// Scope 安装作用域 -type Scope string - -const ( - ScopeGlobal Scope = "global" - ScopeProject Scope = "project" -) - -// Repository 源仓库配置 -type Repository struct { - Name string `json:"name"` - URL string `json:"url"` - Branch string `json:"branch"` - AddedAt time.Time `json:"added_at"` -} - -// RepositoryConfig 仓库配置文件结构 -type RepositoryConfig struct { - Repositories []Repository `json:"repositories"` -} - -// InstallRecord 安装记录 -type InstallRecord struct { - Type ItemType `json:"type"` - Name string `json:"name"` - SourceRepo string `json:"source_repo"` - Platform Platform `json:"platform"` - Scope Scope `json:"scope"` - InstallPath string `json:"install_path"` - InstalledAt time.Time `json:"installed_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// InstallConfig 安装配置文件结构 -type InstallConfig struct { - Installations []InstallRecord `json:"installations"` -} - -// SkillMetadata skill 元数据(从 SKILL.md frontmatter 解析) -type SkillMetadata struct { - Name string - Description string - SourceRepo string -} - -// CommandGroup 命令组信息 -type CommandGroup struct { - Name string // 命令组名称(目录名) - Files []string // 命令文件列表 - SourceRepo string // 来源仓库 -} diff --git a/manager/pkg/fileutil/fileutil.go b/manager/pkg/fileutil/fileutil.go deleted file mode 100644 index 19fd4e6..0000000 --- a/manager/pkg/fileutil/fileutil.go +++ /dev/null @@ -1,62 +0,0 @@ -package fileutil - -import ( - "io" - "os" - "path/filepath" -) - -// CopyFile 复制文件,保留权限 -func CopyFile(src, dst string) error { - srcFile, err := os.Open(src) - if err != nil { - return err - } - defer srcFile.Close() - - // 确保目标目录存在 - if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { - return err - } - - dstFile, err := os.Create(dst) - if err != nil { - return err - } - defer dstFile.Close() - - if _, err := io.Copy(dstFile, srcFile); err != nil { - return err - } - - // 保留文件权限,移除特殊权限位(SETUID/SETGID/STICKY) - srcInfo, err := os.Stat(src) - if err != nil { - return err - } - // 只保留标准权限位,移除特殊权限位 - mode := srcInfo.Mode() & os.ModePerm - return os.Chmod(dst, mode) -} - -// CopyDir 递归复制目录 -func CopyDir(src, dst string) error { - return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - relPath, err := filepath.Rel(src, path) - if err != nil { - return err - } - - dstPath := filepath.Join(dst, relPath) - - if info.IsDir() { - return os.MkdirAll(dstPath, info.Mode()) - } - - return CopyFile(path, dstPath) - }) -} diff --git a/manager/pkg/fileutil/fileutil_test.go b/manager/pkg/fileutil/fileutil_test.go deleted file mode 100644 index 7a16f33..0000000 --- a/manager/pkg/fileutil/fileutil_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package fileutil - -import ( - "os" - "path/filepath" - "testing" -) - -func TestCopyFile(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "skillmgr-fileutil-test-*") - if err != nil { - t.Fatalf("创建临时目录失败: %v", err) - } - defer os.RemoveAll(tmpDir) - - // 创建源文件 - srcFile := filepath.Join(tmpDir, "src.txt") - content := []byte("test content") - if err := os.WriteFile(srcFile, content, 0644); err != nil { - t.Fatalf("创建源文件失败: %v", err) - } - - // 复制 - destFile := filepath.Join(tmpDir, "dest.txt") - if err := CopyFile(srcFile, destFile); err != nil { - t.Fatalf("CopyFile 失败: %v", err) - } - - // 验证 - destContent, err := os.ReadFile(destFile) - if err != nil { - t.Fatalf("读取目标文件失败: %v", err) - } - - if string(destContent) != string(content) { - t.Errorf("内容不匹配:期望 %s,得到 %s", content, destContent) - } -} - -func TestCopyDir(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "skillmgr-fileutil-test-*") - if err != nil { - t.Fatalf("创建临时目录失败: %v", err) - } - defer os.RemoveAll(tmpDir) - - // 创建源目录结构 - srcDir := filepath.Join(tmpDir, "src") - os.MkdirAll(filepath.Join(srcDir, "subdir"), 0755) - os.WriteFile(filepath.Join(srcDir, "file1.txt"), []byte("file1"), 0644) - os.WriteFile(filepath.Join(srcDir, "subdir", "file2.txt"), []byte("file2"), 0644) - - // 复制 - destDir := filepath.Join(tmpDir, "dest") - if err := CopyDir(srcDir, destDir); err != nil { - t.Fatalf("CopyDir 失败: %v", err) - } - - // 验证文件存在 - files := []string{ - filepath.Join(destDir, "file1.txt"), - filepath.Join(destDir, "subdir", "file2.txt"), - } - - for _, file := range files { - if _, err := os.Stat(file); os.IsNotExist(err) { - t.Errorf("文件应该存在: %s", file) - } - } -} - -func TestCopyFile_PreservePermissions(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "skillmgr-fileutil-test-*") - if err != nil { - t.Fatalf("创建临时目录失败: %v", err) - } - defer os.RemoveAll(tmpDir) - - // 创建可执行文件 - srcFile := filepath.Join(tmpDir, "src.sh") - if err := os.WriteFile(srcFile, []byte("#!/bin/bash"), 0755); err != nil { - t.Fatalf("创建源文件失败: %v", err) - } - - // 复制 - destFile := filepath.Join(tmpDir, "dest.sh") - if err := CopyFile(srcFile, destFile); err != nil { - t.Fatalf("CopyFile 失败: %v", err) - } - - // 验证权限 - info, err := os.Stat(destFile) - if err != nil { - t.Fatalf("获取文件信息失败: %v", err) - } - - if info.Mode().Perm() != 0755 { - t.Errorf("权限不匹配:期望 0755,得到 %o", info.Mode().Perm()) - } -} diff --git a/manager/scripts/sandbox.sh b/manager/scripts/sandbox.sh deleted file mode 100755 index ac5e3e7..0000000 --- a/manager/scripts/sandbox.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -# sandbox.sh - 手动测试沙盒环境 - -set -e - -cd "$(dirname "$0")/.." - -# 创建沙盒目录 -SANDBOX_DIR="${1:-/tmp/skillmgr-sandbox}" -mkdir -p "$SANDBOX_DIR" - -echo "=== 沙盒环境 ===" -echo "目录: $SANDBOX_DIR" -echo "" - -# 设置环境变量 -export SKILLMGR_TEST_ROOT="$SANDBOX_DIR/config" -export SKILLMGR_TEST_BASE="$SANDBOX_DIR/install" - -# 确保 skillmgr 已构建 -if [ ! -f "bin/skillmgr" ]; then - echo "构建 skillmgr..." - go build -o bin/skillmgr ./cmd/skillmgr -fi - -echo "环境变量已设置:" -echo " SKILLMGR_TEST_ROOT=$SKILLMGR_TEST_ROOT" -echo " SKILLMGR_TEST_BASE=$SKILLMGR_TEST_BASE" -echo "" -echo "可执行文件: $(pwd)/bin/skillmgr" -echo "" -echo "示例命令:" -echo " ./bin/skillmgr --help" -echo " ./bin/skillmgr add https://github.com/example/skills.git --name example" -echo " ./bin/skillmgr repos" -echo "" -echo "清理沙盒:" -echo " rm -rf $SANDBOX_DIR" -echo "" - -# 进入子 shell -echo "进入沙盒 shell (exit 退出)..." -$SHELL diff --git a/manager/scripts/test.sh b/manager/scripts/test.sh deleted file mode 100755 index c06a307..0000000 --- a/manager/scripts/test.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# test.sh - 运行测试 - -set -e - -cd "$(dirname "$0")/.." - -# 创建临时测试目录 -TEST_DIR=$(mktemp -d) -trap "rm -rf $TEST_DIR" EXIT - -# 设置测试环境变量 -export SKILLMGR_TEST_ROOT="$TEST_DIR/config" -export SKILLMGR_TEST_BASE="$TEST_DIR/install" - -echo "=== 测试环境 ===" -echo "SKILLMGR_TEST_ROOT: $SKILLMGR_TEST_ROOT" -echo "SKILLMGR_TEST_BASE: $SKILLMGR_TEST_BASE" -echo "" - -# 运行测试 -echo "=== 运行测试 ===" -go test -v ./... - -echo "" -echo "=== 测试完成 ===" diff --git a/manager/testdata/fixtures/test-repo/commands/test-cmd/init.md b/manager/testdata/fixtures/test-repo/commands/test-cmd/init.md deleted file mode 100644 index 2e7701d..0000000 --- a/manager/testdata/fixtures/test-repo/commands/test-cmd/init.md +++ /dev/null @@ -1,9 +0,0 @@ -# Init Command - -Test command for initialization. - -## Usage - -``` -/test-cmd:init -``` diff --git a/manager/testdata/fixtures/test-repo/commands/test-cmd/run.md b/manager/testdata/fixtures/test-repo/commands/test-cmd/run.md deleted file mode 100644 index 0fbe4f4..0000000 --- a/manager/testdata/fixtures/test-repo/commands/test-cmd/run.md +++ /dev/null @@ -1,9 +0,0 @@ -# Run Command - -Test command for running. - -## Usage - -``` -/test-cmd:run -``` diff --git a/manager/testdata/fixtures/test-repo/skills/test-skill-2/SKILL.md b/manager/testdata/fixtures/test-repo/skills/test-skill-2/SKILL.md deleted file mode 100644 index 1b21b91..0000000 --- a/manager/testdata/fixtures/test-repo/skills/test-skill-2/SKILL.md +++ /dev/null @@ -1,7 +0,0 @@ -# Test Skill 2 - -Second test skill for testing multiple skills. - -## Description - -Another test skill fixture. diff --git a/manager/testdata/fixtures/test-repo/skills/test-skill/SKILL.md b/manager/testdata/fixtures/test-repo/skills/test-skill/SKILL.md deleted file mode 100644 index 1735d4a..0000000 --- a/manager/testdata/fixtures/test-repo/skills/test-skill/SKILL.md +++ /dev/null @@ -1,7 +0,0 @@ -# Test Skill - -This is a test skill for unit and integration tests. - -## Description - -A simple skill that does nothing but serves as a test fixture. diff --git a/publish.sh b/publish.sh new file mode 100644 index 0000000..7ce86e9 --- /dev/null +++ b/publish.sh @@ -0,0 +1,134 @@ +#!/bin/bash +# +# Skill 发布脚本 +# +# 使用方式: +# ./publish.sh +# +# 示例: +# ./publish.sh lyxy-kb +# ./publish.sh lyxy-reader-office +# + +set -e + +# 获取脚本所在目录 +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR" + +# 配置 +TARGET_REPO_URL="https://github.com/lanyuanxiaoyao/skills.git" +TEMP_DIR_BASE="${TMPDIR:-/tmp}/lyxy-skill-publish" + +# 颜色输出 +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# 打印信息 +info() { + echo -e "${GREEN}>>>${NC} $1" +} + +error() { + echo -e "${RED}错误:${NC} $1" +} + +# 显示使用说明 +show_usage() { + echo "使用方式:" + echo " $0 " + echo "" + echo "可用的 skills:" + for dir in skills/*/; do + if [ -d "$dir" ]; then + name="$(basename "$dir")" + echo " - $name" + fi + done + echo "" + echo "示例:" + echo " $0 lyxy-kb" + echo " $0 lyxy-reader-office" + exit 1 +} + +# 检查参数 +if [ $# -eq 0 ]; then + error "缺少 skill 名称参数" + show_usage +fi + +SKILL_NAME="$1" +SKILL_DIR="$SCRIPT_DIR/skills/$SKILL_NAME" +TARGET_PATH="skills/$SKILL_NAME" + +# 检查 skill 目录是否存在 +if [ ! -d "$SKILL_DIR" ]; then + error "Skill 目录不存在: $SKILL_DIR" + echo "" + echo "可用的 skills:" + for dir in skills/*/; do + if [ -d "$dir" ]; then + name="$(basename "$dir")" + echo " - $name" + fi + done + exit 1 +fi + +info "发布 Skill: $SKILL_NAME" +info "目标仓库: $TARGET_REPO_URL" +info "目标路径: $TARGET_PATH" +echo "" + +# 创建临时目录 +TIMESTAMP=$(date +%s) +TEMP_DIR="$TEMP_DIR_BASE-$TIMESTAMP" +mkdir -p "$TEMP_DIR" + +# 清理函数 +cleanup() { + if [ -d "$TEMP_DIR" ]; then + rm -rf "$TEMP_DIR" + fi +} +trap cleanup EXIT + +# Clone 仓库 +info "[1/4] Clone 仓库..." +REPO_DIR="$TEMP_DIR/skills-repo" +git clone --depth 1 "$TARGET_REPO_URL" "$REPO_DIR" + +# 清空目标目录 +info "[2/4] 清空目标目录..." +TARGET_DIR="$REPO_DIR/$TARGET_PATH" +if [ -d "$TARGET_DIR" ]; then + rm -rf "$TARGET_DIR" +fi +mkdir -p "$TARGET_DIR" + +# 复制内容 +info "[3/4] 复制 Skill 文件..." +# 复制除了 __pycache__ 之外的所有内容 +for item in "$SKILL_DIR"/*; do + basename_item="$(basename "$item")" + # 跳过 __pycache__ + if [ "$basename_item" = "__pycache__" ]; then + continue + fi + if [ -d "$item" ]; then + cp -r "$item" "$TARGET_DIR/" + else + cp "$item" "$TARGET_DIR/" + fi +done + +# 提交并推送 +info "[4/4] 提交并推送到 GitHub..." +cd "$REPO_DIR" +git add . +git commit -m "publish: $SKILL_NAME" +git push + +info "发布成功!"