add VS Code profile and tests
This commit is contained in:
@@ -5,4 +5,5 @@ export { codexProfile } from './codex.js';
|
|||||||
export { cursorProfile } from './cursor.js';
|
export { cursorProfile } from './cursor.js';
|
||||||
export { rooProfile } from './roo.js';
|
export { rooProfile } from './roo.js';
|
||||||
export { traeProfile } from './trae.js';
|
export { traeProfile } from './trae.js';
|
||||||
|
export { vscodeProfile } from './vscode.js';
|
||||||
export { windsurfProfile } from './windsurf.js';
|
export { windsurfProfile } from './windsurf.js';
|
||||||
|
|||||||
41
scripts/profiles/vscode.js
Normal file
41
scripts/profiles/vscode.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// VS Code conversion profile for rule-transformer
|
||||||
|
import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js';
|
||||||
|
|
||||||
|
// Create and export vscode profile using the base factory
|
||||||
|
export const vscodeProfile = createProfile({
|
||||||
|
name: 'vscode',
|
||||||
|
displayName: 'VS Code',
|
||||||
|
url: 'code.visualstudio.com',
|
||||||
|
docsUrl: 'code.visualstudio.com/docs',
|
||||||
|
profileDir: '.vscode', // MCP config location
|
||||||
|
rulesDir: '.github/instructions', // VS Code instructions location
|
||||||
|
mcpConfig: true,
|
||||||
|
mcpConfigName: 'mcp.json',
|
||||||
|
fileExtension: '.mdc',
|
||||||
|
targetExtension: '.md',
|
||||||
|
toolMappings: COMMON_TOOL_MAPPINGS.STANDARD, // VS Code uses standard tool names
|
||||||
|
customFileMap: {
|
||||||
|
'cursor_rules.mdc': 'vscode_rules.md' // Rename cursor_rules to vscode_rules
|
||||||
|
},
|
||||||
|
customReplacements: [
|
||||||
|
// Core VS Code directory structure changes
|
||||||
|
{ from: /\.cursor\/rules/g, to: '.github/instructions' },
|
||||||
|
{ from: /\.cursor\/mcp\.json/g, to: '.vscode/mcp.json' },
|
||||||
|
|
||||||
|
// Fix any remaining vscode/rules references that might be created during transformation
|
||||||
|
{ from: /\.vscode\/rules/g, to: '.github/instructions' },
|
||||||
|
|
||||||
|
// VS Code custom instructions format - use applyTo with quoted patterns instead of globs
|
||||||
|
{ from: /^globs:\s*(.+)$/gm, to: 'applyTo: "$1"' },
|
||||||
|
|
||||||
|
// Essential markdown link transformations for VS Code structure
|
||||||
|
{
|
||||||
|
from: /\[(.+?)\]\(mdc:\.cursor\/rules\/(.+?)\.mdc\)/g,
|
||||||
|
to: '[$1](.github/instructions/$2.md)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// VS Code specific terminology
|
||||||
|
{ from: /rules directory/g, to: 'instructions directory' },
|
||||||
|
{ from: /cursor rules/gi, to: 'VS Code instructions' }
|
||||||
|
]
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @typedef {'claude' | 'cline' | 'codex' | 'cursor' | 'roo' | 'trae' | 'windsurf'} RulesProfile
|
* @typedef {'claude' | 'cline' | 'codex' | 'cursor' | 'roo' | 'trae' | 'windsurf' | 'vscode'} RulesProfile
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
* - cursor: Cursor IDE rules
|
* - cursor: Cursor IDE rules
|
||||||
* - roo: Roo Code IDE rules
|
* - roo: Roo Code IDE rules
|
||||||
* - trae: Trae IDE rules
|
* - trae: Trae IDE rules
|
||||||
|
* - vscode: VS Code with GitHub Copilot integration
|
||||||
* - windsurf: Windsurf IDE rules
|
* - windsurf: Windsurf IDE rules
|
||||||
*
|
*
|
||||||
* To add a new rule profile:
|
* To add a new rule profile:
|
||||||
@@ -30,6 +31,7 @@ export const RULE_PROFILES = [
|
|||||||
'cursor',
|
'cursor',
|
||||||
'roo',
|
'roo',
|
||||||
'trae',
|
'trae',
|
||||||
|
'vscode',
|
||||||
'windsurf'
|
'windsurf'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -67,14 +67,8 @@ function getProfileDisplayName(name) {
|
|||||||
return profile?.displayName || name.charAt(0).toUpperCase() + name.slice(1);
|
return profile?.displayName || name.charAt(0).toUpperCase() + name.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamically generate availableRulesProfiles from RULE_PROFILES
|
// Note: Profile choices are now generated dynamically within runInteractiveProfilesSetup()
|
||||||
const availableRulesProfiles = RULE_PROFILES.map((name) => {
|
// to ensure proper alphabetical sorting and pagination configuration
|
||||||
const displayName = getProfileDisplayName(name);
|
|
||||||
return {
|
|
||||||
name: displayName,
|
|
||||||
value: name
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launches an interactive prompt for selecting which rule profiles to include in your project.
|
* Launches an interactive prompt for selecting which rule profiles to include in your project.
|
||||||
@@ -115,6 +109,7 @@ export async function runInteractiveProfilesSetup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
profileName,
|
||||||
displayName,
|
displayName,
|
||||||
description
|
description
|
||||||
};
|
};
|
||||||
@@ -142,11 +137,21 @@ export async function runInteractiveProfilesSetup() {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Generate choices in the same order as the display text above
|
||||||
|
const sortedChoices = profileDescriptions.map(
|
||||||
|
({ profileName, displayName }) => ({
|
||||||
|
name: displayName,
|
||||||
|
value: profileName
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const ruleProfilesQuestion = {
|
const ruleProfilesQuestion = {
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
name: 'ruleProfiles',
|
name: 'ruleProfiles',
|
||||||
message: 'Which rule profiles would you like to add to your project?',
|
message: 'Which rule profiles would you like to add to your project?',
|
||||||
choices: availableRulesProfiles,
|
choices: sortedChoices,
|
||||||
|
pageSize: sortedChoices.length, // Show all options without pagination
|
||||||
|
loop: false, // Disable loop scrolling
|
||||||
validate: (input) => input.length > 0 || 'You must select at least one.'
|
validate: (input) => input.length > 0 || 'You must select at least one.'
|
||||||
};
|
};
|
||||||
const { ruleProfiles } = await inquirer.prompt([ruleProfilesQuestion]);
|
const { ruleProfiles } = await inquirer.prompt([ruleProfilesQuestion]);
|
||||||
|
|||||||
@@ -5,18 +5,18 @@ import path from 'path';
|
|||||||
describe('MCP Configuration Validation', () => {
|
describe('MCP Configuration Validation', () => {
|
||||||
describe('Profile MCP Configuration Properties', () => {
|
describe('Profile MCP Configuration Properties', () => {
|
||||||
const expectedMcpConfigurations = {
|
const expectedMcpConfigurations = {
|
||||||
|
cline: {
|
||||||
|
shouldHaveMcp: false,
|
||||||
|
expectedDir: '.clinerules',
|
||||||
|
expectedConfigName: 'cline_mcp_settings.json',
|
||||||
|
expectedPath: '.clinerules/cline_mcp_settings.json'
|
||||||
|
},
|
||||||
cursor: {
|
cursor: {
|
||||||
shouldHaveMcp: true,
|
shouldHaveMcp: true,
|
||||||
expectedDir: '.cursor',
|
expectedDir: '.cursor',
|
||||||
expectedConfigName: 'mcp.json',
|
expectedConfigName: 'mcp.json',
|
||||||
expectedPath: '.cursor/mcp.json'
|
expectedPath: '.cursor/mcp.json'
|
||||||
},
|
},
|
||||||
windsurf: {
|
|
||||||
shouldHaveMcp: true,
|
|
||||||
expectedDir: '.windsurf',
|
|
||||||
expectedConfigName: 'mcp.json',
|
|
||||||
expectedPath: '.windsurf/mcp.json'
|
|
||||||
},
|
|
||||||
roo: {
|
roo: {
|
||||||
shouldHaveMcp: true,
|
shouldHaveMcp: true,
|
||||||
expectedDir: '.roo',
|
expectedDir: '.roo',
|
||||||
@@ -29,11 +29,17 @@ describe('MCP Configuration Validation', () => {
|
|||||||
expectedConfigName: 'trae_mcp_settings.json',
|
expectedConfigName: 'trae_mcp_settings.json',
|
||||||
expectedPath: '.trae/trae_mcp_settings.json'
|
expectedPath: '.trae/trae_mcp_settings.json'
|
||||||
},
|
},
|
||||||
cline: {
|
vscode: {
|
||||||
shouldHaveMcp: false,
|
shouldHaveMcp: true,
|
||||||
expectedDir: '.clinerules',
|
expectedDir: '.vscode',
|
||||||
expectedConfigName: 'cline_mcp_settings.json',
|
expectedConfigName: 'mcp.json',
|
||||||
expectedPath: '.clinerules/cline_mcp_settings.json'
|
expectedPath: '.vscode/mcp.json'
|
||||||
|
},
|
||||||
|
windsurf: {
|
||||||
|
shouldHaveMcp: true,
|
||||||
|
expectedDir: '.windsurf',
|
||||||
|
expectedConfigName: 'mcp.json',
|
||||||
|
expectedPath: '.windsurf/mcp.json'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,7 +104,7 @@ describe('MCP Configuration Validation', () => {
|
|||||||
|
|
||||||
describe('MCP Configuration File Names', () => {
|
describe('MCP Configuration File Names', () => {
|
||||||
test('should use standard mcp.json for MCP-enabled profiles', () => {
|
test('should use standard mcp.json for MCP-enabled profiles', () => {
|
||||||
const standardMcpProfiles = ['cursor', 'windsurf', 'roo'];
|
const standardMcpProfiles = ['cursor', 'roo', 'vscode', 'windsurf'];
|
||||||
standardMcpProfiles.forEach((profileName) => {
|
standardMcpProfiles.forEach((profileName) => {
|
||||||
const profile = getRulesProfile(profileName);
|
const profile = getRulesProfile(profileName);
|
||||||
expect(profile.mcpConfigName).toBe('mcp.json');
|
expect(profile.mcpConfigName).toBe('mcp.json');
|
||||||
@@ -162,12 +168,13 @@ describe('MCP Configuration Validation', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(mcpEnabledProfiles).toContain('cursor');
|
expect(mcpEnabledProfiles).toContain('cursor');
|
||||||
expect(mcpEnabledProfiles).toContain('windsurf');
|
|
||||||
expect(mcpEnabledProfiles).toContain('roo');
|
expect(mcpEnabledProfiles).toContain('roo');
|
||||||
expect(mcpEnabledProfiles).not.toContain('cline');
|
expect(mcpEnabledProfiles).toContain('vscode');
|
||||||
expect(mcpEnabledProfiles).not.toContain('trae');
|
expect(mcpEnabledProfiles).toContain('windsurf');
|
||||||
expect(mcpEnabledProfiles).not.toContain('claude');
|
expect(mcpEnabledProfiles).not.toContain('claude');
|
||||||
|
expect(mcpEnabledProfiles).not.toContain('cline');
|
||||||
expect(mcpEnabledProfiles).not.toContain('codex');
|
expect(mcpEnabledProfiles).not.toContain('codex');
|
||||||
|
expect(mcpEnabledProfiles).not.toContain('trae');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should provide all necessary information for MCP config creation', () => {
|
test('should provide all necessary information for MCP config creation', () => {
|
||||||
|
|||||||
311
tests/unit/profiles/rule-transformer-vscode.test.js
Normal file
311
tests/unit/profiles/rule-transformer-vscode.test.js
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
// Mock fs module before importing anything that uses it
|
||||||
|
jest.mock('fs', () => ({
|
||||||
|
readFileSync: jest.fn(),
|
||||||
|
writeFileSync: jest.fn(),
|
||||||
|
existsSync: jest.fn(),
|
||||||
|
mkdirSync: jest.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Import modules after mocking
|
||||||
|
import fs from 'fs';
|
||||||
|
import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js';
|
||||||
|
import { vscodeProfile } from '../../../scripts/profiles/vscode.js';
|
||||||
|
|
||||||
|
describe('VS Code Rule Transformer', () => {
|
||||||
|
// Set up spies on the mocked modules
|
||||||
|
const mockReadFileSync = jest.spyOn(fs, 'readFileSync');
|
||||||
|
const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync');
|
||||||
|
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||||
|
const mockMkdirSync = jest.spyOn(fs, 'mkdirSync');
|
||||||
|
const mockConsoleError = jest
|
||||||
|
.spyOn(console, 'error')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
// Setup default mocks
|
||||||
|
mockReadFileSync.mockReturnValue('');
|
||||||
|
mockWriteFileSync.mockImplementation(() => {});
|
||||||
|
mockExistsSync.mockReturnValue(true);
|
||||||
|
mockMkdirSync.mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly convert basic terms', () => {
|
||||||
|
const testContent = `---
|
||||||
|
description: Test Cursor rule for basic terms
|
||||||
|
globs: **/*
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a Cursor rule that references cursor.so and uses the word Cursor multiple times.
|
||||||
|
Also has references to .mdc files and cursor rules.`;
|
||||||
|
|
||||||
|
// Mock file read to return our test content
|
||||||
|
mockReadFileSync.mockReturnValue(testContent);
|
||||||
|
|
||||||
|
// Call the actual function
|
||||||
|
const result = convertRuleToProfileRule(
|
||||||
|
'source.mdc',
|
||||||
|
'target.md',
|
||||||
|
vscodeProfile
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the function succeeded
|
||||||
|
expect(result).toBe(true);
|
||||||
|
|
||||||
|
// Verify file operations were called correctly
|
||||||
|
expect(mockReadFileSync).toHaveBeenCalledWith('source.mdc', 'utf8');
|
||||||
|
expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Get the transformed content that was written
|
||||||
|
const writeCall = mockWriteFileSync.mock.calls[0];
|
||||||
|
const transformedContent = writeCall[1];
|
||||||
|
|
||||||
|
// Verify transformations
|
||||||
|
expect(transformedContent).toContain('VS Code');
|
||||||
|
expect(transformedContent).toContain('code.visualstudio.com');
|
||||||
|
expect(transformedContent).toContain('.md');
|
||||||
|
expect(transformedContent).toContain('vscode rules'); // "cursor rules" -> "vscode rules"
|
||||||
|
expect(transformedContent).toContain('applyTo: "**/*"'); // globs -> applyTo transformation
|
||||||
|
expect(transformedContent).not.toContain('cursor.so');
|
||||||
|
expect(transformedContent).not.toContain('Cursor rule');
|
||||||
|
expect(transformedContent).not.toContain('globs:');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly convert tool references', () => {
|
||||||
|
const testContent = `---
|
||||||
|
description: Test Cursor rule for tool references
|
||||||
|
globs: **/*
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
- Use the search tool to find code
|
||||||
|
- The edit_file tool lets you modify files
|
||||||
|
- run_command executes terminal commands
|
||||||
|
- use_mcp connects to external services`;
|
||||||
|
|
||||||
|
// Mock file read to return our test content
|
||||||
|
mockReadFileSync.mockReturnValue(testContent);
|
||||||
|
|
||||||
|
// Call the actual function
|
||||||
|
const result = convertRuleToProfileRule(
|
||||||
|
'source.mdc',
|
||||||
|
'target.md',
|
||||||
|
vscodeProfile
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the function succeeded
|
||||||
|
expect(result).toBe(true);
|
||||||
|
|
||||||
|
// Get the transformed content that was written
|
||||||
|
const writeCall = mockWriteFileSync.mock.calls[0];
|
||||||
|
const transformedContent = writeCall[1];
|
||||||
|
|
||||||
|
// Verify transformations (VS Code uses standard tool names, so no transformation)
|
||||||
|
expect(transformedContent).toContain('search tool');
|
||||||
|
expect(transformedContent).toContain('edit_file tool');
|
||||||
|
expect(transformedContent).toContain('run_command');
|
||||||
|
expect(transformedContent).toContain('use_mcp');
|
||||||
|
expect(transformedContent).toContain('applyTo: "**/*"'); // globs -> applyTo transformation
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly update file references and directory paths', () => {
|
||||||
|
const testContent = `---
|
||||||
|
description: Test Cursor rule for file references
|
||||||
|
globs: .cursor/rules/*.md
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
|
||||||
|
[taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).
|
||||||
|
Files are in the .cursor/rules directory and we should reference the rules directory.`;
|
||||||
|
|
||||||
|
// Mock file read to return our test content
|
||||||
|
mockReadFileSync.mockReturnValue(testContent);
|
||||||
|
|
||||||
|
// Call the actual function
|
||||||
|
const result = convertRuleToProfileRule(
|
||||||
|
'source.mdc',
|
||||||
|
'target.md',
|
||||||
|
vscodeProfile
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the function succeeded
|
||||||
|
expect(result).toBe(true);
|
||||||
|
|
||||||
|
// Get the transformed content that was written
|
||||||
|
const writeCall = mockWriteFileSync.mock.calls[0];
|
||||||
|
const transformedContent = writeCall[1];
|
||||||
|
|
||||||
|
// Verify transformations specific to VS Code
|
||||||
|
expect(transformedContent).toContain(
|
||||||
|
'applyTo: ".github/instructions/*.md"'
|
||||||
|
); // globs -> applyTo with path transformation
|
||||||
|
expect(transformedContent).toContain(
|
||||||
|
'(.github/instructions/taskmaster/dev_workflow.md)'
|
||||||
|
); // File path transformation
|
||||||
|
expect(transformedContent).toContain(
|
||||||
|
'(.github/instructions/taskmaster/taskmaster.md)'
|
||||||
|
); // File path transformation
|
||||||
|
expect(transformedContent).toContain('instructions directory'); // "rules directory" -> "instructions directory"
|
||||||
|
expect(transformedContent).not.toContain('(mdc:.cursor/rules/');
|
||||||
|
expect(transformedContent).not.toContain('.cursor/rules');
|
||||||
|
expect(transformedContent).not.toContain('globs:');
|
||||||
|
expect(transformedContent).not.toContain('rules directory');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform globs to applyTo with various patterns', () => {
|
||||||
|
const testContent = `---
|
||||||
|
description: Test VS Code applyTo transformation
|
||||||
|
globs: .cursor/rules/*.md
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Another section:
|
||||||
|
globs: **/*.ts
|
||||||
|
final: true
|
||||||
|
|
||||||
|
Last one:
|
||||||
|
globs: src/**/*
|
||||||
|
---`;
|
||||||
|
|
||||||
|
// Mock file read to return our test content
|
||||||
|
mockReadFileSync.mockReturnValue(testContent);
|
||||||
|
|
||||||
|
// Call the actual function
|
||||||
|
const result = convertRuleToProfileRule(
|
||||||
|
'source.mdc',
|
||||||
|
'target.md',
|
||||||
|
vscodeProfile
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the function succeeded
|
||||||
|
expect(result).toBe(true);
|
||||||
|
|
||||||
|
// Get the transformed content that was written
|
||||||
|
const writeCall = mockWriteFileSync.mock.calls[0];
|
||||||
|
const transformedContent = writeCall[1];
|
||||||
|
|
||||||
|
// Verify all globs transformations
|
||||||
|
expect(transformedContent).toContain(
|
||||||
|
'applyTo: ".github/instructions/*.md"'
|
||||||
|
); // Path transformation applied
|
||||||
|
expect(transformedContent).toContain('applyTo: "**/*.ts"'); // Pattern with quotes
|
||||||
|
expect(transformedContent).toContain('applyTo: "src/**/*"'); // Complex pattern with quotes
|
||||||
|
expect(transformedContent).not.toContain('globs:'); // No globs should remain
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle VS Code MCP configuration paths correctly', () => {
|
||||||
|
const testContent = `---
|
||||||
|
description: Test MCP configuration paths
|
||||||
|
globs: **/*
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
MCP configuration is at .cursor/mcp.json for Cursor.
|
||||||
|
The .cursor/rules directory contains rules.
|
||||||
|
Update your .cursor/mcp.json file accordingly.`;
|
||||||
|
|
||||||
|
// Mock file read to return our test content
|
||||||
|
mockReadFileSync.mockReturnValue(testContent);
|
||||||
|
|
||||||
|
// Call the actual function
|
||||||
|
const result = convertRuleToProfileRule(
|
||||||
|
'source.mdc',
|
||||||
|
'target.md',
|
||||||
|
vscodeProfile
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the function succeeded
|
||||||
|
expect(result).toBe(true);
|
||||||
|
|
||||||
|
// Get the transformed content that was written
|
||||||
|
const writeCall = mockWriteFileSync.mock.calls[0];
|
||||||
|
const transformedContent = writeCall[1];
|
||||||
|
|
||||||
|
// Verify MCP paths are correctly transformed
|
||||||
|
expect(transformedContent).toContain('.vscode/mcp.json'); // MCP config in .vscode
|
||||||
|
expect(transformedContent).toContain('.github/instructions'); // Rules/instructions in .github/instructions
|
||||||
|
expect(transformedContent).not.toContain('.cursor/mcp.json');
|
||||||
|
expect(transformedContent).not.toContain('.cursor/rules');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle file read errors', () => {
|
||||||
|
// Mock file read to throw an error
|
||||||
|
mockReadFileSync.mockImplementation(() => {
|
||||||
|
throw new Error('File not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call the actual function
|
||||||
|
const result = convertRuleToProfileRule(
|
||||||
|
'nonexistent.mdc',
|
||||||
|
'target.md',
|
||||||
|
vscodeProfile
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the function failed gracefully
|
||||||
|
expect(result).toBe(false);
|
||||||
|
|
||||||
|
// Verify writeFileSync was not called
|
||||||
|
expect(mockWriteFileSync).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Verify error was logged
|
||||||
|
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||||
|
'Error converting rule file: File not found'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle file write errors', () => {
|
||||||
|
const testContent = 'test content';
|
||||||
|
mockReadFileSync.mockReturnValue(testContent);
|
||||||
|
|
||||||
|
// Mock file write to throw an error
|
||||||
|
mockWriteFileSync.mockImplementation(() => {
|
||||||
|
throw new Error('Permission denied');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call the actual function
|
||||||
|
const result = convertRuleToProfileRule(
|
||||||
|
'source.mdc',
|
||||||
|
'target.md',
|
||||||
|
vscodeProfile
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the function failed gracefully
|
||||||
|
expect(result).toBe(false);
|
||||||
|
|
||||||
|
// Verify error was logged
|
||||||
|
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||||
|
'Error converting rule file: Permission denied'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create target directory if it does not exist', () => {
|
||||||
|
const testContent = 'test content';
|
||||||
|
mockReadFileSync.mockReturnValue(testContent);
|
||||||
|
|
||||||
|
// Mock directory doesn't exist initially
|
||||||
|
mockExistsSync.mockReturnValue(false);
|
||||||
|
|
||||||
|
// Call the actual function
|
||||||
|
convertRuleToProfileRule(
|
||||||
|
'source.mdc',
|
||||||
|
'.github/instructions/deep/path/target.md',
|
||||||
|
vscodeProfile
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify directory creation was called
|
||||||
|
expect(mockMkdirSync).toHaveBeenCalledWith(
|
||||||
|
'.github/instructions/deep/path',
|
||||||
|
{
|
||||||
|
recursive: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -19,6 +19,7 @@ describe('Rule Transformer - General', () => {
|
|||||||
'cursor',
|
'cursor',
|
||||||
'roo',
|
'roo',
|
||||||
'trae',
|
'trae',
|
||||||
|
'vscode',
|
||||||
'windsurf'
|
'windsurf'
|
||||||
];
|
];
|
||||||
expectedProfiles.forEach((profile) => {
|
expectedProfiles.forEach((profile) => {
|
||||||
@@ -181,6 +182,11 @@ describe('Rule Transformer - General', () => {
|
|||||||
mcpConfigName: null,
|
mcpConfigName: null,
|
||||||
expectedPath: null
|
expectedPath: null
|
||||||
},
|
},
|
||||||
|
cline: {
|
||||||
|
mcpConfig: false,
|
||||||
|
mcpConfigName: 'cline_mcp_settings.json',
|
||||||
|
expectedPath: '.clinerules/cline_mcp_settings.json'
|
||||||
|
},
|
||||||
codex: {
|
codex: {
|
||||||
mcpConfig: false,
|
mcpConfig: false,
|
||||||
mcpConfigName: null,
|
mcpConfigName: null,
|
||||||
@@ -191,11 +197,6 @@ describe('Rule Transformer - General', () => {
|
|||||||
mcpConfigName: 'mcp.json',
|
mcpConfigName: 'mcp.json',
|
||||||
expectedPath: '.cursor/mcp.json'
|
expectedPath: '.cursor/mcp.json'
|
||||||
},
|
},
|
||||||
windsurf: {
|
|
||||||
mcpConfig: true,
|
|
||||||
mcpConfigName: 'mcp.json',
|
|
||||||
expectedPath: '.windsurf/mcp.json'
|
|
||||||
},
|
|
||||||
roo: {
|
roo: {
|
||||||
mcpConfig: true,
|
mcpConfig: true,
|
||||||
mcpConfigName: 'mcp.json',
|
mcpConfigName: 'mcp.json',
|
||||||
@@ -206,10 +207,15 @@ describe('Rule Transformer - General', () => {
|
|||||||
mcpConfigName: 'trae_mcp_settings.json',
|
mcpConfigName: 'trae_mcp_settings.json',
|
||||||
expectedPath: '.trae/trae_mcp_settings.json'
|
expectedPath: '.trae/trae_mcp_settings.json'
|
||||||
},
|
},
|
||||||
cline: {
|
vscode: {
|
||||||
mcpConfig: false,
|
mcpConfig: true,
|
||||||
mcpConfigName: 'cline_mcp_settings.json',
|
mcpConfigName: 'mcp.json',
|
||||||
expectedPath: '.clinerules/cline_mcp_settings.json'
|
expectedPath: '.vscode/mcp.json'
|
||||||
|
},
|
||||||
|
windsurf: {
|
||||||
|
mcpConfig: true,
|
||||||
|
mcpConfigName: 'mcp.json',
|
||||||
|
expectedPath: '.windsurf/mcp.json'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
291
tests/unit/profiles/vscode-integration.test.js
Normal file
291
tests/unit/profiles/vscode-integration.test.js
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
import { jest } from '@jest/globals';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
|
// Mock external modules
|
||||||
|
jest.mock('child_process', () => ({
|
||||||
|
execSync: jest.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock console methods
|
||||||
|
jest.mock('console', () => ({
|
||||||
|
log: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
clear: jest.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('VS Code Integration', () => {
|
||||||
|
let tempDir;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
|
||||||
|
|
||||||
|
// Spy on fs methods
|
||||||
|
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
||||||
|
jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
|
||||||
|
if (filePath.toString().includes('mcp.json')) {
|
||||||
|
return JSON.stringify({
|
||||||
|
mcpServers: {
|
||||||
|
'task-master-ai': {
|
||||||
|
command: 'node',
|
||||||
|
args: ['mcp-server/src/index.js']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (filePath.toString().includes('instructions')) {
|
||||||
|
return 'VS Code instruction content';
|
||||||
|
}
|
||||||
|
return '{}';
|
||||||
|
});
|
||||||
|
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
|
||||||
|
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up the temporary directory
|
||||||
|
try {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error cleaning up: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test function that simulates the createProjectStructure behavior for VS Code files
|
||||||
|
function mockCreateVSCodeStructure() {
|
||||||
|
// Create .vscode directory for MCP configuration
|
||||||
|
fs.mkdirSync(path.join(tempDir, '.vscode'), { recursive: true });
|
||||||
|
|
||||||
|
// Create .github/instructions directory for VS Code custom instructions
|
||||||
|
fs.mkdirSync(path.join(tempDir, '.github', 'instructions'), {
|
||||||
|
recursive: true
|
||||||
|
});
|
||||||
|
fs.mkdirSync(path.join(tempDir, '.github', 'instructions', 'taskmaster'), {
|
||||||
|
recursive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create MCP configuration file
|
||||||
|
const mcpConfig = {
|
||||||
|
mcpServers: {
|
||||||
|
'task-master-ai': {
|
||||||
|
command: 'node',
|
||||||
|
args: ['mcp-server/src/index.js'],
|
||||||
|
env: {
|
||||||
|
PROJECT_ROOT: process.cwd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempDir, '.vscode', 'mcp.json'),
|
||||||
|
JSON.stringify(mcpConfig, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create sample instruction files
|
||||||
|
const instructionFiles = [
|
||||||
|
'vscode_rules.md',
|
||||||
|
'dev_workflow.md',
|
||||||
|
'self_improve.md'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of instructionFiles) {
|
||||||
|
const content = `---
|
||||||
|
description: VS Code instruction for ${file}
|
||||||
|
applyTo: "**/*.ts,**/*.tsx,**/*.js,**/*.jsx"
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# ${file.replace('.md', '').replace('_', ' ').toUpperCase()}
|
||||||
|
|
||||||
|
This is a VS Code custom instruction file.`;
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempDir, '.github', 'instructions', file),
|
||||||
|
content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create taskmaster subdirectory with additional instructions
|
||||||
|
const taskmasterFiles = ['taskmaster.md', 'commands.md', 'architecture.md'];
|
||||||
|
|
||||||
|
for (const file of taskmasterFiles) {
|
||||||
|
const content = `---
|
||||||
|
description: Task Master specific instruction for ${file}
|
||||||
|
applyTo: "**/*.ts,**/*.js"
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# ${file.replace('.md', '').toUpperCase()}
|
||||||
|
|
||||||
|
Task Master specific VS Code instruction.`;
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempDir, '.github', 'instructions', 'taskmaster', file),
|
||||||
|
content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('creates all required VS Code directories', () => {
|
||||||
|
// Act
|
||||||
|
mockCreateVSCodeStructure();
|
||||||
|
|
||||||
|
// Assert - .vscode directory for MCP config
|
||||||
|
expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.vscode'), {
|
||||||
|
recursive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert - .github/instructions directory for custom instructions
|
||||||
|
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||||
|
path.join(tempDir, '.github', 'instructions'),
|
||||||
|
{ recursive: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert - taskmaster subdirectory
|
||||||
|
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||||
|
path.join(tempDir, '.github', 'instructions', 'taskmaster'),
|
||||||
|
{ recursive: true }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('creates VS Code MCP configuration file', () => {
|
||||||
|
// Act
|
||||||
|
mockCreateVSCodeStructure();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const expectedMcpPath = path.join(tempDir, '.vscode', 'mcp.json');
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||||
|
expectedMcpPath,
|
||||||
|
expect.stringContaining('task-master-ai')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('creates VS Code instruction files with applyTo patterns', () => {
|
||||||
|
// Act
|
||||||
|
mockCreateVSCodeStructure();
|
||||||
|
|
||||||
|
// Assert main instruction files
|
||||||
|
const mainInstructionFiles = [
|
||||||
|
'vscode_rules.md',
|
||||||
|
'dev_workflow.md',
|
||||||
|
'self_improve.md'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of mainInstructionFiles) {
|
||||||
|
const expectedPath = path.join(tempDir, '.github', 'instructions', file);
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||||
|
expectedPath,
|
||||||
|
expect.stringContaining('applyTo:')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('creates taskmaster specific instruction files', () => {
|
||||||
|
// Act
|
||||||
|
mockCreateVSCodeStructure();
|
||||||
|
|
||||||
|
// Assert taskmaster subdirectory files
|
||||||
|
const taskmasterFiles = ['taskmaster.md', 'commands.md', 'architecture.md'];
|
||||||
|
|
||||||
|
for (const file of taskmasterFiles) {
|
||||||
|
const expectedPath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.github',
|
||||||
|
'instructions',
|
||||||
|
'taskmaster',
|
||||||
|
file
|
||||||
|
);
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||||
|
expectedPath,
|
||||||
|
expect.stringContaining('applyTo:')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('VS Code instruction files use applyTo instead of globs', () => {
|
||||||
|
// Act
|
||||||
|
mockCreateVSCodeStructure();
|
||||||
|
|
||||||
|
// Get all the writeFileSync calls for .md files
|
||||||
|
const mdFileWrites = fs.writeFileSync.mock.calls.filter((call) =>
|
||||||
|
call[0].toString().endsWith('.md')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert that all .md files contain applyTo and not globs
|
||||||
|
for (const writeCall of mdFileWrites) {
|
||||||
|
const content = writeCall[1];
|
||||||
|
expect(content).toContain('applyTo:');
|
||||||
|
expect(content).not.toContain('globs:');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('MCP configuration includes correct structure for VS Code', () => {
|
||||||
|
// Act
|
||||||
|
mockCreateVSCodeStructure();
|
||||||
|
|
||||||
|
// Get the MCP config write call
|
||||||
|
const mcpConfigWrite = fs.writeFileSync.mock.calls.find((call) =>
|
||||||
|
call[0].toString().includes('mcp.json')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mcpConfigWrite).toBeDefined();
|
||||||
|
|
||||||
|
const mcpContent = mcpConfigWrite[1];
|
||||||
|
const mcpConfig = JSON.parse(mcpContent);
|
||||||
|
|
||||||
|
// Assert MCP structure
|
||||||
|
expect(mcpConfig).toHaveProperty('mcpServers');
|
||||||
|
expect(mcpConfig.mcpServers).toHaveProperty('task-master-ai');
|
||||||
|
expect(mcpConfig.mcpServers['task-master-ai']).toHaveProperty(
|
||||||
|
'command',
|
||||||
|
'node'
|
||||||
|
);
|
||||||
|
expect(mcpConfig.mcpServers['task-master-ai']).toHaveProperty('args');
|
||||||
|
expect(mcpConfig.mcpServers['task-master-ai'].args).toContain(
|
||||||
|
'mcp-server/src/index.js'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('directory structure follows VS Code conventions', () => {
|
||||||
|
// Act
|
||||||
|
mockCreateVSCodeStructure();
|
||||||
|
|
||||||
|
// Assert the specific directory structure VS Code expects
|
||||||
|
const expectedDirs = [
|
||||||
|
path.join(tempDir, '.vscode'),
|
||||||
|
path.join(tempDir, '.github', 'instructions'),
|
||||||
|
path.join(tempDir, '.github', 'instructions', 'taskmaster')
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const dir of expectedDirs) {
|
||||||
|
expect(fs.mkdirSync).toHaveBeenCalledWith(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('instruction files contain VS Code specific formatting', () => {
|
||||||
|
// Act
|
||||||
|
mockCreateVSCodeStructure();
|
||||||
|
|
||||||
|
// Get a sample instruction file write
|
||||||
|
const instructionWrite = fs.writeFileSync.mock.calls.find((call) =>
|
||||||
|
call[0].toString().includes('vscode_rules.md')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(instructionWrite).toBeDefined();
|
||||||
|
|
||||||
|
const content = instructionWrite[1];
|
||||||
|
|
||||||
|
// Assert VS Code specific patterns
|
||||||
|
expect(content).toContain('---'); // YAML frontmatter
|
||||||
|
expect(content).toContain('description:');
|
||||||
|
expect(content).toContain('applyTo:');
|
||||||
|
expect(content).toContain('alwaysApply:');
|
||||||
|
expect(content).toContain('**/*.ts'); // File patterns in quotes
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user