From 814beb40d39fb6ee0c246d7c72d52e22ee3ed92d Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Wed, 27 May 2026 21:48:07 +0800 Subject: [PATCH 1/2] fix(platform): detect Windows fnm CLI shims under GUI-launched PATH (#3062) Scan %LOCALAPPDATA%\fnm\node-versions, fnm_multishells session dirs, and %APPDATA%\npm when building wellKnownUserToolchainBins on Windows so packaged Open Design finds Codex and other npm globals installed via fnm even when launched from Start Menu without an initialized shell PATH. --- .../tests/runtimes/env-and-detection.test.ts | 24 ++++++++++++++ packages/platform/src/index.ts | 22 +++++++++++++ packages/platform/tests/index.test.ts | 31 +++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/apps/daemon/tests/runtimes/env-and-detection.test.ts b/apps/daemon/tests/runtimes/env-and-detection.test.ts index 124fe7b27..95d8446f0 100644 --- a/apps/daemon/tests/runtimes/env-and-detection.test.ts +++ b/apps/daemon/tests/runtimes/env-and-detection.test.ts @@ -363,6 +363,30 @@ test('resolveAgentExecutable accepts Windows CODEX_BIN overrides with executable } }); +test('resolveAgentExecutable searches Windows fnm multishell shims under a minimal GUI PATH (#3062)', () => { + const home = mkdtempSync(join(tmpdir(), 'od-agent-fnm-multishell-')); + const localAppData = join(home, 'AppData', 'Local'); + const fnmMultishell = join(localAppData, 'fnm_multishells', '12345-abcd'); + try { + return withEnvSnapshot(['PATH', 'PATHEXT', 'OD_AGENT_HOME', 'LOCALAPPDATA'], () => { + mkdirSync(fnmMultishell, { recursive: true }); + writeFileSync(join(fnmMultishell, 'codex.CMD'), '@echo off\r\nexit /b 0\r\n'); + process.env.OD_AGENT_HOME = home; + process.env.LOCALAPPDATA = localAppData; + process.env.PATH = ''; + process.env.PATHEXT = '.EXE;.CMD;.BAT'; + + const resolved = withPlatform('win32', () => + resolveAgentExecutable(minimalAgentDef({ id: 'codex', bin: 'codex' })), + ); + + assert.equal(resolved, join(fnmMultishell, 'codex.CMD')); + }); + } finally { + rmSync(home, { recursive: true, force: true }); + } +}); + test('detectAgents applies configured env while probing the CLI', async () => { const dir = mkdtempSync(join(tmpdir(), 'od-agent-env-')); try { diff --git a/packages/platform/src/index.ts b/packages/platform/src/index.ts index 110041199..977714288 100644 --- a/packages/platform/src/index.ts +++ b/packages/platform/src/index.ts @@ -625,6 +625,28 @@ export function wellKnownUserToolchainBins( dirs.push(dir); } } + if (process.platform === "win32") { + const localAppData = + resolveUserScopedHome(env.LOCALAPPDATA, home) ?? join(home, "AppData", "Local"); + const appData = + resolveUserScopedHome(env.APPDATA, home) ?? join(home, "AppData", "Roaming"); + dirs.push(join(appData, "npm")); + const fnmDir = typeof env.FNM_DIR === "string" ? env.FNM_DIR.trim() : ""; + for (const root of [ + join(localAppData, "fnm", "node-versions"), + ...(fnmDir.length > 0 ? [join(fnmDir, "node-versions")] : []), + ]) { + for (const dir of existingChildBinDirs(root, ["installation", "bin"])) { + dirs.push(dir); + } + } + // fnm exposes npm-global CLIs via per-shell shims under fnm_multishells + // after `fnm env`. GUI-launched Electron inherits a stripped PATH without + // those ephemeral dirs (issue #3062). + for (const dir of existingChildBinDirs(join(localAppData, "fnm_multishells"), [])) { + dirs.push(dir); + } + } return dirs; } diff --git a/packages/platform/tests/index.test.ts b/packages/platform/tests/index.test.ts index 8d677b352..fdb31f7db 100644 --- a/packages/platform/tests/index.test.ts +++ b/packages/platform/tests/index.test.ts @@ -669,4 +669,35 @@ describe("wellKnownUserToolchainBins", () => { rmSync(home, { recursive: true, force: true }); } }); + + it("includes Windows fnm node installs, multishell shims, and npm globals (#3062)", () => { + const originalPlatform = process.platform; + Object.defineProperty(process, "platform", { configurable: true, value: "win32" }); + const home = mkdtempSync(join(tmpdir(), "wkutb-win-fnm-")); + const localAppData = join(home, "AppData", "Local"); + const appData = join(home, "AppData", "Roaming"); + const fnmNodeBin = join(localAppData, "fnm", "node-versions", "v20.20.2", "installation", "bin"); + const fnmMultishell = join(localAppData, "fnm_multishells", "12345-abcd"); + const npmGlobal = join(appData, "npm"); + try { + mkdirSync(fnmNodeBin, { recursive: true }); + mkdirSync(fnmMultishell, { recursive: true }); + mkdirSync(npmGlobal, { recursive: true }); + writeFileSync(join(fnmNodeBin, "marker"), ""); + writeFileSync(join(fnmMultishell, "marker"), ""); + writeFileSync(join(npmGlobal, "marker"), ""); + + const dirs = wellKnownUserToolchainBins({ + home, + env: { LOCALAPPDATA: localAppData, APPDATA: appData }, + includeSystemBins: false, + }); + expect(dirs).toContain(fnmNodeBin); + expect(dirs).toContain(fnmMultishell); + expect(dirs).toContain(npmGlobal); + } finally { + Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform }); + rmSync(home, { recursive: true, force: true }); + } + }); }); From f4faf08787a73191e3469cba4cc97d420d6e3f79 Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Wed, 27 May 2026 21:56:39 +0800 Subject: [PATCH 2/2] chore: retrigger CI after flaky orbit namespace collision