'use strict'; const crypto = require('crypto'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { execFileSync } = require('child_process'); const SCHEMA = 'ecc.codex-legacy-sync.v1'; const BEGIN_MARKER = ''; const END_MARKER = ''; function getStatePath(codexHome) { return path.join(codexHome, 'ecc', 'legacy-sync-state.json'); } function openRegularFileNoFollow(filePath, writable = false) { const noFollow = fs.constants.O_NOFOLLOW || 0; const flags = (writable ? fs.constants.O_RDWR : fs.constants.O_RDONLY) | noFollow; let pathStat; try { pathStat = fs.lstatSync(filePath, { bigint: true }); } catch (error) { if (error.code === 'ENOENT') return null; throw error; } if (!pathStat.isFile() || pathStat.isSymbolicLink()) { throw new Error(`Refusing to manage non-regular legacy sync path: ${filePath}`); } let descriptor; try { descriptor = fs.openSync(filePath, flags); } catch (error) { if (error.code === 'ENOENT') return null; if (error.code === 'ELOOP') { throw new Error(`Refusing to manage non-regular legacy sync path: ${filePath}`); } throw error; } const descriptorStat = fs.fstatSync(descriptor, { bigint: true }); let finalPathStat; try { finalPathStat = fs.lstatSync(filePath, { bigint: true }); } catch (error) { fs.closeSync(descriptor); if (error.code === 'ENOENT') { throw new Error(`Legacy sync path changed while opening: ${filePath}`); } throw error; } if ( !descriptorStat.isFile() || !finalPathStat.isFile() || finalPathStat.isSymbolicLink() || descriptorStat.dev !== pathStat.dev || descriptorStat.ino !== pathStat.ino || descriptorStat.dev !== finalPathStat.dev || descriptorStat.ino !== finalPathStat.ino ) { fs.closeSync(descriptor); throw new Error(`Refusing to manage non-regular legacy sync path: ${filePath}`); } return { descriptor, stat: fs.fstatSync(descriptor) }; } function readRegularFileNoFollow(filePath, encoding = null) { const opened = openRegularFileNoFollow(filePath); if (!opened) return null; try { return { content: fs.readFileSync(opened.descriptor, encoding || undefined), mode: opened.stat.mode & 0o777, }; } finally { fs.closeSync(opened.descriptor); } } function replaceOpenedRegularFile(opened, content, mode = null) { const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content); fs.ftruncateSync(opened.descriptor, 0); fs.writeSync(opened.descriptor, buffer, 0, buffer.length, 0); if (mode) fs.fchmodSync(opened.descriptor, mode); fs.fsyncSync(opened.descriptor); } function createRegularFileNoFollow(filePath, content, mode = 0o600) { const noFollow = fs.constants.O_NOFOLLOW || 0; const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow; const descriptor = fs.openSync(filePath, flags, mode); try { const stat = fs.fstatSync(descriptor); if (!stat.isFile()) { throw new Error(`Refusing to create non-regular legacy sync path: ${filePath}`); } fs.writeFileSync(descriptor, content); fs.fchmodSync(descriptor, mode); fs.fsyncSync(descriptor); } finally { fs.closeSync(descriptor); } } function removeOpenedRegularFile(filePath, opened) { const quarantineDir = fs.mkdtempSync(path.join(path.dirname(filePath), '.ecc-remove-')); const quarantinePath = path.join(quarantineDir, path.basename(filePath)); fs.renameSync(filePath, quarantinePath); const quarantined = openRegularFileNoFollow(quarantinePath); const openedStat = fs.fstatSync(opened.descriptor, { bigint: true }); const quarantinedStat = fs.fstatSync(quarantined.descriptor, { bigint: true }); fs.closeSync(quarantined.descriptor); if (quarantinedStat.dev !== openedStat.dev || quarantinedStat.ino !== openedStat.ino) { try { fs.linkSync(quarantinePath, filePath); fs.unlinkSync(quarantinePath); fs.rmdirSync(quarantineDir); } catch (_restoreError) { throw new Error( `Legacy sync path changed before removal; preserved replacement at ${quarantinePath}` ); } throw new Error(`Legacy sync path changed before removal: ${filePath}`); } fs.unlinkSync(quarantinePath); fs.rmdirSync(quarantineDir); } function atomicWriteJson(filePath, value) { fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 }); const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`; fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 }); fs.renameSync(tempPath, filePath); } function readState(statePath) { const snapshot = readRegularFileNoFollow(statePath, 'utf8'); if (!snapshot) throw new Error(`Legacy Codex sync state not found at ${statePath}`); return parseState(snapshot.content, statePath); } function readStateIfPresent(statePath) { const snapshot = readRegularFileNoFollow(statePath, 'utf8'); return snapshot ? parseState(snapshot.content, statePath) : null; } function parseState(content, statePath) { const state = JSON.parse(content); if (state.schema !== SCHEMA || !Array.isArray(state.paths)) { throw new Error(`Invalid legacy Codex sync state at ${statePath}`); } return state; } function hasUnsafeManagedAncestor(filePath, codexHome) { const relativePath = path.relative(codexHome, filePath); if (relativePath === '' || relativePath.startsWith('..') || path.isAbsolute(relativePath)) { return relativePath !== ''; } const segments = relativePath.split(path.sep).slice(0, -1); let currentPath = codexHome; for (const segment of [null, ...segments]) { if (segment !== null) currentPath = path.join(currentPath, segment); try { const stat = fs.lstatSync(currentPath); if (stat.isSymbolicLink() || !stat.isDirectory()) return true; } catch (error) { if (error.code === 'ENOENT') break; throw error; } } return false; } function isWithinRoot(filePath, rootPath) { const relativePath = path.relative(rootPath, filePath); return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)); } function getTrustedRoot(state, filePath) { const roots = Array.isArray(state.trustedRoots) && state.trustedRoots.length > 0 ? state.trustedRoots : [state.codexHome]; return roots .map(rootPath => path.resolve(rootPath)) .find(rootPath => isWithinRoot(filePath, rootPath)) || null; } function snapshotLegacyPath(filePath) { const snapshot = readRegularFileNoFollow(filePath); const previousType = snapshot ? 'file' : 'missing'; return { path: filePath, installedSha256: null, previousType, previousContentBase64: snapshot ? snapshot.content.toString('base64') : null, previousMode: snapshot ? snapshot.mode : null, }; } function assertInstalledStateUnmodified(state) { for (const entry of state.paths) { const filePath = path.resolve(entry.path); const trustedRoot = getTrustedRoot(state, filePath); if (!trustedRoot || hasUnsafeManagedAncestor(filePath, trustedRoot)) { throw new Error(`Refusing to reuse unsafe legacy Codex ownership path: ${filePath}`); } const snapshot = readRegularFileNoFollow(filePath); if (!entry.installedSha256) { if (snapshot) throw new Error(`Refusing to replace modified legacy Codex artifact: ${filePath}`); continue; } const digest = snapshot ? crypto.createHash('sha256').update(snapshot.content).digest('hex') : null; if (digest !== entry.installedSha256) { throw new Error(`Refusing to replace modified legacy Codex artifact: ${filePath}`); } } } function beginLegacySyncState(options) { const codexHome = path.resolve(options.codexHome); const statePath = getStatePath(codexHome); const configPath = path.join(codexHome, 'config.toml'); const agentsPath = path.join(codexHome, 'AGENTS.md'); const installedHooksPath = options.installedHooksPath ? path.resolve(options.installedHooksPath) : null; const priorState = readStateIfPresent(statePath); if (priorState && priorState.status !== 'installed') { throw new Error(`Legacy Codex sync state requires recovery before reinstall: ${statePath}`); } if (priorState) assertInstalledStateUnmodified(priorState); const trustedRoots = [...new Set([ codexHome, ...(Array.isArray(priorState?.trustedRoots) ? priorState.trustedRoots : []), ...(priorState?.installedHooksPath ? [priorState.installedHooksPath] : []), ...(installedHooksPath ? [installedHooksPath] : []), ].map(rootPath => path.resolve(rootPath)))]; const state = priorState ? { ...priorState, status: 'applying', updatedAt: new Date().toISOString(), backupDir: options.backupDir ? path.resolve(options.backupDir) : priorState.backupDir, installedHooksPath, trustedRoots, rollbackPreviousHooksPath: options.previousHooksPath || null, rollbackPaths: priorState.paths.map(entry => snapshotLegacyPath(path.resolve(entry.path))), previousInstalledState: priorState, } : { schema: SCHEMA, status: 'applying', createdAt: new Date().toISOString(), codexHome, backupDir: options.backupDir ? path.resolve(options.backupDir) : null, previousHooksPath: options.previousHooksPath || null, installedHooksPath, trustedRoots, before: {}, paths: [], rollbackPaths: [], }; for (const [key, filePath] of [['config', configPath], ['agents', agentsPath]]) { if (priorState) break; const snapshot = readRegularFileNoFollow(filePath, 'utf8'); state.before[key] = snapshot ? snapshot.content : null; } atomicWriteJson(statePath, state); return statePath; } function recordLegacySyncPath(options) { const state = readState(options.statePath); const filePath = path.resolve(options.filePath); const trustedRoot = getTrustedRoot(state, filePath); if (!trustedRoot) { throw new Error(`Refusing to record a legacy sync path outside trusted roots: ${filePath}`); } if (hasUnsafeManagedAncestor(filePath, trustedRoot)) { throw new Error(`Refusing to manage legacy sync path through symlinked ancestor: ${filePath}`); } if (!state.paths.some(entry => entry.path === filePath)) { const snapshot = snapshotLegacyPath(filePath); state.paths.push(snapshot); state.rollbackPaths = [...(state.rollbackPaths || []), { ...snapshot }]; atomicWriteJson(options.statePath, state); } } function rollbackLegacyCodexSync(options) { const state = readState(options.statePath); const restoredPaths = []; const retainedPaths = []; const rollbackPaths = Array.isArray(state.rollbackPaths) ? state.rollbackPaths : state.paths; for (const entry of [...rollbackPaths].reverse()) { const filePath = path.resolve(entry.path); const trustedRoot = getTrustedRoot(state, filePath); if (!trustedRoot) { retainedPaths.push(filePath); continue; } if (hasUnsafeManagedAncestor(filePath, trustedRoot)) { retainedPaths.push(filePath); continue; } let opened = null; try { opened = openRegularFileNoFollow(filePath, true); } catch (_error) { retainedPaths.push(filePath); continue; } if (entry.previousType === 'file' && typeof entry.previousContentBase64 === 'string') { const previousContent = Buffer.from(entry.previousContentBase64, 'base64'); const previousMode = entry.previousMode || 0o600; fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 }); if (opened) { try { replaceOpenedRegularFile(opened, previousContent, previousMode); } finally { fs.closeSync(opened.descriptor); } } else { createRegularFileNoFollow(filePath, previousContent, previousMode); } restoredPaths.push(filePath); } else if (entry.previousType === 'missing' || entry.previousType === undefined) { if (opened) { try { removeOpenedRegularFile(filePath, opened); } finally { fs.closeSync(opened.descriptor); } } restoredPaths.push(filePath); } else { if (opened) fs.closeSync(opened.descriptor); retainedPaths.push(filePath); } } if (state.installedHooksPath) { const getHooks = options.getGlobalHooksPath || defaultGetHooksPath; const setHooks = options.setGlobalHooksPath || defaultSetHooksPath; const currentHooks = getHooks(); if (currentHooks && path.resolve(currentHooks) === path.resolve(state.installedHooksPath)) { setHooks(state.rollbackPreviousHooksPath ?? state.previousHooksPath ?? ''); } else if (currentHooks && currentHooks !== (state.rollbackPreviousHooksPath ?? state.previousHooksPath)) { retainedPaths.push(`git:core.hooksPath=${currentHooks}`); } } if (retainedPaths.length === 0) { if (state.previousInstalledState) { atomicWriteJson(options.statePath, state.previousInstalledState); } else { fs.rmSync(options.statePath, { force: true }); } } return { status: retainedPaths.length === 0 ? 'rolled-back' : 'partial', statePath: options.statePath, restoredPaths, retainedPaths: [...new Set(retainedPaths)].sort(), }; } function finalizeLegacySyncState(options) { const state = readState(options.statePath); state.status = 'installed'; state.installedAt = new Date().toISOString(); delete state.rollbackPaths; delete state.rollbackPreviousHooksPath; delete state.previousInstalledState; state.paths = state.paths.map(entry => { const trustedRoot = getTrustedRoot(state, path.resolve(entry.path)); let installedSha256 = null; if (trustedRoot && !hasUnsafeManagedAncestor(entry.path, trustedRoot)) { try { const snapshot = readRegularFileNoFollow(entry.path); installedSha256 = snapshot ? crypto.createHash('sha256').update(snapshot.content).digest('hex') : null; } catch (_error) { installedSha256 = null; } } return { ...entry, installedSha256 }; }); atomicWriteJson(options.statePath, state); return state; } function stripMarkerBlock(content) { const markers = []; let fence = null; let offset = 0; for (const lineWithEnding of content.match(/.*(?:\r?\n|$)/g) || []) { if (lineWithEnding === '') continue; const line = lineWithEnding.replace(/\r?\n$/, ''); const fenceMatch = line.match(/^\s*(`{3,}|~{3,})(.*)$/); if (fenceMatch) { const run = fenceMatch[1]; const marker = run[0]; if (!fence) { fence = { marker, length: run.length }; } else if ( marker === fence.marker && run.length >= fence.length && fenceMatch[2].trim() === '' ) { fence = null; } } else if (!fence && (line === BEGIN_MARKER || line === END_MARKER)) { markers.push({ marker: line, index: offset }); } offset += lineWithEnding.length; } const begins = markers.filter(match => match.marker === BEGIN_MARKER); const ends = markers.filter(match => match.marker === END_MARKER); if (begins.length !== 1 || ends.length !== 1 || ends[0].index < begins[0].index) { return content; } const suffixStart = ends[0].index + END_MARKER.length; const suffixWithLineEnding = content.slice(suffixStart).replace(/^\r?\n/, ''); return `${content.slice(0, begins[0].index)}${suffixWithLineEnding}`; } function defaultGetHooksPath() { try { return execFileSync('git', ['config', '--global', '--get', 'core.hooksPath'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000, }).trim(); } catch (_error) { return ''; } } function defaultSetHooksPath(value) { const args = value ? ['config', '--global', 'core.hooksPath', value] : ['config', '--global', '--unset-all', 'core.hooksPath']; try { execFileSync('git', args, { stdio: 'ignore', timeout: 5000 }); } catch (error) { if (value || error.status !== 5) throw error; } } function listLegacyCandidates(codexHome) { const candidates = []; const promptsDir = path.join(codexHome, 'prompts'); if (fs.existsSync(promptsDir)) { for (const entry of fs.readdirSync(promptsDir)) { if (entry.startsWith('ecc-') || entry.startsWith('ecc_') || entry.includes('ecc-rules-pack')) { candidates.push(path.join(promptsDir, entry)); } } } for (const relativePath of [ 'docs/CODEX-NAVIGATION-GUIDE.md', 'docs/COMMAND-AGENT-MAP.md', 'COMMANDS-QUICK-REF.md', 'CONTRIBUTING.md', '.github/PULL_REQUEST_TEMPLATE.md', 'ecc-prompts-manifest.txt', 'ecc-extension-prompts-manifest.txt', ]) { const candidate = path.join(codexHome, relativePath); if (fs.existsSync(candidate)) candidates.push(candidate); } return candidates; } function uninstallLegacyCodexSync(options = {}) { const codexHome = path.resolve(options.codexHome || process.env.CODEX_HOME || path.join(process.env.HOME || os.homedir(), '.codex')); const statePath = getStatePath(codexHome); const dryRun = options.dryRun === true; const retainedPaths = []; const plannedRemovals = []; const removedPaths = []; const agentsPath = path.join(codexHome, 'AGENTS.md'); const state = readStateIfPresent(statePath); if (!state) { let openedAgents = null; try { openedAgents = openRegularFileNoFollow(agentsPath, !dryRun); if (openedAgents) { const content = fs.readFileSync(openedAgents.descriptor, 'utf8'); const stripped = stripMarkerBlock(content); if (stripped !== content) { plannedRemovals.push(`${agentsPath}#ecc-marker-block`); if (!dryRun) replaceOpenedRegularFile(openedAgents, stripped, openedAgents.stat.mode & 0o777); } } } catch (_error) { retainedPaths.push(agentsPath); } finally { if (openedAgents) fs.closeSync(openedAgents.descriptor); } retainedPaths.push(...listLegacyCandidates(codexHome)); return { status: dryRun ? 'planned' : retainedPaths.length > 0 ? 'partial' : plannedRemovals.length > 0 ? 'uninstalled' : 'not-found', statePath: null, plannedRemovals, removedPaths, retainedPaths: [...new Set(retainedPaths)].sort(), warnings: retainedPaths.length > 0 ? ['Legacy Codex artifacts without an ownership manifest were preserved for manual review.'] : [], }; } for (const entry of state.paths) { const filePath = path.resolve(entry.path); const trustedRoot = getTrustedRoot(state, filePath); if (!trustedRoot) { retainedPaths.push(filePath); continue; } if (hasUnsafeManagedAncestor(filePath, trustedRoot)) { retainedPaths.push(filePath); continue; } let opened = null; try { opened = openRegularFileNoFollow(filePath, !dryRun); } catch (_error) { retainedPaths.push(filePath); continue; } if (!opened) continue; const currentContent = fs.readFileSync(opened.descriptor); const matches = entry.installedSha256 ? crypto.createHash('sha256').update(currentContent).digest('hex') === entry.installedSha256 : false; if (!matches) { fs.closeSync(opened.descriptor); retainedPaths.push(filePath); continue; } plannedRemovals.push(filePath); if (!dryRun) { if (entry.previousType === 'file' && typeof entry.previousContentBase64 === 'string') { replaceOpenedRegularFile( opened, Buffer.from(entry.previousContentBase64, 'base64'), entry.previousMode || 0o600 ); } else if (entry.previousType === 'missing' || entry.previousType === undefined) { removeOpenedRegularFile(filePath, opened); } else { fs.closeSync(opened.descriptor); retainedPaths.push(filePath); continue; } removedPaths.push(filePath); } fs.closeSync(opened.descriptor); } if (state.installedHooksPath) { const getHooks = options.getGlobalHooksPath || defaultGetHooksPath; const setHooks = options.setGlobalHooksPath || defaultSetHooksPath; const currentHooks = getHooks(); if (path.resolve(currentHooks || '.') === path.resolve(state.installedHooksPath)) { if (!dryRun) setHooks(state.previousHooksPath || ''); } else if (currentHooks) { retainedPaths.push(`git:core.hooksPath=${currentHooks}`); } } if (!dryRun && retainedPaths.length === 0) { fs.rmSync(statePath, { force: true }); } return { status: dryRun ? 'planned' : retainedPaths.length > 0 ? 'partial' : 'uninstalled', statePath, plannedRemovals: [...new Set(plannedRemovals)], removedPaths, retainedPaths: [...new Set(retainedPaths)].sort(), warnings: retainedPaths.length > 0 ? ['Modified or unverifiable legacy Codex artifacts were preserved.'] : [], }; } module.exports = { BEGIN_MARKER, END_MARKER, SCHEMA, beginLegacySyncState, finalizeLegacySyncState, getStatePath, recordLegacySyncPath, rollbackLegacyCodexSync, stripMarkerBlock, uninstallLegacyCodexSync, };