const crypto = require('crypto'); const fs = require('fs'); const { execFileSync } = require('child_process'); const os = require('os'); const path = require('path'); const { resolveInstallPlan, loadInstallManifests } = require('./install-manifests'); const { readInstallState, validateInstallState } = require('./install-state'); const { assertWithinTrustedRoot } = require('./path-safety'); const { createManifestInstallPlan } = require('./install-executor'); const { prepareClaudeSkillMigration, } = require('./install/claude-skill-migration'); const { getLegacyAntigravityLocation, inspectLegacyAntigravityState, } = require('./install/antigravity-legacy-migration'); const { adaptAntigravityAgent } = require('./install/antigravity-agent'); const { buildInstallIndex, rewriteRelativeLinks } = require('./install/link-rewrite'); const { getInstallTargetAdapter, listInstallTargetAdapters } = require('./install-targets/registry'); const OPENCODE_BUILD_ARTIFACT = path.join('.opencode', 'dist'); const OPENCODE_BUILD_SCRIPT = path.join('scripts', 'build-opencode.js'); const OPENCODE_PLUGIN_NOT_BUILT_CODE = 'opencode-plugin-not-built'; 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 hasOpencodeBuildError(issues) { return Array.isArray(issues) && issues.some(issue => issue.code === OPENCODE_PLUGIN_NOT_BUILT_CODE); } function getOpencodeBuildValidationIssues(context) { return getInstallTargetAdapter('opencode').validate({ homeDir: context.homeDir, repoRoot: context.repoRoot, }); } function buildOpencodePayload(repoRoot, buildRunner = execFileSync) { buildRunner(process.execPath, [path.join(repoRoot, OPENCODE_BUILD_SCRIPT)], { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], }); } function formatBuildErrorMessage(error) { const stderr = typeof error.stderr === 'string' ? error.stderr.trim() : ''; const stdout = typeof error.stdout === 'string' ? error.stdout.trim() : ''; return stderr || stdout || error.message || 'Failed to build OpenCode payload'; } function getManagedOperations(state) { return Array.isArray(state && state.operations) ? state.operations.filter(operation => operation.ownership === 'managed') : []; } function createUnsafeRepairSourceError() { return new Error( 'Refusing unsafe repair source metadata: sources must stay within the repository.' ); } function assertSafeRepairSourcePath(sourcePath, repoRoot) { try { return assertWithinTrustedRoot(sourcePath, repoRoot, 'read repair source'); } catch { throw createUnsafeRepairSourceError(); } } function resolveOperationSourcePath(repoRoot, operation) { if (operation.sourceRelativePath) { if (typeof operation.sourceRelativePath !== 'string') { throw createUnsafeRepairSourceError(); } const sourceRelativePath = operation.sourceRelativePath; const hasParentTraversal = sourceRelativePath .split(/[/\\]+/) .includes('..'); const isAbsolute = path.isAbsolute(sourceRelativePath) || path.win32.isAbsolute(sourceRelativePath); if (isAbsolute || hasParentTraversal) { throw createUnsafeRepairSourceError(); } return assertSafeRepairSourcePath( path.resolve(repoRoot, sourceRelativePath), repoRoot ); } if (!operation.sourcePath) { return null; } if ( typeof operation.sourcePath !== 'string' || !path.isAbsolute(operation.sourcePath) ) { throw createUnsafeRepairSourceError(); } return assertSafeRepairSourcePath(operation.sourcePath, repoRoot); } function areFilesEqual(leftPath, rightPath) { try { return readFileNoFollow(leftPath).equals(readFileNoFollow(rightPath)); } catch (_error) { return false; } } function hasRecordedContentDigest(operation) { return /^[a-f0-9]{64}$/i.test(String(operation && operation.contentSha256 || '')); } function fileMatchesRecordedContent(filePath, operation) { if (!hasRecordedContentDigest(operation)) { return false; } try { return crypto.createHash('sha256') .update(readFileNoFollow(filePath)) .digest('hex') === operation.contentSha256.toLowerCase(); } catch (_error) { return false; } } function isMarkdownPath(filePath) { return /\.(md|mdx|markdown)$/i.test(String(filePath || '')); } function buildLinkIndexForOperations(operations, trustedRoot) { const mappings = (operations || []) .filter(operation => operation.kind === 'copy-file' && operation.sourceRelativePath) .map(operation => ({ sourceRel: operation.sourceRelativePath, destRel: path.relative(trustedRoot, operation.destinationPath), })); return buildInstallIndex(mappings); } function transformCopyFileContent(operation, content) { if (!operation.contentTransform) { return content; } if (operation.contentTransform === 'antigravity-agent-frontmatter') { return adaptAntigravityAgent(content, operation.sourceRelativePath); } throw new Error(`Unknown install content transform: ${operation.contentTransform}`); } function getExpectedCopyFileContent(operation, content, linkIndex) { const transformed = transformCopyFileContent(operation, content); if (!linkIndex || !operation.sourceRelativePath || !isMarkdownPath(operation.destinationPath)) { return transformed; } return rewriteRelativeLinks(transformed, { sourceRel: operation.sourceRelativePath, index: linkIndex, }); } function isPlainObject(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); } function cloneJsonValue(value) { if (value === undefined) { return undefined; } return JSON.parse(JSON.stringify(value)); } function parseJsonLikeValue(value, label) { if (value === undefined) { return undefined; } if (typeof value === 'string') { try { return JSON.parse(value); } catch (error) { throw new Error(`Invalid ${label}: ${error.message}`); } } if (value === null || Array.isArray(value) || isPlainObject(value) || typeof value === 'number' || typeof value === 'boolean') { return cloneJsonValue(value); } throw new Error(`Invalid ${label}: expected JSON-compatible data`); } function getOperationTextContent(operation) { const candidateKeys = ['renderedContent', 'content', 'managedContent', 'expectedContent', 'templateOutput']; for (const key of candidateKeys) { if (typeof operation[key] === 'string') { return operation[key]; } } return null; } function getOperationJsonPayload(operation) { const candidateKeys = ['mergePayload', 'managedPayload', 'payload', 'value', 'expectedValue']; for (const key of candidateKeys) { if (operation[key] !== undefined) { return parseJsonLikeValue(operation[key], `${operation.kind}.${key}`); } } return undefined; } function getOperationPreviousContent(operation) { const candidateKeys = ['previousContent', 'originalContent', 'backupContent']; for (const key of candidateKeys) { if (typeof operation[key] === 'string') { return operation[key]; } } return null; } function getOperationPreviousJson(operation) { const candidateKeys = ['previousValue', 'previousJson', 'originalValue']; for (const key of candidateKeys) { if (operation[key] !== undefined) { return parseJsonLikeValue(operation[key], `${operation.kind}.${key}`); } } return undefined; } function formatJson(value) { return `${JSON.stringify(value, null, 2)}\n`; } function getManagedDestination( destinationPath, trustedRoot, action, { allowFinalSymlink = false } = {} ) { if (!destinationPath || typeof destinationPath !== 'string') { throw new Error(`Refusing to ${action}: missing destination path.`); } const canonicalRoot = assertWithinTrustedRoot(trustedRoot, trustedRoot, action); const resolvedDestination = path.resolve(destinationPath); const canonicalParent = assertWithinTrustedRoot( path.dirname(resolvedDestination), canonicalRoot, action ); const managedPath = path.join(canonicalParent, path.basename(resolvedDestination)); let stat = null; try { stat = fs.lstatSync(managedPath); } catch (error) { if (!error || (error.code !== 'ENOENT' && error.code !== 'ENOTDIR')) { throw error; } } if (stat && stat.isSymbolicLink() && !allowFinalSymlink) { const error = new Error( `Refusing to ${action}: managed destination is a final symlink.` ); error.code = 'ECC_FINAL_DESTINATION_SYMLINK'; throw error; } return { canonicalRoot, exists: stat !== null, isFinalSymlink: Boolean(stat && stat.isSymbolicLink()), managedPath }; } function ensureContainedParentDir(destinationPath, trustedRoot, action) { const initialDestination = getManagedDestination( destinationPath, trustedRoot, action ); const { canonicalRoot, managedPath } = initialDestination; const canonicalParent = path.dirname(managedPath); const relativeParent = path.relative(canonicalRoot, canonicalParent); const pathSegments = relativeParent ? relativeParent.split(path.sep).filter(Boolean) : []; let currentPath = canonicalRoot; for (const segment of pathSegments) { const validatedParent = assertWithinTrustedRoot(currentPath, canonicalRoot, action); const nextPath = path.join(validatedParent, segment); try { fs.mkdirSync(nextPath); } catch (error) { if (!error || error.code !== 'EEXIST') { throw error; } } const validatedNext = assertWithinTrustedRoot(nextPath, canonicalRoot, action); const nextStat = fs.lstatSync(validatedNext); if (!nextStat.isDirectory() || nextStat.isSymbolicLink()) { throw new Error(`Refusing to ${action}: destination parent is not a trusted directory.`); } currentPath = validatedNext; } return getManagedDestination(managedPath, canonicalRoot, action).managedPath; } function prepareContainedWriteDestination(destinationPath, trustedRoot, action) { return ensureContainedParentDir(destinationPath, trustedRoot, action); } function getContainedExistingPath( destinationPath, trustedRoot, action, { allowFinalSymlink = false } = {} ) { const initialDestination = getManagedDestination( destinationPath, trustedRoot, action, { allowFinalSymlink } ); const followsToExistingPath = fs.existsSync(initialDestination.managedPath); if (!followsToExistingPath && !initialDestination.isFinalSymlink) { return null; } const finalDestination = getManagedDestination( initialDestination.managedPath, trustedRoot, action, { allowFinalSymlink } ); return finalDestination.exists ? finalDestination.managedPath : null; } function hasSameFileIdentity(leftStat, rightStat) { return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino; } function createChangedDestinationError(action) { return new Error( `Refusing to ${action}: managed destination changed during the write.` ); } function getStableParentStat(filePath, action) { const parentStat = fs.lstatSync(path.dirname(filePath)); if (!parentStat.isDirectory() || parentStat.isSymbolicLink()) { throw createChangedDestinationError(action); } return parentStat; } function assertPinnedWriteDestination( filePath, fileDescriptor, expectedParentStat, trustedRoot, action ) { const liveDestination = getManagedDestination(filePath, trustedRoot, action); if (path.resolve(liveDestination.managedPath) !== path.resolve(filePath)) { throw createChangedDestinationError(action); } const liveParentStat = getStableParentStat(filePath, action); if (!hasSameFileIdentity(expectedParentStat, liveParentStat)) { throw createChangedDestinationError(action); } const descriptorStat = fs.fstatSync(fileDescriptor); const livePathStat = fs.lstatSync(liveDestination.managedPath); if ( !descriptorStat.isFile() || !livePathStat.isFile() || livePathStat.isSymbolicLink() || !hasSameFileIdentity(descriptorStat, livePathStat) ) { throw createChangedDestinationError(action); } } function writeFileNoFollow(filePath, content, mode, trustedRoot, action) { const expectedParentStat = getStableParentStat(filePath, action); const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | (fs.constants.O_NOFOLLOW || 0); const fileDescriptor = fs.openSync(filePath, flags, mode); try { assertPinnedWriteDestination( filePath, fileDescriptor, expectedParentStat, trustedRoot, action ); fs.ftruncateSync(fileDescriptor, 0); fs.writeFileSync(fileDescriptor, content); if (mode !== undefined) { fs.fchmodSync(fileDescriptor, mode); } } finally { fs.closeSync(fileDescriptor); } } function readFileWithMetadataNoFollow(filePath, encoding) { const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0); const fileDescriptor = fs.openSync(filePath, flags); try { const stat = fs.fstatSync(fileDescriptor); if (!stat.isFile()) { throw new Error(`Refusing to read non-file path: ${filePath}`); } return { content: fs.readFileSync(fileDescriptor, encoding), mode: stat.mode, }; } finally { fs.closeSync(fileDescriptor); } } function readFileNoFollow(filePath, encoding) { return readFileWithMetadataNoFollow(filePath, encoding).content; } function readJsonNoFollow(filePath) { return JSON.parse(readFileNoFollow(filePath, 'utf8')); } function writeContainedFile(destinationPath, content, trustedRoot, action, mode) { const preparedDestination = prepareContainedWriteDestination(destinationPath, trustedRoot, action); const finalDestination = getManagedDestination( preparedDestination, trustedRoot, action ).managedPath; writeFileNoFollow( finalDestination, content, mode, trustedRoot, action ); return finalDestination; } function copyContainedFile(sourcePath, destinationPath, trustedRoot, action) { const source = readFileWithMetadataNoFollow(sourcePath); return writeContainedFile( destinationPath, source.content, trustedRoot, action, source.mode & 0o777 ); } function removeContainedPath(destinationPath, trustedRoot, action, options = {}) { const existingDestination = getContainedExistingPath( destinationPath, trustedRoot, action, { allowFinalSymlink: true } ); if (!existingDestination) { return null; } const managedDestination = getManagedDestination( existingDestination, trustedRoot, action, { allowFinalSymlink: true } ); 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; } function deepMergeJson(baseValue, patchValue) { if (!isPlainObject(baseValue) || !isPlainObject(patchValue)) { return cloneJsonValue(patchValue); } const merged = { ...baseValue }; for (const [key, value] of Object.entries(patchValue)) { if (isPlainObject(value) && isPlainObject(merged[key])) { merged[key] = deepMergeJson(merged[key], value); } else { merged[key] = cloneJsonValue(value); } } return merged; } function jsonContainsSubset(actualValue, expectedValue) { if (isPlainObject(expectedValue)) { if (!isPlainObject(actualValue)) { return false; } return Object.entries(expectedValue).every(([key, value]) => Object.prototype.hasOwnProperty.call(actualValue, key) && jsonContainsSubset(actualValue[key], value)); } if (Array.isArray(expectedValue)) { if (!Array.isArray(actualValue) || actualValue.length !== expectedValue.length) { return false; } return expectedValue.every((item, index) => jsonContainsSubset(actualValue[index], item)); } return actualValue === expectedValue; } const JSON_REMOVE_SENTINEL = Symbol('json-remove'); function deepRemoveJsonSubset(currentValue, managedValue) { if (isPlainObject(managedValue)) { if (!isPlainObject(currentValue)) { return currentValue; } const nextValue = { ...currentValue }; for (const [key, value] of Object.entries(managedValue)) { if (!Object.prototype.hasOwnProperty.call(nextValue, key)) { continue; } if (isPlainObject(value)) { const nestedValue = deepRemoveJsonSubset(nextValue[key], value); if (nestedValue === JSON_REMOVE_SENTINEL) { delete nextValue[key]; } else { nextValue[key] = nestedValue; } continue; } if (Array.isArray(value)) { if (Array.isArray(nextValue[key]) && jsonContainsSubset(nextValue[key], value)) { delete nextValue[key]; } continue; } if (nextValue[key] === value) { delete nextValue[key]; } } return Object.keys(nextValue).length === 0 ? JSON_REMOVE_SENTINEL : nextValue; } if (Array.isArray(managedValue)) { return jsonContainsSubset(currentValue, managedValue) ? JSON_REMOVE_SENTINEL : currentValue; } return currentValue === managedValue ? JSON_REMOVE_SENTINEL : currentValue; } function hydrateRecordedOperations(repoRoot, operations) { return operations.map(operation => { if (operation.kind !== 'copy-file') { return { ...operation }; } return { ...operation, sourcePath: resolveOperationSourcePath(repoRoot, operation) }; }); } function buildRecordedStatePreview(state, context, operations) { return { ...state, operations: operations.map(operation => ({ ...operation })), source: { ...state.source, repoVersion: context.packageVersion, manifestVersion: context.manifestVersion }, lastValidatedAt: new Date().toISOString() }; } function shouldRepairFromRecordedOperations(state) { return getManagedOperations(state).some(operation => operation.kind !== 'copy-file'); } function executeRepairOperation(repoRoot, operation, trustedRoot, linkIndex = null) { // Install-state is attacker-controllable; never write/delete outside the // adapter-derived trusted root, regardless of what the state file claims // (GHSA-hfpv-w6mp-5g95). if (operation.kind === 'copy-file') { const sourcePath = resolveOperationSourcePath(repoRoot, operation); if (!sourcePath || !fs.existsSync(sourcePath)) { throw new Error(`Missing source file for repair: ${sourcePath || operation.sourceRelativePath}`); } if (operation.contentTransform || isMarkdownPath(operation.destinationPath)) { const source = readFileWithMetadataNoFollow(sourcePath, 'utf8'); writeContainedFile( operation.destinationPath, getExpectedCopyFileContent(operation, source.content, linkIndex), trustedRoot, 'repair', source.mode & 0o777 ); } else { copyContainedFile(sourcePath, operation.destinationPath, trustedRoot, 'repair'); } return operation.destinationPath; } if (operation.kind === 'render-template') { const renderedContent = getOperationTextContent(operation); if (renderedContent === null) { throw new Error(`Missing rendered content for repair: ${operation.destinationPath}`); } writeContainedFile(operation.destinationPath, renderedContent, trustedRoot, 'repair'); return operation.destinationPath; } if (operation.kind === 'merge-json') { const payload = getOperationJsonPayload(operation); if (payload === undefined) { throw new Error(`Missing merge payload for repair: ${operation.destinationPath}`); } const existingDestination = getContainedExistingPath(operation.destinationPath, trustedRoot, 'repair'); const currentValue = existingDestination ? readJsonNoFollow( getManagedDestination(existingDestination, trustedRoot, 'repair').managedPath ) : {}; const mergedValue = deepMergeJson(currentValue, payload); writeContainedFile(operation.destinationPath, formatJson(mergedValue), trustedRoot, 'repair'); return operation.destinationPath; } if (operation.kind === 'remove') { const removedPath = removeContainedPath( operation.destinationPath, trustedRoot, 'repair', { recursive: true, force: true } ); return removedPath ? operation.destinationPath : null; } throw new Error(`Unsupported repair operation kind: ${operation.kind}`); } function executeUninstallOperation(operation, trustedRoot, options = {}) { // Confine deletes to the trusted install root (GHSA-hfpv-w6mp-5g95). if (operation.kind === 'copy-file') { if (options.preserveDriftedCopies) { const existingDestination = getContainedExistingPath( operation.destinationPath, trustedRoot, 'uninstall', { allowFinalSymlink: true } ); if (!existingDestination) { return { removedPaths: [], 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') .update(readFileNoFollow(existingDestination)) .digest('hex') : null; if (!currentDigest || currentDigest !== recordedDigest.toLowerCase()) { return { removedPaths: [], cleanupTargets: [], retainedPaths: [operation.destinationPath] }; } } const removedPath = removeContainedPath( operation.destinationPath, trustedRoot, 'uninstall', { force: true } ); if (!removedPath) { return { removedPaths: [], cleanupTargets: [] }; } return { removedPaths: [operation.destinationPath], cleanupTargets: [removedPath] }; } if (operation.kind === 'render-template') { const previousContent = getOperationPreviousContent(operation); if (previousContent !== null) { writeContainedFile(operation.destinationPath, previousContent, trustedRoot, 'uninstall'); return { removedPaths: [], cleanupTargets: [] }; } const previousJson = getOperationPreviousJson(operation); if (previousJson !== undefined) { writeContainedFile(operation.destinationPath, formatJson(previousJson), trustedRoot, 'uninstall'); return { removedPaths: [], cleanupTargets: [] }; } const removedPath = removeContainedPath( operation.destinationPath, trustedRoot, 'uninstall', { force: true } ); if (!removedPath) { return { removedPaths: [], cleanupTargets: [] }; } return { removedPaths: [operation.destinationPath], cleanupTargets: [removedPath] }; } if (operation.kind === 'merge-json') { const previousContent = getOperationPreviousContent(operation); if (previousContent !== null) { writeContainedFile(operation.destinationPath, previousContent, trustedRoot, 'uninstall'); return { removedPaths: [], cleanupTargets: [] }; } const previousJson = getOperationPreviousJson(operation); if (previousJson !== undefined) { writeContainedFile(operation.destinationPath, formatJson(previousJson), trustedRoot, 'uninstall'); return { removedPaths: [], cleanupTargets: [] }; } const existingDestination = getContainedExistingPath( operation.destinationPath, trustedRoot, 'uninstall' ); if (!existingDestination) { return { removedPaths: [], cleanupTargets: [] }; } const payload = getOperationJsonPayload(operation); if (payload === undefined) { throw new Error(`Missing merge payload for uninstall: ${operation.destinationPath}`); } const currentValue = readJsonNoFollow( getManagedDestination(existingDestination, trustedRoot, 'uninstall').managedPath ); const nextValue = deepRemoveJsonSubset(currentValue, payload); if (nextValue === JSON_REMOVE_SENTINEL) { const removedPath = removeContainedPath( operation.destinationPath, trustedRoot, 'uninstall', { force: true } ); return { removedPaths: removedPath ? [operation.destinationPath] : [], cleanupTargets: removedPath ? [removedPath] : [] }; } writeContainedFile(operation.destinationPath, formatJson(nextValue), trustedRoot, 'uninstall'); return { removedPaths: [], cleanupTargets: [] }; } if (operation.kind === 'remove') { const previousContent = getOperationPreviousContent(operation); if (previousContent !== null) { writeContainedFile(operation.destinationPath, previousContent, trustedRoot, 'uninstall'); return { removedPaths: [], cleanupTargets: [] }; } const previousJson = getOperationPreviousJson(operation); if (previousJson !== undefined) { writeContainedFile(operation.destinationPath, formatJson(previousJson), trustedRoot, 'uninstall'); return { removedPaths: [], cleanupTargets: [] }; } return { removedPaths: [], cleanupTargets: [] }; } throw new Error(`Unsupported uninstall operation kind: ${operation.kind}`); } function inspectManagedOperation(repoRoot, trustedRoot, operation, linkIndex = null) { const destinationPath = operation.destinationPath; if (!destinationPath) { return { status: 'invalid-destination', operation }; } let managedDestination; try { managedDestination = getManagedDestination( destinationPath, trustedRoot, 'inspect managed operation', { allowFinalSymlink: operation.kind === 'remove' } ); } catch (error) { return { status: 'unsafe-destination', operation, destinationPath, reason: error && error.code === 'ECC_FINAL_DESTINATION_SYMLINK' ? 'final-symlink' : 'outside-root' }; } const inspectedPath = managedDestination.managedPath; if (operation.kind === 'remove') { if (managedDestination.exists) { return { status: 'drifted', operation, destinationPath }; } return { status: 'ok', operation, destinationPath }; } let copySourcePath = null; if (operation.kind === 'copy-file') { try { copySourcePath = resolveOperationSourcePath(repoRoot, operation); } catch { return { status: 'unsafe-source', operation, destinationPath }; } } if (!managedDestination.exists) { return { status: 'missing', operation, destinationPath }; } if (operation.kind === 'copy-file') { if (!copySourcePath || !fs.existsSync(copySourcePath)) { return { status: 'missing-source', operation, destinationPath, sourcePath: copySourcePath }; } let contentMatches; try { contentMatches = hasRecordedContentDigest(operation) ? fileMatchesRecordedContent(inspectedPath, operation) : operation.contentTransform || isMarkdownPath(operation.destinationPath) ? readFileNoFollow(inspectedPath, 'utf8') === getExpectedCopyFileContent( operation, readFileNoFollow(copySourcePath, 'utf8'), linkIndex ) : areFilesEqual(copySourcePath, inspectedPath); } catch (_error) { return { status: 'unverified', operation, destinationPath, sourcePath: copySourcePath }; } if (!contentMatches) { return { status: 'drifted', operation, destinationPath, sourcePath: copySourcePath }; } return { status: 'ok', operation, destinationPath, sourcePath: copySourcePath }; } if (operation.kind === 'render-template') { const renderedContent = getOperationTextContent(operation); if (renderedContent === null) { return { status: 'unverified', operation, destinationPath }; } try { if (readFileNoFollow(inspectedPath, 'utf8') !== renderedContent) { return { status: 'drifted', operation, destinationPath }; } } catch { return { status: 'drifted', operation, destinationPath }; } return { status: 'ok', operation, destinationPath }; } if (operation.kind === 'merge-json') { const payload = getOperationJsonPayload(operation); if (payload === undefined) { return { status: 'unverified', operation, destinationPath }; } try { const currentValue = readJsonNoFollow(inspectedPath); if (!jsonContainsSubset(currentValue, payload)) { return { status: 'drifted', operation, destinationPath }; } } catch (_error) { return { status: 'drifted', operation, destinationPath }; } return { status: 'ok', operation, destinationPath }; } return { status: 'unverified', operation, destinationPath }; } function summarizeManagedOperationHealth(repoRoot, trustedRoot, operations) { const linkIndex = buildLinkIndexForOperations(operations, trustedRoot); return operations.reduce( (summary, operation) => { const inspection = inspectManagedOperation(repoRoot, trustedRoot, operation, linkIndex); 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 === 'unsafe-source') { summary.unsafeSource.push(inspection); } else if (inspection.status === 'unsafe-destination') { summary.unsafeDestination.push(inspection); } else if (inspection.status === 'unverified' || inspection.status === 'invalid-destination') { summary.unverified.push(inspection); } return summary; }, { missing: [], drifted: [], missingSource: [], unsafeSource: [], unsafeDestination: [], unverified: [] } ); } function getUnsafeManagedDestinationError(operationHealth) { const hasFinalSymlink = operationHealth.unsafeDestination.some( inspection => inspection.reason === 'final-symlink' ); if (hasFinalSymlink) { return 'Refusing unsafe managed destination: final symlink detected.'; } return 'Refusing unsafe managed destination outside adapter-derived install root.'; } function getUnsafeOperationResult(record, operationHealth) { const error = operationHealth.unsafeDestination.length > 0 ? getUnsafeManagedDestinationError(operationHealth) : operationHealth.unsafeSource.length > 0 ? createUnsafeRepairSourceError().message : null; if (!error) { return null; } return { adapter: record.adapter, status: 'error', installStatePath: record.installStatePath, repairedPaths: [], plannedRepairs: [], stateRefreshed: false, error }; } function buildDiscoveryRecord(adapter, context, location = null, knownState = null) { const installTargetInput = { homeDir: context.homeDir, projectRoot: context.projectRoot, repoRoot: context.projectRoot }; const targetRoot = location ? location.targetRoot : adapter.resolveRoot(installTargetInput); const installStatePath = location ? location.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, legacy: Boolean(location) }; } if (knownState) { return { adapter: { id: adapter.id, target: adapter.target, kind: adapter.kind }, targetRoot, installStatePath, exists: true, state: knownState, error: null, legacy: Boolean(location) }; } try { const state = readInstallState(installStatePath); return { adapter: { id: adapter.id, target: adapter.target, kind: adapter.kind }, targetRoot, installStatePath, exists: true, state, error: null, legacy: Boolean(location) }; } catch (error) { return { adapter: { id: adapter.id, target: adapter.target, kind: adapter.kind }, targetRoot, installStatePath, exists: true, state: null, error: error.message, legacy: Boolean(location) }; } } function discoverInstalledStates(options = {}) { const context = { homeDir: options.homeDir || process.env.HOME || os.homedir(), projectRoot: options.projectRoot || process.cwd() }; const targets = normalizeTargets(options.targets); return targets.flatMap(target => { const adapter = getInstallTargetAdapter(target); const canonicalRecord = buildDiscoveryRecord(adapter, context); if (adapter.target !== 'antigravity') { return [canonicalRecord]; } const legacyLocation = getLegacyAntigravityLocation(context.projectRoot); const legacyInspection = inspectLegacyAntigravityState(legacyLocation); if ( path.resolve(legacyLocation.installStatePath) === path.resolve(canonicalRecord.installStatePath) || legacyInspection.status === 'absent' || legacyInspection.status === 'invalid' ) { return [canonicalRecord]; } if (legacyInspection.status === 'unreadable') { return [canonicalRecord, { adapter: { id: adapter.id, target: adapter.target, kind: adapter.kind, }, targetRoot: legacyLocation.targetRoot, installStatePath: legacyLocation.installStatePath, exists: true, state: null, error: legacyInspection.error, legacy: true, }]; } return [ canonicalRecord, buildDiscoveryRecord(adapter, context, legacyLocation, legacyInspection.state), ]; }); } 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.legacy) { issues.push(buildIssue( 'warning', 'legacy-antigravity-layout', 'Legacy Antigravity install-state remains under .agent. Review and move any preserved modified or unmanaged files out of .agent, then rerun the Antigravity install to finish migration.' )); } 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, record.targetRoot, managedOperations ); const missingManagedOperations = operationHealth.missing; if (operationHealth.unsafeDestination.length > 0) { issues.push( buildIssue( 'error', 'unsafe-managed-destination', `${operationHealth.unsafeDestination.length} managed operation(s) target an unsafe destination` ) ); } if (operationHealth.unsafeSource.length > 0) { issues.push( buildIssue( 'error', 'unsafe-repair-source', `${operationHealth.unsafeSource.length} managed operation(s) reference unsafe repair source metadata` ) ); } 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 || os.homedir(), 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, options = {}) { const state = record.state; if (!state) { throw new Error('No install-state available for repair'); } if (state.request.legacyMode || shouldRepairFromRecordedOperations(state)) { const operations = hydrateRecordedOperations(context.repoRoot, getManagedOperations(state)); const statePreview = buildRecordedStatePreview(state, context, operations); return { mode: state.request.legacyMode ? 'legacy' : 'recorded', 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, exemptValidationCodes: options.exemptValidationCodes || [], }); return { ...desiredPlan, statePreview: { ...desiredPlan.statePreview, installedAt: state.installedAt, lastValidatedAt: new Date().toISOString() } }; } function buildAdapterDerivedStatePreview(statePreview, record) { return { ...statePreview, target: { ...statePreview.target, id: record.adapter.id, target: record.adapter.target, kind: record.adapter.kind, root: record.targetRoot, installStatePath: record.installStatePath } }; } function assertValidInstallStateForWrite(state, label) { const validation = validateInstallState(state); if (validation.valid) { return; } const details = validation.errors .map(error => `${error.instancePath || '/'} ${error.message}`) .join('; '); throw new Error(`Invalid install-state (${label}): ${details}`); } function writeRefreshedInstallState(record, statePreview) { const trustedStatePreview = buildAdapterDerivedStatePreview(statePreview, record); const stateWithCurrentDigests = { ...trustedStatePreview, operations: (trustedStatePreview.operations || []).map(operation => { if (!operation.destinationPath) { return { ...operation }; } try { const contentSha256 = crypto.createHash('sha256') .update(readFileNoFollow(operation.destinationPath)) .digest('hex'); return { ...operation, contentSha256 }; } catch (_error) { const { contentSha256: _staleDigest, ...operationWithoutDigest } = operation; return operationWithoutDigest; } }), }; assertValidInstallStateForWrite(stateWithCurrentDigests, record.installStatePath); return writeContainedFile( record.installStatePath, formatJson(stateWithCurrentDigests), record.targetRoot, 'repair' ); } function prepareRepairMigration(plan, record) { const trustedPlan = { ...plan, adapter: record.adapter, targetRoot: record.targetRoot, installRoot: record.targetRoot, installStatePath: record.installStatePath, statePreview: buildAdapterDerivedStatePreview(plan.statePreview, record), }; const migration = prepareClaudeSkillMigration(trustedPlan); return { migration, plan: { ...trustedPlan, operations: migration.finalState.operations, statePreview: migration.finalState, warnings: [ ...(Array.isArray(plan.warnings) ? plan.warnings : []), ...migration.warnings, ], }, }; } function repairInstalledStates(options = {}) { const repoRoot = options.repoRoot || DEFAULT_REPO_ROOT; const manifests = loadInstallManifests({ repoRoot }); const context = { repoRoot, homeDir: options.homeDir || process.env.HOME || os.homedir(), projectRoot: options.projectRoot || process.cwd(), manifestVersion: manifests.modulesVersion, packageVersion: readPackageVersion(repoRoot) }; const buildOpencodeRunner = typeof options.buildOpencodePayload === 'function' ? options.buildOpencodePayload : buildOpencodePayload; const records = discoverInstalledStates({ homeDir: context.homeDir, projectRoot: context.projectRoot, targets: options.targets }).filter(record => record.exists && !record.legacy); const results = records.map(record => { if (record.error) { return { adapter: record.adapter, status: 'error', installStatePath: record.installStatePath, repairedPaths: [], plannedRepairs: [], error: record.error }; } try { const needsOpencodeBuild = record.adapter.target === 'opencode' && hasOpencodeBuildError(getOpencodeBuildValidationIssues(context)); const opencodeBuildRepairPath = path.join(context.repoRoot, OPENCODE_BUILD_ARTIFACT); if (needsOpencodeBuild && options.dryRun) { const rawPlan = createRepairPlanFromRecord(record, context, { exemptValidationCodes: [OPENCODE_PLUGIN_NOT_BUILT_CODE], }); const { plan: desiredPlan } = prepareRepairMigration(rawPlan, record); const operationHealth = summarizeManagedOperationHealth( context.repoRoot, record.targetRoot, desiredPlan.operations ); const unsafeOperationResult = getUnsafeOperationResult( record, operationHealth ); if (unsafeOperationResult) { return unsafeOperationResult; } const repairOperations = [...operationHealth.missing.map(entry => ({ ...entry.operation })), ...operationHealth.drifted.map(entry => ({ ...entry.operation }))]; const plannedRepairs = [opencodeBuildRepairPath, ...repairOperations.map(operation => operation.destinationPath)]; return { adapter: record.adapter, status: 'planned', installStatePath: record.installStatePath, repairedPaths: [], plannedRepairs, stateRefreshed: false, warnings: desiredPlan.warnings, error: null }; } if (needsOpencodeBuild) { try { buildOpencodeRunner(context.repoRoot); } catch (error) { return { adapter: record.adapter, status: 'error', installStatePath: record.installStatePath, repairedPaths: [], plannedRepairs: [], error: formatBuildErrorMessage(error) }; } } const rawPlan = createRepairPlanFromRecord(record, context); const { migration, plan: desiredPlan, } = prepareRepairMigration(rawPlan, record); const operationHealth = summarizeManagedOperationHealth( context.repoRoot, record.targetRoot, desiredPlan.operations ); const unsafeOperationResult = getUnsafeOperationResult( record, operationHealth ); if (unsafeOperationResult) { return unsafeOperationResult; } if (operationHealth.missingSource.length > 0) { return { adapter: record.adapter, status: 'error', installStatePath: record.installStatePath, repairedPaths: [], plannedRepairs: [], warnings: desiredPlan.warnings, 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 repairLinkIndex = buildLinkIndexForOperations(desiredPlan.operations, record.targetRoot); const legacyMigrationPaths = migration.legacyOperationsToRemove.map( operation => operation.destinationPath ); const plannedRepairs = [...new Set([ ...(needsOpencodeBuild ? [opencodeBuildRepairPath] : []), ...repairOperations.map(operation => operation.destinationPath), ...legacyMigrationPaths, ])]; if (options.dryRun) { return { adapter: record.adapter, status: plannedRepairs.length > 0 ? 'planned' : 'ok', installStatePath: record.installStatePath, repairedPaths: [], plannedRepairs, stateRefreshed: plannedRepairs.length === 0, warnings: desiredPlan.warnings, error: null }; } const hasLegacyMigration = migration.legacyOperationsToRemove.length > 0; const repairedPaths = needsOpencodeBuild ? [opencodeBuildRepairPath] : []; if (migration.requiresBridgeState && (repairOperations.length > 0 || hasLegacyMigration)) { writeRefreshedInstallState(record, migration.bridgeState); } for (const operation of repairOperations) { const repairedPath = executeRepairOperation( context.repoRoot, operation, record.targetRoot, repairLinkIndex ); if (repairedPath) { repairedPaths.push(repairedPath); } } if (hasLegacyMigration) { for (const operation of migration.legacyOperationsToRemove) { const removedPath = removeContainedPath( operation.destinationPath, record.targetRoot, 'migrate managed Claude skill', { force: true } ); if (removedPath) { repairedPaths.push(removedPath); } } } const changedInstalledBytes = repairOperations.length > 0 || needsOpencodeBuild || hasLegacyMigration; const statePreviewToWrite = changedInstalledBytes ? desiredPlan.statePreview : { ...desiredPlan.statePreview, installedAt: record.state.installedAt, source: { ...record.state.source }, }; writeRefreshedInstallState(record, statePreviewToWrite); return { adapter: record.adapter, status: (repairOperations.length > 0 || needsOpencodeBuild || hasLegacyMigration) ? 'repaired' : 'ok', installStatePath: record.installStatePath, repairedPaths, plannedRepairs: [], stateRefreshed: true, warnings: desiredPlan.warnings, 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) { const trustedStopAt = assertWithinTrustedRoot(stopAt, stopAt, 'clean up'); const trustedFilePath = assertWithinTrustedRoot(filePath, trustedStopAt, 'clean up'); let currentPath = path.dirname(trustedFilePath); while (currentPath) { const relativePath = path.relative(trustedStopAt, currentPath); const isContained = relativePath !== '..' && !relativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativePath); if (!isContained || relativePath === '') { break; } let validatedPath = assertWithinTrustedRoot(currentPath, trustedStopAt, 'clean up'); if (!fs.existsSync(validatedPath)) { currentPath = path.dirname(validatedPath); continue; } validatedPath = assertWithinTrustedRoot(validatedPath, trustedStopAt, 'clean up'); const stat = fs.lstatSync(validatedPath); if (!stat.isDirectory() || stat.isSymbolicLink()) { break; } validatedPath = assertWithinTrustedRoot(validatedPath, trustedStopAt, 'clean up'); if (fs.readdirSync(validatedPath).length > 0) { break; } const finalPath = assertWithinTrustedRoot(validatedPath, trustedStopAt, 'clean up'); const removedPath = removeContainedPath(finalPath, trustedStopAt, 'clean up'); if (!removedPath) break; currentPath = path.dirname(removedPath); } } 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 managedOperations = getManagedOperations(state); if (record.legacy && managedOperations.length > 0) { return { adapter: record.adapter, status: 'partial', installStatePath: record.installStatePath, removedPaths: [], plannedRemovals: [], retainedPaths: managedOperations.map(operation => operation.destinationPath), warning: 'Legacy Antigravity files were preserved because their provenance cannot be revalidated during uninstall. Rerun the Antigravity installer to migrate verified files, then review .agent manually.', error: null }; } const plannedRemovals = Array.from(new Set([ ...managedOperations.map(operation => operation.destinationPath), record.installStatePath ])); if (options.dryRun) { return { adapter: record.adapter, status: 'planned', installStatePath: record.installStatePath, removedPaths: [], plannedRemovals, error: null }; } try { const removedPaths = []; const cleanupTargets = []; const retainedPaths = []; const operations = getManagedOperations(state); for (const operation of operations) { const outcome = executeUninstallOperation(operation, record.targetRoot, { preserveDriftedCopies: true, }); removedPaths.push(...outcome.removedPaths); cleanupTargets.push(...outcome.cleanupTargets); retainedPaths.push(...(outcome.retainedPaths || [])); } 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) { cleanupEmptyParentDirs(cleanupTarget, record.targetRoot); } return { adapter: record.adapter, 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) { 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), partialCount: accumulator.partialCount + (result.status === 'partial' ? 1 : 0), errorCount: accumulator.errorCount + (result.status === 'error' ? 1 : 0) }), { checkedCount: 0, uninstalledCount: 0, plannedRemovalCount: 0, partialCount: 0, errorCount: 0 } ); return { dryRun: Boolean(options.dryRun), generatedAt: new Date().toISOString(), results, summary }; } module.exports = { DEFAULT_REPO_ROOT, buildDoctorReport, discoverInstalledStates, normalizeTargets, repairInstalledStates, uninstallInstalledStates };