1
0
Files
Skill/manager/internal/installer/installer.go

305 lines
7.7 KiB
Go

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