mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
feat: orchestration harness, selective install, observer improvements
This commit is contained in:
763
scripts/lib/install-lifecycle.js
Normal file
763
scripts/lib/install-lifecycle.js
Normal file
@@ -0,0 +1,763 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { resolveInstallPlan, loadInstallManifests } = require('./install-manifests');
|
||||
const { readInstallState, writeInstallState } = require('./install-state');
|
||||
const {
|
||||
applyInstallPlan,
|
||||
createLegacyInstallPlan,
|
||||
createManifestInstallPlan,
|
||||
} = require('./install-executor');
|
||||
const {
|
||||
getInstallTargetAdapter,
|
||||
listInstallTargetAdapters,
|
||||
} = require('./install-targets/registry');
|
||||
|
||||
const DEFAULT_REPO_ROOT = path.join(__dirname, '../..');
|
||||
|
||||
function readPackageVersion(repoRoot) {
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
|
||||
return packageJson.version || null;
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTargets(targets) {
|
||||
if (!Array.isArray(targets) || targets.length === 0) {
|
||||
return listInstallTargetAdapters().map(adapter => adapter.target);
|
||||
}
|
||||
|
||||
const normalizedTargets = [];
|
||||
for (const target of targets) {
|
||||
const adapter = getInstallTargetAdapter(target);
|
||||
if (!normalizedTargets.includes(adapter.target)) {
|
||||
normalizedTargets.push(adapter.target);
|
||||
}
|
||||
}
|
||||
|
||||
return normalizedTargets;
|
||||
}
|
||||
|
||||
function compareStringArrays(left, right) {
|
||||
const leftValues = Array.isArray(left) ? left : [];
|
||||
const rightValues = Array.isArray(right) ? right : [];
|
||||
|
||||
if (leftValues.length !== rightValues.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return leftValues.every((value, index) => value === rightValues[index]);
|
||||
}
|
||||
|
||||
function getManagedOperations(state) {
|
||||
return Array.isArray(state && state.operations)
|
||||
? state.operations.filter(operation => operation.ownership === 'managed')
|
||||
: [];
|
||||
}
|
||||
|
||||
function resolveOperationSourcePath(repoRoot, operation) {
|
||||
if (operation.sourceRelativePath) {
|
||||
return path.join(repoRoot, operation.sourceRelativePath);
|
||||
}
|
||||
|
||||
return operation.sourcePath || null;
|
||||
}
|
||||
|
||||
function areFilesEqual(leftPath, rightPath) {
|
||||
try {
|
||||
const leftStat = fs.statSync(leftPath);
|
||||
const rightStat = fs.statSync(rightPath);
|
||||
if (!leftStat.isFile() || !rightStat.isFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return fs.readFileSync(leftPath).equals(fs.readFileSync(rightPath));
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function inspectManagedOperation(repoRoot, operation) {
|
||||
const destinationPath = operation.destinationPath;
|
||||
if (!destinationPath) {
|
||||
return {
|
||||
status: 'invalid-destination',
|
||||
operation,
|
||||
};
|
||||
}
|
||||
|
||||
if (!fs.existsSync(destinationPath)) {
|
||||
return {
|
||||
status: 'missing',
|
||||
operation,
|
||||
destinationPath,
|
||||
};
|
||||
}
|
||||
|
||||
if (operation.kind !== 'copy-file') {
|
||||
return {
|
||||
status: 'unverified',
|
||||
operation,
|
||||
destinationPath,
|
||||
};
|
||||
}
|
||||
|
||||
const sourcePath = resolveOperationSourcePath(repoRoot, operation);
|
||||
if (!sourcePath || !fs.existsSync(sourcePath)) {
|
||||
return {
|
||||
status: 'missing-source',
|
||||
operation,
|
||||
destinationPath,
|
||||
sourcePath,
|
||||
};
|
||||
}
|
||||
|
||||
if (!areFilesEqual(sourcePath, destinationPath)) {
|
||||
return {
|
||||
status: 'drifted',
|
||||
operation,
|
||||
destinationPath,
|
||||
sourcePath,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
operation,
|
||||
destinationPath,
|
||||
sourcePath,
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeManagedOperationHealth(repoRoot, operations) {
|
||||
return operations.reduce((summary, operation) => {
|
||||
const inspection = inspectManagedOperation(repoRoot, operation);
|
||||
if (inspection.status === 'missing') {
|
||||
summary.missing.push(inspection);
|
||||
} else if (inspection.status === 'drifted') {
|
||||
summary.drifted.push(inspection);
|
||||
} else if (inspection.status === 'missing-source') {
|
||||
summary.missingSource.push(inspection);
|
||||
} else if (inspection.status === 'unverified' || inspection.status === 'invalid-destination') {
|
||||
summary.unverified.push(inspection);
|
||||
}
|
||||
return summary;
|
||||
}, {
|
||||
missing: [],
|
||||
drifted: [],
|
||||
missingSource: [],
|
||||
unverified: [],
|
||||
});
|
||||
}
|
||||
|
||||
function buildDiscoveryRecord(adapter, context) {
|
||||
const installTargetInput = {
|
||||
homeDir: context.homeDir,
|
||||
projectRoot: context.projectRoot,
|
||||
repoRoot: context.projectRoot,
|
||||
};
|
||||
const targetRoot = adapter.resolveRoot(installTargetInput);
|
||||
const installStatePath = adapter.getInstallStatePath(installTargetInput);
|
||||
const exists = fs.existsSync(installStatePath);
|
||||
|
||||
if (!exists) {
|
||||
return {
|
||||
adapter: {
|
||||
id: adapter.id,
|
||||
target: adapter.target,
|
||||
kind: adapter.kind,
|
||||
},
|
||||
targetRoot,
|
||||
installStatePath,
|
||||
exists: false,
|
||||
state: null,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const state = readInstallState(installStatePath);
|
||||
return {
|
||||
adapter: {
|
||||
id: adapter.id,
|
||||
target: adapter.target,
|
||||
kind: adapter.kind,
|
||||
},
|
||||
targetRoot,
|
||||
installStatePath,
|
||||
exists: true,
|
||||
state,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
adapter: {
|
||||
id: adapter.id,
|
||||
target: adapter.target,
|
||||
kind: adapter.kind,
|
||||
},
|
||||
targetRoot,
|
||||
installStatePath,
|
||||
exists: true,
|
||||
state: null,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function discoverInstalledStates(options = {}) {
|
||||
const context = {
|
||||
homeDir: options.homeDir || process.env.HOME,
|
||||
projectRoot: options.projectRoot || process.cwd(),
|
||||
};
|
||||
const targets = normalizeTargets(options.targets);
|
||||
|
||||
return targets.map(target => {
|
||||
const adapter = getInstallTargetAdapter(target);
|
||||
return buildDiscoveryRecord(adapter, context);
|
||||
});
|
||||
}
|
||||
|
||||
function buildIssue(severity, code, message, extra = {}) {
|
||||
return {
|
||||
severity,
|
||||
code,
|
||||
message,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
function determineStatus(issues) {
|
||||
if (issues.some(issue => issue.severity === 'error')) {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
if (issues.some(issue => issue.severity === 'warning')) {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
function analyzeRecord(record, context) {
|
||||
const issues = [];
|
||||
|
||||
if (record.error) {
|
||||
issues.push(buildIssue('error', 'invalid-install-state', record.error));
|
||||
return {
|
||||
...record,
|
||||
status: determineStatus(issues),
|
||||
issues,
|
||||
};
|
||||
}
|
||||
|
||||
const state = record.state;
|
||||
if (!state) {
|
||||
return {
|
||||
...record,
|
||||
status: 'missing',
|
||||
issues,
|
||||
};
|
||||
}
|
||||
|
||||
if (!fs.existsSync(state.target.root)) {
|
||||
issues.push(buildIssue(
|
||||
'error',
|
||||
'missing-target-root',
|
||||
`Target root does not exist: ${state.target.root}`
|
||||
));
|
||||
}
|
||||
|
||||
if (state.target.root !== record.targetRoot) {
|
||||
issues.push(buildIssue(
|
||||
'warning',
|
||||
'target-root-mismatch',
|
||||
`Recorded target root differs from current target root (${record.targetRoot})`,
|
||||
{
|
||||
recordedTargetRoot: state.target.root,
|
||||
currentTargetRoot: record.targetRoot,
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
if (state.target.installStatePath !== record.installStatePath) {
|
||||
issues.push(buildIssue(
|
||||
'warning',
|
||||
'install-state-path-mismatch',
|
||||
`Recorded install-state path differs from current path (${record.installStatePath})`,
|
||||
{
|
||||
recordedInstallStatePath: state.target.installStatePath,
|
||||
currentInstallStatePath: record.installStatePath,
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
const managedOperations = getManagedOperations(state);
|
||||
const operationHealth = summarizeManagedOperationHealth(context.repoRoot, managedOperations);
|
||||
const missingManagedOperations = operationHealth.missing;
|
||||
|
||||
if (missingManagedOperations.length > 0) {
|
||||
issues.push(buildIssue(
|
||||
'error',
|
||||
'missing-managed-files',
|
||||
`${missingManagedOperations.length} managed file(s) are missing`,
|
||||
{
|
||||
paths: missingManagedOperations.map(entry => entry.destinationPath),
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
if (operationHealth.drifted.length > 0) {
|
||||
issues.push(buildIssue(
|
||||
'warning',
|
||||
'drifted-managed-files',
|
||||
`${operationHealth.drifted.length} managed file(s) differ from the source repo`,
|
||||
{
|
||||
paths: operationHealth.drifted.map(entry => entry.destinationPath),
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
if (operationHealth.missingSource.length > 0) {
|
||||
issues.push(buildIssue(
|
||||
'error',
|
||||
'missing-source-files',
|
||||
`${operationHealth.missingSource.length} source file(s) referenced by install-state are missing`,
|
||||
{
|
||||
paths: operationHealth.missingSource.map(entry => entry.sourcePath).filter(Boolean),
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
if (operationHealth.unverified.length > 0) {
|
||||
issues.push(buildIssue(
|
||||
'warning',
|
||||
'unverified-managed-operations',
|
||||
`${operationHealth.unverified.length} managed operation(s) could not be content-verified`,
|
||||
{
|
||||
paths: operationHealth.unverified.map(entry => entry.destinationPath).filter(Boolean),
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
if (state.source.manifestVersion !== context.manifestVersion) {
|
||||
issues.push(buildIssue(
|
||||
'warning',
|
||||
'manifest-version-mismatch',
|
||||
`Recorded manifest version ${state.source.manifestVersion} differs from current manifest version ${context.manifestVersion}`
|
||||
));
|
||||
}
|
||||
|
||||
if (
|
||||
context.packageVersion
|
||||
&& state.source.repoVersion
|
||||
&& state.source.repoVersion !== context.packageVersion
|
||||
) {
|
||||
issues.push(buildIssue(
|
||||
'warning',
|
||||
'repo-version-mismatch',
|
||||
`Recorded repo version ${state.source.repoVersion} differs from current repo version ${context.packageVersion}`
|
||||
));
|
||||
}
|
||||
|
||||
if (!state.request.legacyMode) {
|
||||
try {
|
||||
const desiredPlan = resolveInstallPlan({
|
||||
repoRoot: context.repoRoot,
|
||||
projectRoot: context.projectRoot,
|
||||
homeDir: context.homeDir,
|
||||
target: record.adapter.target,
|
||||
profileId: state.request.profile || null,
|
||||
moduleIds: state.request.modules || [],
|
||||
includeComponentIds: state.request.includeComponents || [],
|
||||
excludeComponentIds: state.request.excludeComponents || [],
|
||||
});
|
||||
|
||||
if (
|
||||
!compareStringArrays(desiredPlan.selectedModuleIds, state.resolution.selectedModules)
|
||||
|| !compareStringArrays(desiredPlan.skippedModuleIds, state.resolution.skippedModules)
|
||||
) {
|
||||
issues.push(buildIssue(
|
||||
'warning',
|
||||
'resolution-drift',
|
||||
'Current manifest resolution differs from recorded install-state',
|
||||
{
|
||||
expectedSelectedModules: desiredPlan.selectedModuleIds,
|
||||
recordedSelectedModules: state.resolution.selectedModules,
|
||||
expectedSkippedModules: desiredPlan.skippedModuleIds,
|
||||
recordedSkippedModules: state.resolution.skippedModules,
|
||||
}
|
||||
));
|
||||
}
|
||||
} catch (error) {
|
||||
issues.push(buildIssue(
|
||||
'error',
|
||||
'resolution-unavailable',
|
||||
error.message
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
status: determineStatus(issues),
|
||||
issues,
|
||||
};
|
||||
}
|
||||
|
||||
function buildDoctorReport(options = {}) {
|
||||
const repoRoot = options.repoRoot || DEFAULT_REPO_ROOT;
|
||||
const manifests = loadInstallManifests({ repoRoot });
|
||||
const records = discoverInstalledStates({
|
||||
homeDir: options.homeDir,
|
||||
projectRoot: options.projectRoot,
|
||||
targets: options.targets,
|
||||
}).filter(record => record.exists);
|
||||
const context = {
|
||||
repoRoot,
|
||||
homeDir: options.homeDir || process.env.HOME,
|
||||
projectRoot: options.projectRoot || process.cwd(),
|
||||
manifestVersion: manifests.modulesVersion,
|
||||
packageVersion: readPackageVersion(repoRoot),
|
||||
};
|
||||
const results = records.map(record => analyzeRecord(record, context));
|
||||
const summary = results.reduce((accumulator, result) => {
|
||||
const errorCount = result.issues.filter(issue => issue.severity === 'error').length;
|
||||
const warningCount = result.issues.filter(issue => issue.severity === 'warning').length;
|
||||
|
||||
return {
|
||||
checkedCount: accumulator.checkedCount + 1,
|
||||
okCount: accumulator.okCount + (result.status === 'ok' ? 1 : 0),
|
||||
errorCount: accumulator.errorCount + errorCount,
|
||||
warningCount: accumulator.warningCount + warningCount,
|
||||
};
|
||||
}, {
|
||||
checkedCount: 0,
|
||||
okCount: 0,
|
||||
errorCount: 0,
|
||||
warningCount: 0,
|
||||
});
|
||||
|
||||
return {
|
||||
generatedAt: new Date().toISOString(),
|
||||
packageVersion: context.packageVersion,
|
||||
manifestVersion: context.manifestVersion,
|
||||
results,
|
||||
summary,
|
||||
};
|
||||
}
|
||||
|
||||
function createRepairPlanFromRecord(record, context) {
|
||||
const state = record.state;
|
||||
if (!state) {
|
||||
throw new Error('No install-state available for repair');
|
||||
}
|
||||
|
||||
if (state.request.legacyMode) {
|
||||
const operations = getManagedOperations(state).map(operation => ({
|
||||
...operation,
|
||||
sourcePath: resolveOperationSourcePath(context.repoRoot, operation),
|
||||
}));
|
||||
|
||||
const statePreview = {
|
||||
...state,
|
||||
operations: operations.map(operation => ({ ...operation })),
|
||||
source: {
|
||||
...state.source,
|
||||
repoVersion: context.packageVersion,
|
||||
manifestVersion: context.manifestVersion,
|
||||
},
|
||||
lastValidatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return {
|
||||
mode: 'legacy',
|
||||
target: record.adapter.target,
|
||||
adapter: record.adapter,
|
||||
targetRoot: state.target.root,
|
||||
installRoot: state.target.root,
|
||||
installStatePath: state.target.installStatePath,
|
||||
warnings: [],
|
||||
languages: Array.isArray(state.request.legacyLanguages)
|
||||
? [...state.request.legacyLanguages]
|
||||
: [],
|
||||
operations,
|
||||
statePreview,
|
||||
};
|
||||
}
|
||||
|
||||
const desiredPlan = createManifestInstallPlan({
|
||||
sourceRoot: context.repoRoot,
|
||||
target: record.adapter.target,
|
||||
profileId: state.request.profile || null,
|
||||
moduleIds: state.request.modules || [],
|
||||
includeComponentIds: state.request.includeComponents || [],
|
||||
excludeComponentIds: state.request.excludeComponents || [],
|
||||
projectRoot: context.projectRoot,
|
||||
homeDir: context.homeDir,
|
||||
});
|
||||
|
||||
return {
|
||||
...desiredPlan,
|
||||
statePreview: {
|
||||
...desiredPlan.statePreview,
|
||||
installedAt: state.installedAt,
|
||||
lastValidatedAt: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function repairInstalledStates(options = {}) {
|
||||
const repoRoot = options.repoRoot || DEFAULT_REPO_ROOT;
|
||||
const manifests = loadInstallManifests({ repoRoot });
|
||||
const context = {
|
||||
repoRoot,
|
||||
homeDir: options.homeDir || process.env.HOME,
|
||||
projectRoot: options.projectRoot || process.cwd(),
|
||||
manifestVersion: manifests.modulesVersion,
|
||||
packageVersion: readPackageVersion(repoRoot),
|
||||
};
|
||||
const records = discoverInstalledStates({
|
||||
homeDir: context.homeDir,
|
||||
projectRoot: context.projectRoot,
|
||||
targets: options.targets,
|
||||
}).filter(record => record.exists);
|
||||
|
||||
const results = records.map(record => {
|
||||
if (record.error) {
|
||||
return {
|
||||
adapter: record.adapter,
|
||||
status: 'error',
|
||||
installStatePath: record.installStatePath,
|
||||
repairedPaths: [],
|
||||
plannedRepairs: [],
|
||||
error: record.error,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const desiredPlan = createRepairPlanFromRecord(record, context);
|
||||
const operationHealth = summarizeManagedOperationHealth(context.repoRoot, desiredPlan.operations);
|
||||
|
||||
if (operationHealth.missingSource.length > 0) {
|
||||
return {
|
||||
adapter: record.adapter,
|
||||
status: 'error',
|
||||
installStatePath: record.installStatePath,
|
||||
repairedPaths: [],
|
||||
plannedRepairs: [],
|
||||
error: `Missing source file(s): ${operationHealth.missingSource.map(entry => entry.sourcePath).join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
const repairOperations = [
|
||||
...operationHealth.missing.map(entry => ({ ...entry.operation })),
|
||||
...operationHealth.drifted.map(entry => ({ ...entry.operation })),
|
||||
];
|
||||
const plannedRepairs = repairOperations.map(operation => operation.destinationPath);
|
||||
|
||||
if (options.dryRun) {
|
||||
return {
|
||||
adapter: record.adapter,
|
||||
status: plannedRepairs.length > 0 ? 'planned' : 'ok',
|
||||
installStatePath: record.installStatePath,
|
||||
repairedPaths: [],
|
||||
plannedRepairs,
|
||||
stateRefreshed: plannedRepairs.length === 0,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (repairOperations.length > 0) {
|
||||
applyInstallPlan({
|
||||
...desiredPlan,
|
||||
operations: repairOperations,
|
||||
statePreview: desiredPlan.statePreview,
|
||||
});
|
||||
} else {
|
||||
writeInstallState(desiredPlan.installStatePath, desiredPlan.statePreview);
|
||||
}
|
||||
|
||||
return {
|
||||
adapter: record.adapter,
|
||||
status: repairOperations.length > 0 ? 'repaired' : 'ok',
|
||||
installStatePath: record.installStatePath,
|
||||
repairedPaths: plannedRepairs,
|
||||
plannedRepairs: [],
|
||||
stateRefreshed: true,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
adapter: record.adapter,
|
||||
status: 'error',
|
||||
installStatePath: record.installStatePath,
|
||||
repairedPaths: [],
|
||||
plannedRepairs: [],
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const summary = results.reduce((accumulator, result) => ({
|
||||
checkedCount: accumulator.checkedCount + 1,
|
||||
repairedCount: accumulator.repairedCount + (result.status === 'repaired' ? 1 : 0),
|
||||
plannedRepairCount: accumulator.plannedRepairCount + (result.status === 'planned' ? 1 : 0),
|
||||
errorCount: accumulator.errorCount + (result.status === 'error' ? 1 : 0),
|
||||
}), {
|
||||
checkedCount: 0,
|
||||
repairedCount: 0,
|
||||
plannedRepairCount: 0,
|
||||
errorCount: 0,
|
||||
});
|
||||
|
||||
return {
|
||||
dryRun: Boolean(options.dryRun),
|
||||
generatedAt: new Date().toISOString(),
|
||||
results,
|
||||
summary,
|
||||
};
|
||||
}
|
||||
|
||||
function cleanupEmptyParentDirs(filePath, stopAt) {
|
||||
let currentPath = path.dirname(filePath);
|
||||
const normalizedStopAt = path.resolve(stopAt);
|
||||
|
||||
while (
|
||||
currentPath
|
||||
&& path.resolve(currentPath).startsWith(normalizedStopAt)
|
||||
&& path.resolve(currentPath) !== normalizedStopAt
|
||||
) {
|
||||
if (!fs.existsSync(currentPath)) {
|
||||
currentPath = path.dirname(currentPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
const stat = fs.lstatSync(currentPath);
|
||||
if (!stat.isDirectory() || fs.readdirSync(currentPath).length > 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
fs.rmdirSync(currentPath);
|
||||
currentPath = path.dirname(currentPath);
|
||||
}
|
||||
}
|
||||
|
||||
function uninstallInstalledStates(options = {}) {
|
||||
const records = discoverInstalledStates({
|
||||
homeDir: options.homeDir,
|
||||
projectRoot: options.projectRoot,
|
||||
targets: options.targets,
|
||||
}).filter(record => record.exists);
|
||||
|
||||
const results = records.map(record => {
|
||||
if (record.error || !record.state) {
|
||||
return {
|
||||
adapter: record.adapter,
|
||||
status: 'error',
|
||||
installStatePath: record.installStatePath,
|
||||
removedPaths: [],
|
||||
plannedRemovals: [],
|
||||
error: record.error || 'No valid install-state available',
|
||||
};
|
||||
}
|
||||
|
||||
const state = record.state;
|
||||
const plannedRemovals = Array.from(new Set([
|
||||
...getManagedOperations(state).map(operation => operation.destinationPath),
|
||||
state.target.installStatePath,
|
||||
]));
|
||||
|
||||
if (options.dryRun) {
|
||||
return {
|
||||
adapter: record.adapter,
|
||||
status: 'planned',
|
||||
installStatePath: record.installStatePath,
|
||||
removedPaths: [],
|
||||
plannedRemovals,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const removedPaths = [];
|
||||
const cleanupTargets = [];
|
||||
const filePaths = Array.from(new Set(
|
||||
getManagedOperations(state).map(operation => operation.destinationPath)
|
||||
)).sort((left, right) => right.length - left.length);
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const stat = fs.lstatSync(filePath);
|
||||
if (stat.isDirectory()) {
|
||||
throw new Error(`Refusing to remove managed directory path without explicit support: ${filePath}`);
|
||||
}
|
||||
|
||||
fs.rmSync(filePath, { force: true });
|
||||
removedPaths.push(filePath);
|
||||
cleanupTargets.push(filePath);
|
||||
}
|
||||
|
||||
if (fs.existsSync(state.target.installStatePath)) {
|
||||
fs.rmSync(state.target.installStatePath, { force: true });
|
||||
removedPaths.push(state.target.installStatePath);
|
||||
cleanupTargets.push(state.target.installStatePath);
|
||||
}
|
||||
|
||||
for (const cleanupTarget of cleanupTargets) {
|
||||
cleanupEmptyParentDirs(cleanupTarget, state.target.root);
|
||||
}
|
||||
|
||||
return {
|
||||
adapter: record.adapter,
|
||||
status: 'uninstalled',
|
||||
installStatePath: record.installStatePath,
|
||||
removedPaths,
|
||||
plannedRemovals: [],
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
adapter: record.adapter,
|
||||
status: 'error',
|
||||
installStatePath: record.installStatePath,
|
||||
removedPaths: [],
|
||||
plannedRemovals,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const summary = results.reduce((accumulator, result) => ({
|
||||
checkedCount: accumulator.checkedCount + 1,
|
||||
uninstalledCount: accumulator.uninstalledCount + (result.status === 'uninstalled' ? 1 : 0),
|
||||
plannedRemovalCount: accumulator.plannedRemovalCount + (result.status === 'planned' ? 1 : 0),
|
||||
errorCount: accumulator.errorCount + (result.status === 'error' ? 1 : 0),
|
||||
}), {
|
||||
checkedCount: 0,
|
||||
uninstalledCount: 0,
|
||||
plannedRemovalCount: 0,
|
||||
errorCount: 0,
|
||||
});
|
||||
|
||||
return {
|
||||
dryRun: Boolean(options.dryRun),
|
||||
generatedAt: new Date().toISOString(),
|
||||
results,
|
||||
summary,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_REPO_ROOT,
|
||||
buildDoctorReport,
|
||||
discoverInstalledStates,
|
||||
normalizeTargets,
|
||||
repairInstalledStates,
|
||||
uninstallInstalledStates,
|
||||
};
|
||||
Reference in New Issue
Block a user