diff --git a/mcp-server/src/core/direct-functions/initialize-project.js b/mcp-server/src/core/direct-functions/initialize-project.js index 4132bd86..8df3db1b 100644 --- a/mcp-server/src/core/direct-functions/initialize-project.js +++ b/mcp-server/src/core/direct-functions/initialize-project.js @@ -5,7 +5,8 @@ import { // isSilentMode // Not used directly here } from '../../../../scripts/modules/utils.js'; 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. @@ -75,8 +76,10 @@ export async function initializeProjectDirect(args, log, context = {}) { options.rules = args.rules; log.info(`Including rules: ${args.rules.join(', ')}`); } else { - options.rules = BRAND_NAMES; - log.info(`No rules specified, defaulting to: ${BRAND_NAMES.join(', ')}`); + options.rules = RULES_PROFILES; + log.info( + `No rules profiles specified, defaulting to: ${RULES_PROFILES.join(', ')}` + ); } log.info(`Initializing project with options: ${JSON.stringify(options)}`); diff --git a/mcp-server/src/core/direct-functions/rules.js b/mcp-server/src/core/direct-functions/rules.js index 93c0cd20..5c503a19 100644 --- a/mcp-server/src/core/direct-functions/rules.js +++ b/mcp-server/src/core/direct-functions/rules.js @@ -8,12 +8,12 @@ import { disableSilentMode } from '../../../../scripts/modules/utils.js'; import { - removeBrandRules, - convertAllRulesToBrandRules, - BRAND_NAMES, - isValidBrand, - getBrandProfile + convertAllRulesToProfileRules, + removeProfileRules, + getRulesProfile, + isValidProfile } from '../../../../src/utils/rule-transformer.js'; +import { RULES_PROFILES } from '../../../../src/constants/profiles.js'; import path from 'path'; import fs from 'fs'; @@ -21,7 +21,7 @@ import fs from 'fs'; * Direct function wrapper for adding or removing rules. * @param {Object} args - Command arguments * @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 {boolean} [args.yes=true] - Run non-interactively * @param {Object} log - Logger object @@ -31,18 +31,18 @@ import fs from 'fs'; export async function rulesDirect(args, log, context = {}) { enableSilentMode(); try { - const { action, rules, projectRoot, yes } = args; + const { action, profiles, projectRoot, yes } = args; if ( !action || - !Array.isArray(rules) || - rules.length === 0 || + !Array.isArray(profiles) || + profiles.length === 0 || !projectRoot ) { return { success: false, error: { 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 = []; if (action === 'remove') { - for (const brand of rules) { - if (!isValidBrand(brand)) { + for (const profile of profiles) { + if (!isValidProfile(profile)) { removalResults.push({ - brandName: brand, + profileName: profile, 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; } - const profile = getBrandProfile(brand); - const result = removeBrandRules(projectRoot, profile); + const profileConfig = getRulesProfile(profile); + const result = removeProfileRules(projectRoot, profileConfig); removalResults.push(result); } const successes = removalResults .filter((r) => r.success) - .map((r) => r.brandName); + .map((r) => r.profileName); const skipped = removalResults .filter((r) => r.skipped) - .map((r) => r.brandName); + .map((r) => r.profileName); const errors = removalResults.filter( (r) => r.error && !r.success && !r.skipped ); @@ -83,7 +83,7 @@ export async function rulesDirect(args, log, context = {}) { } if (errors.length > 0) { summary += errors - .map((r) => `Error removing ${r.brandName}: ${r.error}`) + .map((r) => `Error removing ${r.profileName}: ${r.error}`) .join(' '); } disableSilentMode(); @@ -92,43 +92,42 @@ export async function rulesDirect(args, log, context = {}) { data: { summary, results: removalResults } }; } else if (action === 'add') { - for (const brand of rules) { - if (!isValidBrand(brand)) { + for (const profile of profiles) { + if (!isValidProfile(profile)) { addResults.push({ - brandName: brand, + profileName: profile, 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; } - const profile = getBrandProfile(brand); - const { success, failed } = convertAllRulesToBrandRules( + const profileConfig = getRulesProfile(profile); + const { success, failed } = convertAllRulesToProfileRules( projectRoot, - profile + profileConfig ); // Determine paths - const rulesDir = profile.rulesDir; - const brandRulesDir = path.join(projectRoot, rulesDir); - const brandDir = profile.brandDir; - const mcpConfig = profile.mcpConfig !== false; - const mcpConfigName = profile.mcpConfigName || 'mcp.json'; - const mcpPath = path.join(projectRoot, brandDir, mcpConfigName); + const rulesDir = profileConfig.rulesDir; + const profileRulesDir = path.join(projectRoot, rulesDir); + const profileDir = profileConfig.profileDir; + const mcpConfig = profileConfig.mcpConfig !== false; + const mcpPath = path.join(projectRoot, profileConfig.mcpConfigPath); // Check what was created const mcpConfigCreated = mcpConfig ? fs.existsSync(mcpPath) : undefined; - const rulesDirCreated = fs.existsSync(brandRulesDir); - const brandFolderCreated = fs.existsSync( - path.join(projectRoot, brandDir) + const rulesDirCreated = fs.existsSync(profileRulesDir); + const profileFolderCreated = fs.existsSync( + path.join(projectRoot, profileDir) ); const error = failed > 0 ? `${failed} rule files failed to convert.` : null; const resultObj = { - brandName: brand, + profileName: profile, mcpConfigCreated, rulesDirCreated, - brandFolderCreated, + profileFolderCreated, skipped: false, error, success: @@ -142,7 +141,7 @@ export async function rulesDirect(args, log, context = {}) { const successes = addResults .filter((r) => r.success) - .map((r) => r.brandName); + .map((r) => r.profileName); const errors = addResults.filter((r) => r.error && !r.success); let summary = ''; @@ -151,7 +150,7 @@ export async function rulesDirect(args, log, context = {}) { } if (errors.length > 0) { summary += errors - .map((r) => ` Error adding ${r.brandName}: ${r.error}`) + .map((r) => ` Error adding ${r.profileName}: ${r.error}`) .join(' '); } disableSilentMode(); diff --git a/mcp-server/src/tools/initialize-project.js b/mcp-server/src/tools/initialize-project.js index 1895aeaa..17e65c04 100644 --- a/mcp-server/src/tools/initialize-project.js +++ b/mcp-server/src/tools/initialize-project.js @@ -5,7 +5,7 @@ import { withNormalizedProjectRoot } from './utils.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) { 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.' ), rules: z - .array(z.enum(BRAND_RULE_OPTIONS)) + .array(z.enum(RULES_PROFILES)) .optional() .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) => { diff --git a/mcp-server/src/tools/rules.js b/mcp-server/src/tools/rules.js index 65c5a7fe..c5df5504 100644 --- a/mcp-server/src/tools/rules.js +++ b/mcp-server/src/tools/rules.js @@ -10,7 +10,7 @@ import { withNormalizedProjectRoot } from './utils.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 @@ -20,16 +20,16 @@ export function registerRulesTool(server) { server.addTool({ name: 'rules', 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({ action: z .enum(['add', 'remove']) - .describe('Whether to add or remove rules.'), - rules: z - .array(z.enum(BRAND_RULE_OPTIONS)) + .describe('Whether to add or remove rules profiles.'), + profiles: z + .array(z.enum(RULES_PROFILES)) .min(1) .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 .string() @@ -40,7 +40,7 @@ export function registerRulesTool(server) { execute: withNormalizedProjectRoot(async (args, { log, session }) => { try { 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 }); return handleApiResult(result, log); diff --git a/scripts/init.js b/scripts/init.js index b8483654..450453d4 100755 --- a/scripts/init.js +++ b/scripts/init.js @@ -24,7 +24,11 @@ import figlet from 'figlet'; import boxen from 'boxen'; import gradient from 'gradient-string'; 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 { execSync } from 'child_process'; @@ -305,10 +309,10 @@ async function initializeProject(options = {}) { } const skipPrompts = options.yes || (options.name && options.description); - let selectedBrandRules = + let selectedRulesProfiles = options.rules && Array.isArray(options.rules) && options.rules.length > 0 ? options.rules - : BRAND_NAMES; // Default to all rules + : RULES_PROFILES; // Default to all profiles if (skipPrompts) { if (!isSilentMode()) { @@ -331,7 +335,7 @@ async function initializeProject(options = {}) { } try { - createProjectStructure(addAliases, dryRun, selectedBrandRules); + createProjectStructure(addAliases, dryRun, selectedRulesProfiles); } catch (error) { log('error', `Error during initialization process: ${error.message}`); process.exit(1); @@ -378,9 +382,12 @@ async function initializeProject(options = {}) { // Only run interactive rules if rules flag not provided via command line 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 { - selectedBrandRules = await runInteractiveRulesSetup(); + selectedRulesProfiles = await runInteractiveRulesSetup(); } const dryRun = options.dryRun || false; @@ -398,8 +405,7 @@ async function initializeProject(options = {}) { } // Create structure using only necessary values - createProjectStructure(addAliasesPrompted, dryRun, selectedBrandRules); - + createProjectStructure(addAliasesPrompted, dryRun, selectedRulesProfiles); } catch (error) { rl.close(); log('error', `Error during initialization process: ${error.message}`); @@ -421,13 +427,12 @@ function promptQuestion(rl, question) { function createProjectStructure( addAliases, dryRun, - selectedBrandRules = BRAND_NAMES // Default to all rules + selectedRulesProfiles = RULES_PROFILES // Default to all rules profiles ) { const targetDir = process.cwd(); log('info', `Initializing project in ${targetDir}`); // Create directories - ensureDirectoryExists(path.join(targetDir, '.cursor', 'rules')); ensureDirectoryExists(path.join(targetDir, 'scripts')); ensureDirectoryExists(path.join(targetDir, 'tasks')); @@ -436,17 +441,14 @@ function createProjectStructure( year: new Date().getFullYear() }; - // Helper function to process a single brand rule - function _processSingleBrandRule(ruleName) { - const profile = BRAND_PROFILES[ruleName]; + // Helper function to process a single profile rule + function _processSingleProfileRule(profileName) { + const profile = getRulesProfile(profileName); if (profile) { - convertAllRulesToBrandRules(targetDir, profile); - // Ensure MCP config is set up under the correct brand folder for any non-cursor rule - if (ruleName !== 'cursor') { - setupMCPConfiguration(path.join(targetDir, `.${ruleName}`)); - } + convertAllRulesToProfileRules(targetDir, profile); + // The convertAllRulesToProfileRules function also triggers MCP config setup (if needed). } 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'); } - // === Generate Brand Rules from assets/rules === - log('info', 'Generating brand rules from assets/rules...'); - for (const rule of selectedBrandRules) { - _processSingleBrandRule(rule); + // Generate profile rules from assets/rules + log('info', 'Generating profile rules from assets/rules...'); + for (const profileName of selectedRulesProfiles) { + _processSingleProfileRule(profileName); } // 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 export { initializeProject, log }; diff --git a/scripts/modules/commands.js b/scripts/modules/commands.js index c913231e..b2c1cc79 100644 --- a/scripts/modules/commands.js +++ b/scripts/modules/commands.js @@ -65,7 +65,7 @@ import { displayApiKeyStatus, displayAiUsageSummary } from './ui.js'; -import { confirmRulesRemove } from '../../src/ui/confirm.js'; +import { confirmProfilesRemove } from '../../src/ui/confirm.js'; import { initializeProject } from '../init.js'; import { @@ -80,12 +80,12 @@ import { TASK_STATUS_OPTIONS } from '../../src/constants/task-status.js'; import { getTaskMasterVersion } from '../../src/utils/getVersion.js'; +import { RULES_PROFILES } from '../../src/constants/profiles.js'; import { - convertAllRulesToBrandRules, - removeBrandRules, - BRAND_NAMES, - isValidBrand, - getBrandProfile + convertAllRulesToProfileRules, + removeProfileRules, + isValidProfile, + getRulesProfile } from '../../src/utils/rule-transformer.js'; import { runInteractiveRulesSetup } from '../../src/utils/rules-setup.js'; @@ -2125,24 +2125,24 @@ function registerCommands(programInstance) { .action(async (cmdOptions) => { // cmdOptions contains parsed arguments // Parse rules: accept space or comma separated, default to all available rules - let selectedBrands = BRAND_NAMES; + let selectedProfiles = RULES_PROFILES; let rulesExplicitlyProvided = false; - + if (cmdOptions.rules && Array.isArray(cmdOptions.rules)) { - const userSpecifiedBrands = cmdOptions.rules + const userSpecifiedProfiles = cmdOptions.rules .flatMap((r) => r.split(',')) .map((r) => r.trim()) .filter(Boolean); // Only override defaults if user specified valid rules - if (userSpecifiedBrands.length > 0) { - selectedBrands = userSpecifiedBrands; + if (userSpecifiedProfiles.length > 0) { + selectedProfiles = userSpecifiedProfiles; rulesExplicitlyProvided = true; } } - - cmdOptions.rules = selectedBrands; + + cmdOptions.rules = selectedProfiles; cmdOptions.rulesExplicitlyProvided = rulesExplicitlyProvided; - + try { // Directly call the initializeProject function, passing the parsed options await initializeProject(cmdOptions); @@ -2377,65 +2377,68 @@ Examples: return; // Stop execution here }); - // Add/remove brand rules command + // Add/remove profile rules command programInstance - .command('rules [brands...]') + .command('rules [profiles...]') .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( '-f, --force', 'Skip confirmation prompt when removing rules (dangerous)' ) - .action(async (action, brands, options) => { + .action(async (action, profiles, options) => { const projectDir = process.cwd(); /** * '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. * * Example usage: * $ 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') { // Run interactive rules setup ONLY (no project init) - const selectedBrandRules = await runInteractiveRulesSetup(); - for (const brand of selectedBrandRules) { - if (!isValidBrand(brand)) { + const selectedRulesProfiles = await runInteractiveRulesSetup(); + for (const profile of selectedRulesProfiles) { + if (!isValidProfile(profile)) { 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; } - const profile = getBrandProfile(brand); - const addResult = convertAllRulesToBrandRules(projectDir, profile); - if (typeof profile.onAddBrandRules === 'function') { - profile.onAddBrandRules(projectDir); + const profileConfig = getRulesProfile(profile); + const addResult = convertAllRulesToProfileRules( + projectDir, + profileConfig + ); + if (typeof profileConfig.onAddRulesProfile === 'function') { + profileConfig.onAddRulesProfile(projectDir); } console.log( chalk.green( - `Summary for ${brand}: ${addResult.success} rules added, ${addResult.failed} failed.` + `Summary for ${profile}: ${addResult.success} rules added, ${addResult.failed} failed.` ) ); } return; } - if (!brands || brands.length === 0) { + if (!profiles || profiles.length === 0) { 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); } - // Support both space- and comma-separated brand lists - const expandedBrands = brands + // Support both space- and comma-separated profile lists + const expandedProfiles = profiles .flatMap((b) => b.split(',').map((s) => s.trim())) .filter(Boolean); @@ -2443,7 +2446,7 @@ Examples: let confirmed = true; if (!options.force) { const ui = await import('./ui.js'); - confirmed = await confirmRulesRemove(expandedBrands); + confirmed = await confirmProfilesRemove(expandedProfiles); } if (!confirmed) { console.log(chalk.yellow('Aborted: No rules were removed.')); @@ -2451,36 +2454,39 @@ Examples: } } - // (removed duplicate projectDir, brands check, and expandedBrands parsing) - const removalResults = []; - for (const brand of expandedBrands) { - if (!isValidBrand(brand)) { + for (const profile of expandedProfiles) { + if (!isValidProfile(profile)) { 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; } - const profile = getBrandProfile(brand); + const profileConfig = getRulesProfile(profile); if (action === 'add') { - console.log(chalk.blue(`Adding rules for brand: ${brand}...`)); - const addResult = convertAllRulesToBrandRules(projectDir, profile); - if (typeof profile.onAddBrandRules === 'function') { - profile.onAddBrandRules(projectDir); + console.log(chalk.blue(`Adding rules for profile: ${profile}...`)); + const addResult = convertAllRulesToProfileRules( + 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( 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') { - console.log(chalk.blue(`Removing rules for brand: ${brand}...`)); - const result = removeBrandRules(projectDir, profile); + console.log(chalk.blue(`Removing rules for profile: ${profile}...`)); + const result = removeProfileRules(projectDir, profileConfig); removalResults.push(result); - console.log(chalk.blue(`Completed removal for brand: ${brand}`)); + console.log(chalk.blue(`Completed removal for profile: ${profile}`)); } else { console.error('Unknown action. Use "add" or "remove".'); process.exit(1); @@ -2491,10 +2497,10 @@ Examples: if (action === 'remove') { const successes = removalResults .filter((r) => r.success) - .map((r) => r.brandName); + .map((r) => r.profileName); const skipped = removalResults .filter((r) => r.skipped) - .map((r) => r.brandName); + .map((r) => r.profileName); const errors = removalResults.filter( (r) => r.error && !r.success && !r.skipped ); @@ -2513,7 +2519,9 @@ Examples: } if (errors.length > 0) { errors.forEach((r) => { - console.log(chalk.red(`Error removing ${r.brandName}: ${r.error}`)); + console.log( + chalk.red(`Error removing ${r.profileName}: ${r.error}`) + ); }); } } diff --git a/scripts/modules/ui.js b/scripts/modules/ui.js index 0432a6a0..bbc68811 100644 --- a/scripts/modules/ui.js +++ b/scripts/modules/ui.js @@ -1769,8 +1769,6 @@ IMPORTANT: Make sure to include an analysis for EVERY task listed above, with th `; } - - /** * Confirm overwriting existing tasks.json file * @param {string} tasksPath - Path to the tasks.json file diff --git a/scripts/profiles/cline.js b/scripts/profiles/cline.js index 818fd186..46f26e52 100644 --- a/scripts/profiles/cline.js +++ b/scripts/profiles/cline.js @@ -1,11 +1,12 @@ // Cline conversion profile for rule-transformer import path from 'path'; -const brandName = 'Cline'; -const brandDir = '.clinerules'; +const profileName = 'Cline'; +const profileDir = '.clinerules'; const rulesDir = '.clinerules'; const mcpConfig = false; const mcpConfigName = 'cline_mcp_settings.json'; +const mcpConfigPath = `${profileDir}/${mcpConfigName}`; // File name mapping (specific files with naming changes) const fileMap = { @@ -43,8 +44,8 @@ const globalReplacements = [ ]; const conversionConfig = { - // Product and brand name replacements - brandTerms: [ + // Profile name replacements + profileTerms: [ { 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' }, @@ -136,10 +137,11 @@ export { conversionConfig, fileMap, globalReplacements, - brandName, - brandDir, + profileName, + profileDir, rulesDir, - getTargetRuleFilename, mcpConfig, - mcpConfigName + mcpConfigName, + mcpConfigPath, + getTargetRuleFilename }; diff --git a/scripts/profiles/cursor.js b/scripts/profiles/cursor.js index 025cf846..372c836b 100644 --- a/scripts/profiles/cursor.js +++ b/scripts/profiles/cursor.js @@ -1,11 +1,12 @@ // Cursor conversion profile for rule-transformer import path from 'path'; -const brandName = 'Cursor'; -const brandDir = '.cursor'; +const profileName = 'Cursor'; +const profileDir = '.cursor'; const rulesDir = '.cursor/rules'; const mcpConfig = true; const mcpConfigName = 'mcp.json'; +const mcpConfigPath = `${profileDir}/${mcpConfigName}`; // File name mapping (specific files with naming changes) const fileMap = { @@ -39,8 +40,8 @@ const globalReplacements = [ ]; const conversionConfig = { - // Product and brand name replacements - brandTerms: [ + // Profile name replacements + profileTerms: [ { 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' }, @@ -82,10 +83,11 @@ export { conversionConfig, fileMap, globalReplacements, - brandName, - brandDir, + profileName, + profileDir, rulesDir, - getTargetRuleFilename, mcpConfig, - mcpConfigName + mcpConfigName, + mcpConfigPath, + getTargetRuleFilename }; diff --git a/scripts/profiles/index.js b/scripts/profiles/index.js index 1f0a7290..f624aaee 100644 --- a/scripts/profiles/index.js +++ b/scripts/profiles/index.js @@ -2,4 +2,4 @@ export * as clineProfile from './cline.js'; export * as cursorProfile from './cursor.js'; export * as rooProfile from './roo.js'; -export * as windsurfProfile from './windsurf.js'; \ No newline at end of file +export * as windsurfProfile from './windsurf.js'; diff --git a/scripts/profiles/roo.js b/scripts/profiles/roo.js index ace3fcc3..24731361 100644 --- a/scripts/profiles/roo.js +++ b/scripts/profiles/roo.js @@ -7,11 +7,12 @@ import { isSilentMode, log } from '../modules/utils.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const brandName = 'Roo'; -const brandDir = '.roo'; +const profileName = 'Roo'; +const profileDir = '.roo'; const rulesDir = '.roo/rules'; const mcpConfig = true; const mcpConfigName = 'mcp.json'; +const mcpConfigPath = `${profileDir}/${mcpConfigName}`; // File name mapping (specific files with naming changes) const fileMap = { @@ -48,8 +49,8 @@ const globalReplacements = [ ]; const conversionConfig = { - // Product and brand name replacements - brandTerms: [ + // Profile name replacements + profileTerms: [ { 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' }, @@ -126,7 +127,7 @@ const conversionConfig = { // 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'); copyRecursiveSync(sourceDir, targetDir); @@ -182,8 +183,8 @@ function copyRecursiveSync(src, dest) { } } -export function onRemoveBrandRules(targetDir) { - log('debug', `[Roo] onRemoveBrandRules called for ${targetDir}`); +export function onRemoveRulesProfile(targetDir) { + log('debug', `[Roo] onRemoveRulesProfile called for ${targetDir}`); const roomodesPath = path.join(targetDir, '.roomodes'); if (fs.existsSync(roomodesPath)) { 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) { return fs.readdirSync(dirPath).length === 0; } -function onPostConvertBrandRules(targetDir) { - onAddBrandRules(targetDir); +function onPostConvertRulesProfile(targetDir) { + onAddRulesProfile(targetDir); } function getTargetRuleFilename(sourceFilename) { @@ -238,11 +239,12 @@ export { conversionConfig, fileMap, globalReplacements, - brandName, - brandDir, + profileName, + profileDir, rulesDir, - getTargetRuleFilename, mcpConfig, mcpConfigName, - onPostConvertBrandRules + mcpConfigPath, + getTargetRuleFilename, + onPostConvertRulesProfile }; diff --git a/scripts/profiles/windsurf.js b/scripts/profiles/windsurf.js index 287ae33c..21dd3e7e 100644 --- a/scripts/profiles/windsurf.js +++ b/scripts/profiles/windsurf.js @@ -1,11 +1,12 @@ // Windsurf conversion profile for rule-transformer import path from 'path'; -const brandName = 'Windsurf'; -const brandDir = '.windsurf'; +const profileName = 'Windsurf'; +const profileDir = '.windsurf'; const rulesDir = '.windsurf/rules'; const mcpConfig = true; const mcpConfigName = 'mcp.json'; +const mcpConfigPath = `${profileDir}/${mcpConfigName}`; // File name mapping (specific files with naming changes) const fileMap = { @@ -42,8 +43,8 @@ const globalReplacements = [ ]; const conversionConfig = { - // Product and brand name replacements - brandTerms: [ + // Profile name replacements + profileTerms: [ { 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' }, @@ -132,10 +133,11 @@ export { conversionConfig, fileMap, globalReplacements, - brandName, - brandDir, + profileName, + profileDir, rulesDir, - getTargetRuleFilename, mcpConfig, - mcpConfigName + mcpConfigName, + mcpConfigPath, + getTargetRuleFilename }; diff --git a/src/constants/profiles.js b/src/constants/profiles.js new file mode 100644 index 00000000..4af652d4 --- /dev/null +++ b/src/constants/profiles.js @@ -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); +} diff --git a/src/constants/rules.js b/src/constants/rules.js deleted file mode 100644 index e29e734c..00000000 --- a/src/constants/rules.js +++ /dev/null @@ -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); -} \ No newline at end of file diff --git a/src/ui/confirm.js b/src/ui/confirm.js index c77fb351..c36624d3 100644 --- a/src/ui/confirm.js +++ b/src/ui/confirm.js @@ -2,19 +2,19 @@ import chalk from 'chalk'; import boxen from 'boxen'; /** - * Confirm removing brand rules (destructive operation) - * @param {string[]} brands - Array of brand names to remove + * Confirm removing profile rules (destructive operation) + * @param {string[]} profiles - Array of profile names to remove * @returns {Promise} - Promise resolving to true if user confirms, false otherwise */ -async function confirmRulesRemove(brands) { - const brandList = brands +async function confirmProfilesRemove(profiles) { + const profileList = profiles .map((b) => b.charAt(0).toUpperCase() + b.slice(1)) .join(', '); console.log( boxen( chalk.yellow( - `WARNING: This will permanently delete all rules and configuration for: ${brandList}. -This will remove the entire .[brand] directory for each selected brand.\n\nAre you sure you want to proceed?` + `WARNING: This will permanently delete all rules and configuration for: ${profileList}. +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' } ) @@ -31,4 +31,4 @@ This will remove the entire .[brand] directory for each selected brand.\n\nAre y return confirm; } -export { confirmRulesRemove }; \ No newline at end of file +export { confirmProfilesRemove }; diff --git a/src/utils/mcp-utils.js b/src/utils/mcp-utils.js index da11b334..433e7997 100644 --- a/src/utils/mcp-utils.js +++ b/src/utils/mcp-utils.js @@ -3,10 +3,12 @@ import path from 'path'; import { log } from '../../scripts/modules/utils.js'; // Structure matches project conventions (see scripts/init.js) -export function setupMCPConfiguration(configDir) { - const mcpPath = path.join(configDir, 'mcp.json'); +export function setupMCPConfiguration(projectDir, mcpConfigPath) { + // 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 const newMCPServer = { @@ -26,10 +28,12 @@ export function setupMCPConfiguration(configDir) { } } }; + // Create config directory if it doesn't exist if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } + if (fs.existsSync(mcpPath)) { log( 'info', @@ -94,6 +98,6 @@ export function setupMCPConfiguration(configDir) { mcpServers: newMCPServer }; fs.writeFileSync(mcpPath, JSON.stringify(newMCPConfig, null, 4)); - log('success', 'Created MCP configuration file'); + log('success', `Created MCP configuration file at ${mcpPath}`); } -} \ No newline at end of file +} diff --git a/src/utils/rule-transformer.js b/src/utils/rule-transformer.js index 496aa91f..0b45d4be 100644 --- a/src/utils/rule-transformer.js +++ b/src/utils/rule-transformer.js @@ -1,45 +1,59 @@ /** * 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. */ import fs from 'fs'; import path from 'path'; +import { fileURLToPath } from 'url'; import { log } from '../../scripts/modules/utils.js'; // Import the shared MCP configuration helper import { setupMCPConfiguration } from './mcp-utils.js'; -// --- Centralized Brand Helpers --- -import { clineProfile, cursorProfile, rooProfile, windsurfProfile } from '../../scripts/profiles/index.js'; +// Import profile constants (single source of truth) +import { RULES_PROFILES } from '../constants/profiles.js'; -export const BRAND_PROFILES = { - cline: clineProfile, - cursor: cursorProfile, - roo: rooProfile, - windsurf: windsurfProfile -}; +// --- Profile Imports --- +import * as profilesModule from '../../scripts/profiles/index.js'; -export const BRAND_NAMES = Object.keys(BRAND_PROFILES); - -export function isValidBrand(brand) { - return BRAND_NAMES.includes(brand); -} - -export function getBrandProfile(brand) { - return BRAND_PROFILES[brand]; +export function isValidProfile(profile) { + return RULES_PROFILES.includes(profile); } /** - * 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) { let result = content; - // Apply brand term replacements - conversionConfig.brandTerms.forEach((pattern) => { + // Apply profile term replacements + conversionConfig.profileTerms.forEach((pattern) => { if (typeof pattern.to === 'function') { result = result.replace(pattern.from, pattern.to); } 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) { 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) { 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, now brand-generic -function transformCursorToBrandRules( +function transformCursorToProfileRules( content, conversionConfig, globalReplacements = [] @@ -128,7 +141,7 @@ function transformCursorToBrandRules( result = updateDocReferences(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 // This ensures critical transformations are applied even in contexts we didn't anticipate 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) { - const { conversionConfig, brandName, globalReplacements } = profile; +export function convertRuleToProfileRule(sourcePath, targetPath, profile) { + const { conversionConfig, globalReplacements } = profile; try { log( '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 const content = fs.readFileSync(sourcePath, 'utf8'); // Transform content - const transformedContent = transformCursorToBrandRules( + const transformedContent = transformCursorToProfileRules( content, conversionConfig, 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) { - const { fileMap, brandName, rulesDir, mcpConfig, mcpConfigName } = profile; - // Use assets/rules as the source of rules instead of .cursor/rules - const cursorRulesDir = path.join(projectDir, 'assets', 'rules'); - const brandRulesDir = path.join(projectDir, rulesDir); +export function convertAllRulesToProfileRules(projectDir, profile) { + const sourceDir = fileURLToPath(new URL('../../assets/rules', import.meta.url)); + const targetDir = path.join(projectDir, profile.rulesDir); - if (!fs.existsSync(cursorRulesDir)) { - log('warn', `Cursor rules directory not found: ${cursorRulesDir}`); - return { success: 0, failed: 0 }; + // Ensure target directory exists + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); } - // Ensure brand rules directory exists - if (!fs.existsSync(brandRulesDir)) { - fs.mkdirSync(brandRulesDir, { recursive: true }); - log('debug', `Created ${brandName} rules directory: ${brandRulesDir}`); - // Also create MCP configuration in the brand directory if enabled - if (mcpConfig !== false) { - const brandDir = profile.brandDir; - setupMCPConfiguration(path.join(projectDir, brandDir), mcpConfigName); - } + // Setup MCP configuration if enabled + if (profile.mcpConfig !== false) { + setupMCPConfiguration( + projectDir, + profile.mcpConfigPath + ); } - // Count successful and failed conversions let success = 0; let failed = 0; - // Process each file from assets/rules listed in fileMap - const getTargetRuleFilename = profile.getTargetRuleFilename || ((f) => f); - 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); + // Use fileMap to determine which files to copy + const sourceFiles = Object.keys(profile.fileMap); - // Convert the file - if (convertRuleToBrandRule(sourcePath, targetPath, profile)) { - success++; - } else { - failed++; + for (const sourceFile of sourceFiles) { + try { + const sourcePath = path.join(sourceDir, sourceFile); + + // 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( - 'warn', - `File listed in fileMap not found in rules dir: ${sourcePath}` + 'debug', + `[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) - if (typeof profile.onPostConvertBrandRules === 'function') { - profile.onPostConvertBrandRules(projectDir); + if (typeof profile.onPostConvertRulesProfile === 'function') { + profile.onPostConvertRulesProfile(projectDir); } return { success, failed }; } /** - * Remove a brand's rules directory, its mcp.json, and the parent brand folder recursively. - * @param {string} projectDir - The root directory of the project - * @param {object} profile - The brand profile object - * @returns {boolean} - True if removal succeeded, false otherwise + * Remove profile rules for a specific profile + * @param {string} projectDir - Target project directory + * @param {Object} profile - Profile configuration + * @returns {Object} Result object */ -function removeBrandRules(projectDir, profile) { - const { brandName, rulesDir, mcpConfig, mcpConfigName } = profile; - const brandDir = profile.brandDir; - const brandRulesDir = path.join(projectDir, rulesDir); - const mcpPath = path.join(projectDir, brandDir, mcpConfigName); +export function removeProfileRules(projectDir, profile) { + const targetDir = path.join(projectDir, profile.rulesDir); + const profileDir = path.join(projectDir, profile.profileDir); + const mcpConfigPath = path.join(projectDir, profile.mcpConfigPath); - const result = { - brandName, - mcpConfigRemoved: false, - rulesDirRemoved: false, - brandFolderRemoved: false, + let result = { + profileName: profile.profileName, + success: false, skipped: false, - error: null, - success: false // Overall success for this brand + error: null }; - 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 { - fs.rmSync(brandDir, { recursive: true, force: true }); - result.brandFolderRemoved = true; - } catch (e) { - const errorMessage = `Failed to remove brand folder at ${brandDir}: ${e.message}`; - 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 rules directory + if (fs.existsSync(targetDir)) { + fs.rmSync(targetDir, { recursive: true, force: true }); + log('debug', `[Rule Transformer] Removed rules directory: ${targetDir}`); } + + // 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; } - -export { - convertAllRulesToBrandRules, - convertRuleToBrandRule, - removeBrandRules -}; \ No newline at end of file diff --git a/src/utils/rules-setup.js b/src/utils/rules-setup.js index f338c9b6..8a14242e 100644 --- a/src/utils/rules-setup.js +++ b/src/utils/rules-setup.js @@ -1,13 +1,13 @@ import readline from 'readline'; import inquirer from 'inquirer'; 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 -const availableBrandRules = BRAND_NAMES.map((name) => { - const displayName = - BRAND_PROFILES[name]?.brandName || - name.charAt(0).toUpperCase() + name.slice(1); +// Dynamically generate availableRulesProfiles from RULES_PROFILES +const availableRulesProfiles = RULES_PROFILES.map((name) => { + const displayName = getProfileDisplayName(name); return { name: name === 'cursor' ? `${displayName} (default)` : displayName, value: name @@ -15,18 +15,26 @@ const availableBrandRules = BRAND_NAMES.map((name) => { }); /** - * Runs the interactive rules setup flow (brand rules selection only) - * @returns {Promise} The selected brand rules + * Get the display name for a profile + */ +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} 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. - * The user must select at least one brand (default: cursor). The result is an array of selected brand names. + * This function dynamically lists all available profiles (from RULES_PROFILES) and presents them as checkboxes. + * 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. * - * @returns {Promise} Array of selected brand rule names (e.g., ['cursor', 'windsurf']) + * @returns {Promise} Array of selected profile rule names (e.g., ['cursor', 'windsurf']) */ export async function runInteractiveRulesSetup() { console.log( @@ -34,14 +42,14 @@ export async function runInteractiveRulesSetup() { '\nRules help enforce best practices and conventions for Task Master.' ) ); - const brandRulesQuestion = { + const rulesProfilesQuestion = { type: 'checkbox', - name: 'brandRules', + name: 'rulesProfiles', message: 'Which IDEs would you like rules included for?', - choices: availableBrandRules, + choices: availableRulesProfiles, default: ['cursor'], validate: (input) => input.length > 0 || 'You must select at least one.' }; - const { brandRules } = await inquirer.prompt([brandRulesQuestion]); - return brandRules; -} \ No newline at end of file + const { rulesProfiles } = await inquirer.prompt([rulesProfilesQuestion]); + return rulesProfiles; +} diff --git a/tests/integration/cursor-init-functionality.test.js b/tests/integration/cursor-init-functionality.test.js index ebd8870f..0e8a9e49 100644 --- a/tests/integration/cursor-init-functionality.test.js +++ b/tests/integration/cursor-init-functionality.test.js @@ -14,8 +14,8 @@ describe('Cursor Profile Initialization Functionality', () => { cursorProfileContent = fs.readFileSync(cursorJsPath, 'utf8'); }); - test('cursor.js exports correct brandName and rulesDir', () => { - expect(cursorProfileContent).toContain("const brandName = 'Cursor'"); + test('cursor.js exports correct profileName and rulesDir', () => { + expect(cursorProfileContent).toContain("const profileName = 'Cursor'"); expect(cursorProfileContent).toContain("const rulesDir = '.cursor/rules'"); }); diff --git a/tests/integration/roo-files-inclusion.test.js b/tests/integration/roo-files-inclusion.test.js index 77bccede..deea8265 100644 --- a/tests/integration/roo-files-inclusion.test.js +++ b/tests/integration/roo-files-inclusion.test.js @@ -22,7 +22,7 @@ describe('Roo Files Inclusion in Package', () => { const rooJsContent = fs.readFileSync(rooJsPath, 'utf8'); // 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 expect( diff --git a/tests/integration/roo-init-functionality.test.js b/tests/integration/roo-init-functionality.test.js index 94441c5d..02513ac1 100644 --- a/tests/integration/roo-init-functionality.test.js +++ b/tests/integration/roo-init-functionality.test.js @@ -11,36 +11,31 @@ describe('Roo Profile Initialization Functionality', () => { rooProfileContent = fs.readFileSync(rooJsPath, 'utf8'); }); - test('roo.js profile ensures Roo directory structure via onAddBrandRules', () => { - // Check if onAddBrandRules function exists - expect(rooProfileContent).toContain('onAddBrandRules(targetDir)'); + test('roo.js profile ensures Roo directory structure via onAddRulesProfile', () => { + // Check if onAddRulesProfile function exists + expect(rooProfileContent).toContain('onAddRulesProfile(targetDir)'); // Check for the general copy of assets/roocode which includes .roo base structure expect(rooProfileContent).toContain( - 'copyRecursiveSync(sourceDir, targetDir)' + "const sourceDir = path.resolve(__dirname, '../../assets/roocode');" ); expect(rooProfileContent).toContain( - "path.resolve(__dirname, '../../assets/roocode')" - ); // Verifies sourceDir definition - - // Check for the loop that processes rooModes - expect(rooProfileContent).toContain('for (const mode of rooModes)'); - - // 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( - 'fs.mkdirSync(destDir, { recursive: true });' + 'copyRecursiveSync(sourceDir, targetDir);' + ); + + // Check for the specific .roo modes directory handling + expect(rooProfileContent).toContain( + "const rooModesDir = path.join(sourceDir, '.roo');" + ); + expect(rooProfileContent).toContain( + "const rooModes = ['architect', 'ask', 'boomerang', 'code', 'debug', 'test'];" ); - expect(rooProfileContent).toContain('const destDir = path.dirname(dest);'); // part of the same logic block }); - test('roo.js profile copies .roomodes file via onAddBrandRules', () => { - expect(rooProfileContent).toContain('onAddBrandRules(targetDir)'); + test('roo.js profile copies .roomodes file via onAddRulesProfile', () => { + expect(rooProfileContent).toContain('onAddRulesProfile(targetDir)'); // Check for the specific .roomodes copy logic - expect(rooProfileContent).toContain( - 'fs.copyFileSync(roomodesSrc, roomodesDest);' - ); expect(rooProfileContent).toContain( "const roomodesSrc = path.join(sourceDir, '.roomodes');" ); @@ -48,27 +43,20 @@ describe('Roo Profile Initialization Functionality', () => { "const roomodesDest = path.join(targetDir, '.roomodes');" ); expect(rooProfileContent).toContain( - "path.resolve(__dirname, '../../assets/roocode')" - ); // sourceDir for roomodesSrc + 'fs.copyFileSync(roomodesSrc, roomodesDest);' + ); }); - test('roo.js profile copies mode-specific rule files via onAddBrandRules', () => { - expect(rooProfileContent).toContain('onAddBrandRules(targetDir)'); + test('roo.js profile copies mode-specific rule files via onAddRulesProfile', () => { + expect(rooProfileContent).toContain('onAddRulesProfile(targetDir)'); expect(rooProfileContent).toContain('for (const mode of rooModes)'); // 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( 'const src = path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`);' ); - // Check destination path construction for mode rules expect(rooProfileContent).toContain( "const dest = path.join(targetDir, '.roo', `rules-${mode}`, `${mode}-rules`);" ); - expect(rooProfileContent).toContain( - "const rooModesDir = path.join(sourceDir, '.roo');" - ); // part of src path }); }); diff --git a/tests/integration/rules-files-inclusion.test.js b/tests/integration/rules-files-inclusion.test.js index 35484f7b..9c7d24c4 100644 --- a/tests/integration/rules-files-inclusion.test.js +++ b/tests/integration/rules-files-inclusion.test.js @@ -1,42 +1,95 @@ +import { jest } from '@jest/globals'; import fs from 'fs'; import path from 'path'; +import os from 'os'; +import { execSync } from 'child_process'; 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 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/**'); }); - 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'); + expect(fs.existsSync(rulesDir)).toBe(true); + + // Check for the 4 files that currently exist const expectedFiles = [ - 'ai_providers.mdc', - 'ai_services.mdc', - 'architecture.mdc', - 'changeset.mdc', - 'commands.mdc', - 'cursor_rules.mdc', - 'dependencies.mdc', 'dev_workflow.mdc', - 'glossary.mdc', - 'mcp.mdc', - 'new_features.mdc', - 'self_improve.mdc', 'taskmaster.mdc', - 'tasks.mdc', - 'tests.mdc', - 'ui.mdc', - 'utilities.mdc' + 'self_improve.mdc', + 'cursor_rules.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', () => { - const rulesDir = path.join(process.cwd(), 'assets', 'rules'); - const files = fs.readdirSync(rulesDir).filter((f) => !f.startsWith('.')); - expect(files.length).toBeGreaterThan(0); + test('source Roo files exist in assets directory', () => { + // Verify that the source files for Roo integration exist + expect( + fs.existsSync(path.join(process.cwd(), 'assets', 'roocode', '.roo')) + ).toBe(true); + expect( + fs.existsSync(path.join(process.cwd(), 'assets', 'roocode', '.roomodes')) + ).toBe(true); }); }); diff --git a/tests/integration/windsurf-init-functionality.test.js b/tests/integration/windsurf-init-functionality.test.js index f90ce1d5..28ab0ea5 100644 --- a/tests/integration/windsurf-init-functionality.test.js +++ b/tests/integration/windsurf-init-functionality.test.js @@ -14,8 +14,8 @@ describe('Windsurf Profile Initialization Functionality', () => { windsurfProfileContent = fs.readFileSync(windsurfJsPath, 'utf8'); }); - test('windsurf.js exports correct brandName and rulesDir', () => { - expect(windsurfProfileContent).toContain("const brandName = 'Windsurf'"); + test('windsurf.js exports correct profileName and rulesDir', () => { + expect(windsurfProfileContent).toContain("const profileName = 'Windsurf'"); expect(windsurfProfileContent).toContain( "const rulesDir = '.windsurf/rules'" ); diff --git a/tests/unit/commands.test.js b/tests/unit/commands.test.js index 99802fb8..df76e494 100644 --- a/tests/unit/commands.test.js +++ b/tests/unit/commands.test.js @@ -1102,31 +1102,31 @@ describe('rules command', () => { mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); }); - test('should handle rules add command', async () => { + test('should handle rules add command', async () => { // Simulate: task-master rules add roo await program.parseAsync(['rules', 'add', 'roo'], { from: 'user' }); // Expect some log output indicating success expect(mockConsoleLog).toHaveBeenCalledWith( - expect.stringMatching(/adding rules for brand: roo/i) + expect.stringMatching(/adding rules for profile: roo/i) ); 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 expect(mockExit).not.toHaveBeenCalledWith(1); }); - test('should handle rules remove command', async () => { + test('should handle rules remove command', async () => { // Simulate: task-master rules remove roo --force await program.parseAsync(['rules', 'remove', 'roo', '--force'], { from: 'user' }); // Expect some log output indicating removal expect(mockConsoleLog).toHaveBeenCalledWith( - expect.stringMatching(/removing rules for brand: roo/i) + expect.stringMatching(/removing rules for profile: roo/i) ); expect(mockConsoleLog).toHaveBeenCalledWith( - expect.stringMatching(/completed removal for brand: roo/i) + expect.stringMatching(/completed removal for profile: roo/i) ); // Should not exit with error expect(mockExit).not.toHaveBeenCalledWith(1); diff --git a/tests/unit/mcp-config-validation.test.js b/tests/unit/mcp-config-validation.test.js new file mode 100644 index 00000000..c4a638b8 --- /dev/null +++ b/tests/unit/mcp-config-validation.test.js @@ -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); + } + }); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/rule-transformer-cursor.test.js b/tests/unit/rule-transformer-cursor.test.js index 3c77ef1c..49f6162b 100644 --- a/tests/unit/rule-transformer-cursor.test.js +++ b/tests/unit/rule-transformer-cursor.test.js @@ -3,8 +3,9 @@ import path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import { - convertRuleToBrandRule, - convertAllRulesToBrandRules + convertAllRulesToProfileRules, + convertRuleToProfileRule, + getRulesProfile } from '../../src/utils/rule-transformer.js'; import * as cursorProfile from '../../scripts/profiles/cursor.js'; @@ -44,7 +45,7 @@ Also has references to .mdc files.`; // Convert it const testCursorOut = path.join(testDir, 'basic-terms.mdc'); - convertRuleToBrandRule(testCursorRule, testCursorOut, cursorProfile); + convertRuleToProfileRule(testCursorRule, testCursorOut, cursorProfile); // Read the converted file const convertedContent = fs.readFileSync(testCursorOut, 'utf8'); @@ -75,7 +76,7 @@ alwaysApply: true // Convert it const testCursorOut = path.join(testDir, 'tool-refs.mdc'); - convertRuleToBrandRule(testCursorRule, testCursorOut, cursorProfile); + convertRuleToProfileRule(testCursorRule, testCursorOut, cursorProfile); // Read the converted file const convertedContent = fs.readFileSync(testCursorOut, 'utf8'); @@ -105,7 +106,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and // Convert it const testCursorOut = path.join(testDir, 'file-refs.mdc'); - convertRuleToBrandRule(testCursorRule, testCursorOut, cursorProfile); + convertRuleToProfileRule(testCursorRule, testCursorOut, cursorProfile); // Read the converted file const convertedContent = fs.readFileSync(testCursorOut, 'utf8'); diff --git a/tests/unit/rule-transformer-roo.test.js b/tests/unit/rule-transformer-roo.test.js index 6abe0467..2c2a65f2 100644 --- a/tests/unit/rule-transformer-roo.test.js +++ b/tests/unit/rule-transformer-roo.test.js @@ -3,8 +3,9 @@ import path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import { - convertRuleToBrandRule, - convertAllRulesToBrandRules + convertAllRulesToProfileRules, + convertRuleToProfileRule, + getRulesProfile } from '../../src/utils/rule-transformer.js'; import * as rooProfile from '../../scripts/profiles/roo.js'; @@ -44,7 +45,7 @@ Also has references to .mdc files.`; // Convert it const testRooRule = path.join(testDir, 'basic-terms.md'); - convertRuleToBrandRule(testCursorRule, testRooRule, rooProfile); + convertRuleToProfileRule(testCursorRule, testRooRule, rooProfile); // Read the converted file const convertedContent = fs.readFileSync(testRooRule, 'utf8'); @@ -75,7 +76,7 @@ alwaysApply: true // Convert it const testRooRule = path.join(testDir, 'tool-refs.md'); - convertRuleToBrandRule(testCursorRule, testRooRule, rooProfile); + convertRuleToProfileRule(testCursorRule, testRooRule, rooProfile); // Read the converted file const convertedContent = fs.readFileSync(testRooRule, 'utf8'); @@ -103,7 +104,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and // Convert it const testRooRule = path.join(testDir, 'file-refs.md'); - convertRuleToBrandRule(testCursorRule, testRooRule, rooProfile); + convertRuleToProfileRule(testCursorRule, testRooRule, rooProfile); // Read the converted file 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'); fs.writeFileSync(assetRule, 'dummy'); // 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 const rooDir = path.join(testDir, '.roo'); const found = fs.readdirSync(rooDir).some((f) => f.startsWith('rules-')); diff --git a/tests/unit/rule-transformer-windsurf.test.js b/tests/unit/rule-transformer-windsurf.test.js index 7382cdcd..c24ce765 100644 --- a/tests/unit/rule-transformer-windsurf.test.js +++ b/tests/unit/rule-transformer-windsurf.test.js @@ -2,7 +2,7 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; 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'; const __filename = fileURLToPath(import.meta.url); @@ -41,7 +41,7 @@ Also has references to .mdc files.`; // Convert it const testWindsurfRule = path.join(testDir, 'basic-terms.md'); - convertRuleToBrandRule(testCursorRule, testWindsurfRule, windsurfProfile); + convertRuleToProfileRule(testCursorRule, testWindsurfRule, windsurfProfile); // Read the converted file const convertedContent = fs.readFileSync(testWindsurfRule, 'utf8'); @@ -72,7 +72,7 @@ alwaysApply: true // Convert it const testWindsurfRule = path.join(testDir, 'tool-refs.md'); - convertRuleToBrandRule(testCursorRule, testWindsurfRule, windsurfProfile); + convertRuleToProfileRule(testCursorRule, testWindsurfRule, windsurfProfile); // Read the converted file const convertedContent = fs.readFileSync(testWindsurfRule, 'utf8'); @@ -100,7 +100,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and // Convert it const testWindsurfRule = path.join(testDir, 'file-refs.md'); - convertRuleToBrandRule(testCursorRule, testWindsurfRule, windsurfProfile); + convertRuleToProfileRule(testCursorRule, testWindsurfRule, windsurfProfile); // Read the converted file const convertedContent = fs.readFileSync(testWindsurfRule, 'utf8'); diff --git a/tests/unit/rule-transformer.test.js b/tests/unit/rule-transformer.test.js index c17ed757..f7542231 100644 --- a/tests/unit/rule-transformer.test.js +++ b/tests/unit/rule-transformer.test.js @@ -1,67 +1,204 @@ import { - BRAND_PROFILES, - BRAND_NAMES, - isValidBrand, - getBrandProfile + isValidProfile, + getRulesProfile } 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('Brand Configuration Validation', () => { - it('should have BRAND_PROFILES that match BRAND_RULE_OPTIONS', () => { - // Ensure BRAND_PROFILES keys match the authoritative list from constants/rules.js - const profileKeys = Object.keys(BRAND_PROFILES).sort(); - const ruleOptions = [...BRAND_RULE_OPTIONS].sort(); + describe('Profile Configuration Validation', () => { + it('should use RULES_PROFILES as the single source of truth', () => { + // Ensure RULES_PROFILES is properly defined and contains expected profiles + expect(Array.isArray(RULES_PROFILES)).toBe(true); + 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', () => { - const expectedNames = Object.keys(BRAND_PROFILES); - expect(BRAND_NAMES).toEqual(expectedNames); - }); - - it('should validate brands correctly with isValidBrand', () => { - // Test valid brands - BRAND_RULE_OPTIONS.forEach(brand => { - expect(isValidBrand(brand)).toBe(true); + it('should validate profiles correctly with isValidProfile', () => { + // Test valid profiles + RULES_PROFILES.forEach((profile) => { + expect(isValidProfile(profile)).toBe(true); }); - // Test invalid brands - expect(isValidBrand('invalid')).toBe(false); - expect(isValidBrand('vscode')).toBe(false); - expect(isValidBrand('')).toBe(false); - expect(isValidBrand(null)).toBe(false); - expect(isValidBrand(undefined)).toBe(false); + // Test invalid profiles + expect(isValidProfile('invalid')).toBe(false); + expect(isValidProfile('')).toBe(false); + expect(isValidProfile(null)).toBe(false); + expect(isValidProfile(undefined)).toBe(false); }); - it('should return correct brand profiles with getBrandProfile', () => { - BRAND_RULE_OPTIONS.forEach(brand => { - const profile = getBrandProfile(brand); - expect(profile).toBeDefined(); - expect(profile.brandName.toLowerCase()).toBe(brand); + it('should return correct rules profile with getRulesProfile', () => { + // Test valid profiles + RULES_PROFILES.forEach((profile) => { + const profileConfig = getRulesProfile(profile); + expect(profileConfig).toBeDefined(); + expect(profileConfig.profileName.toLowerCase()).toBe(profile); }); - // Test invalid brand - expect(getBrandProfile('invalid')).toBeUndefined(); + // Test invalid profile - should return null + expect(getRulesProfile('invalid')).toBeNull(); }); }); - describe('Brand Profile Structure', () => { - it('should have all required properties for each brand profile', () => { - BRAND_RULE_OPTIONS.forEach(brand => { - const profile = BRAND_PROFILES[brand]; - + describe('Profile Structure', () => { + it('should have all required properties for each profile', () => { + RULES_PROFILES.forEach((profile) => { + const profileConfig = getRulesProfile(profile); + // Check required properties - expect(profile).toHaveProperty('brandName'); - expect(profile).toHaveProperty('conversionConfig'); - expect(profile).toHaveProperty('fileMap'); - expect(profile).toHaveProperty('rulesDir'); - expect(profile).toHaveProperty('brandDir'); - - // Verify brand name matches (brandName is capitalized in profiles) - expect(profile.brandName.toLowerCase()).toBe(brand); + expect(profileConfig).toHaveProperty('profileName'); + expect(profileConfig).toHaveProperty('conversionConfig'); + expect(profileConfig).toHaveProperty('fileMap'); + expect(profileConfig).toHaveProperty('rulesDir'); + expect(profileConfig).toHaveProperty('profileDir'); + + // Check that conversionConfig has required structure + 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()); }); }); }); -}); \ No newline at end of file + + 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); + }); + }); +});