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,109 @@
package repo
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"skillmgr/internal/config"
)
// URLToPathName 将 URL 转换为缓存目录名
// 例如: https://github.com/user/repo.git -> github.com_user_repo
func URLToPathName(url string) string {
clean := strings.TrimPrefix(url, "https://")
clean = strings.TrimPrefix(clean, "http://")
clean = strings.TrimSuffix(clean, ".git")
clean = strings.ReplaceAll(clean, "/", "_")
return clean
}
// CloneOrPull 克隆或更新仓库
// 如果仓库不存在则 clone存在则 pull
func CloneOrPull(url, branch string) (string, error) {
cachePath, err := config.GetCachePath()
if err != nil {
return "", err
}
repoPath := filepath.Join(cachePath, URLToPathName(url))
// 检查是否已存在
if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil {
// 已存在,执行 pull
return repoPath, pullRepo(repoPath, branch)
}
// 不存在,执行 clone
return repoPath, cloneRepo(url, branch, repoPath)
}
// cloneRepo 克隆仓库
func cloneRepo(url, branch, dest string) error {
args := []string{"clone", "--depth", "1"}
if branch != "" {
args = append(args, "--branch", branch)
}
args = append(args, url, dest)
cmd := exec.Command("git", args...)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("git clone 失败: %w\n%s", err, output)
}
return nil
}
// pullRepo 更新仓库
func pullRepo(path, branch string) error {
// 先 fetch
fetchCmd := exec.Command("git", "-C", path, "fetch", "origin")
if output, err := fetchCmd.CombinedOutput(); err != nil {
return fmt.Errorf("git fetch 失败: %w\n%s", err, output)
}
// 然后 pull
pullArgs := []string{"-C", path, "pull", "origin"}
if branch != "" {
pullArgs = append(pullArgs, branch)
}
pullCmd := exec.Command("git", pullArgs...)
output, err := pullCmd.CombinedOutput()
if err != nil {
return fmt.Errorf("git pull 失败: %w\n%s", err, output)
}
return nil
}
// GetRepoPath 获取仓库缓存路径
func GetRepoPath(url string) (string, error) {
cachePath, err := config.GetCachePath()
if err != nil {
return "", err
}
return filepath.Join(cachePath, URLToPathName(url)), nil
}
// CloneTemporary 克隆临时仓库到临时目录
// 返回临时目录路径和清理函数
func CloneTemporary(url, branch string) (repoPath string, cleanup func(), err error) {
// 创建临时目录
tmpDir, err := os.MkdirTemp("", "skillmgr-temp-*")
if err != nil {
return "", nil, fmt.Errorf("创建临时目录失败: %w", err)
}
cleanup = func() {
os.RemoveAll(tmpDir)
}
// 克隆到临时目录
if err := cloneRepo(url, branch, tmpDir); err != nil {
cleanup()
return "", nil, err
}
return tmpDir, cleanup, nil
}

View File

@@ -0,0 +1,24 @@
package repo
import (
"testing"
)
func TestURLToPathName(t *testing.T) {
tests := []struct {
url string
expected string
}{
{"https://github.com/user/repo.git", "github.com_user_repo"},
{"https://github.com/user/repo", "github.com_user_repo"},
{"http://gitlab.com/org/project.git", "gitlab.com_org_project"},
{"https://github.com/user/my-repo.git", "github.com_user_my-repo"},
}
for _, tc := range tests {
result := URLToPathName(tc.url)
if result != tc.expected {
t.Errorf("URLToPathName(%s): 期望 %s得到 %s", tc.url, tc.expected, result)
}
}
}

View File

