package skills import ( "bytes" "crypto/sha256" "fmt" "io/fs" "os" "path" "path/filepath" "strings" "sync" "cursor/internal/appdata" "cursor/internal/securefile" ) var operationMu sync.Mutex func checkManagedPaths(project, name string) error { paths := []string{filepath.Join(project, ".cursor"), skillsDir(project), skillTargetDir(project, name), projectCommandsDir(project)} for _, file := range commandFileNames(name) { paths = append(paths, filepath.Join(projectCommandsDir(project), file)) } for _, entry := range paths { info, err := os.Lstat(entry) if os.IsNotExist(err) { continue } if err != nil { return err } if info.Mode()&os.ModeSymlink != 0 { return fmt.Errorf("安全保护:拒绝通过符号链接修改 %s", entry) } } target := skillTargetDir(project, name) if _, err := os.Stat(target); os.IsNotExist(err) { return nil } return filepath.WalkDir(target, func(p string, d fs.DirEntry, err error) error { if err != nil { return err } if d.Type()&os.ModeSymlink != 0 { return fmt.Errorf("安全保护:skill 内含符号链接 %s", p) } return nil }) } func backupBeforeReplace(target string, next []byte) error { previous, err := os.ReadFile(target) if os.IsNotExist(err) { return nil } if err != nil { return err } if bytes.Equal(previous, next) { return nil } id := fmt.Sprintf("%x", sha256.Sum256([]byte(target))) dir := filepath.Join(appdata.DataRootPath(), "skill-backups", id[:16]) if err = securefile.PrivateDir(dir); err != nil { return err } file, err := os.CreateTemp(dir, filepath.Base(target)+"-*.bak") if err != nil { return err } name := file.Name() if err = file.Close(); err != nil { return err } return securefile.Write(name, previous) } func ensureNoCustomSkillFiles(project, name string) error { allowed := map[string]bool{} err := fs.WalkDir(builtinFS, path.Join(builtinRoot, name), func(p string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { rel := strings.TrimPrefix(p, path.Join(builtinRoot, name)+"/") allowed[rel] = true } return nil }) if err != nil { return err } for _, file := range retiredSkillFiles[name] { allowed[file] = true } target := skillTargetDir(project, name) if _, err = os.Stat(target); os.IsNotExist(err) { return nil } return filepath.WalkDir(target, func(p string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { rel, _ := filepath.Rel(target, p) if !allowed[filepath.ToSlash(rel)] { return fmt.Errorf("skill 含自建文件 %s,已保留;请先备份后手动移除", strings.TrimPrefix(p, target)) } } return nil }) }