305 lines
7.7 KiB
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)
|
|
}
|