2026-04-13 00:31:20 -07:00
#!/usr/bin/env node
const fs = require ( 'fs' );
const os = require ( 'os' );
const path = require ( 'path' );
const { spawnSync } = require ( 'child_process' );
const { discoverInstalledStates } = require ( './lib/install-lifecycle' );
2026-08-07 15:18:14 -04:00
const { getRecordedHookConsent } = require ( './lib/install/hook-consent' );
2026-04-13 00:31:20 -07:00
const { SUPPORTED_INSTALL_TARGETS } = require ( './lib/install-manifests' );
function showHelp ( exitCode = 0 ) {
console . log ( `
Usage: node scripts/auto-update.js [--target < ${ SUPPORTED_INSTALL_TARGETS . join ( '|' ) } >] [--repo-root <path>] [--dry-run] [--json]
Pull the latest ECC repo changes and reinstall the current context's managed targets
using the original install-state request.
` );
process . exit ( exitCode );
}
function parseArgs ( argv ) {
const args = argv . slice ( 2 );
const parsed = {
targets : [],
repoRoot : null ,
dryRun : false ,
json : false ,
2026-06-18 20:03:24 -04:00
help : false
2026-04-13 00:31:20 -07:00
};
for ( let index = 0 ; index < args . length ; index += 1 ) {
const arg = args [ index ];
if ( arg === '--target' ) {
parsed . targets . push ( args [ index + 1 ] || null );
index += 1 ;
} else if ( arg === '--repo-root' ) {
parsed . repoRoot = args [ index + 1 ] || null ;
index += 1 ;
} else if ( arg === '--dry-run' ) {
parsed . dryRun = true ;
} else if ( arg === '--json' ) {
parsed . json = true ;
} else if ( arg === '--help' || arg === '-h' ) {
parsed . help = true ;
} else {
throw new Error ( `Unknown argument: ${ arg } ` );
}
}
return parsed ;
}
function deriveRepoRootFromState ( state ) {
const operations = Array . isArray ( state && state . operations ) ? state . operations : [];
for ( const operation of operations ) {
if ( typeof operation . sourcePath !== 'string' || ! operation . sourcePath . trim ()) {
continue ;
}
if ( typeof operation . sourceRelativePath !== 'string' || ! operation . sourceRelativePath . trim ()) {
continue ;
}
2026-06-18 20:03:24 -04:00
const relativeParts = operation . sourceRelativePath . split ( /[\\/]+/ ). filter ( Boolean );
2026-04-13 00:31:20 -07:00
if ( relativeParts . length === 0 ) {
continue ;
}
let repoRoot = path . resolve ( operation . sourcePath );
for ( let index = 0 ; index < relativeParts . length ; index += 1 ) {
repoRoot = path . dirname ( repoRoot );
}
return repoRoot ;
}
throw new Error ( 'Unable to infer ECC repo root from install-state operations' );
}
function buildInstallApplyArgs ( record ) {
const state = record . state ;
const target = state . target . target || record . adapter . target ;
const request = state . request || {};
const args = [];
2026-08-07 15:18:14 -04:00
const hookConsent = getRecordedHookConsent ( state );
2026-04-13 00:31:20 -07:00
if ( target ) {
args . push ( '--target' , target );
}
if ( request . profile ) {
args . push ( '--profile' , request . profile );
}
if ( Array . isArray ( request . modules ) && request . modules . length > 0 ) {
args . push ( '--modules' , request . modules . join ( ',' ));
}
for ( const componentId of Array . isArray ( request . includeComponents ) ? request . includeComponents : []) {
args . push ( '--with' , componentId );
}
for ( const componentId of Array . isArray ( request . excludeComponents ) ? request . excludeComponents : []) {
args . push ( '--without' , componentId );
}
2026-08-07 15:18:14 -04:00
if ( hookConsent === 'enabled' ) {
args . push ( '--enable-hooks' );
} else if ( hookConsent === 'declined' ) {
args . push ( '--no-hooks' );
}
2026-04-13 00:31:20 -07:00
for ( const language of Array . isArray ( request . legacyLanguages ) ? request . legacyLanguages : []) {
args . push ( language );
}
return args ;
}
function determineInstallCwd ( record , repoRoot ) {
if ( record . adapter . kind === 'project' ) {
return path . dirname ( record . state . target . root );
}
return repoRoot ;
}
2026-06-18 19:54:22 -04:00
// Recognized ECC package names. A repo root is only trusted to run its
// install-apply.js if its package.json identifies it as ECC — otherwise a
// cloned project that ships a nested `evil/{package.json,scripts/install-apply.js}`
// could drive auto-update into executing attacker code (GHSA-hfpv-w6mp-5g95).
const ECC_PACKAGE_NAMES = new Set ([ 'ecc-universal' , 'everything-claude-code' ]);
2026-04-13 00:31:20 -07:00
function validateRepoRoot ( repoRoot ) {
const normalized = path . resolve ( repoRoot );
const packageJsonPath = path . join ( normalized , 'package.json' );
const installApplyPath = path . join ( normalized , 'scripts' , 'install-apply.js' );
if ( ! fs . existsSync ( packageJsonPath )) {
throw new Error ( `Invalid ECC repo root: missing package.json at ${ packageJsonPath } ` );
}
if ( ! fs . existsSync ( installApplyPath )) {
throw new Error ( `Invalid ECC repo root: missing install script at ${ installApplyPath } ` );
}
2026-06-18 19:54:22 -04:00
let pkgName = null ;
try {
pkgName = JSON . parse ( fs . readFileSync ( packageJsonPath , 'utf8' )). name ;
} catch {
throw new Error ( `Invalid ECC repo root: unreadable package.json at ${ packageJsonPath } ` );
}
if ( ! ECC_PACKAGE_NAMES . has ( pkgName )) {
2026-06-18 20:03:24 -04:00
throw new Error ( `Refusing to run install from untrusted repo root ${ normalized } : package.json name ' ${ pkgName } ' is not an official ECC package.` );
2026-06-18 19:54:22 -04:00
}
2026-04-13 00:31:20 -07:00
return normalized ;
}
function runExternalCommand ( command , args , options = {}) {
const result = spawnSync ( command , args , {
cwd : options . cwd ,
env : options . env || process . env ,
encoding : 'utf8' ,
2026-06-18 20:03:24 -04:00
maxBuffer : 10 * 1024 * 1024
2026-04-13 00:31:20 -07:00
});
if ( result . error ) {
throw result . error ;
}
if ( typeof result . status === 'number' && result . status !== 0 ) {
const errorOutput = ( result . stderr || result . stdout || '' ). trim ();
throw new Error ( ` ${ command } ${ args . join ( ' ' ) } failed ${ errorOutput ? `: ${ errorOutput } ` : '' } ` );
}
return result ;
}
2026-08-25 12:29:35 -04:00
function legacyMigrationWarning ( record ) {
if ( record . legacyLayout === 'opencode' ) {
return 'Found only a legacy OpenCode ~/.opencode install-state. Run the OpenCode installer once to migrate it to the configured OpenCode directory before auto-updating.' ;
}
return 'Found only a legacy Antigravity .agent install-state. Run the Antigravity installer once to migrate it to .agents before auto-updating.' ;
}
2026-04-13 00:31:20 -07:00
function runAutoUpdate ( options = {}, dependencies = {}) {
const discover = dependencies . discoverInstalledStates || discoverInstalledStates ;
const execute = dependencies . runExternalCommand || runExternalCommand ;
const homeDir = options . homeDir || process . env . HOME || os . homedir ();
const projectRoot = options . projectRoot || process . cwd ();
const requestedRepoRoot = options . repoRoot ? validateRepoRoot ( options . repoRoot ) : null ;
2026-08-13 16:42:51 -04:00
const discoveredRecords = discover ({
2026-04-13 00:31:20 -07:00
homeDir ,
projectRoot ,
2026-06-18 20:03:24 -04:00
targets : options . targets
2026-08-13 16:42:51 -04:00
});
const records = discoveredRecords . filter ( record => record . exists && ! record . legacy );
const legacyRecords = discoveredRecords . filter ( record => record . exists && record . legacy );
const warnings = records . length === 0 && legacyRecords . length > 0
2026-08-25 12:29:35 -04:00
? [... new Set ( legacyRecords . map ( legacyMigrationWarning ))]
2026-08-13 16:42:51 -04:00
: [];
2026-04-13 00:31:20 -07:00
const results = [];
if ( records . length === 0 ) {
return {
dryRun : Boolean ( options . dryRun ),
repoRoot : requestedRepoRoot ,
results ,
2026-08-13 16:42:51 -04:00
warnings ,
2026-04-13 00:31:20 -07:00
summary : {
checkedCount : 0 ,
updatedCount : 0 ,
2026-06-18 20:03:24 -04:00
errorCount : 0
}
2026-04-13 00:31:20 -07:00
};
}
const validRecords = [];
const inferredRepoRoots = [];
for ( const record of records ) {
if ( record . error || ! record . state ) {
results . push ({
adapter : record . adapter ,
installStatePath : record . installStatePath ,
status : 'error' ,
2026-06-18 20:03:24 -04:00
error : record . error || 'No valid install-state available'
2026-04-13 00:31:20 -07:00
});
continue ;
}
const recordRepoRoot = requestedRepoRoot || validateRepoRoot ( deriveRepoRootFromState ( record . state ));
inferredRepoRoots . push ( recordRepoRoot );
validRecords . push ({
record ,
2026-06-18 20:03:24 -04:00
repoRoot : recordRepoRoot
2026-04-13 00:31:20 -07:00
});
}
if ( ! requestedRepoRoot ) {
const uniqueRepoRoots = [... new Set ( inferredRepoRoots )];
if ( uniqueRepoRoots . length > 1 ) {
throw new Error ( `Multiple ECC repo roots detected: ${ uniqueRepoRoots . join ( ', ' ) } ` );
}
}
const repoRoot = requestedRepoRoot || inferredRepoRoots [ 0 ] || null ;
if ( ! repoRoot ) {
return {
dryRun : Boolean ( options . dryRun ),
repoRoot ,
results ,
2026-08-13 16:42:51 -04:00
warnings ,
2026-04-13 00:31:20 -07:00
summary : {
checkedCount : results . length ,
updatedCount : 0 ,
2026-06-18 20:03:24 -04:00
errorCount : results . length
}
2026-04-13 00:31:20 -07:00
};
}
const env = {
... process . env ,
HOME : homeDir ,
2026-06-18 20:03:24 -04:00
USERPROFILE : homeDir
2026-04-13 00:31:20 -07:00
};
if ( ! options . dryRun ) {
execute ( 'git' , [ 'fetch' , '--all' , '--prune' ], { cwd : repoRoot , env });
execute ( 'git' , [ 'pull' , '--ff-only' ], { cwd : repoRoot , env });
}
for ( const entry of validRecords ) {
const installArgs = buildInstallApplyArgs ( entry . record );
2026-06-18 20:03:24 -04:00
const args = [ path . join ( repoRoot , 'scripts' , 'install-apply.js' ), ... installArgs , '--json' ];
2026-04-13 00:31:20 -07:00
if ( options . dryRun ) {
args . push ( '--dry-run' );
}
try {
const commandResult = execute ( process . execPath , args , {
cwd : determineInstallCwd ( entry . record , repoRoot ),
2026-06-18 20:03:24 -04:00
env
2026-04-13 00:31:20 -07:00
});
let payload = null ;
if ( commandResult . stdout && commandResult . stdout . trim ()) {
payload = JSON . parse ( commandResult . stdout );
}
results . push ({
adapter : entry . record . adapter ,
installStatePath : entry . record . installStatePath ,
repoRoot ,
cwd : determineInstallCwd ( entry . record , repoRoot ),
installArgs ,
status : options . dryRun ? 'planned' : 'updated' ,
2026-06-18 20:03:24 -04:00
payload
2026-04-13 00:31:20 -07:00
});
} catch ( error ) {
results . push ({
adapter : entry . record . adapter ,
installStatePath : entry . record . installStatePath ,
repoRoot ,
installArgs ,
status : 'error' ,
2026-06-18 20:03:24 -04:00
error : error . message
2026-04-13 00:31:20 -07:00
});
}
}
return {
dryRun : Boolean ( options . dryRun ),
repoRoot ,
results ,
2026-08-13 16:42:51 -04:00
warnings ,
2026-04-13 00:31:20 -07:00
summary : {
checkedCount : results . length ,
updatedCount : results . filter ( result => result . status === 'updated' || result . status === 'planned' ). length ,
2026-06-18 20:03:24 -04:00
errorCount : results . filter ( result => result . status === 'error' ). length
}
2026-04-13 00:31:20 -07:00
};
}
function printHuman ( result ) {
if ( result . results . length === 0 ) {
2026-08-13 16:42:51 -04:00
const hasWarnings = Array . isArray ( result . warnings ) && result . warnings . length > 0 ;
console . log ( hasWarnings
? 'No active ECC install-state files found for the current home/project context.'
: 'No ECC install-state files found for the current home/project context.' );
for ( const warning of Array . isArray ( result . warnings ) ? result . warnings : []) {
console . log ( `Warning: ${ warning } ` );
}
2026-04-13 00:31:20 -07:00
return ;
}
console . log ( ` ${ result . dryRun ? 'Auto-update dry run' : 'Auto-update summary' } :\n` );
if ( result . repoRoot ) {
console . log ( `Repo root: ${ result . repoRoot } \n` );
}
for ( const entry of result . results ) {
console . log ( `- ${ entry . adapter . id } ` );
console . log ( ` Status: ${ entry . status . toUpperCase () } ` );
console . log ( ` Install-state: ${ entry . installStatePath } ` );
if ( entry . error ) {
console . log ( ` Error: ${ entry . error } ` );
continue ;
}
console . log ( ` Reinstall args: ${ entry . installArgs . join ( ' ' ) || '(none)' } ` );
}
console . log ( `\nSummary: checked= ${ result . summary . checkedCount } , ${ result . dryRun ? 'planned' : 'updated' } = ${ result . summary . updatedCount } , errors= ${ result . summary . errorCount } ` );
}
function main () {
try {
const options = parseArgs ( process . argv );
if ( options . help ) {
showHelp ( 0 );
}
const result = runAutoUpdate ({
homeDir : process . env . HOME || os . homedir (),
projectRoot : process . cwd (),
targets : options . targets ,
repoRoot : options . repoRoot ,
2026-06-18 20:03:24 -04:00
dryRun : options . dryRun
2026-04-13 00:31:20 -07:00
});
if ( options . json ) {
console . log ( JSON . stringify ( result , null , 2 ));
} else {
printHuman ( result );
}
process . exitCode = result . summary . errorCount > 0 ? 1 : 0 ;
} catch ( error ) {
console . error ( `Error: ${ error . message } ` );
process . exit ( 1 );
}
}
if ( require . main === module ) {
main ();
}
module . exports = {
parseArgs ,
deriveRepoRootFromState ,
buildInstallApplyArgs ,
determineInstallCwd ,
2026-06-18 20:03:24 -04:00
runAutoUpdate
2026-04-13 00:31:20 -07:00
};