Files
Local-Cursor/internal/skills/skills.go
FuDie0915 188225fae6 Ð޸´ȫÏîĿÉó²éÎÊÌⲢ·¢²¼ v1.1.9
¸ÄÓñ¾»ú¶ÀÁ¢ CA£¬»ָ´ CLI TLS УÑ飬±£»¤ƾ¾ÝÓëÅäÖò¢·¢±£´档
Ð޸´ Cursor Êý¾ݡ¢»Ự·¾¶¡¢´úÀíÉúÃüÖÜÆڡ¢ģÐÍÇëÇóÓë Skills Îļþ²Ù×÷¡£
Ϊ¶ÀÁ¢ Tab ·þÎñÔö¼ӿͻ§¶˼øȨ£¬Éý¼¶ÒÀÀµÓ빤¾ßÁ´£¬ÍêÉƷ¢²¼ԪÐÅϢͬ²½¡£

Ñé֤£ºÁ½¸ö Go ģ¿éµIJâÊÔ/vet¡¢¹ؼüģ¿é race¡¢ǰ¶˲âÊÔ/Éú²ú¹¹½¨/Éó¼ơ¢
Go ¿ɴï©¶´ɨÃèÓë macOS/Linux ¿ÉÐÐģ¿齻²æ±àÒë¾ùͨ¹ý¡£
Windows ¸ôÀëÆô¶¯ͨ¹ý£»δÍê³ɵĵã»÷ðÑ̺ÍԭÉúƽ̨Ñé֤¼ûÉó²鱨¸档
2026-11-10 09:59:34 +08:00

