From 624de7fcfce77d037562a1efc05edf9f8fcf4df1 Mon Sep 17 00:00:00 2001 From: haelyra <49814733+haelyra@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:29:35 -0400 Subject: [PATCH] fix(opencode): complete legacy root migration --- scripts/auto-update.js | 11 +- scripts/lib/install-executor.js | 1 + scripts/lib/install-lifecycle.js | 5 +- scripts/lib/install-manifests.js | 1 + .../lib/install/opencode-legacy-migration.js | 129 ++++++++++-------- 5 files changed, 89 insertions(+), 58 deletions(-) diff --git a/scripts/auto-update.js b/scripts/auto-update.js index 67793d945..52c83c06f 100644 --- a/scripts/auto-update.js +++ b/scripts/auto-update.js @@ -173,6 +173,13 @@ function runExternalCommand(command, args, options = {}) { return result; } +function legacyMigrationWarning(record) { + if (record.legacyLayout === 'opencode') { + return 'Found only a legacy OpenCode ~/.opencode install-state. Run the OpenCode installer once to migrate it to the configured OpenCode directory before auto-updating.'; + } + return 'Found only a legacy Antigravity .agent install-state. Run the Antigravity installer once to migrate it to .agents before auto-updating.'; +} + function runAutoUpdate(options = {}, dependencies = {}) { const discover = dependencies.discoverInstalledStates || discoverInstalledStates; const execute = dependencies.runExternalCommand || runExternalCommand; @@ -187,9 +194,7 @@ function runAutoUpdate(options = {}, dependencies = {}) { const records = discoveredRecords.filter(record => record.exists && !record.legacy); const legacyRecords = discoveredRecords.filter(record => record.exists && record.legacy); const warnings = records.length === 0 && legacyRecords.length > 0 - ? [ - 'Found only a legacy Antigravity .agent install-state. Run the Antigravity installer once to migrate it to .agents before auto-updating.', - ] + ? [...new Set(legacyRecords.map(legacyMigrationWarning))] : []; const results = []; diff --git a/scripts/lib/install-executor.js b/scripts/lib/install-executor.js index 31ee46874..197823302 100644 --- a/scripts/lib/install-executor.js +++ b/scripts/lib/install-executor.js @@ -830,6 +830,7 @@ function createManifestInstallPlan(options = {}) { target: adapter.target, kind: adapter.kind }, + homeDir: plan.homeDir, targetRoot: plan.targetRoot, installRoot: plan.targetRoot, installStatePath: plan.installStatePath, diff --git a/scripts/lib/install-lifecycle.js b/scripts/lib/install-lifecycle.js index 54bae3e8b..bc2ef7bd8 100644 --- a/scripts/lib/install-lifecycle.js +++ b/scripts/lib/install-lifecycle.js @@ -1593,7 +1593,10 @@ function createRepairPlanFromRecord(record, context, options = {}) { throw new Error('No install-state available for repair'); } - if (state.request.legacyMode || shouldRepairFromRecordedOperations(state)) { + if ( + record.legacyLayout !== 'opencode' + && (state.request.legacyMode || shouldRepairFromRecordedOperations(state)) + ) { const operations = hydrateRecordedOperations(context.repoRoot, getManagedOperations(state)); const statePreview = buildRecordedStatePreview(state, context, operations); diff --git a/scripts/lib/install-manifests.js b/scripts/lib/install-manifests.js index eeeb3afe1..d76c96ce8 100644 --- a/scripts/lib/install-manifests.js +++ b/scripts/lib/install-manifests.js @@ -722,6 +722,7 @@ function resolveInstallPlan(options = {}) { skippedModules, excludedModules, targetAdapterId: scaffoldPlan ? scaffoldPlan.adapter.id : null, + homeDir: targetPlanningInput ? targetPlanningInput.homeDir : null, targetRoot: scaffoldPlan ? scaffoldPlan.targetRoot : null, installStatePath: scaffoldPlan ? scaffoldPlan.installStatePath : null, operations: scaffoldPlan ? scaffoldPlan.operations : [], diff --git a/scripts/lib/install/opencode-legacy-migration.js b/scripts/lib/install/opencode-legacy-migration.js index 3ff5b848a..5b79faab5 100644 --- a/scripts/lib/install/opencode-legacy-migration.js +++ b/scripts/lib/install/opencode-legacy-migration.js @@ -47,6 +47,9 @@ function getLegacyLocationForPlan(plan) { ) { return null; } + if (typeof plan.homeDir === 'string' && plan.homeDir.trim() !== '') { + return getLegacyOpencodeLocation(plan.homeDir); + } const canonicalRoot = path.resolve(plan.targetRoot); if ( path.basename(canonicalRoot) !== 'opencode' @@ -99,13 +102,13 @@ function hashFileNoFollow(filePath) { const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0); const descriptor = fs.openSync(filePath, flags); try { - const before = fs.fstatSync(descriptor); + const before = fs.fstatSync(descriptor, { bigint: true }); if (!before.isFile()) { throw new Error(`Refusing to read a non-file at ${filePath}`); } const content = fs.readFileSync(descriptor); - const after = fs.fstatSync(descriptor); - const finalPathStat = fs.lstatSync(filePath); + const after = fs.fstatSync(descriptor, { bigint: true }); + const finalPathStat = fs.lstatSync(filePath, { bigint: true }); const unchanged = before.dev === after.dev && before.ino === after.ino && before.size === after.size @@ -150,10 +153,11 @@ function removeEmptyParents(startPath, legacyRoot) { } function verifyManagedLegacyFile(operation, location, sourceRoot) { + if (operation?.ownership !== 'managed' || operation?.kind !== 'copy-file') { + return { skipped: true }; + } if ( - operation?.kind !== 'copy-file' - || operation.ownership !== 'managed' - || typeof operation.destinationPath !== 'string' + typeof operation.destinationPath !== 'string' || typeof operation.sourceRelativePath !== 'string' || !/^[a-f0-9]{64}$/i.test(operation.contentSha256 || '') ) { @@ -214,7 +218,7 @@ function removeVerifiedLegacyFile(entry, location) { const quarantinePath = path.join(quarantineDir, path.basename(safePath)); try { fs.renameSync(safePath, quarantinePath); - const quarantinedStat = fs.lstatSync(quarantinePath); + const quarantinedStat = fs.lstatSync(quarantinePath, { bigint: true }); const identityMatches = !quarantinedStat.isSymbolicLink() && quarantinedStat.isFile() && quarantinedStat.dev === entry.stat.dev @@ -242,32 +246,79 @@ function removeVerifiedLegacyFile(entry, location) { } } -function cleanupLegacyOpencodeInstall(plan) { - const location = getLegacyLocationForPlan(plan); - const emptyResult = { +function emptyCleanupResult() { + return { detected: false, complete: false, removedPaths: [], retainedPaths: [], warnings: [], }; - if (!location || typeof plan.sourceRoot !== 'string' || !pathExists(plan.installStatePath)) { - return emptyResult; - } +} +function hasTrustedCanonicalState(plan) { + if (typeof plan.sourceRoot !== 'string' || !pathExists(plan.installStatePath)) { + return false; + } try { const canonicalState = readInstallState(plan.installStatePath); - if ( + return !( (canonicalState.target.target !== OPENCODE_TARGET && canonicalState.target.id !== 'opencode-home') || !samePath(canonicalState.target.root, plan.targetRoot) || !samePath(canonicalState.target.installStatePath, plan.installStatePath) - ) { - return emptyResult; + ); + } catch (_error) { + return false; + } +} + +function classifyLegacyOperations(inspection, location, sourceRoot) { + const removable = []; + const retainedPaths = []; + for (const operation of inspection.state.operations || []) { + const verified = verifyManagedLegacyFile(operation, location, sourceRoot); + if (verified.destinationPath) removable.push(verified); + else if (verified.retainedPath) retainedPaths.push(verified.retainedPath); + } + return { removable, retainedPaths }; +} + +function removeLegacyFiles(removable, location, retainedPaths) { + const removedPaths = []; + for (const entry of removable) { + try { + if (!removeVerifiedLegacyFile(entry, location)) { + retainedPaths.push(entry.destinationPath); + continue; + } + removedPaths.push(entry.destinationPath); + removeEmptyParents(entry.destinationPath, location.targetRoot); + } catch (_error) { + retainedPaths.push(entry.destinationPath); + } + } + return removedPaths; +} + +function finalizeLegacyCleanup(location, retainedPaths, removedPaths) { + if (retainedPaths.length > 0) return false; + fs.rmSync(location.installStatePath, { force: true }); + removedPaths.push(location.installStatePath); + try { + if (pathExists(location.targetRoot) && fs.readdirSync(location.targetRoot).length === 0) { + fs.rmdirSync(location.targetRoot); } } catch (_error) { - return emptyResult; + // Removing an empty legacy root is best effort after ownership is cleared. } + return true; +} + +function cleanupLegacyOpencodeInstall(plan) { + const location = getLegacyLocationForPlan(plan); + const emptyResult = emptyCleanupResult(); + if (!location || !hasTrustedCanonicalState(plan)) return emptyResult; const inspection = inspectLegacyOpencodeState(location); if (inspection.status === 'unreadable') { @@ -282,43 +333,13 @@ function cleanupLegacyOpencodeInstall(plan) { return emptyResult; } - const removable = []; - const retainedPaths = []; - for (const operation of inspection.state.operations || []) { - const verified = verifyManagedLegacyFile(operation, location, plan.sourceRoot); - if (verified.destinationPath) { - removable.push(verified); - } else if (verified.retainedPath) { - retainedPaths.push(verified.retainedPath); - } - } - - const removedPaths = []; - for (const entry of removable) { - try { - if (!removeVerifiedLegacyFile(entry, location)) { - retainedPaths.push(entry.destinationPath); - continue; - } - removedPaths.push(entry.destinationPath); - removeEmptyParents(entry.destinationPath, location.targetRoot); - } catch (_error) { - retainedPaths.push(entry.destinationPath); - } - } - - const complete = retainedPaths.length === 0; - if (complete) { - fs.rmSync(location.installStatePath, { force: true }); - removedPaths.push(location.installStatePath); - try { - if (pathExists(location.targetRoot) && fs.readdirSync(location.targetRoot).length === 0) { - fs.rmdirSync(location.targetRoot); - } - } catch (_error) { - // Removing an empty legacy root is best effort after ownership is cleared. - } - } + const { removable, retainedPaths } = classifyLegacyOperations( + inspection, + location, + plan.sourceRoot + ); + const removedPaths = removeLegacyFiles(removable, location, retainedPaths); + const complete = finalizeLegacyCleanup(location, retainedPaths, removedPaths); return { detected: true,