mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(windows): probe fnm's real default alias dir for Node fallback (#11359)
* fix(windows): probe fnm's real default alias dir for Node fallback On Windows, fnm stores the default alias under FNM_DIR, else %APPDATA%\fnm (legacy ~\.fnm), and node.exe sits directly in aliases\default with no bin subdir. Probe those dirs and append them to PATH so fnm's default only backfills a missing Node instead of shadowing an inherited one. Fixes #11356 Co-authored-by: niceSprite <16523864+iFwu@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> * test: keep onTestFinished callbacks returning void --------- Co-authored-by: Neil <neil@stably.ai> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
niceSprite
Claude
Neil
parent
62d9eb635b
commit
27a8963806
@@ -9,6 +9,7 @@ import { stripElectronRunAsNode } from '../runtime/orca-app-executable'
|
||||
import { rejectRemoteSelectionFlags } from '../remote-selection-flag-rejection'
|
||||
import { buildWslExecArgs, buildWslLoginShellCommand } from '../../shared/wsl-login-shell-command'
|
||||
import { quotePosixShell } from '../../shared/shell-argument-quoting'
|
||||
import { isWindowsFnmDefaultAliasDir } from '../../shared/fnm-default-alias-dirs'
|
||||
import {
|
||||
getVersionManagerBinPaths,
|
||||
resolveCliCommand,
|
||||
@@ -55,7 +56,9 @@ function addAgentNodePaths(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
const existing = new Set(currentEntries)
|
||||
const missing = getVersionManagerBinPaths().filter((entry) => !existing.has(entry))
|
||||
if (missing.length > 0) {
|
||||
env[pathKey] = [...missing, ...currentEntries].join(delimiter)
|
||||
const append = missing.filter(isWindowsFnmDefaultAliasDir)
|
||||
const prepend = missing.filter((entry) => !append.includes(entry))
|
||||
env[pathKey] = [...prepend, ...currentEntries, ...append].join(delimiter)
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { chmodSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, dirname, join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, onTestFinished } from 'vitest'
|
||||
import { afterEach, describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import {
|
||||
getVersionManagerBinPaths,
|
||||
withCliRuntimeOnPath,
|
||||
@@ -319,6 +319,33 @@ describe('getVersionManagerBinPaths', () => {
|
||||
expect(paths).toContain(join(root, '.local', 'bin'))
|
||||
expect(paths).toContain(join(root, 'AppData', 'Roaming', 'npm'))
|
||||
})
|
||||
|
||||
it('uses the Windows fnm default alias layout, honoring FNM_DIR (stablyai/orca#11356)', () => {
|
||||
const root = makeCommandFixture('orca-vm-paths-')
|
||||
const appDataAlias = join(root, 'AppData', 'Roaming', 'fnm', 'aliases', 'default')
|
||||
vi.stubEnv('FNM_DIR', '')
|
||||
vi.stubEnv('APPDATA', '')
|
||||
onTestFinished(() => {
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
const paths = getVersionManagerBinPaths({ platform: 'win32', homePath: root })
|
||||
expect(paths).toContain(appDataAlias)
|
||||
expect(paths).not.toContain(join(root, '.fnm', 'aliases', 'default', 'bin'))
|
||||
|
||||
const redirectedAppData = join(root, 'redirected')
|
||||
const redirectedAlias = join(redirectedAppData, 'fnm', 'aliases', 'default')
|
||||
vi.stubEnv('APPDATA', redirectedAppData)
|
||||
expect(getVersionManagerBinPaths({ platform: 'win32', homePath: root })).toContain(
|
||||
redirectedAlias
|
||||
)
|
||||
|
||||
const fnmDir = join(root, 'custom-fnm')
|
||||
vi.stubEnv('FNM_DIR', fnmDir)
|
||||
const overridden = getVersionManagerBinPaths({ platform: 'win32', homePath: root })
|
||||
expect(overridden).toContain(join(fnmDir, 'aliases', 'default'))
|
||||
expect(overridden).not.toContain(redirectedAlias)
|
||||
})
|
||||
})
|
||||
|
||||
describe('withCliRuntimeOnPath', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { homedir, tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
|
||||
vi.mock('electron', () => {
|
||||
const paths = new Map<string, string>([['appData', '/tmp/app-data']])
|
||||
@@ -224,6 +224,10 @@ describe('patchPackagedProcessPath', () => {
|
||||
|
||||
setPlatform('win32')
|
||||
Object.defineProperty(app, 'isPackaged', { configurable: true, value: true })
|
||||
vi.stubEnv('FNM_DIR', 'C:\\fnm')
|
||||
onTestFinished(() => {
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
const pathDelimiter = process.platform === 'win32' ? ';' : ':'
|
||||
process.env.PATH = `C:\\Windows\\System32${pathDelimiter}C:\\Windows`
|
||||
|
||||
@@ -233,6 +237,9 @@ describe('patchPackagedProcessPath', () => {
|
||||
const userLocalBin = join(homedir(), '.local', 'bin')
|
||||
expect(segments).toContain(userLocalBin)
|
||||
expect(segments.indexOf(userLocalBin)).toBeLessThan(segments.indexOf('C:\\Windows\\System32'))
|
||||
// #11356: fnm's default Node only backfills; it must not shadow an inherited system Node.
|
||||
const fnmAlias = join('C:\\fnm', 'aliases', 'default')
|
||||
expect(segments.indexOf(fnmAlias)).toBeGreaterThan(segments.indexOf('C:\\Windows'))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { mkdirSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { getVersionManagerBinPaths } from '../codex-cli/command'
|
||||
import { isWindowsFnmDefaultAliasDir } from '../../shared/fnm-default-alias-dirs'
|
||||
import { getMainE2EConfig } from '../e2e-config'
|
||||
import { DISABLED_CHROMIUM_FEATURES } from './disabled-chromium-features'
|
||||
import { readHttp1CompatibilityMarker } from './http1-compatibility-marker'
|
||||
@@ -158,7 +159,13 @@ export function patchPackagedProcessPath(): void {
|
||||
}
|
||||
|
||||
// env-node shebangs require runtime shims; exclude generic user bins so arbitrary wrappers cannot shadow system tools.
|
||||
prependPaths.push(...getVersionManagerBinPaths().filter((path) => !isGenericUserBinDir(path)))
|
||||
for (const path of getVersionManagerBinPaths()) {
|
||||
if (isWindowsFnmDefaultAliasDir(path)) {
|
||||
appendPaths.push(path)
|
||||
} else if (!isGenericUserBinDir(path)) {
|
||||
prependPaths.push(path)
|
||||
}
|
||||
}
|
||||
|
||||
const pathKey = process.platform === 'win32' && process.env.Path !== undefined ? 'Path' : 'PATH'
|
||||
const currentPath = process.env[pathKey] ?? ''
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
/** fnm's `default` alias dir. Windows: FNM_DIR, else %APPDATA%\fnm, else legacy ~\.fnm; node.exe has no bin subdir (#11356). */
|
||||
export function getFnmDefaultAliasDirectories(
|
||||
platform: NodeJS.Platform,
|
||||
homePath: string
|
||||
): string[] {
|
||||
if (platform !== 'win32') {
|
||||
return [join(homePath, '.fnm', 'aliases', 'default', 'bin')]
|
||||
}
|
||||
const fnmDir = process.env.FNM_DIR
|
||||
const appData = process.env.APPDATA || join(homePath, 'AppData', 'Roaming')
|
||||
const baseDirs = fnmDir ? [fnmDir] : [join(appData, 'fnm'), join(homePath, '.fnm')]
|
||||
return baseDirs.map((baseDir) => join(baseDir, 'aliases', 'default'))
|
||||
}
|
||||
|
||||
/** Windows fnm default only backfills a missing node; leading PATH would shadow an inherited system/nvm-windows/`fnm use` Node. */
|
||||
export function isWindowsFnmDefaultAliasDir(path: string): boolean {
|
||||
return (
|
||||
process.platform === 'win32' &&
|
||||
getFnmDefaultAliasDirectories(process.platform, homedir()).includes(path)
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { accessSync, constants, existsSync, readFileSync, readdirSync, statSync
|
||||
import { homedir } from 'node:os'
|
||||
import { delimiter, dirname, isAbsolute, join } from 'node:path'
|
||||
import { firstWindowsPathEnvKey, isNativeExecutable } from './cli-runtime-pairing-inputs'
|
||||
import { getFnmDefaultAliasDirectories } from './fnm-default-alias-dirs'
|
||||
import { getSystemCliInstallDirectories } from './system-cli-install-dirs'
|
||||
|
||||
type ResolveCommandOptions = {
|
||||
@@ -93,7 +94,7 @@ function getBaseVersionManagerDirectories(platform: NodeJS.Platform, homePath: s
|
||||
const directories = [
|
||||
join(homePath, '.volta', 'bin'),
|
||||
join(homePath, '.asdf', 'shims'),
|
||||
join(homePath, '.fnm', 'aliases', 'default', 'bin'),
|
||||
...getFnmDefaultAliasDirectories(platform, homePath),
|
||||
join(homePath, '.local', 'share', 'mise', 'shims')
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user