fix(codex): use WSL login shell for account setup (#7993)
This commit is contained in:
parent
27f30c2548
commit
f55d753721
|
|
@ -15,6 +15,7 @@ import { tmpdir } from 'node:os'
|
|||
import { join } from 'node:path'
|
||||
import { PassThrough } from 'node:stream'
|
||||
import type { GlobalSettings } from '../../shared/types'
|
||||
import { buildWslCodexAvailabilityArgs, buildWslCodexLoginArgs } from './wsl-codex-command'
|
||||
|
||||
const testState = {
|
||||
userDataDir: '',
|
||||
|
|
@ -766,20 +767,20 @@ describe('CodexAccountService config sync', () => {
|
|||
if (script.includes('readlink -f')) {
|
||||
return `${wslLinuxHomePath}\n`
|
||||
}
|
||||
if (script.includes('command -v codex')) {
|
||||
throw new Error('bash -ic does not inherit the distro login-shell PATH')
|
||||
}
|
||||
if (args.slice(2, 5).join(' ') === '-- sh -c') {
|
||||
expect(args).toEqual(buildWslCodexAvailabilityArgs('Debian'))
|
||||
return ''
|
||||
}
|
||||
mkdirSync(wslManagedHomePath, { recursive: true })
|
||||
writeFileSync(join(wslManagedHomePath, '.orca-managed-home'), 'account-id-for-test\n')
|
||||
return ''
|
||||
})
|
||||
const spawnMock = vi.fn((command: string, args: string[]) => {
|
||||
expect(command).toBe('wsl.exe')
|
||||
expect(args).toEqual([
|
||||
'-d',
|
||||
'Debian',
|
||||
'--exec',
|
||||
'bash',
|
||||
'-ic',
|
||||
`export CODEX_HOME='${wslLinuxHomePath}'; exec codex login`
|
||||
])
|
||||
expect(args).toEqual(buildWslCodexLoginArgs('Debian', wslLinuxHomePath))
|
||||
// Why: codex login runs inside WSL, so the rewritten path must be the
|
||||
// Linux-side ~/.codex, not a Windows UNC path.
|
||||
expect(readFileSync(join(wslManagedHomePath, 'config.toml'), 'utf-8')).toBe(
|
||||
|
|
@ -876,8 +877,8 @@ describe('CodexAccountService config sync', () => {
|
|||
if (script.includes('readlink -f')) {
|
||||
return `${wslLinuxHomePath}\n`
|
||||
}
|
||||
if (script.includes('command -v codex')) {
|
||||
expect(args.slice(0, 5)).toEqual(['-d', 'Debian', '--exec', 'bash', '-ic'])
|
||||
if (args.slice(2, 5).join(' ') === '-- sh -c') {
|
||||
expect(args).toEqual(buildWslCodexAvailabilityArgs('Debian'))
|
||||
throw new Error('codex missing')
|
||||
}
|
||||
mkdirSync(wslManagedHomePath, { recursive: true })
|
||||
|
|
@ -960,14 +961,7 @@ describe('CodexAccountService config sync', () => {
|
|||
})
|
||||
const spawnMock = vi.fn((command: string, args: string[]) => {
|
||||
expect(command).toBe('wsl.exe')
|
||||
expect(args).toEqual([
|
||||
'-d',
|
||||
'Ubuntu',
|
||||
'--exec',
|
||||
'bash',
|
||||
'-ic',
|
||||
`export CODEX_HOME='${wslLinuxHomePath}'; exec codex login`
|
||||
])
|
||||
expect(args).toEqual(buildWslCodexLoginArgs('Ubuntu', wslLinuxHomePath))
|
||||
const child = new EventEmitter() as EventEmitter & {
|
||||
stdout: PassThrough
|
||||
stderr: PassThrough
|
||||
|
|
@ -1075,14 +1069,7 @@ describe('CodexAccountService config sync', () => {
|
|||
})
|
||||
const spawnMock = vi.fn((command: string, args: string[]) => {
|
||||
expect(command).toBe('wsl.exe')
|
||||
expect(args).toEqual([
|
||||
'-d',
|
||||
'Ubuntu',
|
||||
'--exec',
|
||||
'bash',
|
||||
'-ic',
|
||||
`export CODEX_HOME='${wslLinuxHomePath}'; exec codex login`
|
||||
])
|
||||
expect(args).toEqual(buildWslCodexLoginArgs('Ubuntu', wslLinuxHomePath))
|
||||
expect(readFileSync(join(wslManagedHomePath, '.orca-managed-home'), 'utf-8')).toBe(
|
||||
'account-1\n'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ import type { RateLimitService } from '../rate-limits/service'
|
|||
import { parseWslUncPath } from '../../shared/wsl-paths'
|
||||
import { toWindowsWslPath } from '../wsl'
|
||||
import { buildEncodedWslBashCommand } from '../wsl-bash-command'
|
||||
import {
|
||||
buildWslCodexAvailabilityArgs,
|
||||
buildWslCodexLoginArgs,
|
||||
WSL_CODEX_AVAILABILITY_TIMEOUT_MS
|
||||
} from './wsl-codex-command'
|
||||
import {
|
||||
getCodexSelectionTargetForAccount,
|
||||
getSelectedCodexAccountIdForTarget,
|
||||
|
|
@ -805,15 +810,7 @@ export class CodexAccountService {
|
|||
const spawnConfig = wslInfo
|
||||
? {
|
||||
command: 'wsl.exe',
|
||||
// Why: nvm and similar WSL installs often initialize PATH from interactive shell config.
|
||||
args: [
|
||||
'-d',
|
||||
wslInfo.distro,
|
||||
'--exec',
|
||||
'bash',
|
||||
'-ic',
|
||||
`export CODEX_HOME=${shellQuote(wslInfo.linuxPath)}; exec codex login`
|
||||
],
|
||||
args: buildWslCodexLoginArgs(wslInfo.distro, wslInfo.linuxPath),
|
||||
env: process.env,
|
||||
codexCommand: 'codex'
|
||||
}
|
||||
|
|
@ -923,18 +920,10 @@ export class CodexAccountService {
|
|||
|
||||
private assertWslCodexCliAvailable(wslInfo: { distro: string; linuxPath: string }): void {
|
||||
try {
|
||||
execFileSync(
|
||||
'wsl.exe',
|
||||
[
|
||||
'-d',
|
||||
wslInfo.distro,
|
||||
'--exec',
|
||||
'bash',
|
||||
'-ic',
|
||||
buildEncodedWslBashCommand('command -v codex >/dev/null 2>&1')
|
||||
],
|
||||
{ encoding: 'utf-8', timeout: 5000 }
|
||||
)
|
||||
execFileSync('wsl.exe', buildWslCodexAvailabilityArgs(wslInfo.distro), {
|
||||
encoding: 'utf-8',
|
||||
timeout: WSL_CODEX_AVAILABILITY_TIMEOUT_MS
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Codex CLI is not available in WSL ${wslInfo.distro}. Install Codex in that distro or switch Account location to Windows.`,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { buildWslCodexAvailabilityArgs, buildWslCodexLoginArgs } from './wsl-codex-command'
|
||||
|
||||
describe('WSL Codex commands', () => {
|
||||
it('checks the alias-neutral PATH from the distro login shell', () => {
|
||||
const args = buildWslCodexAvailabilityArgs('Ubuntu24-Dev')
|
||||
|
||||
expect(args.slice(0, 5)).toEqual(['-d', 'Ubuntu24-Dev', '--', 'sh', '-c'])
|
||||
expect(args.at(-1)).toContain('getent passwd')
|
||||
expect(args.at(-1)).toContain('_orca_lookup_command=')
|
||||
expect(args.at(-1)).toContain('codex')
|
||||
expect(args.at(-1)).not.toContain('bash -ic')
|
||||
})
|
||||
|
||||
it('launches the resolved Codex executable with its quoted managed home', () => {
|
||||
const args = buildWslCodexLoginArgs('Ubuntu', '/home/alice/managed-home')
|
||||
const command = args.at(-1)
|
||||
|
||||
expect(command).toContain('export CODEX_HOME=')
|
||||
expect(command).toContain('/home/alice/managed-home')
|
||||
expect(command).toContain('exec "\\$resolved" login')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { buildPosixCommandPathLookupScript } from '../../shared/posix-command-path-lookup'
|
||||
import {
|
||||
buildWslLoginShellCommand,
|
||||
escapeWslShCommandForWindows,
|
||||
quotePosixShell
|
||||
} from '../../shared/wsl-login-shell-command'
|
||||
|
||||
export const WSL_CODEX_AVAILABILITY_TIMEOUT_MS = 5_000
|
||||
|
||||
export function buildWslCodexAvailabilityArgs(distro: string): string[] {
|
||||
const command = [buildCodexPathLookup(), '[ -n "$resolved" ]'].join('\n')
|
||||
return buildWslCodexShellArgs(distro, command)
|
||||
}
|
||||
|
||||
export function buildWslCodexLoginArgs(distro: string, linuxHomePath: string): string[] {
|
||||
const command = [
|
||||
buildCodexPathLookup(),
|
||||
'if [ -z "$resolved" ]; then',
|
||||
" printf '%s\\n' 'Codex CLI not found in the WSL login-shell PATH.' >&2",
|
||||
' exit 127',
|
||||
'fi',
|
||||
`export CODEX_HOME=${quotePosixShell(linuxHomePath)}`,
|
||||
'exec "$resolved" login'
|
||||
].join('\n')
|
||||
return buildWslCodexShellArgs(distro, command)
|
||||
}
|
||||
|
||||
function buildCodexPathLookup(): string {
|
||||
return buildPosixCommandPathLookupScript({ kind: 'literal', value: 'codex' })
|
||||
}
|
||||
|
||||
function buildWslCodexShellArgs(distro: string, command: string): string[] {
|
||||
// Why: Codex must use the distro user's configured login shell, whose PATH
|
||||
// can differ from a hard-coded non-login bash invocation.
|
||||
return [
|
||||
'-d',
|
||||
distro,
|
||||
'--',
|
||||
'sh',
|
||||
'-c',
|
||||
escapeWslShCommandForWindows(buildWslLoginShellCommand(command))
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue