mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-13 21:33:32 +08:00
Merge pull request #1393 from affaan-m/fix/cursor-rule-mdc-install
fix: install Cursor rules as .mdc files
This commit is contained in:
@@ -1,11 +1,23 @@
|
|||||||
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
createFlatRuleOperations,
|
createFlatRuleOperations,
|
||||||
createInstallTargetAdapter,
|
createInstallTargetAdapter,
|
||||||
|
createManagedOperation,
|
||||||
isForeignPlatformPath,
|
isForeignPlatformPath,
|
||||||
} = require('./helpers');
|
} = 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({
|
module.exports = createInstallTargetAdapter({
|
||||||
id: 'cursor-project',
|
id: 'cursor-project',
|
||||||
target: 'cursor',
|
target: 'cursor',
|
||||||
@@ -17,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,
|
||||||
@@ -28,23 +41,98 @@ 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) => ({
|
||||||
if (sourceRelativePath === 'rules') {
|
module,
|
||||||
return createFlatRuleOperations({
|
sourceRelativePath,
|
||||||
moduleId: module.id,
|
moduleIndex,
|
||||||
repoRoot,
|
pathIndex,
|
||||||
sourceRelativePath,
|
}));
|
||||||
destinationDir: path.join(targetRoot, 'rules'),
|
}).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),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -181,7 +181,13 @@ function createNamespacedFlatRuleOperations(adapter, moduleId, sourceRelativePat
|
|||||||
return operations;
|
return operations;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFlatRuleOperations({ moduleId, repoRoot, sourceRelativePath, destinationDir }) {
|
function createFlatRuleOperations({
|
||||||
|
moduleId,
|
||||||
|
repoRoot,
|
||||||
|
sourceRelativePath,
|
||||||
|
destinationDir,
|
||||||
|
destinationNameTransform,
|
||||||
|
}) {
|
||||||
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
|
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
|
||||||
const sourceRoot = path.join(repoRoot || '', normalizedSourcePath);
|
const sourceRoot = path.join(repoRoot || '', normalizedSourcePath);
|
||||||
|
|
||||||
@@ -201,19 +207,33 @@ function createFlatRuleOperations({ moduleId, repoRoot, sourceRelativePath, dest
|
|||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
const relativeFiles = listRelativeFiles(entryPath);
|
const relativeFiles = listRelativeFiles(entryPath);
|
||||||
for (const relativeFile of relativeFiles) {
|
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({
|
operations.push(createManagedOperation({
|
||||||
moduleId,
|
moduleId,
|
||||||
sourceRelativePath: path.join(normalizedSourcePath, namespace, relativeFile),
|
sourceRelativePath: sourceRelativeFile,
|
||||||
destinationPath: path.join(destinationDir, flattenedFileName),
|
destinationPath: path.join(destinationDir, flattenedFileName),
|
||||||
strategy: 'flatten-copy',
|
strategy: 'flatten-copy',
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} else if (entry.isFile()) {
|
} 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({
|
operations.push(createManagedOperation({
|
||||||
moduleId,
|
moduleId,
|
||||||
sourceRelativePath: path.join(normalizedSourcePath, entry.name),
|
sourceRelativePath: sourceRelativeFile,
|
||||||
destinationPath: path.join(destinationDir, entry.name),
|
destinationPath: path.join(destinationDir, destinationFileName),
|
||||||
strategy: 'flatten-copy',
|
strategy: 'flatten-copy',
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const http = require('http');
|
||||||
|
const https = require('https');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { spawn, spawnSync } = require('child_process');
|
const { spawn, spawnSync } = require('child_process');
|
||||||
@@ -109,6 +111,39 @@ function waitForFile(filePath, timeoutMs = 5000) {
|
|||||||
}
|
}
|
||||||
throw new Error(`Timed out waiting for ${filePath}`);
|
throw new Error(`Timed out waiting for ${filePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function waitForHttpReady(urlString, timeoutMs = 5000) {
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
const { protocol } = new URL(urlString);
|
||||||
|
const client = protocol === 'https:' ? https : http;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const attempt = () => {
|
||||||
|
const req = client.request(urlString, { method: 'GET' }, res => {
|
||||||
|
res.resume();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
req.setTimeout(250, () => {
|
||||||
|
req.destroy(new Error('timeout'));
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', error => {
|
||||||
|
if (Date.now() >= deadline) {
|
||||||
|
reject(new Error(`Timed out waiting for ${urlString}: ${error.message}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(attempt, 25);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
attempt();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function runTests() {
|
async function runTests() {
|
||||||
console.log('\n=== Testing mcp-health-check.js ===\n');
|
console.log('\n=== Testing mcp-health-check.js ===\n');
|
||||||
|
|
||||||
@@ -329,6 +364,7 @@ async function runTests() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const port = waitForFile(portFile).trim();
|
const port = waitForFile(portFile).trim();
|
||||||
|
await waitForHttpReady(`http://127.0.0.1:${port}/mcp`);
|
||||||
|
|
||||||
writeConfig(configPath, {
|
writeConfig(configPath, {
|
||||||
mcpServers: {
|
mcpServers: {
|
||||||
@@ -391,6 +427,7 @@ async function runTests() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const port = waitForFile(portFile).trim();
|
const port = waitForFile(portFile).trim();
|
||||||
|
await waitForHttpReady(`http://127.0.0.1:${port}/mcp`);
|
||||||
|
|
||||||
writeConfig(configPath, {
|
writeConfig(configPath, {
|
||||||
mcpServers: {
|
mcpServers: {
|
||||||
|
|||||||
@@ -116,10 +116,19 @@ function runTests() {
|
|||||||
assert.ok(plan.operations.length > 0, 'Should include scaffold operations');
|
assert.ok(plan.operations.length > 0, 'Should include scaffold operations');
|
||||||
assert.ok(
|
assert.ok(
|
||||||
plan.operations.some(operation => (
|
plan.operations.some(operation => (
|
||||||
operation.sourceRelativePath === '.cursor'
|
operation.sourceRelativePath === '.cursor/hooks.json'
|
||||||
&& operation.strategy === 'sync-root-children'
|
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'hooks.json')
|
||||||
|
&& operation.strategy === 'preserve-relative-path'
|
||||||
)),
|
)),
|
||||||
'Should flatten the native cursor root'
|
'Should preserve non-rule Cursor platform files'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
plan.operations.some(operation => (
|
||||||
|
operation.sourceRelativePath === 'rules/common/agents.md'
|
||||||
|
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-agents.mdc')
|
||||||
|
&& operation.strategy === 'flatten-copy'
|
||||||
|
)),
|
||||||
|
'Should produce Cursor .mdc rules while preferring rules-core over duplicate platform copies'
|
||||||
);
|
);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
|||||||
@@ -90,20 +90,22 @@ function runTests() {
|
|||||||
assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.cursor'));
|
assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.cursor'));
|
||||||
assert.strictEqual(plan.installStatePath, path.join(projectRoot, '.cursor', 'ecc-install-state.json'));
|
assert.strictEqual(plan.installStatePath, path.join(projectRoot, '.cursor', 'ecc-install-state.json'));
|
||||||
|
|
||||||
const flattened = plan.operations.find(operation => operation.sourceRelativePath === '.cursor');
|
const hooksJson = plan.operations.find(operation => (
|
||||||
|
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/hooks.json'
|
||||||
|
));
|
||||||
const preserved = plan.operations.find(operation => (
|
const preserved = plan.operations.find(operation => (
|
||||||
normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md'
|
normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md'
|
||||||
));
|
));
|
||||||
|
|
||||||
assert.ok(flattened, 'Should include .cursor scaffold operation');
|
assert.ok(hooksJson, 'Should preserve non-rule Cursor platform config files');
|
||||||
assert.strictEqual(flattened.strategy, 'sync-root-children');
|
assert.strictEqual(hooksJson.strategy, 'preserve-relative-path');
|
||||||
assert.strictEqual(flattened.destinationPath, path.join(projectRoot, '.cursor'));
|
assert.strictEqual(hooksJson.destinationPath, path.join(projectRoot, '.cursor', 'hooks.json'));
|
||||||
|
|
||||||
assert.ok(preserved, 'Should include flattened rules scaffold operations');
|
assert.ok(preserved, 'Should include flattened rules scaffold operations');
|
||||||
assert.strictEqual(preserved.strategy, 'flatten-copy');
|
assert.strictEqual(preserved.strategy, 'flatten-copy');
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
preserved.destinationPath,
|
preserved.destinationPath,
|
||||||
path.join(projectRoot, '.cursor', 'rules', 'common-coding-style.md')
|
path.join(projectRoot, '.cursor', 'rules', 'common-coding-style.mdc')
|
||||||
);
|
);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
@@ -126,16 +128,16 @@ function runTests() {
|
|||||||
assert.ok(
|
assert.ok(
|
||||||
plan.operations.some(operation => (
|
plan.operations.some(operation => (
|
||||||
normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md'
|
normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md'
|
||||||
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-coding-style.md')
|
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-coding-style.mdc')
|
||||||
)),
|
)),
|
||||||
'Should flatten common rules into namespaced files'
|
'Should flatten common rules into namespaced .mdc files'
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
plan.operations.some(operation => (
|
plan.operations.some(operation => (
|
||||||
normalizedRelativePath(operation.sourceRelativePath) === 'rules/typescript/testing.md'
|
normalizedRelativePath(operation.sourceRelativePath) === 'rules/typescript/testing.md'
|
||||||
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'typescript-testing.md')
|
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'typescript-testing.mdc')
|
||||||
)),
|
)),
|
||||||
'Should flatten language rules into namespaced files'
|
'Should flatten language rules into namespaced .mdc files'
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!plan.operations.some(operation => (
|
!plan.operations.some(operation => (
|
||||||
@@ -143,6 +145,132 @@ function runTests() {
|
|||||||
)),
|
)),
|
||||||
'Should not preserve nested rule directories for cursor installs'
|
'Should not preserve nested rule directories for cursor installs'
|
||||||
);
|
);
|
||||||
|
assert.ok(
|
||||||
|
!plan.operations.some(operation => (
|
||||||
|
operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-coding-style.md')
|
||||||
|
)),
|
||||||
|
'Should not emit .md Cursor rule files'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
!plan.operations.some(operation => (
|
||||||
|
normalizedRelativePath(operation.sourceRelativePath) === 'rules/README.md'
|
||||||
|
)),
|
||||||
|
'Should not install Cursor README docs as runtime rule files'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
!plan.operations.some(operation => (
|
||||||
|
normalizedRelativePath(operation.sourceRelativePath) === 'rules/zh/README.md'
|
||||||
|
)),
|
||||||
|
'Should not flatten localized README docs into Cursor rule files'
|
||||||
|
);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('plans cursor platform rule files as .mdc and excludes rule README docs', () => {
|
||||||
|
const repoRoot = path.join(__dirname, '..', '..');
|
||||||
|
const projectRoot = '/workspace/app';
|
||||||
|
|
||||||
|
const plan = planInstallTargetScaffold({
|
||||||
|
target: 'cursor',
|
||||||
|
repoRoot,
|
||||||
|
projectRoot,
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
id: 'platform-configs',
|
||||||
|
paths: ['.cursor'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
plan.operations.some(operation => (
|
||||||
|
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/rules/common-agents.md'
|
||||||
|
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-agents.mdc')
|
||||||
|
)),
|
||||||
|
'Should rename Cursor platform rule files to .mdc'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
!plan.operations.some(operation => (
|
||||||
|
operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-agents.md')
|
||||||
|
)),
|
||||||
|
'Should not preserve .md Cursor platform rule files'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
plan.operations.some(operation => (
|
||||||
|
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/hooks.json'
|
||||||
|
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'hooks.json')
|
||||||
|
)),
|
||||||
|
'Should preserve non-rule Cursor platform config files'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
!plan.operations.some(operation => (
|
||||||
|
operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'README.mdc')
|
||||||
|
)),
|
||||||
|
'Should not emit Cursor rule README docs as .mdc files'
|
||||||
|
);
|
||||||
|
})) 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++;
|
})) 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', () => {
|
||||||
|
|||||||
@@ -130,8 +130,11 @@ function runTests() {
|
|||||||
const result = run(['--target', 'cursor', 'typescript'], { cwd: projectDir, homeDir });
|
const result = run(['--target', 'cursor', 'typescript'], { cwd: projectDir, homeDir });
|
||||||
assert.strictEqual(result.code, 0, result.stderr);
|
assert.strictEqual(result.code, 0, result.stderr);
|
||||||
|
|
||||||
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-coding-style.md')));
|
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-coding-style.mdc')));
|
||||||
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'typescript-testing.md')));
|
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'typescript-testing.mdc')));
|
||||||
|
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-agents.mdc')));
|
||||||
|
assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-agents.md')));
|
||||||
|
assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'README.mdc')));
|
||||||
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'agents', 'architect.md')));
|
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'agents', 'architect.md')));
|
||||||
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'commands', 'plan.md')));
|
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'commands', 'plan.md')));
|
||||||
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json')));
|
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json')));
|
||||||
@@ -304,7 +307,8 @@ function runTests() {
|
|||||||
});
|
});
|
||||||
assert.strictEqual(result.code, 0, result.stderr);
|
assert.strictEqual(result.code, 0, result.stderr);
|
||||||
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json')));
|
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json')));
|
||||||
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-agents.md')));
|
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-agents.mdc')));
|
||||||
|
assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-agents.md')));
|
||||||
|
|
||||||
const state = readJson(path.join(projectDir, '.cursor', 'ecc-install-state.json'));
|
const state = readJson(path.join(projectDir, '.cursor', 'ecc-install-state.json'));
|
||||||
assert.strictEqual(state.request.profile, null);
|
assert.strictEqual(state.request.profile, null);
|
||||||
|
|||||||
Reference in New Issue
Block a user