fix(cli): write Codex bin command shims on Windows

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-05-26 11:18:47 +09:00
parent cc98f90bd0
commit 22bcb4b45a
2 changed files with 55 additions and 2 deletions
+20
View File
@@ -44,4 +44,24 @@ describe("codex-cache", () => {
const linkedTarget = await readlink(join(binDir, "omo-hook"))
expect(linkedTarget).toBe(join(pluginRoot, "dist", "cli.js"))
})
test("writes Windows command shims for cached plugin bins", async () => {
// given
const root = await mkdtemp(join(tmpdir(), "omo-codex-cache-"))
const pluginRoot = join(root, "plugin")
const binDir = join(root, "bin")
await mkdir(pluginRoot, { recursive: true })
await writeFile(join(pluginRoot, "package.json"), JSON.stringify({ name: "@scope/omo", bin: { "omo-hook": "dist/cli.js" } }))
await mkdir(join(pluginRoot, "dist"), { recursive: true })
await writeFile(join(pluginRoot, "dist", "cli.js"), "#!/usr/bin/env node\n")
// when
const linked = await linkCachedPluginBins({ binDir, pluginRoot, platform: "win32" })
// then
expect(linked).toEqual([{ name: "omo-hook", path: join(binDir, "omo-hook.cmd"), target: join(pluginRoot, "dist", "cli.js") }])
const commandShim = await readFile(join(binDir, "omo-hook.cmd"), "utf8")
expect(commandShim).toContain("@echo off")
expect(commandShim).toContain(`node "${join(pluginRoot, "dist", "cli.js")}" %*`)
})
})
+35 -2
View File
@@ -2,6 +2,8 @@ import { cp, lstat, mkdir, readFile, readdir, readlink, rename, rm, symlink, wri
import { basename, dirname, join, sep } from "node:path"
import type { InstalledPlugin, RunCommand } from "./types"
type LinkPlatform = NodeJS.Platform
export async function installCachedPlugin(input: {
readonly codexHome: string
readonly marketplaceName: string
@@ -38,18 +40,34 @@ export async function pruneMarketplaceCache(input: {
export async function linkCachedPluginBins(input: {
readonly binDir: string
readonly pluginRoot: string
readonly platform?: LinkPlatform
}): Promise<readonly { name: string; path: string; target: string }[]> {
const binLinks = await discoverPackageBins(input.pluginRoot)
await mkdir(input.binDir, { recursive: true })
const linked: Array<{ name: string; path: string; target: string }> = []
for (const link of binLinks) {
const linkPath = join(input.binDir, link.name)
await replaceSymlink(linkPath, link.target)
const linkPath = await linkCachedPluginBin(input.binDir, link, input.platform ?? process.platform)
linked.push({ name: link.name, path: linkPath, target: link.target })
}
return linked
}
async function linkCachedPluginBin(
binDir: string,
link: { readonly name: string; readonly target: string },
platform: LinkPlatform,
): Promise<string> {
if (platform === "win32") {
const linkPath = join(binDir, `${link.name}.cmd`)
await replaceCommandShim(linkPath, link.target)
return linkPath
}
const linkPath = join(binDir, link.name)
await replaceSymlink(linkPath, link.target)
return linkPath
}
export async function rewriteCachedMcpManifest(pluginRoot: string): Promise<void> {
const manifestPath = join(pluginRoot, ".mcp.json")
if (!(await exists(manifestPath))) return
@@ -143,6 +161,21 @@ async function replaceSymlink(linkPath: string, targetPath: string): Promise<voi
await symlink(targetPath, linkPath)
}
async function replaceCommandShim(linkPath: string, targetPath: string): Promise<void> {
if (await existingNonShim(linkPath)) throw new Error(`${linkPath} already exists and is not a command shim`)
await writeFile(linkPath, `@echo off\r\nnode "${targetPath}" %*\r\n`)
}
async function existingNonShim(path: string): Promise<boolean> {
try {
const stat = await lstat(path)
return !stat.isFile()
} catch (error) {
if (isNodeErrorWithCode(error) && error.code === "ENOENT") return false
throw error
}
}
async function existingNonSymlink(path: string): Promise<boolean> {
try {
const stat = await lstat(path)