combine to /src/utils/profiles.js; add codex and claude code profiles

This commit is contained in:
Joe Danziger
2025-05-27 15:45:08 -04:00
parent 9681c9171c
commit 08ad455463
20 changed files with 587 additions and 253 deletions

View File

@@ -18,7 +18,7 @@ import { RULES_ACTIONS } from '../../../../src/constants/rules-actions.js';
import {
wouldRemovalLeaveNoProfiles,
getInstalledProfiles
} from '../../../../src/utils/profile-detection.js';
} from '../../../../src/utils/profiles.js';
import path from 'path';
import fs from 'fs';

View File

@@ -29,7 +29,7 @@ import {
convertAllRulesToProfileRules,
getRulesProfile
} from '../src/utils/rule-transformer.js';
import { runInteractiveRulesSetup } from '../src/utils/rules-setup.js';
import { runInteractiveRulesSetup } from '../src/utils/profiles.js';
import { execSync } from 'child_process';
const __filename = fileURLToPath(import.meta.url);

View File

@@ -75,7 +75,7 @@ import {
import {
wouldRemovalLeaveNoProfiles,
getInstalledProfiles
} from '../../src/utils/profile-detection.js';
} from '../../src/utils/profiles.js';
import { initializeProject } from '../init.js';
import {
@@ -102,7 +102,13 @@ import {
isValidProfile,
getRulesProfile
} from '../../src/utils/rule-transformer.js';
import { runInteractiveRulesSetup } from '../../src/utils/rules-setup.js';
import {
runInteractiveRulesSetup,
generateProfileSummary,
categorizeProfileResults,
generateProfileRemovalSummary,
categorizeRemovalResults
} from '../../src/utils/profiles.js';
/**
* Runs the interactive setup process for model configuration.
@@ -2701,7 +2707,7 @@ Examples:
for (const profile of selectedRulesProfiles) {
if (!isValidProfile(profile)) {
console.warn(
`Rules profile for "${profile}" not found. Valid profiles: ${RULE_PROFILES.join(', ')}. Skipping.`
`Rule profile for "${profile}" not found. Valid profiles: ${RULE_PROFILES.join(', ')}. Skipping.`
);
continue;
}
@@ -2713,18 +2719,15 @@ Examples:
if (typeof profileConfig.onAddRulesProfile === 'function') {
profileConfig.onAddRulesProfile(projectDir);
}
console.log(
chalk.green(
`Summary for ${profile}: ${addResult.success} rules added, ${addResult.failed} failed.`
)
);
console.log(chalk.green(generateProfileSummary(profile, addResult)));
}
return;
}
if (!profiles || profiles.length === 0) {
console.error(
'Please specify at least one rules profile (e.g., windsurf, roo).'
'Please specify at least one rule profile (e.g., windsurf, roo).'
);
process.exit(1);
}
@@ -2760,7 +2763,7 @@ Examples:
for (const profile of expandedProfiles) {
if (!isValidProfile(profile)) {
console.warn(
`Rules profile for "${profile}" not found. Valid profiles: ${RULE_PROFILES.join(', ')}. Skipping.`
`Rule profile for "${profile}" not found. Valid profiles: ${RULE_PROFILES.join(', ')}. Skipping.`
);
continue;
}
@@ -2786,16 +2789,14 @@ Examples:
failed: addResult.failed
});
console.log(
chalk.green(
`Summary for ${profile}: ${addResult.success} rules added, ${addResult.failed} failed.`
)
);
console.log(chalk.green(generateProfileSummary(profile, addResult)));
} else if (action === RULES_ACTIONS.REMOVE) {
console.log(chalk.blue(`Removing rules for profile: ${profile}...`));
const result = removeProfileRules(projectDir, profileConfig);
removalResults.push(result);
console.log(chalk.blue(`Completed removal for profile: ${profile}`));
console.log(
chalk.green(generateProfileRemovalSummary(profile, result))
);
} else {
console.error(
`Unknown action. Use "${RULES_ACTIONS.ADD}" or "${RULES_ACTIONS.REMOVE}".`
@@ -2806,67 +2807,91 @@ Examples:
// Print summary for additions
if (action === RULES_ACTIONS.ADD && addResults.length > 0) {
const totalSuccess = addResults.reduce((sum, r) => sum + r.success, 0);
const totalFailed = addResults.reduce((sum, r) => sum + r.failed, 0);
const successfulProfiles = addResults
.filter((r) => r.success > 0)
.map((r) => r.profileName);
const {
allSuccessfulProfiles,
totalSuccess,
totalFailed,
simpleProfiles
} = categorizeProfileResults(addResults);
if (successfulProfiles.length > 0) {
if (allSuccessfulProfiles.length > 0) {
console.log(
chalk.green(
`\nSuccessfully added rules for: ${successfulProfiles.join(', ')}`
)
);
console.log(
chalk.green(
`Total: ${totalSuccess} rules added, ${totalFailed} failed.`
`\nSuccessfully added rules for: ${allSuccessfulProfiles.join(', ')}`
)
);
// Create a more descriptive summary
if (totalSuccess > 0 && simpleProfiles.length > 0) {
console.log(
chalk.green(
`Total: ${totalSuccess} rules added, ${totalFailed} failed, ${simpleProfiles.length} integration guide(s) copied.`
)
);
} else if (totalSuccess > 0) {
console.log(
chalk.green(
`Total: ${totalSuccess} rules added, ${totalFailed} failed.`
)
);
} else if (simpleProfiles.length > 0) {
console.log(
chalk.green(
`Total: ${simpleProfiles.length} integration guide(s) copied.`
)
);
}
}
}
// Print summary for removals
if (action === RULES_ACTIONS.REMOVE) {
const successes = removalResults
.filter((r) => r.success)
.map((r) => r.profileName);
const skipped = removalResults
.filter((r) => r.skipped)
.map((r) => r.profileName);
const errors = removalResults.filter(
(r) => r.error && !r.success && !r.skipped
);
const withNotices = removalResults.filter((r) => r.notice);
if (action === RULES_ACTIONS.REMOVE && removalResults.length > 0) {
const {
successfulRemovals,
skippedRemovals,
failedRemovals,
removalsWithNotices
} = categorizeRemovalResults(removalResults);
if (successes.length > 0) {
if (successfulRemovals.length > 0) {
console.log(
chalk.green(
`Successfully removed Task Master rules: ${successes.join(', ')}`
`\nSuccessfully removed rules for: ${successfulRemovals.join(', ')}`
)
);
}
if (skipped.length > 0) {
if (skippedRemovals.length > 0) {
console.log(
chalk.yellow(
`Skipped (default or protected): ${skipped.join(', ')}`
`Skipped (default or protected): ${skippedRemovals.join(', ')}`
)
);
}
if (errors.length > 0) {
errors.forEach((r) => {
console.log(
chalk.red(`Error removing ${r.profileName}: ${r.error}`)
);
if (failedRemovals.length > 0) {
console.log(chalk.red('\nErrors occurred:'));
failedRemovals.forEach((r) => {
console.log(chalk.red(` ${r.profileName}: ${r.error}`));
});
}
// Display notices about preserved files/configurations
if (withNotices.length > 0) {
if (removalsWithNotices.length > 0) {
console.log(chalk.cyan('\nNotices:'));
withNotices.forEach((r) => {
removalsWithNotices.forEach((r) => {
console.log(chalk.cyan(` ${r.profileName}: ${r.notice}`));
});
}
// Overall summary
const totalProcessed = removalResults.length;
const totalSuccessful = successfulRemovals.length;
const totalSkipped = skippedRemovals.length;
const totalFailed = failedRemovals.length;
console.log(
chalk.blue(
`\nTotal: ${totalProcessed} profile(s) processed - ${totalSuccessful} removed, ${totalSkipped} skipped, ${totalFailed} failed.`
)
);
}
});

View File

@@ -0,0 +1,62 @@
// Claude Code profile for rule-transformer
import path from 'path';
import fs from 'fs';
import { isSilentMode, log } from '../modules/utils.js';
// Lifecycle functions for Claude Code profile
function onAddRulesProfile(targetDir) {
const sourceFile = path.join(process.cwd(), 'assets', 'AGENTS.md');
const destFile = path.join(targetDir, 'CLAUDE.md');
if (fs.existsSync(sourceFile)) {
try {
fs.copyFileSync(sourceFile, destFile);
log('debug', `[Claude] Copied AGENTS.md to ${destFile}`);
} catch (err) {
log('debug', `[Claude] Failed to copy AGENTS.md: ${err.message}`);
}
} else {
log('debug', `[Claude] AGENTS.md not found at ${sourceFile}`);
}
}
function onRemoveRulesProfile(targetDir) {
log('debug', `[Claude] onRemoveRulesProfile called for ${targetDir}`);
const claudeFile = path.join(targetDir, 'CLAUDE.md');
if (fs.existsSync(claudeFile)) {
try {
fs.rmSync(claudeFile, { force: true });
log('debug', `[Claude] Removed CLAUDE.md from ${targetDir}`);
} catch (err) {
log('debug', `[Claude] Failed to remove CLAUDE.md: ${err.message}`);
}
}
log('debug', `[Claude] onRemoveRulesProfile completed for ${targetDir}`);
}
function onPostConvertRulesProfile(targetDir) {
onAddRulesProfile(targetDir);
}
// Simple filename function
function getTargetRuleFilename(sourceFilename) {
return sourceFilename;
}
// Simple profile configuration - bypasses base-profile system
export const claudeProfile = {
profileName: 'claude',
displayName: 'Claude Code',
profileDir: '.', // Root directory
rulesDir: '.', // No rules directory needed
mcpConfig: false, // No MCP config needed
mcpConfigName: null,
mcpConfigPath: null,
conversionConfig: {},
fileMap: {},
globalReplacements: [],
getTargetRuleFilename,
onAddRulesProfile,
onRemoveRulesProfile,
onPostConvertRulesProfile
};

62
scripts/profiles/codex.js Normal file
View File

@@ -0,0 +1,62 @@
// Codex profile for rule-transformer
import path from 'path';
import fs from 'fs';
import { isSilentMode, log } from '../modules/utils.js';
// Lifecycle functions for Codex profile
function onAddRulesProfile(targetDir) {
const sourceFile = path.join(process.cwd(), 'assets', 'AGENTS.md');
const destFile = path.join(targetDir, 'AGENTS.md');
if (fs.existsSync(sourceFile)) {
try {
fs.copyFileSync(sourceFile, destFile);
log('debug', `[Codex] Copied AGENTS.md to ${destFile}`);
} catch (err) {
log('debug', `[Codex] Failed to copy AGENTS.md: ${err.message}`);
}
} else {
log('debug', `[Codex] AGENTS.md not found at ${sourceFile}`);
}
}
function onRemoveRulesProfile(targetDir) {
log('debug', `[Codex] onRemoveRulesProfile called for ${targetDir}`);
const agentsFile = path.join(targetDir, 'AGENTS.md');
if (fs.existsSync(agentsFile)) {
try {
fs.rmSync(agentsFile, { force: true });
log('debug', `[Codex] Removed AGENTS.md from ${targetDir}`);
} catch (err) {
log('debug', `[Codex] Failed to remove AGENTS.md: ${err.message}`);
}
}
log('debug', `[Codex] onRemoveRulesProfile completed for ${targetDir}`);
}
function onPostConvertRulesProfile(targetDir) {
onAddRulesProfile(targetDir);
}
// Simple filename function
function getTargetRuleFilename(sourceFilename) {
return sourceFilename;
}
// Simple profile configuration - bypasses base-profile system
export const codexProfile = {
profileName: 'codex',
displayName: 'Codex',
profileDir: '.', // Root directory
rulesDir: '.', // No rules directory needed
mcpConfig: false, // No MCP config needed
mcpConfigName: null,
mcpConfigPath: null,
conversionConfig: {},
fileMap: {},
globalReplacements: [],
getTargetRuleFilename,
onAddRulesProfile,
onRemoveRulesProfile,
onPostConvertRulesProfile
};

View File

@@ -1,6 +1,8 @@
// Profile exports for centralized importing
export * as clineProfile from './cline.js';
export * as cursorProfile from './cursor.js';
export * as rooProfile from './roo.js';
export * as traeProfile from './trae.js';
export * as windsurfProfile from './windsurf.js';
export { claudeProfile } from './claude.js';
export { clineProfile } from './cline.js';
export { codexProfile } from './codex.js';
export { cursorProfile } from './cursor.js';
export { rooProfile } from './roo.js';
export { traeProfile } from './trae.js';
export { windsurfProfile } from './windsurf.js';

View File

@@ -1,5 +1,5 @@
/**
* @typedef {'cline' | 'cursor' | 'roo' | 'trae' | 'windsurf'} RulesProfile
* @typedef {'claude' | 'cline' | 'codex' | 'cursor' | 'roo' | 'trae' | 'windsurf'} RulesProfile
*/
/**
@@ -10,8 +10,10 @@
*
* @type {RulesProfile[]}
* @description Defines possible rule profile sets:
* - claude: Claude Code integration
* - cline: Cline IDE rules
* - cursor: Cursor IDE rules (default)
* - codex: Codex integration
* - cursor: Cursor IDE rules
* - roo: Roo Code IDE rules
* - trae: Trae IDE rules
* - windsurf: Windsurf IDE rules
@@ -21,7 +23,15 @@
* 2. Create a profile file in scripts/profiles/{profile}.js
* 3. Export it as {profile}Profile in scripts/profiles/index.js
*/
export const RULE_PROFILES = ['cline', 'cursor', 'roo', 'trae', 'windsurf'];
export const RULE_PROFILES = [
'claude',
'cline',
'codex',
'cursor',
'roo',
'trae',
'windsurf'
];
/**
* Check if a given rule profile is valid

View File

@@ -1,48 +0,0 @@
/**
* Profile Detection Utility
* Helper functions to detect existing profiles in a project
*/
import fs from 'fs';
import path from 'path';
import { RULE_PROFILES } from '../constants/profiles.js';
import { getRulesProfile } from './rule-transformer.js';
/**
* Detect which profiles are currently installed in the project
* @param {string} projectRoot - Project root directory
* @returns {string[]} Array of installed profile names
*/
export function getInstalledProfiles(projectRoot) {
const installedProfiles = [];
for (const profileName of RULE_PROFILES) {
const profileConfig = getRulesProfile(profileName);
if (!profileConfig) continue;
// Check if the profile directory exists
const profileDir = path.join(projectRoot, profileConfig.profileDir);
const rulesDir = path.join(projectRoot, profileConfig.rulesDir);
// A profile is considered installed if either the profile dir or rules dir exists
if (fs.existsSync(profileDir) || fs.existsSync(rulesDir)) {
installedProfiles.push(profileName);
}
}
return installedProfiles;
}
/**
* Check if removing the specified profiles would result in no profiles remaining
* @param {string} projectRoot - Project root directory
* @param {string[]} profilesToRemove - Array of profile names to remove
* @returns {boolean} True if removal would result in no profiles remaining
*/
export function wouldRemovalLeaveNoProfiles(projectRoot, profilesToRemove) {
const installedProfiles = getInstalledProfiles(projectRoot);
const remainingProfiles = installedProfiles.filter(
(profile) => !profilesToRemove.includes(profile)
);
return remainingProfiles.length === 0 && installedProfiles.length > 0;
}

228
src/utils/profiles.js Normal file
View File

@@ -0,0 +1,228 @@
/**
* Profiles Utility
* Consolidated utilities for profile detection, setup, and summary generation
*/
import fs from 'fs';
import path from 'path';
import readline from 'readline';
import inquirer from 'inquirer';
import chalk from 'chalk';
import { log } from '../../scripts/modules/utils.js';
import { getRulesProfile } from './rule-transformer.js';
import { RULE_PROFILES } from '../constants/profiles.js';
// =============================================================================
// PROFILE DETECTION
// =============================================================================
/**
* Detect which profiles are currently installed in the project
* @param {string} projectRoot - Project root directory
* @returns {string[]} Array of installed profile names
*/
export function getInstalledProfiles(projectRoot) {
const installedProfiles = [];
for (const profileName of RULE_PROFILES) {
const profileConfig = getRulesProfile(profileName);
if (!profileConfig) continue;
// Check if the profile directory exists
const profileDir = path.join(projectRoot, profileConfig.profileDir);
const rulesDir = path.join(projectRoot, profileConfig.rulesDir);
// A profile is considered installed if either the profile dir or rules dir exists
if (fs.existsSync(profileDir) || fs.existsSync(rulesDir)) {
installedProfiles.push(profileName);
}
}
return installedProfiles;
}
/**
* Check if removing the specified profiles would result in no profiles remaining
* @param {string} projectRoot - Project root directory
* @param {string[]} profilesToRemove - Array of profile names to remove
* @returns {boolean} True if removal would result in no profiles remaining
*/
export function wouldRemovalLeaveNoProfiles(projectRoot, profilesToRemove) {
const installedProfiles = getInstalledProfiles(projectRoot);
const remainingProfiles = installedProfiles.filter(
(profile) => !profilesToRemove.includes(profile)
);
return remainingProfiles.length === 0 && installedProfiles.length > 0;
}
// =============================================================================
// PROFILE SETUP
// =============================================================================
/**
* Get the display name for a profile
*/
function getProfileDisplayName(name) {
const profile = getRulesProfile(name);
return profile?.displayName || name.charAt(0).toUpperCase() + name.slice(1);
}
// Dynamically generate availableRulesProfiles from RULE_PROFILES
const availableRulesProfiles = RULE_PROFILES.map((name) => {
const displayName = getProfileDisplayName(name);
return {
name: displayName,
value: name
};
});
/**
* Launches an interactive prompt for selecting which rule profiles to include in your project.
*
* This function dynamically lists all available profiles (from RULE_PROFILES) and presents them as checkboxes.
* The user must select at least one profile (no defaults are pre-selected). The result is an array of selected profile names.
*
* Used by both project initialization (init) and the CLI 'task-master rules setup' command.
*
* @returns {Promise<string[]>} Array of selected profile names (e.g., ['cursor', 'windsurf'])
*/
export async function runInteractiveRulesSetup() {
console.log(
chalk.cyan(
'\nRule profiles help enforce best practices and conventions for Task Master.'
)
);
const rulesProfilesQuestion = {
type: 'checkbox',
name: 'rulesProfiles',
message: 'Which tools would you like rule profiles included for?',
choices: availableRulesProfiles,
validate: (input) => input.length > 0 || 'You must select at least one.'
};
const { rulesProfiles } = await inquirer.prompt([rulesProfilesQuestion]);
return rulesProfiles;
}
// =============================================================================
// PROFILE SUMMARY
// =============================================================================
/**
* Generate appropriate summary message for a profile based on its type
* @param {string} profileName - Name of the profile
* @param {Object} addResult - Result object with success/failed counts
* @returns {string} Formatted summary message
*/
export function generateProfileSummary(profileName, addResult) {
const profileConfig = getRulesProfile(profileName);
const isSimpleProfile = Object.keys(profileConfig.fileMap).length === 0;
if (isSimpleProfile) {
// Simple profiles like Claude and Codex only copy AGENTS.md
const targetFileName = profileName === 'claude' ? 'CLAUDE.md' : 'AGENTS.md';
return `Summary for ${profileName}: Integration guide copied to ${targetFileName}`;
} else {
return `Summary for ${profileName}: ${addResult.success} rules added, ${addResult.failed} failed.`;
}
}
/**
* Generate appropriate summary message for profile removal
* @param {string} profileName - Name of the profile
* @param {Object} removeResult - Result object from removal operation
* @returns {string} Formatted summary message
*/
export function generateProfileRemovalSummary(profileName, removeResult) {
const profileConfig = getRulesProfile(profileName);
const isSimpleProfile = Object.keys(profileConfig.fileMap).length === 0;
if (removeResult.skipped) {
return `Summary for ${profileName}: Skipped (default or protected files)`;
}
if (removeResult.error && !removeResult.success) {
return `Summary for ${profileName}: Failed to remove - ${removeResult.error}`;
}
if (isSimpleProfile) {
// Simple profiles like Claude and Codex only have an integration guide
const targetFileName = profileName === 'claude' ? 'CLAUDE.md' : 'AGENTS.md';
return `Summary for ${profileName}: Integration guide (${targetFileName}) removed`;
} else {
// Full profiles have rules directories and potentially MCP configs
let summary = `Summary for ${profileName}: Rules directory removed`;
if (removeResult.notice) {
summary += ` (${removeResult.notice})`;
}
return summary;
}
}
/**
* Categorize profiles and generate final summary statistics
* @param {Array} addResults - Array of add result objects
* @returns {Object} Object with categorized profiles and totals
*/
export function categorizeProfileResults(addResults) {
const successfulProfiles = [];
const simpleProfiles = [];
let totalSuccess = 0;
let totalFailed = 0;
addResults.forEach((r) => {
totalSuccess += r.success;
totalFailed += r.failed;
const profileConfig = getRulesProfile(r.profileName);
const isSimpleProfile = Object.keys(profileConfig.fileMap).length === 0;
if (isSimpleProfile) {
// Simple profiles are successful if they completed without error
simpleProfiles.push(r.profileName);
} else if (r.success > 0) {
// Full profiles are successful if they added rules
successfulProfiles.push(r.profileName);
}
});
return {
successfulProfiles,
simpleProfiles,
allSuccessfulProfiles: [...successfulProfiles, ...simpleProfiles],
totalSuccess,
totalFailed
};
}
/**
* Categorize removal results and generate final summary statistics
* @param {Array} removalResults - Array of removal result objects
* @returns {Object} Object with categorized removal results
*/
export function categorizeRemovalResults(removalResults) {
const successfulRemovals = [];
const skippedRemovals = [];
const failedRemovals = [];
const removalsWithNotices = [];
removalResults.forEach((result) => {
if (result.success) {
successfulRemovals.push(result.profileName);
} else if (result.skipped) {
skippedRemovals.push(result.profileName);
} else if (result.error) {
failedRemovals.push(result);
}
if (result.notice) {
removalsWithNotices.push(result);
}
});
return {
successfulRemovals,
skippedRemovals,
failedRemovals,
removalsWithNotices
};
}

View File

@@ -299,7 +299,23 @@ export function removeProfileRules(projectDir, profile) {
};
try {
// Check if profile directory exists at all
// Handle simple profiles (Claude, Codex) that just copy files to root
const isSimpleProfile = Object.keys(profile.fileMap).length === 0;
if (isSimpleProfile) {
// For simple profiles, just call their removal hook and return
if (typeof profile.onRemoveRulesProfile === 'function') {
profile.onRemoveRulesProfile(projectDir);
}
result.success = true;
log(
'debug',
`[Rule Transformer] Successfully removed ${profile.profileName} files from ${projectDir}`
);
return result;
}
// Check if profile directory exists at all (for full profiles)
if (!fs.existsSync(profileDir)) {
result.success = true;
result.skipped = true;

View File

@@ -1,54 +0,0 @@
import readline from 'readline';
import inquirer from 'inquirer';
import chalk from 'chalk';
import { log } from '../../scripts/modules/utils.js';
import { getRulesProfile } from './rule-transformer.js';
import { RULE_PROFILES } from '../constants/profiles.js';
// Dynamically generate availableRulesProfiles from RULE_PROFILES
const availableRulesProfiles = RULE_PROFILES.map((name) => {
const displayName = getProfileDisplayName(name);
return {
name: displayName,
value: name
};
});
/**
* Get the display name for a profile
*/
function getProfileDisplayName(name) {
const profile = getRulesProfile(name);
return profile?.displayName || name.charAt(0).toUpperCase() + name.slice(1);
}
/**
* Runs the interactive rules setup flow (profile rules selection only)
* @returns {Promise<string[]>} The selected profile rules
*/
/**
* Launches an interactive prompt for selecting which profile rules to include in your project.
*
* This function dynamically lists all available profiles (from RULE_PROFILES) and presents them as checkboxes.
* The user must select at least one profile (no defaults are pre-selected). The result is an array of selected profile names.
*
* Used by both project initialization (init) and the CLI 'task-master rules setup' command to ensure DRY, consistent UX.
*
* @returns {Promise<string[]>} Array of selected profile rule names (e.g., ['cursor', 'windsurf'])
*/
export async function runInteractiveRulesSetup() {
console.log(
chalk.cyan(
'\nRules help enforce best practices and conventions for Task Master.'
)
);
const rulesProfilesQuestion = {
type: 'checkbox',
name: 'rulesProfiles',
message: 'Which IDEs would you like rule profiles included for?',
choices: availableRulesProfiles,
validate: (input) => input.length > 0 || 'You must select at least one.'
};
const { rulesProfiles } = await inquirer.prompt([rulesProfilesQuestion]);
return rulesProfiles;
}

View File

@@ -1126,7 +1126,9 @@ describe('rules command', () => {
expect.stringMatching(/removing rules for profile: roo/i)
);
expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringMatching(/completed removal for profile: roo/i)
expect.stringMatching(
/Summary for roo: (Rules directory removed|Skipped \(default or protected files\))/i
)
);
// Should not exit with error
expect(mockExit).not.toHaveBeenCalledWith(1);

View File

@@ -117,16 +117,38 @@ describe('MCP Configuration Validation', () => {
describe('Profile Directory Structure', () => {
test('should ensure each profile has a unique directory', () => {
const profileDirs = new Set();
// Simple profiles that use root directory (can share the same directory)
const simpleProfiles = ['claude', 'codex'];
RULE_PROFILES.forEach((profileName) => {
const profile = getRulesProfile(profileName);
// Simple profiles can share the root directory
if (simpleProfiles.includes(profileName)) {
expect(profile.profileDir).toBe('.');
return;
}
// Full profiles should have unique directories
expect(profileDirs.has(profile.profileDir)).toBe(false);
profileDirs.add(profile.profileDir);
});
});
test('should ensure profile directories follow expected naming convention', () => {
// Simple profiles that use root directory
const simpleProfiles = ['claude', 'codex'];
RULE_PROFILES.forEach((profileName) => {
const profile = getRulesProfile(profileName);
// Simple profiles use root directory
if (simpleProfiles.includes(profileName)) {
expect(profile.profileDir).toBe('.');
return;
}
// Full profiles should follow the .name pattern
expect(profile.profileDir).toMatch(/^\.[\w-]+$/);
});
});
@@ -144,6 +166,8 @@ describe('MCP Configuration Validation', () => {
expect(mcpEnabledProfiles).toContain('roo');
expect(mcpEnabledProfiles).not.toContain('cline');
expect(mcpEnabledProfiles).not.toContain('trae');
expect(mcpEnabledProfiles).not.toContain('claude');
expect(mcpEnabledProfiles).not.toContain('codex');
});
test('should provide all necessary information for MCP config creation', () => {

View File

@@ -3,7 +3,7 @@ import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { convertRuleToProfileRule } from '../../src/utils/rule-transformer.js';
import * as clineProfile from '../../scripts/profiles/cline.js';
import { clineProfile } from '../../scripts/profiles/cline.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -41,11 +41,7 @@ Also has references to .mdc files.`;
// Convert it
const testClineRule = path.join(testDir, 'basic-terms.md');
convertRuleToProfileRule(
testCursorRule,
testClineRule,
clineProfile.clineProfile
);
convertRuleToProfileRule(testCursorRule, testClineRule, clineProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testClineRule, 'utf8');
@@ -76,11 +72,7 @@ alwaysApply: true
// Convert it
const testClineRule = path.join(testDir, 'tool-refs.md');
convertRuleToProfileRule(
testCursorRule,
testClineRule,
clineProfile.clineProfile
);
convertRuleToProfileRule(testCursorRule, testClineRule, clineProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testClineRule, 'utf8');
@@ -108,11 +100,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
// Convert it
const testClineRule = path.join(testDir, 'file-refs.md');
convertRuleToProfileRule(
testCursorRule,
testClineRule,
clineProfile.clineProfile
);
convertRuleToProfileRule(testCursorRule, testClineRule, clineProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testClineRule, 'utf8');

View File

@@ -7,7 +7,7 @@ import {
convertRuleToProfileRule,
getRulesProfile
} from '../../src/utils/rule-transformer.js';
import * as cursorProfile from '../../scripts/profiles/cursor.js';
import { cursorProfile } from '../../scripts/profiles/cursor.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -45,11 +45,7 @@ Also has references to .mdc files.`;
// Convert it
const testCursorOut = path.join(testDir, 'basic-terms.mdc');
convertRuleToProfileRule(
testCursorRule,
testCursorOut,
cursorProfile.cursorProfile
);
convertRuleToProfileRule(testCursorRule, testCursorOut, cursorProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testCursorOut, 'utf8');
@@ -80,11 +76,7 @@ alwaysApply: true
// Convert it
const testCursorOut = path.join(testDir, 'tool-refs.mdc');
convertRuleToProfileRule(
testCursorRule,
testCursorOut,
cursorProfile.cursorProfile
);
convertRuleToProfileRule(testCursorRule, testCursorOut, cursorProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testCursorOut, 'utf8');
@@ -114,11 +106,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
// Convert it
const testCursorOut = path.join(testDir, 'file-refs.mdc');
convertRuleToProfileRule(
testCursorRule,
testCursorOut,
cursorProfile.cursorProfile
);
convertRuleToProfileRule(testCursorRule, testCursorOut, cursorProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testCursorOut, 'utf8');

View File

@@ -7,7 +7,7 @@ import {
convertRuleToProfileRule,
getRulesProfile
} from '../../src/utils/rule-transformer.js';
import * as rooProfile from '../../scripts/profiles/roo.js';
import { rooProfile } from '../../scripts/profiles/roo.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -45,11 +45,7 @@ Also has references to .mdc files.`;
// Convert it
const testRooRule = path.join(testDir, 'basic-terms.md');
convertRuleToProfileRule(
testCursorRule,
testRooRule,
rooProfile.rooProfile
);
convertRuleToProfileRule(testCursorRule, testRooRule, rooProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
@@ -80,11 +76,7 @@ alwaysApply: true
// Convert it
const testRooRule = path.join(testDir, 'tool-refs.md');
convertRuleToProfileRule(
testCursorRule,
testRooRule,
rooProfile.rooProfile
);
convertRuleToProfileRule(testCursorRule, testRooRule, rooProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
@@ -112,11 +104,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
// Convert it
const testRooRule = path.join(testDir, 'file-refs.md');
convertRuleToProfileRule(
testCursorRule,
testRooRule,
rooProfile.rooProfile
);
convertRuleToProfileRule(testCursorRule, testRooRule, rooProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
@@ -134,7 +122,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
const assetRule = path.join(assetsRulesDir, 'dev_workflow.mdc');
fs.writeFileSync(assetRule, 'dummy');
// Should create .roo/rules and call post-processing
convertAllRulesToProfileRules(testDir, rooProfile.rooProfile);
convertAllRulesToProfileRules(testDir, rooProfile);
// Check for post-processing artifacts, e.g., rules-* folders or extra files
const rooDir = path.join(testDir, '.roo');
const found = fs.readdirSync(rooDir).some((f) => f.startsWith('rules-'));

View File

@@ -3,7 +3,7 @@ import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { convertRuleToProfileRule } from '../../src/utils/rule-transformer.js';
import * as traeProfile from '../../scripts/profiles/trae.js';
import { traeProfile } from '../../scripts/profiles/trae.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -41,11 +41,7 @@ Also has references to .mdc files.`;
// Convert it
const testTraeRule = path.join(testDir, 'basic-terms.md');
convertRuleToProfileRule(
testCursorRule,
testTraeRule,
traeProfile.traeProfile
);
convertRuleToProfileRule(testCursorRule, testTraeRule, traeProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testTraeRule, 'utf8');
@@ -76,11 +72,7 @@ alwaysApply: true
// Convert it
const testTraeRule = path.join(testDir, 'tool-refs.md');
convertRuleToProfileRule(
testCursorRule,
testTraeRule,
traeProfile.traeProfile
);
convertRuleToProfileRule(testCursorRule, testTraeRule, traeProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testTraeRule, 'utf8');
@@ -108,11 +100,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
// Convert it
const testTraeRule = path.join(testDir, 'file-refs.md');
convertRuleToProfileRule(
testCursorRule,
testTraeRule,
traeProfile.traeProfile
);
convertRuleToProfileRule(testCursorRule, testTraeRule, traeProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testTraeRule, 'utf8');

View File

@@ -3,7 +3,7 @@ import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { convertRuleToProfileRule } from '../../src/utils/rule-transformer.js';
import * as windsurfProfile from '../../scripts/profiles/windsurf.js';
import { windsurfProfile } from '../../scripts/profiles/windsurf.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -41,11 +41,7 @@ Also has references to .mdc files.`;
// Convert it
const testWindsurfRule = path.join(testDir, 'basic-terms.md');
convertRuleToProfileRule(
testCursorRule,
testWindsurfRule,
windsurfProfile.windsurfProfile
);
convertRuleToProfileRule(testCursorRule, testWindsurfRule, windsurfProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testWindsurfRule, 'utf8');
@@ -76,11 +72,7 @@ alwaysApply: true
// Convert it
const testWindsurfRule = path.join(testDir, 'tool-refs.md');
convertRuleToProfileRule(
testCursorRule,
testWindsurfRule,
windsurfProfile.windsurfProfile
);
convertRuleToProfileRule(testCursorRule, testWindsurfRule, windsurfProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testWindsurfRule, 'utf8');
@@ -108,11 +100,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
// Convert it
const testWindsurfRule = path.join(testDir, 'file-refs.md');
convertRuleToProfileRule(
testCursorRule,
testWindsurfRule,
windsurfProfile.windsurfProfile
);
convertRuleToProfileRule(testCursorRule, testWindsurfRule, windsurfProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testWindsurfRule, 'utf8');

View File

@@ -12,7 +12,15 @@ describe('Rule Transformer - General', () => {
expect(RULE_PROFILES.length).toBeGreaterThan(0);
// Verify expected profiles are present
const expectedProfiles = ['cline', 'cursor', 'roo', 'trae', 'windsurf'];
const expectedProfiles = [
'claude',
'cline',
'codex',
'cursor',
'roo',
'trae',
'windsurf'
];
expectedProfiles.forEach((profile) => {
expect(RULE_PROFILES).toContain(profile);
});
@@ -31,7 +39,7 @@ describe('Rule Transformer - General', () => {
expect(isValidProfile(undefined)).toBe(false);
});
it('should return correct rules profile with getRulesProfile', () => {
it('should return correct rule profile with getRulesProfile', () => {
// Test valid profiles
RULE_PROFILES.forEach((profile) => {
const profileConfig = getRulesProfile(profile);
@@ -46,6 +54,9 @@ describe('Rule Transformer - General', () => {
describe('Profile Structure', () => {
it('should have all required properties for each profile', () => {
// Simple profiles that only copy files (no rule transformation)
const simpleProfiles = ['claude', 'codex'];
RULE_PROFILES.forEach((profile) => {
const profileConfig = getRulesProfile(profile);
@@ -56,7 +67,15 @@ describe('Rule Transformer - General', () => {
expect(profileConfig).toHaveProperty('rulesDir');
expect(profileConfig).toHaveProperty('profileDir');
// Check that conversionConfig has required structure
// Simple profiles have minimal structure
if (simpleProfiles.includes(profile)) {
// For simple profiles, conversionConfig and fileMap can be empty
expect(typeof profileConfig.conversionConfig).toBe('object');
expect(typeof profileConfig.fileMap).toBe('object');
return;
}
// Check that conversionConfig has required structure for full profiles
expect(profileConfig.conversionConfig).toHaveProperty('profileTerms');
expect(profileConfig.conversionConfig).toHaveProperty('toolNames');
expect(profileConfig.conversionConfig).toHaveProperty('toolContexts');
@@ -89,6 +108,9 @@ describe('Rule Transformer - General', () => {
'taskmaster.mdc'
];
// Simple profiles that only copy files (no rule transformation)
const simpleProfiles = ['claude', 'codex'];
RULE_PROFILES.forEach((profile) => {
const profileConfig = getRulesProfile(profile);
@@ -97,7 +119,12 @@ describe('Rule Transformer - General', () => {
expect(typeof profileConfig.fileMap).toBe('object');
expect(profileConfig.fileMap).not.toBeNull();
// Check that fileMap is not empty
// Simple profiles can have empty fileMap since they don't transform rules
if (simpleProfiles.includes(profile)) {
return;
}
// Check that fileMap is not empty for full profiles
const fileMapKeys = Object.keys(profileConfig.fileMap);
expect(fileMapKeys.length).toBeGreaterThan(0);
@@ -116,6 +143,9 @@ describe('Rule Transformer - General', () => {
describe('MCP Configuration Properties', () => {
it('should have all required MCP properties for each profile', () => {
// Simple profiles that only copy files (no MCP configuration)
const simpleProfiles = ['claude', 'codex'];
RULE_PROFILES.forEach((profile) => {
const profileConfig = getRulesProfile(profile);
@@ -124,7 +154,15 @@ describe('Rule Transformer - General', () => {
expect(profileConfig).toHaveProperty('mcpConfigName');
expect(profileConfig).toHaveProperty('mcpConfigPath');
// Check types
// Simple profiles have no MCP configuration
if (simpleProfiles.includes(profile)) {
expect(profileConfig.mcpConfig).toBe(false);
expect(profileConfig.mcpConfigName).toBe(null);
expect(profileConfig.mcpConfigPath).toBe(null);
return;
}
// Check types for full profiles
expect(typeof profileConfig.mcpConfig).toBe('boolean');
expect(typeof profileConfig.mcpConfigName).toBe('string');
expect(typeof profileConfig.mcpConfigPath).toBe('string');
@@ -138,6 +176,16 @@ describe('Rule Transformer - General', () => {
it('should have correct MCP configuration for each profile', () => {
const expectedConfigs = {
claude: {
mcpConfig: false,
mcpConfigName: null,
expectedPath: null
},
codex: {
mcpConfig: false,
mcpConfigName: null,
expectedPath: null
},
cursor: {
mcpConfig: true,
mcpConfigName: 'mcp.json',
@@ -176,9 +224,18 @@ describe('Rule Transformer - General', () => {
});
it('should have consistent profileDir and mcpConfigPath relationship', () => {
// Simple profiles that only copy files (no MCP configuration)
const simpleProfiles = ['claude', 'codex'];
RULE_PROFILES.forEach((profile) => {
const profileConfig = getRulesProfile(profile);
// Simple profiles have null mcpConfigPath
if (simpleProfiles.includes(profile)) {
expect(profileConfig.mcpConfigPath).toBe(null);
return;
}
// The mcpConfigPath should start with the profileDir
expect(profileConfig.mcpConfigPath).toMatch(
new RegExp(
@@ -201,8 +258,11 @@ describe('Rule Transformer - General', () => {
return profileConfig.profileDir;
});
// Note: Claude and Codex both use "." (root directory) so we expect some duplication
const uniqueProfileDirs = [...new Set(profileDirs)];
expect(uniqueProfileDirs).toHaveLength(profileDirs.length);
// We should have fewer unique directories than total profiles due to simple profiles using root
expect(uniqueProfileDirs.length).toBeLessThanOrEqual(profileDirs.length);
expect(uniqueProfileDirs.length).toBeGreaterThan(0);
});
it('should have unique MCP config paths', () => {
@@ -211,8 +271,13 @@ describe('Rule Transformer - General', () => {
return profileConfig.mcpConfigPath;
});
// Note: Claude and Codex both have null mcpConfigPath so we expect some duplication
const uniqueMcpConfigPaths = [...new Set(mcpConfigPaths)];
expect(uniqueMcpConfigPaths).toHaveLength(mcpConfigPaths.length);
// We should have fewer unique paths than total profiles due to simple profiles having null
expect(uniqueMcpConfigPaths.length).toBeLessThanOrEqual(
mcpConfigPaths.length
);
expect(uniqueMcpConfigPaths.length).toBeGreaterThan(0);
});
});
});

View File

@@ -1,7 +1,7 @@
import {
getInstalledProfiles,
wouldRemovalLeaveNoProfiles
} from '../../src/utils/profile-detection.js';
} from '../../src/utils/profiles.js';
import { rulesDirect } from '../../mcp-server/src/core/direct-functions/rules.js';
import fs from 'fs';
import path from 'path';