fix: prefer cursor native hooks during install

This commit is contained in:
Affaan Mustafa
2026-04-13 00:07:15 -07:00
parent 30f6ae4253
commit 9e607ebb30
3 changed files with 150 additions and 38 deletions

View File

@@ -29,6 +29,7 @@ module.exports = createInstallTargetAdapter({
const modules = Array.isArray(input.modules) const modules = Array.isArray(input.modules)
? input.modules ? input.modules
: (input.module ? [input.module] : []); : (input.module ? [input.module] : []);
const seenDestinationPaths = new Set();
const { const {
repoRoot, repoRoot,
projectRoot, projectRoot,
@@ -40,20 +41,66 @@ module.exports = createInstallTargetAdapter({
homeDir, homeDir,
}; };
const targetRoot = adapter.resolveRoot(planningInput); const targetRoot = adapter.resolveRoot(planningInput);
const entries = modules.flatMap((module, moduleIndex) => {
return modules.flatMap(module => {
const paths = Array.isArray(module.paths) ? module.paths : []; const paths = Array.isArray(module.paths) ? module.paths : [];
return paths return paths
.filter(p => !isForeignPlatformPath(p, adapter.target)) .filter(p => !isForeignPlatformPath(p, adapter.target))
.flatMap(sourceRelativePath => { .map((sourceRelativePath, pathIndex) => ({
module,
sourceRelativePath,
moduleIndex,
pathIndex,
}));
}).sort((left, right) => {
const getPriority = value => {
if (value === 'rules') {
return 0;
}
if (value === '.cursor') {
return 1;
}
return 2;
};
const leftPriority = getPriority(left.sourceRelativePath);
const rightPriority = getPriority(right.sourceRelativePath);
if (leftPriority !== rightPriority) {
return leftPriority - rightPriority;
}
if (left.moduleIndex !== right.moduleIndex) {
return left.moduleIndex - right.moduleIndex;
}
return left.pathIndex - right.pathIndex;
});
function takeUniqueOperations(operations) {
return operations.filter(operation => {
if (!operation || !operation.destinationPath) {
return false;
}
if (seenDestinationPaths.has(operation.destinationPath)) {
return false;
}
seenDestinationPaths.add(operation.destinationPath);
return true;
});
}
return entries.flatMap(({ module, sourceRelativePath }) => {
if (sourceRelativePath === 'rules') { if (sourceRelativePath === 'rules') {
return createFlatRuleOperations({ return takeUniqueOperations(createFlatRuleOperations({
moduleId: module.id, moduleId: module.id,
repoRoot, repoRoot,
sourceRelativePath, sourceRelativePath,
destinationDir: path.join(targetRoot, 'rules'), destinationDir: path.join(targetRoot, 'rules'),
destinationNameTransform: toCursorRuleFileName, destinationNameTransform: toCursorRuleFileName,
}); }));
} }
if (sourceRelativePath === '.cursor') { if (sourceRelativePath === '.cursor') {
@@ -80,11 +127,12 @@ module.exports = createInstallTargetAdapter({
destinationNameTransform: toCursorRuleFileName, destinationNameTransform: toCursorRuleFileName,
}); });
return [...childOperations, ...ruleOperations]; return takeUniqueOperations([...childOperations, ...ruleOperations]);
} }
return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)]; return takeUniqueOperations([
}); adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput),
]);
}); });
}, },
}); });

View File

@@ -124,11 +124,11 @@ function runTests() {
); );
assert.ok( assert.ok(
plan.operations.some(operation => ( plan.operations.some(operation => (
operation.sourceRelativePath === '.cursor/rules/common-agents.md' operation.sourceRelativePath === 'rules/common/agents.md'
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-agents.mdc') && operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-agents.mdc')
&& operation.strategy === 'flatten-copy' && operation.strategy === 'flatten-copy'
)), )),
'Should flatten Cursor platform rules into .mdc files' 'Should produce Cursor .mdc rules while preferring rules-core over duplicate platform copies'
); );
})) passed++; else failed++; })) passed++; else failed++;

View File

@@ -209,6 +209,70 @@ function runTests() {
); );
})) passed++; else failed++; })) passed++; else failed++;
if (test('deduplicates cursor rule destinations when rules-core and platform-configs overlap', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'cursor',
repoRoot,
projectRoot,
modules: [
{
id: 'rules-core',
paths: ['rules'],
},
{
id: 'platform-configs',
paths: ['.cursor'],
},
],
});
const commonAgentsDestinations = plan.operations.filter(operation => (
operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-agents.mdc')
));
assert.strictEqual(commonAgentsDestinations.length, 1, 'Should keep only one common-agents.mdc operation');
assert.strictEqual(
normalizedRelativePath(commonAgentsDestinations[0].sourceRelativePath),
'rules/common/agents.md',
'Should prefer rules-core when cursor platform rules would collide'
);
})) passed++; else failed++;
if (test('prefers native cursor hooks when hooks-runtime and platform-configs overlap', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'cursor',
repoRoot,
projectRoot,
modules: [
{
id: 'hooks-runtime',
paths: ['hooks', 'scripts/hooks', 'scripts/lib'],
},
{
id: 'platform-configs',
paths: ['.cursor'],
},
],
});
const hooksDestinations = plan.operations.filter(operation => (
operation.destinationPath === path.join(projectRoot, '.cursor', 'hooks')
));
assert.strictEqual(hooksDestinations.length, 1, 'Should keep only one .cursor/hooks scaffold operation');
assert.strictEqual(
normalizedRelativePath(hooksDestinations[0].sourceRelativePath),
'.cursor/hooks',
'Should prefer native Cursor hooks over generic hooks-runtime hooks'
);
})) passed++; else failed++;
if (test('plans antigravity remaps for workflows, skills, and flat rules', () => { if (test('plans antigravity remaps for workflows, skills, and flat rules', () => {
const repoRoot = path.join(__dirname, '..', '..'); const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app'; const projectRoot = '/workspace/app';