mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 21:39:54 +02:00
On Windows the official cursor-agent installer ships cursor-agent.cmd whose body is `powershell ... -File cursor-agent.ps1 %*`. CreateProcess for a .cmd file goes through cmd.exe, and `%*` in a batch file is expanded by re-tokenising the original command line, which mangles arguments containing newlines or other whitespace - most notably a long, multi-line `-p <prompt>`. The agent then only sees a truncated prompt and fails with "Workspace Trust Required" or exits 1 immediately. When LookPath resolves cursor-agent to a .cmd/.bat launcher and a sibling cursor-agent.ps1 exists, invoke PowerShell directly with `-File <ps1>` so Go's os/exec passes each argv as a discrete token. This is exactly what the .cmd does internally; we just skip the cmd.exe re-tokenisation step. PowerShell host resolution prefers pwsh.exe (PS 7) on PATH, then powershell.exe on PATH, and finally falls back to %SystemRoot%\System32\WindowsPowerShell\v1.0. Platform-specific code is split via build tags (cursor_invocation_windows.go / cursor_invocation_other.go) so non-Windows builds carry no Windows-only dependencies. The lookup is exposed as a package variable to make the Windows path fully unit-testable without spawning real PowerShell. Five unit tests cover: passthrough on non-launcher targets, successful rewrite with a multi-line prompt, .exe direct launch (skip), missing .ps1 (skip), and missing PowerShell host (skip). The change leaves macOS / Linux behaviour entirely untouched and stays on the official cursor-agent launch chain - no node.exe direct invocation, no prompt mutation, no extra flags. Closes #1297 Made-with: Cursor
81 lines
2.3 KiB
Go
81 lines
2.3 KiB
Go
//go:build windows
|
|
|
|
package agent
|
|
|
|
import (
|
|
"log/slog"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// powerShellLookup resolves the PowerShell host to use. It is overridable in
|
|
// tests; production callers should leave it at its default.
|
|
var powerShellLookup = defaultPowerShellLookup
|
|
|
|
// platformCursorInvocation rewrites the cursor-agent invocation on Windows
|
|
// when the resolved executable is the official cursor-agent.cmd launcher
|
|
// (or a .bat alias) that delegates to cursor-agent.ps1.
|
|
//
|
|
// We replace
|
|
//
|
|
// cursor-agent.cmd <args...>
|
|
//
|
|
// with
|
|
//
|
|
// powershell.exe -NoProfile -ExecutionPolicy Bypass -File cursor-agent.ps1 <args...>
|
|
//
|
|
// which is exactly what the .cmd does internally, but lets Go pass each arg
|
|
// as a discrete token instead of routing through cmd.exe's %* re-expansion
|
|
// (which mangles multi-line / whitespace-heavy prompts such as a long -p).
|
|
func platformCursorInvocation(lookedUp string, args []string, logger *slog.Logger) (string, []string, bool) {
|
|
ext := strings.ToLower(filepath.Ext(lookedUp))
|
|
if ext != ".cmd" && ext != ".bat" {
|
|
return "", nil, false
|
|
}
|
|
dir := filepath.Dir(lookedUp)
|
|
ps1 := filepath.Join(dir, "cursor-agent.ps1")
|
|
if st, err := os.Stat(ps1); err != nil || st.IsDir() {
|
|
return "", nil, false
|
|
}
|
|
|
|
psExe, ok := powerShellLookup()
|
|
if !ok {
|
|
return "", nil, false
|
|
}
|
|
|
|
full := make([]string, 0, 5+len(args))
|
|
full = append(full, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ps1)
|
|
full = append(full, args...)
|
|
|
|
if logger != nil {
|
|
logger.Info("cursor-agent: routing through powershell -File to preserve argv tokens",
|
|
"powershell", psExe,
|
|
"ps1", ps1,
|
|
"original", lookedUp,
|
|
)
|
|
}
|
|
return psExe, full, true
|
|
}
|
|
|
|
// defaultPowerShellLookup prefers PowerShell on PATH (PowerShell 7's pwsh.exe
|
|
// or any user-overridden powershell.exe) and falls back to the system path
|
|
// shipped with Windows.
|
|
func defaultPowerShellLookup() (string, bool) {
|
|
for _, name := range []string{"pwsh.exe", "powershell.exe"} {
|
|
if p, err := exec.LookPath(name); err == nil {
|
|
return p, true
|
|
}
|
|
}
|
|
root := os.Getenv("SystemRoot")
|
|
if root == "" {
|
|
root = `C:\Windows`
|
|
}
|
|
candidate := filepath.Join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
|
|
if st, err := os.Stat(candidate); err == nil && !st.IsDir() {
|
|
return candidate, true
|
|
}
|
|
return "", false
|
|
}
|