update semantics and terminology from 'brand rules' to 'rules profiles'

This commit is contained in:
Joe Danziger
2025-05-26 19:07:10 -04:00
parent ba55615d55
commit 9db5f78da3
29 changed files with 918 additions and 513 deletions

View File

@@ -5,7 +5,8 @@ import {
// isSilentMode // Not used directly here // isSilentMode // Not used directly here
} from '../../../../scripts/modules/utils.js'; } from '../../../../scripts/modules/utils.js';
import os from 'os'; // Import os module for home directory check import os from 'os'; // Import os module for home directory check
import { BRAND_NAMES } from '../../../../src/utils/rule-transformer.js'; import { RULES_PROFILES } from '../../../../src/constants/profiles.js';
import { convertAllRulesToProfileRules } from '../../../../src/utils/rule-transformer.js';
/** /**
* Direct function wrapper for initializing a project. * Direct function wrapper for initializing a project.
@@ -75,8 +76,10 @@ export async function initializeProjectDirect(args, log, context = {}) {
options.rules = args.rules; options.rules = args.rules;
log.info(`Including rules: ${args.rules.join(', ')}`); log.info(`Including rules: ${args.rules.join(', ')}`);
} else { } else {
options.rules = BRAND_NAMES; options.rules = RULES_PROFILES;
log.info(`No rules specified, defaulting to: ${BRAND_NAMES.join(', ')}`); log.info(
`No rules profiles specified, defaulting to: ${RULES_PROFILES.join(', ')}`
);
} }
log.info(`Initializing project with options: ${JSON.stringify(options)}`); log.info(`Initializing project with options: ${JSON.stringify(options)}`);

View File

@@ -8,12 +8,12 @@ import {
disableSilentMode disableSilentMode
} from '../../../../scripts/modules/utils.js'; } from '../../../../scripts/modules/utils.js';
import { import {
removeBrandRules, convertAllRulesToProfileRules,
convertAllRulesToBrandRules, removeProfileRules,
BRAND_NAMES, getRulesProfile,
isValidBrand, isValidProfile
getBrandProfile
} from '../../../../src/utils/rule-transformer.js'; } from '../../../../src/utils/rule-transformer.js';
import { RULES_PROFILES } from '../../../../src/constants/profiles.js';
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
@@ -21,7 +21,7 @@ import fs from 'fs';
* Direct function wrapper for adding or removing rules. * Direct function wrapper for adding or removing rules.
* @param {Object} args - Command arguments * @param {Object} args - Command arguments
* @param {"add"|"remove"} args.action - Action to perform: add or remove rules * @param {"add"|"remove"} args.action - Action to perform: add or remove rules
* @param {string[]} args.rules - List of rules to add or remove * @param {string[]} args.profiles - List of profiles to add or remove
* @param {string} args.projectRoot - Absolute path to the project root * @param {string} args.projectRoot - Absolute path to the project root
* @param {boolean} [args.yes=true] - Run non-interactively * @param {boolean} [args.yes=true] - Run non-interactively
* @param {Object} log - Logger object * @param {Object} log - Logger object
@@ -31,18 +31,18 @@ import fs from 'fs';
export async function rulesDirect(args, log, context = {}) { export async function rulesDirect(args, log, context = {}) {
enableSilentMode(); enableSilentMode();
try { try {
const { action, rules, projectRoot, yes } = args; const { action, profiles, projectRoot, yes } = args;
if ( if (
!action || !action ||
!Array.isArray(rules) || !Array.isArray(profiles) ||
rules.length === 0 || profiles.length === 0 ||
!projectRoot !projectRoot
) { ) {
return { return {
success: false, success: false,
error: { error: {
code: 'MISSING_ARGUMENT', code: 'MISSING_ARGUMENT',
message: 'action, rules, and projectRoot are required.' message: 'action, profiles, and projectRoot are required.'
} }
}; };
} }
@@ -51,25 +51,25 @@ export async function rulesDirect(args, log, context = {}) {
const addResults = []; const addResults = [];
if (action === 'remove') { if (action === 'remove') {
for (const brand of rules) { for (const profile of profiles) {
if (!isValidBrand(brand)) { if (!isValidProfile(profile)) {
removalResults.push({ removalResults.push({
brandName: brand, profileName: profile,
success: false, success: false,
error: `The requested rules for '${brand}' are unavailable. Supported rules are: ${BRAND_NAMES.join(', ')}.` error: `The requested rules profile for '${profile}' is unavailable. Supported profiles are: ${RULES_PROFILES.join(', ')}.`
}); });
continue; continue;
} }
const profile = getBrandProfile(brand); const profileConfig = getRulesProfile(profile);
const result = removeBrandRules(projectRoot, profile); const result = removeProfileRules(projectRoot, profileConfig);
removalResults.push(result); removalResults.push(result);
} }
const successes = removalResults const successes = removalResults
.filter((r) => r.success) .filter((r) => r.success)
.map((r) => r.brandName); .map((r) => r.profileName);
const skipped = removalResults const skipped = removalResults
.filter((r) => r.skipped) .filter((r) => r.skipped)
.map((r) => r.brandName); .map((r) => r.profileName);
const errors = removalResults.filter( const errors = removalResults.filter(
(r) => r.error && !r.success && !r.skipped (r) => r.error && !r.success && !r.skipped
); );
@@ -83,7 +83,7 @@ export async function rulesDirect(args, log, context = {}) {
} }
if (errors.length > 0) { if (errors.length > 0) {
summary += errors summary += errors
.map((r) => `Error removing ${r.brandName}: ${r.error}`) .map((r) => `Error removing ${r.profileName}: ${r.error}`)
.join(' '); .join(' ');
} }
disableSilentMode(); disableSilentMode();
@@ -92,43 +92,42 @@ export async function rulesDirect(args, log, context = {}) {
data: { summary, results: removalResults } data: { summary, results: removalResults }
}; };
} else if (action === 'add') { } else if (action === 'add') {
for (const brand of rules) { for (const profile of profiles) {
if (!isValidBrand(brand)) { if (!isValidProfile(profile)) {
addResults.push({ addResults.push({
brandName: brand, profileName: profile,
success: false, success: false,
error: `Profile not found: static import missing for '${brand}'. Valid brands: ${BRAND_NAMES.join(', ')}` error: `Profile not found: static import missing for '${profile}'. Valid profiles: ${RULES_PROFILES.join(', ')}`
}); });
continue; continue;
} }
const profile = getBrandProfile(brand); const profileConfig = getRulesProfile(profile);
const { success, failed } = convertAllRulesToBrandRules( const { success, failed } = convertAllRulesToProfileRules(
projectRoot, projectRoot,
profile profileConfig
); );
// Determine paths // Determine paths
const rulesDir = profile.rulesDir; const rulesDir = profileConfig.rulesDir;
const brandRulesDir = path.join(projectRoot, rulesDir); const profileRulesDir = path.join(projectRoot, rulesDir);
const brandDir = profile.brandDir; const profileDir = profileConfig.profileDir;
const mcpConfig = profile.mcpConfig !== false; const mcpConfig = profileConfig.mcpConfig !== false;
const mcpConfigName = profile.mcpConfigName || 'mcp.json'; const mcpPath = path.join(projectRoot, profileConfig.mcpConfigPath);
const mcpPath = path.join(projectRoot, brandDir, mcpConfigName);
// Check what was created // Check what was created
const mcpConfigCreated = mcpConfig ? fs.existsSync(mcpPath) : undefined; const mcpConfigCreated = mcpConfig ? fs.existsSync(mcpPath) : undefined;
const rulesDirCreated = fs.existsSync(brandRulesDir); const rulesDirCreated = fs.existsSync(profileRulesDir);
const brandFolderCreated = fs.existsSync( const profileFolderCreated = fs.existsSync(
path.join(projectRoot, brandDir) path.join(projectRoot, profileDir)
); );
const error = const error =
failed > 0 ? `${failed} rule files failed to convert.` : null; failed > 0 ? `${failed} rule files failed to convert.` : null;
const resultObj = { const resultObj = {
brandName: brand, profileName: profile,
mcpConfigCreated, mcpConfigCreated,
rulesDirCreated, rulesDirCreated,
brandFolderCreated, profileFolderCreated,
skipped: false, skipped: false,
error, error,
success: success:
@@ -142,7 +141,7 @@ export async function rulesDirect(args, log, context = {}) {
const successes = addResults const successes = addResults
.filter((r) => r.success) .filter((r) => r.success)
.map((r) => r.brandName); .map((r) => r.profileName);
const errors = addResults.filter((r) => r.error && !r.success); const errors = addResults.filter((r) => r.error && !r.success);
let summary = ''; let summary = '';
@@ -151,7 +150,7 @@ export async function rulesDirect(args, log, context = {}) {
} }
if (errors.length > 0) { if (errors.length > 0) {
summary += errors summary += errors
.map((r) => ` Error adding ${r.brandName}: ${r.error}`) .map((r) => ` Error adding ${r.profileName}: ${r.error}`)
.join(' '); .join(' ');
} }
disableSilentMode(); disableSilentMode();

View File

@@ -5,7 +5,7 @@ import {
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from './utils.js';
import { initializeProjectDirect } from '../core/task-master-core.js'; import { initializeProjectDirect } from '../core/task-master-core.js';
import { BRAND_RULE_OPTIONS } from '../../../src/constants/rules.js'; import { RULES_PROFILES } from '../../../src/constants/profiles.js';
export function registerInitializeProjectTool(server) { export function registerInitializeProjectTool(server) {
server.addTool({ server.addTool({
@@ -38,10 +38,10 @@ export function registerInitializeProjectTool(server) {
'The root directory for the project. ALWAYS SET THIS TO THE PROJECT ROOT DIRECTORY. IF NOT SET, THE TOOL WILL NOT WORK.' 'The root directory for the project. ALWAYS SET THIS TO THE PROJECT ROOT DIRECTORY. IF NOT SET, THE TOOL WILL NOT WORK.'
), ),
rules: z rules: z
.array(z.enum(BRAND_RULE_OPTIONS)) .array(z.enum(RULES_PROFILES))
.optional() .optional()
.describe( .describe(
`List of rules to include at initialization. If omitted, defaults to all available brand rules. Available options: ${BRAND_RULE_OPTIONS.join(', ')}` `List of rules profiles to include at initialization. If omitted, defaults to all available profiles. Available options: ${RULES_PROFILES.join(', ')}`
) )
}), }),
execute: withNormalizedProjectRoot(async (args, context) => { execute: withNormalizedProjectRoot(async (args, context) => {

View File

@@ -10,7 +10,7 @@ import {
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from './utils.js';
import { rulesDirect } from '../core/direct-functions/rules.js'; import { rulesDirect } from '../core/direct-functions/rules.js';
import { BRAND_RULE_OPTIONS } from '../../../src/constants/rules.js'; import { RULES_PROFILES } from '../../../src/constants/profiles.js';
/** /**
* Register the rules tool with the MCP server * Register the rules tool with the MCP server
@@ -20,16 +20,16 @@ export function registerRulesTool(server) {
server.addTool({ server.addTool({
name: 'rules', name: 'rules',
description: description:
'Add or remove rules and MCP config from the project (mirrors CLI rules add/remove).', 'Add or remove rules profiles from the project.',
parameters: z.object({ parameters: z.object({
action: z action: z
.enum(['add', 'remove']) .enum(['add', 'remove'])
.describe('Whether to add or remove rules.'), .describe('Whether to add or remove rules profiles.'),
rules: z profiles: z
.array(z.enum(BRAND_RULE_OPTIONS)) .array(z.enum(RULES_PROFILES))
.min(1) .min(1)
.describe( .describe(
`List of rules to add or remove. Available options: ${BRAND_RULE_OPTIONS.join(', ')}` `List of rules profiles to add or remove (e.g., [\"cursor\", \"roo\"]). Available options: ${RULES_PROFILES.join(', ')}`
), ),
projectRoot: z projectRoot: z
.string() .string()
@@ -40,7 +40,7 @@ export function registerRulesTool(server) {
execute: withNormalizedProjectRoot(async (args, { log, session }) => { execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try { try {
log.info( log.info(
`[rules tool] Executing action: ${args.action} for rules: ${args.rules.join(', ')} in ${args.projectRoot}` `[rules tool] Executing action: ${args.action} for profiles: ${args.profiles.join(', ')} in ${args.projectRoot}`
); );
const result = await rulesDirect(args, log, { session }); const result = await rulesDirect(args, log, { session });
return handleApiResult(result, log); return handleApiResult(result, log);

View File

@@ -24,7 +24,11 @@ import figlet from 'figlet';
import boxen from 'boxen'; import boxen from 'boxen';
import gradient from 'gradient-string'; import gradient from 'gradient-string';
import { isSilentMode } from './modules/utils.js'; import { isSilentMode } from './modules/utils.js';
import { convertAllRulesToBrandRules } from '../src/utils/rule-transformer.js'; import { RULES_PROFILES } from '../src/constants/profiles.js';
import {
convertAllRulesToProfileRules,
getRulesProfile
} from '../src/utils/rule-transformer.js';
import { runInteractiveRulesSetup } from '../src/utils/rules-setup.js'; import { runInteractiveRulesSetup } from '../src/utils/rules-setup.js';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
@@ -305,10 +309,10 @@ async function initializeProject(options = {}) {
} }
const skipPrompts = options.yes || (options.name && options.description); const skipPrompts = options.yes || (options.name && options.description);
let selectedBrandRules = let selectedRulesProfiles =
options.rules && Array.isArray(options.rules) && options.rules.length > 0 options.rules && Array.isArray(options.rules) && options.rules.length > 0
? options.rules ? options.rules
: BRAND_NAMES; // Default to all rules : RULES_PROFILES; // Default to all profiles
if (skipPrompts) { if (skipPrompts) {
if (!isSilentMode()) { if (!isSilentMode()) {
@@ -331,7 +335,7 @@ async function initializeProject(options = {}) {
} }
try { try {
createProjectStructure(addAliases, dryRun, selectedBrandRules); createProjectStructure(addAliases, dryRun, selectedRulesProfiles);
} catch (error) { } catch (error) {
log('error', `Error during initialization process: ${error.message}`); log('error', `Error during initialization process: ${error.message}`);
process.exit(1); process.exit(1);
@@ -378,9 +382,12 @@ async function initializeProject(options = {}) {
// Only run interactive rules if rules flag not provided via command line // Only run interactive rules if rules flag not provided via command line
if (options.rulesExplicitlyProvided) { if (options.rulesExplicitlyProvided) {
log('info', `Using rules provided via command line: ${selectedBrandRules.join(', ')}`); log(
'info',
`Using rules profiles provided via command line: ${selectedRulesProfiles.join(', ')}`
);
} else { } else {
selectedBrandRules = await runInteractiveRulesSetup(); selectedRulesProfiles = await runInteractiveRulesSetup();
} }
const dryRun = options.dryRun || false; const dryRun = options.dryRun || false;
@@ -398,8 +405,7 @@ async function initializeProject(options = {}) {
} }
// Create structure using only necessary values // Create structure using only necessary values
createProjectStructure(addAliasesPrompted, dryRun, selectedBrandRules); createProjectStructure(addAliasesPrompted, dryRun, selectedRulesProfiles);
} catch (error) { } catch (error) {
rl.close(); rl.close();
log('error', `Error during initialization process: ${error.message}`); log('error', `Error during initialization process: ${error.message}`);
@@ -421,13 +427,12 @@ function promptQuestion(rl, question) {
function createProjectStructure( function createProjectStructure(
addAliases, addAliases,
dryRun, dryRun,
selectedBrandRules = BRAND_NAMES // Default to all rules selectedRulesProfiles = RULES_PROFILES // Default to all rules profiles
) { ) {
const targetDir = process.cwd(); const targetDir = process.cwd();
log('info', `Initializing project in ${targetDir}`); log('info', `Initializing project in ${targetDir}`);
// Create directories // Create directories
ensureDirectoryExists(path.join(targetDir, '.cursor', 'rules'));
ensureDirectoryExists(path.join(targetDir, 'scripts')); ensureDirectoryExists(path.join(targetDir, 'scripts'));
ensureDirectoryExists(path.join(targetDir, 'tasks')); ensureDirectoryExists(path.join(targetDir, 'tasks'));
@@ -436,17 +441,14 @@ function createProjectStructure(
year: new Date().getFullYear() year: new Date().getFullYear()
}; };
// Helper function to process a single brand rule // Helper function to process a single profile rule
function _processSingleBrandRule(ruleName) { function _processSingleProfileRule(profileName) {
const profile = BRAND_PROFILES[ruleName]; const profile = getRulesProfile(profileName);
if (profile) { if (profile) {
convertAllRulesToBrandRules(targetDir, profile); convertAllRulesToProfileRules(targetDir, profile);
// Ensure MCP config is set up under the correct brand folder for any non-cursor rule // The convertAllRulesToProfileRules function also triggers MCP config setup (if needed).
if (ruleName !== 'cursor') {
setupMCPConfiguration(path.join(targetDir, `.${ruleName}`));
}
} else { } else {
log('warn', `Unknown rules profile: ${ruleName}`); log('warn', `Unknown rules profile: ${profileName}`);
} }
} }
@@ -510,10 +512,10 @@ function createProjectStructure(
log('warn', 'Git not available, skipping repository initialization'); log('warn', 'Git not available, skipping repository initialization');
} }
// === Generate Brand Rules from assets/rules === // Generate profile rules from assets/rules
log('info', 'Generating brand rules from assets/rules...'); log('info', 'Generating profile rules from assets/rules...');
for (const rule of selectedBrandRules) { for (const profileName of selectedRulesProfiles) {
_processSingleBrandRule(rule); _processSingleProfileRule(profileName);
} }
// Add shell aliases if requested // Add shell aliases if requested
@@ -685,10 +687,5 @@ function createProjectStructure(
} }
} }
// Import MCP configuration helper
import { setupMCPConfiguration } from '../src/utils/mcp-utils.js';
// Import centralized brand profile logic
import { BRAND_PROFILES, BRAND_NAMES } from '../src/utils/rule-transformer.js';
// Ensure necessary functions are exported // Ensure necessary functions are exported
export { initializeProject, log }; export { initializeProject, log };

View File

@@ -65,7 +65,7 @@ import {
displayApiKeyStatus, displayApiKeyStatus,
displayAiUsageSummary displayAiUsageSummary
} from './ui.js'; } from './ui.js';
import { confirmRulesRemove } from '../../src/ui/confirm.js'; import { confirmProfilesRemove } from '../../src/ui/confirm.js';
import { initializeProject } from '../init.js'; import { initializeProject } from '../init.js';
import { import {
@@ -80,12 +80,12 @@ import {
TASK_STATUS_OPTIONS TASK_STATUS_OPTIONS
} from '../../src/constants/task-status.js'; } from '../../src/constants/task-status.js';
import { getTaskMasterVersion } from '../../src/utils/getVersion.js'; import { getTaskMasterVersion } from '../../src/utils/getVersion.js';
import { RULES_PROFILES } from '../../src/constants/profiles.js';
import { import {
convertAllRulesToBrandRules, convertAllRulesToProfileRules,
removeBrandRules, removeProfileRules,
BRAND_NAMES, isValidProfile,
isValidBrand, getRulesProfile
getBrandProfile
} from '../../src/utils/rule-transformer.js'; } from '../../src/utils/rule-transformer.js';
import { runInteractiveRulesSetup } from '../../src/utils/rules-setup.js'; import { runInteractiveRulesSetup } from '../../src/utils/rules-setup.js';
@@ -2125,24 +2125,24 @@ function registerCommands(programInstance) {
.action(async (cmdOptions) => { .action(async (cmdOptions) => {
// cmdOptions contains parsed arguments // cmdOptions contains parsed arguments
// Parse rules: accept space or comma separated, default to all available rules // Parse rules: accept space or comma separated, default to all available rules
let selectedBrands = BRAND_NAMES; let selectedProfiles = RULES_PROFILES;
let rulesExplicitlyProvided = false; let rulesExplicitlyProvided = false;
if (cmdOptions.rules && Array.isArray(cmdOptions.rules)) { if (cmdOptions.rules && Array.isArray(cmdOptions.rules)) {
const userSpecifiedBrands = cmdOptions.rules const userSpecifiedProfiles = cmdOptions.rules
.flatMap((r) => r.split(',')) .flatMap((r) => r.split(','))
.map((r) => r.trim()) .map((r) => r.trim())
.filter(Boolean); .filter(Boolean);
// Only override defaults if user specified valid rules // Only override defaults if user specified valid rules
if (userSpecifiedBrands.length > 0) { if (userSpecifiedProfiles.length > 0) {
selectedBrands = userSpecifiedBrands; selectedProfiles = userSpecifiedProfiles;
rulesExplicitlyProvided = true; rulesExplicitlyProvided = true;
} }
} }
cmdOptions.rules = selectedBrands; cmdOptions.rules = selectedProfiles;
cmdOptions.rulesExplicitlyProvided = rulesExplicitlyProvided; cmdOptions.rulesExplicitlyProvided = rulesExplicitlyProvided;
try { try {
// Directly call the initializeProject function, passing the parsed options // Directly call the initializeProject function, passing the parsed options
await initializeProject(cmdOptions); await initializeProject(cmdOptions);
@@ -2377,65 +2377,68 @@ Examples:
return; // Stop execution here return; // Stop execution here
}); });
// Add/remove brand rules command // Add/remove profile rules command
programInstance programInstance
.command('rules <action> [brands...]') .command('rules <action> [profiles...]')
.description( .description(
'Add or remove rules for one or more brands (e.g., task-master rules add windsurf roo)' 'Add or remove rules for one or more profiles (e.g., task-master rules add windsurf roo)'
) )
.option( .option(
'-f, --force', '-f, --force',
'Skip confirmation prompt when removing rules (dangerous)' 'Skip confirmation prompt when removing rules (dangerous)'
) )
.action(async (action, brands, options) => { .action(async (action, profiles, options) => {
const projectDir = process.cwd(); const projectDir = process.cwd();
/** /**
* 'task-master rules setup' action: * 'task-master rules setup' action:
* *
* Launches an interactive prompt to select which brand rules to apply to the current project. * Launches an interactive prompt to select which rules profiles to add to the current project.
* This does NOT perform project initialization or ask about shell aliases—only rules selection. * This does NOT perform project initialization or ask about shell aliases—only rules selection.
* *
* Example usage: * Example usage:
* $ task-master rules setup * $ task-master rules setup
* *
* Useful for updating/enforcing rules after project creation, or switching brands. * Useful for adding rules after project creation.
* *
* The list of brands is always up-to-date with the available profiles. * The list of profiles is always up-to-date with the available profiles.
*/ */
if (action === 'setup') { if (action === 'setup') {
// Run interactive rules setup ONLY (no project init) // Run interactive rules setup ONLY (no project init)
const selectedBrandRules = await runInteractiveRulesSetup(); const selectedRulesProfiles = await runInteractiveRulesSetup();
for (const brand of selectedBrandRules) { for (const profile of selectedRulesProfiles) {
if (!isValidBrand(brand)) { if (!isValidProfile(profile)) {
console.warn( console.warn(
`Rules profile for brand "${brand}" not found. Valid brands: ${BRAND_NAMES.join(', ')}. Skipping.` `Rules profile for "${profile}" not found. Valid profiles: ${RULES_PROFILES.join(', ')}. Skipping.`
); );
continue; continue;
} }
const profile = getBrandProfile(brand); const profileConfig = getRulesProfile(profile);
const addResult = convertAllRulesToBrandRules(projectDir, profile); const addResult = convertAllRulesToProfileRules(
if (typeof profile.onAddBrandRules === 'function') { projectDir,
profile.onAddBrandRules(projectDir); profileConfig
);
if (typeof profileConfig.onAddRulesProfile === 'function') {
profileConfig.onAddRulesProfile(projectDir);
} }
console.log( console.log(
chalk.green( chalk.green(
`Summary for ${brand}: ${addResult.success} rules added, ${addResult.failed} failed.` `Summary for ${profile}: ${addResult.success} rules added, ${addResult.failed} failed.`
) )
); );
} }
return; return;
} }
if (!brands || brands.length === 0) { if (!profiles || profiles.length === 0) {
console.error( console.error(
'Please specify at least one brand (e.g., windsurf, roo).' 'Please specify at least one rules profile (e.g., windsurf, roo).'
); );
process.exit(1); process.exit(1);
} }
// Support both space- and comma-separated brand lists // Support both space- and comma-separated profile lists
const expandedBrands = brands const expandedProfiles = profiles
.flatMap((b) => b.split(',').map((s) => s.trim())) .flatMap((b) => b.split(',').map((s) => s.trim()))
.filter(Boolean); .filter(Boolean);
@@ -2443,7 +2446,7 @@ Examples:
let confirmed = true; let confirmed = true;
if (!options.force) { if (!options.force) {
const ui = await import('./ui.js'); const ui = await import('./ui.js');
confirmed = await confirmRulesRemove(expandedBrands); confirmed = await confirmProfilesRemove(expandedProfiles);
} }
if (!confirmed) { if (!confirmed) {
console.log(chalk.yellow('Aborted: No rules were removed.')); console.log(chalk.yellow('Aborted: No rules were removed.'));
@@ -2451,36 +2454,39 @@ Examples:
} }
} }
// (removed duplicate projectDir, brands check, and expandedBrands parsing)
const removalResults = []; const removalResults = [];
for (const brand of expandedBrands) { for (const profile of expandedProfiles) {
if (!isValidBrand(brand)) { if (!isValidProfile(profile)) {
console.warn( console.warn(
`Rules profile for brand "${brand}" not found. Valid brands: ${BRAND_NAMES.join(', ')}. Skipping.` `Rules profile for "${profile}" not found. Valid profiles: ${RULES_PROFILES.join(', ')}. Skipping.`
); );
continue; continue;
} }
const profile = getBrandProfile(brand); const profileConfig = getRulesProfile(profile);
if (action === 'add') { if (action === 'add') {
console.log(chalk.blue(`Adding rules for brand: ${brand}...`)); console.log(chalk.blue(`Adding rules for profile: ${profile}...`));
const addResult = convertAllRulesToBrandRules(projectDir, profile); const addResult = convertAllRulesToProfileRules(
if (typeof profile.onAddBrandRules === 'function') { projectDir,
profile.onAddBrandRules(projectDir); profileConfig
);
if (typeof profileConfig.onAddRulesProfile === 'function') {
profileConfig.onAddRulesProfile(projectDir);
} }
console.log(chalk.blue(`Completed adding rules for brand: ${brand}`)); console.log(
chalk.blue(`Completed adding rules for profile: ${profile}`)
);
console.log( console.log(
chalk.green( chalk.green(
`Summary for ${brand}: ${addResult.success} rules added, ${addResult.failed} failed.` `Summary for ${profile}: ${addResult.success} rules added, ${addResult.failed} failed.`
) )
); );
} else if (action === 'remove') { } else if (action === 'remove') {
console.log(chalk.blue(`Removing rules for brand: ${brand}...`)); console.log(chalk.blue(`Removing rules for profile: ${profile}...`));
const result = removeBrandRules(projectDir, profile); const result = removeProfileRules(projectDir, profileConfig);
removalResults.push(result); removalResults.push(result);
console.log(chalk.blue(`Completed removal for brand: ${brand}`)); console.log(chalk.blue(`Completed removal for profile: ${profile}`));
} else { } else {
console.error('Unknown action. Use "add" or "remove".'); console.error('Unknown action. Use "add" or "remove".');
process.exit(1); process.exit(1);
@@ -2491,10 +2497,10 @@ Examples:
if (action === 'remove') { if (action === 'remove') {
const successes = removalResults const successes = removalResults
.filter((r) => r.success) .filter((r) => r.success)
.map((r) => r.brandName); .map((r) => r.profileName);
const skipped = removalResults const skipped = removalResults
.filter((r) => r.skipped) .filter((r) => r.skipped)
.map((r) => r.brandName); .map((r) => r.profileName);
const errors = removalResults.filter( const errors = removalResults.filter(
(r) => r.error && !r.success && !r.skipped (r) => r.error && !r.success && !r.skipped
); );
@@ -2513,7 +2519,9 @@ Examples:
} }
if (errors.length > 0) { if (errors.length > 0) {
errors.forEach((r) => { errors.forEach((r) => {
console.log(chalk.red(`Error removing ${r.brandName}: ${r.error}`)); console.log(
chalk.red(`Error removing ${r.profileName}: ${r.error}`)
);
}); });
} }
} }

View File

@@ -1769,8 +1769,6 @@ IMPORTANT: Make sure to include an analysis for EVERY task listed above, with th
`; `;
} }
/** /**
* Confirm overwriting existing tasks.json file * Confirm overwriting existing tasks.json file
* @param {string} tasksPath - Path to the tasks.json file * @param {string} tasksPath - Path to the tasks.json file

View File

@@ -1,11 +1,12 @@
// Cline conversion profile for rule-transformer // Cline conversion profile for rule-transformer
import path from 'path'; import path from 'path';
const brandName = 'Cline'; const profileName = 'Cline';
const brandDir = '.clinerules'; const profileDir = '.clinerules';
const rulesDir = '.clinerules'; const rulesDir = '.clinerules';
const mcpConfig = false; const mcpConfig = false;
const mcpConfigName = 'cline_mcp_settings.json'; const mcpConfigName = 'cline_mcp_settings.json';
const mcpConfigPath = `${profileDir}/${mcpConfigName}`;
// File name mapping (specific files with naming changes) // File name mapping (specific files with naming changes)
const fileMap = { const fileMap = {
@@ -43,8 +44,8 @@ const globalReplacements = [
]; ];
const conversionConfig = { const conversionConfig = {
// Product and brand name replacements // Profile name replacements
brandTerms: [ profileTerms: [
{ from: /cursor\.so/g, to: 'cline.bot' }, { from: /cursor\.so/g, to: 'cline.bot' },
{ from: /\[cursor\.so\]/g, to: '[cline.bot]' }, { from: /\[cursor\.so\]/g, to: '[cline.bot]' },
{ from: /href="https:\/\/cursor\.so/g, to: 'href="https://cline.bot' }, { from: /href="https:\/\/cursor\.so/g, to: 'href="https://cline.bot' },
@@ -136,10 +137,11 @@ export {
conversionConfig, conversionConfig,
fileMap, fileMap,
globalReplacements, globalReplacements,
brandName, profileName,
brandDir, profileDir,
rulesDir, rulesDir,
getTargetRuleFilename,
mcpConfig, mcpConfig,
mcpConfigName mcpConfigName,
mcpConfigPath,
getTargetRuleFilename
}; };

View File

@@ -1,11 +1,12 @@
// Cursor conversion profile for rule-transformer // Cursor conversion profile for rule-transformer
import path from 'path'; import path from 'path';
const brandName = 'Cursor'; const profileName = 'Cursor';
const brandDir = '.cursor'; const profileDir = '.cursor';
const rulesDir = '.cursor/rules'; const rulesDir = '.cursor/rules';
const mcpConfig = true; const mcpConfig = true;
const mcpConfigName = 'mcp.json'; const mcpConfigName = 'mcp.json';
const mcpConfigPath = `${profileDir}/${mcpConfigName}`;
// File name mapping (specific files with naming changes) // File name mapping (specific files with naming changes)
const fileMap = { const fileMap = {
@@ -39,8 +40,8 @@ const globalReplacements = [
]; ];
const conversionConfig = { const conversionConfig = {
// Product and brand name replacements // Profile name replacements
brandTerms: [ profileTerms: [
{ from: /cursor\.so/g, to: 'cursor.so' }, { from: /cursor\.so/g, to: 'cursor.so' },
{ from: /\[cursor\.so\]/g, to: '[cursor.so]' }, { from: /\[cursor\.so\]/g, to: '[cursor.so]' },
{ from: /href="https:\/\/cursor\.so/g, to: 'href="https://cursor.so' }, { from: /href="https:\/\/cursor\.so/g, to: 'href="https://cursor.so' },
@@ -82,10 +83,11 @@ export {
conversionConfig, conversionConfig,
fileMap, fileMap,
globalReplacements, globalReplacements,
brandName, profileName,
brandDir, profileDir,
rulesDir, rulesDir,
getTargetRuleFilename,
mcpConfig, mcpConfig,
mcpConfigName mcpConfigName,
mcpConfigPath,
getTargetRuleFilename
}; };

View File

@@ -2,4 +2,4 @@
export * as clineProfile from './cline.js'; export * as clineProfile from './cline.js';
export * as cursorProfile from './cursor.js'; export * as cursorProfile from './cursor.js';
export * as rooProfile from './roo.js'; export * as rooProfile from './roo.js';
export * as windsurfProfile from './windsurf.js'; export * as windsurfProfile from './windsurf.js';

View File

@@ -7,11 +7,12 @@ import { isSilentMode, log } from '../modules/utils.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const brandName = 'Roo'; const profileName = 'Roo';
const brandDir = '.roo'; const profileDir = '.roo';
const rulesDir = '.roo/rules'; const rulesDir = '.roo/rules';
const mcpConfig = true; const mcpConfig = true;
const mcpConfigName = 'mcp.json'; const mcpConfigName = 'mcp.json';
const mcpConfigPath = `${profileDir}/${mcpConfigName}`;
// File name mapping (specific files with naming changes) // File name mapping (specific files with naming changes)
const fileMap = { const fileMap = {
@@ -48,8 +49,8 @@ const globalReplacements = [
]; ];
const conversionConfig = { const conversionConfig = {
// Product and brand name replacements // Profile name replacements
brandTerms: [ profileTerms: [
{ from: /cursor\.so/g, to: 'roocode.com' }, { from: /cursor\.so/g, to: 'roocode.com' },
{ from: /\[cursor\.so\]/g, to: '[roocode.com]' }, { from: /\[cursor\.so\]/g, to: '[roocode.com]' },
{ from: /href="https:\/\/cursor\.so/g, to: 'href="https://roocode.com' }, { from: /href="https:\/\/cursor\.so/g, to: 'href="https://roocode.com' },
@@ -126,7 +127,7 @@ const conversionConfig = {
// Recursively copy everything from assets/roocode to the project root // Recursively copy everything from assets/roocode to the project root
export function onAddBrandRules(targetDir) { export function onAddRulesProfile(targetDir) {
const sourceDir = path.resolve(__dirname, '../../assets/roocode'); const sourceDir = path.resolve(__dirname, '../../assets/roocode');
copyRecursiveSync(sourceDir, targetDir); copyRecursiveSync(sourceDir, targetDir);
@@ -182,8 +183,8 @@ function copyRecursiveSync(src, dest) {
} }
} }
export function onRemoveBrandRules(targetDir) { export function onRemoveRulesProfile(targetDir) {
log('debug', `[Roo] onRemoveBrandRules called for ${targetDir}`); log('debug', `[Roo] onRemoveRulesProfile called for ${targetDir}`);
const roomodesPath = path.join(targetDir, '.roomodes'); const roomodesPath = path.join(targetDir, '.roomodes');
if (fs.existsSync(roomodesPath)) { if (fs.existsSync(roomodesPath)) {
try { try {
@@ -216,15 +217,15 @@ export function onRemoveBrandRules(targetDir) {
} }
} }
} }
log('debug', `[Roo] onRemoveBrandRules completed for ${targetDir}`); log('debug', `[Roo] onRemoveRulesProfile completed for ${targetDir}`);
} }
function isDirectoryEmpty(dirPath) { function isDirectoryEmpty(dirPath) {
return fs.readdirSync(dirPath).length === 0; return fs.readdirSync(dirPath).length === 0;
} }
function onPostConvertBrandRules(targetDir) { function onPostConvertRulesProfile(targetDir) {
onAddBrandRules(targetDir); onAddRulesProfile(targetDir);
} }
function getTargetRuleFilename(sourceFilename) { function getTargetRuleFilename(sourceFilename) {
@@ -238,11 +239,12 @@ export {
conversionConfig, conversionConfig,
fileMap, fileMap,
globalReplacements, globalReplacements,
brandName, profileName,
brandDir, profileDir,
rulesDir, rulesDir,
getTargetRuleFilename,
mcpConfig, mcpConfig,
mcpConfigName, mcpConfigName,
onPostConvertBrandRules mcpConfigPath,
getTargetRuleFilename,
onPostConvertRulesProfile
}; };

View File

@@ -1,11 +1,12 @@
// Windsurf conversion profile for rule-transformer // Windsurf conversion profile for rule-transformer
import path from 'path'; import path from 'path';
const brandName = 'Windsurf'; const profileName = 'Windsurf';
const brandDir = '.windsurf'; const profileDir = '.windsurf';
const rulesDir = '.windsurf/rules'; const rulesDir = '.windsurf/rules';
const mcpConfig = true; const mcpConfig = true;
const mcpConfigName = 'mcp.json'; const mcpConfigName = 'mcp.json';
const mcpConfigPath = `${profileDir}/${mcpConfigName}`;
// File name mapping (specific files with naming changes) // File name mapping (specific files with naming changes)
const fileMap = { const fileMap = {
@@ -42,8 +43,8 @@ const globalReplacements = [
]; ];
const conversionConfig = { const conversionConfig = {
// Product and brand name replacements // Profile name replacements
brandTerms: [ profileTerms: [
{ from: /cursor\.so/g, to: 'windsurf.com' }, { from: /cursor\.so/g, to: 'windsurf.com' },
{ from: /\[cursor\.so\]/g, to: '[windsurf.com]' }, { from: /\[cursor\.so\]/g, to: '[windsurf.com]' },
{ from: /href="https:\/\/cursor\.so/g, to: 'href="https://windsurf.com' }, { from: /href="https:\/\/cursor\.so/g, to: 'href="https://windsurf.com' },
@@ -132,10 +133,11 @@ export {
conversionConfig, conversionConfig,
fileMap, fileMap,
globalReplacements, globalReplacements,
brandName, profileName,
brandDir, profileDir,
rulesDir, rulesDir,
getTargetRuleFilename,
mcpConfig, mcpConfig,
mcpConfigName mcpConfigName,
mcpConfigPath,
getTargetRuleFilename
}; };

32
src/constants/profiles.js Normal file
View File

@@ -0,0 +1,32 @@
/**
* @typedef {'cline' | 'cursor' | 'roo' | 'windsurf'} RulesProfile
*/
/**
* Available rules profiles for project initialization and rules command
*
* ⚠️ SINGLE SOURCE OF TRUTH: This is the authoritative list of all supported rules profiles.
* This constant is used directly throughout the codebase (previously aliased as PROFILE_NAMES).
*
* @type {RulesProfile[]}
* @description Defines possible rules profile sets:
* - cline: Cline IDE rules
* - cursor: Cursor IDE rules (default)
* - roo: Roo Code IDE rules
* - windsurf: Windsurf IDE rules
*
* To add a new rules profile:
* 1. Add the profile name to this array
* 2. Create a profile file in scripts/profiles/{profile}.js
* 3. Export it as {profile}Profile in scripts/profiles/index.js
*/
export const RULES_PROFILES = ['cline', 'cursor', 'roo', 'windsurf'];
/**
* Check if a given rules profile is valid
* @param {string} rulesProfile - The rules profile to check
* @returns {boolean} True if the rules profile is valid, false otherwise
*/
export function isValidRulesProfile(rulesProfile) {
return RULES_PROFILES.includes(rulesProfile);
}

