diff --git a/src/targets/codex.ts b/src/targets/codex.ts index f38a4dd..9e8ba8b 100644 --- a/src/targets/codex.ts +++ b/src/targets/codex.ts @@ -1,5 +1,5 @@ import path from "path" -import { copyDir, ensureDir, writeText } from "../utils/files" +import { backupFile, copyDir, ensureDir, writeText } from "../utils/files" import type { CodexBundle } from "../types/codex" import type { ClaudeMcpServer } from "../types/claude" @@ -30,7 +30,12 @@ export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle): const config = renderCodexConfig(bundle.mcpServers) if (config) { - await writeText(path.join(codexRoot, "config.toml"), 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) } } diff --git a/src/targets/opencode.ts b/src/targets/opencode.ts index 09f372a..24e8faf 100644 --- a/src/targets/opencode.ts +++ b/src/targets/opencode.ts @@ -1,10 +1,15 @@ import path from "path" -import { copyDir, ensureDir, writeJson, writeText } from "../utils/files" +import { backupFile, copyDir, ensureDir, writeJson, writeText } from "../utils/files" import type { OpenCodeBundle } from "../types/opencode" export async function writeOpenCodeBundle(outputRoot: string, bundle: OpenCodeBundle): Promise { const paths = resolveOpenCodePaths(outputRoot) await ensureDir(paths.root) + + const backupPath = await backupFile(paths.configPath) + if (backupPath) { + console.log(`Backed up existing config to ${backupPath}`) + } await writeJson(paths.configPath, bundle.config) const agentsDir = paths.agentsDir diff --git a/src/utils/files.ts b/src/utils/files.ts index 5fd1453..9994d0c 100644 --- a/src/utils/files.ts +++ b/src/utils/files.ts @@ -1,6 +1,19 @@ import { promises as fs } from "fs" import path from "path" +export async function backupFile(filePath: string): Promise { + if (!(await pathExists(filePath))) return null + + try { + const timestamp = new Date().toISOString().replace(/[:.]/g, "-") + const backupPath = `${filePath}.bak.${timestamp}` + await fs.copyFile(filePath, backupPath) + return backupPath + } catch { + return null + } +} + export async function pathExists(filePath: string): Promise { try { await fs.access(filePath) diff --git a/tests/codex-writer.test.ts b/tests/codex-writer.test.ts index ad2f03a..3aeb42e 100644 --- a/tests/codex-writer.test.ts +++ b/tests/codex-writer.test.ts @@ -73,4 +73,36 @@ describe("writeCodexBundle", () => { expect(await exists(path.join(codexRoot, "prompts", "command-one.md"))).toBe(true) expect(await exists(path.join(codexRoot, "skills", "skill-one", "SKILL.md"))).toBe(true) }) + + test("backs up existing config.toml before overwriting", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-backup-")) + const codexRoot = path.join(tempRoot, ".codex") + const configPath = path.join(codexRoot, "config.toml") + + // Create existing config + await fs.mkdir(codexRoot, { recursive: true }) + const originalContent = "# My original config\n[custom]\nkey = \"value\"\n" + await fs.writeFile(configPath, originalContent) + + const bundle: CodexBundle = { + prompts: [], + skillDirs: [], + generatedSkills: [], + mcpServers: { test: { command: "echo" } }, + } + + await writeCodexBundle(codexRoot, bundle) + + // New config should be written + const newConfig = await fs.readFile(configPath, "utf8") + expect(newConfig).toContain("[mcp_servers.test]") + + // Backup should exist with original content + const files = await fs.readdir(codexRoot) + const backupFileName = files.find((f) => f.startsWith("config.toml.bak.")) + expect(backupFileName).toBeDefined() + + const backupContent = await fs.readFile(path.join(codexRoot, backupFileName!), "utf8") + expect(backupContent).toBe(originalContent) + }) }) diff --git a/tests/opencode-writer.test.ts b/tests/opencode-writer.test.ts index c481520..0bafcc0 100644 --- a/tests/opencode-writer.test.ts +++ b/tests/opencode-writer.test.ts @@ -84,4 +84,36 @@ describe("writeOpenCodeBundle", () => { expect(await exists(path.join(outputRoot, "skills", "skill-one", "SKILL.md"))).toBe(true) expect(await exists(path.join(outputRoot, ".opencode"))).toBe(false) }) + + test("backs up existing opencode.json before overwriting", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-backup-")) + const outputRoot = path.join(tempRoot, ".opencode") + const configPath = path.join(outputRoot, "opencode.json") + + // Create existing config + await fs.mkdir(outputRoot, { recursive: true }) + const originalConfig = { $schema: "https://opencode.ai/config.json", custom: "value" } + await fs.writeFile(configPath, JSON.stringify(originalConfig, null, 2)) + + const bundle: OpenCodeBundle = { + config: { $schema: "https://opencode.ai/config.json", new: "config" }, + agents: [], + plugins: [], + skillDirs: [], + } + + await writeOpenCodeBundle(outputRoot, bundle) + + // New config should be written + const newConfig = JSON.parse(await fs.readFile(configPath, "utf8")) + expect(newConfig.new).toBe("config") + + // Backup should exist with original content + const files = await fs.readdir(outputRoot) + const backupFileName = files.find((f) => f.startsWith("opencode.json.bak.")) + expect(backupFileName).toBeDefined() + + const backupContent = JSON.parse(await fs.readFile(path.join(outputRoot, backupFileName!), "utf8")) + expect(backupContent.custom).toBe("value") + }) })