@@ -0,0 +1,191 @@
package repo
import (
"fmt"
"os"
"path/filepath"
"skillmgr/internal/config"
"skillmgr/internal/types"
)
// ScanSkills 扫描仓库中的 skills
func ScanSkills(repoPath string) ([]types.SkillMetadata, error) {
skillsPath := filepath.Join(repoPath, "skills")
entries, err := os.ReadDir(skillsPath)
if err != nil {
if os.IsNotExist(err) {
return []types.SkillMetadata{}, nil
}
return nil, err
}
var skills []types.SkillMetadata
for _, entry := range entries {
if !entry.IsDir() {
continue
}
// 检查是否有 SKILL.md
skillFile := filepath.Join(skillsPath, entry.Name(), "SKILL.md")
if _, err := os.Stat(skillFile); err == nil {
skills = append(skills, types.SkillMetadata{
Name: entry.Name(),
})
}
}
return skills, nil
}
// ScanCommands 扫描仓库中的 commands
func ScanCommands(repoPath string) ([]types.CommandGroup, error) {
commandsPath := filepath.Join(repoPath, "commands")
entries, err := os.ReadDir(commandsPath)
if err != nil {
if os.IsNotExist(err) {
return []types.CommandGroup{}, nil
}
return nil, err
}
var groups []types.CommandGroup
for _, entry := range entries {
if !entry.IsDir() {
continue
}
// 列出目录下的 .md 文件
files, err := filepath.Glob(filepath.Join(commandsPath, entry.Name(), "*.md"))
if err != nil {
fmt.Fprintf(os.Stderr, "警告: 无法扫描 %s 下的 markdown 文件: %v\n", entry.Name(), err)
continue
}
var fileNames []string
for _, f := range files {
fileNames = append(fileNames, filepath.Base(f))
}
if len(fileNames) > 0 {
groups = append(groups, types.CommandGroup{
Name: entry.Name(),
Files: fileNames,
})
}
}
return groups, nil
}
// FindSkill 在所有仓库中查找 skill
func FindSkill(name string) (repoPath, skillPath string, repoName string, err error) {
cfg, err := config.LoadRepositoryConfig()
if err != nil {
return "", "", "", err
}
cachePath, err := config.GetCachePath()
if err != nil {
return "", "", "", err
}
for _, repo := range cfg.Repositories {
rp := filepath.Join(cachePath, URLToPathName(repo.URL))
sp := filepath.Join(rp, "skills", name)
if _, err := os.Stat(filepath.Join(sp, "SKILL.md")); err == nil {
return rp, sp, repo.Name, nil
}
}
return "", "", "", fmt.Errorf("skill '%s' 未在任何仓库中找到", name)
}
// FindCommand 在所有仓库中查找 command
func FindCommand(name string) (repoPath, commandPath string, repoName string, err error) {
cfg, err := config.LoadRepositoryConfig()
if err != nil {
return "", "", "", err
}
cachePath, err := config.GetCachePath()
if err != nil {
return "", "", "", err
}
for _, repo := range cfg.Repositories {
rp := filepath.Join(cachePath, URLToPathName(repo.URL))
cp := filepath.Join(rp, "commands", name)
if info, err := os.Stat(cp); err == nil && info.IsDir() {
// 检查目录是否包含 .md 文件
files, _ := filepath.Glob(filepath.Join(cp, "*.md"))
if len(files) > 0 {
return rp, cp, repo.Name, nil
}
// 目录存在但为空,返回特定错误
return "", "", "", fmt.Errorf("command group '%s' 不包含任何命令文件", name)
}
}
return "", "", "", fmt.Errorf("command '%s' 未在任何仓库中找到", name)
}
// ListAvailableSkills 列出所有可用的 skills
func ListAvailableSkills() ([]types.SkillMetadata, error) {
cfg, err := config.LoadRepositoryConfig()
if err != nil {
return nil, err
}
cachePath, err := config.GetCachePath()
if err != nil {
return nil, err
}
var allSkills []types.SkillMetadata
for _, repo := range cfg.Repositories {
rp := filepath.Join(cachePath, URLToPathName(repo.URL))
skills, err := ScanSkills(rp)
if err != nil {
continue
}
for i := range skills {
skills[i].SourceRepo = repo.Name
}
allSkills = append(allSkills, skills...)
}
return allSkills, nil
}
// ListAvailableCommands 列出所有可用的 commands
func ListAvailableCommands() ([]types.CommandGroup, error) {
cfg, err := config.LoadRepositoryConfig()
if err != nil {
return nil, err
}
cachePath, err := config.GetCachePath()
if err != nil {
return nil, err
}
var allCommands []types.CommandGroup
for _, repo := range cfg.Repositories {
rp := filepath.Join(cachePath, URLToPathName(repo.URL))
commands, err := ScanCommands(rp)
if err != nil {
continue
}
for i := range commands {
commands[i].SourceRepo = repo.Name
}
allCommands = append(allCommands, commands...)
}
return allCommands, nil
}