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"))
|
const linkedTarget = await readlink(join(binDir, "omo-hook"))
|
||||||
expect(linkedTarget).toBe(join(pluginRoot, "dist", "cli.js"))
|
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 { basename, dirname, join, sep } from "node:path"
|
||||||
import type { InstalledPlugin, RunCommand } from "./types"
|
import type { InstalledPlugin, RunCommand } from "./types"
|
||||||
|
|
||||||
|
type LinkPlatform = NodeJS.Platform
|
||||||
|
|
||||||
export async function installCachedPlugin(input: {
|
export async function installCachedPlugin(input: {
|
||||||
readonly codexHome: string
|
readonly codexHome: string
|
||||||
readonly marketplaceName: string
|
readonly marketplaceName: string
|
||||||
@@ -38,18 +40,34 @@ export async function pruneMarketplaceCache(input: {
|
|||||||
export async function linkCachedPluginBins(input: {
|
export async function linkCachedPluginBins(input: {
|
||||||
readonly binDir: string
|
readonly binDir: string
|
||||||
readonly pluginRoot: string
|
readonly pluginRoot: string
|
||||||
|
readonly platform?: LinkPlatform
|
||||||
}): Promise<readonly { name: string; path: string; target: string }[]> {
|
}): Promise<readonly { name: string; path: string; target: string }[]> {
|
||||||
const binLinks = await discoverPackageBins(input.pluginRoot)
|
const binLinks = await discoverPackageBins(input.pluginRoot)
|
||||||
await mkdir(input.binDir, { recursive: true })
|
await mkdir(input.binDir, { recursive: true })
|
||||||
const linked: Array<{ name: string; path: string; target: string }> = []
|
const linked: Array<{ name: string; path: string; target: string }> = []
|
||||||
for (const link of binLinks) {
|
for (const link of binLinks) {
|
||||||
const linkPath = join(input.binDir, link.name)
|
const linkPath = await linkCachedPluginBin(input.binDir, link, input.platform ?? process.platform)
|
||||||
await replaceSymlink(linkPath, link.target)
|
|
||||||
linked.push({ name: link.name, path: linkPath, target: link.target })
|
linked.push({ name: link.name, path: linkPath, target: link.target })
|
||||||
}
|
}
|
||||||
return linked
|
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> {
|
export async function rewriteCachedMcpManifest(pluginRoot: string): Promise<void> {
|
||||||
const manifestPath = join(pluginRoot, ".mcp.json")
|
const manifestPath = join(pluginRoot, ".mcp.json")
|
||||||
if (!(await exists(manifestPath))) return
|
if (!(await exists(manifestPath))) return
|
||||||
@@ -143,6 +161,21 @@ async function replaceSymlink(linkPath: string, targetPath: string): Promise<voi
|
|||||||
await symlink(targetPath, linkPath)
|
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> {
|
async function existingNonSymlink(path: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const stat = await lstat(path)
|
const stat = await lstat(path)
|
||||||
|
|||||||
Reference in New Issue
Block a user