From 09d6d22c09608704e5fa891698f08e8b031ddc4e Mon Sep 17 00:00:00 2001 From: Santhi Prakash Date: Sat, 22 Aug 2026 18:19:11 +0000 Subject: [PATCH] fix(scripts): auto-detect legacy sync-ecc-to-codex.sh installs in uninstall When no install-state is found for the current context, `ecc uninstall` now checks for the legacy `sync-ecc-to-codex.sh` ownership manifest under `~/.codex/ecc/legacy-sync-state.json` and, if present, rolls back the managed Codex artifacts it recorded. It restores previous `config.toml` and `AGENTS.md` content instead of deleting them, removes generated prompts/docs/copies, and leaves unrelated Codex conversation history and user config keys untouched. A fallback `--legacy-codex-sync` flag still forces the legacy path explicitly, and `--dry-run` previews the cleanup. Co-Authored-By: Paperclip --- scripts/uninstall.js | 96 ++++++++++++++++++++++++--------- tests/scripts/uninstall.test.js | 63 ++++++++++++++++++++++ 2 files changed, 133 insertions(+), 26 deletions(-) diff --git a/scripts/uninstall.js b/scripts/uninstall.js index f9a651ebb..ff515aacc 100644 --- a/scripts/uninstall.js +++ b/scripts/uninstall.js @@ -1,6 +1,7 @@ #!/usr/bin/env node const os = require('os'); +const path = require('path'); const { uninstallInstalledStates } = require('./lib/install-lifecycle'); const { SUPPORTED_INSTALL_TARGETS } = require('./lib/install-manifests'); const { exitFeedbackLines } = require('./lib/feedback-links'); @@ -11,7 +12,9 @@ function showHelp(exitCode = 0) { Usage: node scripts/uninstall.js [--target <${SUPPORTED_INSTALL_TARGETS.join('|')}>] [--legacy-codex-sync] [--dry-run] [--json] Remove ECC-managed files recorded in install-state for the current context. -Use --legacy-codex-sync explicitly for the older sync-ecc-to-codex.sh installation. +When no install-state is found, the uninstaller also detects and removes +artifacts left by the older scripts/sync-ecc-to-codex.sh installer. +Use --legacy-codex-sync to force the legacy path explicitly. `); process.exit(exitCode); } @@ -87,6 +90,34 @@ function printHuman(result) { } } +function detectLegacyCodexSync(codexHome) { + const probe = uninstallLegacyCodexSync({ + codexHome, + dryRun: true, + }); + return probe.status !== 'not-found'; +} + +function printLegacy(result, dryRun) { + console.log('Legacy Codex sync cleanup summary:\n'); + console.log(`Status: ${result.status.toUpperCase()}`); + const paths = dryRun ? result.plannedRemovals : result.removedPaths; + console.log(`${dryRun ? 'Planned changes' : 'Removed paths'}: ${paths.length}`); + if (result.retainedPaths.length > 0) { + console.log(`Retained paths: ${result.retainedPaths.length}`); + for (const retainedPath of result.retainedPaths) console.log(` - ${retainedPath}`); + } + for (const warning of result.warnings) console.log(`Warning: ${warning}`); +} + +function codexHomePath() { + return process.env.CODEX_HOME || path.join(process.env.HOME || os.homedir(), '.codex'); +} + +function includesCodexTarget(targets) { + return targets.length === 0 || targets.includes('codex'); +} + async function main() { try { const options = parseArgs(process.argv); @@ -97,41 +128,54 @@ async function main() { if (options.legacyCodexSync && options.targets.length > 0) { throw new Error('--legacy-codex-sync cannot be combined with --target'); } - const result = options.legacyCodexSync - ? uninstallLegacyCodexSync({ - codexHome: process.env.CODEX_HOME, - dryRun: options.dryRun, - }) - : uninstallInstalledStates({ - homeDir: process.env.HOME || os.homedir(), - projectRoot: process.cwd(), - targets: options.targets, - dryRun: options.dryRun, - }); - if (!options.dryRun && !options.legacyCodexSync) { - const { reconcileCanonicalInstallStates } = require('./lib/install-state-store-sync'); - result.installStateProjection = await reconcileCanonicalInstallStates({ + + let result; + let mode = 'install-state'; + + if (options.legacyCodexSync) { + result = uninstallLegacyCodexSync({ + codexHome: codexHomePath(), + dryRun: options.dryRun, + }); + mode = 'legacy-codex-sync'; + } else { + result = uninstallInstalledStates({ homeDir: process.env.HOME || os.homedir(), projectRoot: process.cwd(), targets: options.targets, + dryRun: options.dryRun, }); + + if ( + result.results.length === 0 + && includesCodexTarget(options.targets) + && detectLegacyCodexSync(codexHomePath()) + ) { + result = uninstallLegacyCodexSync({ + codexHome: codexHomePath(), + dryRun: options.dryRun, + }); + mode = 'legacy-codex-sync'; + } + + if (mode === 'install-state' && !options.dryRun) { + const { reconcileCanonicalInstallStates } = require('./lib/install-state-store-sync'); + result.installStateProjection = await reconcileCanonicalInstallStates({ + homeDir: process.env.HOME || os.homedir(), + projectRoot: process.cwd(), + targets: options.targets, + }); + } } - const hasErrors = options.legacyCodexSync + + const hasErrors = mode === 'legacy-codex-sync' ? result.status === 'partial' : result.summary.errorCount > 0 || result.summary.partialCount > 0; if (options.json) { console.log(JSON.stringify(result, null, 2)); - } else if (options.legacyCodexSync) { - console.log('Legacy Codex sync cleanup summary:\n'); - console.log(`Status: ${result.status.toUpperCase()}`); - const paths = options.dryRun ? result.plannedRemovals : result.removedPaths; - console.log(`${options.dryRun ? 'Planned changes' : 'Removed paths'}: ${paths.length}`); - if (result.retainedPaths.length > 0) { - console.log(`Retained paths: ${result.retainedPaths.length}`); - for (const retainedPath of result.retainedPaths) console.log(` - ${retainedPath}`); - } - for (const warning of result.warnings) console.log(`Warning: ${warning}`); + } else if (mode === 'legacy-codex-sync') { + printLegacy(result, options.dryRun); } else { printHuman(result); } diff --git a/tests/scripts/uninstall.test.js b/tests/scripts/uninstall.test.js index 285d2fdae..aeae14ed2 100644 --- a/tests/scripts/uninstall.test.js +++ b/tests/scripts/uninstall.test.js @@ -23,6 +23,11 @@ const { createInstallState, writeInstallState, } = require('../../scripts/lib/install-state'); +const { + beginLegacySyncState, + recordLegacySyncPath, + finalizeLegacySyncState, +} = require('../../scripts/lib/codex-legacy-sync'); function createTempDir(prefix) { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); @@ -43,6 +48,11 @@ function run(args = [], options = {}) { ...process.env, HOME: options.homeDir || process.env.HOME, }; + if (options.homeDir) { + env.CODEX_HOME = path.join(options.homeDir, '.codex'); + } else { + delete env.CODEX_HOME; + } try { const stdout = execFileSync('node', [SCRIPT, ...args], { @@ -355,6 +365,59 @@ function runTests() { } })) passed++; else failed++; + if (test('auto-detects legacy sync-ecc-to-codex.sh install and removes artifacts without touching conversations or unrelated config keys', () => { + const homeDir = createTempDir('uninstall-legacy-codex-home-'); + const projectRoot = createTempDir('uninstall-legacy-codex-project-'); + + try { + const codexHome = path.join(homeDir, '.codex'); + const configPath = path.join(codexHome, 'config.toml'); + const agentsPath = path.join(codexHome, 'AGENTS.md'); + const promptPath = path.join(codexHome, 'prompts', 'ecc-plan.md'); + const conversationPath = path.join(codexHome, 'conversations', 'keep-me.md'); + const userFilePath = path.join(codexHome, 'user-owned.txt'); + + fs.mkdirSync(codexHome, { recursive: true }); + fs.writeFileSync(configPath, 'model = "user"\n'); + fs.writeFileSync(agentsPath, '# User instructions\n'); + fs.mkdirSync(path.dirname(promptPath), { recursive: true }); + + const statePath = beginLegacySyncState({ + codexHome, + backupDir: path.join(codexHome, 'backups', 'ecc-test'), + }); + recordLegacySyncPath({ statePath, filePath: configPath }); + recordLegacySyncPath({ statePath, filePath: agentsPath }); + recordLegacySyncPath({ statePath, filePath: promptPath }); + + fs.writeFileSync(configPath, 'model = "user"\napproval_policy = "on-request"\n'); + fs.writeFileSync( + agentsPath, + '# User instructions\n\n\n# ECC managed\n\n' + ); + fs.writeFileSync(promptPath, '# ECC generated prompt\n'); + finalizeLegacySyncState({ statePath }); + + fs.mkdirSync(path.dirname(conversationPath), { recursive: true }); + fs.writeFileSync(conversationPath, 'conversation history'); + fs.writeFileSync(userFilePath, 'unrelated'); + + 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.ok(!fs.existsSync(promptPath)); + assert.strictEqual(fs.readFileSync(configPath, 'utf8'), 'model = "user"\n'); + assert.strictEqual(fs.readFileSync(agentsPath, 'utf8'), '# User instructions\n'); + assert.strictEqual(fs.readFileSync(conversationPath, 'utf8'), 'conversation history'); + assert.strictEqual(fs.readFileSync(userFilePath, 'utf8'), 'unrelated'); + assert.ok(!fs.existsSync(statePath)); + } finally { + cleanup(homeDir); + cleanup(projectRoot); + } + })) passed++; else failed++; + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); }