Merge pull request #855 from luojiyin1987/fix/doctor-windows-opencode

fix: handle opencode.ps1 in doctor on Windows
This commit is contained in:
Kenny
2026-01-16 15:36:58 -05:00
committed by GitHub
2 changed files with 153 additions and 5 deletions

View File

@@ -43,6 +43,94 @@ describe("opencode check", () => {
})
})
describe("command helpers", () => {
it("selects where on Windows", () => {
// #given win32 platform
// #when selecting lookup command
// #then should use where
expect(opencode.getBinaryLookupCommand("win32")).toBe("where")
})
it("selects which on non-Windows", () => {
// #given linux platform
// #when selecting lookup command
// #then should use which
expect(opencode.getBinaryLookupCommand("linux")).toBe("which")
expect(opencode.getBinaryLookupCommand("darwin")).toBe("which")
})
it("parses command output into paths", () => {
// #given raw output with multiple lines and spaces
const output = "C:\\\\bin\\\\opencode.ps1\r\nC:\\\\bin\\\\opencode.exe\n\n"
// #when parsing
const paths = opencode.parseBinaryPaths(output)
// #then should return trimmed, non-empty paths
expect(paths).toEqual(["C:\\\\bin\\\\opencode.ps1", "C:\\\\bin\\\\opencode.exe"])
})
it("prefers exe/cmd/bat over ps1 on Windows", () => {
// #given windows paths
const paths = [
"C:\\\\bin\\\\opencode.ps1",
"C:\\\\bin\\\\opencode.cmd",
"C:\\\\bin\\\\opencode.exe",
]
// #when selecting binary
const selected = opencode.selectBinaryPath(paths, "win32")
// #then should prefer exe
expect(selected).toBe("C:\\\\bin\\\\opencode.exe")
})
it("falls back to ps1 when it is the only Windows candidate", () => {
// #given only ps1 path
const paths = ["C:\\\\bin\\\\opencode.ps1"]
// #when selecting binary
const selected = opencode.selectBinaryPath(paths, "win32")
// #then should return ps1 path
expect(selected).toBe("C:\\\\bin\\\\opencode.ps1")
})
it("builds PowerShell command for ps1 on Windows", () => {
// #given a ps1 path on Windows
const command = opencode.buildVersionCommand(
"C:\\\\bin\\\\opencode.ps1",
"win32"
)
// #when building command
// #then should use PowerShell
expect(command).toEqual([
"powershell",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"C:\\\\bin\\\\opencode.ps1",
"--version",
])
})
it("builds direct command for non-ps1 binaries", () => {
// #given an exe on Windows and a binary on linux
const winCommand = opencode.buildVersionCommand(
"C:\\\\bin\\\\opencode.exe",
"win32"
)
const linuxCommand = opencode.buildVersionCommand("opencode", "linux")
// #when building commands
// #then should execute directly
expect(winCommand).toEqual(["C:\\\\bin\\\\opencode.exe", "--version"])
expect(linuxCommand).toEqual(["opencode", "--version"])
})
})
describe("getOpenCodeInfo", () => {
it("returns installed: false when binary not found", async () => {
// #given no opencode binary

View File

@@ -1,14 +1,70 @@
import type { CheckResult, CheckDefinition, OpenCodeInfo } from "../types"
import { CHECK_IDS, CHECK_NAMES, MIN_OPENCODE_VERSION, OPENCODE_BINARIES } from "../constants"
const WINDOWS_EXECUTABLE_EXTS = [".exe", ".cmd", ".bat", ".ps1"]
export function getBinaryLookupCommand(platform: NodeJS.Platform): "which" | "where" {
return platform === "win32" ? "where" : "which"
}
export function parseBinaryPaths(output: string): string[] {
return output
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0)
}
export function selectBinaryPath(
paths: string[],
platform: NodeJS.Platform
): string | null {
if (paths.length === 0) return null
if (platform !== "win32") return paths[0]
const normalized = paths.map((path) => path.toLowerCase())
for (const ext of WINDOWS_EXECUTABLE_EXTS) {
const index = normalized.findIndex((path) => path.endsWith(ext))
if (index !== -1) return paths[index]
}
return paths[0]
}
export function buildVersionCommand(
binaryPath: string,
platform: NodeJS.Platform
): string[] {
if (
platform === "win32" &&
binaryPath.toLowerCase().endsWith(".ps1")
) {
return [
"powershell",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
binaryPath,
"--version",
]
}
return [binaryPath, "--version"]
}
export async function findOpenCodeBinary(): Promise<{ binary: string; path: string } | null> {
for (const binary of OPENCODE_BINARIES) {
try {
const proc = Bun.spawn(["which", binary], { stdout: "pipe", stderr: "pipe" })
const lookupCommand = getBinaryLookupCommand(process.platform)
const proc = Bun.spawn([lookupCommand, binary], { stdout: "pipe", stderr: "pipe" })
const output = await new Response(proc.stdout).text()
await proc.exited
if (proc.exitCode === 0) {
return { binary, path: output.trim() }
const paths = parseBinaryPaths(output)
const selectedPath = selectBinaryPath(paths, process.platform)
if (selectedPath) {
return { binary, path: selectedPath }
}
}
} catch {
continue
@@ -17,9 +73,13 @@ export async function findOpenCodeBinary(): Promise<{ binary: string; path: stri
return null
}
export async function getOpenCodeVersion(binary: string): Promise<string | null> {
export async function getOpenCodeVersion(
binaryPath: string,
platform: NodeJS.Platform = process.platform
): Promise<string | null> {
try {
const proc = Bun.spawn([binary, "--version"], { stdout: "pipe", stderr: "pipe" })
const command = buildVersionCommand(binaryPath, platform)
const proc = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" })
const output = await new Response(proc.stdout).text()
await proc.exited
if (proc.exitCode === 0) {
@@ -61,7 +121,7 @@ export async function getOpenCodeInfo(): Promise<OpenCodeInfo> {
}
}
const version = await getOpenCodeVersion(binaryInfo.binary)
const version = await getOpenCodeVersion(binaryInfo.path ?? binaryInfo.binary)
return {
installed: true,