2026-03-14 12:55:25 -07:00
'use strict' ;
2026-08-06 15:39:49 -04:00
const crypto = require ( 'crypto' );
2026-03-14 12:55:25 -07:00
const fs = require ( 'fs' );
2026-03-27 07:52:03 -04:00
const path = require ( 'path' );
2026-03-14 12:55:25 -07:00
2026-08-12 00:43:58 -04:00
const {
hasExplicitCommitAttributionPreference ,
withCommitAttributionDisabled ,
} = require ( '../claude-commit-attribution' );
2026-09-07 00:53:13 +08:00
const { readInstallState , writeInstallState } = require ( '../install-state' );
2026-08-07 15:18:14 -04:00
const { assertHookConsentReady , planMaterializesHookRuntime } = require ( './hook-consent' );
2026-09-07 00:53:13 +08:00
const {
2026-09-07 01:57:04 +08:00
getClaudeSettingsPath ,
2026-09-07 00:53:13 +08:00
mergeManagedHooks ,
readSettings ,
2026-09-07 01:57:04 +08:00
runWithSettingsLock ,
2026-09-07 00:53:13 +08:00
uninstallManagedHooks ,
updateSettingsAtomic ,
validateManagedHooks ,
2026-09-07 01:57:04 +08:00
validateRecordedManagedHooks ,
2026-09-07 00:53:13 +08:00
} = require ( './claude-settings' );
2026-04-05 14:37:28 -07:00
const { filterMcpConfig , parseDisabledMcpServers } = require ( '../mcp-config' );
2026-08-06 15:39:49 -04:00
const { assertWithinTrustedRoot } = require ( '../path-safety' );
2026-07-26 03:20:06 -07:00
const {
assertSafeClaudeSkillOperation ,
prepareClaudeSkillMigration ,
removeLegacyClaudeSkillFiles ,
} = require ( './claude-skill-migration' );
2026-08-13 16:42:51 -04:00
const { cleanupLegacyAntigravityInstall } = require ( './antigravity-legacy-migration' );
2026-09-07 16:32:58 -04:00
const {
assertNoNewUserOwnedFile ,
prepareUserOwnedFileGuard ,
preserveUnwrittenFiles ,
} = require ( './ownership-guard' );
2026-08-24 20:59:57 -04:00
const { cleanupLegacyOpencodeInstall } = require ( './opencode-legacy-migration' );
2026-07-26 03:20:06 -07:00
const { buildInstallIndex , rewriteRelativeLinks } = require ( './link-rewrite' );
2026-08-13 16:42:51 -04:00
const { adaptAntigravityAgent } = require ( './antigravity-agent' );
2026-07-04 09:09:24 +05:30
function isMarkdownPath ( filePath ) {
return /\.(md|mdx|markdown)$/i . test ( String ( filePath || '' ));
}
2026-08-13 16:42:51 -04:00
function transformInstallContent ( 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 } ` );
}
2026-07-04 09:09:24 +05:30
// Map every copy-file operation to { sourceRel, destRel } so relative links in
// namespaced markdown can be rewritten to the file's actual installed location
// (issue #2340). Returns null when the plan lacks the data needed to do so.
function buildLinkIndexForPlan ( plan ) {
if ( ! plan || ! plan . targetRoot || ! Array . isArray ( plan . operations )) {
return null ;
}
const mappings = [];
for ( const operation of plan . operations ) {
if ( operation . kind === 'copy-file' && operation . sourceRelativePath ) {
mappings . push ({
sourceRel : operation . sourceRelativePath ,
destRel : path . relative ( plan . targetRoot , operation . destinationPath ),
});
}
}
return buildInstallIndex ( mappings );
}
2026-03-14 12:55:25 -07:00
2026-03-28 19:55:28 -04:00
function readJsonObject ( filePath , label ) {
let parsed ;
try {
parsed = JSON . parse ( fs . readFileSync ( filePath , 'utf8' ));
} catch ( error ) {
2026-08-13 18:21:21 -04:00
const wrappedError = new Error ( `Failed to parse ${ label } at ${ filePath } : ${ error . message } ` );
wrappedError . code = error . code ;
throw wrappedError ;
2026-03-28 19:55:28 -04:00
}
if ( ! parsed || typeof parsed !== 'object' || Array . isArray ( parsed )) {
throw new Error ( `Invalid ${ label } at ${ filePath } : expected a JSON object` );
}
return parsed ;
}
2026-08-13 18:21:21 -04:00
function readOptionalJsonObject ( filePath , label ) {
try {
return readJsonObject ( filePath , label );
} catch ( error ) {
if ( error . code === 'ENOENT' ) {
return {};
}
throw error ;
}
}
function readInstalledFileNoFollow ( plan , operation ) {
assertSafeInstallOperation ( plan , operation );
assertSafeClaudeSkillOperation ( plan , operation );
const flags = fs . constants . O_RDONLY | ( fs . constants . O_NOFOLLOW || 0 );
let descriptor ;
try {
descriptor = fs . openSync ( operation . destinationPath , flags );
} catch ( error ) {
if ( error . code === 'ENOENT' ) {
return null ;
}
throw error ;
}
try {
2026-08-13 18:29:21 -04:00
const openedStat = fs . fstatSync ( descriptor , { bigint : true });
const finalPathStat = fs . lstatSync ( operation . destinationPath , { bigint : true });
if ( finalPathStat . isSymbolicLink () || ! finalPathStat . isFile ()) {
return null ;
}
const identityMatches = openedStat . ino === finalPathStat . ino
&& ( ! openedStat . dev || ! finalPathStat . dev || openedStat . dev === finalPathStat . dev );
if ( ! openedStat . isFile () || ! identityMatches ) {
throw new Error (
`Refusing to hash changed install destination: ${ operation . destinationPath } `
);
}
2026-08-13 18:21:21 -04:00
// Revalidate the full path after opening. The descriptor pins the file so
// the digest and metadata refer to the same object.
assertSafeInstallOperation ( plan , operation );
assertSafeClaudeSkillOperation ( plan , operation );
return fs . readFileSync ( descriptor );
} finally {
fs . closeSync ( descriptor );
}
}
2026-08-13 18:17:26 -04:00
function stateWithContentDigests ( state , plan ) {
2026-08-24 21:16:39 -04:00
const currentDestinations = new Set (( plan . operations || [])
. filter ( operation => operation . destinationPath )
. map ( operation => {
const resolved = path . resolve ( operation . destinationPath );
return process . platform === 'win32' ? resolved . toLowerCase () : resolved ;
}));
2026-08-06 15:39:49 -04:00
return {
... state ,
operations : ( state . operations || []). map ( operation => {
2026-08-13 18:17:26 -04:00
if ( ! operation . destinationPath ) {
return { ... operation };
}
2026-08-24 21:16:39 -04:00
const resolved = path . resolve ( operation . destinationPath );
const destinationKey = process . platform === 'win32'
? resolved . toLowerCase ()
: resolved ;
if ( ! currentDestinations . has ( destinationKey )) {
return { ... operation };
}
2026-08-13 18:21:21 -04:00
const installedContent = readInstalledFileNoFollow ( plan , operation );
if ( installedContent === null ) {
2026-08-06 15:39:49 -04:00
return { ... operation };
}
return {
... operation ,
contentSha256 : crypto . createHash ( 'sha256' )
2026-08-13 18:21:21 -04:00
. update ( installedContent )
2026-08-06 15:39:49 -04:00
. digest ( 'hex' ),
};
}),
};
}
2026-04-21 18:35:21 -04:00
function cloneJsonValue ( value ) {
if ( value === undefined ) {
return undefined ;
}
return JSON . parse ( JSON . stringify ( value ));
}
function isPlainObject ( value ) {
return Boolean ( value ) && typeof value === 'object' && ! Array . isArray ( value );
}
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 formatJson ( value ) {
return ` ${ JSON . stringify ( value , null , 2 ) } \n` ;
}
2026-08-12 00:43:58 -04:00
function shouldSetClaudeCommitAttributionPreference ( plan ) {
if ( ! plan ? . adapter || ! [ 'claude' , 'claude-project' ]. includes ( plan . adapter . target )) {
return false ;
}
return plan . operations . some ( operation => {
if ( typeof operation ? . destinationPath !== 'string' ) {
return false ;
}
const relativePath = path . relative ( plan . targetRoot , operation . destinationPath );
return relativePath && ! relativePath . startsWith ( `docs ${ path . sep } ` ) && relativePath !== 'docs' ;
});
}
2026-09-07 00:53:13 +08:00
function writeClaudeCommitAttributionPreference ( settingsPath , options = {}) {
2026-09-07 01:13:20 +08:00
let settings ;
2026-08-12 00:43:58 -04:00
try {
2026-09-07 01:13:20 +08:00
settings = readSettings ( settingsPath );
2026-09-07 00:53:13 +08:00
} catch ( _error ) {
// Unreadable or malformed settings belong to the user; leave them untouched.
2026-08-12 00:43:58 -04:00
return false ;
}
2026-09-07 01:13:20 +08:00
if ( hasExplicitCommitAttributionPreference ( settings )) {
return false ;
}
let changed = false ;
updateSettingsAtomic ( settingsPath , latestSettings => {
if ( hasExplicitCommitAttributionPreference ( latestSettings )) {
return { settings : latestSettings };
}
changed = true ;
return { settings : withCommitAttributionDisabled ( latestSettings ) };
}, options );
return changed ;
2026-03-28 19:55:28 -04:00
}
2026-04-05 14:37:28 -07:00
function isMcpConfigPath ( filePath ) {
const basename = path . basename ( String ( filePath || '' ));
return basename === '.mcp.json' || basename === 'mcp.json' ;
}
2026-08-06 15:39:49 -04:00
function assertSafeInstallOperation ( plan , operation ) {
if ( ! operation || typeof operation . destinationPath !== 'string' ) {
throw new Error ( 'Refusing to apply install operation: missing destination path.' );
}
const targetRoot = plan && plan . targetRoot ;
assertWithinTrustedRoot ( operation . destinationPath , targetRoot , 'install ECC file' );
const resolvedRoot = path . resolve ( targetRoot );
const resolvedTarget = path . resolve ( operation . destinationPath );
const relativePath = path . relative ( resolvedRoot , resolvedTarget );
const segments = relativePath ? relativePath . split ( path . sep ) : [];
for ( const segmentIndex of Array . from ({ length : segments . length + 1 }, ( _value , index ) => index )) {
const currentPath = segmentIndex === 0
? resolvedRoot
: path . join ( resolvedRoot , ... segments . slice ( 0 , segmentIndex ));
try {
const stats = fs . lstatSync ( currentPath );
if ( stats . isSymbolicLink ()) {
throw new Error (
`Refusing to install ECC file through symlinked path: ' ${ currentPath } '.`
);
}
} catch ( error ) {
if ( error && error . code === 'ENOENT' ) {
break ;
}
throw error ;
}
}
}
2026-09-07 00:53:13 +08:00
function readPreviousInstallState ( plan ) {
if ( ! fs . existsSync ( plan . installStatePath )) {
return null ;
}
return readInstallState ( plan . installStatePath );
}
function comparablePath ( filePath ) {
const resolved = path . resolve ( filePath );
return process . platform === 'win32' ? resolved . toLowerCase () : resolved ;
}
function findPreviousManagedHooks ( previousState , plan , operation ) {
if (
! previousState
|| previousState . target . id !== plan . adapter . id
|| comparablePath ( previousState . target . root ) !== comparablePath ( plan . targetRoot )
|| comparablePath ( previousState . target . installStatePath ) !== comparablePath ( plan . installStatePath )
) {
2026-03-28 19:55:28 -04:00
return null ;
2026-03-27 07:52:03 -04:00
}
2026-09-07 00:53:13 +08:00
const previousOperation = ( previousState . operations || []). find ( candidate => (
candidate . kind === operation . kind
2026-09-07 01:57:04 +08:00
&& comparablePath ( candidate . destinationPath ) === comparablePath ( operation . destinationPath )
2026-09-07 00:53:13 +08:00
));
if ( ! previousOperation || ! previousOperation . managedHooks ) {
2026-03-28 19:55:28 -04:00
return null ;
2026-03-27 07:52:03 -04:00
}
2026-09-07 01:57:04 +08:00
return validateRecordedManagedHooks (
2026-09-07 00:53:13 +08:00
previousOperation . managedHooks ,
'previous managed hooks'
);
}
function preflightClaudeSettingsOperations ( plan ) {
const settingsOperations = plan . operations . filter ( operation => (
operation . kind === 'update-claude-settings'
|| operation . kind === 'remove-claude-settings-hooks'
));
if ( settingsOperations . length === 0 ) {
return new Map ();
2026-03-27 07:52:03 -04:00
}
2026-09-07 00:53:13 +08:00
const previousState = readPreviousInstallState ( plan );
return new Map ( settingsOperations . map ( operation => {
assertSafeInstallOperation ( plan , operation );
const managedHooks = validateManagedHooks ( operation . managedHooks );
const settings = readSettings ( operation . destinationPath );
const previousManagedHooks = findPreviousManagedHooks ( previousState , plan , operation );
if ( operation . kind === 'remove-claude-settings-hooks' ) {
const removal = uninstallManagedHooks ( settings , managedHooks );
if ( removal . retained . length > 0 ) {
throw new Error (
`Refusing to disable modified Claude hooks in ${ operation . destinationPath } ; `
+ 'run the ECC uninstaller to review retained entries.'
);
}
} else {
mergeManagedHooks ( settings , managedHooks , { previousManagedHooks });
}
return [ operation , { managedHooks , previousManagedHooks }];
}));
}
function prepareHookConsentMigration ( plan , migration ) {
if ( plan . hookConsent !== 'declined' ) {
return migration ;
}
const previousState = readPreviousInstallState ( plan );
if ( ! previousState ) {
return migration ;
}
const removals = ( previousState . operations || [])
. filter ( operation => operation . kind === 'update-claude-settings' )
. map ( operation => ({
... operation ,
kind : 'remove-claude-settings-hooks' ,
strategy : 'remove-hook-ids' ,
scaffoldOnly : false ,
}));
if ( removals . length === 0 ) {
return migration ;
}
const removalDestinations = new Set ( removals . map ( operation => comparablePath (
operation . destinationPath
)));
2026-03-28 19:55:28 -04:00
return {
2026-09-07 00:53:13 +08:00
... migration ,
// Disable hooks only after every ordinary install operation succeeds so a
// partial reinstall cannot silently revoke working hooks before failing.
appliedOperations : [... migration . appliedOperations , ... removals ],
finalState : {
... migration . finalState ,
operations : migration . finalState . operations . filter ( operation => ! (
operation . kind === 'update-claude-settings'
&& removalDestinations . has ( comparablePath ( operation . destinationPath ))
)),
2026-04-01 16:04:56 -07:00
},
2026-09-07 00:53:13 +08:00
bridgeState : {
... migration . bridgeState ,
request : {
... migration . bridgeState . request ,
hookConsent : 'enabled' ,
},
resolution : {
... migration . bridgeState . resolution ,
selectedModules : [... new Set ([
... migration . bridgeState . resolution . selectedModules ,
'hooks-runtime' ,
])],
},
},
requiresBridgeState : true ,
2026-03-28 19:55:28 -04:00
};
2026-03-27 07:52:03 -04:00
}
2026-07-26 03:20:06 -07:00
function previewInstallPlan ( plan ) {
2026-09-07 00:53:13 +08:00
const migration = prepareHookConsentMigration (
plan ,
2026-09-07 16:32:58 -04:00
prepareUserOwnedFileGuard ( plan , prepareClaudeSkillMigration ( plan ))
2026-09-07 00:53:13 +08:00
);
const appliedPlan = {
... plan ,
operations : migration . appliedOperations ,
};
preflightClaudeSettingsOperations ( appliedPlan );
2026-08-07 15:18:14 -04:00
const hookConsentWarnings = planMaterializesHookRuntime ( plan ) && plan . hookConsent !== 'enabled'
? [ 'Applying this plan requires an explicit hook decision: --enable-hooks or --no-hooks.' ]
: [];
2026-07-26 03:20:06 -07:00
return {
... plan ,
statePreview : migration . finalState ,
plannedOperations : [... plan . operations ],
operations : migration . appliedOperations ,
skippedOperations : migration . skippedOperations ,
warnings : [
...( Array . isArray ( plan . warnings ) ? plan . warnings : []),
... migration . warnings ,
2026-08-07 15:18:14 -04:00
... hookConsentWarnings ,
2026-07-26 03:20:06 -07:00
],
applied : false ,
};
}
2026-03-28 19:55:28 -04:00
2026-07-26 03:20:06 -07:00
function applyInstallPlan ( plan , dependencies = {}) {
2026-08-07 15:18:14 -04:00
assertHookConsentReady ( plan );
2026-09-07 00:53:13 +08:00
const isClaudeManualTarget = plan . adapter
&& ( plan . adapter . target === 'claude' || plan . adapter . target === 'claude-project' );
const settingsPathToLock = isClaudeManualTarget
2026-09-07 01:57:04 +08:00
? getClaudeSettingsPath ( plan . targetRoot )
2026-09-07 00:53:13 +08:00
: null ;
if ( settingsPathToLock ) {
assertSafeInstallOperation ( plan , { destinationPath : settingsPathToLock });
}
2026-09-07 01:57:04 +08:00
return settingsPathToLock
? runWithSettingsLock (
settingsPathToLock ,
() => applyInstallPlanLocked ( plan , dependencies , true )
)
: applyInstallPlanLocked ( plan , dependencies , false );
2026-09-07 00:53:13 +08:00
}
function applyInstallPlanLocked ( plan , dependencies = {}, settingsLockHeld = false ) {
2026-07-26 03:20:06 -07:00
const persistInstallState = dependencies . writeInstallState || writeInstallState ;
2026-08-21 02:19:17 +08:00
const beforeInstallStateRead = dependencies . beforeInstallStateRead ;
2026-08-06 15:39:49 -04:00
const beforeOperationWrite = dependencies . beforeOperationWrite ;
const beforeInstallStateWrite = dependencies . beforeInstallStateWrite ;
2026-08-21 02:19:17 +08:00
if ( typeof beforeInstallStateRead === 'function' ) {
beforeInstallStateRead ({ plan });
}
2026-09-07 00:53:13 +08:00
const migration = prepareHookConsentMigration (
plan ,
2026-09-07 16:32:58 -04:00
prepareUserOwnedFileGuard ( plan , prepareClaudeSkillMigration ( plan ))
2026-09-07 00:53:13 +08:00
);
2026-07-26 03:20:06 -07:00
const appliedPlan = {
... plan ,
operations : migration . appliedOperations ,
};
2026-09-07 00:53:13 +08:00
const preparedClaudeSettings = preflightClaudeSettingsOperations ( appliedPlan );
2026-07-26 03:20:06 -07:00
const disabledServers = parseDisabledMcpServers ( process . env . ECC_DISABLED_MCPS );
const linkIndex = buildLinkIndexForPlan ( appliedPlan );
const hasLegacyMigration = migration . legacyOperationsToRemove . length > 0 ;
2026-09-07 00:53:13 +08:00
const hookRemovalCount = appliedPlan . operations . filter ( operation => (
operation . kind === 'remove-claude-settings-hooks'
)). length ;
let completedHookRemovalCount = 0 ;
2026-09-07 16:32:58 -04:00
const writtenDestinations = new Set ();
2026-09-07 00:53:13 +08:00
if ( migration . requiresBridgeState ) {
// Own every operation that may be written during a flat-skill migration
// before the first copy. A later failure is retryable and uninstall can
// clean the entire partial install, including non-skill files. During
// legacy migration the bridge also retains the prior managed operations.
if ( typeof beforeInstallStateWrite === 'function' ) {
beforeInstallStateWrite ({ plan : appliedPlan , state : migration . bridgeState });
}
persistInstallState ( plan . installStatePath , migration . bridgeState );
2026-08-06 15:39:49 -04:00
}
2026-07-26 03:20:06 -07:00
2026-09-07 00:53:13 +08:00
let finalState ;
try {
for ( const operation of appliedPlan . operations ) {
2026-08-13 18:17:26 -04:00
assertSafeInstallOperation ( appliedPlan , operation );
assertSafeClaudeSkillOperation ( appliedPlan , operation );
fs . mkdirSync ( path . dirname ( operation . destinationPath ), { recursive : true });
// Recheck directories that were absent during the first validation. This
// narrows the symlink-swap window around mkdirSync, but path checks cannot
// eliminate a later TOCTOU race before the file write.
assertSafeInstallOperation ( appliedPlan , operation );
assertSafeClaudeSkillOperation ( appliedPlan , operation );
if ( typeof beforeOperationWrite === 'function' ) {
beforeOperationWrite ({ plan : appliedPlan , operation });
2026-04-21 18:35:21 -04:00
}
2026-09-07 16:32:58 -04:00
assertNoNewUserOwnedFile ( migration , operation );
2026-04-21 18:35:21 -04:00
2026-09-07 00:53:13 +08:00
if (
operation . kind === 'update-claude-settings'
|| operation . kind === 'remove-claude-settings-hooks'
) {
// Re-read at the write boundary so unrelated settings added after
// planning are preserved. A same-ID change still fails closed.
const prepared = preparedClaudeSettings . get ( operation );
assertSafeInstallOperation ( appliedPlan , operation );
updateSettingsAtomic ( operation . destinationPath , latestSettings => {
const merged = operation . kind === 'remove-claude-settings-hooks'
? uninstallManagedHooks ( latestSettings , prepared . managedHooks )
: mergeManagedHooks ( latestSettings , prepared . managedHooks , {
previousManagedHooks : prepared . previousManagedHooks ,
});
if (
operation . kind === 'remove-claude-settings-hooks'
&& merged . retained . length > 0
) {
throw new Error (
`Refusing to disable modified Claude hooks in ${ operation . destinationPath } ; `
+ 'run the ECC uninstaller to review retained entries.'
);
}
return merged ;
}, {
lockHeld : settingsLockHeld ,
beforeCommit () {
assertSafeInstallOperation ( appliedPlan , operation );
},
});
2026-09-07 16:32:58 -04:00
writtenDestinations . add ( operation . destinationPath );
2026-09-07 00:53:13 +08:00
if ( operation . kind === 'remove-claude-settings-hooks' ) {
completedHookRemovalCount += 1 ;
}
continue ;
}
2026-08-13 18:17:26 -04:00
if ( operation . kind === 'merge-json' ) {
const payload = cloneJsonValue ( operation . mergePayload );
if ( payload === undefined ) {
throw new Error ( `Missing merge payload for ${ operation . destinationPath } ` );
}
2026-04-21 18:35:21 -04:00
2026-08-13 18:17:26 -04:00
const filteredPayload = (
isMcpConfigPath ( operation . destinationPath ) && disabledServers . length > 0
)
? filterMcpConfig ( payload , disabledServers ). config
: payload ;
2026-04-21 18:35:21 -04:00
2026-08-13 18:21:21 -04:00
const currentValue = readOptionalJsonObject (
operation . destinationPath ,
'existing JSON config'
);
2026-08-13 18:17:26 -04:00
const mergedValue = deepMergeJson ( currentValue , filteredPayload );
fs . writeFileSync ( operation . destinationPath , formatJson ( mergedValue ), 'utf8' );
2026-09-07 16:32:58 -04:00
writtenDestinations . add ( operation . destinationPath );
2026-08-13 18:17:26 -04:00
continue ;
}
2026-04-21 18:35:21 -04:00
2026-08-13 18:17:26 -04:00
if ( operation . kind === 'copy-file' && isMcpConfigPath ( operation . destinationPath ) && disabledServers . length > 0 ) {
const sourceConfig = readJsonObject ( operation . sourcePath , 'MCP config' );
const filteredConfig = filterMcpConfig ( sourceConfig , disabledServers ). config ;
fs . writeFileSync ( operation . destinationPath , formatJson ( filteredConfig ), 'utf8' );
2026-09-07 16:32:58 -04:00
writtenDestinations . add ( operation . destinationPath );
2026-08-13 18:17:26 -04:00
continue ;
}
// Declared transforms are part of the install contract and always apply.
// Markdown link rewriting is additive when the plan has a usable index.
const needsLinkRewrite = Boolean (
linkIndex
&& operation . sourceRelativePath
&& isMarkdownPath ( operation . destinationPath )
2026-07-04 09:09:24 +05:30
);
2026-08-13 18:17:26 -04:00
if ( operation . kind === 'copy-file' && ( operation . contentTransform || needsLinkRewrite )) {
const transformed = transformInstallContent (
operation ,
fs . readFileSync ( operation . sourcePath , 'utf8' )
);
const installedContent = needsLinkRewrite
? rewriteRelativeLinks ( transformed , {
sourceRel : operation . sourceRelativePath ,
index : linkIndex ,
})
: transformed ;
fs . writeFileSync ( operation . destinationPath , installedContent , 'utf8' );
2026-09-07 16:32:58 -04:00
writtenDestinations . add ( operation . destinationPath );
2026-08-13 18:17:26 -04:00
continue ;
}
fs . copyFileSync ( operation . sourcePath , operation . destinationPath );
2026-09-07 16:32:58 -04:00
writtenDestinations . add ( operation . destinationPath );
2026-08-13 18:17:26 -04:00
}
2026-03-28 19:55:28 -04:00
2026-09-07 00:53:13 +08:00
if ( hasLegacyMigration ) {
removeLegacyClaudeSkillFiles ( migration , plan . targetRoot );
}
2026-08-12 00:43:58 -04:00
2026-09-07 00:53:13 +08:00
if ( shouldSetClaudeCommitAttributionPreference ( appliedPlan )) {
writeClaudeCommitAttributionPreference (
2026-09-07 01:57:04 +08:00
getClaudeSettingsPath ( plan . targetRoot ),
2026-09-07 00:53:13 +08:00
{ lockHeld : settingsLockHeld }
2026-08-13 18:29:21 -04:00
);
2026-08-13 18:17:26 -04:00
}
2026-09-07 00:53:13 +08:00
finalState = stateWithContentDigests ( migration . finalState , appliedPlan );
if ( typeof beforeInstallStateWrite === 'function' ) {
beforeInstallStateWrite ({ plan : appliedPlan , state : finalState });
}
persistInstallState ( plan . installStatePath , finalState );
} catch ( error ) {
if ( migration . requiresBridgeState ) {
try {
// The bridge was committed before any writes. Refresh it with hashes of
// files that now exist so uninstall can remove only bytes this attempt
// actually installed while preserving user changes.
persistInstallState (
plan . installStatePath ,
stateWithContentDigests (
2026-09-07 16:32:58 -04:00
preserveUnwrittenFiles (
hookRemovalCount > 0 && completedHookRemovalCount === hookRemovalCount
? migration . finalState
: migration . bridgeState ,
migration ,
writtenDestinations
),
{
... appliedPlan ,
operations : appliedPlan . operations . filter ( operation => (
writtenDestinations . has ( operation . destinationPath )
)),
}
2026-09-07 00:53:13 +08:00
)
);
} catch ( checkpointError ) {
throw new Error (
` ${ error . message } Install-state checkpoint also failed: ${ checkpointError . message } ` ,
{ cause : error }
);
}
}
throw error ;
2026-08-13 18:17:26 -04:00
}
2026-09-07 00:53:13 +08:00
let antigravityMigrationWarnings = [];
2026-08-13 18:03:37 -04:00
try {
const antigravityMigration = cleanupLegacyAntigravityInstall ( appliedPlan );
if ( antigravityMigration . detected && ! antigravityMigration . complete ) {
antigravityMigrationWarnings = [
2026-08-13 16:42:51 -04:00
'Legacy Antigravity migration is incomplete. ECC preserved modified, unverifiable, or unmanaged content under .agent; review and move anything you want to keep, then rerun the Antigravity install.' ,
...( Array . isArray ( antigravityMigration . warnings ) ? antigravityMigration . warnings : []),
2026-08-13 18:03:37 -04:00
];
}
} catch ( error ) {
antigravityMigrationWarnings = [
`Legacy Antigravity cleanup did not finish: ${ error . message } . Content under .agent was preserved; remove it manually or rerun the Antigravity install.` ,
];
}
2026-03-14 12:55:25 -07:00
2026-08-24 20:59:57 -04:00
let opencodeMigrationWarnings = [];
try {
const opencodeMigration = cleanupLegacyOpencodeInstall ( appliedPlan );
if ( opencodeMigration . detected && ! opencodeMigration . complete ) {
opencodeMigrationWarnings = [
'Legacy OpenCode migration is incomplete. ECC preserved modified or unverifiable managed content under ~/.opencode; review it and rerun the OpenCode install.' ,
...( Array . isArray ( opencodeMigration . warnings ) ? opencodeMigration . warnings : []),
];
}
} catch ( error ) {
opencodeMigrationWarnings = [
`Legacy OpenCode cleanup did not finish: ${ error . message } . Content under ~/.opencode was preserved; rerun the OpenCode install or review it manually.` ,
];
}
2026-09-07 00:53:13 +08:00
return {
... plan ,
statePreview : finalState ,
plannedOperations : [... plan . operations ],
operations : migration . appliedOperations ,
skippedOperations : migration . skippedOperations ,
warnings : [
...( Array . isArray ( plan . warnings ) ? plan . warnings : []),
... migration . warnings ,
... antigravityMigrationWarnings ,
... opencodeMigrationWarnings ,
],
applied : true ,
};
2026-03-14 12:55:25 -07:00
}
module . exports = {
applyInstallPlan ,
2026-08-06 15:39:49 -04:00
assertSafeInstallOperation ,
2026-07-26 03:20:06 -07:00
previewInstallPlan ,
2026-03-14 12:55:25 -07:00
};