fix(shared): migrate legacy config files atomically

Write the canonical config through a temp file and remove the legacy dual-file trap by archiving or deleting the old basename after migration.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-04-04 01:32:31 +09:00
parent d60ca63ed4
commit fedb73c736
+51 -5
View File
@@ -1,4 +1,4 @@
import { existsSync, copyFileSync, renameSync } from "node:fs"
import { closeSync, existsSync, fsyncSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
import { join, dirname, basename } from "node:path"
import { log } from "./logger"
@@ -10,6 +10,49 @@ function buildCanonicalPath(legacyPath: string): string {
return join(dir, `${CONFIG_BASENAME}${ext}`)
}
function writeFileAtomically(filePath: string, content: string): void {
const tempPath = `${filePath}.tmp`
writeFileSync(tempPath, content, "utf-8")
const tempFileDescriptor = openSync(tempPath, "r")
try {
fsyncSync(tempFileDescriptor)
} finally {
closeSync(tempFileDescriptor)
}
renameSync(tempPath, filePath)
}
function archiveLegacyConfigFile(legacyPath: string): boolean {
const backupPath = `${legacyPath}.bak`
try {
renameSync(legacyPath, backupPath)
log("[migrateLegacyConfigFile] Legacy config was migrated and renamed to backup. Update the canonical file only.", {
legacyPath,
backupPath,
})
return true
} catch (renameError) {
try {
rmSync(legacyPath)
log("[migrateLegacyConfigFile] Legacy config was migrated and removed after backup rename failed. Update the canonical file only.", {
legacyPath,
backupPath,
renameError,
})
return true
} catch (removeError) {
log("[migrateLegacyConfigFile] WARNING: canonical config was written but the legacy file still exists and will be ignored. Remove or rename it manually.", {
legacyPath,
backupPath,
renameError,
removeError,
})
return false
}
}
}
export function migrateLegacyConfigFile(legacyPath: string): boolean {
if (!existsSync(legacyPath)) return false
if (!basename(legacyPath).startsWith(LEGACY_CONFIG_BASENAME)) return false
@@ -18,14 +61,17 @@ export function migrateLegacyConfigFile(legacyPath: string): boolean {
if (existsSync(canonicalPath)) return false
try {
copyFileSync(legacyPath, canonicalPath)
log("[migrateLegacyConfigFile] Copied legacy config to canonical path", {
const content = readFileSync(legacyPath, "utf-8")
writeFileAtomically(canonicalPath, content)
const archivedLegacyConfig = archiveLegacyConfigFile(legacyPath)
log("[migrateLegacyConfigFile] Migrated legacy config to canonical path", {
from: legacyPath,
to: canonicalPath,
archivedLegacyConfig,
})
return true
return archivedLegacyConfig
} catch (error) {
log("[migrateLegacyConfigFile] Failed to copy legacy config file", { legacyPath, error })
log("[migrateLegacyConfigFile] Failed to migrate legacy config file", { legacyPath, error })
return false
}
}