Files
Local-Cursor/internal/bridge/cli_terminal.go
FuDie0915 db73cf5a39 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
2026-11-10 01:24:28 +08:00

252 lines
7.6 KiB
Go
Raw Permalink 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 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"
}
}