mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(wsl): open WSL paths in Cursor's WSL mode; ignore Windows-side CLIs in WSL hook setup (#27681)
* fix(editor): open WSL worktrees in Cursor as WSL remote windows Cursor received the raw WSL UNC path and opened it as a local Windows folder. Its CLI accepts --remote wsl+<distro> like VS Code, so route WSL UNC paths through the same remote args while keeping --new-window. * fix(wsl): ignore Windows-mounted CLIs in guest hook agent detection WSL appends the Windows PATH, so the guest relay's agent lookup reported a Windows-only npm shim (e.g. /mnt/c/.../npm/codex) as installed in the distro and installed hooks for it. Reuse the Windows-mount skip that host-side WSL detection already applies; fish shells now hand their login PATH to the same POSIX walk.
This commit is contained in:
@@ -405,14 +405,44 @@ describe('resolveExternalEditorLaunchSpec', () => {
|
||||
}
|
||||
)
|
||||
|
||||
it.each([
|
||||
['C:\\Tools\\cursor.exe', '\\\\wsl.localhost\\Ubuntu\\home\\ada\\project', 'wsl+Ubuntu'],
|
||||
[
|
||||
'C:\\Users\\me\\AppData\\Local\\Programs\\cursor\\resources\\app\\bin\\cursor.cmd',
|
||||
'\\\\wsl$\\Debian\\home\\ada\\project',
|
||||
'wsl+Debian'
|
||||
]
|
||||
])('opens WSL UNC workspaces with %s in a new WSL remote window', (command, pathValue, auth) => {
|
||||
expect(resolveExternalEditorLaunchSpec(command, pathValue, { platform: 'win32' })).toEqual({
|
||||
kind: 'executable',
|
||||
hideWindowsConsole: true,
|
||||
spawnCmd: command,
|
||||
spawnArgs: ['--new-window', '--remote', auth, '/home/ada/project']
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['darwin', 'linux'] as const)(
|
||||
'keeps WSL-looking Cursor paths local on %s',
|
||||
(platform) => {
|
||||
const pathValue = '\\\\wsl.localhost\\Ubuntu\\home\\ada\\project'
|
||||
expect(resolveExternalEditorLaunchSpec('cursor', pathValue, { platform }).spawnArgs).toEqual([
|
||||
'--new-window',
|
||||
pathValue
|
||||
])
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps non-WSL Windows paths local for Cursor', () => {
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec('C:\\Tools\\cursor.exe', '\\\\server\\share\\project', {
|
||||
platform: 'win32'
|
||||
}).spawnArgs
|
||||
).toEqual(['--new-window', '\\\\server\\share\\project'])
|
||||
})
|
||||
|
||||
it('does not add VS Code remote arguments to other editors', () => {
|
||||
const pathValue = '\\\\wsl.localhost\\Ubuntu\\home\\ada\\project'
|
||||
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec('C:\\Tools\\cursor.exe', pathValue, {
|
||||
platform: 'win32'
|
||||
}).spawnArgs
|
||||
).toEqual(['--new-window', pathValue])
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec('C:\\Tools\\codium.exe', pathValue, {
|
||||
platform: 'win32'
|
||||
|
||||
@@ -98,20 +98,25 @@ function buildExecutableArgs(
|
||||
pathValue: string,
|
||||
platform: NodeJS.Platform
|
||||
): string[] {
|
||||
const launcherBaseName = getLauncherBaseName(editorCommand)
|
||||
if (launcherBaseName === 'cursor') {
|
||||
// Why: Cursor can route bare folder launches through the last active
|
||||
// workbench. A new window keeps "Open in Cursor" scoped to this worktree.
|
||||
return ['--new-window', pathValue]
|
||||
const isCursor = getLauncherBaseName(editorCommand) === 'cursor'
|
||||
const targetArgs = buildWslRemoteTargetArgs(editorCommand, pathValue, platform, isCursor)
|
||||
// Why: Cursor can route bare folder launches through the last active
|
||||
// workbench. A new window keeps "Open in Cursor" scoped to this worktree.
|
||||
return isCursor ? ['--new-window', ...targetArgs] : targetArgs
|
||||
}
|
||||
|
||||
function buildWslRemoteTargetArgs(
|
||||
editorCommand: string,
|
||||
pathValue: string,
|
||||
platform: NodeJS.Platform,
|
||||
isCursor: boolean
|
||||
): string[] {
|
||||
if (platform !== 'win32' || !(isCursor || isVsCodeLauncherExecutable(editorCommand))) {
|
||||
return [pathValue]
|
||||
}
|
||||
if (platform === 'win32' && isVsCodeLauncherExecutable(editorCommand)) {
|
||||
const wslPath = parseWslUncPath(pathValue)
|
||||
if (wslPath) {
|
||||
// Why: VS Code otherwise treats a WSL UNC path as a local Windows folder.
|
||||
return ['--remote', `wsl+${wslPath.distro}`, wslPath.linuxPath]
|
||||
}
|
||||
}
|
||||
return [pathValue]
|
||||
const wslPath = parseWslUncPath(pathValue)
|
||||
// Why: both editors otherwise treat a WSL UNC path as a local Windows folder.
|
||||
return wslPath ? ['--remote', `wsl+${wslPath.distro}`, wslPath.linuxPath] : [pathValue]
|
||||
}
|
||||
|
||||
function isCompoundShellCommand(command: string): boolean {
|
||||
|
||||
@@ -40,28 +40,20 @@ import {
|
||||
PreflightHandler
|
||||
} from './preflight-handler'
|
||||
|
||||
function lookupArgs(command: string, mode: '-lc' | '-ilc' = '-lc'): string[] {
|
||||
function shLookupScript(command: string): string {
|
||||
return [
|
||||
mode,
|
||||
[
|
||||
buildPosixCommandPathLookupScript({ kind: 'literal', value: command }),
|
||||
'if [ -n "$resolved" ]; then',
|
||||
'printf \'__ORCA_AGENT_PATH__%s\\n\' "$resolved"',
|
||||
'fi'
|
||||
].join('\n')
|
||||
]
|
||||
buildPosixCommandPathLookupScript(
|
||||
{ kind: 'literal', value: command },
|
||||
{ skipWindowsMountDirs: true }
|
||||
),
|
||||
'if [ -n "$resolved" ]; then',
|
||||
'printf \'__ORCA_AGENT_PATH__%s\\n\' "$resolved"',
|
||||
'fi'
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function fishLookupArgs(command: string): string[] {
|
||||
return [
|
||||
'-ilc',
|
||||
[
|
||||
`set -l resolved (command -v ${command} 2>/dev/null)`,
|
||||
'if test -n "$resolved"',
|
||||
'printf \'__ORCA_AGENT_PATH__%s\\n\' "$resolved"',
|
||||
'end'
|
||||
].join('\n')
|
||||
]
|
||||
function lookupArgs(command: string, mode: '-lc' | '-ilc' = '-lc'): string[] {
|
||||
return [mode, shLookupScript(command)]
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -102,10 +94,18 @@ describe('buildCommandLookupSpecs', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('uses fish syntax for trusted fish shells', () => {
|
||||
it('skips Windows-mounted PATH dirs so a Windows-only shim is not a guest install', () => {
|
||||
const [shellSpec] = buildCommandLookupSpecs('codex', 'linux', { SHELL: '/bin/bash' }, null)
|
||||
expect(shellSpec.args[1]).toContain('/proc/mounts')
|
||||
expect(shellSpec.args[1]).toBe(shLookupScript('codex'))
|
||||
})
|
||||
|
||||
it('runs the POSIX lookup under the fish login PATH for trusted fish shells', () => {
|
||||
const script = shLookupScript('codex')
|
||||
const fishQuoted = `'${script.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
||||
expect(buildCommandLookupSpecs('codex', 'linux', { SHELL: '/usr/bin/fish' }, null)[0]).toEqual({
|
||||
file: '/usr/bin/fish',
|
||||
args: fishLookupArgs("'codex'")
|
||||
args: ['-ilc', `exec /bin/sh -c ${fishQuoted}`]
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -268,7 +268,12 @@ function buildPosixCommandLookupSpec(command: string, shell: string): CommandLoo
|
||||
function buildShCommandLookupScript(command: string): string {
|
||||
// Why: login shells may define aliases or functions that mask the PATH executable.
|
||||
return [
|
||||
buildPosixCommandPathLookupScript({ kind: 'literal', value: command }),
|
||||
// Why: WSL appends the Windows PATH, so a Windows-only npm shim would
|
||||
// otherwise read as a guest install; same skip as host-side WSL detection.
|
||||
buildPosixCommandPathLookupScript(
|
||||
{ kind: 'literal', value: command },
|
||||
{ skipWindowsMountDirs: true }
|
||||
),
|
||||
'if [ -n "$resolved" ]; then',
|
||||
`printf '${AGENT_PATH_PREFIX}%s\\n' "$resolved"`,
|
||||
'fi'
|
||||
@@ -276,13 +281,8 @@ function buildShCommandLookupScript(command: string): string {
|
||||
}
|
||||
|
||||
function buildFishCommandLookupScript(command: string): string {
|
||||
const quotedCommand = shellQuote(command)
|
||||
return [
|
||||
`set -l resolved (command -v ${quotedCommand} 2>/dev/null)`,
|
||||
'if test -n "$resolved"',
|
||||
`printf '${AGENT_PATH_PREFIX}%s\\n' "$resolved"`,
|
||||
'end'
|
||||
].join('\n')
|
||||
// Why: fish only supplies its login PATH; the POSIX walk owns the Windows-mount skip.
|
||||
return `exec /bin/sh -c ${fishQuote(buildShCommandLookupScript(command))}`
|
||||
}
|
||||
|
||||
function resolveAccountLoginShell(
|
||||
@@ -327,6 +327,7 @@ function getShellCommandMode(shell: string): '-lc' | '-ilc' {
|
||||
return shellName === 'sh' || shellName === 'dash' ? '-lc' : '-ilc'
|
||||
}
|
||||
|
||||
function shellQuote(value: string): string {
|
||||
return `'${value.replace(/'/g, "'\\''")}'`
|
||||
// Fish single quotes treat only `\\` and `\'` as escapes.
|
||||
function fishQuote(value: string): string {
|
||||
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user