package bridge import ( "fmt" "os" "os/exec" "path/filepath" "strings" goruntime "runtime" "cursor/internal/cursor" "cursor/internal/logger" ) // CLITerminalResult 返回打开 CLI 终端的结果。 type CLITerminalResult struct { Ok bool `json:"ok"` Error string `json:"error"` WrapperDir string `json:"wrapperDir"` } // cliTerminalInitFilename 是 Windows 终端初始化批处理脚本的文件名。 const cliTerminalInitFilename = "cli-terminal-init.bat" // extractEnvSetupLines 从 wrapper 脚本提取环境变量设置行。 // wrapper 是 CLI 环境配置的唯一来源,终端初始化脚本从中派生, // 避免两套独立的环境变量配置逻辑产生不一致。 func extractEnvSetupLines(wrapperPath string) ([]string, error) { data, err := os.ReadFile(wrapperPath) if err != nil { return nil, err } var lines []string for _, line := range strings.Split(string(data), "\n") { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "set \"") || // Windows: set "KEY=VALUE" strings.HasPrefix(trimmed, "export ") || // Unix: export KEY="VALUE" strings.HasPrefix(trimmed, "unset ") { // Unix: unset KEY lines = append(lines, trimmed) } } return lines, nil } // OpenCLITerminal 检测终端并打开一个预配置 wrapper 环境的终端窗口。 // // 行为: // - 检测 CLI 版 cursor-agent 是否已安装 // - 检测 wrapper 是否已生成(服务启动时自动生成) // - 以管理员/elevated 权限启动终端 // - 终端启动时自动设置 wrapper 所需的环境变量 // - 不自动执行 cursor-agent 命令(用户需自行 cd 到项目目录后调用) func (s *WindowService) OpenCLITerminal() (result CLITerminalResult) { logServiceCall("window", "open_cli_terminal") defer func() { if result.Ok { logServiceResult("window", "open_cli_terminal", nil) return } logServiceResult("window", "open_cli_terminal", fmt.Errorf("%s", result.Error)) }() cliPath := cursor.DetectCLIInstallPath() if cliPath == "" { return CLITerminalResult{ Ok: false, Error: "未检测到 CLI 版 cursor-agent,请先安装", } } if !cursor.CLIWrapperExists() { return CLITerminalResult{ Ok: false, Error: "CLI wrapper 尚未生成,请先启动服务", } } wrapperDir := cursor.CLIWrapperDir() if err := openElevatedTerminal(wrapperDir); err != nil { logger.Errorf("openCLITerminal failed: %v", err) return CLITerminalResult{ Ok: false, Error: err.Error(), WrapperDir: wrapperDir, } } return CLITerminalResult{ Ok: true, WrapperDir: wrapperDir, } } // openElevatedTerminal 以管理员/elevated 权限启动终端, // 并通过 shell 初始化脚本预设 wrapper 环境变量。 func openElevatedTerminal(workDir string) error { switch detectOS() { case "windows": return openWindowsTerminal(workDir) case "darwin": return openDarwinTerminal(workDir) default: return openLinuxTerminal(workDir) } } // ── Windows ── func openWindowsTerminal(workDir string) error { // 生成 batch 文件设置环境变量,避免 PowerShell 内联引号转义问题 batchPath, err := writeWindowsEnvBatch(workDir) if err != nil { return fmt.Errorf("生成终端初始化脚本失败: %w", err) } // 优先使用 Windows Terminal (wt.exe),回退到 cmd.exe // batch 文件内部已 cd /d 到工作目录,无需 -WorkingDirectory 参数 wtPath, wtErr := exec.LookPath("wt.exe") if wtErr == nil { args := []string{ "-NoProfile", "-Command", fmt.Sprintf( "Start-Process -FilePath '%s' -ArgumentList @('-d','%s','cmd.exe','/k','%s') -Verb RunAs", wtPath, workDir, batchPath, ), } return exec.Command("powershell", args...).Start() } // 回退到 cmd.exe,以管理员启动 args := []string{ "-NoProfile", "-Command", fmt.Sprintf( "Start-Process -FilePath 'cmd.exe' -ArgumentList @('/k','%s') -Verb RunAs", batchPath, ), } return exec.Command("powershell", args...).Start() } // writeWindowsEnvBatch 生成 batch 文件设置 wrapper 环境变量。 // 环境变量配置直接从 wrapper 脚本提取,确保与 wrapper 保持一致。 func writeWindowsEnvBatch(workDir string) (string, error) { envLines, err := extractEnvSetupLines(cursor.CLIWrapperPath()) if err != nil { return "", fmt.Errorf("读取 wrapper 环境配置失败: %w", err) } var b strings.Builder b.WriteString("@echo off\n") for _, line := range envLines { b.WriteString(line) b.WriteByte('\n') } fmt.Fprintf(&b, "cd /d \"%s\"\n", workDir) b.WriteString("echo CLI environment initialized. cd to your project and run: cursor-agent\n") b.WriteString("title Local-Cursor CLI Terminal\n") batchPath := filepath.Join(cursor.CLIWrapperDir(), cliTerminalInitFilename) if err := os.WriteFile(batchPath, []byte(b.String()), 0o644); err != nil { return "", fmt.Errorf("写入终端初始化脚本失败: %w", err) } return batchPath, nil } // ── macOS ── func openDarwinTerminal(workDir string) error { envLines, err := extractEnvSetupLines(cursor.CLIWrapperPath()) if err != nil { return fmt.Errorf("读取 wrapper 环境配置失败: %w", err) } parts := []string{fmt.Sprintf("cd '%s'", workDir)} parts = append(parts, envLines...) parts = append(parts, "echo 'CLI environment initialized. cd to your project and run: cursor-agent'") initCmd := strings.Join(parts, " && ") // 用 osascript 以管理员权限打开 Terminal 并执行初始化 script := fmt.Sprintf(` do shell script "osascript -e 'tell application \"Terminal\" to do script \"%s\"' with administrator privileges" with prompt "Local-Cursor needs to open a terminal with administrator privileges" `, escapeAppleScript(initCmd)) return exec.Command("osascript", "-e", script).Start() } // escapeAppleScript 转义 AppleScript 字符串中的特殊字符。 func escapeAppleScript(s string) string { s = strings.ReplaceAll(s, `\`, `\\`) s = strings.ReplaceAll(s, `"`, `\"`) return s } // ── Linux ── func openLinuxTerminal(workDir string) error { terminals := []string{"gnome-terminal", "konsole", "xterm", "xfce4-terminal", "alacritty", "kitty"} var terminalPath string for _, t := range terminals { if p, err := exec.LookPath(t); err == nil { terminalPath = p break } } if terminalPath == "" { return fmt.Errorf("未检测到可用的终端模拟器") } envLines, err := extractEnvSetupLines(cursor.CLIWrapperPath()) if err != nil { return fmt.Errorf("读取 wrapper 环境配置失败: %w", err) } parts := []string{fmt.Sprintf("cd '%s'", workDir)} parts = append(parts, envLines...) parts = append(parts, "echo 'CLI environment initialized. cd to your project and run: cursor-agent'") parts = append(parts, "exec bash") initCmd := strings.Join(parts, " && ") // 用 pkexec 以管理员权限启动终端,不同终端的参数不同 var cmd *exec.Cmd switch filepath.Base(terminalPath) { case "gnome-terminal": cmd = exec.Command("pkexec", terminalPath, "--", "bash", "-c", initCmd) case "konsole", "xfce4-terminal": cmd = exec.Command("pkexec", terminalPath, "-e", "bash", "-c", initCmd) case "xterm": cmd = exec.Command("pkexec", terminalPath, "-e", "bash", "-c", initCmd) case "alacritty": cmd = exec.Command("pkexec", terminalPath, "-e", "bash", "-c", initCmd) case "kitty": cmd = exec.Command("pkexec", terminalPath, "bash", "-c", initCmd) default: cmd = exec.Command("pkexec", terminalPath, "-e", "bash", "-c", initCmd) } return cmd.Start() } // detectOS 返回当前操作系统名称。 func detectOS() string { switch goruntime.GOOS { case "windows": return "windows" case "darwin": return "darwin" default: return "linux" } }