mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 21:39:54 +02:00
Bug 1: detect copilot.cmd/.bat on Windows and invoke the sibling .ps1 directly via powershell -File, bypassing cmd.exe %* re-tokenisation that mangled the multi-line -p prompt. Shared rewriteCmdToPS1() now serves cursor, pi, and copilot. Bug 2: filterCustomArgs (shared by all agent backends) strips one outer layer of shell quotes via unshellQuoteArg() before processing, so shell-style custom args like --deny-tool='write' no longer reach the CLI with literal quotes.
86 lines
2.8 KiB
Go
86 lines
2.8 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
|
|
|
|
// rewriteCmdToPS1 handles the .cmd → PowerShell rewrite for npm-installed
|
|
// agent CLIs on Windows. Each CLI ships a <name>.cmd launcher whose body is
|
|
// effectively
|
|
//
|
|
// powershell -NoProfile -ExecutionPolicy Bypass -File <name>.ps1 %*
|
|
//
|
|
// Going through cmd.exe causes %* re-expansion to re-tokenise the raw
|
|
// command-line string, which mangles multi-line arguments (most critically
|
|
// the -p prompt). We instead invoke PowerShell with -File <name>.ps1
|
|
// directly so Go passes each argv element as a discrete token.
|
|
//
|
|
// toolName is used both for the log message and to derive the canonical ps1
|
|
// filename (toolName + ".ps1" in the same directory as lookedUp). Using the
|
|
// canonical name rather than the launcher basename ensures we find the right
|
|
// script even when the user has a custom .bat wrapper with a different name.
|
|
func rewriteCmdToPS1(toolName, lookedUp string, args []string, logger *slog.Logger) (string, []string, bool) {
|
|
ext := strings.ToLower(filepath.Ext(lookedUp))
|
|
if ext != ".cmd" && ext != ".bat" {
|
|
return "", nil, false
|
|
}
|
|
ps1 := filepath.Join(filepath.Dir(lookedUp), toolName+".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(toolName+": routing through powershell -File to preserve argv tokens",
|
|
"powershell", psExe,
|
|
"ps1", ps1,
|
|
"original", lookedUp,
|
|
)
|
|
}
|
|
return psExe, full, true
|
|
}
|
|
|
|
// platformCursorInvocation rewrites cursor-agent.cmd → PowerShell -File
|
|
// cursor-agent.ps1 on Windows to avoid cmd.exe %* re-tokenisation.
|
|
func platformCursorInvocation(lookedUp string, args []string, logger *slog.Logger) (string, []string, bool) {
|
|
return rewriteCmdToPS1("cursor-agent", lookedUp, args, logger)
|
|
}
|
|
|
|
// 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
|
|
}
|