Files
ECC/scripts/lib/install/antigravity-legacy-migration.js
T
1db5c8ab4a fix(install): harden ECC installer lifecycle
Make Antigravity 2.0 installs native and safely migrate legacy state. Ensure doctor, repair, status projection, repeat installs, legacy Codex sync, and uninstall converge without losing user files. Exclude Python bytecode and harden repo-scan bootstrap guidance.

Gate publishing and pull-request merges on one exact packed artifact completing install, repeat, drift, repair, status, and uninstall across Linux, macOS, and Windows.

Co-authored-by: lorencifernando-coder <lorenci.fernando@gmail.com>

Co-authored-by: Suliman Abdulrazzaq <suliman9000a@gmail.com>

Co-authored-by: Wu Shuwen <mikewushuwen@outlook.com>
2026-08-13 16:42:51 -04:00

414 lines
12 KiB
JavaScript

'use strict';
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { readInstallState } = require('../install-state');
const { assertWithinTrustedRoot } = require('../path-safety');
const ANTIGRAVITY_TARGET = 'antigravity';
const CANONICAL_ROOT_NAME = '.agents';
const LEGACY_ROOT_NAME = '.agent';
const INSTALL_STATE_NAME = 'ecc-install-state.json';
function samePath(leftPath, rightPath) {
const left = path.resolve(leftPath);
const right = path.resolve(rightPath);
if (process.platform === 'win32') {
return left.toLowerCase() === right.toLowerCase();
}
return left === right;
}
function pathExists(filePath) {
try {
fs.lstatSync(filePath);
return true;
} catch (error) {
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) {
return false;
}
throw error;
}
}
function getLegacyAntigravityLocation(projectRoot) {
const targetRoot = path.join(path.resolve(projectRoot), LEGACY_ROOT_NAME);
return {
targetRoot,
installStatePath: path.join(targetRoot, INSTALL_STATE_NAME),
};
}
function getLegacyLocationForPlan(plan) {
if (
!plan
|| !plan.adapter
|| plan.adapter.target !== ANTIGRAVITY_TARGET
|| typeof plan.targetRoot !== 'string'
|| path.basename(path.resolve(plan.targetRoot)) !== CANONICAL_ROOT_NAME
) {
return null;
}
return getLegacyAntigravityLocation(path.dirname(path.resolve(plan.targetRoot)));
}
function inspectLegacyAntigravityState(location) {
if (!location) {
return { status: 'absent', state: null, error: null };
}
try {
if (!pathExists(location.installStatePath)) {
return { status: 'absent', state: null, error: null };
}
const rootStat = fs.lstatSync(location.targetRoot);
const stateStat = fs.lstatSync(location.installStatePath);
if (
!rootStat.isDirectory()
|| rootStat.isSymbolicLink()
|| !stateStat.isFile()
|| stateStat.isSymbolicLink()
) {
return { status: 'invalid', state: null, error: null };
}
const state = readInstallState(location.installStatePath);
const isAntigravity = state.target.target === ANTIGRAVITY_TARGET
|| state.target.id === 'antigravity-project';
if (
!isAntigravity
|| !samePath(state.target.root, location.targetRoot)
|| !samePath(state.target.installStatePath, location.installStatePath)
|| state.operations.some(operation => (
operation.kind !== 'copy-file'
|| operation.ownership !== 'managed'
))
) {
return { status: 'invalid', state: null, error: null };
}
return { status: 'valid', state, error: null };
} catch (error) {
return {
status: 'unreadable',
state: null,
error: `Unable to inspect legacy Antigravity install-state at ${location.installStatePath}: ${error.message}`,
};
}
}
function readValidLegacyAntigravityState(location) {
const inspection = inspectLegacyAntigravityState(location);
return inspection.status === 'valid' ? inspection.state : null;
}
function sha256FileNoFollow(filePath) {
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
const descriptor = fs.openSync(filePath, flags);
try {
const stat = fs.fstatSync(descriptor);
if (!stat.isFile()) {
return null;
}
return {
digest: crypto.createHash('sha256').update(fs.readFileSync(descriptor)).digest('hex'),
stat,
};
} finally {
fs.closeSync(descriptor);
}
}
function getExpectedLegacyDestination(operation, legacyRoot) {
const sourceRelativePath = String(operation.sourceRelativePath || '')
.replace(/\\/g, '/')
.replace(/^\.\//, '');
const parts = sourceRelativePath.split('/');
if (
path.posix.isAbsolute(sourceRelativePath)
|| path.win32.isAbsolute(sourceRelativePath)
|| parts.some(part => part === '' || part === '.' || part === '..')
) {
return null;
}
if (parts[0] === 'rules' && parts.length >= 2) {
const fileName = parts.length === 2
? parts[1]
: `${parts[1]}-${parts.slice(2).join('-')}`;
return path.join(legacyRoot, 'rules', fileName);
}
if (parts[0] === 'commands' && parts.length >= 2) {
return path.join(legacyRoot, 'workflows', ...parts.slice(1));
}
if (parts[0] === 'agents' && parts.length >= 2) {
return path.join(legacyRoot, 'skills', ...parts.slice(1));
}
if (parts[0] === '.agents' && parts.length >= 2) {
return path.join(legacyRoot, '.agents', ...parts.slice(1));
}
if (sourceRelativePath === 'AGENTS.md') {
return path.join(legacyRoot, 'AGENTS.md');
}
return null;
}
function getVerifiedManagedFile(operation, legacyRoot, sourceRoot) {
if (
!operation
|| operation.ownership !== 'managed'
|| operation.kind !== 'copy-file'
|| typeof operation.destinationPath !== 'string'
|| typeof sourceRoot !== 'string'
|| !/^[a-f0-9]{64}$/i.test(operation.contentSha256 || '')
) {
return null;
}
let destinationPath;
let sourcePath;
try {
const expectedDestination = getExpectedLegacyDestination(operation, legacyRoot);
if (!expectedDestination || !samePath(operation.destinationPath, expectedDestination)) {
return null;
}
destinationPath = assertWithinTrustedRoot(
operation.destinationPath,
legacyRoot,
'migrate legacy Antigravity install'
);
sourcePath = assertWithinTrustedRoot(
path.join(sourceRoot, operation.sourceRelativePath),
sourceRoot,
'verify legacy Antigravity source'
);
} catch (_error) {
return null;
}
if (!pathExists(destinationPath)) {
return { destinationPath, missing: true };
}
const stat = fs.lstatSync(destinationPath);
if (!stat.isFile() || stat.isSymbolicLink()) {
return null;
}
const destination = sha256FileNoFollow(destinationPath);
if (
!destination
|| destination.digest !== operation.contentSha256.toLowerCase()
) {
return null;
}
if (!pathExists(sourcePath)) {
return {
destinationPath,
fileStat: destination.stat,
missing: false,
retainedReason: 'The current ECC source file is unavailable, so its provenance cannot be revalidated.',
};
}
const source = sha256FileNoFollow(sourcePath);
if (!source || destination.digest !== source.digest) {
return {
destinationPath,
fileStat: destination.stat,
missing: false,
retainedReason: 'The current ECC source differs from the recorded installed content, so the legacy file was preserved.',
};
}
return { destinationPath, fileStat: destination.stat, missing: false };
}
function cleanupResult(overrides = {}) {
return {
detected: false,
complete: false,
removedPaths: [],
retainedPaths: [],
warnings: [],
...overrides,
};
}
function removeEmptyParents(startPath, legacyRoot) {
let currentPath = path.dirname(startPath);
while (!samePath(currentPath, legacyRoot)) {
let safePath;
try {
safePath = assertWithinTrustedRoot(
currentPath,
legacyRoot,
'clean legacy Antigravity install'
);
} catch (_error) {
return;
}
if (!pathExists(safePath)) {
currentPath = path.dirname(safePath);
continue;
}
const stat = fs.lstatSync(safePath);
if (!stat.isDirectory() || stat.isSymbolicLink() || fs.readdirSync(safePath).length > 0) {
return;
}
fs.rmdirSync(safePath);
currentPath = path.dirname(safePath);
}
}
function listLegacyContent(legacyRoot, installStatePath) {
if (!pathExists(legacyRoot)) {
return [];
}
const content = [];
const pending = [legacyRoot];
while (pending.length > 0) {
const currentPath = pending.pop();
for (const entry of fs.readdirSync(currentPath, { withFileTypes: true })) {
const entryPath = path.join(currentPath, entry.name);
if (samePath(entryPath, installStatePath)) {
continue;
}
if (entry.isDirectory() && !entry.isSymbolicLink()) {
pending.push(entryPath);
} else {
content.push(entryPath);
}
}
}
return content;
}
function removeLegacyStateWhenEmpty(location) {
if (listLegacyContent(location.targetRoot, location.installStatePath).length > 0) {
return false;
}
try {
fs.rmSync(location.installStatePath, { force: true });
} catch (_error) {
return false;
}
try {
if (pathExists(location.targetRoot) && fs.readdirSync(location.targetRoot).length === 0) {
fs.rmdirSync(location.targetRoot);
}
} catch (_error) {
// Root cleanup is best effort after the legacy state is gone.
}
return true;
}
function cleanupLegacyAntigravityInstall(plan) {
const location = getLegacyLocationForPlan(plan);
if (!location || typeof plan.sourceRoot !== 'string' || !pathExists(plan.installStatePath)) {
return cleanupResult();
}
try {
const canonicalState = readInstallState(plan.installStatePath);
const isCanonicalState = (
canonicalState.target.target === ANTIGRAVITY_TARGET
|| canonicalState.target.id === 'antigravity-project'
)
&& samePath(canonicalState.target.root, plan.targetRoot)
&& samePath(canonicalState.target.installStatePath, plan.installStatePath);
if (!isCanonicalState) {
return cleanupResult();
}
} catch (_error) {
return cleanupResult();
}
const legacyInspection = inspectLegacyAntigravityState(location);
if (legacyInspection.status === 'unreadable') {
return cleanupResult({
detected: true,
retainedPaths: [location.targetRoot],
warnings: [legacyInspection.error],
});
}
if (legacyInspection.status !== 'valid') {
return cleanupResult();
}
const legacyState = legacyInspection.state;
const removedPaths = [];
const filesToRemove = [];
const warnings = [];
for (const operation of legacyState.operations || []) {
const verified = getVerifiedManagedFile(operation, location.targetRoot, plan.sourceRoot);
if (!verified) {
continue;
}
if (verified.missing) {
continue;
}
if (verified.retainedReason) {
warnings.push(`${verified.destinationPath}: ${verified.retainedReason}`);
continue;
}
filesToRemove.push({
destinationPath: verified.destinationPath,
fileStat: verified.fileStat,
});
}
for (const { destinationPath, fileStat } of filesToRemove) {
try {
const safeDestination = assertWithinTrustedRoot(
destinationPath,
location.targetRoot,
'remove verified legacy Antigravity file'
);
const currentStat = fs.lstatSync(safeDestination);
if (
currentStat.isSymbolicLink()
|| !currentStat.isFile()
|| currentStat.dev !== fileStat.dev
|| currentStat.ino !== fileStat.ino
) {
continue;
}
fs.rmSync(safeDestination);
removedPaths.push(safeDestination);
removeEmptyParents(safeDestination, location.targetRoot);
} catch (_error) {
// Keep failed deletions tracked in legacy state so a later install can retry.
}
}
let complete = false;
try {
complete = removeLegacyStateWhenEmpty(location);
} catch (_error) {
complete = false;
}
if (complete) {
removedPaths.push(location.installStatePath);
}
let retainedPaths = [];
if (!complete) {
try {
retainedPaths = listLegacyContent(location.targetRoot, location.installStatePath);
} catch (_error) {
retainedPaths = [location.targetRoot];
}
}
return cleanupResult({ detected: true, complete, removedPaths, retainedPaths, warnings });
}
module.exports = {
cleanupLegacyAntigravityInstall,
getLegacyAntigravityLocation,
inspectLegacyAntigravityState,
readValidLegacyAntigravityState,
};