Fix Codex Microsoft Store binary detection on Windows (#1020)
Fixes #1013
This commit is contained in:
@@ -52,7 +52,7 @@ import {
|
|||||||
resolveProviderCommandPrefix,
|
resolveProviderCommandPrefix,
|
||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
} from "../provider-launch-config.js";
|
} 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 { createPathEquivalenceMatcher } from "../../../utils/path.js";
|
||||||
import { spawnProcess } from "../../../utils/spawn.js";
|
import { spawnProcess } from "../../../utils/spawn.js";
|
||||||
import { extractCodexTerminalSessionId, nonEmptyString } from "./tool-call-mapper-utils.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> {
|
async function resolveCodexBinary(): Promise<string> {
|
||||||
const found = await findExecutable("codex");
|
const found = await findDefaultCodexBinary();
|
||||||
if (found) {
|
if (found) {
|
||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
@@ -5299,13 +5352,13 @@ export class CodexAppServerAgentClient implements AgentClient {
|
|||||||
if (command?.mode === "replace") {
|
if (command?.mode === "replace") {
|
||||||
return await isCommandAvailable(command.argv[0]);
|
return await isCommandAvailable(command.argv[0]);
|
||||||
}
|
}
|
||||||
return await isCommandAvailable("codex");
|
return (await findDefaultCodexBinary()) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
||||||
try {
|
try {
|
||||||
const available = await this.isAvailable();
|
const available = await this.isAvailable();
|
||||||
const resolvedBinary = await findExecutable("codex");
|
const resolvedBinary = await findDefaultCodexBinary();
|
||||||
const entries: Array<{ label: string; value: string }> = [
|
const entries: Array<{ label: string; value: string }> = [
|
||||||
{
|
{
|
||||||
label: "Binary",
|
label: "Binary",
|
||||||
@@ -5448,6 +5501,8 @@ export const __codexAppServerInternals = {
|
|||||||
CodexAppServerClient,
|
CodexAppServerClient,
|
||||||
codexModelSupportsFastMode,
|
codexModelSupportsFastMode,
|
||||||
CodexAppServerAgentSession,
|
CodexAppServerAgentSession,
|
||||||
|
findCodexMicrosoftStoreBinary,
|
||||||
|
findDefaultCodexBinary,
|
||||||
formatCodexQuestionPrompts,
|
formatCodexQuestionPrompts,
|
||||||
mapCodexQuestionRequestToToolCall,
|
mapCodexQuestionRequestToToolCall,
|
||||||
mapCodexPatchNotificationToToolCall,
|
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 { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterEach, describe, expect, test } from "vitest";
|
import { afterEach, describe, expect, test } from "vitest";
|
||||||
@@ -9,10 +9,11 @@ import { AgentManager } from "../agent-manager.js";
|
|||||||
import { AgentStorage } from "../agent-storage.js";
|
import { AgentStorage } from "../agent-storage.js";
|
||||||
|
|
||||||
import { ClaudeAgentClient } from "./claude/agent.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";
|
import { OpenCodeAgentClient } from "./opencode-agent.js";
|
||||||
|
|
||||||
const originalEnv = {
|
const originalEnv = {
|
||||||
|
LOCALAPPDATA: process.env.LOCALAPPDATA,
|
||||||
PATH: process.env.PATH,
|
PATH: process.env.PATH,
|
||||||
PATHEXT: process.env.PATHEXT,
|
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(() => {
|
afterEach(() => {
|
||||||
|
if (originalEnv.LOCALAPPDATA === undefined) {
|
||||||
|
delete process.env.LOCALAPPDATA;
|
||||||
|
} else {
|
||||||
|
process.env.LOCALAPPDATA = originalEnv.LOCALAPPDATA;
|
||||||
|
}
|
||||||
process.env.PATH = originalEnv.PATH;
|
process.env.PATH = originalEnv.PATH;
|
||||||
process.env.PATHEXT = originalEnv.PATHEXT;
|
process.env.PATHEXT = originalEnv.PATHEXT;
|
||||||
for (const dir of tempDirs.splice(0)) {
|
for (const dir of tempDirs.splice(0)) {
|
||||||
@@ -42,12 +55,51 @@ afterEach(() => {
|
|||||||
describe("default provider availability", () => {
|
describe("default provider availability", () => {
|
||||||
test("Codex reports unavailable when the default command cannot be resolved", async () => {
|
test("Codex reports unavailable when the default command cannot be resolved", async () => {
|
||||||
const binDir = makeTempDir("provider-availability-codex-");
|
const binDir = makeTempDir("provider-availability-codex-");
|
||||||
isolatePathTo(binDir);
|
isolateCodexDefaultDiscoveryTo(binDir);
|
||||||
const client = new CodexAppServerAgentClient(createTestLogger());
|
const client = new CodexAppServerAgentClient(createTestLogger());
|
||||||
|
|
||||||
await expect(client.isAvailable()).resolves.toBe(false);
|
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 () => {
|
test("Claude reports unavailable when the default command cannot be resolved", async () => {
|
||||||
const binDir = makeTempDir("provider-availability-claude-");
|
const binDir = makeTempDir("provider-availability-claude-");
|
||||||
isolatePathTo(binDir);
|
isolatePathTo(binDir);
|
||||||
@@ -66,7 +118,7 @@ describe("default provider availability", () => {
|
|||||||
|
|
||||||
test("AgentManager reports Codex unavailable without throwing", async () => {
|
test("AgentManager reports Codex unavailable without throwing", async () => {
|
||||||
const binDir = makeTempDir("provider-availability-manager-bin-");
|
const binDir = makeTempDir("provider-availability-manager-bin-");
|
||||||
isolatePathTo(binDir);
|
isolateCodexDefaultDiscoveryTo(binDir);
|
||||||
const workdir = makeTempDir("provider-availability-manager-work-");
|
const workdir = makeTempDir("provider-availability-manager-work-");
|
||||||
const storage = new AgentStorage(join(workdir, "agents"), createTestLogger());
|
const storage = new AgentStorage(join(workdir, "agents"), createTestLogger());
|
||||||
const manager = new AgentManager({
|
const manager = new AgentManager({
|
||||||
@@ -88,7 +140,7 @@ describe("default provider availability", () => {
|
|||||||
|
|
||||||
test("resumeAgentFromPersistence stops before provider spawn when Codex is unavailable", async () => {
|
test("resumeAgentFromPersistence stops before provider spawn when Codex is unavailable", async () => {
|
||||||
const binDir = makeTempDir("provider-availability-resume-bin-");
|
const binDir = makeTempDir("provider-availability-resume-bin-");
|
||||||
isolatePathTo(binDir);
|
isolateCodexDefaultDiscoveryTo(binDir);
|
||||||
const workdir = makeTempDir("provider-availability-resume-work-");
|
const workdir = makeTempDir("provider-availability-resume-work-");
|
||||||
const storage = new AgentStorage(join(workdir, "agents"), createTestLogger());
|
const storage = new AgentStorage(join(workdir, "agents"), createTestLogger());
|
||||||
const manager = new AgentManager({
|
const manager = new AgentManager({
|
||||||
|
|||||||
Reference in New Issue
Block a user