feat: strengthen install lifecycle and target adapters (#512)

* fix: strengthen install lifecycle adapters

* fix: restore template content on uninstall
This commit is contained in:
Affaan Mustafa
2026-03-15 21:47:31 -07:00
committed by GitHub
parent 1e0238de96
commit 131f977841
11 changed files with 1987 additions and 125 deletions

View File

@@ -1,4 +1,10 @@
const { createInstallTargetAdapter } = require('./helpers');
const path = require('path');
const {
createFlatRuleOperations,
createInstallTargetAdapter,
createManagedScaffoldOperation,
} = require('./helpers');
module.exports = createInstallTargetAdapter({
id: 'antigravity-project',
@@ -6,4 +12,58 @@ module.exports = createInstallTargetAdapter({
kind: 'project',
rootSegments: ['.agent'],
installStatePathSegments: ['ecc-install-state.json'],
planOperations(input, adapter) {
const modules = Array.isArray(input.modules)
? input.modules
: (input.module ? [input.module] : []);
const {
repoRoot,
projectRoot,
homeDir,
} = input;
const planningInput = {
repoRoot,
projectRoot,
homeDir,
};
const targetRoot = adapter.resolveRoot(planningInput);
return modules.flatMap(module => {
const paths = Array.isArray(module.paths) ? module.paths : [];
return paths.flatMap(sourceRelativePath => {
if (sourceRelativePath === 'rules') {
return createFlatRuleOperations({
moduleId: module.id,
repoRoot,
sourceRelativePath,
destinationDir: path.join(targetRoot, 'rules'),
});
}
if (sourceRelativePath === 'commands') {
return [
createManagedScaffoldOperation(
module.id,
sourceRelativePath,
path.join(targetRoot, 'workflows'),
'preserve-relative-path'
),
];
}
if (sourceRelativePath === 'agents') {
return [
createManagedScaffoldOperation(
module.id,
sourceRelativePath,
path.join(targetRoot, 'skills'),
'preserve-relative-path'
),
];
}
return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)];
});
});
},
});

View File

@@ -1,4 +1,9 @@
const { createInstallTargetAdapter } = require('./helpers');
const path = require('path');
const {
createFlatRuleOperations,
createInstallTargetAdapter,
} = require('./helpers');
module.exports = createInstallTargetAdapter({
id: 'cursor-project',
@@ -7,4 +12,36 @@ module.exports = createInstallTargetAdapter({
rootSegments: ['.cursor'],
installStatePathSegments: ['ecc-install-state.json'],
nativeRootRelativePath: '.cursor',
planOperations(input, adapter) {
const modules = Array.isArray(input.modules)
? input.modules
: (input.module ? [input.module] : []);
const {
repoRoot,
projectRoot,
homeDir,
} = input;
const planningInput = {
repoRoot,
projectRoot,
homeDir,
};
const targetRoot = adapter.resolveRoot(planningInput);
return modules.flatMap(module => {
const paths = Array.isArray(module.paths) ? module.paths : [];
return paths.flatMap(sourceRelativePath => {
if (sourceRelativePath === 'rules') {
return createFlatRuleOperations({
moduleId: module.id,
repoRoot,
sourceRelativePath,
destinationDir: path.join(targetRoot, 'rules'),
});
}
return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)];
});
});
},
});

View File

@@ -1,3 +1,4 @@
const fs = require('fs');
const os = require('os');
const path = require('path');
@@ -24,6 +25,182 @@ function resolveBaseRoot(scope, input = {}) {
throw new Error(`Unsupported install target scope: ${scope}`);
}
function buildValidationIssue(severity, code, message, extra = {}) {
return {
severity,
code,
message,
...extra,
};
}
function listRelativeFiles(dirPath, prefix = '') {
if (!fs.existsSync(dirPath)) {
return [];
}
const entries = fs.readdirSync(dirPath, { withFileTypes: true }).sort((left, right) => (
left.name.localeCompare(right.name)
));
const files = [];
for (const entry of entries) {
const entryPrefix = prefix ? path.join(prefix, entry.name) : entry.name;
const absolutePath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
files.push(...listRelativeFiles(absolutePath, entryPrefix));
} else if (entry.isFile()) {
files.push(normalizeRelativePath(entryPrefix));
}
}
return files;
}
function createManagedOperation({
kind = 'copy-path',
moduleId,
sourceRelativePath,
destinationPath,
strategy = 'preserve-relative-path',
ownership = 'managed',
scaffoldOnly = true,
...rest
}) {
return {
kind,
moduleId,
sourceRelativePath: normalizeRelativePath(sourceRelativePath),
destinationPath,
strategy,
ownership,
scaffoldOnly,
...rest,
};
}
function defaultValidateAdapterInput(config, input = {}) {
if (config.kind === 'project' && !input.projectRoot && !input.repoRoot) {
return [
buildValidationIssue(
'error',
'missing-project-root',
'projectRoot or repoRoot is required for project install targets'
),
];
}
if (config.kind === 'home' && !input.homeDir && !os.homedir()) {
return [
buildValidationIssue(
'error',
'missing-home-dir',
'homeDir is required for home install targets'
),
];
}
return [];
}
function createRemappedOperation(adapter, moduleId, sourceRelativePath, destinationPath, options = {}) {
return createManagedOperation({
kind: options.kind || 'copy-path',
moduleId,
sourceRelativePath,
destinationPath,
strategy: options.strategy || 'preserve-relative-path',
ownership: options.ownership || 'managed',
scaffoldOnly: Object.hasOwn(options, 'scaffoldOnly') ? options.scaffoldOnly : true,
...options.extra,
});
}
function createNamespacedFlatRuleOperations(adapter, moduleId, sourceRelativePath, input = {}) {
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
const sourceRoot = path.join(input.repoRoot || '', normalizedSourcePath);
if (!input.repoRoot || !fs.existsSync(sourceRoot) || !fs.statSync(sourceRoot).isDirectory()) {
return [];
}
const targetRulesDir = path.join(adapter.resolveRoot(input), 'rules');
const operations = [];
const entries = fs.readdirSync(sourceRoot, { withFileTypes: true }).sort((left, right) => (
left.name.localeCompare(right.name)
));
for (const entry of entries) {
const namespace = entry.name;
const entryPath = path.join(sourceRoot, entry.name);
if (entry.isDirectory()) {
const relativeFiles = listRelativeFiles(entryPath);
for (const relativeFile of relativeFiles) {
const flattenedFileName = `${namespace}-${normalizeRelativePath(relativeFile).replace(/\//g, '-')}`;
const sourceRelativeFile = path.join(normalizedSourcePath, namespace, relativeFile);
operations.push(createManagedOperation({
moduleId,
sourceRelativePath: sourceRelativeFile,
destinationPath: path.join(targetRulesDir, flattenedFileName),
strategy: 'flatten-copy',
}));
}
} else if (entry.isFile()) {
operations.push(createManagedOperation({
moduleId,
sourceRelativePath: path.join(normalizedSourcePath, entry.name),
destinationPath: path.join(targetRulesDir, entry.name),
strategy: 'flatten-copy',
}));
}
}
return operations;
}
function createFlatRuleOperations({ moduleId, repoRoot, sourceRelativePath, destinationDir }) {
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
const sourceRoot = path.join(repoRoot || '', normalizedSourcePath);
if (!repoRoot || !fs.existsSync(sourceRoot) || !fs.statSync(sourceRoot).isDirectory()) {
return [];
}
const operations = [];
const entries = fs.readdirSync(sourceRoot, { withFileTypes: true }).sort((left, right) => (
left.name.localeCompare(right.name)
));
for (const entry of entries) {
const namespace = entry.name;
const entryPath = path.join(sourceRoot, entry.name);
if (entry.isDirectory()) {
const relativeFiles = listRelativeFiles(entryPath);
for (const relativeFile of relativeFiles) {
const flattenedFileName = `${namespace}-${normalizeRelativePath(relativeFile).replace(/\//g, '-')}`;
operations.push(createManagedOperation({
moduleId,
sourceRelativePath: path.join(normalizedSourcePath, namespace, relativeFile),
destinationPath: path.join(destinationDir, flattenedFileName),
strategy: 'flatten-copy',
}));
}
} else if (entry.isFile()) {
operations.push(createManagedOperation({
moduleId,
sourceRelativePath: path.join(normalizedSourcePath, entry.name),
destinationPath: path.join(destinationDir, entry.name),
strategy: 'flatten-copy',
}));
}
}
return operations;
}
function createInstallTargetAdapter(config) {
const adapter = {
id: config.id,
@@ -68,15 +245,43 @@ function createInstallTargetAdapter(config) {
},
createScaffoldOperation(moduleId, sourceRelativePath, input = {}) {
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
return {
kind: 'copy-path',
return createManagedOperation({
moduleId,
sourceRelativePath: normalizedSourcePath,
destinationPath: adapter.resolveDestinationPath(normalizedSourcePath, input),
strategy: adapter.determineStrategy(normalizedSourcePath),
ownership: 'managed',
scaffoldOnly: true,
};
});
},
planOperations(input = {}) {
if (typeof config.planOperations === 'function') {
return config.planOperations(input, adapter);
}
if (Array.isArray(input.modules)) {
return input.modules.flatMap(module => {
const paths = Array.isArray(module.paths) ? module.paths : [];
return paths.map(sourceRelativePath => adapter.createScaffoldOperation(
module.id,
sourceRelativePath,
input
));
});
}
const module = input.module || {};
const paths = Array.isArray(module.paths) ? module.paths : [];
return paths.map(sourceRelativePath => adapter.createScaffoldOperation(
module.id,
sourceRelativePath,
input
));
},
validate(input = {}) {
if (typeof config.validate === 'function') {
return config.validate(input, adapter);
}
return defaultValidateAdapterInput(config, input);
},
};
@@ -84,6 +289,19 @@ function createInstallTargetAdapter(config) {
}
module.exports = {
buildValidationIssue,
createFlatRuleOperations,
createInstallTargetAdapter,
createManagedOperation,
createManagedScaffoldOperation: (moduleId, sourceRelativePath, destinationPath, strategy) => (
createManagedOperation({
moduleId,
sourceRelativePath,
destinationPath,
strategy,
})
),
createNamespacedFlatRuleOperations,
createRemappedOperation,
normalizeRelativePath,
};

View File

@@ -34,15 +34,16 @@ function planInstallTargetScaffold(options = {}) {
projectRoot: options.projectRoot || options.repoRoot,
homeDir: options.homeDir,
};
const validationIssues = adapter.validate(planningInput);
const blockingIssues = validationIssues.filter(issue => issue.severity === 'error');
if (blockingIssues.length > 0) {
throw new Error(blockingIssues.map(issue => issue.message).join('; '));
}
const targetRoot = adapter.resolveRoot(planningInput);
const installStatePath = adapter.getInstallStatePath(planningInput);
const operations = modules.flatMap(module => {
const paths = Array.isArray(module.paths) ? module.paths : [];
return paths.map(sourceRelativePath => adapter.createScaffoldOperation(
module.id,
sourceRelativePath,
planningInput
));
const operations = adapter.planOperations({
...planningInput,
modules,
});
return {
@@ -53,6 +54,7 @@ function planInstallTargetScaffold(options = {}) {
},
targetRoot,
installStatePath,
validationIssues,
operations,
};
}