1
0

完成一个简易的全局skill、command管理器

This commit is contained in:
2026-02-25 14:33:56 +08:00
parent f4cb809f9d
commit 2d327b5af8
60 changed files with 6053 additions and 1 deletions

View File

@@ -0,0 +1,90 @@
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 <url>",
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)
}

View File

@@ -0,0 +1,80 @@
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)
}

View File

@@ -0,0 +1,64 @@
package main
import (
"fmt"
"github.com/spf13/cobra"
"skillmgr/internal/installer"
"skillmgr/internal/types"
)
var installCmd = &cobra.Command{
Use: "install <type> <name>",
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)
}

View File

@@ -0,0 +1,86 @@
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)
}

View File

@@ -0,0 +1,44 @@
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 <url>' 添加仓库")
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)
}

View File

@@ -0,0 +1,5 @@
package main
func main() {
Execute()
}

View File

@@ -0,0 +1,43 @@
package main
import (
"fmt"
"github.com/spf13/cobra"
"skillmgr/internal/config"
)
var removeCmd = &cobra.Command{
Use: "remove <name>",
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)
}

View File

@@ -0,0 +1,55 @@
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)
}
}

View File

@@ -0,0 +1,118 @@
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)
}

View File

@@ -0,0 +1,70 @@
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)
}

View File

@@ -0,0 +1,53 @@
package main
import (
"fmt"
"github.com/spf13/cobra"
"skillmgr/internal/installer"
"skillmgr/internal/types"
)
var uninstallCmd = &cobra.Command{
Use: "uninstall <type> <name>",
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)
}

View File

@@ -0,0 +1,99 @@
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)
}