mirror of
https://github.com/affaan-m/ECC.git
synced 2026-08-17 21:15:40 +02:00
fix(uninstall): preserve path identity and user drift
This commit is contained in:
@@ -529,13 +529,46 @@ function removeContainedPath(destinationPath, trustedRoot, action, options = {})
|
||||
return null;
|
||||
}
|
||||
|
||||
const finalDestination = getManagedDestination(
|
||||
const managedDestination = getManagedDestination(
|
||||
existingDestination,
|
||||
trustedRoot,
|
||||
action,
|
||||
{ allowFinalSymlink: true }
|
||||
).managedPath;
|
||||
fs.rmSync(finalDestination, options);
|
||||
);
|
||||
const finalDestination = managedDestination.managedPath;
|
||||
const expectedStat = fs.lstatSync(finalDestination, { bigint: true });
|
||||
const quarantineDir = fs.mkdtempSync(path.join(
|
||||
path.dirname(managedDestination.canonicalRoot),
|
||||
'.ecc-remove-'
|
||||
));
|
||||
const quarantinePath = path.join(quarantineDir, path.basename(finalDestination));
|
||||
|
||||
try {
|
||||
fs.renameSync(finalDestination, quarantinePath);
|
||||
} catch (error) {
|
||||
fs.rmdirSync(quarantineDir);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const quarantinedStat = fs.lstatSync(quarantinePath, { bigint: true });
|
||||
if (!hasSameFileIdentity(expectedStat, quarantinedStat)) {
|
||||
try {
|
||||
fs.renameSync(quarantinePath, finalDestination);
|
||||
fs.rmdirSync(quarantineDir);
|
||||
} catch (_restoreError) {
|
||||
throw new Error(
|
||||
`Refusing to ${action}: managed destination changed before removal; replacement preserved at ${quarantinePath}.`
|
||||
);
|
||||
}
|
||||
throw createChangedDestinationError(action);
|
||||
}
|
||||
|
||||
if (quarantinedStat.isDirectory() && !options.recursive) {
|
||||
fs.rmdirSync(quarantinePath);
|
||||
} else {
|
||||
fs.rmSync(quarantinePath, options);
|
||||
}
|
||||
fs.rmdirSync(quarantineDir);
|
||||
return finalDestination;
|
||||
}
|
||||
|
||||
@@ -724,7 +757,8 @@ function executeUninstallOperation(operation, trustedRoot, options = {}) {
|
||||
const existingDestination = getContainedExistingPath(
|
||||
operation.destinationPath,
|
||||
trustedRoot,
|
||||
'uninstall'
|
||||
'uninstall',
|
||||
{ allowFinalSymlink: true }
|
||||
);
|
||||
if (!existingDestination) {
|
||||
return {
|
||||
@@ -732,6 +766,13 @@ function executeUninstallOperation(operation, trustedRoot, options = {}) {
|
||||
cleanupTargets: []
|
||||
};
|
||||
}
|
||||
if (fs.lstatSync(existingDestination).isSymbolicLink()) {
|
||||
return {
|
||||
removedPaths: [],
|
||||
cleanupTargets: [],
|
||||
retainedPaths: [operation.destinationPath]
|
||||
};
|
||||
}
|
||||
const recordedDigest = operation.contentSha256;
|
||||
const currentDigest = /^[a-f0-9]{64}$/i.test(recordedDigest || '')
|
||||
? crypto.createHash('sha256')
|
||||
@@ -741,7 +782,8 @@ function executeUninstallOperation(operation, trustedRoot, options = {}) {
|
||||
if (!currentDigest || currentDigest !== recordedDigest.toLowerCase()) {
|
||||
return {
|
||||
removedPaths: [],
|
||||
cleanupTargets: []
|
||||
cleanupTargets: [],
|
||||
retainedPaths: [operation.destinationPath]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1869,8 +1911,9 @@ function cleanupEmptyParentDirs(filePath, stopAt) {
|
||||
}
|
||||
|
||||
const finalPath = assertWithinTrustedRoot(validatedPath, trustedStopAt, 'clean up');
|
||||
fs.rmdirSync(finalPath);
|
||||
currentPath = path.dirname(finalPath);
|
||||
const removedPath = removeContainedPath(finalPath, trustedStopAt, 'clean up');
|
||||
if (!removedPath) break;
|
||||
currentPath = path.dirname(removedPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1926,25 +1969,29 @@ function uninstallInstalledStates(options = {}) {
|
||||
try {
|
||||
const removedPaths = [];
|
||||
const cleanupTargets = [];
|
||||
const retainedPaths = [];
|
||||
const operations = getManagedOperations(state);
|
||||
|
||||
for (const operation of operations) {
|
||||
const outcome = executeUninstallOperation(operation, record.targetRoot, {
|
||||
preserveDriftedCopies: record.legacy,
|
||||
preserveDriftedCopies: true,
|
||||
});
|
||||
removedPaths.push(...outcome.removedPaths);
|
||||
cleanupTargets.push(...outcome.cleanupTargets);
|
||||
retainedPaths.push(...(outcome.retainedPaths || []));
|
||||
}
|
||||
|
||||
const removedStatePath = removeContainedPath(
|
||||
record.installStatePath,
|
||||
record.targetRoot,
|
||||
'uninstall',
|
||||
{ force: true }
|
||||
);
|
||||
if (removedStatePath) {
|
||||
removedPaths.push(record.installStatePath);
|
||||
cleanupTargets.push(removedStatePath);
|
||||
if (retainedPaths.length === 0) {
|
||||
const removedStatePath = removeContainedPath(
|
||||
record.installStatePath,
|
||||
record.targetRoot,
|
||||
'uninstall',
|
||||
{ force: true }
|
||||
);
|
||||
if (removedStatePath) {
|
||||
removedPaths.push(record.installStatePath);
|
||||
cleanupTargets.push(removedStatePath);
|
||||
}
|
||||
}
|
||||
|
||||
for (const cleanupTarget of cleanupTargets) {
|
||||
@@ -1953,10 +2000,14 @@ function uninstallInstalledStates(options = {}) {
|
||||
|
||||
return {
|
||||
adapter: record.adapter,
|
||||
status: 'uninstalled',
|
||||
status: retainedPaths.length > 0 ? 'partial' : 'uninstalled',
|
||||
installStatePath: record.installStatePath,
|
||||
removedPaths,
|
||||
retainedPaths: [...new Set(retainedPaths)].sort(),
|
||||
plannedRemovals: [],
|
||||
warning: retainedPaths.length > 0
|
||||
? 'Modified or unverifiable managed files were preserved together with install-state for review.'
|
||||
: null,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -350,20 +350,25 @@ function applyInstallPlan(plan, dependencies = {}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Markdown may reference files whose installed paths move, such as rules
|
||||
// copied under rules/ecc. Rewrite only links that point at installed targets;
|
||||
// untouched links and non-markdown files stay on the byte-for-byte path.
|
||||
if (
|
||||
// Declared transforms are part of the install contract and always apply.
|
||||
// Markdown link rewriting is additive when the plan has a usable index.
|
||||
const needsLinkRewrite = Boolean(
|
||||
linkIndex
|
||||
&& operation.kind === 'copy-file'
|
||||
&& operation.sourceRelativePath
|
||||
&& isMarkdownPath(operation.destinationPath)
|
||||
) {
|
||||
const rewritten = rewriteRelativeLinks(
|
||||
transformInstallContent(operation, fs.readFileSync(operation.sourcePath, 'utf8')),
|
||||
{ sourceRel: operation.sourceRelativePath, index: linkIndex }
|
||||
);
|
||||
if (operation.kind === 'copy-file' && (operation.contentTransform || needsLinkRewrite)) {
|
||||
const transformed = transformInstallContent(
|
||||
operation,
|
||||
fs.readFileSync(operation.sourcePath, 'utf8')
|
||||
);
|
||||
fs.writeFileSync(operation.destinationPath, rewritten, 'utf8');
|
||||
const installedContent = needsLinkRewrite
|
||||
? rewriteRelativeLinks(transformed, {
|
||||
sourceRel: operation.sourceRelativePath,
|
||||
index: linkIndex,
|
||||
})
|
||||
: transformed;
|
||||
fs.writeFileSync(operation.destinationPath, installedContent, 'utf8');
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user