update semantics and terminology from 'brand rules' to 'rules profiles'
This commit is contained in:
@@ -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)}`);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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}`)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
32
src/constants/profiles.js
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'");
|
||||
});
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'"
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
201
tests/unit/mcp-config-validation.test.js
Normal file
201
tests/unit/mcp-config-validation.test.js
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
@@ -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-'));
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user