View File

@@ -1,35 +0,0 @@
/**
* @typedef {'cursor' | 'roo' | 'windsurf' | 'cline'} BrandRule
*/
/**
* Available brand rules for project initialization
*
* @type {BrandRule[]}
* @description Defines possible brand rule sets:
* - cursor: Cursor IDE rules (default)
* - roo: Roo Code IDE rules
* - windsurf: Windsurf IDE rules
* - cline: Cline IDE rules
*
* To add a new brand:
* 1. Add the brand name to this array
* 2. Create a profile file in scripts/profiles/{brand}.js
* 3. Export it in scripts/profiles/index.js
* 4. Add it to BRAND_PROFILES in src/utils/rule-transformer.js
*/
export const BRAND_RULE_OPTIONS = [
'cursor',
'roo',
'windsurf',
'cline'
];
/**
* Check if a given brand rule is valid
* @param {string} brandRule - The brand rule to check
* @returns {boolean} True if the brand rule is valid, false otherwise
*/
export function isValidBrandRule(brandRule) {
return BRAND_RULE_OPTIONS.includes(brandRule);
}

View File

@@ -2,19 +2,19 @@ import chalk from 'chalk';
import boxen from 'boxen'; import boxen from 'boxen';
/** /**
* Confirm removing brand rules (destructive operation) * Confirm removing profile rules (destructive operation)
* @param {string[]} brands - Array of brand names to remove * @param {string[]} profiles - Array of profile names to remove
* @returns {Promise<boolean>} - Promise resolving to true if user confirms, false otherwise * @returns {Promise<boolean>} - Promise resolving to true if user confirms, false otherwise
*/ */
async function confirmRulesRemove(brands) { async function confirmProfilesRemove(profiles) {
const brandList = brands const profileList = profiles
.map((b) => b.charAt(0).toUpperCase() + b.slice(1)) .map((b) => b.charAt(0).toUpperCase() + b.slice(1))
.join(', '); .join(', ');
console.log( console.log(
boxen( boxen(
chalk.yellow( chalk.yellow(
`WARNING: This will permanently delete all rules and configuration for: ${brandList}. `WARNING: This will permanently delete all rules and configuration for: ${profileList}.
This will remove the entire .[brand] directory for each selected brand.\n\nAre you sure you want to proceed?` This will remove the entire .[profile] directory for each selected profile.\n\nAre you sure you want to proceed?`
), ),
{ padding: 1, borderColor: 'yellow', borderStyle: 'round' } { padding: 1, borderColor: 'yellow', borderStyle: 'round' }
) )
@@ -31,4 +31,4 @@ This will remove the entire .[brand] directory for each selected brand.\n\nAre y
return confirm; return confirm;
} }
export { confirmRulesRemove }; export { confirmProfilesRemove };

View File

@@ -3,10 +3,12 @@ import path from 'path';
import { log } from '../../scripts/modules/utils.js'; import { log } from '../../scripts/modules/utils.js';
// Structure matches project conventions (see scripts/init.js) // Structure matches project conventions (see scripts/init.js)
export function setupMCPConfiguration(configDir) { export function setupMCPConfiguration(projectDir, mcpConfigPath) {
const mcpPath = path.join(configDir, 'mcp.json'); // Build the full path to the MCP config file
const mcpPath = path.join(projectDir, mcpConfigPath);
const configDir = path.dirname(mcpPath);
log('info', 'Setting up MCP configuration for brand integration...'); log('info', `Setting up MCP configuration at ${mcpPath}...`);
// New MCP config to be added - references the installed package // New MCP config to be added - references the installed package
const newMCPServer = { const newMCPServer = {
@@ -26,10 +28,12 @@ export function setupMCPConfiguration(configDir) {
} }
} }
}; };
// Create config directory if it doesn't exist // Create config directory if it doesn't exist
if (!fs.existsSync(configDir)) { if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true }); fs.mkdirSync(configDir, { recursive: true });
} }
if (fs.existsSync(mcpPath)) { if (fs.existsSync(mcpPath)) {
log( log(
'info', 'info',
@@ -94,6 +98,6 @@ export function setupMCPConfiguration(configDir) {
mcpServers: newMCPServer mcpServers: newMCPServer
}; };
fs.writeFileSync(mcpPath, JSON.stringify(newMCPConfig, null, 4)); fs.writeFileSync(mcpPath, JSON.stringify(newMCPConfig, null, 4));
log('success', 'Created MCP configuration file'); log('success', `Created MCP configuration file at ${mcpPath}`);
} }
} }

