Fix Codex wrapper launch paths (#1395)

This commit is contained in:
nettee 2026-05-12 17:20:32 +08:00 committed by GitHub
parent 6c3fd86642
commit 28d3e5faf5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 410 additions and 35 deletions

View file

@ -6,6 +6,7 @@ export {
inspectAgentExecutableResolution,
resolveAgentExecutable,
} from './runtimes/executables.js';
export { applyAgentLaunchEnv, resolveAgentLaunch } from './runtimes/launch.js';
export { resolveAgentBin } from './runtimes/resolution.js';
export { spawnEnvForAgent } from './runtimes/env.js';
export { buildLiveArtifactsMcpServersForAgent } from './runtimes/mcp.js';

View file

@ -21,9 +21,9 @@ import { promises as fsp } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
applyAgentLaunchEnv,
getAgentDef,
inspectAgentExecutableResolution,
resolveAgentBin,
resolveAgentLaunch,
spawnEnvForAgent,
} from './agents.js';
import { createCommandInvocation } from '@open-design/platform';
@ -1003,12 +1003,9 @@ async function testAgentConnectionInternal(
validateAgentCliEnv(input.agentCliEnv),
input.agentId,
);
const executableResolution = inspectAgentExecutableResolution(
def,
configuredAgentEnv,
);
const resolvedBin = resolveAgentBin(input.agentId, configuredAgentEnv);
if (!resolvedBin) {
const executableResolution = resolveAgentLaunch(def, configuredAgentEnv);
const resolvedBin = executableResolution.selectedPath;
if (!resolvedBin || !executableResolution.launchPath) {
return {
ok: false,
kind: 'agent_not_installed',
@ -1128,16 +1125,16 @@ async function testAgentConnectionInternal(
}
const stdinMode =
def.promptViaStdin || def.streamFormat === 'acp-json-rpc' ? 'pipe' : 'ignore';
const env = spawnEnvForAgent(
const env = applyAgentLaunchEnv(spawnEnvForAgent(
input.agentId,
{
...process.env,
...(def.env || {}),
},
configuredAgentEnv,
);
), executableResolution);
const invocation = createCommandInvocation({
command: resolvedBin,
command: executableResolution.launchPath,
args,
env,
});
@ -1176,11 +1173,11 @@ async function testAgentConnectionInternal(
const latencyMs = Date.now() - start;
const detail = redactSecrets(winner.error.message);
const guidance = redactSecrets(
codexExecutableGuidance(
`${codexExecutableGuidance(
input.agentId,
executableResolution.configuredOverridePath,
executableResolution.pathResolvedPath,
),
)}${executableResolution.diagnostic ? ` ${executableResolution.diagnostic}` : ''}`,
);
const errnoCode = (winner.error as NodeJS.ErrnoException).code;
const isMissing = errnoCode === 'ENOENT';
@ -1263,11 +1260,11 @@ async function testAgentConnectionInternal(
.join(' · '),
);
const guidance = redactSecrets(
codexExecutableGuidance(
`${codexExecutableGuidance(
input.agentId,
executableResolution.configuredOverridePath,
executableResolution.pathResolvedPath,
),
)}${executableResolution.diagnostic ? ` ${executableResolution.diagnostic}` : ''}`,
);
const label = buffered ? 'exit_failed' : 'no_text';
console.warn(
@ -1394,11 +1391,15 @@ export async function testAgentConnection(
const configuredAgentEnv = agentCliEnvForAgent(validatedPrefs, input.agentId);
const def = getAgentDef(input.agentId);
const executableResolution = def
? inspectAgentExecutableResolution(def, configuredAgentEnv)
? resolveAgentLaunch(def, configuredAgentEnv)
: {
configuredOverridePath: null,
pathResolvedPath: null,
selectedPath: null,
launchPath: null,
launchKind: 'selected' as const,
childPathPrepend: [],
diagnostic: null,
};
if (
input.agentId === 'codex' &&
@ -1409,7 +1410,7 @@ export async function testAgentConnection(
return {
...primaryResult,
configuredExecutablePath: executableResolution.configuredOverridePath,
usedExecutablePath: executableResolution.configuredOverridePath,
usedExecutablePath: executableResolution.launchPath ?? executableResolution.configuredOverridePath,
usedExecutableSource: 'configured',
...(executableResolution.pathResolvedPath
? { detectedExecutablePath: executableResolution.pathResolvedPath }
@ -1426,7 +1427,7 @@ export async function testAgentConnection(
...primaryResult,
configuredExecutablePath: configuredCodexBin,
detectedExecutablePath: executableResolution.pathResolvedPath,
usedExecutablePath: executableResolution.pathResolvedPath,
usedExecutablePath: executableResolution.launchPath ?? executableResolution.pathResolvedPath,
usedExecutableSource: 'fallback_invalid',
detail: redactSecrets(
codexInvalidConfiguredPathFallbackDetail(
@ -1460,7 +1461,7 @@ export async function testAgentConnection(
...fallbackResult,
configuredExecutablePath: executableResolution.configuredOverridePath,
detectedExecutablePath: executableResolution.pathResolvedPath,
usedExecutablePath: executableResolution.pathResolvedPath,
usedExecutablePath: executableResolution.launchPath ?? executableResolution.pathResolvedPath,
usedExecutableSource: 'fallback_failed',
detail: redactSecrets(
codexExecutableFallbackSuccessDetail(

View file

@ -0,0 +1,164 @@
import { accessSync, constants, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
import path, { delimiter } from 'node:path';
import { inspectAgentExecutableResolution } from './executables.js';
import type { RuntimeAgentDef } from './types.js';
export type AgentLaunchKind = 'selected' | 'codex-native';
export type AgentLaunchResolution = ReturnType<typeof inspectAgentExecutableResolution> & {
launchPath: string | null;
launchKind: AgentLaunchKind;
childPathPrepend: string[];
diagnostic: string | null;
};
export function resolveAgentLaunch(
def: RuntimeAgentDef,
configuredEnv: Record<string, string> = {},
): AgentLaunchResolution {
const resolution = inspectAgentExecutableResolution(def, configuredEnv);
if (!resolution.selectedPath) {
return { ...resolution, launchPath: null, launchKind: 'selected', childPathPrepend: [], diagnostic: null };
}
const childPathPrepend = path.isAbsolute(resolution.selectedPath)
? [path.dirname(resolution.selectedPath)]
: [];
if (def.id !== 'codex') {
return { ...resolution, launchPath: resolution.selectedPath, launchKind: 'selected', childPathPrepend, diagnostic: null };
}
const native = tryResolveCodexNativeBinary(resolution.selectedPath);
return {
...resolution,
launchPath: native.path ?? resolution.selectedPath,
launchKind: native.path ? 'codex-native' : 'selected',
childPathPrepend: [...childPathPrepend, ...native.childPathPrepend],
diagnostic: native.diagnostic,
};
}
export function applyAgentLaunchEnv(
env: NodeJS.ProcessEnv,
launch: Pick<AgentLaunchResolution, 'childPathPrepend'>,
): NodeJS.ProcessEnv {
if (launch.childPathPrepend.length === 0) return env;
const existing = typeof env.PATH === 'string' ? env.PATH : '';
const PATH = [...launch.childPathPrepend, ...existing.split(delimiter)]
.filter((entry, index, entries) => entry.length > 0 && entries.indexOf(entry) === index)
.join(delimiter);
return { ...env, PATH };
}
function tryResolveCodexNativeBinary(wrapperPath: string): {
path: string | null;
childPathPrepend: string[];
diagnostic: string | null;
} {
const packageSuffix = codexNativePackageSuffix();
const targetTriple = codexNativeTargetTriple();
for (const root of codexSearchRoots(wrapperPath)) {
for (const candidate of codexNativeCandidates(root, packageSuffix, targetTriple)) {
if (isExecutableFile(candidate.path)) {
return { path: candidate.path, childPathPrepend: existingDirectories(candidate.childPathPrepend), diagnostic: null };
}
}
}
if (!looksLikeCodexNodeWrapper(wrapperPath)) return { path: null, childPathPrepend: [], diagnostic: null };
return {
path: null,
childPathPrepend: [],
diagnostic: `Codex native binary was not found for ${packageSuffix}/${targetTriple}; falling back to wrapper ${wrapperPath}. Set CODEX_BIN to a native Codex binary if this wrapper cannot launch from a GUI environment.`,
};
}
function codexSearchRoots(wrapperPath: string): string[] {
const roots = new Set<string>();
for (const seed of [wrapperPath, safeRealpath(wrapperPath)]) {
if (!seed) continue;
let current = path.dirname(seed);
while (current !== path.dirname(current)) {
roots.add(current);
current = path.dirname(current);
}
}
return [...roots];
}
function codexNativeCandidates(
root: string,
packageSuffix: string,
targetTriple: string,
): Array<{ path: string; childPathPrepend: string[] }> {
const scoped = path.join(root, 'node_modules', '@openai');
const packageDirs = [path.join(scoped, `codex-${packageSuffix}`)];
try {
for (const entry of readdirSync(scoped, { encoding: 'utf8', withFileTypes: true })) {
if (entry.isDirectory() && entry.name.startsWith('codex-')) packageDirs.push(path.join(scoped, entry.name));
}
} catch {
// Optional package layouts vary by npm version; absence uses wrapper fallback.
}
return [...new Set(packageDirs)].flatMap((dir) => {
const vendorPathDir = path.join(dir, 'vendor', targetTriple, 'path');
const childPathPrepend = [vendorPathDir];
return [
{ path: path.join(dir, 'vendor', targetTriple, 'codex', 'codex'), childPathPrepend },
{ path: path.join(dir, 'vendor', targetTriple, 'codex', 'codex.exe'), childPathPrepend },
{ path: path.join(dir, 'codex'), childPathPrepend },
{ path: path.join(dir, 'bin', 'codex'), childPathPrepend },
{ path: path.join(dir, 'vendor', 'codex'), childPathPrepend },
{ path: path.join(dir, 'codex.exe'), childPathPrepend },
{ path: path.join(dir, 'bin', 'codex.exe'), childPathPrepend },
];
});
}
function codexNativePackageSuffix(): string {
return `${process.platform}-${process.arch}`;
}
function codexNativeTargetTriple(): string {
if (process.platform === 'darwin' && process.arch === 'arm64') return 'aarch64-apple-darwin';
if (process.platform === 'darwin' && process.arch === 'x64') return 'x86_64-apple-darwin';
if (process.platform === 'linux' && process.arch === 'arm64') return 'aarch64-unknown-linux-musl';
if (process.platform === 'linux' && process.arch === 'x64') return 'x86_64-unknown-linux-musl';
if (process.platform === 'win32' && process.arch === 'arm64') return 'aarch64-pc-windows-msvc';
if (process.platform === 'win32' && process.arch === 'x64') return 'x86_64-pc-windows-msvc';
return `${process.platform}-${process.arch}`;
}
function looksLikeCodexNodeWrapper(filePath: string): boolean {
try {
const body = readFileSync(filePath, { encoding: 'utf8' }).slice(0, 64_000);
return /node|@openai\/codex|codex-/i.test(body);
} catch {
return false;
}
}
function safeRealpath(filePath: string): string | null {
try {
return realpathSync(filePath);
} catch {
return null;
}
}
function existingDirectories(dirs: string[]): string[] {
return dirs.filter((dir) => {
try {
return statSync(dir).isDirectory();
} catch {
return false;
}
});
}
function isExecutableFile(filePath: string): boolean {
try {
if (!statSync(filePath).isFile()) return false;
if (process.platform !== 'win32') accessSync(filePath, constants.X_OK);
return true;
} catch {
return false;
}
}

View file

@ -26,7 +26,8 @@ import {
detectAgents,
getAgentDef,
isKnownModel,
resolveAgentBin,
applyAgentLaunchEnv,
resolveAgentLaunch,
sanitizeCustomModel,
spawnEnvForAgent,
} from './agents.js';
@ -3575,7 +3576,8 @@ export async function startServer({
configuredAgentEnv = {};
}
const resolvedBin = resolveAgentBin(agentId, configuredAgentEnv);
const agentLaunch = resolveAgentLaunch(def, configuredAgentEnv);
const resolvedBin = agentLaunch.selectedPath;
const args = def.buildArgs(
composed,
@ -3597,7 +3599,7 @@ export async function startServer({
// doesn't have to special-case it.
const cmdShimBudgetError = checkWindowsCmdShimCommandLineBudget(
def,
resolvedBin,
agentLaunch.launchPath ?? resolvedBin,
args,
);
if (cmdShimBudgetError) {
@ -3624,7 +3626,7 @@ export async function startServer({
// users hit a generic `spawn ENAMETOOLONG`.
const directExeBudgetError = checkWindowsDirectExeCommandLineBudget(
def,
resolvedBin,
agentLaunch.launchPath ?? resolvedBin,
args,
);
if (directExeBudgetError) {
@ -3716,7 +3718,7 @@ export async function startServer({
// pointing at /api/agents instead of silently falling back to
// spawn(def.bin) — that fallback re-introduces the exact ENOENT symptom
// from issue #10.
if (!resolvedBin) {
if (!resolvedBin || !agentLaunch.launchPath) {
revokeToolToken('child_exit');
unregisterChatAgentEventSink();
send('error', createSseErrorPayload(
@ -3772,7 +3774,7 @@ export async function startServer({
def.promptViaStdin || def.streamFormat === 'acp-json-rpc'
? 'pipe'
: 'ignore';
const env = {
const env = applyAgentLaunchEnv({
...spawnEnvForAgent(
def.id,
{
@ -3782,10 +3784,10 @@ export async function startServer({
configuredAgentEnv,
),
...odMediaEnv,
};
}, agentLaunch);
spawnedAgentEnv = env;
const invocation = createCommandInvocation({
command: resolvedBin,
command: agentLaunch.launchPath,
args,
env,
});

View file

@ -11,12 +11,14 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
AGENT_DEFS,
applyAgentLaunchEnv,
buildLiveArtifactsMcpServersForAgent,
checkPromptArgvBudget,
checkWindowsCmdShimCommandLineBudget,
checkWindowsDirectExeCommandLineBudget,
detectAgents,
inspectAgentExecutableResolution,
resolveAgentLaunch,
resolveAgentExecutable,
spawnEnvForAgent,
} from '../../../src/agents.js';
@ -25,6 +27,7 @@ import type { RuntimeAgentDef } from '../../../src/runtimes/types.js';
export {
assert,
AGENT_DEFS,
applyAgentLaunchEnv,
buildLiveArtifactsMcpServersForAgent,
checkPromptArgvBudget,
checkWindowsCmdShimCommandLineBudget,
@ -36,6 +39,7 @@ export {
mkdirSync,
mkdtempSync,
resolveAgentExecutable,
resolveAgentLaunch,
rmSync,
spawnEnvForAgent,
tmpdir,

View file

@ -0,0 +1,159 @@
import { delimiter, join } from 'node:path';
import { realpathSync, symlinkSync } from 'node:fs';
import { test } from 'vitest';
import {
applyAgentLaunchEnv,
assert,
chmodSync,
codex,
mkdirSync,
mkdtempSync,
resolveAgentLaunch,
rmSync,
tmpdir,
withEnvSnapshot,
writeFileSync,
} from './helpers/test-helpers.js';
const fsTest = process.platform === 'win32' ? test.skip : test;
test('applyAgentLaunchEnv prepends the selected executable dirname and dedupes PATH', () => {
const launch = {
childPathPrepend: ['/opt/tools/bin', '/opt/tools/bin'],
};
const env = applyAgentLaunchEnv(
{ PATH: ['/usr/bin', '/opt/tools/bin', '/bin', '/usr/bin'].join(delimiter) },
launch,
);
assert.equal(env.PATH, ['/opt/tools/bin', '/usr/bin', '/bin'].join(delimiter));
});
fsTest('resolveAgentLaunch selects nvm-installed codex under a minimal PATH and prepends its dirname', () => {
const home = mkdtempSync(join(tmpdir(), 'od-launch-nvm-'));
try {
return withEnvSnapshot(['HOME', 'PATH', 'OD_AGENT_HOME'], () => {
const binDir = join(home, '.nvm', 'versions', 'node', '24.11.0', 'bin');
const codexBin = join(binDir, 'codex');
mkdirSync(binDir, { recursive: true });
writeFileSync(codexBin, '#!/bin/sh\nexit 0\n');
chmodSync(codexBin, 0o755);
process.env.HOME = home;
process.env.PATH = '/usr/bin:/bin';
process.env.OD_AGENT_HOME = home;
const launch = resolveAgentLaunch(codex);
assert.equal(launch.selectedPath, codexBin);
assert.equal(launch.launchPath, codexBin);
assert.deepEqual(launch.childPathPrepend, [binDir]);
});
} finally {
rmSync(home, { recursive: true, force: true });
}
});
fsTest('resolveAgentLaunch resolves a Codex npm wrapper to the native packaged binary', () => {
const root = mkdtempSync(join(tmpdir(), 'od-launch-codex-wrapper-'));
try {
return withEnvSnapshot(['PATH', 'OD_AGENT_HOME'], () => {
const wrapperPkgDir = join(root, 'node_modules', '@openai', 'codex');
const wrapperRealPath = join(wrapperPkgDir, 'bin', 'codex.js');
const wrapperLinkDir = join(root, 'node_modules', '.bin');
const wrapperLinkPath = join(wrapperLinkDir, 'codex');
const nativePkgDir = join(wrapperPkgDir, 'node_modules', '@openai', `codex-${process.platform}-${process.arch}`);
const nativePathDir = join(nativePkgDir, 'vendor', codexNativeTargetTriple(), 'path');
const nativeBin = join(nativePkgDir, 'vendor', codexNativeTargetTriple(), 'codex', 'codex');
mkdirSync(join(wrapperPkgDir, 'bin'), { recursive: true });
mkdirSync(wrapperLinkDir, { recursive: true });
mkdirSync(join(nativePkgDir, 'vendor', codexNativeTargetTriple(), 'codex'), { recursive: true });
mkdirSync(nativePathDir, { recursive: true });
writeFileSync(wrapperRealPath, '#!/usr/bin/env node\nrequire("@openai/codex");\n');
writeFileSync(nativeBin, '#!/bin/sh\nexit 0\n');
chmodSync(wrapperRealPath, 0o755);
chmodSync(nativeBin, 0o755);
symlinkSync(wrapperRealPath, wrapperLinkPath);
process.env.PATH = wrapperLinkDir;
process.env.OD_AGENT_HOME = root;
const launch = resolveAgentLaunch(codex);
assert.equal(launch.selectedPath, wrapperLinkPath);
assert.equal(launch.launchPath, realpathSync(nativeBin));
assert.equal(launch.launchKind, 'codex-native');
assert.deepEqual(launch.childPathPrepend, [wrapperLinkDir, realpathSync(nativePathDir)]);
assert.equal(launch.diagnostic, null);
});
} finally {
rmSync(root, { recursive: true, force: true });
}
});
function codexNativeTargetTriple(): string {
if (process.platform === 'darwin' && process.arch === 'arm64') return 'aarch64-apple-darwin';
if (process.platform === 'darwin' && process.arch === 'x64') return 'x86_64-apple-darwin';
if (process.platform === 'linux' && process.arch === 'arm64') return 'aarch64-unknown-linux-musl';
if (process.platform === 'linux' && process.arch === 'x64') return 'x86_64-unknown-linux-musl';
if (process.platform === 'win32' && process.arch === 'arm64') return 'aarch64-pc-windows-msvc';
if (process.platform === 'win32' && process.arch === 'x64') return 'x86_64-pc-windows-msvc';
return `${process.platform}-${process.arch}`;
}
fsTest('resolveAgentLaunch preserves a direct native CODEX_BIN override as the selected launch path', () => {
const root = mkdtempSync(join(tmpdir(), 'od-launch-codex-direct-native-'));
try {
return withEnvSnapshot(['PATH', 'OD_AGENT_HOME'], () => {
const nativeBin = join(root, 'codex-native');
const pathCodex = join(root, 'codex');
writeFileSync(nativeBin, '#!/bin/sh\nexit 0\n');
writeFileSync(pathCodex, '#!/bin/sh\nexit 0\n');
chmodSync(nativeBin, 0o755);
chmodSync(pathCodex, 0o755);
process.env.PATH = root;
process.env.OD_AGENT_HOME = root;
const launch = resolveAgentLaunch(codex, { CODEX_BIN: nativeBin });
assert.equal(launch.configuredOverridePath, nativeBin);
assert.equal(launch.pathResolvedPath, pathCodex);
assert.equal(launch.selectedPath, nativeBin);
assert.equal(launch.launchPath, nativeBin);
assert.equal(launch.launchKind, 'selected');
assert.deepEqual(launch.childPathPrepend, [root]);
assert.equal(launch.diagnostic, null);
});
} finally {
rmSync(root, { recursive: true, force: true });
}
});
fsTest('resolveAgentLaunch falls back to the Codex wrapper when the native package is missing', () => {
const root = mkdtempSync(join(tmpdir(), 'od-launch-codex-fallback-'));
try {
return withEnvSnapshot(['PATH', 'OD_AGENT_HOME'], () => {
const wrapperPkgDir = join(root, 'node_modules', '@openai', 'codex');
const wrapperRealPath = join(wrapperPkgDir, 'bin', 'codex.js');
const wrapperLinkDir = join(root, 'node_modules', '.bin');
const wrapperLinkPath = join(wrapperLinkDir, 'codex');
mkdirSync(join(wrapperPkgDir, 'bin'), { recursive: true });
mkdirSync(wrapperLinkDir, { recursive: true });
writeFileSync(wrapperRealPath, '#!/usr/bin/env node\nrequire("@openai/codex");\n');
chmodSync(wrapperRealPath, 0o755);
symlinkSync(wrapperRealPath, wrapperLinkPath);
process.env.PATH = wrapperLinkDir;
process.env.OD_AGENT_HOME = root;
const launch = resolveAgentLaunch(codex);
assert.equal(launch.selectedPath, wrapperLinkPath);
assert.equal(launch.launchPath, wrapperLinkPath);
assert.equal(launch.launchKind, 'selected');
assert.deepEqual(launch.childPathPrepend, [wrapperLinkDir]);
assert.match(launch.diagnostic ?? '', /native binary/i);
assert.match(launch.diagnostic ?? '', /CODEX_BIN/);
});
} finally {
rmSync(root, { recursive: true, force: true });
}
});

View file

@ -544,10 +544,38 @@ function existingChildBinDirs(root: string, segments: string[]): string[] {
} catch {
return out;
}
for (const entry of entries) {
for (const entry of sortVersionedDirEntries(entries)) {
if (!entry.isDirectory()) continue;
const candidate = join(root, entry.name, ...segments);
if (existsSync(candidate)) out.push(candidate);
}
return out;
}
type SemverParts = [major: number, minor: number, patch: number];
function sortVersionedDirEntries(entries: import("node:fs").Dirent<string>[]): import("node:fs").Dirent<string>[] {
return [...entries].sort((left, right) => compareVersionLikeDirNames(left.name, right.name));
}
function compareVersionLikeDirNames(left: string, right: string): number {
const leftSemver = parseVersionLikeDirName(left);
const rightSemver = parseVersionLikeDirName(right);
if (leftSemver && rightSemver) {
for (let index = 0; index < leftSemver.length; index += 1) {
const difference = rightSemver[index] - leftSemver[index];
if (difference !== 0) return difference;
}
} else if (leftSemver) {
return -1;
} else if (rightSemver) {
return 1;
}
return left.localeCompare(right);
}
function parseVersionLikeDirName(name: string): SemverParts | null {
const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(name);
if (!match) return null;
return [Number(match[1]), Number(match[2]), Number(match[3])];
}

View file

@ -502,26 +502,42 @@ describe("wellKnownUserToolchainBins", () => {
}
});
it("expands per-version Node toolchains for mise / nvm / fnm", () => {
it("surfaces GUI-safe PATH additions and sorts versioned Node bins by highest semver first", () => {
const home = mkdtempSync(join(tmpdir(), "wkutb-versioned-"));
try {
const miseBin = join(home, ".local", "share", "mise", "installs", "node", "24.14.1", "bin");
const nvmBin = join(home, ".nvm", "versions", "node", "v22.10.0", "bin");
const newestNvmBin = join(home, ".nvm", "versions", "node", "v24.1.0", "bin");
const olderNvmBin = join(home, ".nvm", "versions", "node", "v22.10.0", "bin");
const fnmBin = join(home, ".local", "share", "fnm", "node-versions", "v20.11.1", "installation", "bin");
mkdirSync(miseBin, { recursive: true });
mkdirSync(nvmBin, { recursive: true });
mkdirSync(newestNvmBin, { recursive: true });
mkdirSync(olderNvmBin, { recursive: true });
mkdirSync(fnmBin, { recursive: true });
writeFileSync(join(miseBin, "marker"), "");
writeFileSync(join(nvmBin, "marker"), "");
writeFileSync(join(newestNvmBin, "marker"), "");
writeFileSync(join(olderNvmBin, "marker"), "");
writeFileSync(join(fnmBin, "marker"), "");
chmodSync(join(miseBin, "marker"), 0o644);
chmodSync(join(nvmBin, "marker"), 0o644);
chmodSync(join(newestNvmBin, "marker"), 0o644);
chmodSync(join(olderNvmBin, "marker"), 0o644);
chmodSync(join(fnmBin, "marker"), 0o644);
const dirs = wellKnownUserToolchainBins({ home, env: {}, includeSystemBins: false });
const dirs = wellKnownUserToolchainBins({
home,
env: { PATH: "/usr/bin:/bin:/usr/sbin:/sbin" },
includeSystemBins: true,
});
const newestNvmIdx = dirs.indexOf(newestNvmBin);
const olderNvmIdx = dirs.indexOf(olderNvmBin);
expect(dirs).toContain("/opt/homebrew/bin");
expect(dirs).toContain("/usr/local/bin");
expect(dirs).toContain(miseBin);
expect(dirs).toContain(nvmBin);
expect(dirs).toContain(newestNvmBin);
expect(dirs).toContain(olderNvmBin);
expect(dirs).toContain(fnmBin);
expect(newestNvmIdx).toBeGreaterThan(-1);
expect(olderNvmIdx).toBeGreaterThan(newestNvmIdx);
} finally {
rmSync(home, { recursive: true, force: true });
}