Skip to main content
Glama
profiles.js•12.2 kB
/** * Profiles Utility * Consolidated utilities for profile detection, setup, and summary generation */ import fs from 'fs'; import path from 'path'; import boxen from 'boxen'; import chalk from 'chalk'; import inquirer from 'inquirer'; import { RULE_PROFILES } from '../constants/profiles.js'; import { convertAllRulesToProfileRules, getRulesProfile, isValidProfile } from './rule-transformer.js'; import { getPreSelectedProfiles } from '@tm/profiles'; import { getOperatingMode } from '../../scripts/modules/config-manager.js'; // ============================================================================= // PROFILE DETECTION // ============================================================================= /** * Get the display name for a profile * @param {string} profileName - The profile name * @returns {string} - The display name */ export function getProfileDisplayName(profileName) { try { const profile = getRulesProfile(profileName); return profile.displayName || profileName; } catch (error) { return profileName; } } /** * Get installed profiles in the project directory * @param {string} projectRoot - Project directory path * @returns {string[]} - Array of installed profile names */ export function getInstalledProfiles(projectRoot) { const installedProfiles = []; for (const profileName of RULE_PROFILES) { try { const profile = getRulesProfile(profileName); const profileDir = path.join(projectRoot, profile.profileDir); // Check if profile directory exists (skip root directory check) if (profile.profileDir === '.' || fs.existsSync(profileDir)) { // Check if any files from the profile's fileMap exist const rulesDir = path.join(projectRoot, profile.rulesDir); if (fs.existsSync(rulesDir)) { const ruleFiles = Object.values(profile.fileMap); const hasRuleFiles = ruleFiles.some((ruleFile) => fs.existsSync(path.join(rulesDir, ruleFile)) ); if (hasRuleFiles) { installedProfiles.push(profileName); } } } } catch (error) { // Skip profiles that can't be loaded } } return installedProfiles; } /** * Check if removing specified profiles would leave no profiles installed * @param {string} projectRoot - Project root directory * @param {string[]} profilesToRemove - Array of profile names to remove * @returns {boolean} - True if removal would leave no profiles */ export function wouldRemovalLeaveNoProfiles(projectRoot, profilesToRemove) { const installedProfiles = getInstalledProfiles(projectRoot); // If no profiles are currently installed, removal cannot leave no profiles if (installedProfiles.length === 0) { return false; } const remainingProfiles = installedProfiles.filter( (profile) => !profilesToRemove.includes(profile) ); return remainingProfiles.length === 0; } // ============================================================================= // PROFILE SETUP // ============================================================================= // Note: Profile choices are now generated dynamically within runInteractiveProfilesSetup() // to ensure proper alphabetical sorting and pagination configuration /** * 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. * Detected IDE profiles (based on directory markers like .cursor, .claude, etc.) 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. * * @param {string} [projectRoot=process.cwd()] - Project root directory for IDE detection * @returns {Promise<string[]>} Array of selected profile names (e.g., ['cursor', 'windsurf']) */ export async function runInteractiveProfilesSetup(projectRoot = process.cwd()) { // Auto-detect installed IDEs for pre-selection const preSelected = getPreSelectedProfiles({ projectRoot }); if (preSelected.length > 0) { const detectedNames = preSelected .map((p) => getProfileDisplayName(p)) .join(', '); console.log( chalk.cyan(`\n🔍 Auto-detected IDEs: ${detectedNames}`) + chalk.gray(' (pre-selected below)\n') ); } // Generate the profile list dynamically with proper display names, alphabetized const profileDescriptions = RULE_PROFILES.map((profileName) => { const displayName = getProfileDisplayName(profileName); const profile = getRulesProfile(profileName); // Determine description based on profile capabilities let description; const hasRules = Object.keys(profile.fileMap).length > 0; const hasMcpConfig = profile.mcpConfig === true; if (!profile.includeDefaultRules) { // Integration guide profiles (claude, codex, gemini, opencode, zed, amp) - don't include standard coding rules if (profileName === 'claude') { description = 'Integration guide with Task Master slash commands'; } else if (profileName === 'codex') { description = 'Comprehensive Task Master integration guide'; } else if (hasMcpConfig) { description = 'Integration guide and MCP config'; } else { description = 'Integration guide'; } } else if (hasRules && hasMcpConfig) { // Full rule profiles with MCP config if (profileName === 'roo') { description = 'Rule profile, MCP config, and agent modes'; } else { description = 'Rule profile and MCP config'; } } else if (hasRules) { // Rule profiles without MCP config description = 'Rule profile'; } return { profileName, displayName, description }; }).sort((a, b) => a.displayName.localeCompare(b.displayName)); const profileListText = profileDescriptions .map( ({ displayName, description }) => `${chalk.white('• ')}${chalk.yellow(displayName)}${chalk.white(` - ${description}`)}` ) .join('\n'); console.log( boxen( `${chalk.white.bold('Rule Profiles Setup')}\n\n${chalk.white( 'Rule profiles help enforce best practices and conventions for Task Master.\n' + 'Each profile provides coding guidelines tailored for specific AI coding environments.\n\n' )}${chalk.cyan('Available Profiles:')}\n${profileListText}`, { padding: 1, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 1 } } ) ); // Generate choices in the same order as the display text above // Pre-select profiles that were auto-detected const sortedChoices = profileDescriptions.map( ({ profileName, displayName }) => ({ name: preSelected.includes(profileName) ? `${displayName} ${chalk.dim('(detected)')}` : displayName, value: profileName, checked: preSelected.includes(profileName) }) ); const ruleProfilesQuestion = { type: 'checkbox', name: 'ruleProfiles', message: 'Which rule profiles would you like to add to your project?', choices: sortedChoices, pageSize: sortedChoices.length, // Show all options without pagination loop: false, // Disable loop scrolling validate: (input) => input.length > 0 || 'You must select at least one.' }; const { ruleProfiles } = await inquirer.prompt([ruleProfilesQuestion]); return ruleProfiles; } // ============================================================================= // PROFILE PROCESSING // ============================================================================= /** * Processes rule profiles by validating, resolving mode, and installing rules. * @param {string[]} profiles - Array of profile names to process * @param {string} projectRoot - Project root directory path * @param {string|undefined} modeOption - Operating mode option from CLI * @returns {Promise<Array<{profileName: string, success: number, failed: number}>>} Results for each profile */ export async function processRuleProfiles(profiles, projectRoot, modeOption) { const results = []; const mode = await getOperatingMode(modeOption); for (let i = 0; i < profiles.length; i++) { const profile = profiles[i]; console.log( chalk.blue( `Processing profile ${i + 1}/${profiles.length}: ${profile}...` ) ); if (!isValidProfile(profile)) { console.warn( `Rule profile for "${profile}" not found. Valid profiles: ${RULE_PROFILES.join(', ')}. Skipping.` ); continue; } const profileConfig = getRulesProfile(profile); const addResult = convertAllRulesToProfileRules( projectRoot, profileConfig, { mode } ); results.push({ profileName: profile, success: addResult.success, failed: addResult.failed }); console.log(chalk.green(generateProfileSummary(profile, addResult))); } return results; } // ============================================================================= // 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); if (!profileConfig.includeDefaultRules) { // Integration guide profiles (claude, codex, gemini, amp) return `Summary for ${profileName}: Integration guide installed.`; } else { // Rule profiles with coding guidelines return `Summary for ${profileName}: ${addResult.success} files processed, ${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) { 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}`; } const profileConfig = getRulesProfile(profileName); if (!profileConfig.includeDefaultRules) { // Integration guide profiles (claude, codex, gemini, amp) const baseMessage = `Summary for ${profileName}: Integration guide removed`; if (removeResult.notice) { return `${baseMessage} (${removeResult.notice})`; } return baseMessage; } else { // Rule profiles with coding guidelines const baseMessage = `Summary for ${profileName}: Rule profile removed`; if (removeResult.notice) { return `${baseMessage} (${removeResult.notice})`; } return baseMessage; } } /** * 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 = []; let totalSuccess = 0; let totalFailed = 0; addResults.forEach((r) => { totalSuccess += r.success; totalFailed += r.failed; // All profiles are considered successful if they completed without major errors if (r.success > 0 || r.failed === 0) { successfulProfiles.push(r.profileName); } }); return { successfulProfiles, allSuccessfulProfiles: successfulProfiles, 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 }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/eyaltoledano/claude-task-master'

If you have feedback or need assistance with the MCP directory API, please join our Discord server