Fix Codex Microsoft Store binary detection on Windows (#1020)
Fixes #1013
This commit is contained in:
@@ -52,7 +52,7 @@ import {
|
||||
resolveProviderCommandPrefix,
|
||||
type ProviderRuntimeSettings,
|
||||
} from "../provider-launch-config.js";
|
||||
import { findExecutable, isCommandAvailable } from "../../../utils/executable.js";
|
||||
import { findExecutable, isCommandAvailable, probeExecutable } from "../../../utils/executable.js";
|
||||
import { createPathEquivalenceMatcher } from "../../../utils/path.js";
|
||||
import { spawnProcess } from "../../../utils/spawn.js";
|
||||
import { extractCodexTerminalSessionId, nonEmptyString } from "./tool-call-mapper-utils.js";
|
||||
@@ -375,8 +375,61 @@ function mergeCodexConfiguredDefaults(
|
||||
};
|
||||
}
|
||||
|
||||
function codexMicrosoftStorePackageRoot(): string | null {
|
||||
const localAppData = process.env.LOCALAPPDATA;
|
||||
if (!localAppData) {
|
||||
return null;
|
||||
}
|
||||
return path.join(localAppData, "Packages");
|
||||
}
|
||||
|
||||
async function findCodexMicrosoftStoreBinary(): Promise<string | null> {
|
||||
if (process.platform !== "win32") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const packageRoot = codexMicrosoftStorePackageRoot();
|
||||
if (!packageRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let entries: Dirent[];
|
||||
try {
|
||||
entries = await fs.readdir(packageRoot, { withFileTypes: true });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const codexPackages = entries
|
||||
.filter((entry) => entry.isDirectory() && entry.name.startsWith("OpenAI.Codex_"))
|
||||
.map((entry) => entry.name)
|
||||
.sort();
|
||||
|
||||
for (const packageName of codexPackages) {
|
||||
const candidate = path.join(
|
||||
packageRoot,
|
||||
packageName,
|
||||
"LocalCache",
|
||||
"Local",
|
||||
"OpenAI",
|
||||
"Codex",
|
||||
"bin",
|
||||
"codex.exe",
|
||||
);
|
||||
if (await probeExecutable(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function findDefaultCodexBinary(): Promise<string | null> {
|
||||
return (await findExecutable("codex")) ?? (await findCodexMicrosoftStoreBinary());
|
||||
}
|
||||
|
||||
async function resolveCodexBinary(): Promise<string> {
|
||||
const found = await findExecutable("codex");
|
||||
const found = await findDefaultCodexBinary();
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
@@ -5299,13 +5352,13 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
if (command?.mode === "replace") {
|
||||
return await isCommandAvailable(command.argv[0]);
|
||||
}
|
||||
return await isCommandAvailable("codex");
|
||||
return (await findDefaultCodexBinary()) !== null;
|
||||
}
|
||||
|
||||
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
||||
try {
|
||||
const available = await this.isAvailable();
|
||||
const resolvedBinary = await findExecutable("codex");
|
||||
const resolvedBinary = await findDefaultCodexBinary();
|
||||
const entries: Array<{ label: string; value: string }> = [
|
||||
{
|
||||
label: "Binary",
|
||||
@@ -5448,6 +5501,8 @@ export const __codexAppServerInternals = {
|
||||
CodexAppServerClient,
|
||||
codexModelSupportsFastMode,
|
||||
CodexAppServerAgentSession,
|
||||
findCodexMicrosoftStoreBinary,
|
||||
findDefaultCodexBinary,
|
||||
formatCodexQuestionPrompts,
|
||||
mapCodexQuestionRequestToToolCall,
|
||||
mapCodexPatchNotificationToToolCall,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
@@ -9,10 +9,11 @@ import { AgentManager } from "../agent-manager.js";
|
||||
import { AgentStorage } from "../agent-storage.js";
|
||||
|
||||
import { ClaudeAgentClient } from "./claude/agent.js";
|
||||
import { CodexAppServerAgentClient } from "./codex-app-server-agent.js";
|
||||
import { __codexAppServerInternals, CodexAppServerAgentClient } from "./codex-app-server-agent.js";
|
||||
import { OpenCodeAgentClient } from "./opencode-agent.js";
|
||||
|
||||
const originalEnv = {
|
||||
LOCALAPPDATA: process.env.LOCALAPPDATA,
|
||||
PATH: process.env.PATH,
|
||||
PATHEXT: process.env.PATHEXT,
|
||||
};
|
||||
@@ -31,7 +32,19 @@ function isolatePathTo(dir: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function isolateCodexDefaultDiscoveryTo(dir: string): void {
|
||||
isolatePathTo(dir);
|
||||
if (process.platform === "win32") {
|
||||
process.env.LOCALAPPDATA = dir;
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv.LOCALAPPDATA === undefined) {
|
||||
delete process.env.LOCALAPPDATA;
|
||||
} else {
|
||||
process.env.LOCALAPPDATA = originalEnv.LOCALAPPDATA;
|
||||
}
|
||||
process.env.PATH = originalEnv.PATH;
|
||||
process.env.PATHEXT = originalEnv.PATHEXT;
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
@@ -42,12 +55,51 @@ afterEach(() => {
|
||||
describe("default provider availability", () => {
|
||||
test("Codex reports unavailable when the default command cannot be resolved", async () => {
|
||||
const binDir = makeTempDir("provider-availability-codex-");
|
||||
isolatePathTo(binDir);
|
||||
isolateCodexDefaultDiscoveryTo(binDir);
|
||||
const client = new CodexAppServerAgentClient(createTestLogger());
|
||||
|
||||
await expect(client.isAvailable()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
test("Codex reports available from a Microsoft Store install path when PATH misses codex", async () => {
|
||||
const originalPlatform = process.platform;
|
||||
const originalLocalAppData = process.env.LOCALAPPDATA;
|
||||
const root = makeTempDir("provider-availability-codex-store-");
|
||||
const emptyPathDir = join(root, "empty-path");
|
||||
const codexBinDir = join(
|
||||
root,
|
||||
"Packages",
|
||||
"OpenAI.Codex_abc123",
|
||||
"LocalCache",
|
||||
"Local",
|
||||
"OpenAI",
|
||||
"Codex",
|
||||
"bin",
|
||||
);
|
||||
const codexExe = join(codexBinDir, "codex.exe");
|
||||
mkdirSync(emptyPathDir, { recursive: true });
|
||||
mkdirSync(codexBinDir, { recursive: true });
|
||||
copyFileSync(process.execPath, codexExe);
|
||||
Object.defineProperty(process, "platform", { value: "win32", writable: true });
|
||||
process.env.LOCALAPPDATA = root;
|
||||
isolatePathTo(emptyPathDir);
|
||||
process.env.PATHEXT = ".EXE";
|
||||
|
||||
try {
|
||||
const client = new CodexAppServerAgentClient(createTestLogger());
|
||||
|
||||
await expect(__codexAppServerInternals.findDefaultCodexBinary()).resolves.toBe(codexExe);
|
||||
await expect(client.isAvailable()).resolves.toBe(true);
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform, writable: true });
|
||||
if (originalLocalAppData === undefined) {
|
||||
delete process.env.LOCALAPPDATA;
|
||||
} else {
|
||||
process.env.LOCALAPPDATA = originalLocalAppData;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("Claude reports unavailable when the default command cannot be resolved", async () => {
|
||||
const binDir = makeTempDir("provider-availability-claude-");
|
||||
isolatePathTo(binDir);
|
||||
@@ -66,7 +118,7 @@ describe("default provider availability", () => {
|
||||
|
||||
test("AgentManager reports Codex unavailable without throwing", async () => {
|
||||
const binDir = makeTempDir("provider-availability-manager-bin-");
|
||||
isolatePathTo(binDir);
|
||||
isolateCodexDefaultDiscoveryTo(binDir);
|
||||
const workdir = makeTempDir("provider-availability-manager-work-");
|
||||
const storage = new AgentStorage(join(workdir, "agents"), createTestLogger());
|
||||
const manager = new AgentManager({
|
||||
@@ -88,7 +140,7 @@ describe("default provider availability", () => {
|
||||
|
||||
test("resumeAgentFromPersistence stops before provider spawn when Codex is unavailable", async () => {
|
||||
const binDir = makeTempDir("provider-availability-resume-bin-");
|
||||
isolatePathTo(binDir);
|
||||
isolateCodexDefaultDiscoveryTo(binDir);
|
||||
const workdir = makeTempDir("provider-availability-resume-work-");
|
||||
const storage = new AgentStorage(join(workdir, "agents"), createTestLogger());
|
||||
const manager = new AgentManager({
|
||||
|
||||
Reference in New Issue
Block a user