combine to /src/utils/profiles.js; add codex and claude code profiles
This commit is contained in:
@@ -18,7 +18,7 @@ import { RULES_ACTIONS } from '../../../../src/constants/rules-actions.js';
|
||||
import {
|
||||
wouldRemovalLeaveNoProfiles,
|
||||
getInstalledProfiles
|
||||
} from '../../../../src/utils/profile-detection.js';
|
||||
} from '../../../../src/utils/profiles.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
convertAllRulesToProfileRules,
|
||||
getRulesProfile
|
||||
} from '../src/utils/rule-transformer.js';
|
||||
import { runInteractiveRulesSetup } from '../src/utils/rules-setup.js';
|
||||
import { runInteractiveRulesSetup } from '../src/utils/profiles.js';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
||||
@@ -75,7 +75,7 @@ import {
|
||||
import {
|
||||
wouldRemovalLeaveNoProfiles,
|
||||
getInstalledProfiles
|
||||
} from '../../src/utils/profile-detection.js';
|
||||
} from '../../src/utils/profiles.js';
|
||||
|
||||
import { initializeProject } from '../init.js';
|
||||
import {
|
||||
@@ -102,7 +102,13 @@ import {
|
||||
isValidProfile,
|
||||
getRulesProfile
|
||||
} from '../../src/utils/rule-transformer.js';
|
||||
import { runInteractiveRulesSetup } from '../../src/utils/rules-setup.js';
|
||||
import {
|
||||
runInteractiveRulesSetup,
|
||||
generateProfileSummary,
|
||||
categorizeProfileResults,
|
||||
generateProfileRemovalSummary,
|
||||
categorizeRemovalResults
|
||||
} from '../../src/utils/profiles.js';
|
||||
|
||||
/**
|
||||
* Runs the interactive setup process for model configuration.
|
||||
@@ -2701,7 +2707,7 @@ Examples:
|
||||
for (const profile of selectedRulesProfiles) {
|
||||
if (!isValidProfile(profile)) {
|
||||
console.warn(
|
||||
`Rules profile for "${profile}" not found. Valid profiles: ${RULE_PROFILES.join(', ')}. Skipping.`
|
||||
`Rule profile for "${profile}" not found. Valid profiles: ${RULE_PROFILES.join(', ')}. Skipping.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -2713,18 +2719,15 @@ Examples:
|
||||
if (typeof profileConfig.onAddRulesProfile === 'function') {
|
||||
profileConfig.onAddRulesProfile(projectDir);
|
||||
}
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Summary for ${profile}: ${addResult.success} rules added, ${addResult.failed} failed.`
|
||||
)
|
||||
);
|
||||
|
||||
console.log(chalk.green(generateProfileSummary(profile, addResult)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!profiles || profiles.length === 0) {
|
||||
console.error(
|
||||
'Please specify at least one rules profile (e.g., windsurf, roo).'
|
||||
'Please specify at least one rule profile (e.g., windsurf, roo).'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -2760,7 +2763,7 @@ Examples:
|
||||
for (const profile of expandedProfiles) {
|
||||
if (!isValidProfile(profile)) {
|
||||
console.warn(
|
||||
`Rules profile for "${profile}" not found. Valid profiles: ${RULE_PROFILES.join(', ')}. Skipping.`
|
||||
`Rule profile for "${profile}" not found. Valid profiles: ${RULE_PROFILES.join(', ')}. Skipping.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -2786,16 +2789,14 @@ Examples:
|
||||
failed: addResult.failed
|
||||
});
|
||||
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Summary for ${profile}: ${addResult.success} rules added, ${addResult.failed} failed.`
|
||||
)
|
||||
);
|
||||
console.log(chalk.green(generateProfileSummary(profile, addResult)));
|
||||
} else if (action === RULES_ACTIONS.REMOVE) {
|
||||
console.log(chalk.blue(`Removing rules for profile: ${profile}...`));
|
||||
const result = removeProfileRules(projectDir, profileConfig);
|
||||
removalResults.push(result);
|
||||
console.log(chalk.blue(`Completed removal for profile: ${profile}`));
|
||||
console.log(
|
||||
chalk.green(generateProfileRemovalSummary(profile, result))
|
||||
);
|
||||
} else {
|
||||
console.error(
|
||||
`Unknown action. Use "${RULES_ACTIONS.ADD}" or "${RULES_ACTIONS.REMOVE}".`
|
||||
@@ -2806,67 +2807,91 @@ Examples:
|
||||
|
||||
// Print summary for additions
|
||||
if (action === RULES_ACTIONS.ADD && addResults.length > 0) {
|
||||
const totalSuccess = addResults.reduce((sum, r) => sum + r.success, 0);
|
||||
const totalFailed = addResults.reduce((sum, r) => sum + r.failed, 0);
|
||||
const successfulProfiles = addResults
|
||||
.filter((r) => r.success > 0)
|
||||
.map((r) => r.profileName);
|
||||
const {
|
||||
allSuccessfulProfiles,
|
||||
totalSuccess,
|
||||
totalFailed,
|
||||
simpleProfiles
|
||||
} = categorizeProfileResults(addResults);
|
||||
|
||||
if (successfulProfiles.length > 0) {
|
||||
if (allSuccessfulProfiles.length > 0) {
|
||||
console.log(
|
||||
chalk.green(
|
||||
`\nSuccessfully added rules for: ${successfulProfiles.join(', ')}`
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Total: ${totalSuccess} rules added, ${totalFailed} failed.`
|
||||
`\nSuccessfully added rules for: ${allSuccessfulProfiles.join(', ')}`
|
||||
)
|
||||
);
|
||||
|
||||
// Create a more descriptive summary
|
||||
if (totalSuccess > 0 && simpleProfiles.length > 0) {
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Total: ${totalSuccess} rules added, ${totalFailed} failed, ${simpleProfiles.length} integration guide(s) copied.`
|
||||
)
|
||||
);
|
||||
} else if (totalSuccess > 0) {
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Total: ${totalSuccess} rules added, ${totalFailed} failed.`
|
||||
)
|
||||
);
|
||||
} else if (simpleProfiles.length > 0) {
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Total: ${simpleProfiles.length} integration guide(s) copied.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print summary for removals
|
||||
if (action === RULES_ACTIONS.REMOVE) {
|
||||
const successes = removalResults
|
||||
.filter((r) => r.success)
|
||||
.map((r) => r.profileName);
|
||||
const skipped = removalResults
|
||||
.filter((r) => r.skipped)
|
||||
.map((r) => r.profileName);
|
||||
const errors = removalResults.filter(
|
||||
(r) => r.error && !r.success && !r.skipped
|
||||
);
|
||||
const withNotices = removalResults.filter((r) => r.notice);
|
||||
if (action === RULES_ACTIONS.REMOVE && removalResults.length > 0) {
|
||||
const {
|
||||
successfulRemovals,
|
||||
skippedRemovals,
|
||||
failedRemovals,
|
||||
removalsWithNotices
|
||||
} = categorizeRemovalResults(removalResults);
|
||||
|
||||
if (successes.length > 0) {
|
||||
if (successfulRemovals.length > 0) {
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Successfully removed Task Master rules: ${successes.join(', ')}`
|
||||
`\nSuccessfully removed rules for: ${successfulRemovals.join(', ')}`
|
||||
)
|
||||
);
|
||||
}
|
||||
if (skipped.length > 0) {
|
||||
if (skippedRemovals.length > 0) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Skipped (default or protected): ${skipped.join(', ')}`
|
||||
`Skipped (default or protected): ${skippedRemovals.join(', ')}`
|
||||
)
|
||||
);
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
errors.forEach((r) => {
|
||||
console.log(
|
||||
chalk.red(`Error removing ${r.profileName}: ${r.error}`)
|
||||
);
|
||||
if (failedRemovals.length > 0) {
|
||||
console.log(chalk.red('\nErrors occurred:'));
|
||||
failedRemovals.forEach((r) => {
|
||||
console.log(chalk.red(` ${r.profileName}: ${r.error}`));
|
||||
});
|
||||
}
|
||||
// Display notices about preserved files/configurations
|
||||
if (withNotices.length > 0) {
|
||||
if (removalsWithNotices.length > 0) {
|
||||
console.log(chalk.cyan('\nNotices:'));
|
||||
withNotices.forEach((r) => {
|
||||
removalsWithNotices.forEach((r) => {
|
||||
console.log(chalk.cyan(` ${r.profileName}: ${r.notice}`));
|
||||
});
|
||||
}
|
||||
|
||||
// Overall summary
|
||||
const totalProcessed = removalResults.length;
|
||||
const totalSuccessful = successfulRemovals.length;
|
||||
const totalSkipped = skippedRemovals.length;
|
||||
const totalFailed = failedRemovals.length;
|
||||
|
||||
console.log(
|
||||
chalk.blue(
|
||||
`\nTotal: ${totalProcessed} profile(s) processed - ${totalSuccessful} removed, ${totalSkipped} skipped, ${totalFailed} failed.`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
62
scripts/profiles/claude.js
Normal file
62
scripts/profiles/claude.js
Normal file
@@ -0,0 +1,62 @@
|
||||
// Claude Code profile for rule-transformer
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { isSilentMode, log } from '../modules/utils.js';
|
||||
|
||||
// Lifecycle functions for Claude Code profile
|
||||
function onAddRulesProfile(targetDir) {
|
||||
const sourceFile = path.join(process.cwd(), 'assets', 'AGENTS.md');
|
||||
const destFile = path.join(targetDir, 'CLAUDE.md');
|
||||
|
||||
if (fs.existsSync(sourceFile)) {
|
||||
try {
|
||||
fs.copyFileSync(sourceFile, destFile);
|
||||
log('debug', `[Claude] Copied AGENTS.md to ${destFile}`);
|
||||
} catch (err) {
|
||||
log('debug', `[Claude] Failed to copy AGENTS.md: ${err.message}`);
|
||||
}
|
||||
} else {
|
||||
log('debug', `[Claude] AGENTS.md not found at ${sourceFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
function onRemoveRulesProfile(targetDir) {
|
||||
log('debug', `[Claude] onRemoveRulesProfile called for ${targetDir}`);
|
||||
const claudeFile = path.join(targetDir, 'CLAUDE.md');
|
||||
if (fs.existsSync(claudeFile)) {
|
||||
try {
|
||||
fs.rmSync(claudeFile, { force: true });
|
||||
log('debug', `[Claude] Removed CLAUDE.md from ${targetDir}`);
|
||||
} catch (err) {
|
||||
log('debug', `[Claude] Failed to remove CLAUDE.md: ${err.message}`);
|
||||
}
|
||||
}
|
||||
log('debug', `[Claude] onRemoveRulesProfile completed for ${targetDir}`);
|
||||
}
|
||||
|
||||
function onPostConvertRulesProfile(targetDir) {
|
||||
onAddRulesProfile(targetDir);
|
||||
}
|
||||
|
||||
// Simple filename function
|
||||
function getTargetRuleFilename(sourceFilename) {
|
||||
return sourceFilename;
|
||||
}
|
||||
|
||||
// Simple profile configuration - bypasses base-profile system
|
||||
export const claudeProfile = {
|
||||
profileName: 'claude',
|
||||
displayName: 'Claude Code',
|
||||
profileDir: '.', // Root directory
|
||||
rulesDir: '.', // No rules directory needed
|
||||
mcpConfig: false, // No MCP config needed
|
||||
mcpConfigName: null,
|
||||
mcpConfigPath: null,
|
||||
conversionConfig: {},
|
||||
fileMap: {},
|
||||
globalReplacements: [],
|
||||
getTargetRuleFilename,
|
||||
onAddRulesProfile,
|
||||
onRemoveRulesProfile,
|
||||
onPostConvertRulesProfile
|
||||
};
|
||||
62
scripts/profiles/codex.js
Normal file
62
scripts/profiles/codex.js
Normal file
@@ -0,0 +1,62 @@
|
||||
// Codex profile for rule-transformer
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { isSilentMode, log } from '../modules/utils.js';
|
||||
|
||||
// Lifecycle functions for Codex profile
|
||||
function onAddRulesProfile(targetDir) {
|
||||
const sourceFile = path.join(process.cwd(), 'assets', 'AGENTS.md');
|
||||
const destFile = path.join(targetDir, 'AGENTS.md');
|
||||
|
||||
if (fs.existsSync(sourceFile)) {
|
||||
try {
|
||||
fs.copyFileSync(sourceFile, destFile);
|
||||
log('debug', `[Codex] Copied AGENTS.md to ${destFile}`);
|
||||
} catch (err) {
|
||||
log('debug', `[Codex] Failed to copy AGENTS.md: ${err.message}`);
|
||||
}
|
||||
} else {
|
||||
log('debug', `[Codex] AGENTS.md not found at ${sourceFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
function onRemoveRulesProfile(targetDir) {
|
||||
log('debug', `[Codex] onRemoveRulesProfile called for ${targetDir}`);
|
||||
const agentsFile = path.join(targetDir, 'AGENTS.md');
|
||||
if (fs.existsSync(agentsFile)) {
|
||||
try {
|
||||
fs.rmSync(agentsFile, { force: true });
|
||||
log('debug', `[Codex] Removed AGENTS.md from ${targetDir}`);
|
||||
} catch (err) {
|
||||
log('debug', `[Codex] Failed to remove AGENTS.md: ${err.message}`);
|
||||
}
|
||||
}
|
||||
log('debug', `[Codex] onRemoveRulesProfile completed for ${targetDir}`);
|
||||
}
|
||||
|
||||
function onPostConvertRulesProfile(targetDir) {
|
||||
onAddRulesProfile(targetDir);
|
||||
}
|
||||
|
||||
// Simple filename function
|
||||
function getTargetRuleFilename(sourceFilename) {
|
||||
return sourceFilename;
|
||||
}
|
||||
|
||||
// Simple profile configuration - bypasses base-profile system
|
||||
export const codexProfile = {
|
||||
profileName: 'codex',
|
||||
displayName: 'Codex',
|
||||
profileDir: '.', // Root directory
|
||||
rulesDir: '.', // No rules directory needed
|
||||
mcpConfig: false, // No MCP config needed
|
||||
mcpConfigName: null,
|
||||
mcpConfigPath: null,
|
||||
conversionConfig: {},
|
||||
fileMap: {},
|
||||
globalReplacements: [],
|
||||
getTargetRuleFilename,
|
||||
onAddRulesProfile,
|
||||
onRemoveRulesProfile,
|
||||
onPostConvertRulesProfile
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
// Profile exports for centralized importing
|
||||
export * as clineProfile from './cline.js';
|
||||
export * as cursorProfile from './cursor.js';
|
||||
export * as rooProfile from './roo.js';
|
||||
export * as traeProfile from './trae.js';
|
||||
export * as windsurfProfile from './windsurf.js';
|
||||
export { claudeProfile } from './claude.js';
|
||||
export { clineProfile } from './cline.js';
|
||||
export { codexProfile } from './codex.js';
|
||||
export { cursorProfile } from './cursor.js';
|
||||
export { rooProfile } from './roo.js';
|
||||
export { traeProfile } from './trae.js';
|
||||
export { windsurfProfile } from './windsurf.js';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @typedef {'cline' | 'cursor' | 'roo' | 'trae' | 'windsurf'} RulesProfile
|
||||
* @typedef {'claude' | 'cline' | 'codex' | 'cursor' | 'roo' | 'trae' | 'windsurf'} RulesProfile
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -10,8 +10,10 @@
|
||||
*
|
||||
* @type {RulesProfile[]}
|
||||
* @description Defines possible rule profile sets:
|
||||
* - claude: Claude Code integration
|
||||
* - cline: Cline IDE rules
|
||||
* - cursor: Cursor IDE rules (default)
|
||||
* - codex: Codex integration
|
||||
* - cursor: Cursor IDE rules
|
||||
* - roo: Roo Code IDE rules
|
||||
* - trae: Trae IDE rules
|
||||
* - windsurf: Windsurf IDE rules
|
||||
@@ -21,7 +23,15 @@
|
||||
* 2. Create a profile file in scripts/profiles/{profile}.js
|
||||
* 3. Export it as {profile}Profile in scripts/profiles/index.js
|
||||
*/
|
||||
export const RULE_PROFILES = ['cline', 'cursor', 'roo', 'trae', 'windsurf'];
|
||||
export const RULE_PROFILES = [
|
||||
'claude',
|
||||
'cline',
|
||||
'codex',
|
||||
'cursor',
|
||||
'roo',
|
||||
'trae',
|
||||
'windsurf'
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if a given rule profile is valid
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Profile Detection Utility
|
||||
* Helper functions to detect existing profiles in a project
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { RULE_PROFILES } from '../constants/profiles.js';
|
||||
import { getRulesProfile } from './rule-transformer.js';
|
||||
|
||||
/**
|
||||
* Detect which profiles are currently installed in the project
|
||||
* @param {string} projectRoot - Project root directory
|
||||
* @returns {string[]} Array of installed profile names
|
||||
*/
|
||||
export function getInstalledProfiles(projectRoot) {
|
||||
const installedProfiles = [];
|
||||
|
||||
for (const profileName of RULE_PROFILES) {
|
||||
const profileConfig = getRulesProfile(profileName);
|
||||
if (!profileConfig) continue;
|
||||
|
||||
// Check if the profile directory exists
|
||||
const profileDir = path.join(projectRoot, profileConfig.profileDir);
|
||||
const rulesDir = path.join(projectRoot, profileConfig.rulesDir);
|
||||
|
||||
// A profile is considered installed if either the profile dir or rules dir exists
|
||||
if (fs.existsSync(profileDir) || fs.existsSync(rulesDir)) {
|
||||
installedProfiles.push(profileName);
|
||||
}
|
||||
}
|
||||
|
||||
return installedProfiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if removing the specified profiles would result in no profiles remaining
|
||||
* @param {string} projectRoot - Project root directory
|
||||
* @param {string[]} profilesToRemove - Array of profile names to remove
|
||||
* @returns {boolean} True if removal would result in no profiles remaining
|
||||
*/
|
||||
export function wouldRemovalLeaveNoProfiles(projectRoot, profilesToRemove) {
|
||||
const installedProfiles = getInstalledProfiles(projectRoot);
|
||||
const remainingProfiles = installedProfiles.filter(
|
||||
(profile) => !profilesToRemove.includes(profile)
|
||||
);
|
||||
|
||||
return remainingProfiles.length === 0 && installedProfiles.length > 0;
|
||||
}
|
||||
228
src/utils/profiles.js
Normal file
228
src/utils/profiles.js
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* Profiles Utility
|
||||
* Consolidated utilities for profile detection, setup, and summary generation
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import readline from 'readline';
|
||||
import inquirer from 'inquirer';
|
||||
import chalk from 'chalk';
|
||||
import { log } from '../../scripts/modules/utils.js';
|
||||
import { getRulesProfile } from './rule-transformer.js';
|
||||
import { RULE_PROFILES } from '../constants/profiles.js';
|
||||
|
||||
// =============================================================================
|
||||
// PROFILE DETECTION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Detect which profiles are currently installed in the project
|
||||
* @param {string} projectRoot - Project root directory
|
||||
* @returns {string[]} Array of installed profile names
|
||||
*/
|
||||
export function getInstalledProfiles(projectRoot) {
|
||||
const installedProfiles = [];
|
||||
|
||||
for (const profileName of RULE_PROFILES) {
|
||||
const profileConfig = getRulesProfile(profileName);
|
||||
if (!profileConfig) continue;
|
||||
|
||||
// Check if the profile directory exists
|
||||
const profileDir = path.join(projectRoot, profileConfig.profileDir);
|
||||
const rulesDir = path.join(projectRoot, profileConfig.rulesDir);
|
||||
|
||||
// A profile is considered installed if either the profile dir or rules dir exists
|
||||
if (fs.existsSync(profileDir) || fs.existsSync(rulesDir)) {
|
||||
installedProfiles.push(profileName);
|
||||
}
|
||||
}
|
||||
|
||||
return installedProfiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if removing the specified profiles would result in no profiles remaining
|
||||
* @param {string} projectRoot - Project root directory
|
||||
* @param {string[]} profilesToRemove - Array of profile names to remove
|
||||
* @returns {boolean} True if removal would result in no profiles remaining
|
||||
*/
|
||||
export function wouldRemovalLeaveNoProfiles(projectRoot, profilesToRemove) {
|
||||
const installedProfiles = getInstalledProfiles(projectRoot);
|
||||
const remainingProfiles = installedProfiles.filter(
|
||||
(profile) => !profilesToRemove.includes(profile)
|
||||
);
|
||||
|
||||
return remainingProfiles.length === 0 && installedProfiles.length > 0;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROFILE SETUP
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get the display name for a profile
|
||||
*/
|
||||
function getProfileDisplayName(name) {
|
||||
const profile = getRulesProfile(name);
|
||||
return profile?.displayName || name.charAt(0).toUpperCase() + name.slice(1);
|
||||
}
|
||||
|
||||
// Dynamically generate availableRulesProfiles from RULE_PROFILES
|
||||
const availableRulesProfiles = RULE_PROFILES.map((name) => {
|
||||
const displayName = getProfileDisplayName(name);
|
||||
return {
|
||||
name: displayName,
|
||||
value: name
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Launches an interactive prompt for selecting which rule profiles to include in your project.
|
||||
*
|
||||
* This function dynamically lists all available profiles (from RULE_PROFILES) and presents them as checkboxes.
|
||||
* The user must select at least one profile (no defaults are pre-selected). The result is an array of selected profile names.
|
||||
*
|
||||
* Used by both project initialization (init) and the CLI 'task-master rules setup' command.
|
||||
*
|
||||
* @returns {Promise<string[]>} Array of selected profile names (e.g., ['cursor', 'windsurf'])
|
||||
*/
|
||||
export async function runInteractiveRulesSetup() {
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
'\nRule profiles help enforce best practices and conventions for Task Master.'
|
||||
)
|
||||
);
|
||||
const rulesProfilesQuestion = {
|
||||
type: 'checkbox',
|
||||
name: 'rulesProfiles',
|
||||
message: 'Which tools would you like rule profiles included for?',
|
||||
choices: availableRulesProfiles,
|
||||
validate: (input) => input.length > 0 || 'You must select at least one.'
|
||||
};
|
||||
const { rulesProfiles } = await inquirer.prompt([rulesProfilesQuestion]);
|
||||
return rulesProfiles;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROFILE SUMMARY
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate appropriate summary message for a profile based on its type
|
||||
* @param {string} profileName - Name of the profile
|
||||
* @param {Object} addResult - Result object with success/failed counts
|
||||
* @returns {string} Formatted summary message
|
||||
*/
|
||||
export function generateProfileSummary(profileName, addResult) {
|
||||
const profileConfig = getRulesProfile(profileName);
|
||||
const isSimpleProfile = Object.keys(profileConfig.fileMap).length === 0;
|
||||
|
||||
if (isSimpleProfile) {
|
||||
// Simple profiles like Claude and Codex only copy AGENTS.md
|
||||
const targetFileName = profileName === 'claude' ? 'CLAUDE.md' : 'AGENTS.md';
|
||||
return `Summary for ${profileName}: Integration guide copied to ${targetFileName}`;
|
||||
} else {
|
||||
return `Summary for ${profileName}: ${addResult.success} rules added, ${addResult.failed} failed.`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate appropriate summary message for profile removal
|
||||
* @param {string} profileName - Name of the profile
|
||||
* @param {Object} removeResult - Result object from removal operation
|
||||
* @returns {string} Formatted summary message
|
||||
*/
|
||||
export function generateProfileRemovalSummary(profileName, removeResult) {
|
||||
const profileConfig = getRulesProfile(profileName);
|
||||
const isSimpleProfile = Object.keys(profileConfig.fileMap).length === 0;
|
||||
|
||||
if (removeResult.skipped) {
|
||||
return `Summary for ${profileName}: Skipped (default or protected files)`;
|
||||
}
|
||||
|
||||
if (removeResult.error && !removeResult.success) {
|
||||
return `Summary for ${profileName}: Failed to remove - ${removeResult.error}`;
|
||||
}
|
||||
|
||||
if (isSimpleProfile) {
|
||||
// Simple profiles like Claude and Codex only have an integration guide
|
||||
const targetFileName = profileName === 'claude' ? 'CLAUDE.md' : 'AGENTS.md';
|
||||
return `Summary for ${profileName}: Integration guide (${targetFileName}) removed`;
|
||||
} else {
|
||||
// Full profiles have rules directories and potentially MCP configs
|
||||
let summary = `Summary for ${profileName}: Rules directory removed`;
|
||||
if (removeResult.notice) {
|
||||
summary += ` (${removeResult.notice})`;
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize profiles and generate final summary statistics
|
||||
* @param {Array} addResults - Array of add result objects
|
||||
* @returns {Object} Object with categorized profiles and totals
|
||||
*/
|
||||
export function categorizeProfileResults(addResults) {
|
||||
const successfulProfiles = [];
|
||||
const simpleProfiles = [];
|
||||
let totalSuccess = 0;
|
||||
let totalFailed = 0;
|
||||
|
||||
addResults.forEach((r) => {
|
||||
totalSuccess += r.success;
|
||||
totalFailed += r.failed;
|
||||
|
||||
const profileConfig = getRulesProfile(r.profileName);
|
||||
const isSimpleProfile = Object.keys(profileConfig.fileMap).length === 0;
|
||||
|
||||
if (isSimpleProfile) {
|
||||
// Simple profiles are successful if they completed without error
|
||||
simpleProfiles.push(r.profileName);
|
||||
} else if (r.success > 0) {
|
||||
// Full profiles are successful if they added rules
|
||||
successfulProfiles.push(r.profileName);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
successfulProfiles,
|
||||
simpleProfiles,
|
||||
allSuccessfulProfiles: [...successfulProfiles, ...simpleProfiles],
|
||||
totalSuccess,
|
||||
totalFailed
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize removal results and generate final summary statistics
|
||||
* @param {Array} removalResults - Array of removal result objects
|
||||
* @returns {Object} Object with categorized removal results
|
||||
*/
|
||||
export function categorizeRemovalResults(removalResults) {
|
||||
const successfulRemovals = [];
|
||||
const skippedRemovals = [];
|
||||
const failedRemovals = [];
|
||||
const removalsWithNotices = [];
|
||||
|
||||
removalResults.forEach((result) => {
|
||||
if (result.success) {
|
||||
successfulRemovals.push(result.profileName);
|
||||
} else if (result.skipped) {
|
||||
skippedRemovals.push(result.profileName);
|
||||
} else if (result.error) {
|
||||
failedRemovals.push(result);
|
||||
}
|
||||
|
||||
if (result.notice) {
|
||||
removalsWithNotices.push(result);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
successfulRemovals,
|
||||
skippedRemovals,
|
||||
failedRemovals,
|
||||
removalsWithNotices
|
||||
};
|
||||
}
|
||||
@@ -299,7 +299,23 @@ export function removeProfileRules(projectDir, profile) {
|
||||
};
|
||||
|
||||
try {
|
||||
// Check if profile directory exists at all
|
||||
// Handle simple profiles (Claude, Codex) that just copy files to root
|
||||
const isSimpleProfile = Object.keys(profile.fileMap).length === 0;
|
||||
|
||||
if (isSimpleProfile) {
|
||||
// For simple profiles, just call their removal hook and return
|
||||
if (typeof profile.onRemoveRulesProfile === 'function') {
|
||||
profile.onRemoveRulesProfile(projectDir);
|
||||
}
|
||||
result.success = true;
|
||||
log(
|
||||
'debug',
|
||||
`[Rule Transformer] Successfully removed ${profile.profileName} files from ${projectDir}`
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check if profile directory exists at all (for full profiles)
|
||||
if (!fs.existsSync(profileDir)) {
|
||||
result.success = true;
|
||||
result.skipped = true;
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import readline from 'readline';
|
||||
import inquirer from 'inquirer';
|
||||
import chalk from 'chalk';
|
||||
import { log } from '../../scripts/modules/utils.js';
|
||||
import { getRulesProfile } from './rule-transformer.js';
|
||||
import { RULE_PROFILES } from '../constants/profiles.js';
|
||||
|
||||
// Dynamically generate availableRulesProfiles from RULE_PROFILES
|
||||
const availableRulesProfiles = RULE_PROFILES.map((name) => {
|
||||
const displayName = getProfileDisplayName(name);
|
||||
return {
|
||||
name: displayName,
|
||||
value: name
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the display name for a profile
|
||||
*/
|
||||
function getProfileDisplayName(name) {
|
||||
const profile = getRulesProfile(name);
|
||||
return profile?.displayName || name.charAt(0).toUpperCase() + name.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the interactive rules setup flow (profile rules selection only)
|
||||
* @returns {Promise<string[]>} The selected profile rules
|
||||
*/
|
||||
/**
|
||||
* Launches an interactive prompt for selecting which profile rules to include in your project.
|
||||
*
|
||||
* This function dynamically lists all available profiles (from RULE_PROFILES) and presents them as checkboxes.
|
||||
* The user must select at least one profile (no defaults are pre-selected). The result is an array of selected profile names.
|
||||
*
|
||||
* Used by both project initialization (init) and the CLI 'task-master rules setup' command to ensure DRY, consistent UX.
|
||||
*
|
||||
* @returns {Promise<string[]>} Array of selected profile rule names (e.g., ['cursor', 'windsurf'])
|
||||
*/
|
||||
export async function runInteractiveRulesSetup() {
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
'\nRules help enforce best practices and conventions for Task Master.'
|
||||
)
|
||||
);
|
||||
const rulesProfilesQuestion = {
|
||||
type: 'checkbox',
|
||||
name: 'rulesProfiles',
|
||||
message: 'Which IDEs would you like rule profiles included for?',
|
||||
choices: availableRulesProfiles,
|
||||
validate: (input) => input.length > 0 || 'You must select at least one.'
|
||||
};
|
||||
const { rulesProfiles } = await inquirer.prompt([rulesProfilesQuestion]);
|
||||
return rulesProfiles;
|
||||
}
|
||||
@@ -1126,7 +1126,9 @@ describe('rules command', () => {
|
||||
expect.stringMatching(/removing rules for profile: roo/i)
|
||||
);
|
||||
expect(mockConsoleLog).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/completed removal for profile: roo/i)
|
||||
expect.stringMatching(
|
||||
/Summary for roo: (Rules directory removed|Skipped \(default or protected files\))/i
|
||||
)
|
||||
);
|
||||
// Should not exit with error
|
||||
expect(mockExit).not.toHaveBeenCalledWith(1);
|
||||
|
||||
@@ -117,16 +117,38 @@ describe('MCP Configuration Validation', () => {
|
||||
describe('Profile Directory Structure', () => {
|
||||
test('should ensure each profile has a unique directory', () => {
|
||||
const profileDirs = new Set();
|
||||
// Simple profiles that use root directory (can share the same directory)
|
||||
const simpleProfiles = ['claude', 'codex'];
|
||||
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
|
||||
// Simple profiles can share the root directory
|
||||
if (simpleProfiles.includes(profileName)) {
|
||||
expect(profile.profileDir).toBe('.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Full profiles should have unique directories
|
||||
expect(profileDirs.has(profile.profileDir)).toBe(false);
|
||||
profileDirs.add(profile.profileDir);
|
||||
});
|
||||
});
|
||||
|
||||
test('should ensure profile directories follow expected naming convention', () => {
|
||||
// Simple profiles that use root directory
|
||||
const simpleProfiles = ['claude', 'codex'];
|
||||
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
|
||||
// Simple profiles use root directory
|
||||
if (simpleProfiles.includes(profileName)) {
|
||||
expect(profile.profileDir).toBe('.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Full profiles should follow the .name pattern
|
||||
expect(profile.profileDir).toMatch(/^\.[\w-]+$/);
|
||||
});
|
||||
});
|
||||
@@ -144,6 +166,8 @@ describe('MCP Configuration Validation', () => {
|
||||
expect(mcpEnabledProfiles).toContain('roo');
|
||||
expect(mcpEnabledProfiles).not.toContain('cline');
|
||||
expect(mcpEnabledProfiles).not.toContain('trae');
|
||||
expect(mcpEnabledProfiles).not.toContain('claude');
|
||||
expect(mcpEnabledProfiles).not.toContain('codex');
|
||||
});
|
||||
|
||||
test('should provide all necessary information for MCP config creation', () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import { convertRuleToProfileRule } from '../../src/utils/rule-transformer.js';
|
||||
import * as clineProfile from '../../scripts/profiles/cline.js';
|
||||
import { clineProfile } from '../../scripts/profiles/cline.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -41,11 +41,7 @@ Also has references to .mdc files.`;
|
||||
|
||||
// Convert it
|
||||
const testClineRule = path.join(testDir, 'basic-terms.md');
|
||||
convertRuleToProfileRule(
|
||||
testCursorRule,
|
||||
testClineRule,
|
||||
clineProfile.clineProfile
|
||||
);
|
||||
convertRuleToProfileRule(testCursorRule, testClineRule, clineProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testClineRule, 'utf8');
|
||||
@@ -76,11 +72,7 @@ alwaysApply: true
|
||||
|
||||
// Convert it
|
||||
const testClineRule = path.join(testDir, 'tool-refs.md');
|
||||
convertRuleToProfileRule(
|
||||
testCursorRule,
|
||||
testClineRule,
|
||||
clineProfile.clineProfile
|
||||
);
|
||||
convertRuleToProfileRule(testCursorRule, testClineRule, clineProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testClineRule, 'utf8');
|
||||
@@ -108,11 +100,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
|
||||
|
||||
// Convert it
|
||||
const testClineRule = path.join(testDir, 'file-refs.md');
|
||||
convertRuleToProfileRule(
|
||||
testCursorRule,
|
||||
testClineRule,
|
||||
clineProfile.clineProfile
|
||||
);
|
||||
convertRuleToProfileRule(testCursorRule, testClineRule, clineProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testClineRule, 'utf8');
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
convertRuleToProfileRule,
|
||||
getRulesProfile
|
||||
} from '../../src/utils/rule-transformer.js';
|
||||
import * as cursorProfile from '../../scripts/profiles/cursor.js';
|
||||
import { cursorProfile } from '../../scripts/profiles/cursor.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -45,11 +45,7 @@ Also has references to .mdc files.`;
|
||||
|
||||
// Convert it
|
||||
const testCursorOut = path.join(testDir, 'basic-terms.mdc');
|
||||
convertRuleToProfileRule(
|
||||
testCursorRule,
|
||||
testCursorOut,
|
||||
cursorProfile.cursorProfile
|
||||
);
|
||||
convertRuleToProfileRule(testCursorRule, testCursorOut, cursorProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testCursorOut, 'utf8');
|
||||
@@ -80,11 +76,7 @@ alwaysApply: true
|
||||
|
||||
// Convert it
|
||||
const testCursorOut = path.join(testDir, 'tool-refs.mdc');
|
||||
convertRuleToProfileRule(
|
||||
testCursorRule,
|
||||
testCursorOut,
|
||||
cursorProfile.cursorProfile
|
||||
);
|
||||
convertRuleToProfileRule(testCursorRule, testCursorOut, cursorProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testCursorOut, 'utf8');
|
||||
@@ -114,11 +106,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
|
||||
|
||||
// Convert it
|
||||
const testCursorOut = path.join(testDir, 'file-refs.mdc');
|
||||
convertRuleToProfileRule(
|
||||
testCursorRule,
|
||||
testCursorOut,
|
||||
cursorProfile.cursorProfile
|
||||
);
|
||||
convertRuleToProfileRule(testCursorRule, testCursorOut, cursorProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testCursorOut, 'utf8');
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
convertRuleToProfileRule,
|
||||
getRulesProfile
|
||||
} from '../../src/utils/rule-transformer.js';
|
||||
import * as rooProfile from '../../scripts/profiles/roo.js';
|
||||
import { rooProfile } from '../../scripts/profiles/roo.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -45,11 +45,7 @@ Also has references to .mdc files.`;
|
||||
|
||||
// Convert it
|
||||
const testRooRule = path.join(testDir, 'basic-terms.md');
|
||||
convertRuleToProfileRule(
|
||||
testCursorRule,
|
||||
testRooRule,
|
||||
rooProfile.rooProfile
|
||||
);
|
||||
convertRuleToProfileRule(testCursorRule, testRooRule, rooProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
|
||||
@@ -80,11 +76,7 @@ alwaysApply: true
|
||||
|
||||
// Convert it
|
||||
const testRooRule = path.join(testDir, 'tool-refs.md');
|
||||
convertRuleToProfileRule(
|
||||
testCursorRule,
|
||||
testRooRule,
|
||||
rooProfile.rooProfile
|
||||
);
|
||||
convertRuleToProfileRule(testCursorRule, testRooRule, rooProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
|
||||
@@ -112,11 +104,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
|
||||
|
||||
// Convert it
|
||||
const testRooRule = path.join(testDir, 'file-refs.md');
|
||||
convertRuleToProfileRule(
|
||||
testCursorRule,
|
||||
testRooRule,
|
||||
rooProfile.rooProfile
|
||||
);
|
||||
convertRuleToProfileRule(testCursorRule, testRooRule, rooProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
|
||||
@@ -134,7 +122,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
|
||||
const assetRule = path.join(assetsRulesDir, 'dev_workflow.mdc');
|
||||
fs.writeFileSync(assetRule, 'dummy');
|
||||
// Should create .roo/rules and call post-processing
|
||||
convertAllRulesToProfileRules(testDir, rooProfile.rooProfile);
|
||||
convertAllRulesToProfileRules(testDir, rooProfile);
|
||||
// Check for post-processing artifacts, e.g., rules-* folders or extra files
|
||||
const rooDir = path.join(testDir, '.roo');
|
||||
const found = fs.readdirSync(rooDir).some((f) => f.startsWith('rules-'));
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import { convertRuleToProfileRule } from '../../src/utils/rule-transformer.js';
|
||||
import * as traeProfile from '../../scripts/profiles/trae.js';
|
||||
import { traeProfile } from '../../scripts/profiles/trae.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -41,11 +41,7 @@ Also has references to .mdc files.`;
|
||||
|
||||
// Convert it
|
||||
const testTraeRule = path.join(testDir, 'basic-terms.md');
|
||||
convertRuleToProfileRule(
|
||||
testCursorRule,
|
||||
testTraeRule,
|
||||
traeProfile.traeProfile
|
||||
);
|
||||
convertRuleToProfileRule(testCursorRule, testTraeRule, traeProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testTraeRule, 'utf8');
|
||||
@@ -76,11 +72,7 @@ alwaysApply: true
|
||||
|
||||
// Convert it
|
||||
const testTraeRule = path.join(testDir, 'tool-refs.md');
|
||||
convertRuleToProfileRule(
|
||||
testCursorRule,
|
||||
testTraeRule,
|
||||
traeProfile.traeProfile
|
||||
);
|
||||
convertRuleToProfileRule(testCursorRule, testTraeRule, traeProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testTraeRule, 'utf8');
|
||||
@@ -108,11 +100,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
|
||||
|
||||
// Convert it
|
||||
const testTraeRule = path.join(testDir, 'file-refs.md');
|
||||
convertRuleToProfileRule(
|
||||
testCursorRule,
|
||||
testTraeRule,
|
||||
traeProfile.traeProfile
|
||||
);
|
||||
convertRuleToProfileRule(testCursorRule, testTraeRule, traeProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testTraeRule, 'utf8');
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import { convertRuleToProfileRule } from '../../src/utils/rule-transformer.js';
|
||||
import * as windsurfProfile from '../../scripts/profiles/windsurf.js';
|
||||
import { windsurfProfile } from '../../scripts/profiles/windsurf.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -41,11 +41,7 @@ Also has references to .mdc files.`;
|
||||
|
||||
// Convert it
|
||||
const testWindsurfRule = path.join(testDir, 'basic-terms.md');
|
||||
convertRuleToProfileRule(
|
||||
testCursorRule,
|
||||
testWindsurfRule,
|
||||
windsurfProfile.windsurfProfile
|
||||
);
|
||||
convertRuleToProfileRule(testCursorRule, testWindsurfRule, windsurfProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testWindsurfRule, 'utf8');
|
||||
@@ -76,11 +72,7 @@ alwaysApply: true
|
||||
|
||||
// Convert it
|
||||
const testWindsurfRule = path.join(testDir, 'tool-refs.md');
|
||||
convertRuleToProfileRule(
|
||||
testCursorRule,
|
||||
testWindsurfRule,
|
||||
windsurfProfile.windsurfProfile
|
||||
);
|
||||
convertRuleToProfileRule(testCursorRule, testWindsurfRule, windsurfProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testWindsurfRule, 'utf8');
|
||||
@@ -108,11 +100,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
|
||||
|
||||
// Convert it
|
||||
const testWindsurfRule = path.join(testDir, 'file-refs.md');
|
||||
convertRuleToProfileRule(
|
||||
testCursorRule,
|
||||
testWindsurfRule,
|
||||
windsurfProfile.windsurfProfile
|
||||
);
|
||||
convertRuleToProfileRule(testCursorRule, testWindsurfRule, windsurfProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testWindsurfRule, 'utf8');
|
||||
|
||||
@@ -12,7 +12,15 @@ describe('Rule Transformer - General', () => {
|
||||
expect(RULE_PROFILES.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify expected profiles are present
|
||||
const expectedProfiles = ['cline', 'cursor', 'roo', 'trae', 'windsurf'];
|
||||
const expectedProfiles = [
|
||||
'claude',
|
||||
'cline',
|
||||
'codex',
|
||||
'cursor',
|
||||
'roo',
|
||||
'trae',
|
||||
'windsurf'
|
||||
];
|
||||
expectedProfiles.forEach((profile) => {
|
||||
expect(RULE_PROFILES).toContain(profile);
|
||||
});
|
||||
@@ -31,7 +39,7 @@ describe('Rule Transformer - General', () => {
|
||||
expect(isValidProfile(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return correct rules profile with getRulesProfile', () => {
|
||||
it('should return correct rule profile with getRulesProfile', () => {
|
||||
// Test valid profiles
|
||||
RULE_PROFILES.forEach((profile) => {
|
||||
const profileConfig = getRulesProfile(profile);
|
||||
@@ -46,6 +54,9 @@ describe('Rule Transformer - General', () => {
|
||||
|
||||
describe('Profile Structure', () => {
|
||||
it('should have all required properties for each profile', () => {
|
||||
// Simple profiles that only copy files (no rule transformation)
|
||||
const simpleProfiles = ['claude', 'codex'];
|
||||
|
||||
RULE_PROFILES.forEach((profile) => {
|
||||
const profileConfig = getRulesProfile(profile);
|
||||
|
||||
@@ -56,7 +67,15 @@ describe('Rule Transformer - General', () => {
|
||||
expect(profileConfig).toHaveProperty('rulesDir');
|
||||
expect(profileConfig).toHaveProperty('profileDir');
|
||||
|
||||
// Check that conversionConfig has required structure
|
||||
// Simple profiles have minimal structure
|
||||
if (simpleProfiles.includes(profile)) {
|
||||
// For simple profiles, conversionConfig and fileMap can be empty
|
||||
expect(typeof profileConfig.conversionConfig).toBe('object');
|
||||
expect(typeof profileConfig.fileMap).toBe('object');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that conversionConfig has required structure for full profiles
|
||||
expect(profileConfig.conversionConfig).toHaveProperty('profileTerms');
|
||||
expect(profileConfig.conversionConfig).toHaveProperty('toolNames');
|
||||
expect(profileConfig.conversionConfig).toHaveProperty('toolContexts');
|
||||
@@ -89,6 +108,9 @@ describe('Rule Transformer - General', () => {
|
||||
'taskmaster.mdc'
|
||||
];
|
||||
|
||||
// Simple profiles that only copy files (no rule transformation)
|
||||
const simpleProfiles = ['claude', 'codex'];
|
||||
|
||||
RULE_PROFILES.forEach((profile) => {
|
||||
const profileConfig = getRulesProfile(profile);
|
||||
|
||||
@@ -97,7 +119,12 @@ describe('Rule Transformer - General', () => {
|
||||
expect(typeof profileConfig.fileMap).toBe('object');
|
||||
expect(profileConfig.fileMap).not.toBeNull();
|
||||
|
||||
// Check that fileMap is not empty
|
||||
// Simple profiles can have empty fileMap since they don't transform rules
|
||||
if (simpleProfiles.includes(profile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that fileMap is not empty for full profiles
|
||||
const fileMapKeys = Object.keys(profileConfig.fileMap);
|
||||
expect(fileMapKeys.length).toBeGreaterThan(0);
|
||||
|
||||
@@ -116,6 +143,9 @@ describe('Rule Transformer - General', () => {
|
||||
|
||||
describe('MCP Configuration Properties', () => {
|
||||
it('should have all required MCP properties for each profile', () => {
|
||||
// Simple profiles that only copy files (no MCP configuration)
|
||||
const simpleProfiles = ['claude', 'codex'];
|
||||
|
||||
RULE_PROFILES.forEach((profile) => {
|
||||
const profileConfig = getRulesProfile(profile);
|
||||
|
||||
@@ -124,7 +154,15 @@ describe('Rule Transformer - General', () => {
|
||||
expect(profileConfig).toHaveProperty('mcpConfigName');
|
||||
expect(profileConfig).toHaveProperty('mcpConfigPath');
|
||||
|
||||
// Check types
|
||||
// Simple profiles have no MCP configuration
|
||||
if (simpleProfiles.includes(profile)) {
|
||||
expect(profileConfig.mcpConfig).toBe(false);
|
||||
expect(profileConfig.mcpConfigName).toBe(null);
|
||||
expect(profileConfig.mcpConfigPath).toBe(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check types for full profiles
|
||||
expect(typeof profileConfig.mcpConfig).toBe('boolean');
|
||||
expect(typeof profileConfig.mcpConfigName).toBe('string');
|
||||
expect(typeof profileConfig.mcpConfigPath).toBe('string');
|
||||
@@ -138,6 +176,16 @@ describe('Rule Transformer - General', () => {
|
||||
|
||||
it('should have correct MCP configuration for each profile', () => {
|
||||
const expectedConfigs = {
|
||||
claude: {
|
||||
mcpConfig: false,
|
||||
mcpConfigName: null,
|
||||
expectedPath: null
|
||||
},
|
||||
codex: {
|
||||
mcpConfig: false,
|
||||
mcpConfigName: null,
|
||||
expectedPath: null
|
||||
},
|
||||
cursor: {
|
||||
mcpConfig: true,
|
||||
mcpConfigName: 'mcp.json',
|
||||
@@ -176,9 +224,18 @@ describe('Rule Transformer - General', () => {
|
||||
});
|
||||
|
||||
it('should have consistent profileDir and mcpConfigPath relationship', () => {
|
||||
// Simple profiles that only copy files (no MCP configuration)
|
||||
const simpleProfiles = ['claude', 'codex'];
|
||||
|
||||
RULE_PROFILES.forEach((profile) => {
|
||||
const profileConfig = getRulesProfile(profile);
|
||||
|
||||
// Simple profiles have null mcpConfigPath
|
||||
if (simpleProfiles.includes(profile)) {
|
||||
expect(profileConfig.mcpConfigPath).toBe(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// The mcpConfigPath should start with the profileDir
|
||||
expect(profileConfig.mcpConfigPath).toMatch(
|
||||
new RegExp(
|
||||
@@ -201,8 +258,11 @@ describe('Rule Transformer - General', () => {
|
||||
return profileConfig.profileDir;
|
||||
});
|
||||
|
||||
// Note: Claude and Codex both use "." (root directory) so we expect some duplication
|
||||
const uniqueProfileDirs = [...new Set(profileDirs)];
|
||||
expect(uniqueProfileDirs).toHaveLength(profileDirs.length);
|
||||
// We should have fewer unique directories than total profiles due to simple profiles using root
|
||||
expect(uniqueProfileDirs.length).toBeLessThanOrEqual(profileDirs.length);
|
||||
expect(uniqueProfileDirs.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should have unique MCP config paths', () => {
|
||||
@@ -211,8 +271,13 @@ describe('Rule Transformer - General', () => {
|
||||
return profileConfig.mcpConfigPath;
|
||||
});
|
||||
|
||||
// Note: Claude and Codex both have null mcpConfigPath so we expect some duplication
|
||||
const uniqueMcpConfigPaths = [...new Set(mcpConfigPaths)];
|
||||
expect(uniqueMcpConfigPaths).toHaveLength(mcpConfigPaths.length);
|
||||
// We should have fewer unique paths than total profiles due to simple profiles having null
|
||||
expect(uniqueMcpConfigPaths.length).toBeLessThanOrEqual(
|
||||
mcpConfigPaths.length
|
||||
);
|
||||
expect(uniqueMcpConfigPaths.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
getInstalledProfiles,
|
||||
wouldRemovalLeaveNoProfiles
|
||||
} from '../../src/utils/profile-detection.js';
|
||||
} from '../../src/utils/profiles.js';
|
||||
import { rulesDirect } from '../../mcp-server/src/core/direct-functions/rules.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
Reference in New Issue
Block a user