chore: import Local-Cursor v1.1.8 source snapshot

Includes the current working directory changes. Historical commits and release binaries are excluded to limit Gitea storage.
Source base: d7515973fbfd6dd2001fa156c483455b8081c7cb
This commit is contained in:
FuDie0915
2026-11-10 01:24:28 +08:00
commit db73cf5a39
337 changed files with 147502 additions and 0 deletions

View File

@@ -0,0 +1,221 @@
//go:build windows
// Windows 版 Node 环境探测:GUI 进程往往没有继承 shell 注入的 Node 路径
// (fnm/nvm 等按 shell 修改 PATH),因此合并注册表 PATH 与常见安装目录。
package cursor
import (
"os"
"path/filepath"
"strings"
"cursor/internal/winexec"
"golang.org/x/sys/windows/registry"
)
// mergeNodeSearchPath 合并进程 PATH、注册表用户/系统 PATH 与常见 Node 安装目录。
// 返回合并后的 PATH,以及是否发现了额外的候选目录。
func mergeNodeSearchPath() (string, bool) {
parts := filepath.SplitList(os.Getenv("PATH"))
seen := make(map[string]struct{}, len(parts))
for _, p := range parts {
seen[strings.ToLower(strings.TrimSpace(p))] = struct{}{}
}
found := false
add := func(dir string) {
dir = strings.TrimSpace(dir)
if dir == "" {
return
}
if info, err := os.Stat(dir); err != nil || !info.IsDir() {
return
}
key := strings.ToLower(dir)
if _, exists := seen[key]; exists {
return
}
seen[key] = struct{}{}
parts = append(parts, dir)
found = true
}
// 注册表中的用户与系统 PATH(GUI 进程可能未刷新)。
for _, source := range []struct {
root registry.Key
path string
}{
{registry.CURRENT_USER, `Environment`},
{registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Session Manager\Environment`},
} {
value, err := registryExpandString(source.root, source.path, "Path")
if err != nil {
continue
}
for _, dir := range filepath.SplitList(value) {
add(dir)
probeFnmDataDirs(dir, add)
}
}
// 常见 Node 安装目录兜底(fnm/nvm/volta/scoop/官方安装器)。
homeDir, _ := os.UserHomeDir()
localAppData := os.Getenv("LOCALAPPDATA")
fnmDir := os.Getenv("FNM_DIR")
candidates := []string{
filepath.Join(os.Getenv("ProgramFiles"), "nodejs"),
filepath.Join(localAppData, "Programs", "nodejs"),
filepath.Join(homeDir, "AppData", "Roaming", "nvm"),
filepath.Join(homeDir, "scoop", "apps", "nodejs", "current"),
filepath.Join(homeDir, ".volta", "bin"),
}
if localAppData != "" {
base := fnmDir
if base == "" {
base = filepath.Join(localAppData, "fnm")
}
// fnm:aliases/default 指向默认版本;node-versions 下枚举各版本。
candidates = append(candidates,
filepath.Join(base, "aliases", "default"),
filepath.Join(base, "node-versions"),
)
}
for _, dir := range candidates {
add(dir)
probeFnmDataDirs(dir, add)
if strings.EqualFold(filepath.Base(dir), "node-versions") {
if versions, err := os.ReadDir(dir); err == nil {
for _, version := range versions {
add(filepath.Join(dir, version.Name(), "installation"))
}
}
}
}
return strings.Join(parts, string(os.PathListSeparator)), found
}
// probeFnmDataDirs 在 PATH 候选目录中发现 fnm.exe 时,追加探测其数据目录下的
// node 版本(fnm 的 FNM_DIR 是 shell 级环境变量,GUI 进程通常拿不到)。
func probeFnmDataDirs(dir string, add func(string)) {
if _, err := os.Stat(filepath.Join(dir, "fnm.exe")); err != nil {
return
}
// 兼容两种布局:<root>\bin + <root>\data,或 <root>\aliases + <root>\node-versions。
for _, dataDir := range []string{
filepath.Join(filepath.Dir(dir), "data"),
dir,
} {
add(filepath.Join(dataDir, "aliases", "default"))
versionsDir := filepath.Join(dataDir, "node-versions")
if versions, err := os.ReadDir(versionsDir); err == nil {
for _, version := range versions {
add(filepath.Join(versionsDir, version.Name(), "installation"))
}
}
}
}
// registryExpandString 读取注册表字符串值并展开 %VAR% 引用。
func registryExpandString(root registry.Key, keyPath string, valueName string) (string, error) {
key, err := registry.OpenKey(root, keyPath, registry.QUERY_VALUE)
if err != nil {
return "", err
}
defer key.Close()
value, _, err := key.GetStringValue(valueName)
if err != nil {
return "", err
}
return os.ExpandEnv(value), nil
}
// detectCursorInstallPathFromRegistry 从 Windows 卸载项中查找 Cursor 安装位置。
func detectCursorInstallPathFromRegistry() string {
for _, source := range []struct {
root registry.Key
path string
}{
{registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Uninstall`},
{registry.LOCAL_MACHINE, `Software\Microsoft\Windows\CurrentVersion\Uninstall`},
{registry.LOCAL_MACHINE, `Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall`},
} {
root, err := registry.OpenKey(source.root, source.path, registry.QUERY_VALUE)
if err != nil {
continue
}
names, err := root.ReadSubKeyNames(-1)
root.Close()
if err != nil {
// 部分安全软件会拦截 Go 进程的注册表枚举,降级用 reg.exe 查询子键名。
names = regQuerySubKeyNames(registryKeyPath(source.root, source.path))
}
for _, name := range names {
sub, err := registry.OpenKey(source.root, source.path+`\`+name, registry.QUERY_VALUE)
if err != nil {
continue
}
// 安装器(如 Inno Setup)常用 GUID 作键名,DisplayName 在值里;
// 不含 cursor 的条目仅在 InstallLocation 指向有效 Cursor 目录时采用。
dir := readCursorInstallFromKey(sub)
sub.Close()
if dir != "" {
return dir
}
}
}
return ""
}
// registryKeyPath 返回 reg.exe 可识别的根键路径表示。
func registryKeyPath(root registry.Key, path string) string {
switch root {
case registry.CURRENT_USER:
return `HKCU\` + path
case registry.LOCAL_MACHINE:
return `HKLM\` + path
default:
return path
}
}
// regQuerySubKeyNames 通过 reg.exe 枚举子键名,作为 Go 注册表枚举被拦截时的降级方案。
func regQuerySubKeyNames(fullPath string) []string {
out, err := winexec.Command("reg", "query", fullPath).Output()
if err != nil {
return nil
}
prefix := strings.ToUpper(fullPath) + `\`
names := make([]string, 0, 16)
for _, line := range strings.Split(string(out), "\n") {
line = strings.TrimSpace(line)
if !strings.HasPrefix(strings.ToUpper(line), prefix) {
continue
}
name := strings.TrimSpace(line[len(prefix):])
if name != "" && !strings.Contains(name, " ") {
names = append(names, name)
}
}
return names
}
// readCursorInstallFromKey 从单个卸载项读取并校验安装目录。
func readCursorInstallFromKey(key registry.Key) string {
candidates := make([]string, 0, 2)
if value, _, err := key.GetStringValue("InstallLocation"); err == nil {
candidates = append(candidates, value)
}
if value, _, err := key.GetStringValue("DisplayIcon"); err == nil {
// DisplayIcon 形如 "D:\path\Cursor.exe" 或带 ",0" 后缀。
icon := strings.SplitN(value, ",", 2)[0]
icon = strings.Trim(icon, `"`)
candidates = append(candidates, filepath.Dir(icon))
}
for _, dir := range candidates {
if isCursorInstallDir(strings.TrimSpace(dir)) {
return filepath.Clean(strings.TrimSpace(dir))
}
}
return ""
}