完成一个简易的全局skill、command管理器
This commit is contained in:
304
manager/internal/installer/installer.go
Normal file
304
manager/internal/installer/installer.go
Normal file
@@ -0,0 +1,304 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user