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:
@@ -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")}" %*`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user