Merge pull request #1393 from affaan-m/fix/cursor-rule-mdc-install

fix: install Cursor rules as .mdc files
This commit is contained in:
Affaan Mustafa
2026-04-13 00:52:03 -07:00
committed by GitHub
6 changed files with 318 additions and 32 deletions

View File

@@ -1,11 +1,23 @@
const fs = require('fs');
const path = require('path');
const {
createFlatRuleOperations,
createInstallTargetAdapter,
createManagedOperation,
isForeignPlatformPath,
} = require('./helpers');
function toCursorRuleFileName(fileName, sourceRelativeFile) {
if (path.basename(sourceRelativeFile).toLowerCase() === 'readme.md') {
return null;
}
return fileName.endsWith('.md')
? `${fileName.slice(0, -3)}.mdc`
: fileName;
}
module.exports = createInstallTargetAdapter({
id: 'cursor-project',
target: 'cursor',
@@ -17,6 +29,7 @@ module.exports = createInstallTargetAdapter({
const modules = Array.isArray(input.modules)
? input.modules
: (input.module ? [input.module] : []);
const seenDestinationPaths = new Set();
const {
repoRoot,
projectRoot,
@@ -28,23 +41,98 @@ module.exports = createInstallTargetAdapter({
homeDir,
};
const targetRoot = adapter.resolveRoot(planningInput);
return modules.flatMap(module => {
const entries = modules.flatMap((module, moduleIndex) => {
const paths = Array.isArray(module.paths) ? module.paths : [];
return paths
.filter(p => !isForeignPlatformPath(p, adapter.target))
.flatMap(sourceRelativePath => {
if (sourceRelativePath === 'rules') {
return createFlatRuleOperations({
moduleId: module.id,
repoRoot,
sourceRelativePath,
destinationDir: path.join(targetRoot, 'rules'),
});
}
.map((sourceRelativePath, pathIndex) => ({
module,
sourceRelativePath,
moduleIndex,
pathIndex,
}));
}).sort((left, right) => {
const getPriority = value => {
if (value === 'rules') {
return 0;
}
return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)];
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') {
return takeUniqueOperations(createFlatRuleOperations({
moduleId: module.id,
repoRoot,
sourceRelativePath,
destinationDir: path.join(targetRoot, 'rules'),
destinationNameTransform: toCursorRuleFileName,
}));
}
if (sourceRelativePath === '.cursor') {
const cursorRoot = path.join(repoRoot, '.cursor');
if (!fs.existsSync(cursorRoot) || !fs.statSync(cursorRoot).isDirectory()) {
return [];
}
const childOperations = fs.readdirSync(cursorRoot, { withFileTypes: true })
.sort((left, right) => left.name.localeCompare(right.name))
.filter(entry => entry.name !== 'rules')
.map(entry => createManagedOperation({
moduleId: module.id,
sourceRelativePath: path.join('.cursor', entry.name),
destinationPath: path.join(targetRoot, entry.name),
strategy: 'preserve-relative-path',
}));
const ruleOperations = createFlatRuleOperations({
moduleId: module.id,
repoRoot,
sourceRelativePath: '.cursor/rules',
destinationDir: path.join(targetRoot, 'rules'),
destinationNameTransform: toCursorRuleFileName,
});
return takeUniqueOperations([...childOperations, ...ruleOperations]);
}
return takeUniqueOperations([
adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput),
]);
});
},
});

View File

@@ -181,7 +181,13 @@ function createNamespacedFlatRuleOperations(adapter, moduleId, sourceRelativePat
return operations;
}
function createFlatRuleOperations({ moduleId, repoRoot, sourceRelativePath, destinationDir }) {
function createFlatRuleOperations({
moduleId,
repoRoot,
sourceRelativePath,
destinationDir,
destinationNameTransform,
}) {
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
const sourceRoot = path.join(repoRoot || '', normalizedSourcePath);
@@ -201,19 +207,33 @@ function createFlatRuleOperations({ moduleId, repoRoot, sourceRelativePath, dest
if (entry.isDirectory()) {
const relativeFiles = listRelativeFiles(entryPath);
for (const relativeFile of relativeFiles) {
const flattenedFileName = `${namespace}-${normalizeRelativePath(relativeFile).replace(/\//g, '-')}`;
const defaultFileName = `${namespace}-${normalizeRelativePath(relativeFile).replace(/\//g, '-')}`;
const sourceRelativeFile = path.join(normalizedSourcePath, namespace, relativeFile);
const flattenedFileName = typeof destinationNameTransform === 'function'
? destinationNameTransform(defaultFileName, sourceRelativeFile)
: defaultFileName;
if (!flattenedFileName) {
continue;
}
operations.push(createManagedOperation({
moduleId,
sourceRelativePath: path.join(normalizedSourcePath, namespace, relativeFile),
sourceRelativePath: sourceRelativeFile,
destinationPath: path.join(destinationDir, flattenedFileName),
strategy: 'flatten-copy',
}));
}
} else if (entry.isFile()) {
const sourceRelativeFile = path.join(normalizedSourcePath, entry.name);
const destinationFileName = typeof destinationNameTransform === 'function'
? destinationNameTransform(entry.name, sourceRelativeFile)
: entry.name;
if (!destinationFileName) {
continue;
}
operations.push(createManagedOperation({
moduleId,
sourceRelativePath: path.join(normalizedSourcePath, entry.name),
destinationPath: path.join(destinationDir, entry.name),
sourceRelativePath: sourceRelativeFile,
destinationPath: path.join(destinationDir, destinationFileName),
strategy: 'flatten-copy',
}));
}