完成一个简易的全局skill、command管理器
This commit is contained in:
50
manager/internal/adapter/adapter.go
Normal file
50
manager/internal/adapter/adapter.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"skillmgr/internal/types"
|
||||
)
|
||||
|
||||
// PlatformAdapter 平台适配器接口
|
||||
type PlatformAdapter interface {
|
||||
// GetSkillInstallPath 获取 skill 安装路径
|
||||
GetSkillInstallPath(scope types.Scope, skillName string) (string, error)
|
||||
|
||||
// GetCommandInstallPath 获取 command 安装路径
|
||||
GetCommandInstallPath(scope types.Scope, commandGroup string) (string, error)
|
||||
|
||||
// AdaptSkill 适配 skill(返回 source → dest 映射)
|
||||
AdaptSkill(sourcePath, destBasePath string) (map[string]string, error)
|
||||
|
||||
// AdaptCommand 适配 command(返回 source → dest 映射)
|
||||
AdaptCommand(sourcePath, destBasePath, commandGroup string) (map[string]string, error)
|
||||
}
|
||||
|
||||
// GetAdapter 获取平台适配器
|
||||
func GetAdapter(platform types.Platform) (PlatformAdapter, error) {
|
||||
switch platform {
|
||||
case types.PlatformClaude:
|
||||
return &ClaudeAdapter{}, nil
|
||||
case types.PlatformOpenCode:
|
||||
return &OpenCodeAdapter{}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的平台: %s", platform)
|
||||
}
|
||||
}
|
||||
|
||||
// getBasePath 获取基础路径
|
||||
// 支持通过环境变量 SKILLMGR_TEST_BASE 覆盖(用于测试隔离)
|
||||
func getBasePath(scope types.Scope) (string, error) {
|
||||
// 测试模式:使用环境变量指定的目录
|
||||
if testBase := os.Getenv("SKILLMGR_TEST_BASE"); testBase != "" {
|
||||
return testBase, nil
|
||||
}
|
||||
|
||||
// 生产模式
|
||||
if scope == types.ScopeGlobal {
|
||||
return os.UserHomeDir()
|
||||
}
|
||||
return os.Getwd()
|
||||
}
|
||||
73
manager/internal/adapter/claude.go
Normal file
73
manager/internal/adapter/claude.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"skillmgr/internal/types"
|
||||
)
|
||||
|
||||
// ClaudeAdapter Claude Code 平台适配器
|
||||
type ClaudeAdapter struct{}
|
||||
|
||||
// GetSkillInstallPath 获取 skill 安装路径
|
||||
func (a *ClaudeAdapter) GetSkillInstallPath(scope types.Scope, skillName string) (string, error) {
|
||||
base, err := getBasePath(scope)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(base, ".claude", "skills", skillName), nil
|
||||
}
|
||||
|
||||
// GetCommandInstallPath 获取 command 安装路径
|
||||
func (a *ClaudeAdapter) GetCommandInstallPath(scope types.Scope, commandGroup string) (string, error) {
|
||||
base, err := getBasePath(scope)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(base, ".claude", "commands", commandGroup), nil
|
||||
}
|
||||
|
||||
// AdaptSkill 适配 skill(遍历源目录,生成文件映射)
|
||||
func (a *ClaudeAdapter) AdaptSkill(sourcePath, destBasePath string) (map[string]string, error) {
|
||||
mapping := make(map[string]string)
|
||||
|
||||
err := filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(sourcePath, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("计算相对路径失败: %w", err)
|
||||
}
|
||||
destPath := filepath.Join(destBasePath, relPath)
|
||||
|
||||
if !info.IsDir() {
|
||||
mapping[path] = destPath
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return mapping, err
|
||||
}
|
||||
|
||||
// AdaptCommand 适配 command(保持目录结构)
|
||||
func (a *ClaudeAdapter) AdaptCommand(sourcePath, destBasePath, commandGroup string) (map[string]string, error) {
|
||||
mapping := make(map[string]string)
|
||||
|
||||
files, err := filepath.Glob(filepath.Join(sourcePath, "*.md"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
fileName := filepath.Base(file)
|
||||
destPath := filepath.Join(destBasePath, fileName)
|
||||
mapping[file] = destPath
|
||||
}
|
||||
|
||||
return mapping, nil
|
||||
}
|
||||
133
manager/internal/adapter/claude_test.go
Normal file
133
manager/internal/adapter/claude_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"skillmgr/internal/types"
|
||||
)
|
||||
|
||||
func setupAdapterTestEnv(t *testing.T) (string, func()) {
|
||||
t.Helper()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "skillmgr-adapter-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("创建临时目录失败: %v", err)
|
||||
}
|
||||
|
||||
os.Setenv("SKILLMGR_TEST_BASE", tmpDir)
|
||||
|
||||
cleanup := func() {
|
||||
os.Unsetenv("SKILLMGR_TEST_BASE")
|
||||
os.RemoveAll(tmpDir)
|
||||
}
|
||||
|
||||
return tmpDir, cleanup
|
||||
}
|
||||
|
||||
func TestClaudeAdapter_GetSkillInstallPath_Global(t *testing.T) {
|
||||
tmpDir, cleanup := setupAdapterTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
adapter := &ClaudeAdapter{}
|
||||
path, err := adapter.GetSkillInstallPath(types.ScopeGlobal, "test-skill")
|
||||
if err != nil {
|
||||
t.Fatalf("GetSkillInstallPath 失败: %v", err)
|
||||
}
|
||||
|
||||
expected := filepath.Join(tmpDir, ".claude", "skills", "test-skill")
|
||||
if path != expected {
|
||||
t.Errorf("期望 %s,得到 %s", expected, path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeAdapter_GetSkillInstallPath_Project(t *testing.T) {
|
||||
_, cleanup := setupAdapterTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
adapter := &ClaudeAdapter{}
|
||||
path, err := adapter.GetSkillInstallPath(types.ScopeProject, "test-skill")
|
||||
if err != nil {
|
||||
t.Fatalf("GetSkillInstallPath 失败: %v", err)
|
||||
}
|
||||
|
||||
// 项目级路径是相对当前目录的
|
||||
if !filepath.IsAbs(path) {
|
||||
// 相对路径应该包含 .claude/skills
|
||||
if filepath.Base(filepath.Dir(path)) != "skills" {
|
||||
t.Errorf("期望路径包含 skills 目录,得到 %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeAdapter_GetCommandInstallPath_Global(t *testing.T) {
|
||||
tmpDir, cleanup := setupAdapterTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
adapter := &ClaudeAdapter{}
|
||||
path, err := adapter.GetCommandInstallPath(types.ScopeGlobal, "test-cmd")
|
||||
if err != nil {
|
||||
t.Fatalf("GetCommandInstallPath 失败: %v", err)
|
||||
}
|
||||
|
||||
expected := filepath.Join(tmpDir, ".claude", "commands", "test-cmd")
|
||||
if path != expected {
|
||||
t.Errorf("期望 %s,得到 %s", expected, path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeAdapter_AdaptSkill(t *testing.T) {
|
||||
tmpDir, cleanup := setupAdapterTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
// 创建源目录
|
||||
srcDir := filepath.Join(tmpDir, "src-skill")
|
||||
os.MkdirAll(srcDir, 0755)
|
||||
os.WriteFile(filepath.Join(srcDir, "SKILL.md"), []byte("test"), 0644)
|
||||
os.WriteFile(filepath.Join(srcDir, "helper.md"), []byte("test"), 0644)
|
||||
|
||||
destDir := filepath.Join(tmpDir, "dest-skill")
|
||||
|
||||
adapter := &ClaudeAdapter{}
|
||||
mapping, err := adapter.AdaptSkill(srcDir, destDir)
|
||||
if err != nil {
|
||||
t.Fatalf("AdaptSkill 失败: %v", err)
|
||||
}
|
||||
|
||||
if len(mapping) != 2 {
|
||||
t.Errorf("期望 2 个文件映射,得到 %d 个", len(mapping))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeAdapter_AdaptCommand(t *testing.T) {
|
||||
tmpDir, cleanup := setupAdapterTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
// 创建源目录
|
||||
srcDir := filepath.Join(tmpDir, "src-cmd")
|
||||
os.MkdirAll(srcDir, 0755)
|
||||
os.WriteFile(filepath.Join(srcDir, "init.md"), []byte("test"), 0644)
|
||||
os.WriteFile(filepath.Join(srcDir, "run.md"), []byte("test"), 0644)
|
||||
|
||||
destDir := filepath.Join(tmpDir, "dest-cmd")
|
||||
|
||||
adapter := &ClaudeAdapter{}
|
||||
mapping, err := adapter.AdaptCommand(srcDir, destDir, "test-cmd")
|
||||
if err != nil {
|
||||
t.Fatalf("AdaptCommand 失败: %v", err)
|
||||
}
|
||||
|
||||
if len(mapping) != 2 {
|
||||
t.Errorf("期望 2 个文件映射,得到 %d 个", len(mapping))
|
||||
}
|
||||
|
||||
// 验证文件名保持原样
|
||||
for src, dest := range mapping {
|
||||
srcBase := filepath.Base(src)
|
||||
destBase := filepath.Base(dest)
|
||||
if srcBase != destBase {
|
||||
t.Errorf("Claude 适配器应保持文件名:源 %s,目标 %s", srcBase, destBase)
|
||||
}
|
||||
}
|
||||
}
|
||||
89
manager/internal/adapter/opencode.go
Normal file
89
manager/internal/adapter/opencode.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"skillmgr/internal/types"
|
||||
)
|
||||
|
||||
// OpenCodeAdapter OpenCode 平台适配器
|
||||
type OpenCodeAdapter struct{}
|
||||
|
||||
// GetSkillInstallPath 获取 skill 安装路径
|
||||
func (a *OpenCodeAdapter) GetSkillInstallPath(scope types.Scope, skillName string) (string, error) {
|
||||
base, err := getBasePath(scope)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if scope == types.ScopeGlobal {
|
||||
// 全局: ~/.config/opencode/skills/<name>/
|
||||
return filepath.Join(base, ".config", "opencode", "skills", skillName), nil
|
||||
}
|
||||
// 项目级: ./.opencode/skills/<name>/
|
||||
return filepath.Join(base, ".opencode", "skills", skillName), nil
|
||||
}
|
||||
|
||||
// GetCommandInstallPath 获取 command 安装路径
|
||||
func (a *OpenCodeAdapter) GetCommandInstallPath(scope types.Scope, commandGroup string) (string, error) {
|
||||
base, err := getBasePath(scope)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if scope == types.ScopeGlobal {
|
||||
// 全局: ~/.config/opencode/commands/(扁平化,所有命令在同一目录)
|
||||
return filepath.Join(base, ".config", "opencode", "commands"), nil
|
||||
}
|
||||
// 项目级: ./.opencode/commands/
|
||||
return filepath.Join(base, ".opencode", "commands"), nil
|
||||
}
|
||||
|
||||
// AdaptSkill 适配 skill(与 Claude 相同,保持目录结构)
|
||||
func (a *OpenCodeAdapter) AdaptSkill(sourcePath, destBasePath string) (map[string]string, error) {
|
||||
mapping := make(map[string]string)
|
||||
|
||||
err := filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(sourcePath, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("计算相对路径失败: %w", err)
|
||||
}
|
||||
destPath := filepath.Join(destBasePath, relPath)
|
||||
|
||||
if !info.IsDir() {
|
||||
mapping[path] = destPath
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return mapping, err
|
||||
}
|
||||
|
||||
// AdaptCommand 适配 command(扁平化文件名:<group>-<action>.md)
|
||||
func (a *OpenCodeAdapter) AdaptCommand(sourcePath, destBasePath, commandGroup string) (map[string]string, error) {
|
||||
mapping := make(map[string]string)
|
||||
|
||||
files, err := filepath.Glob(filepath.Join(sourcePath, "*.md"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
fileName := filepath.Base(file)
|
||||
baseName := strings.TrimSuffix(fileName, ".md")
|
||||
|
||||
// 重命名:init.md → lyxy-kb-init.md
|
||||
newName := commandGroup + "-" + baseName + ".md"
|
||||
destPath := filepath.Join(destBasePath, newName)
|
||||
|
||||
mapping[file] = destPath
|
||||
}
|
||||
|
||||
return mapping, nil
|
||||
}
|
||||
109
manager/internal/adapter/opencode_test.go
Normal file
109
manager/internal/adapter/opencode_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"skillmgr/internal/types"
|
||||
)
|
||||
|
||||
func TestOpenCodeAdapter_GetSkillInstallPath_Global(t *testing.T) {
|
||||
tmpDir, cleanup := setupAdapterTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
adapter := &OpenCodeAdapter{}
|
||||
path, err := adapter.GetSkillInstallPath(types.ScopeGlobal, "test-skill")
|
||||
if err != nil {
|
||||
t.Fatalf("GetSkillInstallPath 失败: %v", err)
|
||||
}
|
||||
|
||||
// OpenCode 全局 skill 使用 ~/.config/opencode/skills/
|
||||
expected := filepath.Join(tmpDir, ".config", "opencode", "skills", "test-skill")
|
||||
if path != expected {
|
||||
t.Errorf("期望 %s,得到 %s", expected, path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenCodeAdapter_GetCommandInstallPath_Global(t *testing.T) {
|
||||
tmpDir, cleanup := setupAdapterTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
adapter := &OpenCodeAdapter{}
|
||||
path, err := adapter.GetCommandInstallPath(types.ScopeGlobal, "test-cmd")
|
||||
if err != nil {
|
||||
t.Fatalf("GetCommandInstallPath 失败: %v", err)
|
||||
}
|
||||
|
||||
// OpenCode 全局 command 使用 ~/.config/opencode/commands/
|
||||
expected := filepath.Join(tmpDir, ".config", "opencode", "commands")
|
||||
if path != expected {
|
||||
t.Errorf("期望 %s,得到 %s", expected, path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenCodeAdapter_AdaptCommand_Flattening(t *testing.T) {
|
||||
tmpDir, cleanup := setupAdapterTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
// 创建源目录
|
||||
srcDir := filepath.Join(tmpDir, "src-cmd")
|
||||
os.MkdirAll(srcDir, 0755)
|
||||
os.WriteFile(filepath.Join(srcDir, "init.md"), []byte("test"), 0644)
|
||||
os.WriteFile(filepath.Join(srcDir, "run.md"), []byte("test"), 0644)
|
||||
|
||||
destDir := filepath.Join(tmpDir, "dest-cmd")
|
||||
|
||||
adapter := &OpenCodeAdapter{}
|
||||
mapping, err := adapter.AdaptCommand(srcDir, destDir, "test-cmd")
|
||||
if err != nil {
|
||||
t.Fatalf("AdaptCommand 失败: %v", err)
|
||||
}
|
||||
|
||||
if len(mapping) != 2 {
|
||||
t.Errorf("期望 2 个文件映射,得到 %d 个", len(mapping))
|
||||
}
|
||||
|
||||
// 验证文件名被扁平化
|
||||
for src, dest := range mapping {
|
||||
srcBase := filepath.Base(src)
|
||||
destBase := filepath.Base(dest)
|
||||
|
||||
// init.md -> test-cmd-init.md
|
||||
nameWithoutExt := strings.TrimSuffix(srcBase, ".md")
|
||||
expectedBase := "test-cmd-" + nameWithoutExt + ".md"
|
||||
if destBase != expectedBase {
|
||||
t.Errorf("OpenCode 适配器应扁平化文件名:期望 %s,得到 %s", expectedBase, destBase)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAdapter_Claude(t *testing.T) {
|
||||
adapter, err := GetAdapter(types.PlatformClaude)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAdapter(claude) 失败: %v", err)
|
||||
}
|
||||
|
||||
if _, ok := adapter.(*ClaudeAdapter); !ok {
|
||||
t.Error("期望 ClaudeAdapter 类型")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAdapter_OpenCode(t *testing.T) {
|
||||
adapter, err := GetAdapter(types.PlatformOpenCode)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAdapter(opencode) 失败: %v", err)
|
||||
}
|
||||
|
||||
if _, ok := adapter.(*OpenCodeAdapter); !ok {
|
||||
t.Error("期望 OpenCodeAdapter 类型")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAdapter_Invalid(t *testing.T) {
|
||||
_, err := GetAdapter(types.Platform("invalid"))
|
||||
if err == nil {
|
||||
t.Error("期望无效平台返回错误")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user