mirror of
https://github.com/EveryInc/compound-engineering-plugin.git
synced 2026-06-19 15:41:46 +02:00
feat(codex): clean up orphan sidecar dirs for retained agents
When writing Codex custom agents, delete any same-basename sibling directory under agentsRoot if the retained agent declares zero sidecarDirs. Previously, cleanupRemovedAgents only swept the sibling directory when the TOML itself was being removed — a same-name agent that lost its sidecar between plugin versions left an orphan directory indefinitely. The new sweep is narrowly scoped: it only runs against base names of agents in the current install and only when the agent declares no sidecars, so unrelated directories under agentsRoot are untouched. Gated by isSafeManagedPath to match the existing cleanup safety pattern. Tests cover the three scenarios: orphan removed when agent has no sidecars, sidecar preserved when agent declares one, unrelated directory left alone. Per docs/plans/2026-04-21-001-refactor-flatten-agents-directory-plan.md Unit 4b. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+11
-2
@@ -91,10 +91,19 @@ export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle):
|
||||
await cleanupRemovedAgents(agentsRoot, manifest, currentAgents)
|
||||
if (agents.length > 0) {
|
||||
for (const agent of agents) {
|
||||
const agentFile = `${sanitizePathName(agent.name)}.toml`
|
||||
const agentBaseName = sanitizePathName(agent.name)
|
||||
const agentFile = `${agentBaseName}.toml`
|
||||
// If the agent declares no sidecars, remove any same-basename sibling
|
||||
// directory left behind by a prior install that did. The manifest-driven
|
||||
// cleanupRemovedAgents sweep above only removes the sibling dir when the
|
||||
// TOML itself is being removed; a same-name agent that loses its sidecar
|
||||
// would otherwise leave an orphan directory.
|
||||
if ((agent.sidecarDirs ?? []).length === 0 && isSafeManagedPath(agentsRoot, agentBaseName)) {
|
||||
await fs.rm(path.join(agentsRoot, agentBaseName), { recursive: true, force: true })
|
||||
}
|
||||
await writeText(path.join(agentsRoot, agentFile), renderCodexAgentToml(agent) + "\n")
|
||||
for (const sidecar of agent.sidecarDirs ?? []) {
|
||||
await copyDir(sidecar.sourceDir, path.join(agentsRoot, sanitizePathName(agent.name), sidecar.targetName))
|
||||
await copyDir(sidecar.sourceDir, path.join(agentsRoot, agentBaseName, sidecar.targetName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,6 +765,87 @@ Workflow handoff:
|
||||
expect(installedSkill).not.toContain("/prompts:settings")
|
||||
expect(installedSkill).not.toContain("https://prompts:www.proofeditor.ai")
|
||||
})
|
||||
|
||||
test("removes orphan sidecar dir when retained agent declares no sidecars", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-test-"))
|
||||
const agentsRoot = path.join(tempRoot, ".codex", "agents")
|
||||
const orphanDir = path.join(agentsRoot, "ce-foo", "stale-content")
|
||||
await fs.mkdir(orphanDir, { recursive: true })
|
||||
await fs.writeFile(path.join(orphanDir, "leftover.txt"), "stale", "utf8")
|
||||
await fs.writeFile(path.join(agentsRoot, "ce-foo.toml"), "old-toml", "utf8")
|
||||
|
||||
const bundle: CodexBundle = {
|
||||
prompts: [],
|
||||
skillDirs: [],
|
||||
generatedSkills: [],
|
||||
agents: [
|
||||
{
|
||||
name: "ce-foo",
|
||||
description: "Foo agent",
|
||||
instructions: "Do foo.",
|
||||
},
|
||||
],
|
||||
mcpServers: {},
|
||||
}
|
||||
|
||||
await writeCodexBundle(tempRoot, bundle)
|
||||
|
||||
expect(await entryExists(path.join(agentsRoot, "ce-foo"))).toBe(false)
|
||||
expect(await exists(path.join(agentsRoot, "ce-foo.toml"))).toBe(true)
|
||||
})
|
||||
|
||||
test("keeps sidecar dir when retained agent declares sidecars", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-test-"))
|
||||
const sidecarSource = await fs.mkdtemp(path.join(os.tmpdir(), "codex-sidecar-src-"))
|
||||
await fs.writeFile(path.join(sidecarSource, "script.sh"), "#!/bin/sh\necho hi\n", "utf8")
|
||||
|
||||
const bundle: CodexBundle = {
|
||||
prompts: [],
|
||||
skillDirs: [],
|
||||
generatedSkills: [],
|
||||
agents: [
|
||||
{
|
||||
name: "ce-foo",
|
||||
description: "Foo agent",
|
||||
instructions: "Do foo.",
|
||||
sidecarDirs: [{ sourceDir: sidecarSource, targetName: "scripts" }],
|
||||
},
|
||||
],
|
||||
mcpServers: {},
|
||||
}
|
||||
|
||||
await writeCodexBundle(tempRoot, bundle)
|
||||
|
||||
const agentsRoot = path.join(tempRoot, ".codex", "agents")
|
||||
expect(await exists(path.join(agentsRoot, "ce-foo.toml"))).toBe(true)
|
||||
expect(await exists(path.join(agentsRoot, "ce-foo", "scripts", "script.sh"))).toBe(true)
|
||||
})
|
||||
|
||||
test("leaves unrelated directories under agentsRoot alone", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-test-"))
|
||||
const agentsRoot = path.join(tempRoot, ".codex", "agents")
|
||||
const unrelatedDir = path.join(agentsRoot, "ce-bar-extra")
|
||||
await fs.mkdir(unrelatedDir, { recursive: true })
|
||||
await fs.writeFile(path.join(unrelatedDir, "keep-me.txt"), "keep", "utf8")
|
||||
|
||||
const bundle: CodexBundle = {
|
||||
prompts: [],
|
||||
skillDirs: [],
|
||||
generatedSkills: [],
|
||||
agents: [
|
||||
{
|
||||
name: "ce-foo",
|
||||
description: "Foo agent",
|
||||
instructions: "Do foo.",
|
||||
},
|
||||
],
|
||||
mcpServers: {},
|
||||
}
|
||||
|
||||
await writeCodexBundle(tempRoot, bundle)
|
||||
|
||||
expect(await exists(path.join(unrelatedDir, "keep-me.txt"))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("renderCodexConfig", () => {
|
||||
|
||||
Reference in New Issue
Block a user