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
} 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)}`);

View File

@@ -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();

View File

@@ -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) => {

View File

@@ -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);

View File

@@ -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 };

View File

@@ -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 <action> [brands...]')
.command('rules <action> [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}`)
);
});
}
}

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
* @param {string} tasksPath - Path to the tasks.json file

View File

@@ -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
};

View File

@@ -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
};

View File

@@ -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';
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 __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
};

View File

@@ -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
};

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';
/**
* 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<boolean>} - 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 };
export { confirmProfilesRemove };

View File

@@ -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}`);
}
}
}

View File

@@ -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
};

View File

@@ -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<string[]>} 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<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.
* 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<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() {
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;
}
const { rulesProfiles } = await inquirer.prompt([rulesProfilesQuestion]);
return rulesProfiles;
}

View File

@@ -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'");
});

View File

@@ -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(

View File

@@ -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
});
});

View File

@@ -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);
});
});

View File

@@ -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'"
);

View File

@@ -1102,31 +1102,31 @@ describe('rules command', () => {
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
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 <brand> command', async () => {
test('should handle rules remove <profile> 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);

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 { 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');

View File

@@ -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-'));

View File

@@ -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');

View File

@@ -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());
});
});
});
});
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);
});
});
});