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) }