From 0543ba3057d8b4af721f4cd91ff4428ca178ec8f Mon Sep 17 00:00:00 2001 From: Joe Danziger Date: Sun, 11 May 2025 10:37:41 -0400 Subject: [PATCH] update MCP responses, centralize rules profiles & helpers --- mcp-server/src/core/direct-functions/rules.js | 61 +++++++++++-- scripts/modules/commands.js | 50 ++++++----- scripts/modules/rule-transformer.js | 88 ++++++++++++++----- scripts/profiles/roo.js | 13 +-- 4 files changed, 154 insertions(+), 58 deletions(-) diff --git a/mcp-server/src/core/direct-functions/rules.js b/mcp-server/src/core/direct-functions/rules.js index bf2cc224..d46698c7 100644 --- a/mcp-server/src/core/direct-functions/rules.js +++ b/mcp-server/src/core/direct-functions/rules.js @@ -3,11 +3,11 @@ * Direct function implementation for adding or removing brand rules */ -import { execSync } from 'child_process'; import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js'; +import { removeBrandRules, convertAllRulesToBrandRules, BRAND_NAMES, isValidBrand, getBrandProfile } from '../../../../scripts/modules/rule-transformer.js'; /** * Direct function wrapper for adding or removing brand rules. @@ -38,13 +38,58 @@ export async function rulesDirect(args, log, context = {}) { } }; } - const rulesList = rules.join(','); - const cmd = `npx task-master rules ${action} ${rulesList}`.trim(); - log.info(`[rulesDirect] Running: ${cmd} in ${projectRoot}`); - const output = execSync(cmd, { cwd: projectRoot, encoding: 'utf8' }); - log.info(`[rulesDirect] Output: ${output}`); - disableSilentMode(); - return { success: true, data: { output } }; + + const removalResults = []; + const addResults = []; + + if (action === 'remove') { + for (const brand of rules) { + if (!isValidBrand(brand)) { + removalResults.push({ brandName: brand, success: false, error: `Profile not found: static import missing for '${brand}'. Valid brands: ${BRAND_NAMES.join(', ')}` }); + continue; + } + const profile = getBrandProfile(brand); + const result = removeBrandRules(projectRoot, profile); + removalResults.push(result); + } + const successes = removalResults.filter(r => r.success).map(r => r.brandName); + const skipped = removalResults.filter(r => r.skipped).map(r => r.brandName); + const errors = removalResults.filter(r => r.error && !r.success && !r.skipped); + + let summary = ''; + if (successes.length > 0) { + summary += `Successfully removed rules: ${successes.join(', ')}. `; + } + if (skipped.length > 0) { + summary += `Skipped (default or protected): ${skipped.join(', ')}. `; + } + if (errors.length > 0) { + summary += errors.map(r => `Error removing ${r.brandName}: ${r.error}`).join(' '); + } + disableSilentMode(); + return { success: errors.length === 0, data: { summary, results: removalResults } }; + } else if (action === 'add') { + for (const brand of rules) { + if (!isValidBrand(brand)) { + addResults.push({ brandName: brand, success: false, error: `Profile not found: static import missing for '${brand}'. Valid brands: ${BRAND_NAMES.join(', ')}` }); + continue; + } + const profile = getBrandProfile(brand); + const result = convertAllRulesToBrandRules(projectRoot, profile); + addResults.push({ brandName: brand, ...result }); + } + disableSilentMode(); + return { success: true, data: { results: addResults } }; + } else { + disableSilentMode(); + return { + success: false, + error: { + code: 'INVALID_ACTION', + message: 'Unknown action. Use "add" or "remove".' + } + }; + } } catch (error) { disableSilentMode(); log.error(`[rulesDirect] Error: ${error.message}`); diff --git a/scripts/modules/commands.js b/scripts/modules/commands.js index 1710d4dd..0c0a3f74 100644 --- a/scripts/modules/commands.js +++ b/scripts/modules/commands.js @@ -73,6 +73,7 @@ import { getApiKeyStatusReport } from './task-manager/models.js'; import { findProjectRoot } from './utils.js'; +import { convertAllRulesToBrandRules, removeBrandRules, BRAND_NAMES, isValidBrand, getBrandProfile } from './rule-transformer.js'; /** * Runs the interactive setup process for model configuration. @@ -511,38 +512,24 @@ function registerCommands(programInstance) { const expandedBrands = brands .flatMap((b) => b.split(',').map((s) => s.trim())) .filter(Boolean); + + const removalResults = []; + for (const brand of expandedBrands) { - let profile; - try { - // Use pathToFileURL for correct ESM dynamic import - const { pathToFileURL } = await import('url'); - const profilePath = path.resolve( - process.cwd(), - 'scripts', - 'profiles', - `${brand}.js` - ); - const profileModule = await import(pathToFileURL(profilePath).href); - profile = profileModule.default || profileModule; - } catch (e) { - console.warn( - `Rules profile for brand "${brand}" not found. Skipping.` - ); - console.warn(`Import error: ${e && e.message ? e.message : e}`); + if (!isValidBrand(brand)) { + console.warn(`Rules profile for brand "${brand}" not found. Valid brands: ${BRAND_NAMES.join(', ')}. Skipping.`); continue; } + const profile = getBrandProfile(brand); if (action === 'add') { - const { convertAllRulesToBrandRules } = await import( - './rule-transformer.js' - ); convertAllRulesToBrandRules(projectDir, profile); if (typeof profile.onAddBrandRules === 'function') { profile.onAddBrandRules(projectDir); } } else if (action === 'remove') { - const { removeBrandRules } = await import('./rule-transformer.js'); - removeBrandRules(projectDir, profile); + const result = removeBrandRules(projectDir, profile); + removalResults.push(result); if (typeof profile.onRemoveBrandRules === 'function') { profile.onRemoveBrandRules(projectDir); } @@ -551,6 +538,25 @@ function registerCommands(programInstance) { process.exit(1); } } + + // Print summary for removals + if (action === 'remove') { + const successes = removalResults.filter(r => r.success).map(r => r.brandName); + const skipped = removalResults.filter(r => r.skipped).map(r => r.brandName); + const errors = removalResults.filter(r => r.error && !r.success && !r.skipped); + + if (successes.length > 0) { + console.log(chalk.green(`Successfully removed rules: ${successes.join(', ')}`)); + } + if (skipped.length > 0) { + console.log(chalk.yellow(`Skipped (default or protected): ${skipped.join(', ')}`)); + } + if (errors.length > 0) { + errors.forEach(r => { + console.log(chalk.red(`Error removing ${r.brandName}: ${r.error}`)); + }); + } + } }); // parse-prd command diff --git a/scripts/modules/rule-transformer.js b/scripts/modules/rule-transformer.js index 9daefc3c..27e7e7ec 100644 --- a/scripts/modules/rule-transformer.js +++ b/scripts/modules/rule-transformer.js @@ -12,6 +12,27 @@ import { log } from './utils.js'; // Import the shared MCP configuration helper import { setupMCPConfiguration } from './mcp-utils.js'; +// --- Centralized Brand Helpers --- +export const BRAND_NAMES = ['cursor', 'roo', 'windsurf']; + +import * as cursorProfile from '../profiles/cursor.js'; +import * as rooProfile from '../profiles/roo.js'; +import * as windsurfProfile from '../profiles/windsurf.js'; + +export const BRAND_PROFILES = { + cursor: cursorProfile, + roo: rooProfile, + windsurf: windsurfProfile +}; + +export function isValidBrand(brand) { + return BRAND_NAMES.includes(brand); +} + +export function getBrandProfile(brand) { + return BRAND_PROFILES[brand]; +} + /** * Replace basic Cursor terms with brand equivalents */ @@ -223,6 +244,7 @@ function convertAllRulesToBrandRules(projectDir, profile) { return { success, failed }; } + /** * Remove a brand's rules directory and, if empty, the parent brand folder (except .cursor) * @param {string} projectDir - The root directory of the project @@ -233,41 +255,63 @@ function removeBrandRules(projectDir, profile) { const { brandName, rulesDir } = profile; const brandRulesDir = path.join(projectDir, rulesDir); const brandDir = path.dirname(brandRulesDir); - // Also remove the mcp.json file if it exists in the brand directory const mcpPath = path.join(brandDir, 'mcp.json'); + + const result = { + brandName, + mcpConfigRemoved: false, + rulesDirRemoved: false, + brandFolderRemoved: false, + skipped: false, + error: null, + success: false // Overall success for this brand + }; + if (fs.existsSync(mcpPath)) { try { fs.unlinkSync(mcpPath); - log('info', `Removed MCP configuration: ${mcpPath}`); + result.mcpConfigRemoved = true; } catch (e) { - log( - 'warn', - `Failed to remove MCP configuration at ${mcpPath}: ${e.message}` - ); + const errorMessage = `Failed to remove MCP configuration at ${mcpPath}: ${e.message}`; + log('warn', errorMessage); + result.error = result.error ? `${result.error}; ${errorMessage}` : errorMessage; } } - // Do not allow removal of the default Cursor rules directory + if (brandName.toLowerCase() === 'cursor') { - log('warn', 'Cannot remove default Cursor rules directory. Skipping.'); - return false; + const skipMessage = 'Cannot remove default Cursor rules directory. Skipping.'; + log('warn', skipMessage); + result.skipped = true; + result.error = skipMessage; + return result; // Early exit for cursor brand } + if (fs.existsSync(brandRulesDir)) { - fs.rmSync(brandRulesDir, { recursive: true, force: true }); - log('info', `Removed rules directory: ${brandRulesDir}`); - // Check if parent brand folder is empty - if ( - fs.existsSync(brandDir) && - path.basename(brandDir) !== '.cursor' && - fs.readdirSync(brandDir).length === 0 - ) { - fs.rmdirSync(brandDir); - log('info', `Removed empty brand folder: ${brandDir}`); + try { + fs.rmSync(brandRulesDir, { recursive: true, force: true }); + result.rulesDirRemoved = true; + + if ( + fs.existsSync(brandDir) && + path.basename(brandDir) !== '.cursor' && + fs.readdirSync(brandDir).length === 0 + ) { + fs.rmdirSync(brandDir); + result.brandFolderRemoved = true; + } + result.success = true; // Mark overall success if rules dir was removed + } catch (e) { + const errorMessage = `Failed to remove rules directory ${brandRulesDir} or brand folder ${brandDir}: ${e.message}`; + log('error', errorMessage); // Log as error since this is a primary operation failing + result.error = result.error ? `${result.error}; ${errorMessage}` : errorMessage; } - return true; } else { - log('warn', `Rules directory not found: ${brandRulesDir}`); - return false; + const warnMessage = `Rules directory not found: ${brandRulesDir}`; + log('warn', warnMessage); + result.error = result.error ? `${result.error}; ${warnMessage}` : warnMessage; + // success remains false as the primary target (rulesDir) was not found } + return result; } export { diff --git a/scripts/profiles/roo.js b/scripts/profiles/roo.js index 38076ac9..82bc4436 100644 --- a/scripts/profiles/roo.js +++ b/scripts/profiles/roo.js @@ -2,6 +2,7 @@ import { fileURLToPath } from 'url'; import path from 'path'; import fs from 'fs'; +import { isSilentMode } from '../modules/utils.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -136,12 +137,12 @@ export function onAddBrandRules(targetDir) { if (fs.existsSync(roomodesSrc)) { try { fs.copyFileSync(roomodesSrc, roomodesDest); - console.log(`[Roo] Copied .roomodes to ${roomodesDest}`); + if (!isSilentMode()) console.log(`[Roo] Copied .roomodes to ${roomodesDest}`); } catch (err) { - console.warn(`[Roo] Failed to copy .roomodes: ${err.message}`); + if (!isSilentMode()) console.warn(`[Roo] Failed to copy .roomodes: ${err.message}`); } } else { - console.warn(`[Roo] .roomodes not found at ${roomodesSrc}`); + if (!isSilentMode()) console.warn(`[Roo] .roomodes not found at ${roomodesSrc}`); } // Copy each -rules file into the corresponding .roo/rules-/ folder @@ -154,12 +155,12 @@ export function onAddBrandRules(targetDir) { const destDir = path.dirname(dest); if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true }); fs.copyFileSync(src, dest); - console.log(`[Roo] Copied ${src} to ${dest}`); + if (!isSilentMode()) console.log(`[Roo] Copied ${src} to ${dest}`); } catch (err) { - console.warn(`[Roo] Failed to copy ${src} to ${dest}: ${err.message}`); + if (!isSilentMode()) console.warn(`[Roo] Failed to copy ${src} to ${dest}: ${err.message}`); } } else { - console.warn(`[Roo] Roo rule file not found for mode '${mode}': ${src}`); + if (!isSilentMode()) console.warn(`[Roo] Roo rule file not found for mode '${mode}': ${src}`); } } }