Files
claude-engineering-plugin/src/targets/codex.ts
Zac Williams c69c47fe9b fix: backup existing config files before overwriting (#119)
Before writing config.toml (Codex) or opencode.json (OpenCode), the CLI
attempts to create a timestamped backup of any existing config file.
This prevents accidental data loss when users have customized configs.

Backup is best-effort - if it fails (e.g., unusual permissions), the
install continues without blocking.

Backup files are named: config.toml.bak.2026-01-23T21-16-40-065Z
2026-02-08 16:58:51 -06:00

97 lines
3.2 KiB
TypeScript

import path from "path"
import { backupFile, copyDir, ensureDir, writeText } from "../utils/files"
import type { CodexBundle } from "../types/codex"
import type { ClaudeMcpServer } from "../types/claude"
export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle): Promise<void> {
const codexRoot = resolveCodexRoot(outputRoot)
await ensureDir(codexRoot)
if (bundle.prompts.length > 0) {
const promptsDir = path.join(codexRoot, "prompts")
for (const prompt of bundle.prompts) {
await writeText(path.join(promptsDir, `${prompt.name}.md`), prompt.content + "\n")
}
}
if (bundle.skillDirs.length > 0) {
const skillsRoot = path.join(codexRoot, "skills")
for (const skill of bundle.skillDirs) {
await copyDir(skill.sourceDir, path.join(skillsRoot, skill.name))
}
}
if (bundle.generatedSkills.length > 0) {
const skillsRoot = path.join(codexRoot, "skills")
for (const skill of bundle.generatedSkills) {
await writeText(path.join(skillsRoot, skill.name, "SKILL.md"), skill.content + "\n")
}
}
const config = renderCodexConfig(bundle.mcpServers)
if (config) {
const configPath = path.join(codexRoot, "config.toml")
const backupPath = await backupFile(configPath)
if (backupPath) {
console.log(`Backed up existing config to ${backupPath}`)
}
await writeText(configPath, config)
}
}
function resolveCodexRoot(outputRoot: string): string {
return path.basename(outputRoot) === ".codex" ? outputRoot : path.join(outputRoot, ".codex")
}
export function renderCodexConfig(mcpServers?: Record<string, ClaudeMcpServer>): string | null {
if (!mcpServers || Object.keys(mcpServers).length === 0) return null
const lines: string[] = ["# Generated by compound-plugin", ""]
for (const [name, server] of Object.entries(mcpServers)) {
const key = formatTomlKey(name)
lines.push(`[mcp_servers.${key}]`)
if (server.command) {
lines.push(`command = ${formatTomlString(server.command)}`)
if (server.args && server.args.length > 0) {
const args = server.args.map((arg) => formatTomlString(arg)).join(", ")
lines.push(`args = [${args}]`)
}
if (server.env && Object.keys(server.env).length > 0) {
lines.push("")
lines.push(`[mcp_servers.${key}.env]`)
for (const [envKey, value] of Object.entries(server.env)) {
lines.push(`${formatTomlKey(envKey)} = ${formatTomlString(value)}`)
}
}
} else if (server.url) {
lines.push(`url = ${formatTomlString(server.url)}`)
if (server.headers && Object.keys(server.headers).length > 0) {
lines.push(`http_headers = ${formatTomlInlineTable(server.headers)}`)
}
}
lines.push("")
}
return lines.join("\n")
}
function formatTomlString(value: string): string {
return JSON.stringify(value)
}
function formatTomlKey(value: string): string {
if (/^[A-Za-z0-9_-]+$/.test(value)) return value
return JSON.stringify(value)
}
function formatTomlInlineTable(entries: Record<string, string>): string {
const parts = Object.entries(entries).map(
([key, value]) => `${formatTomlKey(key)} = ${formatTomlString(value)}`,
)
return `{ ${parts.join(", ")} }`
}