mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 00:02:29 +00:00
fix(startup): read a nushell login PATH from $env.PATH
Use Nushell login command syntax and preserve the actual login PATH value. Related: https://github.com/stablyai/orca/pull/22677 Co-authored-by: Kh05ifr4nD <meandSSH0219@gmail.com> Co-authored-by: Neil <neil@stably.ai>
This commit is contained in:
@@ -238,6 +238,7 @@
|
||||
"../src/main/openclaude/hook-service.ts",
|
||||
"../src/main/rolling-file-backup.ts",
|
||||
"../src/main/startup/hydrate-shell-path.ts",
|
||||
"../src/main/startup/shell-path-probe.ts",
|
||||
"../src/main/startup/windows-shell-path-ownership.ts",
|
||||
// Why: serve-electron-flag-parity.test.ts checks the Electron-side serve argv rewrite against this
|
||||
// project's serve spec; the module has no imports, so listing it pulls in nothing else.
|
||||
|
||||
@@ -210,6 +210,52 @@ describe('hydrateShellPath', () => {
|
||||
expect((await captureProbeEnv()).ORCA_SHELL_PATH_PROBE).toBe('1')
|
||||
})
|
||||
|
||||
// Why: on Windows the probe is PowerShell or Git Bash, so these argv are never produced.
|
||||
it.skipIf(process.platform === 'win32').each(['/bin/bash', '/bin/zsh'])(
|
||||
'keeps the POSIX login probe for %s',
|
||||
async (shell) => {
|
||||
await captureProbeEnv(shell)
|
||||
|
||||
expect(spawnMock.mock.calls[0][1]).toEqual([
|
||||
'-ilc',
|
||||
`printf '%s' '__ORCA_SHELL_PATH__'; printf '%s' "$PATH"; printf '%s' '__ORCA_SHELL_PATH__'`
|
||||
])
|
||||
}
|
||||
)
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'reads Nushell PATH while preserving interactive login configuration',
|
||||
async () => {
|
||||
const proc = createMockShellProcess()
|
||||
spawnMock.mockReturnValue(proc)
|
||||
const resultPromise = hydrateShellPath({
|
||||
shellOverride: '/run/current-system/sw/bin/nu',
|
||||
force: true
|
||||
})
|
||||
await vi.waitFor(() => expect(spawnMock).toHaveBeenCalled())
|
||||
|
||||
expect(spawnMock.mock.calls[0][1]).toEqual([
|
||||
'-ilc',
|
||||
[
|
||||
"print -n '__ORCA_SHELL_PATH__';",
|
||||
'print -n ($env.PATH | str join (char esep));',
|
||||
"print -n '__ORCA_SHELL_PATH__'"
|
||||
].join(' ')
|
||||
])
|
||||
proc.stdout.emit(
|
||||
'data',
|
||||
Buffer.from('__ORCA_SHELL_PATH__/etc/profiles/per-user/me/bin:/usr/bin__ORCA_SHELL_PATH__')
|
||||
)
|
||||
proc.emit('close', 0)
|
||||
|
||||
await expect(resultPromise).resolves.toEqual({
|
||||
segments: ['/etc/profiles/per-user/me/bin', '/usr/bin'],
|
||||
ok: true,
|
||||
failureReason: 'none'
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('overwrites the captured key in place so Windows never carries both Path and PATH', async () => {
|
||||
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
|
||||
const originalWindowsPath = process.env.Path
|
||||
|
||||
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process'
|
||||
import { delimiter, win32 as pathWin32 } from 'node:path'
|
||||
import type { ShellHydrationFailureReason } from '../../shared/shell-path-hydration-types'
|
||||
import { resolveWindowsShellStartupFamily } from '../../shared/windows-terminal-shell'
|
||||
import { SHELL_PATH_DELIMITER as DELIMITER, shellPathProbe } from './shell-path-probe'
|
||||
import { WindowsShellPathOwnership, createWindowsPathKey } from './windows-shell-path-ownership'
|
||||
|
||||
// Why: GUI-launched Electron can miss PATH entries added by shell profiles.
|
||||
@@ -11,7 +12,6 @@ import { WindowsShellPathOwnership, createWindowsPathKey } from './windows-shell
|
||||
//
|
||||
// Probe the profile-loading shell once instead of hard-coding every tool's install path.
|
||||
|
||||
const DELIMITER = '__ORCA_SHELL_PATH__'
|
||||
// Why 10s: 5s was chosen without measurement and a real profile overruns it —
|
||||
// a bash -ilc loading nvm, rvm, conda and gcloud measures ~1s idle but 6-7s on a
|
||||
// loaded machine, so a cold start under load silently fell back to the seeded
|
||||
@@ -179,24 +179,7 @@ export function runWithLaunchPath<T>(action: () => T): T {
|
||||
}
|
||||
}
|
||||
|
||||
function shellPathProbe(shell: string): { args: string[]; pathDelimiter: string } {
|
||||
if (process.platform !== 'win32') {
|
||||
const command = `printf '%s' '${DELIMITER}'; printf '%s' "$PATH"; printf '%s' '${DELIMITER}'`
|
||||
return { args: ['-ilc', command], pathDelimiter: delimiter }
|
||||
}
|
||||
if (resolveWindowsShellStartupFamily(shell) === 'posix') {
|
||||
// Why: native child processes cannot resolve Git Bash's /c/... PATH entries.
|
||||
const command = `printf '%s' '${DELIMITER}'; cygpath -wp "$PATH"; printf '%s' '${DELIMITER}'`
|
||||
return { args: ['-ilc', command], pathDelimiter: ';' }
|
||||
}
|
||||
const command =
|
||||
`[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); ` +
|
||||
`[Console]::Write('${DELIMITER}'); [Console]::Write($env:Path); ` +
|
||||
`[Console]::Write('${DELIMITER}')`
|
||||
// Why: omitting -NoProfile is the behavior this probe exists to capture.
|
||||
return { args: ['-NoLogo', '-Command', command], pathDelimiter: ';' }
|
||||
}
|
||||
|
||||
/** Capture the login PATH without profile banners. */
|
||||
function spawnShellAndReadPath(shell: string): Promise<HydrationResult> {
|
||||
return new Promise((resolve) => {
|
||||
// Why: delimiters isolate PATH from profile banners and MOTDs.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { delimiter, win32 as pathWin32 } from 'node:path'
|
||||
import { resolveWindowsShellStartupFamily } from '../../shared/windows-terminal-shell'
|
||||
|
||||
export const SHELL_PATH_DELIMITER = '__ORCA_SHELL_PATH__'
|
||||
|
||||
/** Build the login PATH probe in the selected shell's native syntax. */
|
||||
export function shellPathProbe(shell: string): { args: string[]; pathDelimiter: string } {
|
||||
if (process.platform !== 'win32') {
|
||||
if (pathWin32.basename(shell).toLowerCase() === 'nu') {
|
||||
// Why: Nushell's PATH is a list; POSIX "$PATH" prints a literal instead.
|
||||
const command = [
|
||||
`print -n '${SHELL_PATH_DELIMITER}';`,
|
||||
'print -n ($env.PATH | str join (char esep));',
|
||||
`print -n '${SHELL_PATH_DELIMITER}'`
|
||||
].join(' ')
|
||||
// Why: existing interactive login flags preserve $nu.is-interactive configuration.
|
||||
return { args: ['-ilc', command], pathDelimiter: delimiter }
|
||||
}
|
||||
const command =
|
||||
`printf '%s' '${SHELL_PATH_DELIMITER}'; printf '%s' "$PATH"; ` +
|
||||
`printf '%s' '${SHELL_PATH_DELIMITER}'`
|
||||
return { args: ['-ilc', command], pathDelimiter: delimiter }
|
||||
}
|
||||
if (resolveWindowsShellStartupFamily(shell) === 'posix') {
|
||||
// Why: native child processes cannot resolve Git Bash's /c/... PATH entries.
|
||||
const command =
|
||||
`printf '%s' '${SHELL_PATH_DELIMITER}'; cygpath -wp "$PATH"; ` +
|
||||
`printf '%s' '${SHELL_PATH_DELIMITER}'`
|
||||
return { args: ['-ilc', command], pathDelimiter: ';' }
|
||||
}
|
||||
const command =
|
||||
`[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); ` +
|
||||
`[Console]::Write('${SHELL_PATH_DELIMITER}'); [Console]::Write($env:Path); ` +
|
||||
`[Console]::Write('${SHELL_PATH_DELIMITER}')`
|
||||
// Why: omitting -NoProfile is the behavior this probe exists to capture.
|
||||
return { args: ['-NoLogo', '-Command', command], pathDelimiter: ';' }
|
||||
}
|
||||
Reference in New Issue
Block a user