2026-08-13 16:42:51 -04:00
const crypto = require ( 'crypto' );
2026-03-14 12:55:25 -07:00
const fs = require ( 'fs' );
2026-07-08 17:14:33 -04:00
const { execFileSync } = require ( 'child_process' );
2026-03-28 11:28:12 +08:00
const os = require ( 'os' );
2026-03-14 12:55:25 -07:00
const path = require ( 'path' );
2026-08-07 15:18:14 -04:00
const { loadInstallManifests } = require ( './install-manifests' );
2026-07-27 11:11:29 -07:00
const { readInstallState , validateInstallState } = require ( './install-state' );
2026-06-18 19:54:22 -04:00
const { assertWithinTrustedRoot } = require ( './path-safety' );
2026-08-07 15:18:14 -04:00
const { createInstallPlanFromRequest } = require ( './install/runtime' );
const { getRecordedHookConsent } = require ( './install/hook-consent' );
2026-07-26 03:20:06 -07:00
const {
prepareClaudeSkillMigration ,
} = require ( './install/claude-skill-migration' );
2026-08-13 16:42:51 -04:00
const {
getLegacyAntigravityLocation ,
inspectLegacyAntigravityState ,
} = require ( './install/antigravity-legacy-migration' );
2026-08-24 20:59:57 -04:00
const {
getLegacyOpencodeLocation ,
inspectLegacyOpencodeState ,
} = require ( './install/opencode-legacy-migration' );
2026-08-13 16:42:51 -04:00
const { adaptAntigravityAgent } = require ( './install/antigravity-agent' );
const { buildInstallIndex , rewriteRelativeLinks } = require ( './install/link-rewrite' );
2026-06-18 20:03:24 -04:00
const { getInstallTargetAdapter , listInstallTargetAdapters } = require ( './install-targets/registry' );
2026-08-25 12:17:21 -04:00
const { resolveInvocationEnvironment } = require ( './invocation-environment' );
2026-07-08 17:14:33 -04:00
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' ;
2026-03-14 12:55:25 -07:00
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 ]);
}
2026-08-07 15:18:14 -04:00
function buildRecordedManifestRequest ( record ) {
const state = record . state || {};
const request = state . request || {};
return {
mode : 'manifest' ,
target : state . target && state . target . target ? state . target . target : record . adapter . target ,
profileId : request . profile || null ,
moduleIds : Array . isArray ( request . modules ) ? [... request . modules ] : [],
includeComponentIds : Array . isArray ( request . includeComponents ) ? [... request . includeComponents ] : [],
excludeComponentIds : Array . isArray ( request . excludeComponents ) ? [... request . excludeComponents ] : [],
legacyLanguages : Array . isArray ( request . legacyLanguages ) ? [... request . legacyLanguages ] : [],
hookConsent : getRecordedHookConsent ( state ),
};
}
function resolveRecordedManifestPlan ( record , context , options = {}) {
return createInstallPlanFromRequest ( buildRecordedManifestRequest ( record ), {
sourceRoot : context . repoRoot ,
projectRoot : context . projectRoot ,
homeDir : context . homeDir ,
env : context . env ,
exemptValidationCodes : options . exemptValidationCodes || [],
});
}
2026-07-08 17:14:33 -04:00
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 ,
2026-08-24 21:20:17 -04:00
env : context . env ,
2026-07-08 17:14:33 -04:00
});
}
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' ;
}
2026-03-14 12:55:25 -07:00
function getManagedOperations ( state ) {
2026-06-18 20:03:24 -04:00
return Array . isArray ( state && state . operations ) ? state . operations . filter ( operation => operation . ownership === 'managed' ) : [];
2026-03-14 12:55:25 -07:00
}
2026-07-27 11:11:29 -07:00
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 ();
}
}
2026-03-14 12:55:25 -07:00
function resolveOperationSourcePath ( repoRoot , operation ) {
if ( operation . sourceRelativePath ) {
2026-07-27 11:11:29 -07:00
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
);
2026-03-14 12:55:25 -07:00
}
2026-07-27 11:11:29 -07:00
if ( ! operation . sourcePath ) {
return null ;
}
if (
typeof operation . sourcePath !== 'string'
|| ! path . isAbsolute ( operation . sourcePath )
) {
throw createUnsafeRepairSourceError ();
}
return assertSafeRepairSourcePath ( operation . sourcePath , repoRoot );
2026-03-14 12:55:25 -07:00
}
function areFilesEqual ( leftPath , rightPath ) {
try {
2026-07-27 11:11:29 -07:00
return readFileNoFollow ( leftPath ). equals ( readFileNoFollow ( rightPath ));
2026-03-14 12:55:25 -07:00
} catch ( _error ) {
return false ;
}
}
2026-08-13 16:42:51 -04:00
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 ,
});
}
2026-03-15 21:47:31 -07:00
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 ) {
2026-06-18 20:03:24 -04:00
const candidateKeys = [ 'renderedContent' , 'content' , 'managedContent' , 'expectedContent' , 'templateOutput' ];
2026-03-15 21:47:31 -07:00
for ( const key of candidateKeys ) {
if ( typeof operation [ key ] === 'string' ) {
return operation [ key ];
}
}
return null ;
}
function getOperationJsonPayload ( operation ) {
2026-06-18 20:03:24 -04:00
const candidateKeys = [ 'mergePayload' , 'managedPayload' , 'payload' , 'value' , 'expectedValue' ];
2026-03-15 21:47:31 -07:00
for ( const key of candidateKeys ) {
if ( operation [ key ] !== undefined ) {
return parseJsonLikeValue ( operation [ key ], ` ${ operation . kind } . ${ key } ` );
}
}
return undefined ;
}
function getOperationPreviousContent ( operation ) {
2026-06-18 20:03:24 -04:00
const candidateKeys = [ 'previousContent' , 'originalContent' , 'backupContent' ];
2026-03-15 21:47:31 -07:00
for ( const key of candidateKeys ) {
if ( typeof operation [ key ] === 'string' ) {
return operation [ key ];
}
}
return null ;
}
function getOperationPreviousJson ( operation ) {
2026-06-18 20:03:24 -04:00
const candidateKeys = [ 'previousValue' , 'previousJson' , 'originalValue' ];
2026-03-15 21:47:31 -07:00
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` ;
}
2026-07-27 11:11:29 -07:00
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
};
2026-03-15 21:47:31 -07:00
}
2026-07-27 11:11:29 -07:00
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 ;
}
2026-08-13 18:02:06 -04:00
const managedDestination = getManagedDestination (
2026-07-27 11:11:29 -07:00
existingDestination ,
trustedRoot ,
action ,
{ allowFinalSymlink : true }
2026-08-13 18:02:06 -04:00
);
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 );
2026-07-27 11:11:29 -07:00
return finalDestination ;
2026-03-15 21:47:31 -07:00
}
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 ;
}
2026-06-18 20:03:24 -04:00
return Object . entries ( expectedValue ). every (([ key , value ]) => Object . prototype . hasOwnProperty . call ( actualValue , key ) && jsonContainsSubset ( actualValue [ key ], value ));
2026-03-15 21:47:31 -07:00
}
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 ,
2026-06-18 20:03:24 -04:00
sourcePath : resolveOperationSourcePath ( repoRoot , operation )
2026-03-15 21:47:31 -07:00
};
});
}
function buildRecordedStatePreview ( state , context , operations ) {
return {
... state ,
operations : operations . map ( operation => ({ ... operation })),
source : {
... state . source ,
repoVersion : context . packageVersion ,
2026-06-18 20:03:24 -04:00
manifestVersion : context . manifestVersion
2026-03-15 21:47:31 -07:00
},
2026-06-18 20:03:24 -04:00
lastValidatedAt : new Date (). toISOString ()
2026-03-15 21:47:31 -07:00
};
}
function shouldRepairFromRecordedOperations ( state ) {
return getManagedOperations ( state ). some ( operation => operation . kind !== 'copy-file' );
}
2026-08-13 16:42:51 -04:00
function executeRepairOperation ( repoRoot , operation , trustedRoot , linkIndex = null ) {
2026-06-18 19:54:22 -04:00
// 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).
2026-03-15 21:47:31 -07:00
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 } ` );
}
2026-08-13 16:42:51 -04:00
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' );
}
2026-07-27 11:11:29 -07:00
return operation . destinationPath ;
2026-03-15 21:47:31 -07:00
}
if ( operation . kind === 'render-template' ) {
const renderedContent = getOperationTextContent ( operation );
if ( renderedContent === null ) {
throw new Error ( `Missing rendered content for repair: ${ operation . destinationPath } ` );
}
2026-07-27 11:11:29 -07:00
writeContainedFile ( operation . destinationPath , renderedContent , trustedRoot , 'repair' );
return operation . destinationPath ;
2026-03-15 21:47:31 -07:00
}
if ( operation . kind === 'merge-json' ) {
const payload = getOperationJsonPayload ( operation );
if ( payload === undefined ) {
throw new Error ( `Missing merge payload for repair: ${ operation . destinationPath } ` );
}
2026-07-27 11:11:29 -07:00
const existingDestination = getContainedExistingPath ( operation . destinationPath , trustedRoot , 'repair' );
const currentValue = existingDestination
? readJsonNoFollow (
getManagedDestination ( existingDestination , trustedRoot , 'repair' ). managedPath
)
: {};
2026-03-15 21:47:31 -07:00
const mergedValue = deepMergeJson ( currentValue , payload );
2026-07-27 11:11:29 -07:00
writeContainedFile ( operation . destinationPath , formatJson ( mergedValue ), trustedRoot , 'repair' );
return operation . destinationPath ;
2026-03-15 21:47:31 -07:00
}
if ( operation . kind === 'remove' ) {
2026-07-27 11:11:29 -07:00
const removedPath = removeContainedPath (
operation . destinationPath ,
trustedRoot ,
'repair' ,
{ recursive : true , force : true }
);
return removedPath ? operation . destinationPath : null ;
2026-03-15 21:47:31 -07:00
}
throw new Error ( `Unsupported repair operation kind: ${ operation . kind } ` );
}
2026-08-13 16:42:51 -04:00
function executeUninstallOperation ( operation , trustedRoot , options = {}) {
2026-06-18 19:54:22 -04:00
// Confine deletes to the trusted install root (GHSA-hfpv-w6mp-5g95).
2026-03-15 21:47:31 -07:00
if ( operation . kind === 'copy-file' ) {
2026-08-13 16:42:51 -04:00
if ( options . preserveDriftedCopies ) {
const existingDestination = getContainedExistingPath (
operation . destinationPath ,
trustedRoot ,
2026-08-13 18:02:06 -04:00
'uninstall' ,
{ allowFinalSymlink : true }
2026-08-13 16:42:51 -04:00
);
if ( ! existingDestination ) {
return {
removedPaths : [],
cleanupTargets : []
};
}
2026-08-13 18:02:06 -04:00
if ( fs . lstatSync ( existingDestination ). isSymbolicLink ()) {
return {
removedPaths : [],
cleanupTargets : [],
retainedPaths : [ operation . destinationPath ]
};
}
2026-08-13 16:42:51 -04:00
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 : [],
2026-08-13 18:02:06 -04:00
cleanupTargets : [],
retainedPaths : [ operation . destinationPath ]
2026-08-13 16:42:51 -04:00
};
}
}
2026-07-27 11:11:29 -07:00
const removedPath = removeContainedPath (
operation . destinationPath ,
trustedRoot ,
'uninstall' ,
{ force : true }
);
if ( ! removedPath ) {
2026-03-15 21:47:31 -07:00
return {
removedPaths : [],
2026-06-18 20:03:24 -04:00
cleanupTargets : []
2026-03-15 21:47:31 -07:00
};
}
return {
removedPaths : [ operation . destinationPath ],
2026-07-27 11:11:29 -07:00
cleanupTargets : [ removedPath ]
2026-03-15 21:47:31 -07:00
};
}
if ( operation . kind === 'render-template' ) {
const previousContent = getOperationPreviousContent ( operation );
if ( previousContent !== null ) {
2026-07-27 11:11:29 -07:00
writeContainedFile ( operation . destinationPath , previousContent , trustedRoot , 'uninstall' );
2026-03-15 21:47:31 -07:00
return {
removedPaths : [],
2026-06-18 20:03:24 -04:00
cleanupTargets : []
2026-03-15 21:47:31 -07:00
};
}
const previousJson = getOperationPreviousJson ( operation );
if ( previousJson !== undefined ) {
2026-07-27 11:11:29 -07:00
writeContainedFile ( operation . destinationPath , formatJson ( previousJson ), trustedRoot , 'uninstall' );
2026-03-15 21:47:31 -07:00
return {
removedPaths : [],
2026-06-18 20:03:24 -04:00
cleanupTargets : []
2026-03-15 21:47:31 -07:00
};
}
2026-07-27 11:11:29 -07:00
const removedPath = removeContainedPath (
operation . destinationPath ,
trustedRoot ,
'uninstall' ,
{ force : true }
);
if ( ! removedPath ) {
2026-03-15 21:47:31 -07:00
return {
removedPaths : [],
2026-06-18 20:03:24 -04:00
cleanupTargets : []
2026-03-15 21:47:31 -07:00
};
}
return {
removedPaths : [ operation . destinationPath ],
2026-07-27 11:11:29 -07:00
cleanupTargets : [ removedPath ]
2026-03-15 21:47:31 -07:00
};
}
if ( operation . kind === 'merge-json' ) {
const previousContent = getOperationPreviousContent ( operation );
if ( previousContent !== null ) {
2026-07-27 11:11:29 -07:00
writeContainedFile ( operation . destinationPath , previousContent , trustedRoot , 'uninstall' );
2026-03-15 21:47:31 -07:00
return {
removedPaths : [],
2026-06-18 20:03:24 -04:00
cleanupTargets : []
2026-03-15 21:47:31 -07:00
};
}
const previousJson = getOperationPreviousJson ( operation );
if ( previousJson !== undefined ) {
2026-07-27 11:11:29 -07:00
writeContainedFile ( operation . destinationPath , formatJson ( previousJson ), trustedRoot , 'uninstall' );
2026-03-15 21:47:31 -07:00
return {
removedPaths : [],
2026-06-18 20:03:24 -04:00
cleanupTargets : []
2026-03-15 21:47:31 -07:00
};
}
2026-07-27 11:11:29 -07:00
const existingDestination = getContainedExistingPath (
operation . destinationPath ,
trustedRoot ,
'uninstall'
);
if ( ! existingDestination ) {
2026-03-15 21:47:31 -07:00
return {
removedPaths : [],
2026-06-18 20:03:24 -04:00
cleanupTargets : []
2026-03-15 21:47:31 -07:00
};
}
const payload = getOperationJsonPayload ( operation );
if ( payload === undefined ) {
throw new Error ( `Missing merge payload for uninstall: ${ operation . destinationPath } ` );
}
2026-07-27 11:11:29 -07:00
const currentValue = readJsonNoFollow (
getManagedDestination ( existingDestination , trustedRoot , 'uninstall' ). managedPath
);
2026-03-15 21:47:31 -07:00
const nextValue = deepRemoveJsonSubset ( currentValue , payload );
if ( nextValue === JSON_REMOVE_SENTINEL ) {
2026-07-27 11:11:29 -07:00
const removedPath = removeContainedPath (
operation . destinationPath ,
trustedRoot ,
'uninstall' ,
{ force : true }
);
2026-03-15 21:47:31 -07:00
return {
2026-07-27 11:11:29 -07:00
removedPaths : removedPath ? [ operation . destinationPath ] : [],
cleanupTargets : removedPath ? [ removedPath ] : []
2026-03-15 21:47:31 -07:00
};
}
2026-07-27 11:11:29 -07:00
writeContainedFile ( operation . destinationPath , formatJson ( nextValue ), trustedRoot , 'uninstall' );
2026-03-15 21:47:31 -07:00
return {
removedPaths : [],
2026-06-18 20:03:24 -04:00
cleanupTargets : []
2026-03-15 21:47:31 -07:00
};
}
if ( operation . kind === 'remove' ) {
const previousContent = getOperationPreviousContent ( operation );
if ( previousContent !== null ) {
2026-07-27 11:11:29 -07:00
writeContainedFile ( operation . destinationPath , previousContent , trustedRoot , 'uninstall' );
2026-03-15 21:47:31 -07:00
return {
removedPaths : [],
2026-06-18 20:03:24 -04:00
cleanupTargets : []
2026-03-15 21:47:31 -07:00
};
}
const previousJson = getOperationPreviousJson ( operation );
if ( previousJson !== undefined ) {
2026-07-27 11:11:29 -07:00
writeContainedFile ( operation . destinationPath , formatJson ( previousJson ), trustedRoot , 'uninstall' );
2026-03-15 21:47:31 -07:00
return {
removedPaths : [],
2026-06-18 20:03:24 -04:00
cleanupTargets : []
2026-03-15 21:47:31 -07:00
};
}
return {
removedPaths : [],
2026-06-18 20:03:24 -04:00
cleanupTargets : []
2026-03-15 21:47:31 -07:00
};
}
throw new Error ( `Unsupported uninstall operation kind: ${ operation . kind } ` );
}
2026-08-13 16:42:51 -04:00
function inspectManagedOperation ( repoRoot , trustedRoot , operation , linkIndex = null ) {
2026-03-14 12:55:25 -07:00
const destinationPath = operation . destinationPath ;
if ( ! destinationPath ) {
return {
status : 'invalid-destination' ,
2026-06-18 20:03:24 -04:00
operation
2026-03-14 12:55:25 -07:00
};
}
2026-07-27 11:11:29 -07:00
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 ;
2026-03-15 21:47:31 -07:00
if ( operation . kind === 'remove' ) {
2026-07-27 11:11:29 -07:00
if ( managedDestination . exists ) {
2026-03-15 21:47:31 -07:00
return {
status : 'drifted' ,
operation ,
2026-06-18 20:03:24 -04:00
destinationPath
2026-03-15 21:47:31 -07:00
};
}
return {
status : 'ok' ,
operation ,
2026-06-18 20:03:24 -04:00
destinationPath
2026-03-15 21:47:31 -07:00
};
}
2026-07-27 11:11:29 -07:00
let copySourcePath = null ;
if ( operation . kind === 'copy-file' ) {
try {
copySourcePath = resolveOperationSourcePath ( repoRoot , operation );
} catch {
return {
status : 'unsafe-source' ,
operation ,
destinationPath
};
}
}
if ( ! managedDestination . exists ) {
2026-03-14 12:55:25 -07:00
return {
status : 'missing' ,
operation ,
2026-06-18 20:03:24 -04:00
destinationPath
2026-03-14 12:55:25 -07:00
};
}
2026-03-15 21:47:31 -07:00
if ( operation . kind === 'copy-file' ) {
2026-07-27 11:11:29 -07:00
if ( ! copySourcePath || ! fs . existsSync ( copySourcePath )) {
2026-03-15 21:47:31 -07:00
return {
status : 'missing-source' ,
operation ,
destinationPath ,
2026-07-27 11:11:29 -07:00
sourcePath : copySourcePath
2026-03-15 21:47:31 -07:00
};
}
2026-08-13 16:42:51 -04:00
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 ) {
2026-03-15 21:47:31 -07:00
return {
status : 'drifted' ,
operation ,
destinationPath ,
2026-07-27 11:11:29 -07:00
sourcePath : copySourcePath
2026-03-15 21:47:31 -07:00
};
}
2026-03-14 12:55:25 -07:00
return {
2026-03-15 21:47:31 -07:00
status : 'ok' ,
2026-03-14 12:55:25 -07:00
operation ,
destinationPath ,
2026-07-27 11:11:29 -07:00
sourcePath : copySourcePath
2026-03-14 12:55:25 -07:00
};
}
2026-03-15 21:47:31 -07:00
if ( operation . kind === 'render-template' ) {
const renderedContent = getOperationTextContent ( operation );
if ( renderedContent === null ) {
return {
status : 'unverified' ,
operation ,
2026-06-18 20:03:24 -04:00
destinationPath
2026-03-15 21:47:31 -07:00
};
}
2026-07-27 11:11:29 -07:00
try {
if ( readFileNoFollow ( inspectedPath , 'utf8' ) !== renderedContent ) {
return {
status : 'drifted' ,
operation ,
destinationPath
};
}
} catch {
2026-03-15 21:47:31 -07:00
return {
status : 'drifted' ,
operation ,
2026-06-18 20:03:24 -04:00
destinationPath
2026-03-15 21:47:31 -07:00
};
}
2026-03-14 12:55:25 -07:00
return {
2026-03-15 21:47:31 -07:00
status : 'ok' ,
operation ,
2026-06-18 20:03:24 -04:00
destinationPath
2026-03-15 21:47:31 -07:00
};
}
if ( operation . kind === 'merge-json' ) {
const payload = getOperationJsonPayload ( operation );
if ( payload === undefined ) {
return {
status : 'unverified' ,
operation ,
2026-06-18 20:03:24 -04:00
destinationPath
2026-03-15 21:47:31 -07:00
};
}
try {
2026-07-27 11:11:29 -07:00
const currentValue = readJsonNoFollow ( inspectedPath );
2026-03-15 21:47:31 -07:00
if ( ! jsonContainsSubset ( currentValue , payload )) {
return {
status : 'drifted' ,
operation ,
2026-06-18 20:03:24 -04:00
destinationPath
2026-03-15 21:47:31 -07:00
};
}
} catch ( _error ) {
return {
status : 'drifted' ,
operation ,
2026-06-18 20:03:24 -04:00
destinationPath
2026-03-15 21:47:31 -07:00
};
}
return {
status : 'ok' ,
2026-03-14 12:55:25 -07:00
operation ,
2026-06-18 20:03:24 -04:00
destinationPath
2026-03-14 12:55:25 -07:00
};
}
return {
2026-03-15 21:47:31 -07:00
status : 'unverified' ,
2026-03-14 12:55:25 -07:00
operation ,
2026-06-18 20:03:24 -04:00
destinationPath
2026-03-14 12:55:25 -07:00
};
}
2026-07-27 11:11:29 -07:00
function summarizeManagedOperationHealth ( repoRoot , trustedRoot , operations ) {
2026-08-13 16:42:51 -04:00
const linkIndex = buildLinkIndexForOperations ( operations , trustedRoot );
2026-06-18 20:03:24 -04:00
return operations . reduce (
( summary , operation ) => {
2026-08-13 16:42:51 -04:00
const inspection = inspectManagedOperation ( repoRoot , trustedRoot , operation , linkIndex );
2026-06-18 20:03:24 -04:00
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 );
2026-07-27 11:11:29 -07:00
} else if ( inspection . status === 'unsafe-source' ) {
summary . unsafeSource . push ( inspection );
} else if ( inspection . status === 'unsafe-destination' ) {
summary . unsafeDestination . push ( inspection );
2026-06-18 20:03:24 -04:00
} else if ( inspection . status === 'unverified' || inspection . status === 'invalid-destination' ) {
summary . unverified . push ( inspection );
}
return summary ;
},
{
missing : [],
drifted : [],
missingSource : [],
2026-07-27 11:11:29 -07:00
unsafeSource : [],
unsafeDestination : [],
2026-06-18 20:03:24 -04:00
unverified : []
2026-03-14 12:55:25 -07:00
}
2026-06-18 20:03:24 -04:00
);
2026-03-14 12:55:25 -07:00
}
2026-07-27 11:11:29 -07:00
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
};
}
2026-08-13 16:42:51 -04:00
function buildDiscoveryRecord ( adapter , context , location = null , knownState = null ) {
2026-03-14 12:55:25 -07:00
const installTargetInput = {
homeDir : context . homeDir ,
projectRoot : context . projectRoot ,
2026-08-24 21:20:17 -04:00
repoRoot : context . projectRoot ,
env : context . env ,
2026-03-14 12:55:25 -07:00
};
2026-08-13 16:42:51 -04:00
const targetRoot = location
? location . targetRoot
: adapter . resolveRoot ( installTargetInput );
const installStatePath = location
? location . installStatePath
: adapter . getInstallStatePath ( installTargetInput );
2026-03-14 12:55:25 -07:00
const exists = fs . existsSync ( installStatePath );
if ( ! exists ) {
return {
adapter : {
id : adapter . id ,
target : adapter . target ,
2026-06-18 20:03:24 -04:00
kind : adapter . kind
2026-03-14 12:55:25 -07:00
},
targetRoot ,
installStatePath ,
exists : false ,
state : null ,
2026-08-13 16:42:51 -04:00
error : null ,
2026-08-24 20:59:57 -04:00
legacy : Boolean ( location ),
legacyLayout : location ? . legacyLayout || null
2026-08-13 16:42:51 -04:00
};
}
if ( knownState ) {
return {
adapter : {
id : adapter . id ,
target : adapter . target ,
kind : adapter . kind
},
targetRoot ,
installStatePath ,
exists : true ,
state : knownState ,
error : null ,
2026-08-24 20:59:57 -04:00
legacy : Boolean ( location ),
legacyLayout : location ? . legacyLayout || null
2026-03-14 12:55:25 -07:00
};
}
try {
const state = readInstallState ( installStatePath );
return {
adapter : {
id : adapter . id ,
target : adapter . target ,
2026-06-18 20:03:24 -04:00
kind : adapter . kind
2026-03-14 12:55:25 -07:00
},
targetRoot ,
installStatePath ,
exists : true ,
state ,
2026-08-13 16:42:51 -04:00
error : null ,
2026-08-24 20:59:57 -04:00
legacy : Boolean ( location ),
legacyLayout : location ? . legacyLayout || null
2026-03-14 12:55:25 -07:00
};
} catch ( error ) {
return {
adapter : {
id : adapter . id ,
target : adapter . target ,
2026-06-18 20:03:24 -04:00
kind : adapter . kind
2026-03-14 12:55:25 -07:00
},
targetRoot ,
installStatePath ,
exists : true ,
state : null ,
2026-08-13 16:42:51 -04:00
error : error . message ,
2026-08-24 20:59:57 -04:00
legacy : Boolean ( location ),
legacyLayout : location ? . legacyLayout || null
2026-03-14 12:55:25 -07:00
};
}
}
function discoverInstalledStates ( options = {}) {
const context = {
2026-03-28 11:28:12 +08:00
homeDir : options . homeDir || process . env . HOME || os . homedir (),
2026-08-24 21:20:17 -04:00
projectRoot : options . projectRoot || process . cwd (),
2026-08-25 12:17:21 -04:00
env : resolveInvocationEnvironment ( options ),
2026-03-14 12:55:25 -07:00
};
const targets = normalizeTargets ( options . targets );
2026-08-13 16:42:51 -04:00
return targets . flatMap ( target => {
2026-03-14 12:55:25 -07:00
const adapter = getInstallTargetAdapter ( target );
2026-08-13 16:42:51 -04:00
const canonicalRecord = buildDiscoveryRecord ( adapter , context );
2026-08-24 20:59:57 -04:00
if ( adapter . target === 'opencode' ) {
const legacyLocation = getLegacyOpencodeLocation ( context . homeDir );
const legacyInspection = inspectLegacyOpencodeState ( 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 ,
legacyLayout : 'opencode' ,
}];
}
return [
canonicalRecord ,
buildDiscoveryRecord ( adapter , context , legacyLocation , legacyInspection . state ),
];
}
2026-08-13 16:42:51 -04:00
if ( adapter . target !== 'antigravity' ) {
return [ canonicalRecord ];
}
2026-08-24 20:59:57 -04:00
const legacyLocation = {
... getLegacyAntigravityLocation ( context . projectRoot ),
legacyLayout : 'antigravity' ,
};
2026-08-13 16:42:51 -04:00
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 ,
2026-08-24 20:59:57 -04:00
error : legacyInspection . error ,
legacy : true ,
legacyLayout : 'antigravity' ,
2026-08-13 16:42:51 -04:00
}];
}
return [
canonicalRecord ,
buildDiscoveryRecord ( adapter , context , legacyLocation , legacyInspection . state ),
];
2026-03-14 12:55:25 -07:00
});
}
function buildIssue ( severity , code , message , extra = {}) {
return {
severity ,
code ,
message ,
2026-06-18 20:03:24 -04:00
... extra
2026-03-14 12:55:25 -07:00
};
}
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 = [];
2026-08-24 20:59:57 -04:00
if ( record . legacyLayout === 'antigravity' ) {
2026-08-13 16:42:51 -04:00
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.'
));
}
2026-08-24 20:59:57 -04:00
if ( record . legacyLayout === 'opencode' ) {
issues . push ( buildIssue (
'warning' ,
'legacy-opencode-layout' ,
'Legacy OpenCode install-state remains under ~/.opencode. Rerun the OpenCode install or repair command to migrate unchanged ECC-managed files to ~/.config/opencode; modified files are preserved for review.'
));
}
2026-03-14 12:55:25 -07:00
if ( record . error ) {
issues . push ( buildIssue ( 'error' , 'invalid-install-state' , record . error ));
return {
... record ,
status : determineStatus ( issues ),
2026-06-18 20:03:24 -04:00
issues
2026-03-14 12:55:25 -07:00
};
}
const state = record . state ;
if ( ! state ) {
return {
... record ,
status : 'missing' ,
2026-06-18 20:03:24 -04:00
issues
2026-03-14 12:55:25 -07:00
};
}
if ( ! fs . existsSync ( state . target . root )) {
2026-06-18 20:03:24 -04:00
issues . push ( buildIssue ( 'error' , 'missing-target-root' , `Target root does not exist: ${ state . target . root } ` ));
2026-03-14 12:55:25 -07:00
}
if ( state . target . root !== record . targetRoot ) {
2026-06-18 20:03:24 -04:00
issues . push (
buildIssue ( 'warning' , 'target-root-mismatch' , `Recorded target root differs from current target root ( ${ record . targetRoot } )` , {
2026-03-14 12:55:25 -07:00
recordedTargetRoot : state . target . root ,
2026-06-18 20:03:24 -04:00
currentTargetRoot : record . targetRoot
})
);
2026-03-14 12:55:25 -07:00
}
if ( state . target . installStatePath !== record . installStatePath ) {
2026-06-18 20:03:24 -04:00
issues . push (
buildIssue ( 'warning' , 'install-state-path-mismatch' , `Recorded install-state path differs from current path ( ${ record . installStatePath } )` , {
2026-03-14 12:55:25 -07:00
recordedInstallStatePath : state . target . installStatePath ,
2026-06-18 20:03:24 -04:00
currentInstallStatePath : record . installStatePath
})
);
2026-03-14 12:55:25 -07:00
}
const managedOperations = getManagedOperations ( state );
2026-07-27 11:11:29 -07:00
const operationHealth = summarizeManagedOperationHealth (
context . repoRoot ,
record . targetRoot ,
managedOperations
);
2026-03-14 12:55:25 -07:00
const missingManagedOperations = operationHealth . missing ;
2026-07-27 11:11:29 -07:00
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`
)
);
}
2026-03-14 12:55:25 -07:00
if ( missingManagedOperations . length > 0 ) {
2026-06-18 20:03:24 -04:00
issues . push (
buildIssue ( 'error' , 'missing-managed-files' , ` ${ missingManagedOperations . length } managed file(s) are missing` , {
paths : missingManagedOperations . map ( entry => entry . destinationPath )
})
);
2026-03-14 12:55:25 -07:00
}
if ( operationHealth . drifted . length > 0 ) {
2026-06-18 20:03:24 -04:00
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 )
})
);
2026-03-14 12:55:25 -07:00
}
if ( operationHealth . missingSource . length > 0 ) {
2026-06-18 20:03:24 -04:00
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 )
})
);
2026-03-14 12:55:25 -07:00
}
if ( operationHealth . unverified . length > 0 ) {
2026-06-18 20:03:24 -04:00
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 )
})
);
2026-03-14 12:55:25 -07:00
}
if ( state . source . manifestVersion !== context . manifestVersion ) {
2026-06-18 20:03:24 -04:00
issues . push ( buildIssue ( 'warning' , 'manifest-version-mismatch' , `Recorded manifest version ${ state . source . manifestVersion } differs from current manifest version ${ context . manifestVersion } ` ));
2026-03-14 12:55:25 -07:00
}
2026-06-18 20:03:24 -04:00
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 } ` ));
2026-03-14 12:55:25 -07:00
}
if ( ! state . request . legacyMode ) {
try {
2026-08-07 15:18:14 -04:00
const desiredPlan = resolveRecordedManifestPlan ( record , context );
2026-03-14 12:55:25 -07:00
2026-06-18 20:03:24 -04:00
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' , {
2026-03-14 12:55:25 -07:00
expectedSelectedModules : desiredPlan . selectedModuleIds ,
recordedSelectedModules : state . resolution . selectedModules ,
expectedSkippedModules : desiredPlan . skippedModuleIds ,
2026-06-18 20:03:24 -04:00
recordedSkippedModules : state . resolution . skippedModules
})
);
2026-03-14 12:55:25 -07:00
}
} catch ( error ) {
2026-06-18 20:03:24 -04:00
issues . push ( buildIssue ( 'error' , 'resolution-unavailable' , error . message ));
2026-03-14 12:55:25 -07:00
}
}
return {
... record ,
status : determineStatus ( issues ),
2026-06-18 20:03:24 -04:00
issues
2026-03-14 12:55:25 -07:00
};
}
function buildDoctorReport ( options = {}) {
const repoRoot = options . repoRoot || DEFAULT_REPO_ROOT ;
const manifests = loadInstallManifests ({ repoRoot });
const records = discoverInstalledStates ({
homeDir : options . homeDir ,
projectRoot : options . projectRoot ,
2026-08-24 21:20:17 -04:00
targets : options . targets ,
2026-08-25 12:17:21 -04:00
env : resolveInvocationEnvironment ( options ),
2026-03-14 12:55:25 -07:00
}). filter ( record => record . exists );
const context = {
repoRoot ,
2026-03-28 11:28:12 +08:00
homeDir : options . homeDir || process . env . HOME || os . homedir (),
2026-03-14 12:55:25 -07:00
projectRoot : options . projectRoot || process . cwd (),
2026-08-25 12:17:21 -04:00
env : resolveInvocationEnvironment ( options ),
2026-03-14 12:55:25 -07:00
manifestVersion : manifests . modulesVersion ,
2026-06-18 20:03:24 -04:00
packageVersion : readPackageVersion ( repoRoot )
2026-03-14 12:55:25 -07:00
};
const results = records . map ( record => analyzeRecord ( record , context ));
2026-06-18 20:03:24 -04:00
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 ;
2026-03-14 12:55:25 -07:00
2026-06-18 20:03:24 -04:00
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
}
);
2026-03-14 12:55:25 -07:00
return {
generatedAt : new Date (). toISOString (),
packageVersion : context . packageVersion ,
manifestVersion : context . manifestVersion ,
results ,
2026-06-18 20:03:24 -04:00
summary
2026-03-14 12:55:25 -07:00
};
}
2026-07-08 17:14:33 -04:00
function createRepairPlanFromRecord ( record , context , options = {}) {
2026-03-14 12:55:25 -07:00
const state = record . state ;
if ( ! state ) {
throw new Error ( 'No install-state available for repair' );
}
2026-08-25 12:29:35 -04:00
if (
record . legacyLayout !== 'opencode'
&& ( state . request . legacyMode || shouldRepairFromRecordedOperations ( state ))
) {
2026-03-15 21:47:31 -07:00
const operations = hydrateRecordedOperations ( context . repoRoot , getManagedOperations ( state ));
const statePreview = buildRecordedStatePreview ( state , context , operations );
2026-03-14 12:55:25 -07:00
return {
2026-03-15 21:47:31 -07:00
mode : state . request . legacyMode ? 'legacy' : 'recorded' ,
2026-03-14 12:55:25 -07:00
target : record . adapter . target ,
adapter : record . adapter ,
targetRoot : state . target . root ,
installRoot : state . target . root ,
installStatePath : state . target . installStatePath ,
warnings : [],
2026-06-18 20:03:24 -04:00
languages : Array . isArray ( state . request . legacyLanguages ) ? [... state . request . legacyLanguages ] : [],
2026-03-14 12:55:25 -07:00
operations ,
2026-06-18 20:03:24 -04:00
statePreview
2026-03-14 12:55:25 -07:00
};
}
2026-08-07 15:18:14 -04:00
const desiredPlan = resolveRecordedManifestPlan ( record , context , options );
2026-03-14 12:55:25 -07:00
return {
... desiredPlan ,
statePreview : {
... desiredPlan . statePreview ,
installedAt : state . installedAt ,
2026-06-18 20:03:24 -04:00
lastValidatedAt : new Date (). toISOString ()
}
2026-03-14 12:55:25 -07:00
};
}
2026-07-27 11:11:29 -07:00
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 );
2026-08-13 16:42:51 -04:00
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 );
2026-07-27 11:11:29 -07:00
return writeContainedFile (
record . installStatePath ,
2026-08-13 16:42:51 -04:00
formatJson ( stateWithCurrentDigests ),
2026-07-27 11:11:29 -07:00
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 );
2026-07-26 03:20:06 -07:00
return {
migration ,
plan : {
2026-07-27 11:11:29 -07:00
... trustedPlan ,
2026-07-26 03:20:06 -07:00
operations : migration . finalState . operations ,
statePreview : migration . finalState ,
warnings : [
...( Array . isArray ( plan . warnings ) ? plan . warnings : []),
... migration . warnings ,
],
},
};
}
2026-03-14 12:55:25 -07:00
function repairInstalledStates ( options = {}) {
const repoRoot = options . repoRoot || DEFAULT_REPO_ROOT ;
const manifests = loadInstallManifests ({ repoRoot });
const context = {
repoRoot ,
2026-03-28 11:28:12 +08:00
homeDir : options . homeDir || process . env . HOME || os . homedir (),
2026-03-14 12:55:25 -07:00
projectRoot : options . projectRoot || process . cwd (),
2026-08-25 12:17:21 -04:00
env : resolveInvocationEnvironment ( options ),
2026-03-14 12:55:25 -07:00
manifestVersion : manifests . modulesVersion ,
2026-06-18 20:03:24 -04:00
packageVersion : readPackageVersion ( repoRoot )
2026-03-14 12:55:25 -07:00
};
2026-07-08 17:14:33 -04:00
const buildOpencodeRunner = typeof options . buildOpencodePayload === 'function'
? options . buildOpencodePayload
: buildOpencodePayload ;
2026-03-14 12:55:25 -07:00
const records = discoverInstalledStates ({
homeDir : context . homeDir ,
projectRoot : context . projectRoot ,
2026-08-24 21:20:17 -04:00
targets : options . targets ,
env : context . env ,
2026-08-24 20:59:57 -04:00
}). filter ( record => (
record . exists
&& ( ! record . legacy || record . legacyLayout === 'opencode' )
));
2026-03-14 12:55:25 -07:00
const results = records . map ( record => {
if ( record . error ) {
return {
adapter : record . adapter ,
status : 'error' ,
installStatePath : record . installStatePath ,
repairedPaths : [],
plannedRepairs : [],
2026-06-18 20:03:24 -04:00
error : record . error
2026-03-14 12:55:25 -07:00
};
}
try {
2026-07-08 17:14:33 -04:00
const needsOpencodeBuild = record . adapter . target === 'opencode'
&& hasOpencodeBuildError ( getOpencodeBuildValidationIssues ( context ));
const opencodeBuildRepairPath = path . join ( context . repoRoot , OPENCODE_BUILD_ARTIFACT );
2026-08-24 20:59:57 -04:00
if ( record . legacyLayout === 'opencode' ) {
if ( needsOpencodeBuild && ! options . dryRun ) {
try {
buildOpencodeRunner ( context . repoRoot );
} catch ( error ) {
return {
adapter : record . adapter ,
status : 'error' ,
installStatePath : record . installStatePath ,
repairedPaths : [],
plannedRepairs : [],
error : formatBuildErrorMessage ( error ),
};
}
}
const canonicalPlan = createRepairPlanFromRecord ( record , context , {
exemptValidationCodes : options . dryRun && needsOpencodeBuild
? [ OPENCODE_PLUGIN_NOT_BUILT_CODE ]
: [],
});
const plannedRepairs = [... new Set ([
...( needsOpencodeBuild ? [ opencodeBuildRepairPath ] : []),
... canonicalPlan . operations . map ( operation => operation . destinationPath ),
... getManagedOperations ( record . state ). map ( operation => operation . destinationPath ),
record . installStatePath ,
])];
if ( options . dryRun ) {
return {
adapter : record . adapter ,
status : 'planned' ,
installStatePath : canonicalPlan . installStatePath ,
repairedPaths : [],
plannedRepairs ,
stateRefreshed : false ,
warnings : canonicalPlan . warnings ,
error : null ,
};
}
// Load lazily to avoid a module cycle during install-lifecycle startup.
const { applyInstallPlan } = require ( './install/apply' );
const appliedPlan = applyInstallPlan ( canonicalPlan );
return {
adapter : record . adapter ,
status : 'repaired' ,
installStatePath : canonicalPlan . installStatePath ,
repairedPaths : [
...( needsOpencodeBuild ? [ opencodeBuildRepairPath ] : []),
... canonicalPlan . operations . map ( operation => operation . destinationPath ),
],
plannedRepairs : [],
stateRefreshed : true ,
warnings : appliedPlan . warnings ,
error : null ,
};
}
2026-07-08 17:14:33 -04:00
if ( needsOpencodeBuild && options . dryRun ) {
2026-07-26 03:20:06 -07:00
const rawPlan = createRepairPlanFromRecord ( record , context , {
2026-07-08 17:14:33 -04:00
exemptValidationCodes : [ OPENCODE_PLUGIN_NOT_BUILT_CODE ],
});
2026-07-27 11:11:29 -07:00
const { plan : desiredPlan } = prepareRepairMigration ( rawPlan , record );
const operationHealth = summarizeManagedOperationHealth (
context . repoRoot ,
record . targetRoot ,
desiredPlan . operations
);
const unsafeOperationResult = getUnsafeOperationResult (
record ,
operationHealth
);
if ( unsafeOperationResult ) {
return unsafeOperationResult ;
}
2026-07-08 17:14:33 -04:00
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 ,
2026-07-26 03:20:06 -07:00
warnings : desiredPlan . warnings ,
2026-07-08 17:14:33 -04:00
error : null
};
}
if ( needsOpencodeBuild ) {
try {
buildOpencodeRunner ( context . repoRoot );
} catch ( error ) {
return {
adapter : record . adapter ,
status : 'error' ,
installStatePath : record . installStatePath ,
repairedPaths : [],
plannedRepairs : [],
error : formatBuildErrorMessage ( error )
};
}
}
2026-07-26 03:20:06 -07:00
const rawPlan = createRepairPlanFromRecord ( record , context );
const {
migration ,
plan : desiredPlan ,
2026-07-27 11:11:29 -07:00
} = prepareRepairMigration ( rawPlan , record );
const operationHealth = summarizeManagedOperationHealth (
context . repoRoot ,
record . targetRoot ,
desiredPlan . operations
);
const unsafeOperationResult = getUnsafeOperationResult (
record ,
operationHealth
);
if ( unsafeOperationResult ) {
return unsafeOperationResult ;
}
2026-03-14 12:55:25 -07:00
if ( operationHealth . missingSource . length > 0 ) {
return {
adapter : record . adapter ,
status : 'error' ,
installStatePath : record . installStatePath ,
repairedPaths : [],
plannedRepairs : [],
2026-07-26 03:20:06 -07:00
warnings : desiredPlan . warnings ,
2026-06-18 20:03:24 -04:00
error : `Missing source file(s): ${ operationHealth . missingSource . map ( entry => entry . sourcePath ). join ( ', ' ) } `
2026-03-14 12:55:25 -07:00
};
}
2026-06-18 20:03:24 -04:00
const repairOperations = [... operationHealth . missing . map ( entry => ({ ... entry . operation })), ... operationHealth . drifted . map ( entry => ({ ... entry . operation }))];
2026-08-13 16:42:51 -04:00
const repairLinkIndex = buildLinkIndexForOperations ( desiredPlan . operations , record . targetRoot );
2026-07-26 03:20:06 -07:00
const legacyMigrationPaths = migration . legacyOperationsToRemove . map (
operation => operation . destinationPath
);
const plannedRepairs = [... new Set ([
...( needsOpencodeBuild ? [ opencodeBuildRepairPath ] : []),
... repairOperations . map ( operation => operation . destinationPath ),
... legacyMigrationPaths ,
])];
2026-03-14 12:55:25 -07:00
if ( options . dryRun ) {
return {
adapter : record . adapter ,
status : plannedRepairs . length > 0 ? 'planned' : 'ok' ,
installStatePath : record . installStatePath ,
repairedPaths : [],
plannedRepairs ,
stateRefreshed : plannedRepairs . length === 0 ,
2026-07-26 03:20:06 -07:00
warnings : desiredPlan . warnings ,
2026-06-18 20:03:24 -04:00
error : null
2026-03-14 12:55:25 -07:00
};
}
2026-07-26 03:20:06 -07:00
const hasLegacyMigration = migration . legacyOperationsToRemove . length > 0 ;
2026-07-27 11:11:29 -07:00
const repairedPaths = needsOpencodeBuild ? [ opencodeBuildRepairPath ] : [];
2026-07-26 03:20:06 -07:00
if ( migration . requiresBridgeState && ( repairOperations . length > 0 || hasLegacyMigration )) {
2026-07-27 11:11:29 -07:00
writeRefreshedInstallState ( record , migration . bridgeState );
2026-07-26 03:20:06 -07:00
}
2026-07-27 11:11:29 -07:00
for ( const operation of repairOperations ) {
const repairedPath = executeRepairOperation (
context . repoRoot ,
operation ,
2026-08-13 16:42:51 -04:00
record . targetRoot ,
repairLinkIndex
2026-07-27 11:11:29 -07:00
);
if ( repairedPath ) {
repairedPaths . push ( repairedPath );
2026-03-15 21:47:31 -07:00
}
2026-03-14 12:55:25 -07:00
}
2026-07-26 03:20:06 -07:00
if ( hasLegacyMigration ) {
2026-07-27 11:11:29 -07:00
for ( const operation of migration . legacyOperationsToRemove ) {
const removedPath = removeContainedPath (
operation . destinationPath ,
record . targetRoot ,
'migrate managed Claude skill' ,
{ force : true }
);
if ( removedPath ) {
repairedPaths . push ( removedPath );
}
}
2026-07-26 03:20:06 -07:00
}
2026-08-13 16:42:51 -04:00
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 );
2026-03-14 12:55:25 -07:00
return {
adapter : record . adapter ,
2026-07-26 03:20:06 -07:00
status : ( repairOperations . length > 0 || needsOpencodeBuild || hasLegacyMigration )
? 'repaired'
: 'ok' ,
2026-03-14 12:55:25 -07:00
installStatePath : record . installStatePath ,
2026-07-27 11:11:29 -07:00
repairedPaths ,
2026-03-14 12:55:25 -07:00
plannedRepairs : [],
stateRefreshed : true ,
2026-07-26 03:20:06 -07:00
warnings : desiredPlan . warnings ,
2026-06-18 20:03:24 -04:00
error : null
2026-03-14 12:55:25 -07:00
};
} catch ( error ) {
return {
adapter : record . adapter ,
status : 'error' ,
installStatePath : record . installStatePath ,
repairedPaths : [],
plannedRepairs : [],
2026-06-18 20:03:24 -04:00
error : error . message
2026-03-14 12:55:25 -07:00
};
}
});
2026-06-18 20:03:24 -04:00
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
}
);
2026-03-14 12:55:25 -07:00
return {
dryRun : Boolean ( options . dryRun ),
generatedAt : new Date (). toISOString (),
results ,
2026-06-18 20:03:24 -04:00
summary
2026-03-14 12:55:25 -07:00
};
}
function cleanupEmptyParentDirs ( filePath , stopAt ) {
2026-07-27 11:11:29 -07:00
const trustedStopAt = assertWithinTrustedRoot ( stopAt , stopAt , 'clean up' );
const trustedFilePath = assertWithinTrustedRoot ( filePath , trustedStopAt , 'clean up' );
let currentPath = path . dirname ( trustedFilePath );
2026-03-14 12:55:25 -07:00
2026-07-27 11:11:29 -07:00
while ( currentPath ) {
const relativePath = path . relative ( trustedStopAt , currentPath );
const isContained = relativePath !== '..'
&& ! relativePath . startsWith ( `.. ${ path . sep } ` )
&& ! path . isAbsolute ( relativePath );
if ( ! isContained || relativePath === '' ) {
2026-03-14 12:55:25 -07:00
break ;
}
2026-07-27 11:11:29 -07:00
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' );
2026-08-13 18:02:06 -04:00
const removedPath = removeContainedPath ( finalPath , trustedStopAt , 'clean up' );
if ( ! removedPath ) break ;
currentPath = path . dirname ( removedPath );
2026-03-14 12:55:25 -07:00
}
}
function uninstallInstalledStates ( options = {}) {
const records = discoverInstalledStates ({
homeDir : options . homeDir ,
projectRoot : options . projectRoot ,
2026-08-24 21:20:17 -04:00
targets : options . targets ,
2026-08-25 12:17:21 -04:00
env : resolveInvocationEnvironment ( options ),
2026-03-14 12:55:25 -07:00
}). 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 : [],
2026-06-18 20:03:24 -04:00
error : record . error || 'No valid install-state available'
2026-03-14 12:55:25 -07:00
};
}
const state = record . state ;
2026-08-13 16:42:51 -04:00
const managedOperations = getManagedOperations ( state );
2026-08-24 20:59:57 -04:00
if ( record . legacyLayout === 'antigravity' && managedOperations . length > 0 ) {
2026-08-13 16:42:51 -04:00
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
};
}
2026-07-27 11:11:29 -07:00
const plannedRemovals = Array . from ( new Set ([
2026-08-13 16:42:51 -04:00
... managedOperations . map ( operation => operation . destinationPath ),
2026-07-27 11:11:29 -07:00
record . installStatePath
]));
2026-03-14 12:55:25 -07:00
if ( options . dryRun ) {
return {
adapter : record . adapter ,
status : 'planned' ,
installStatePath : record . installStatePath ,
removedPaths : [],
plannedRemovals ,
2026-06-18 20:03:24 -04:00
error : null
2026-03-14 12:55:25 -07:00
};
}
try {
const removedPaths = [];
const cleanupTargets = [];
2026-08-13 18:02:06 -04:00
const retainedPaths = [];
2026-03-15 21:47:31 -07:00
const operations = getManagedOperations ( state );
2026-03-14 12:55:25 -07:00
2026-03-15 21:47:31 -07:00
for ( const operation of operations ) {
2026-08-13 16:42:51 -04:00
const outcome = executeUninstallOperation ( operation , record . targetRoot , {
2026-08-13 18:02:06 -04:00
preserveDriftedCopies : true ,
2026-08-13 16:42:51 -04:00
});
2026-03-15 21:47:31 -07:00
removedPaths . push (... outcome . removedPaths );
cleanupTargets . push (... outcome . cleanupTargets );
2026-08-13 18:02:06 -04:00
retainedPaths . push (...( outcome . retainedPaths || []));
2026-03-14 12:55:25 -07:00
}
2026-08-13 18:02:06 -04:00
if ( retainedPaths . length === 0 ) {
const removedStatePath = removeContainedPath (
record . installStatePath ,
record . targetRoot ,
'uninstall' ,
{ force : true }
);
if ( removedStatePath ) {
removedPaths . push ( record . installStatePath );
cleanupTargets . push ( removedStatePath );
}
2026-03-14 12:55:25 -07:00
}
for ( const cleanupTarget of cleanupTargets ) {
2026-07-27 11:11:29 -07:00
cleanupEmptyParentDirs ( cleanupTarget , record . targetRoot );
2026-03-14 12:55:25 -07:00
}
return {
adapter : record . adapter ,
2026-08-13 18:02:06 -04:00
status : retainedPaths . length > 0 ? 'partial' : 'uninstalled' ,
2026-03-14 12:55:25 -07:00
installStatePath : record . installStatePath ,
removedPaths ,
2026-08-13 18:02:06 -04:00
retainedPaths : [... new Set ( retainedPaths )]. sort (),
2026-03-14 12:55:25 -07:00
plannedRemovals : [],
2026-08-13 18:02:06 -04:00
warning : retainedPaths . length > 0
? 'Modified or unverifiable managed files were preserved together with install-state for review.'
: null ,
2026-06-18 20:03:24 -04:00
error : null
2026-03-14 12:55:25 -07:00
};
} catch ( error ) {
return {
adapter : record . adapter ,
status : 'error' ,
installStatePath : record . installStatePath ,
removedPaths : [],
plannedRemovals ,
2026-06-18 20:03:24 -04:00
error : error . message
2026-03-14 12:55:25 -07:00
};
}
});
2026-06-18 20:03:24 -04:00
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 ),
2026-08-13 16:42:51 -04:00
partialCount : accumulator . partialCount + ( result . status === 'partial' ? 1 : 0 ),
2026-06-18 20:03:24 -04:00
errorCount : accumulator . errorCount + ( result . status === 'error' ? 1 : 0 )
}),
{
checkedCount : 0 ,
uninstalledCount : 0 ,
plannedRemovalCount : 0 ,
2026-08-13 16:42:51 -04:00
partialCount : 0 ,
2026-06-18 20:03:24 -04:00
errorCount : 0
}
);
2026-03-14 12:55:25 -07:00
return {
dryRun : Boolean ( options . dryRun ),
generatedAt : new Date (). toISOString (),
results ,
2026-06-18 20:03:24 -04:00
summary
2026-03-14 12:55:25 -07:00
};
}
module . exports = {
DEFAULT_REPO_ROOT ,
buildDoctorReport ,
discoverInstalledStates ,
normalizeTargets ,
repairInstalledStates ,
2026-06-18 20:03:24 -04:00
uninstallInstalledStates
2026-03-14 12:55:25 -07:00
};