mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-22 18:13:41 +08:00
fix: install native Cursor hook and MCP config (#1543)
* fix: install native cursor hook and MCP config * fix: avoid false healthy stdio mcp probes
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"version": 1,
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"sessionStart": [
|
"sessionStart": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -308,10 +308,15 @@ function probeCommandServer(serverName, config) {
|
|||||||
|
|
||||||
let stderr = '';
|
let stderr = '';
|
||||||
let done = false;
|
let done = false;
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
function finish(result) {
|
function finish(result) {
|
||||||
if (done) return;
|
if (done) return;
|
||||||
done = true;
|
done = true;
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
resolve(result);
|
resolve(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +359,19 @@ function probeCommandServer(serverName, config) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
|
// A fast-crashing stdio server can finish before the timer callback runs
|
||||||
|
// on a loaded machine. Check the process state again before classifying it
|
||||||
|
// as healthy on timeout.
|
||||||
|
if (child.exitCode !== null || child.signalCode !== null) {
|
||||||
|
finish({
|
||||||
|
ok: false,
|
||||||
|
statusCode: child.exitCode,
|
||||||
|
reason: stderr.trim() || `process exited before handshake (${child.signalCode || child.exitCode || 'unknown'})`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
child.kill('SIGTERM');
|
child.kill('SIGTERM');
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -184,6 +184,41 @@ function addFileCopyOperation(operations, options) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readJsonObject(filePath, label) {
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to parse ${label} at ${filePath}: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||||
|
throw new Error(`Invalid ${label} at ${filePath}: expected a JSON object`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addJsonMergeOperation(operations, options) {
|
||||||
|
const sourcePath = path.join(options.sourceRoot, options.sourceRelativePath);
|
||||||
|
if (!fs.existsSync(sourcePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
operations.push({
|
||||||
|
kind: 'merge-json',
|
||||||
|
moduleId: options.moduleId,
|
||||||
|
sourceRelativePath: options.sourceRelativePath,
|
||||||
|
destinationPath: options.destinationPath,
|
||||||
|
strategy: 'merge-json',
|
||||||
|
ownership: 'managed',
|
||||||
|
scaffoldOnly: false,
|
||||||
|
mergePayload: readJsonObject(sourcePath, options.sourceRelativePath),
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function addMatchingRuleOperations(operations, options) {
|
function addMatchingRuleOperations(operations, options) {
|
||||||
const sourceDir = path.join(options.sourceRoot, options.sourceRelativeDir);
|
const sourceDir = path.join(options.sourceRoot, options.sourceRelativeDir);
|
||||||
if (!fs.existsSync(sourceDir)) {
|
if (!fs.existsSync(sourceDir)) {
|
||||||
@@ -342,10 +377,10 @@ function planCursorLegacyInstall(context) {
|
|||||||
sourceRelativePath: path.join('.cursor', 'hooks.json'),
|
sourceRelativePath: path.join('.cursor', 'hooks.json'),
|
||||||
destinationPath: path.join(targetRoot, 'hooks.json'),
|
destinationPath: path.join(targetRoot, 'hooks.json'),
|
||||||
});
|
});
|
||||||
addFileCopyOperation(operations, {
|
addJsonMergeOperation(operations, {
|
||||||
moduleId: 'legacy-cursor-install',
|
moduleId: 'legacy-cursor-install',
|
||||||
sourceRoot: context.sourceRoot,
|
sourceRoot: context.sourceRoot,
|
||||||
sourceRelativePath: path.join('.cursor', 'mcp.json'),
|
sourceRelativePath: '.mcp.json',
|
||||||
destinationPath: path.join(targetRoot, 'mcp.json'),
|
destinationPath: path.join(targetRoot, 'mcp.json'),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -540,6 +575,22 @@ function createLegacyCompatInstallPlan(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function materializeScaffoldOperation(sourceRoot, operation) {
|
function materializeScaffoldOperation(sourceRoot, operation) {
|
||||||
|
if (operation.kind === 'merge-json') {
|
||||||
|
return [{
|
||||||
|
kind: 'merge-json',
|
||||||
|
moduleId: operation.moduleId,
|
||||||
|
sourceRelativePath: operation.sourceRelativePath,
|
||||||
|
destinationPath: operation.destinationPath,
|
||||||
|
strategy: operation.strategy || 'merge-json',
|
||||||
|
ownership: operation.ownership || 'managed',
|
||||||
|
scaffoldOnly: Object.hasOwn(operation, 'scaffoldOnly') ? operation.scaffoldOnly : false,
|
||||||
|
mergePayload: readJsonObject(
|
||||||
|
path.join(sourceRoot, operation.sourceRelativePath),
|
||||||
|
operation.sourceRelativePath
|
||||||
|
),
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
const sourcePath = path.join(sourceRoot, operation.sourceRelativePath);
|
const sourcePath = path.join(sourceRoot, operation.sourceRelativePath);
|
||||||
if (!fs.existsSync(sourcePath)) {
|
if (!fs.existsSync(sourcePath)) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -18,6 +18,39 @@ function toCursorRuleFileName(fileName, sourceRelativeFile) {
|
|||||||
: fileName;
|
: fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readJsonObject(filePath, label) {
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to parse ${label} at ${filePath}: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||||
|
throw new Error(`Invalid ${label} at ${filePath}: expected a JSON object`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createJsonMergeOperation({ moduleId, repoRoot, sourceRelativePath, destinationPath }) {
|
||||||
|
const sourcePath = path.join(repoRoot, sourceRelativePath);
|
||||||
|
if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isFile()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return createManagedOperation({
|
||||||
|
kind: 'merge-json',
|
||||||
|
moduleId,
|
||||||
|
sourceRelativePath,
|
||||||
|
destinationPath,
|
||||||
|
strategy: 'merge-json',
|
||||||
|
ownership: 'managed',
|
||||||
|
scaffoldOnly: false,
|
||||||
|
mergePayload: readJsonObject(sourcePath, sourceRelativePath),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = createInstallTargetAdapter({
|
module.exports = createInstallTargetAdapter({
|
||||||
id: 'cursor-project',
|
id: 'cursor-project',
|
||||||
target: 'cursor',
|
target: 'cursor',
|
||||||
@@ -93,6 +126,13 @@ module.exports = createInstallTargetAdapter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return entries.flatMap(({ module, sourceRelativePath }) => {
|
return entries.flatMap(({ module, sourceRelativePath }) => {
|
||||||
|
const cursorMcpOperation = createJsonMergeOperation({
|
||||||
|
moduleId: module.id,
|
||||||
|
repoRoot,
|
||||||
|
sourceRelativePath: '.mcp.json',
|
||||||
|
destinationPath: path.join(targetRoot, 'mcp.json'),
|
||||||
|
});
|
||||||
|
|
||||||
if (sourceRelativePath === 'rules') {
|
if (sourceRelativePath === 'rules') {
|
||||||
return takeUniqueOperations(createFlatRuleOperations({
|
return takeUniqueOperations(createFlatRuleOperations({
|
||||||
moduleId: module.id,
|
moduleId: module.id,
|
||||||
@@ -127,7 +167,21 @@ module.exports = createInstallTargetAdapter({
|
|||||||
destinationNameTransform: toCursorRuleFileName,
|
destinationNameTransform: toCursorRuleFileName,
|
||||||
});
|
});
|
||||||
|
|
||||||
return takeUniqueOperations([...childOperations, ...ruleOperations]);
|
return takeUniqueOperations([
|
||||||
|
...childOperations,
|
||||||
|
...(cursorMcpOperation ? [cursorMcpOperation] : []),
|
||||||
|
...ruleOperations,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceRelativePath === 'mcp-configs') {
|
||||||
|
const operations = [
|
||||||
|
adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput),
|
||||||
|
];
|
||||||
|
if (cursorMcpOperation) {
|
||||||
|
operations.push(cursorMcpOperation);
|
||||||
|
}
|
||||||
|
return takeUniqueOperations(operations);
|
||||||
}
|
}
|
||||||
|
|
||||||
return takeUniqueOperations([
|
return takeUniqueOperations([
|
||||||
|
|||||||
@@ -21,6 +21,38 @@ function readJsonObject(filePath, label) {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cloneJsonValue(value) {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(JSON.stringify(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value) {
|
||||||
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepMergeJson(baseValue, patchValue) {
|
||||||
|
if (!isPlainObject(baseValue) || !isPlainObject(patchValue)) {
|
||||||
|
return cloneJsonValue(patchValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
const merged = { ...baseValue };
|
||||||
|
for (const [key, value] of Object.entries(patchValue)) {
|
||||||
|
if (isPlainObject(value) && isPlainObject(merged[key])) {
|
||||||
|
merged[key] = deepMergeJson(merged[key], value);
|
||||||
|
} else {
|
||||||
|
merged[key] = cloneJsonValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatJson(value) {
|
||||||
|
return `${JSON.stringify(value, null, 2)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
function replacePluginRootPlaceholders(value, pluginRoot) {
|
function replacePluginRootPlaceholders(value, pluginRoot) {
|
||||||
if (!pluginRoot) {
|
if (!pluginRoot) {
|
||||||
return value;
|
return value;
|
||||||
@@ -56,44 +88,6 @@ function isMcpConfigPath(filePath) {
|
|||||||
return basename === '.mcp.json' || basename === 'mcp.json';
|
return basename === '.mcp.json' || basename === 'mcp.json';
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFilteredMcpWrites(plan) {
|
|
||||||
const disabledServers = parseDisabledMcpServers(process.env.ECC_DISABLED_MCPS);
|
|
||||||
if (disabledServers.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const writes = [];
|
|
||||||
|
|
||||||
for (const operation of plan.operations) {
|
|
||||||
if (!isMcpConfigPath(operation.destinationPath) || !operation.sourcePath || !fs.existsSync(operation.sourcePath)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sourceConfig;
|
|
||||||
try {
|
|
||||||
sourceConfig = readJsonObject(operation.sourcePath, 'MCP config');
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sourceConfig.mcpServers || typeof sourceConfig.mcpServers !== 'object' || Array.isArray(sourceConfig.mcpServers)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filtered = filterMcpConfig(sourceConfig, disabledServers);
|
|
||||||
if (filtered.removed.length === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
writes.push({
|
|
||||||
destinationPath: operation.destinationPath,
|
|
||||||
filteredConfig: filtered.config,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return writes;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildResolvedClaudeHooks(plan) {
|
function buildResolvedClaudeHooks(plan) {
|
||||||
if (!plan.adapter || plan.adapter.target !== 'claude') {
|
if (!plan.adapter || plan.adapter.target !== 'claude') {
|
||||||
return null;
|
return null;
|
||||||
@@ -123,10 +117,38 @@ function buildResolvedClaudeHooks(plan) {
|
|||||||
|
|
||||||
function applyInstallPlan(plan) {
|
function applyInstallPlan(plan) {
|
||||||
const resolvedClaudeHooksPlan = buildResolvedClaudeHooks(plan);
|
const resolvedClaudeHooksPlan = buildResolvedClaudeHooks(plan);
|
||||||
const filteredMcpWrites = buildFilteredMcpWrites(plan);
|
const disabledServers = parseDisabledMcpServers(process.env.ECC_DISABLED_MCPS);
|
||||||
|
|
||||||
for (const operation of plan.operations) {
|
for (const operation of plan.operations) {
|
||||||
fs.mkdirSync(path.dirname(operation.destinationPath), { recursive: true });
|
fs.mkdirSync(path.dirname(operation.destinationPath), { recursive: true });
|
||||||
|
|
||||||
|
if (operation.kind === 'merge-json') {
|
||||||
|
const payload = cloneJsonValue(operation.mergePayload);
|
||||||
|
if (payload === undefined) {
|
||||||
|
throw new Error(`Missing merge payload for ${operation.destinationPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredPayload = (
|
||||||
|
isMcpConfigPath(operation.destinationPath) && disabledServers.length > 0
|
||||||
|
)
|
||||||
|
? filterMcpConfig(payload, disabledServers).config
|
||||||
|
: payload;
|
||||||
|
|
||||||
|
const currentValue = fs.existsSync(operation.destinationPath)
|
||||||
|
? readJsonObject(operation.destinationPath, 'existing JSON config')
|
||||||
|
: {};
|
||||||
|
const mergedValue = deepMergeJson(currentValue, filteredPayload);
|
||||||
|
fs.writeFileSync(operation.destinationPath, formatJson(mergedValue), 'utf8');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation.kind === 'copy-file' && isMcpConfigPath(operation.destinationPath) && disabledServers.length > 0) {
|
||||||
|
const sourceConfig = readJsonObject(operation.sourcePath, 'MCP config');
|
||||||
|
const filteredConfig = filterMcpConfig(sourceConfig, disabledServers).config;
|
||||||
|
fs.writeFileSync(operation.destinationPath, formatJson(filteredConfig), 'utf8');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
fs.copyFileSync(operation.sourcePath, operation.destinationPath);
|
fs.copyFileSync(operation.sourcePath, operation.destinationPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,15 +161,6 @@ function applyInstallPlan(plan) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const writePlan of filteredMcpWrites) {
|
|
||||||
fs.mkdirSync(path.dirname(writePlan.destinationPath), { recursive: true });
|
|
||||||
fs.writeFileSync(
|
|
||||||
writePlan.destinationPath,
|
|
||||||
JSON.stringify(writePlan.filteredConfig, null, 2) + '\n',
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
writeInstallState(plan.installStatePath, plan.statePreview);
|
writeInstallState(plan.installStatePath, plan.statePreview);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -122,6 +122,15 @@ function runTests() {
|
|||||||
)),
|
)),
|
||||||
'Should preserve non-rule Cursor platform files'
|
'Should preserve non-rule Cursor platform files'
|
||||||
);
|
);
|
||||||
|
assert.ok(
|
||||||
|
plan.operations.some(operation => (
|
||||||
|
operation.sourceRelativePath === '.mcp.json'
|
||||||
|
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'mcp.json')
|
||||||
|
&& operation.kind === 'merge-json'
|
||||||
|
&& operation.strategy === 'merge-json'
|
||||||
|
)),
|
||||||
|
'Should materialize Cursor MCP config at the native project path'
|
||||||
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
plan.operations.some(operation => (
|
plan.operations.some(operation => (
|
||||||
operation.sourceRelativePath === '.cursor/rules/common-agents.md'
|
operation.sourceRelativePath === '.cursor/rules/common-agents.md'
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ function runTests() {
|
|||||||
const hooksJson = plan.operations.find(operation => (
|
const hooksJson = plan.operations.find(operation => (
|
||||||
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/hooks.json'
|
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/hooks.json'
|
||||||
));
|
));
|
||||||
|
const mcpJson = plan.operations.find(operation => (
|
||||||
|
normalizedRelativePath(operation.sourceRelativePath) === '.mcp.json'
|
||||||
|
));
|
||||||
const preserved = plan.operations.find(operation => (
|
const preserved = plan.operations.find(operation => (
|
||||||
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/rules/common-coding-style.md'
|
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/rules/common-coding-style.md'
|
||||||
));
|
));
|
||||||
@@ -100,6 +103,10 @@ function runTests() {
|
|||||||
assert.ok(hooksJson, 'Should preserve non-rule Cursor platform config files');
|
assert.ok(hooksJson, 'Should preserve non-rule Cursor platform config files');
|
||||||
assert.strictEqual(hooksJson.strategy, 'preserve-relative-path');
|
assert.strictEqual(hooksJson.strategy, 'preserve-relative-path');
|
||||||
assert.strictEqual(hooksJson.destinationPath, path.join(projectRoot, '.cursor', 'hooks.json'));
|
assert.strictEqual(hooksJson.destinationPath, path.join(projectRoot, '.cursor', 'hooks.json'));
|
||||||
|
assert.ok(mcpJson, 'Should materialize a Cursor MCP config from the shared root MCP config');
|
||||||
|
assert.strictEqual(mcpJson.kind, 'merge-json');
|
||||||
|
assert.strictEqual(mcpJson.strategy, 'merge-json');
|
||||||
|
assert.strictEqual(mcpJson.destinationPath, path.join(projectRoot, '.cursor', 'mcp.json'));
|
||||||
|
|
||||||
assert.ok(preserved, 'Should include flattened Cursor rule scaffold operations');
|
assert.ok(preserved, 'Should include flattened Cursor rule scaffold operations');
|
||||||
assert.strictEqual(preserved.strategy, 'flatten-copy');
|
assert.strictEqual(preserved.strategy, 'flatten-copy');
|
||||||
@@ -201,6 +208,14 @@ function runTests() {
|
|||||||
)),
|
)),
|
||||||
'Should preserve non-rule Cursor platform config files'
|
'Should preserve non-rule Cursor platform config files'
|
||||||
);
|
);
|
||||||
|
assert.ok(
|
||||||
|
plan.operations.some(operation => (
|
||||||
|
normalizedRelativePath(operation.sourceRelativePath) === '.mcp.json'
|
||||||
|
&& operation.kind === 'merge-json'
|
||||||
|
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'mcp.json')
|
||||||
|
)),
|
||||||
|
'Should materialize a project-level Cursor MCP config'
|
||||||
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!plan.operations.some(operation => (
|
!plan.operations.some(operation => (
|
||||||
operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'README.mdc')
|
operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'README.mdc')
|
||||||
|
|||||||
@@ -138,11 +138,19 @@ function runTests() {
|
|||||||
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')));
|
||||||
|
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'mcp.json')));
|
||||||
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks', 'session-start.js')));
|
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks', 'session-start.js')));
|
||||||
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'scripts', 'lib', 'utils.js')));
|
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'scripts', 'lib', 'utils.js')));
|
||||||
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'skills', 'tdd-workflow', 'SKILL.md')));
|
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'skills', 'tdd-workflow', 'SKILL.md')));
|
||||||
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'skills', 'coding-standards', 'SKILL.md')));
|
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'skills', 'coding-standards', 'SKILL.md')));
|
||||||
|
|
||||||
|
const hooksConfig = readJson(path.join(projectDir, '.cursor', 'hooks.json'));
|
||||||
|
const mcpConfig = readJson(path.join(projectDir, '.cursor', 'mcp.json'));
|
||||||
|
assert.strictEqual(hooksConfig.version, 1);
|
||||||
|
assert.ok(hooksConfig.hooks.sessionStart, 'Should keep Cursor sessionStart hooks');
|
||||||
|
assert.ok(mcpConfig.mcpServers.github, 'Should install shared MCP servers into Cursor');
|
||||||
|
assert.ok(mcpConfig.mcpServers.context7, 'Should include bundled documentation MCPs');
|
||||||
|
|
||||||
const statePath = path.join(projectDir, '.cursor', 'ecc-install-state.json');
|
const statePath = path.join(projectDir, '.cursor', 'ecc-install-state.json');
|
||||||
const state = readJson(statePath);
|
const state = readJson(statePath);
|
||||||
const normalizedProjectDir = fs.realpathSync(projectDir);
|
const normalizedProjectDir = fs.realpathSync(projectDir);
|
||||||
@@ -163,6 +171,35 @@ function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('installs Cursor MCP config by merging bundled servers into an existing mcp.json', () => {
|
||||||
|
const homeDir = createTempDir('install-apply-home-');
|
||||||
|
const projectDir = createTempDir('install-apply-project-');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cursorRoot = path.join(projectDir, '.cursor');
|
||||||
|
fs.mkdirSync(cursorRoot, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(cursorRoot, 'mcp.json'), JSON.stringify({
|
||||||
|
mcpServers: {
|
||||||
|
custom: {
|
||||||
|
command: 'node',
|
||||||
|
args: ['custom-mcp.js'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
|
const result = run(['--target', 'cursor', 'typescript'], { cwd: projectDir, homeDir });
|
||||||
|
assert.strictEqual(result.code, 0, result.stderr);
|
||||||
|
|
||||||
|
const mcpConfig = readJson(path.join(projectDir, '.cursor', 'mcp.json'));
|
||||||
|
assert.ok(mcpConfig.mcpServers.custom, 'Should preserve existing custom Cursor MCP servers');
|
||||||
|
assert.ok(mcpConfig.mcpServers.github, 'Should merge bundled GitHub MCP server');
|
||||||
|
assert.ok(mcpConfig.mcpServers.playwright, 'Should merge bundled Playwright MCP server');
|
||||||
|
} finally {
|
||||||
|
cleanup(homeDir);
|
||||||
|
cleanup(projectDir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('installs Antigravity configs and writes install-state', () => {
|
if (test('installs Antigravity configs and writes install-state', () => {
|
||||||
const homeDir = createTempDir('install-apply-home-');
|
const homeDir = createTempDir('install-apply-home-');
|
||||||
const projectDir = createTempDir('install-apply-project-');
|
const projectDir = createTempDir('install-apply-project-');
|
||||||
|
|||||||
Reference in New Issue
Block a user