302 lines
9.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package skills 管理内置 Skill 资源及其向目标项目的安装/移除。
// 状态以文件系统为准:项目 .cursor/skills/<name>/ 存在即已安装。
// skill 目录内的 commands/ 子目录是斜杠命令模板,安装时分发到项目 .cursor/commands/。
package skills
import (
"bytes"
"embed"
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"sort"
"strings"
)
//go:embed builtin
var builtinFS embed.FS
const (
// builtinRoot 是 embed FS 中内置资源的根目录名。
builtinRoot = "builtin"
// commandsDirName 是 skill 目录内存放命令模板的子目录名。
commandsDirName = "commands"
)
// retiredSkillFiles 记录各内置 skill 更新时需要清理的退役文件。
var retiredSkillFiles = map[string][]string{
"fd-engineering": {
"references/native-tools.md",
"references/mode-switch.md",
},
}
// CommandInfo 描述 skill 附带的一个斜杠命令。
type CommandInfo struct {
Name string `json:"name"`
Description string `json:"description"`
}
// Skill 描述一个内置 Skill。
type Skill struct {
Name string `json:"name"`
Description string `json:"description"`
Commands []CommandInfo `json:"commands"`
}
// skillsDir 返回项目内的 skill 安装目录。
func skillsDir(projectPath string) string {
return filepath.Join(projectPath, ".cursor", "skills")
}
// skillTargetDir 返回某 skill 在项目内的安装目录。
func skillTargetDir(projectPath string, name string) string {
return filepath.Join(skillsDir(projectPath), name)
}
// projectCommandsDir 返回项目内的斜杠命令目录。
func projectCommandsDir(projectPath string) string {
return filepath.Join(projectPath, ".cursor", "commands")
}
// List 返回所有内置 Skill,按名称排序。
func List() []Skill {
dirEntries, err := fs.ReadDir(builtinFS, builtinRoot)
if err != nil {
return []Skill{}
}
result := make([]Skill, 0, len(dirEntries))
for _, dirEntry := range dirEntries {
if !dirEntry.IsDir() {
continue
}
result = append(result, Skill{
Name: dirEntry.Name(),
Description: readFrontmatterDescription(path.Join(builtinRoot, dirEntry.Name(), "SKILL.md")),
Commands: listCommands(dirEntry.Name()),
})
}
return result
}
// listCommands 列出内置 skill 附带的斜杠命令及其 description。
func listCommands(name string) []CommandInfo {
entries, err := fs.ReadDir(builtinFS, path.Join(builtinRoot, name, commandsDirName))
if err != nil {
return []CommandInfo{}
}
result := make([]CommandInfo, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
continue
}
result = append(result, CommandInfo{
Name: strings.TrimSuffix(entry.Name(), ".md"),
Description: readFrontmatterDescription(path.Join(builtinRoot, name, commandsDirName, entry.Name())),
})
}
sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name })
return result
}
// commandFileNames 返回内置 skill 附带的命令文件名列表。
func commandFileNames(name string) []string {
entries, err := fs.ReadDir(builtinFS, path.Join(builtinRoot, name, commandsDirName))
if err != nil {
return nil
}
names := make([]string, 0, len(entries))
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".md") {
names = append(names, entry.Name())
}
}
return names
}
// Exists 校验内置 skill 是否存在。
func Exists(name string) bool {
name = sanitizeSkillName(name)
if name == "" {
return false
}
info, err := fs.Stat(builtinFS, path.Join(builtinRoot, name, "SKILL.md"))
return err == nil && !info.IsDir()
}
// Install 将内置 skill 同步到项目 .cursor/skills/<name>/,
// 并将其 commands/ 模板分发到项目 .cursor/commands/。
// 目标已存在时同步为最新内置内容,并清理已退役的内置文件。
func Install(projectPath string, name string) error {
name = sanitizeSkillName(name)
projectPath = strings.TrimSpace(projectPath)
operationMu.Lock()
defer operationMu.Unlock()
if !Exists(name) {
return fmt.Errorf("未知的内置 skill")
}
if err := checkManagedPaths(projectPath, name); err != nil {
return err
}
name = sanitizeSkillName(name)
if name == "" || !Exists(name) {
return fmt.Errorf("内置 skill 不存在: %s", name)
}
projectPath = strings.TrimSpace(projectPath)
if info, err := os.Stat(projectPath); err != nil || !info.IsDir() {
return fmt.Errorf("项目目录不存在: %s", projectPath)
}
target := skillTargetDir(projectPath, name)
// commands/ 模板单独分发到 .cursor/commands/,不进入 skill 目录。
if err := copyEmbeddedDir(path.Join(builtinRoot, name), target, commandsDirName); err != nil {
return fmt.Errorf("复制 skill 文件失败: %w", err)
}
for _, relativePath := range retiredSkillFiles[name] {
if err := os.Remove(filepath.Join(target, filepath.FromSlash(relativePath))); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("清理退役 skill 文件失败: %w", err)
}
}
if err := installCommands(projectPath, name); err != nil {
return err
}
return nil
}
// installCommands 将 skill 的命令模板复制到项目 .cursor/commands/。
func installCommands(projectPath string, name string) error {
fileNames := commandFileNames(name)
if len(fileNames) == 0 {
return nil
}
commandsDir := projectCommandsDir(projectPath)
if err := os.MkdirAll(commandsDir, 0o755); err != nil {
return fmt.Errorf("创建命令目录失败: %w", err)
}
for _, fileName := range fileNames {
data, err := builtinFS.ReadFile(path.Join(builtinRoot, name, commandsDirName, fileName))
if err != nil {
return fmt.Errorf("读取命令模板失败: %w", err)
}
if err := backupBeforeReplace(filepath.Join(commandsDir, fileName), data); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(commandsDir, fileName), data, 0o644); err != nil {
return fmt.Errorf("写入命令文件失败: %w", err)
}
}
return nil
}
// Uninstall 移除项目内的 skill 目录与随附的命令文件。不存在时幂等成功。
func Uninstall(projectPath string, name string) error {
name = sanitizeSkillName(name)
projectPath = strings.TrimSpace(projectPath)
operationMu.Lock()
defer operationMu.Unlock()
if !Exists(name) {
return fmt.Errorf("未知的内置 skill")
}
if err := checkManagedPaths(projectPath, name); err != nil {
return err
}
if err := ensureNoCustomSkillFiles(projectPath, name); err != nil {
return err
}
name = sanitizeSkillName(name)
if name == "" {
return fmt.Errorf("skill 名称不能为空")
}
projectPath = strings.TrimSpace(projectPath)
target := skillTargetDir(projectPath, name)
// 防御:只允许删除 .cursor/skills/<name> 一级目录。
if filepath.Base(target) != name || !strings.Contains(filepath.ToSlash(target), "/.cursor/skills/") {
return fmt.Errorf("非法的 skill 路径")
}
if err := os.RemoveAll(target); err != nil {
return fmt.Errorf("移除 skill 失败: %w", err)
}
// 只删除内置清单中的命令文件,不碰用户自建命令。
commandsDir := projectCommandsDir(projectPath)
for _, fileName := range commandFileNames(name) {
expected, _ := builtinFS.ReadFile(path.Join(builtinRoot, name, commandsDirName, fileName))
current, readErr := os.ReadFile(filepath.Join(commandsDir, fileName))
if readErr == nil && !bytes.Equal(current, expected) {
continue
}
if readErr != nil && !os.IsNotExist(readErr) {
return readErr
}
if err := os.Remove(filepath.Join(commandsDir, fileName)); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("移除命令文件失败: %w", err)
}
}
return nil
}
// IsInstalled 检测项目内是否已安装该 skill。
func IsInstalled(projectPath string, name string) bool {
name = sanitizeSkillName(name)
if name == "" {
return false
}
_, err := os.Stat(filepath.Join(skillTargetDir(projectPath, name), "SKILL.md"))
return err == nil
}
// readFrontmatterDescription 解析 markdown frontmatter 中的 description。
func readFrontmatterDescription(embedPath string) string {
raw, err := builtinFS.ReadFile(embedPath)
if err != nil {
return ""
}
for _, line := range strings.Split(string(raw), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "description:") {
return strings.TrimSpace(strings.TrimPrefix(line, "description:"))
}
}
return ""
}
// sanitizeSkillName 防止路径穿越:只允许字母数字、连字符、下划线、点。
func sanitizeSkillName(name string) string {
name = strings.TrimSpace(name)
if name == "" || strings.ContainsAny(name, "/\\") || strings.Contains(name, "..") {
return ""
}
return name
}
// copyEmbeddedDir 递归复制 embed FS 子目录到磁盘,skipDir 指定要跳过的子目录名。
func copyEmbeddedDir(embedDir string, targetDir string, skipDir string) error {
return fs.WalkDir(builtinFS, embedDir, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() && skipDir != "" && d.Name() == skipDir && p != embedDir {
return fs.SkipDir
}
rel, err := filepath.Rel(embedDir, p)
if err != nil {
return err
}
target := filepath.Join(targetDir, rel)
if d.IsDir() {
return os.MkdirAll(target, 0o755)
}
data, err := builtinFS.ReadFile(p)
if err != nil {
return err
}
if err := backupBeforeReplace(target, data); err != nil {
return err
}
return os.WriteFile(target, data, 0o644)
})
}