fix(install): clean legacy home agents state

This commit is contained in:
Dante committed 2026-09-14 15:30:27 +08:00
1 parent 1b9d599aab
commit 8e641003ba
3 files changed
+203 -4

No files matched your search

+18 -2
View File
@@ -13,6 +13,10 @@ const { getRecordedHookConsent } = require('./install/hook-consent');
const {
prepareClaudeSkillMigration,
} = require('./install/claude-skill-migration');
const {
prepareLegacyAgentsMigration,
removeLegacyAgentsFiles,
} = require('./install/legacy-agents-migration');
const {
getLegacyAntigravityLocation,
inspectLegacyAntigravityState,
@@ -1908,7 +1912,10 @@ function prepareRepairMigration(plan, record) {
installStatePath: record.installStatePath,
statePreview: buildAdapterDerivedStatePreview(plan.statePreview, record),
};
const migration = prepareClaudeSkillMigration(trustedPlan);
const migration = prepareLegacyAgentsMigration(
trustedPlan,
prepareClaudeSkillMigration(trustedPlan)
);
return {
migration,
plan: {
@@ -2131,10 +2138,16 @@ function repairInstalledStates(options = {}) {
const legacyMigrationPaths = migration.legacyOperationsToRemove.map(
operation => operation.destinationPath
);
const legacyAgentsMigrationPaths = migration.legacyAgentsOperationsToRemove.map(
operation => operation.destinationPath
);
const hasLegacyAgentsStateMigration = migration.legacyAgentsOperationsToDetach.length > 0;
const plannedRepairs = [...new Set([
...(needsOpencodeBuild ? [opencodeBuildRepairPath] : []),
...repairOperations.map(operation => operation.destinationPath),
...legacyMigrationPaths,
...legacyAgentsMigrationPaths,
...(hasLegacyAgentsStateMigration ? [record.installStatePath] : []),
])];
if (options.dryRun) {
@@ -2150,7 +2163,8 @@ function repairInstalledStates(options = {}) {
};
}
const hasLegacyMigration = migration.legacyOperationsToRemove.length > 0;
const hasLegacyMigration = migration.legacyOperationsToRemove.length > 0
|| hasLegacyAgentsStateMigration;
const repairedPaths = needsOpencodeBuild ? [opencodeBuildRepairPath] : [];
if (migration.requiresBridgeState && (repairOperations.length > 0 || hasLegacyMigration)) {
writeRefreshedInstallState(record, migration.bridgeState);
@@ -2181,6 +2195,8 @@ function repairInstalledStates(options = {}) {
repairedPaths.push(removedPath);
}
}
removeLegacyAgentsFiles(migration, record.targetRoot);
repairedPaths.push(...legacyAgentsMigrationPaths.filter(filePath => !fs.existsSync(filePath)));
}
const changedInstalledBytes = repairOperations.length > 0
|| needsOpencodeBuild
+13 -2
View File
@@ -22,6 +22,10 @@ const {
} = require('./claude-settings');
const { filterMcpConfig, parseDisabledMcpServers } = require('../mcp-config');
const { assertWithinTrustedRoot } = require('../path-safety');
const {
prepareLegacyAgentsMigration,
removeLegacyAgentsFiles,
} = require('./legacy-agents-migration');
const {
assertSafeClaudeSkillOperation,
prepareClaudeSkillMigration,
@@ -398,7 +402,10 @@ function prepareHookConsentMigration(plan, migration) {
function previewInstallPlan(plan) {
const migration = prepareHookConsentMigration(
plan,
prepareUserOwnedFileGuard(plan, prepareClaudeSkillMigration(plan))
prepareUserOwnedFileGuard(
plan,
prepareLegacyAgentsMigration(plan, prepareClaudeSkillMigration(plan))
)
);
const appliedPlan = {
...plan,
@@ -451,7 +458,10 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
}
const migration = prepareHookConsentMigration(
plan,
prepareUserOwnedFileGuard(plan, prepareClaudeSkillMigration(plan))
prepareUserOwnedFileGuard(
plan,
prepareLegacyAgentsMigration(plan, prepareClaudeSkillMigration(plan))
)
);
const appliedPlan = {
...plan,
@@ -590,6 +600,7 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
if (hasLegacyMigration) {
removeLegacyClaudeSkillFiles(migration, plan.targetRoot);
}
removeLegacyAgentsFiles(migration, plan.targetRoot);
if (shouldSetClaudeCommitAttributionPreference(appliedPlan)) {
writeClaudeCommitAttributionPreference(
@@ -0,0 +1,172 @@
'use strict';
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { readInstallState } = require('../install-state');
const { assertWithinTrustedRoot } = require('../path-safety');
const HOME_TARGET_IDS = new Set(['claude-home', 'codex-home']);
function comparablePath(filePath) {
const resolvedPath = path.resolve(filePath);
return process.platform === 'win32' ? resolvedPath.toLowerCase() : resolvedPath;
}
function normalizeRelativePath(relativePath) {
return String(relativePath || '').replace(/\\/g, '/');
}
function isLegacyAgentsOperation(plan, operation) {
if (
!operation
|| operation.kind !== 'copy-file'
|| operation.ownership !== 'managed'
|| typeof operation.destinationPath !== 'string'
) {
return false;
}
const sourceRelativePath = normalizeRelativePath(operation.sourceRelativePath);
if (!sourceRelativePath.startsWith('.agents/')) {
return false;
}
const expectedDestination = path.join(
plan.targetRoot,
...sourceRelativePath.split('/')
);
return comparablePath(operation.destinationPath) === comparablePath(expectedDestination);
}
function inspectLegacyAgentsOperation(plan, operation) {
assertWithinTrustedRoot(
operation.destinationPath,
plan.targetRoot,
'inspect legacy managed .agents file'
);
let stat;
try {
stat = fs.lstatSync(operation.destinationPath);
} catch (error) {
if (error && error.code === 'ENOENT') {
return { removable: false, warning: null };
}
throw error;
}
if (!stat.isFile() || stat.isSymbolicLink()) {
return {
removable: false,
warning: `Preserved legacy .agents path ${operation.destinationPath}: it is not a regular file.`,
};
}
if (!/^[a-f0-9]{64}$/i.test(operation.contentSha256 || '')) {
return {
removable: false,
warning: `Preserved legacy .agents file ${operation.destinationPath}: install-state has no verified content digest.`,
};
}
const contentSha256 = crypto.createHash('sha256')
.update(fs.readFileSync(operation.destinationPath))
.digest('hex');
if (contentSha256 !== operation.contentSha256.toLowerCase()) {
return {
removable: false,
warning: `Preserved modified legacy .agents file ${operation.destinationPath} and released it from ECC management.`,
};
}
return { removable: true, warning: null };
}
function prepareLegacyAgentsMigration(plan, migration) {
if (!HOME_TARGET_IDS.has(plan && plan.adapter && plan.adapter.id)) {
return {
...migration,
legacyAgentsOperationsToDetach: [],
legacyAgentsOperationsToRemove: [],
};
}
const previousState = fs.existsSync(plan.installStatePath)
? readInstallState(plan.installStatePath)
: null;
const legacyOperations = ((previousState && previousState.operations) || [])
.filter(operation => isLegacyAgentsOperation(plan, operation));
if (legacyOperations.length === 0) {
return {
...migration,
legacyAgentsOperationsToDetach: [],
legacyAgentsOperationsToRemove: [],
};
}
const inspections = legacyOperations.map(operation => ({
operation,
...inspectLegacyAgentsOperation(plan, operation),
}));
const legacyDestinations = new Set(
legacyOperations.map(operation => comparablePath(operation.destinationPath))
);
const withoutLegacyAgents = state => ({
...state,
operations: (state.operations || []).filter(operation => (
!legacyDestinations.has(comparablePath(operation.destinationPath))
)),
});
return {
...migration,
finalState: withoutLegacyAgents(migration.finalState),
legacyAgentsOperationsToDetach: legacyOperations,
legacyAgentsOperationsToRemove: inspections
.filter(inspection => inspection.removable)
.map(inspection => inspection.operation),
requiresBridgeState: true,
warnings: [
...(migration.warnings || []),
...inspections.map(inspection => inspection.warning).filter(Boolean),
],
};
}
function cleanupEmptyAgentsParents(filePath, targetRoot) {
const agentsRoot = path.join(targetRoot, '.agents');
let currentPath = path.dirname(filePath);
while (comparablePath(currentPath) !== comparablePath(targetRoot)) {
assertWithinTrustedRoot(currentPath, targetRoot, 'clean legacy managed .agents directory');
if (!fs.existsSync(currentPath) || fs.readdirSync(currentPath).length > 0) {
return;
}
fs.rmdirSync(currentPath);
if (comparablePath(currentPath) === comparablePath(agentsRoot)) {
return;
}
currentPath = path.dirname(currentPath);
}
}
function removeLegacyAgentsFiles(migration, targetRoot) {
for (const operation of migration.legacyAgentsOperationsToRemove || []) {
const latestInspection = inspectLegacyAgentsOperation(
{ targetRoot },
operation
);
if (!latestInspection.removable) {
continue;
}
fs.rmSync(operation.destinationPath, { force: true });
cleanupEmptyAgentsParents(operation.destinationPath, targetRoot);
}
}
module.exports = {
prepareLegacyAgentsMigration,
removeLegacyAgentsFiles,
};