diff --git a/scripts/lib/codex-legacy-sync.js b/scripts/lib/codex-legacy-sync.js index f228afb20..e12a69aec 100644 --- a/scripts/lib/codex-legacy-sync.js +++ b/scripts/lib/codex-legacy-sync.js @@ -475,6 +475,26 @@ function listLegacyCandidates(codexHome) { return candidates; } +function hasMarkerBlock(codexHome) { + const agentsPath = path.join(codexHome, 'AGENTS.md'); + try { + const snapshot = readRegularFileNoFollow(agentsPath, 'utf8'); + if (snapshot) { + const stripped = stripMarkerBlock(snapshot.content); + return stripped !== snapshot.content; + } + } catch (_error) { + // Non-regular or unreadable AGENTS.md is not a clean marker signal. + } + return false; +} + +function detectLegacyCodexSync(codexHome) { + const resolvedCodexHome = path.resolve(codexHome || process.env.CODEX_HOME || path.join(process.env.HOME || os.homedir(), '.codex')); + if (readStateIfPresent(getStatePath(resolvedCodexHome))) return true; + return hasMarkerBlock(resolvedCodexHome); +} + 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); @@ -498,13 +518,17 @@ function uninstallLegacyCodexSync(options = {}) { } } } catch (_error) { - retainedPaths.push(agentsPath); + if (_error.code !== 'ENOENT') retainedPaths.push(agentsPath); } finally { if (openedAgents) fs.closeSync(openedAgents.descriptor); } retainedPaths.push(...listLegacyCandidates(codexHome)); + const hasWork = plannedRemovals.length > 0 || removedPaths.length > 0; + const status = dryRun + ? (hasWork || retainedPaths.length > 0 ? 'planned' : 'not-found') + : (retainedPaths.length > 0 ? 'partial' : (hasWork ? 'uninstalled' : 'not-found')); return { - status: dryRun ? 'planned' : retainedPaths.length > 0 ? 'partial' : plannedRemovals.length > 0 ? 'uninstalled' : 'not-found', + status, statePath: null, plannedRemovals, removedPaths, @@ -594,6 +618,7 @@ module.exports = { END_MARKER, SCHEMA, beginLegacySyncState, + detectLegacyCodexSync, finalizeLegacySyncState, getStatePath, recordLegacySyncPath, diff --git a/scripts/uninstall.js b/scripts/uninstall.js index ff515aacc..eaba93c68 100644 --- a/scripts/uninstall.js +++ b/scripts/uninstall.js @@ -5,7 +5,10 @@ const path = require('path'); const { uninstallInstalledStates } = require('./lib/install-lifecycle'); const { SUPPORTED_INSTALL_TARGETS } = require('./lib/install-manifests'); const { exitFeedbackLines } = require('./lib/feedback-links'); -const { uninstallLegacyCodexSync } = require('./lib/codex-legacy-sync'); +const { + detectLegacyCodexSync, + uninstallLegacyCodexSync, +} = require('./lib/codex-legacy-sync'); function showHelp(exitCode = 0) { console.log(` @@ -90,12 +93,8 @@ function printHuman(result) { } } -function detectLegacyCodexSync(codexHome) { - const probe = uninstallLegacyCodexSync({ - codexHome, - dryRun: true, - }); - return probe.status !== 'not-found'; +function legacyCodexSyncDetected(codexHome) { + return detectLegacyCodexSync(codexHome); } function printLegacy(result, dryRun) { @@ -149,7 +148,7 @@ async function main() { if ( result.results.length === 0 && includesCodexTarget(options.targets) - && detectLegacyCodexSync(codexHomePath()) + && legacyCodexSyncDetected(codexHomePath()) ) { result = uninstallLegacyCodexSync({ codexHome: codexHomePath(), diff --git a/tests/scripts/uninstall.test.js b/tests/scripts/uninstall.test.js index aeae14ed2..b42c6c2f2 100644 --- a/tests/scripts/uninstall.test.js +++ b/tests/scripts/uninstall.test.js @@ -418,6 +418,56 @@ function runTests() { } })) passed++; else failed++; + if (test('does not misclassify a clean Codex home as a legacy install', () => { + const homeDir = createTempDir('uninstall-clean-codex-home-'); + const projectRoot = createTempDir('uninstall-clean-codex-project-'); + + try { + const codexHome = path.join(homeDir, '.codex'); + const configPath = path.join(codexHome, 'config.toml'); + const conversationPath = path.join(codexHome, 'conversations', 'keep-me.md'); + + fs.mkdirSync(codexHome, { recursive: true }); + fs.writeFileSync(configPath, 'model = "user"\n'); + fs.mkdirSync(path.dirname(conversationPath), { recursive: true }); + fs.writeFileSync(conversationPath, 'conversation history'); + + const uninstallResult = run([], { cwd: projectRoot, homeDir }); + assert.strictEqual(uninstallResult.code, 0, uninstallResult.stderr); + assert.ok(uninstallResult.stdout.includes('No ECC install-state files found'), uninstallResult.stdout); + assert.ok(!uninstallResult.stdout.includes('Legacy Codex sync cleanup summary'), uninstallResult.stdout); + assert.strictEqual(fs.readFileSync(configPath, 'utf8'), 'model = "user"\n'); + assert.strictEqual(fs.readFileSync(conversationPath, 'utf8'), 'conversation history'); + } finally { + cleanup(homeDir); + cleanup(projectRoot); + } + })) passed++; else failed++; + + if (test('explicit --legacy-codex-sync on a clean home reports not-found without removing files', () => { + const homeDir = createTempDir('uninstall-legacy-clean-home-'); + const projectRoot = createTempDir('uninstall-legacy-clean-project-'); + + try { + const codexHome = path.join(homeDir, '.codex'); + const configPath = path.join(codexHome, 'config.toml'); + + fs.mkdirSync(codexHome, { recursive: true }); + fs.writeFileSync(configPath, 'model = "user"\n'); + + const uninstallResult = run(['--legacy-codex-sync', '--json'], { cwd: projectRoot, homeDir }); + assert.strictEqual(uninstallResult.code, 0, uninstallResult.stderr); + const parsed = JSON.parse(uninstallResult.stdout); + assert.strictEqual(parsed.status, 'not-found'); + assert.deepStrictEqual(parsed.plannedRemovals, []); + assert.deepStrictEqual(parsed.retainedPaths, []); + assert.strictEqual(fs.readFileSync(configPath, 'utf8'), 'model = "user"\n'); + } finally { + cleanup(homeDir); + cleanup(projectRoot); + } + })) passed++; else failed++; + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); }