View File

@@ -1,45 +1,59 @@
/** /**
* Rule Transformer Module * Rule Transformer Module
* Handles conversion of Cursor rules to brand rules * Handles conversion of Cursor rules to profile rules
* *
* This module procedurally generates .{brand}/rules files from assets/rules files, * This module procedurally generates .{profile}/rules files from assets/rules files,
* eliminating the need to maintain both sets of files manually. * eliminating the need to maintain both sets of files manually.
*/ */
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url';
import { log } from '../../scripts/modules/utils.js'; import { log } from '../../scripts/modules/utils.js';
// Import the shared MCP configuration helper // Import the shared MCP configuration helper
import { setupMCPConfiguration } from './mcp-utils.js'; import { setupMCPConfiguration } from './mcp-utils.js';
// --- Centralized Brand Helpers --- // Import profile constants (single source of truth)
import { clineProfile, cursorProfile, rooProfile, windsurfProfile } from '../../scripts/profiles/index.js'; import { RULES_PROFILES } from '../constants/profiles.js';
export const BRAND_PROFILES = { // --- Profile Imports ---
cline: clineProfile, import * as profilesModule from '../../scripts/profiles/index.js';
cursor: cursorProfile,
roo: rooProfile,
windsurf: windsurfProfile
};
export const BRAND_NAMES = Object.keys(BRAND_PROFILES); export function isValidProfile(profile) {
return RULES_PROFILES.includes(profile);
export function isValidBrand(brand) {
return BRAND_NAMES.includes(brand);
}
export function getBrandProfile(brand) {
return BRAND_PROFILES[brand];
} }
/** /**
* Replace basic Cursor terms with brand equivalents * Get rules profile by name
* @param {string} name - Profile name
* @returns {Object|null} Profile object or null if not found
*/
export function getRulesProfile(name) {
if (!isValidProfile(name)) {
return null;
}
// Get the profile from the imported profiles module
const profileKey = `${name}Profile`;
const profile = profilesModule[profileKey];
if (!profile) {
throw new Error(
`Profile not found: static import missing for '${name}'. Valid profiles: ${RULES_PROFILES.join(', ')}`
);
}
return profile;
}
/**
* Replace basic Cursor terms with profile equivalents
*/ */
function replaceBasicTerms(content, conversionConfig) { function replaceBasicTerms(content, conversionConfig) {
let result = content; let result = content;
// Apply brand term replacements // Apply profile term replacements
conversionConfig.brandTerms.forEach((pattern) => { conversionConfig.profileTerms.forEach((pattern) => {
if (typeof pattern.to === 'function') { if (typeof pattern.to === 'function') {
result = result.replace(pattern.from, pattern.to); result = result.replace(pattern.from, pattern.to);
} else { } else {
@@ -56,7 +70,7 @@ function replaceBasicTerms(content, conversionConfig) {
} }
/** /**
* Replace Cursor tool references with brand tool equivalents * Replace Cursor tool references with profile tool equivalents
*/ */
function replaceToolReferences(content, conversionConfig) { function replaceToolReferences(content, conversionConfig) {
let result = content; let result = content;
@@ -87,7 +101,7 @@ function replaceToolReferences(content, conversionConfig) {
} }
/** /**
* Update documentation URLs to point to brand documentation * Update documentation URLs to point to profile documentation
*/ */
function updateDocReferences(content, conversionConfig) { function updateDocReferences(content, conversionConfig) {
let result = content; let result = content;
@@ -115,8 +129,7 @@ function updateFileReferences(content, conversionConfig) {
/** /**
* Main transformation function that applies all conversions * Main transformation function that applies all conversions
*/ */
// Main transformation function that applies all conversions, now brand-generic function transformCursorToProfileRules(
function transformCursorToBrandRules(
content, content,
conversionConfig, conversionConfig,
globalReplacements = [] globalReplacements = []
@@ -128,7 +141,7 @@ function transformCursorToBrandRules(
result = updateDocReferences(result, conversionConfig); result = updateDocReferences(result, conversionConfig);
result = updateFileReferences(result, conversionConfig); result = updateFileReferences(result, conversionConfig);
// Apply any global/catch-all replacements from the brand profile // Apply any global/catch-all replacements from the profile
// Super aggressive failsafe pass to catch any variations we might have missed // Super aggressive failsafe pass to catch any variations we might have missed
// This ensures critical transformations are applied even in contexts we didn't anticipate // This ensures critical transformations are applied even in contexts we didn't anticipate
globalReplacements.forEach((pattern) => { globalReplacements.forEach((pattern) => {
@@ -143,21 +156,21 @@ function transformCursorToBrandRules(
} }
/** /**
* Convert a single Cursor rule file to brand rule format * Convert a single Cursor rule file to profile rule format
*/ */
function convertRuleToBrandRule(sourcePath, targetPath, profile) { export function convertRuleToProfileRule(sourcePath, targetPath, profile) {
const { conversionConfig, brandName, globalReplacements } = profile; const { conversionConfig, globalReplacements } = profile;
try { try {
log( log(
'debug', 'debug',
`Converting Cursor rule ${path.basename(sourcePath)} to ${brandName} rule ${path.basename(targetPath)}` `Converting Cursor rule ${path.basename(sourcePath)} to ${profile.profileName} rule ${path.basename(targetPath)}`
); );
// Read source content // Read source content
const content = fs.readFileSync(sourcePath, 'utf8'); const content = fs.readFileSync(sourcePath, 'utf8');
// Transform content // Transform content
const transformedContent = transformCursorToBrandRules( const transformedContent = transformCursorToProfileRules(
content, content,
conversionConfig, conversionConfig,
globalReplacements globalReplacements
@@ -187,152 +200,141 @@ function convertRuleToBrandRule(sourcePath, targetPath, profile) {
} }
/** /**
* Process all Cursor rules and convert to brand rules * Convert all Cursor rules to profile rules for a specific profile
*/ */
function convertAllRulesToBrandRules(projectDir, profile) { export function convertAllRulesToProfileRules(projectDir, profile) {
const { fileMap, brandName, rulesDir, mcpConfig, mcpConfigName } = profile; const sourceDir = fileURLToPath(new URL('../../assets/rules', import.meta.url));
// Use assets/rules as the source of rules instead of .cursor/rules const targetDir = path.join(projectDir, profile.rulesDir);
const cursorRulesDir = path.join(projectDir, 'assets', 'rules');
const brandRulesDir = path.join(projectDir, rulesDir);
if (!fs.existsSync(cursorRulesDir)) { // Ensure target directory exists
log('warn', `Cursor rules directory not found: ${cursorRulesDir}`); if (!fs.existsSync(targetDir)) {
return { success: 0, failed: 0 }; fs.mkdirSync(targetDir, { recursive: true });
} }
// Ensure brand rules directory exists // Setup MCP configuration if enabled
if (!fs.existsSync(brandRulesDir)) { if (profile.mcpConfig !== false) {
fs.mkdirSync(brandRulesDir, { recursive: true }); setupMCPConfiguration(
log('debug', `Created ${brandName} rules directory: ${brandRulesDir}`); projectDir,
// Also create MCP configuration in the brand directory if enabled profile.mcpConfigPath
if (mcpConfig !== false) { );
const brandDir = profile.brandDir;
setupMCPConfiguration(path.join(projectDir, brandDir), mcpConfigName);
}
} }
// Count successful and failed conversions
let success = 0; let success = 0;
let failed = 0; let failed = 0;
// Process each file from assets/rules listed in fileMap // Use fileMap to determine which files to copy
const getTargetRuleFilename = profile.getTargetRuleFilename || ((f) => f); const sourceFiles = Object.keys(profile.fileMap);
Object.keys(profile.fileMap).forEach((file) => {
const sourcePath = path.join(cursorRulesDir, file);
if (fs.existsSync(sourcePath)) {
const targetFilename = getTargetRuleFilename(file);
const targetPath = path.join(brandRulesDir, targetFilename);
// Convert the file for (const sourceFile of sourceFiles) {
if (convertRuleToBrandRule(sourcePath, targetPath, profile)) { try {
success++; const sourcePath = path.join(sourceDir, sourceFile);
} else {
failed++; // Check if source file exists
if (!fs.existsSync(sourcePath)) {
log('warn', `[Rule Transformer] Source file not found: ${sourceFile}, skipping`);
continue;
} }
} else {
const targetFilename = profile.getTargetRuleFilename
? profile.getTargetRuleFilename(sourceFile)
: sourceFile;
const targetPath = path.join(targetDir, targetFilename);
// Read source content
let content = fs.readFileSync(sourcePath, 'utf8');
// Apply transformations
content = transformCursorToProfileRules(
content,
profile.conversionConfig,
profile.globalReplacements
);
// Write to target
fs.writeFileSync(targetPath, content, 'utf8');
success++;
log( log(
'warn', 'debug',
`File listed in fileMap not found in rules dir: ${sourcePath}` `[Rule Transformer] Converted ${sourceFile} -> ${targetFilename} for ${profile.profileName}`
);
} catch (error) {
failed++;
log(
'error',
`[Rule Transformer] Failed to convert ${sourceFile} for ${profile.profileName}: ${error.message}`
); );
} }
}); }
log(
'debug',
`Rule conversion complete: ${success} successful, ${failed} failed`
);
// Call post-processing hook if defined (e.g., for Roo's rules-*mode* folders) // Call post-processing hook if defined (e.g., for Roo's rules-*mode* folders)
if (typeof profile.onPostConvertBrandRules === 'function') { if (typeof profile.onPostConvertRulesProfile === 'function') {
profile.onPostConvertBrandRules(projectDir); profile.onPostConvertRulesProfile(projectDir);
} }
return { success, failed }; return { success, failed };
} }
/** /**
* Remove a brand's rules directory, its mcp.json, and the parent brand folder recursively. * Remove profile rules for a specific profile
* @param {string} projectDir - The root directory of the project * @param {string} projectDir - Target project directory
* @param {object} profile - The brand profile object * @param {Object} profile - Profile configuration
* @returns {boolean} - True if removal succeeded, false otherwise * @returns {Object} Result object
*/ */
function removeBrandRules(projectDir, profile) { export function removeProfileRules(projectDir, profile) {
const { brandName, rulesDir, mcpConfig, mcpConfigName } = profile; const targetDir = path.join(projectDir, profile.rulesDir);
const brandDir = profile.brandDir; const profileDir = path.join(projectDir, profile.profileDir);
const brandRulesDir = path.join(projectDir, rulesDir); const mcpConfigPath = path.join(projectDir, profile.mcpConfigPath);
const mcpPath = path.join(projectDir, brandDir, mcpConfigName);
const result = { let result = {
brandName, profileName: profile.profileName,
mcpConfigRemoved: false, success: false,
rulesDirRemoved: false,
brandFolderRemoved: false,
skipped: false, skipped: false,
error: null, error: null
success: false // Overall success for this brand
}; };
if (mcpConfig !== false && fs.existsSync(mcpPath)) {
try {
fs.unlinkSync(mcpPath);
result.mcpConfigRemoved = true;
} catch (e) {
const errorMessage = `Failed to remove MCP configuration at ${mcpPath}: ${e.message}`;
log('warn', errorMessage);
result.error = result.error
? `${result.error}; ${errorMessage}`
: errorMessage;
}
}
// Remove rules directory
if (fs.existsSync(brandRulesDir)) {
try {
fs.rmSync(brandRulesDir, { recursive: true, force: true });
result.rulesDirRemoved = true;
} catch (e) {
const errorMessage = `Failed to remove rules directory at ${brandRulesDir}: ${e.message}`;
log('warn', errorMessage);
result.error = result.error
? `${result.error}; ${errorMessage}`
: errorMessage;
}
}
// Remove brand folder
try { try {
fs.rmSync(brandDir, { recursive: true, force: true }); // Remove rules directory
result.brandFolderRemoved = true; if (fs.existsSync(targetDir)) {
} catch (e) { fs.rmSync(targetDir, { recursive: true, force: true });
const errorMessage = `Failed to remove brand folder at ${brandDir}: ${e.message}`; log('debug', `[Rule Transformer] Removed rules directory: ${targetDir}`);
log('warn', errorMessage);
result.error = result.error
? `${result.error}; ${errorMessage}`
: errorMessage;
}
// Call onRemoveBrandRules hook if present
if (typeof profile.onRemoveBrandRules === 'function') {
try {
profile.onRemoveBrandRules(projectDir);
} catch (e) {
const errorMessage = `Error in onRemoveBrandRules for ${brandName}: ${e.message}`;
log('warn', errorMessage);
result.error = result.error
? `${result.error}; ${errorMessage}`
: errorMessage;
} }
// Remove MCP config if it exists
if (fs.existsSync(mcpConfigPath)) {
fs.rmSync(mcpConfigPath, { force: true });
log('debug', `[Rule Transformer] Removed MCP config: ${mcpConfigPath}`);
}
// Call removal hook if defined
if (typeof profile.onRemoveRulesProfile === 'function') {
profile.onRemoveRulesProfile(projectDir);
}
// Remove profile directory if empty
if (fs.existsSync(profileDir)) {
const remaining = fs.readdirSync(profileDir);
if (remaining.length === 0) {
fs.rmSync(profileDir, { recursive: true, force: true });
log(
'debug',
`[Rule Transformer] Removed empty profile directory: ${profileDir}`
);
}
}
result.success = true;
log(
'debug',
`[Rule Transformer] Successfully removed ${profile.profileName} rules from ${projectDir}`
);
} catch (error) {
result.error = error.message;
log(
'error',
`[Rule Transformer] Failed to remove ${profile.profileName} rules: ${error.message}`
);
} }
result.success =
result.mcpConfigRemoved ||
result.rulesDirRemoved ||
result.brandFolderRemoved;
return result; return result;
} }
export {
convertAllRulesToBrandRules,
convertRuleToBrandRule,
removeBrandRules
};

View File

@@ -1,13 +1,13 @@
import readline from 'readline'; import readline from 'readline';
import inquirer from 'inquirer'; import inquirer from 'inquirer';
import chalk from 'chalk'; import chalk from 'chalk';
import { BRAND_PROFILES, BRAND_NAMES } from './rule-transformer.js'; import { log } from '../../scripts/modules/utils.js';
import { getRulesProfile } from './rule-transformer.js';
import { RULES_PROFILES } from '../constants/profiles.js';
// Dynamically generate availableBrandRules from BRAND_NAMES and brand profiles // Dynamically generate availableRulesProfiles from RULES_PROFILES
const availableBrandRules = BRAND_NAMES.map((name) => { const availableRulesProfiles = RULES_PROFILES.map((name) => {
const displayName = const displayName = getProfileDisplayName(name);
BRAND_PROFILES[name]?.brandName ||
name.charAt(0).toUpperCase() + name.slice(1);
return { return {
name: name === 'cursor' ? `${displayName} (default)` : displayName, name: name === 'cursor' ? `${displayName} (default)` : displayName,
value: name value: name
@@ -15,18 +15,26 @@ const availableBrandRules = BRAND_NAMES.map((name) => {
}); });
/** /**
* Runs the interactive rules setup flow (brand rules selection only) * Get the display name for a profile
* @returns {Promise<string[]>} The selected brand rules */
function getProfileDisplayName(name) {
const profile = getRulesProfile(name);
return profile?.profileName || 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 brand rules to include in your project. * Launches an interactive prompt for selecting which profile rules to include in your project.
* *
* This function dynamically lists all available brands (from BRAND_PROFILES) and presents them as checkboxes. * This function dynamically lists all available profiles (from RULES_PROFILES) and presents them as checkboxes.
* The user must select at least one brand (default: cursor). The result is an array of selected brand names. * The user must select at least one profile (default: cursor). 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. * 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 brand rule names (e.g., ['cursor', 'windsurf']) * @returns {Promise<string[]>} Array of selected profile rule names (e.g., ['cursor', 'windsurf'])
*/ */
export async function runInteractiveRulesSetup() { export async function runInteractiveRulesSetup() {
console.log( console.log(
@@ -34,14 +42,14 @@ export async function runInteractiveRulesSetup() {
'\nRules help enforce best practices and conventions for Task Master.' '\nRules help enforce best practices and conventions for Task Master.'
) )
); );
const brandRulesQuestion = { const rulesProfilesQuestion = {
type: 'checkbox', type: 'checkbox',
name: 'brandRules', name: 'rulesProfiles',
message: 'Which IDEs would you like rules included for?', message: 'Which IDEs would you like rules included for?',
choices: availableBrandRules, choices: availableRulesProfiles,
default: ['cursor'], default: ['cursor'],
validate: (input) => input.length > 0 || 'You must select at least one.' validate: (input) => input.length > 0 || 'You must select at least one.'
}; };
const { brandRules } = await inquirer.prompt([brandRulesQuestion]); const { rulesProfiles } = await inquirer.prompt([rulesProfilesQuestion]);
return brandRules; return rulesProfiles;
} }

View File

@@ -14,8 +14,8 @@ describe('Cursor Profile Initialization Functionality', () => {
cursorProfileContent = fs.readFileSync(cursorJsPath, 'utf8'); cursorProfileContent = fs.readFileSync(cursorJsPath, 'utf8');
}); });
test('cursor.js exports correct brandName and rulesDir', () => { test('cursor.js exports correct profileName and rulesDir', () => {
expect(cursorProfileContent).toContain("const brandName = 'Cursor'"); expect(cursorProfileContent).toContain("const profileName = 'Cursor'");
expect(cursorProfileContent).toContain("const rulesDir = '.cursor/rules'"); expect(cursorProfileContent).toContain("const rulesDir = '.cursor/rules'");
}); });

View File

@@ -22,7 +22,7 @@ describe('Roo Files Inclusion in Package', () => {
const rooJsContent = fs.readFileSync(rooJsPath, 'utf8'); const rooJsContent = fs.readFileSync(rooJsPath, 'utf8');
// Check for the main handler function // Check for the main handler function
expect(rooJsContent.includes('onAddBrandRules(targetDir)')).toBe(true); expect(rooJsContent.includes('onAddRulesProfile(targetDir)')).toBe(true);
// Check for general recursive copy of assets/roocode // Check for general recursive copy of assets/roocode
expect( expect(

View File

@@ -11,36 +11,31 @@ describe('Roo Profile Initialization Functionality', () => {
rooProfileContent = fs.readFileSync(rooJsPath, 'utf8'); rooProfileContent = fs.readFileSync(rooJsPath, 'utf8');
}); });
test('roo.js profile ensures Roo directory structure via onAddBrandRules', () => { test('roo.js profile ensures Roo directory structure via onAddRulesProfile', () => {
// Check if onAddBrandRules function exists // Check if onAddRulesProfile function exists
expect(rooProfileContent).toContain('onAddBrandRules(targetDir)'); expect(rooProfileContent).toContain('onAddRulesProfile(targetDir)');
// Check for the general copy of assets/roocode which includes .roo base structure // Check for the general copy of assets/roocode which includes .roo base structure
expect(rooProfileContent).toContain( expect(rooProfileContent).toContain(
'copyRecursiveSync(sourceDir, targetDir)' "const sourceDir = path.resolve(__dirname, '../../assets/roocode');"
); );
expect(rooProfileContent).toContain( expect(rooProfileContent).toContain(
"path.resolve(__dirname, '../../assets/roocode')" 'copyRecursiveSync(sourceDir, targetDir);'
); // Verifies sourceDir definition );
// Check for the loop that processes rooModes // Check for the specific .roo modes directory handling
expect(rooProfileContent).toContain('for (const mode of rooModes)'); expect(rooProfileContent).toContain(
"const rooModesDir = path.join(sourceDir, '.roo');"
// Check for creation of mode-specific rule directories (e.g., .roo/rules-architect) );
// This is the line: if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true }); expect(rooProfileContent).toContain(
expect(rooProfileContent).toContain( "const rooModes = ['architect', 'ask', 'boomerang', 'code', 'debug', 'test'];"
'fs.mkdirSync(destDir, { recursive: true });'
); );
expect(rooProfileContent).toContain('const destDir = path.dirname(dest);'); // part of the same logic block
}); });
test('roo.js profile copies .roomodes file via onAddBrandRules', () => { test('roo.js profile copies .roomodes file via onAddRulesProfile', () => {
expect(rooProfileContent).toContain('onAddBrandRules(targetDir)'); expect(rooProfileContent).toContain('onAddRulesProfile(targetDir)');
// Check for the specific .roomodes copy logic // Check for the specific .roomodes copy logic
expect(rooProfileContent).toContain(
'fs.copyFileSync(roomodesSrc, roomodesDest);'
);
expect(rooProfileContent).toContain( expect(rooProfileContent).toContain(
"const roomodesSrc = path.join(sourceDir, '.roomodes');" "const roomodesSrc = path.join(sourceDir, '.roomodes');"
); );
@@ -48,27 +43,20 @@ describe('Roo Profile Initialization Functionality', () => {
"const roomodesDest = path.join(targetDir, '.roomodes');" "const roomodesDest = path.join(targetDir, '.roomodes');"
); );
expect(rooProfileContent).toContain( expect(rooProfileContent).toContain(
"path.resolve(__dirname, '../../assets/roocode')" 'fs.copyFileSync(roomodesSrc, roomodesDest);'
); // sourceDir for roomodesSrc );
}); });
test('roo.js profile copies mode-specific rule files via onAddBrandRules', () => { test('roo.js profile copies mode-specific rule files via onAddRulesProfile', () => {
expect(rooProfileContent).toContain('onAddBrandRules(targetDir)'); expect(rooProfileContent).toContain('onAddRulesProfile(targetDir)');
expect(rooProfileContent).toContain('for (const mode of rooModes)'); expect(rooProfileContent).toContain('for (const mode of rooModes)');
// Check for the specific mode rule file copy logic // Check for the specific mode rule file copy logic
expect(rooProfileContent).toContain('fs.copyFileSync(src, dest);');
// Check source path construction for mode rules
expect(rooProfileContent).toContain( expect(rooProfileContent).toContain(
'const src = path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`);' 'const src = path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`);'
); );
// Check destination path construction for mode rules
expect(rooProfileContent).toContain( expect(rooProfileContent).toContain(
"const dest = path.join(targetDir, '.roo', `rules-${mode}`, `${mode}-rules`);" "const dest = path.join(targetDir, '.roo', `rules-${mode}`, `${mode}-rules`);"
); );
expect(rooProfileContent).toContain(
"const rooModesDir = path.join(sourceDir, '.roo');"
); // part of src path
}); });
}); });

View File

@@ -1,42 +1,95 @@
import { jest } from '@jest/globals';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import os from 'os';
import { execSync } from 'child_process';
describe('Rules Files Inclusion in Package', () => { describe('Rules Files Inclusion in Package', () => {
test('package.json includes assets/** in the "files" array for rules files', () => { // This test verifies that the required rules files are included in the final package
test('package.json includes assets/** in the "files" array for rules source files', () => {
// Read the package.json file
const packageJsonPath = path.join(process.cwd(), 'package.json'); const packageJsonPath = path.join(process.cwd(), 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
// Check if assets/** is included in the files array (which contains rules files)
expect(packageJson.files).toContain('assets/**'); expect(packageJson.files).toContain('assets/**');
}); });
test('all rules files exist in assets/rules directory', () => { test('source rules files exist in assets/rules directory', () => {
// Verify that the actual rules files exist
const rulesDir = path.join(process.cwd(), 'assets', 'rules'); const rulesDir = path.join(process.cwd(), 'assets', 'rules');
expect(fs.existsSync(rulesDir)).toBe(true);
// Check for the 4 files that currently exist
const expectedFiles = [ const expectedFiles = [
'ai_providers.mdc',
'ai_services.mdc',
'architecture.mdc',
'changeset.mdc',
'commands.mdc',
'cursor_rules.mdc',
'dependencies.mdc',
'dev_workflow.mdc', 'dev_workflow.mdc',
'glossary.mdc',
'mcp.mdc',
'new_features.mdc',
'self_improve.mdc',
'taskmaster.mdc', 'taskmaster.mdc',
'tasks.mdc', 'self_improve.mdc',
'tests.mdc', 'cursor_rules.mdc'
'ui.mdc',
'utilities.mdc'
]; ];
for (const file of expectedFiles) {
expect(fs.existsSync(path.join(rulesDir, file))).toBe(true); expectedFiles.forEach((file) => {
const filePath = path.join(rulesDir, file);
expect(fs.existsSync(filePath)).toBe(true);
});
});
test('roo.js profile contains logic for Roo directory creation and file copying', () => {
// Read the roo.js profile file
const rooJsPath = path.join(process.cwd(), 'scripts', 'profiles', 'roo.js');
const rooJsContent = fs.readFileSync(rooJsPath, 'utf8');
// Check for the main handler function
expect(rooJsContent.includes('onAddRulesProfile(targetDir)')).toBe(true);
// Check for general recursive copy of assets/roocode
expect(
rooJsContent.includes('copyRecursiveSync(sourceDir, targetDir)')
).toBe(true);
// Check for .roomodes file copying logic (source and destination paths)
expect(rooJsContent.includes("path.join(sourceDir, '.roomodes')")).toBe(
true
);
expect(rooJsContent.includes("path.join(targetDir, '.roomodes')")).toBe(
true
);
// Check for mode-specific rule file copying logic
expect(rooJsContent.includes('for (const mode of rooModes)')).toBe(true);
expect(
rooJsContent.includes(
'path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`)'
)
).toBe(true);
expect(
rooJsContent.includes(
"path.join(targetDir, '.roo', `rules-${mode}`, `${mode}-rules`)"
)
).toBe(true);
// Check for definition of rooModes array and all modes
const rooModesArrayRegex = /const rooModes\s*=\s*\[([^\]]+)\]\s*;?/;
const rooModesMatch = rooJsContent.match(rooModesArrayRegex);
expect(rooModesMatch).not.toBeNull();
if (rooModesMatch) {
expect(rooModesMatch[1].includes('architect')).toBe(true);
expect(rooModesMatch[1].includes('ask')).toBe(true);
expect(rooModesMatch[1].includes('boomerang')).toBe(true);
expect(rooModesMatch[1].includes('code')).toBe(true);
expect(rooModesMatch[1].includes('debug')).toBe(true);
expect(rooModesMatch[1].includes('test')).toBe(true);
} }
}); });
test('assets/rules directory is not empty', () => { test('source Roo files exist in assets directory', () => {
const rulesDir = path.join(process.cwd(), 'assets', 'rules'); // Verify that the source files for Roo integration exist
const files = fs.readdirSync(rulesDir).filter((f) => !f.startsWith('.')); expect(
expect(files.length).toBeGreaterThan(0); fs.existsSync(path.join(process.cwd(), 'assets', 'roocode', '.roo'))
).toBe(true);
expect(
fs.existsSync(path.join(process.cwd(), 'assets', 'roocode', '.roomodes'))
).toBe(true);
}); });
}); });

View File

@@ -14,8 +14,8 @@ describe('Windsurf Profile Initialization Functionality', () => {
windsurfProfileContent = fs.readFileSync(windsurfJsPath, 'utf8'); windsurfProfileContent = fs.readFileSync(windsurfJsPath, 'utf8');
}); });
test('windsurf.js exports correct brandName and rulesDir', () => { test('windsurf.js exports correct profileName and rulesDir', () => {
expect(windsurfProfileContent).toContain("const brandName = 'Windsurf'"); expect(windsurfProfileContent).toContain("const profileName = 'Windsurf'");
expect(windsurfProfileContent).toContain( expect(windsurfProfileContent).toContain(
"const rulesDir = '.windsurf/rules'" "const rulesDir = '.windsurf/rules'"
); );

View File

@@ -1102,31 +1102,31 @@ describe('rules command', () => {
mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
}); });
test('should handle rules add <brand> command', async () => { test('should handle rules add <profile> command', async () => {
// Simulate: task-master rules add roo // Simulate: task-master rules add roo
await program.parseAsync(['rules', 'add', 'roo'], { from: 'user' }); await program.parseAsync(['rules', 'add', 'roo'], { from: 'user' });
// Expect some log output indicating success // Expect some log output indicating success
expect(mockConsoleLog).toHaveBeenCalledWith( expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringMatching(/adding rules for brand: roo/i) expect.stringMatching(/adding rules for profile: roo/i)
); );
expect(mockConsoleLog).toHaveBeenCalledWith( expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringMatching(/completed adding rules for brand: roo/i) expect.stringMatching(/completed adding rules for profile: roo/i)
); );
// Should not exit with error // Should not exit with error
expect(mockExit).not.toHaveBeenCalledWith(1); expect(mockExit).not.toHaveBeenCalledWith(1);
}); });
test('should handle rules remove <brand> command', async () => { test('should handle rules remove <profile> command', async () => {
// Simulate: task-master rules remove roo --force // Simulate: task-master rules remove roo --force
await program.parseAsync(['rules', 'remove', 'roo', '--force'], { await program.parseAsync(['rules', 'remove', 'roo', '--force'], {
from: 'user' from: 'user'
}); });
// Expect some log output indicating removal // Expect some log output indicating removal
expect(mockConsoleLog).toHaveBeenCalledWith( expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringMatching(/removing rules for brand: roo/i) expect.stringMatching(/removing rules for profile: roo/i)
); );
expect(mockConsoleLog).toHaveBeenCalledWith( expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringMatching(/completed removal for brand: roo/i) expect.stringMatching(/completed removal for profile: roo/i)
); );
// Should not exit with error // Should not exit with error
expect(mockExit).not.toHaveBeenCalledWith(1); expect(mockExit).not.toHaveBeenCalledWith(1);

View File

@@ -0,0 +1,201 @@
import { RULES_PROFILES } from '../../src/constants/profiles.js';
import { getRulesProfile } from '../../src/utils/rule-transformer.js';
import path from 'path';
describe('MCP Configuration Validation', () => {
describe('Profile MCP Configuration Properties', () => {
const expectedMcpConfigurations = {
cursor: {
shouldHaveMcp: true,
expectedDir: '.cursor',
expectedConfigName: 'mcp.json',
expectedPath: '.cursor/mcp.json'
},
windsurf: {
shouldHaveMcp: true,
expectedDir: '.windsurf',
expectedConfigName: 'mcp.json',
expectedPath: '.windsurf/mcp.json'
},
roo: {
shouldHaveMcp: true,
expectedDir: '.roo',
expectedConfigName: 'mcp.json',
expectedPath: '.roo/mcp.json'
},
cline: {
shouldHaveMcp: false,
expectedDir: '.clinerules',
expectedConfigName: 'cline_mcp_settings.json',
expectedPath: '.clinerules/cline_mcp_settings.json'
}
};
Object.entries(expectedMcpConfigurations).forEach(([profileName, expected]) => {
test(`should have correct MCP configuration for ${profileName} profile`, () => {
const profile = getRulesProfile(profileName);
expect(profile).toBeDefined();
expect(profile.mcpConfig).toBe(expected.shouldHaveMcp);
expect(profile.profileDir).toBe(expected.expectedDir);
expect(profile.mcpConfigName).toBe(expected.expectedConfigName);
expect(profile.mcpConfigPath).toBe(expected.expectedPath);
});
});
});
describe('MCP Configuration Path Consistency', () => {
test('should ensure all profiles have consistent mcpConfigPath construction', () => {
RULES_PROFILES.forEach(profileName => {
const profile = getRulesProfile(profileName);
if (profile.mcpConfig !== false) {
const expectedPath = path.join(profile.profileDir, profile.mcpConfigName);
expect(profile.mcpConfigPath).toBe(expectedPath);
}
});
});
test('should ensure no two profiles have the same MCP config path', () => {
const mcpPaths = new Set();
RULES_PROFILES.forEach(profileName => {
const profile = getRulesProfile(profileName);
if (profile.mcpConfig !== false) {
expect(mcpPaths.has(profile.mcpConfigPath)).toBe(false);
mcpPaths.add(profile.mcpConfigPath);
}
});
});
test('should ensure all MCP-enabled profiles use proper directory structure', () => {
RULES_PROFILES.forEach(profileName => {
const profile = getRulesProfile(profileName);
if (profile.mcpConfig !== false) {
expect(profile.mcpConfigPath).toMatch(/^\.[\w-]+\/[\w_.]+$/);
}
});
});
test('should ensure all profiles have required MCP properties', () => {
RULES_PROFILES.forEach(profileName => {
const profile = getRulesProfile(profileName);
expect(profile).toHaveProperty('mcpConfig');
expect(profile).toHaveProperty('profileDir');
expect(profile).toHaveProperty('mcpConfigName');
expect(profile).toHaveProperty('mcpConfigPath');
});
});
});
describe('MCP Configuration File Names', () => {
test('should use standard mcp.json for MCP-enabled profiles', () => {
const standardMcpProfiles = ['cursor', 'windsurf', 'roo'];
standardMcpProfiles.forEach(profileName => {
const profile = getRulesProfile(profileName);
expect(profile.mcpConfigName).toBe('mcp.json');
});
});
test('should use profile-specific config name for non-MCP profiles', () => {
const profile = getRulesProfile('cline');
expect(profile.mcpConfigName).toBe('cline_mcp_settings.json');
});
});
describe('Profile Directory Structure', () => {
test('should ensure each profile has a unique directory', () => {
const profileDirs = new Set();
RULES_PROFILES.forEach(profileName => {
const profile = getRulesProfile(profileName);
expect(profileDirs.has(profile.profileDir)).toBe(false);
profileDirs.add(profile.profileDir);
});
});
test('should ensure profile directories follow expected naming convention', () => {
RULES_PROFILES.forEach(profileName => {
const profile = getRulesProfile(profileName);
expect(profile.profileDir).toMatch(/^\.[\w-]+$/);
});
});
});
describe('MCP Configuration Creation Logic', () => {
test('should indicate which profiles require MCP configuration creation', () => {
const mcpEnabledProfiles = RULES_PROFILES.filter(profileName => {
const profile = getRulesProfile(profileName);
return profile.mcpConfig !== false;
});
expect(mcpEnabledProfiles).toContain('cursor');
expect(mcpEnabledProfiles).toContain('windsurf');
expect(mcpEnabledProfiles).toContain('roo');
expect(mcpEnabledProfiles).not.toContain('cline');
});
test('should provide all necessary information for MCP config creation', () => {
RULES_PROFILES.forEach(profileName => {
const profile = getRulesProfile(profileName);
if (profile.mcpConfig !== false) {
expect(profile.mcpConfigPath).toBeDefined();
expect(typeof profile.mcpConfigPath).toBe('string');
expect(profile.mcpConfigPath.length).toBeGreaterThan(0);
}
});
});
});
describe('MCP Configuration Path Usage Verification', () => {
test('should verify that rule transformer functions use mcpConfigPath correctly', () => {
// This test verifies that the mcpConfigPath property exists and is properly formatted
// for use with the setupMCPConfiguration function
RULES_PROFILES.forEach(profileName => {
const profile = getRulesProfile(profileName);
if (profile.mcpConfig !== false) {
// Verify the path is properly formatted for path.join usage
expect(profile.mcpConfigPath.startsWith('/')).toBe(false);
expect(profile.mcpConfigPath).toContain('/');
// Verify it matches the expected pattern: profileDir/configName
const expectedPath = `${profile.profileDir}/${profile.mcpConfigName}`;
expect(profile.mcpConfigPath).toBe(expectedPath);
}
});
});
test('should verify that mcpConfigPath is properly constructed for path.join usage', () => {
RULES_PROFILES.forEach(profileName => {
const profile = getRulesProfile(profileName);
if (profile.mcpConfig !== false) {
// Test that path.join works correctly with the mcpConfigPath
const testProjectRoot = '/test/project';
const fullPath = path.join(testProjectRoot, profile.mcpConfigPath);
// Should result in a proper absolute path
expect(fullPath).toBe(`${testProjectRoot}/${profile.mcpConfigPath}`);
expect(fullPath).toContain(profile.profileDir);
expect(fullPath).toContain(profile.mcpConfigName);
}
});
});
});
describe('MCP Configuration Function Integration', () => {
test('should verify that setupMCPConfiguration receives the correct mcpConfigPath parameter', () => {
// This test verifies the integration between rule transformer and mcp-utils
RULES_PROFILES.forEach(profileName => {
const profile = getRulesProfile(profileName);
if (profile.mcpConfig !== false) {
// Verify that the mcpConfigPath can be used directly with setupMCPConfiguration
// The function signature is: setupMCPConfiguration(projectDir, mcpConfigPath)
expect(profile.mcpConfigPath).toBeDefined();
expect(typeof profile.mcpConfigPath).toBe('string');
// Verify the path structure is correct for the new function signature
const parts = profile.mcpConfigPath.split('/');
expect(parts).toHaveLength(2); // Should be profileDir/configName
expect(parts[0]).toBe(profile.profileDir);
expect(parts[1]).toBe(profile.mcpConfigName);
}
});
});
});
});

View File

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

View File

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

View File

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

View File

@@ -1,67 +1,204 @@
import { import {
BRAND_PROFILES, isValidProfile,
BRAND_NAMES, getRulesProfile
isValidBrand,
getBrandProfile
} from '../../src/utils/rule-transformer.js'; } from '../../src/utils/rule-transformer.js';
import { BRAND_RULE_OPTIONS } from '../../src/constants/rules.js'; import { RULES_PROFILES } from '../../src/constants/profiles.js';
describe('Rule Transformer - General', () => { describe('Rule Transformer - General', () => {
describe('Brand Configuration Validation', () => { describe('Profile Configuration Validation', () => {
it('should have BRAND_PROFILES that match BRAND_RULE_OPTIONS', () => { it('should use RULES_PROFILES as the single source of truth', () => {
// Ensure BRAND_PROFILES keys match the authoritative list from constants/rules.js // Ensure RULES_PROFILES is properly defined and contains expected profiles
const profileKeys = Object.keys(BRAND_PROFILES).sort(); expect(Array.isArray(RULES_PROFILES)).toBe(true);
const ruleOptions = [...BRAND_RULE_OPTIONS].sort(); expect(RULES_PROFILES.length).toBeGreaterThan(0);
expect(profileKeys).toEqual(ruleOptions); // Verify expected profiles are present
const expectedProfiles = ['cline', 'cursor', 'roo', 'windsurf'];
expectedProfiles.forEach(profile => {
expect(RULES_PROFILES).toContain(profile);
});
}); });
it('should have BRAND_NAMES derived from BRAND_PROFILES', () => { it('should validate profiles correctly with isValidProfile', () => {
const expectedNames = Object.keys(BRAND_PROFILES); // Test valid profiles
expect(BRAND_NAMES).toEqual(expectedNames); RULES_PROFILES.forEach((profile) => {
}); expect(isValidProfile(profile)).toBe(true);
it('should validate brands correctly with isValidBrand', () => {
// Test valid brands
BRAND_RULE_OPTIONS.forEach(brand => {
expect(isValidBrand(brand)).toBe(true);
}); });
// Test invalid brands // Test invalid profiles
expect(isValidBrand('invalid')).toBe(false); expect(isValidProfile('invalid')).toBe(false);
expect(isValidBrand('vscode')).toBe(false); expect(isValidProfile('')).toBe(false);
expect(isValidBrand('')).toBe(false); expect(isValidProfile(null)).toBe(false);
expect(isValidBrand(null)).toBe(false); expect(isValidProfile(undefined)).toBe(false);
expect(isValidBrand(undefined)).toBe(false);
}); });
it('should return correct brand profiles with getBrandProfile', () => { it('should return correct rules profile with getRulesProfile', () => {
BRAND_RULE_OPTIONS.forEach(brand => { // Test valid profiles
const profile = getBrandProfile(brand); RULES_PROFILES.forEach((profile) => {
expect(profile).toBeDefined(); const profileConfig = getRulesProfile(profile);
expect(profile.brandName.toLowerCase()).toBe(brand); expect(profileConfig).toBeDefined();
expect(profileConfig.profileName.toLowerCase()).toBe(profile);
}); });
// Test invalid brand // Test invalid profile - should return null
expect(getBrandProfile('invalid')).toBeUndefined(); expect(getRulesProfile('invalid')).toBeNull();
}); });
}); });
describe('Brand Profile Structure', () => { describe('Profile Structure', () => {
it('should have all required properties for each brand profile', () => { it('should have all required properties for each profile', () => {
BRAND_RULE_OPTIONS.forEach(brand => { RULES_PROFILES.forEach((profile) => {
const profile = BRAND_PROFILES[brand]; const profileConfig = getRulesProfile(profile);
// Check required properties // Check required properties
expect(profile).toHaveProperty('brandName'); expect(profileConfig).toHaveProperty('profileName');
expect(profile).toHaveProperty('conversionConfig'); expect(profileConfig).toHaveProperty('conversionConfig');
expect(profile).toHaveProperty('fileMap'); expect(profileConfig).toHaveProperty('fileMap');
expect(profile).toHaveProperty('rulesDir'); expect(profileConfig).toHaveProperty('rulesDir');
expect(profile).toHaveProperty('brandDir'); expect(profileConfig).toHaveProperty('profileDir');
// Verify brand name matches (brandName is capitalized in profiles) // Check that conversionConfig has required structure
expect(profile.brandName.toLowerCase()).toBe(brand); expect(profileConfig.conversionConfig).toHaveProperty('profileTerms');
expect(profileConfig.conversionConfig).toHaveProperty('toolNames');
expect(profileConfig.conversionConfig).toHaveProperty('toolContexts');
expect(profileConfig.conversionConfig).toHaveProperty('toolGroups');
expect(profileConfig.conversionConfig).toHaveProperty('docUrls');
expect(profileConfig.conversionConfig).toHaveProperty('fileReferences');
// Verify arrays are actually arrays
expect(Array.isArray(profileConfig.conversionConfig.profileTerms)).toBe(
true
);
expect(typeof profileConfig.conversionConfig.toolNames).toBe('object');
expect(Array.isArray(profileConfig.conversionConfig.toolContexts)).toBe(
true
);
expect(Array.isArray(profileConfig.conversionConfig.toolGroups)).toBe(
true
);
expect(Array.isArray(profileConfig.conversionConfig.docUrls)).toBe(
true
);
});
});
it('should have valid fileMap with required files for each profile', () => {
const expectedFiles = ['cursor_rules.mdc', 'dev_workflow.mdc', 'self_improve.mdc', 'taskmaster.mdc'];
RULES_PROFILES.forEach((profile) => {
const profileConfig = getRulesProfile(profile);
// Check that fileMap exists and is an object
expect(profileConfig.fileMap).toBeDefined();
expect(typeof profileConfig.fileMap).toBe('object');
expect(profileConfig.fileMap).not.toBeNull();
// Check that fileMap is not empty
const fileMapKeys = Object.keys(profileConfig.fileMap);
expect(fileMapKeys.length).toBeGreaterThan(0);
// Check that all expected source files are defined in fileMap
expectedFiles.forEach(expectedFile => {
expect(fileMapKeys).toContain(expectedFile);
expect(typeof profileConfig.fileMap[expectedFile]).toBe('string');
expect(profileConfig.fileMap[expectedFile].length).toBeGreaterThan(0);
});
// Verify fileMap has exactly the expected files
expect(fileMapKeys.sort()).toEqual(expectedFiles.sort());
}); });
}); });
}); });
});
describe('MCP Configuration Properties', () => {
it('should have all required MCP properties for each profile', () => {
RULES_PROFILES.forEach((profile) => {
const profileConfig = getRulesProfile(profile);
// Check MCP-related properties exist
expect(profileConfig).toHaveProperty('mcpConfig');
expect(profileConfig).toHaveProperty('mcpConfigName');
expect(profileConfig).toHaveProperty('mcpConfigPath');
// Check types
expect(typeof profileConfig.mcpConfig).toBe('boolean');
expect(typeof profileConfig.mcpConfigName).toBe('string');
expect(typeof profileConfig.mcpConfigPath).toBe('string');
// Check that mcpConfigPath is properly constructed
expect(profileConfig.mcpConfigPath).toBe(
`${profileConfig.profileDir}/${profileConfig.mcpConfigName}`
);
});
});
it('should have correct MCP configuration for each profile', () => {
const expectedConfigs = {
cursor: {
mcpConfig: true,
mcpConfigName: 'mcp.json',
expectedPath: '.cursor/mcp.json'
},
windsurf: {
mcpConfig: true,
mcpConfigName: 'mcp.json',
expectedPath: '.windsurf/mcp.json'
},
roo: {
mcpConfig: true,
mcpConfigName: 'mcp.json',
expectedPath: '.roo/mcp.json'
},
cline: {
mcpConfig: false,
mcpConfigName: 'cline_mcp_settings.json',
expectedPath: '.clinerules/cline_mcp_settings.json'
}
};
RULES_PROFILES.forEach((profile) => {
const profileConfig = getRulesProfile(profile);
const expected = expectedConfigs[profile];
expect(profileConfig.mcpConfig).toBe(expected.mcpConfig);
expect(profileConfig.mcpConfigName).toBe(expected.mcpConfigName);
expect(profileConfig.mcpConfigPath).toBe(expected.expectedPath);
});
});
it('should have consistent profileDir and mcpConfigPath relationship', () => {
RULES_PROFILES.forEach((profile) => {
const profileConfig = getRulesProfile(profile);
// The mcpConfigPath should start with the profileDir
expect(profileConfig.mcpConfigPath).toMatch(
new RegExp(`^${profileConfig.profileDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`)
);
// The mcpConfigPath should end with the mcpConfigName
expect(profileConfig.mcpConfigPath).toMatch(
new RegExp(`${profileConfig.mcpConfigName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`)
);
});
});
it('should have unique profile directories', () => {
const profileDirs = RULES_PROFILES.map((profile) => {
const profileConfig = getRulesProfile(profile);
return profileConfig.profileDir;
});
const uniqueProfileDirs = [...new Set(profileDirs)];
expect(uniqueProfileDirs).toHaveLength(profileDirs.length);
});
it('should have unique MCP config paths', () => {
const mcpConfigPaths = RULES_PROFILES.map((profile) => {
const profileConfig = getRulesProfile(profile);
return profileConfig.mcpConfigPath;
});
const uniqueMcpConfigPaths = [...new Set(mcpConfigPaths)];
expect(uniqueMcpConfigPaths).toHaveLength(mcpConfigPaths.length);
});
});
});