diff --git a/.changeset/gold-tigers-tap.md b/.changeset/gold-tigers-tap.md new file mode 100644 index 00000000..e3f1d55b --- /dev/null +++ b/.changeset/gold-tigers-tap.md @@ -0,0 +1,9 @@ +--- +"task-master-ai": minor +--- + +Add auto-detection for IDE profiles in rules command + +- `tm rules add` now opens interactive setup with detected IDEs pre-selected +- `tm rules add -y` auto-detects and installs rules without prompting +- Detects 13 IDEs: Cursor, Claude Code, Windsurf, VS Code, Roo, Cline, Kiro, Zed, Kilo, Trae, Gemini, OpenCode, Codex diff --git a/package-lock.json b/package-lock.json index 12996151..7e146568 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "task-master-ai", - "version": "0.38.0", + "version": "0.39.0-rc.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "task-master-ai", - "version": "0.38.0", + "version": "0.39.0-rc.0", "license": "MIT WITH Commons-Clause", "workspaces": [ "apps/*", @@ -1850,7 +1850,7 @@ "react-dom": "^19.0.0", "tailwind-merge": "^3.3.1", "tailwindcss": "4.1.11", - "task-master-ai": "*", + "task-master-ai": "0.39.0-rc.0", "typescript": "^5.9.2" }, "engines": { diff --git a/packages/tm-profiles/src/detection/detector.spec.ts b/packages/tm-profiles/src/detection/detector.spec.ts new file mode 100644 index 00000000..faaa21a6 --- /dev/null +++ b/packages/tm-profiles/src/detection/detector.spec.ts @@ -0,0 +1,242 @@ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { + detectInstalledIDEs, + detectProfile, + getPreSelectedProfiles +} from './detector.js'; +import { IDE_MARKERS } from './profiles-map.js'; + +// Use real fs operations with temp directories for accurate testing +describe('IDE Detection', () => { + let tempDir: string; + + beforeEach(() => { + // Create a real temp directory for each test + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-detection-test-')); + }); + + afterEach(() => { + // Clean up temp directory + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + describe('detectInstalledIDEs', () => { + it('should detect a single IDE directory', () => { + // Arrange - create .cursor directory + fs.mkdirSync(path.join(tempDir, '.cursor')); + + // Act + const results = detectInstalledIDEs({ projectRoot: tempDir }); + + // Assert + expect(results).toHaveLength(1); + expect(results[0]).toEqual({ + profileName: 'cursor', + markerPath: '.cursor', + displayName: 'Cursor', + exists: true + }); + }); + + it('should detect multiple IDE directories', () => { + // Arrange - create multiple IDE directories + fs.mkdirSync(path.join(tempDir, '.cursor')); + fs.mkdirSync(path.join(tempDir, '.claude')); + fs.mkdirSync(path.join(tempDir, '.windsurf')); + + // Act + const results = detectInstalledIDEs({ projectRoot: tempDir }); + + // Assert + expect(results).toHaveLength(3); + const profileNames = results.map((r) => r.profileName); + expect(profileNames).toContain('cursor'); + expect(profileNames).toContain('claude'); + expect(profileNames).toContain('windsurf'); + }); + + it('should return empty array when no IDEs are detected', () => { + // Act - tempDir has no IDE directories + const results = detectInstalledIDEs({ projectRoot: tempDir }); + + // Assert + expect(results).toHaveLength(0); + }); + + it('should detect directory markers for all directory-based profiles', () => { + // Arrange - create all directory-based markers + const directoryMarkers = [ + '.cursor', + '.claude', + '.windsurf', + '.roo', + '.cline', + '.vscode', + '.kiro', + '.zed', + '.kilo', + '.trae', + '.gemini', + '.opencode', + '.codex' + ]; + + for (const marker of directoryMarkers) { + fs.mkdirSync(path.join(tempDir, marker)); + } + + // Act + const results = detectInstalledIDEs({ projectRoot: tempDir }); + + // Assert + expect(results.length).toBe(directoryMarkers.length); + }); + + it('should detect file-based markers (GEMINI.md)', () => { + // Arrange - create GEMINI.md file (but no .gemini directory) + fs.writeFileSync(path.join(tempDir, 'GEMINI.md'), '# Gemini config'); + + // Act + const results = detectInstalledIDEs({ projectRoot: tempDir }); + + // Assert + expect(results).toHaveLength(1); + expect(results[0]).toEqual({ + profileName: 'gemini', + markerPath: 'GEMINI.md', + displayName: 'Gemini', + exists: true + }); + }); + + it('should prefer directory marker over file marker for Gemini', () => { + // Arrange - create both .gemini directory and GEMINI.md file + fs.mkdirSync(path.join(tempDir, '.gemini')); + fs.writeFileSync(path.join(tempDir, 'GEMINI.md'), '# Gemini config'); + + // Act + const results = detectInstalledIDEs({ projectRoot: tempDir }); + + // Assert - should only detect once, with directory marker + const geminiResults = results.filter((r) => r.profileName === 'gemini'); + expect(geminiResults).toHaveLength(1); + expect(geminiResults[0].markerPath).toBe('.gemini'); + }); + + it('should not detect files as directories', () => { + // Arrange - create a file named .cursor instead of directory + fs.writeFileSync(path.join(tempDir, '.cursor'), 'not a directory'); + + // Act + const results = detectInstalledIDEs({ projectRoot: tempDir }); + + // Assert - should not detect because it's a file, not a directory + expect(results.filter((r) => r.profileName === 'cursor')).toHaveLength(0); + }); + + it('should not detect directories as files', () => { + // Arrange - create a directory named GEMINI.md instead of file + fs.mkdirSync(path.join(tempDir, 'GEMINI.md')); + + // Act + const results = detectInstalledIDEs({ projectRoot: tempDir }); + + // Assert - should not detect GEMINI.md as it's a directory, not a file + const geminiResults = results.filter((r) => r.profileName === 'gemini'); + expect(geminiResults).toHaveLength(0); + }); + }); + + describe('getPreSelectedProfiles', () => { + it('should return profile names only', () => { + // Arrange + fs.mkdirSync(path.join(tempDir, '.cursor')); + fs.mkdirSync(path.join(tempDir, '.claude')); + + // Act + const profiles = getPreSelectedProfiles({ projectRoot: tempDir }); + + // Assert + expect(profiles).toEqual(expect.arrayContaining(['cursor', 'claude'])); + expect(profiles).toHaveLength(2); + }); + + it('should return empty array when no IDEs detected', () => { + // Act + const profiles = getPreSelectedProfiles({ projectRoot: tempDir }); + + // Assert + expect(profiles).toEqual([]); + }); + }); + + describe('detectProfile', () => { + it('should return detection result for existing profile', () => { + // Arrange + fs.mkdirSync(path.join(tempDir, '.cursor')); + + // Act + const result = detectProfile('cursor', tempDir); + + // Assert + expect(result).toEqual({ + profileName: 'cursor', + markerPath: '.cursor', + displayName: 'Cursor', + exists: true + }); + }); + + it('should return null for non-existing profile', () => { + // Act - no .cursor directory exists + const result = detectProfile('cursor', tempDir); + + // Assert + expect(result).toBeNull(); + }); + + it('should return null for non-detectable profile (amp)', () => { + // Act + const result = detectProfile('amp', tempDir); + + // Assert + expect(result).toBeNull(); + }); + }); +}); + +describe('IDE_MARKERS', () => { + it('should have 13 detectable profiles', () => { + // amp is not detectable + expect(IDE_MARKERS).toHaveLength(13); + }); + + it('should have valid marker definitions for all profiles', () => { + for (const marker of IDE_MARKERS) { + expect(marker.profileName).toBeTruthy(); + expect(marker.displayName).toBeTruthy(); + expect(marker.markers).toBeInstanceOf(Array); + expect(marker.markers.length).toBeGreaterThan(0); + + for (const m of marker.markers) { + expect(m.path).toBeTruthy(); + expect(['directory', 'file']).toContain(m.type); + } + } + }); + + it('should include cursor, claude, and windsurf profiles', () => { + const profileNames = IDE_MARKERS.map((m) => m.profileName); + expect(profileNames).toContain('cursor'); + expect(profileNames).toContain('claude'); + expect(profileNames).toContain('windsurf'); + }); + + it('should not include amp (no known marker)', () => { + const profileNames = IDE_MARKERS.map((m) => m.profileName); + expect(profileNames).not.toContain('amp'); + }); +}); diff --git a/packages/tm-profiles/src/detection/detector.ts b/packages/tm-profiles/src/detection/detector.ts new file mode 100644 index 00000000..87df2fb9 --- /dev/null +++ b/packages/tm-profiles/src/detection/detector.ts @@ -0,0 +1,84 @@ +/** + * IDE detection logic for auto-detecting installed IDEs in project directories. + */ +import fs from 'fs'; +import path from 'path'; +import { IDE_MARKERS } from './profiles-map.js'; +import type { DetectionResult, DetectionOptions } from './types.js'; + +/** + * Detect installed IDEs by checking for marker directories/files. + * + * @param options - Detection options including project root + * @returns Array of detected IDE profiles + * + * @example + * ```typescript + * const detected = detectInstalledIDEs({ projectRoot: '/path/to/project' }); + * // Returns: [{ profileName: 'cursor', markerPath: '.cursor', displayName: 'Cursor', exists: true }] + * ``` + */ +export function detectInstalledIDEs( + options: DetectionOptions +): DetectionResult[] { + const { projectRoot } = options; + const results: DetectionResult[] = []; + + for (const ideMarker of IDE_MARKERS) { + // Check each marker (directory or file) - first match wins + for (const marker of ideMarker.markers) { + const fullPath = path.join(projectRoot, marker.path); + + try { + const stat = fs.statSync(fullPath); + const exists = + marker.type === 'directory' ? stat.isDirectory() : stat.isFile(); + + if (exists) { + results.push({ + profileName: ideMarker.profileName, + markerPath: marker.path, + displayName: ideMarker.displayName, + exists: true + }); + break; // Found one marker, no need to check others for this IDE + } + } catch { + // File/directory doesn't exist or can't be accessed - continue to next marker + } + } + } + + return results; +} + +/** + * Get profile names that should be pre-selected based on detected IDEs. + * + * @param options - Detection options including project root + * @returns Array of profile names (e.g., ['cursor', 'claude']) + * + * @example + * ```typescript + * const profiles = getPreSelectedProfiles({ projectRoot: '/path/to/project' }); + * // Returns: ['cursor', 'claude'] + * ``` + */ +export function getPreSelectedProfiles(options: DetectionOptions): string[] { + return detectInstalledIDEs(options).map((r) => r.profileName); +} + +/** + * Detect a specific profile's IDE marker. + * + * @param profileName - Profile to check (e.g., 'cursor') + * @param projectRoot - Project root directory + * @returns Detection result if found, null otherwise + */ +export function detectProfile( + profileName: string, + projectRoot: string +): DetectionResult | null { + const detected = detectInstalledIDEs({ projectRoot }); + return detected.find((r) => r.profileName === profileName) ?? null; +} diff --git a/packages/tm-profiles/src/detection/index.ts b/packages/tm-profiles/src/detection/index.ts new file mode 100644 index 00000000..c1edffa6 --- /dev/null +++ b/packages/tm-profiles/src/detection/index.ts @@ -0,0 +1,20 @@ +/** + * IDE Detection Module + * + * Provides functions to auto-detect installed IDEs based on project markers. + * Used for pre-selecting profiles in `tm rules add` and + * non-interactive mode with `tm rules add -y`. + */ + +export type { DetectionResult, DetectionOptions } from './types.js'; +export type { IDEMarker } from './profiles-map.js'; +export { + IDE_MARKERS, + getIDEMarker, + isDetectableProfile +} from './profiles-map.js'; +export { + detectInstalledIDEs, + getPreSelectedProfiles, + detectProfile +} from './detector.js'; diff --git a/packages/tm-profiles/src/detection/profiles-map.ts b/packages/tm-profiles/src/detection/profiles-map.ts new file mode 100644 index 00000000..cb3444ba --- /dev/null +++ b/packages/tm-profiles/src/detection/profiles-map.ts @@ -0,0 +1,108 @@ +/** + * IDE marker definitions for auto-detection. + * Maps profile names to their filesystem markers. + */ + +/** + * Represents an IDE marker configuration + */ +export interface IDEMarker { + /** Profile name matching RULE_PROFILES */ + profileName: string; + /** Markers to check (first match wins) */ + markers: Array<{ path: string; type: 'directory' | 'file' }>; + /** Human-readable display name */ + displayName: string; +} + +/** + * IDE marker definitions - directory or file-based detection. + * Order matches typical usage frequency for slightly faster detection. + */ +export const IDE_MARKERS: IDEMarker[] = [ + // Most common IDE profiles + { + profileName: 'cursor', + markers: [{ path: '.cursor', type: 'directory' }], + displayName: 'Cursor' + }, + { + profileName: 'claude', + markers: [{ path: '.claude', type: 'directory' }], + displayName: 'Claude Code' + }, + { + profileName: 'windsurf', + markers: [{ path: '.windsurf', type: 'directory' }], + displayName: 'Windsurf' + }, + { + profileName: 'vscode', + markers: [{ path: '.vscode', type: 'directory' }], + displayName: 'VS Code' + }, + { + profileName: 'roo', + markers: [{ path: '.roo', type: 'directory' }], + displayName: 'Roo Code' + }, + { + profileName: 'cline', + markers: [{ path: '.cline', type: 'directory' }], + displayName: 'Cline' + }, + { + profileName: 'kiro', + markers: [{ path: '.kiro', type: 'directory' }], + displayName: 'Kiro' + }, + { + profileName: 'zed', + markers: [{ path: '.zed', type: 'directory' }], + displayName: 'Zed' + }, + { + profileName: 'kilo', + markers: [{ path: '.kilo', type: 'directory' }], + displayName: 'Kilo Code' + }, + { + profileName: 'trae', + markers: [{ path: '.trae', type: 'directory' }], + displayName: 'Trae' + }, + // Integration guides with detectable markers + { + profileName: 'gemini', + markers: [ + { path: '.gemini', type: 'directory' }, + { path: 'GEMINI.md', type: 'file' } + ], + displayName: 'Gemini' + }, + { + profileName: 'opencode', + markers: [{ path: '.opencode', type: 'directory' }], + displayName: 'OpenCode' + }, + { + profileName: 'codex', + markers: [{ path: '.codex', type: 'directory' }], + displayName: 'Codex' + } + // Note: 'amp' has no known project-local marker +]; + +/** + * Get IDE marker config for a specific profile + */ +export function getIDEMarker(profileName: string): IDEMarker | undefined { + return IDE_MARKERS.find((m) => m.profileName === profileName); +} + +/** + * Check if a profile has detectable markers + */ +export function isDetectableProfile(profileName: string): boolean { + return IDE_MARKERS.some((m) => m.profileName === profileName); +} diff --git a/packages/tm-profiles/src/detection/types.ts b/packages/tm-profiles/src/detection/types.ts new file mode 100644 index 00000000..524ff0e8 --- /dev/null +++ b/packages/tm-profiles/src/detection/types.ts @@ -0,0 +1,25 @@ +/** + * Detection types for auto-detecting IDE markers in project directories. + */ + +/** + * Result of detecting an IDE marker in the project + */ +export interface DetectionResult { + /** Profile name (e.g., 'cursor', 'claude') */ + profileName: string; + /** The marker path that was found (e.g., '.cursor') */ + markerPath: string; + /** Human-readable display name (e.g., 'Cursor', 'Claude Code') */ + displayName: string; + /** Whether the marker exists */ + exists: boolean; +} + +/** + * Options for IDE detection + */ +export interface DetectionOptions { + /** Project root directory to search in */ + projectRoot: string; +} diff --git a/packages/tm-profiles/src/index.ts b/packages/tm-profiles/src/index.ts index 90b5365e..e6429b11 100644 --- a/packages/tm-profiles/src/index.ts +++ b/packages/tm-profiles/src/index.ts @@ -8,3 +8,6 @@ export * from './slash-commands/index.js'; // Re-export shell utilities export * from './shell-utils.js'; + +// Re-export IDE detection utilities +export * from './detection/index.js'; diff --git a/scripts/modules/commands.js b/scripts/modules/commands.js index 2b5218b4..b8ac63c2 100644 --- a/scripts/modules/commands.js +++ b/scripts/modules/commands.js @@ -145,8 +145,10 @@ import { categorizeRemovalResults, generateProfileRemovalSummary, generateProfileSummary, + processRuleProfiles, runInteractiveProfilesSetup } from '../../src/utils/profiles.js'; +import { detectInstalledIDEs } from '@tm/profiles'; import { convertAllRulesToProfileRules, getRulesProfile, @@ -4393,14 +4395,19 @@ Examples: '-m, --mode ', 'Operating mode for filtering rules/commands (solo or team). Auto-detected from config if not specified.' ) + .option( + '-y, --yes', + 'Non-interactive mode: auto-detect IDEs and install rules without prompting' + ) .addHelpText( 'after', ` Examples: - $ task-master rules ${RULES_ACTIONS.ADD} windsurf roo # Add Windsurf and Roo rule sets + $ task-master rules ${RULES_ACTIONS.ADD} # Interactive setup with auto-detected IDEs pre-selected + $ task-master rules ${RULES_ACTIONS.ADD} -y # Auto-detect and install without prompting + $ task-master rules ${RULES_ACTIONS.ADD} windsurf roo # Add specific profiles $ task-master rules ${RULES_ACTIONS.ADD} cursor --mode=team # Add Cursor rules for team mode only - $ task-master rules ${RULES_ACTIONS.REMOVE} windsurf # Remove Windsurf rule set - $ task-master rules --${RULES_SETUP_ACTION} # Interactive setup to select rule profiles` + $ task-master rules ${RULES_ACTIONS.REMOVE} windsurf # Remove Windsurf rule set` ) .action(async (action, profiles, options) => { const taskMaster = initTaskMaster({}); @@ -4425,7 +4432,8 @@ Examples: */ if (options[RULES_SETUP_ACTION]) { // Run interactive rules setup ONLY (no project init) - const selectedRuleProfiles = await runInteractiveProfilesSetup(); + const selectedRuleProfiles = + await runInteractiveProfilesSetup(projectRoot); if (!selectedRuleProfiles || selectedRuleProfiles.length === 0) { console.log(chalk.yellow('No profiles selected. Exiting.')); @@ -4438,30 +4446,11 @@ Examples: ) ); - for (let i = 0; i < selectedRuleProfiles.length; i++) { - const profile = selectedRuleProfiles[i]; - console.log( - chalk.blue( - `Processing profile ${i + 1}/${selectedRuleProfiles.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 mode = await getOperatingMode(options.mode); - const addResult = convertAllRulesToProfileRules( - projectRoot, - profileConfig, - { mode } - ); - - console.log(chalk.green(generateProfileSummary(profile, addResult))); - } + await processRuleProfiles( + selectedRuleProfiles, + projectRoot, + options.mode + ); console.log( chalk.green( @@ -4486,6 +4475,98 @@ Examples: process.exit(1); } + /** + * 'task-master rules add' (no profiles): + * + * - Without -y: Opens interactive setup with auto-detected IDEs pre-selected + * - With -y: Non-interactive mode, installs rules for all detected IDEs + * + * Example usage: + * $ task-master rules add # Interactive setup with pre-selection + * $ task-master rules add -y # Auto-detect and install without prompting + */ + if ( + action === RULES_ACTIONS.ADD && + (!profiles || profiles.length === 0) + ) { + let selectedRuleProfiles; + + if (options.yes) { + // Non-interactive mode: auto-detect and install + console.log(chalk.blue('\nšŸ” Auto-detecting installed IDEs...\n')); + + const detected = detectInstalledIDEs({ projectRoot }); + + if (detected.length === 0) { + console.log( + chalk.yellow( + 'No IDE markers detected in this project.\n' + + chalk.gray( + 'Searched for: .cursor, .claude, .windsurf, .roo, .vscode, etc.\n' + ) + ) + ); + console.log( + chalk.cyan( + 'To manually select profiles, run without -y flag:\n' + + ' task-master rules add\n\n' + + 'Or specify profiles directly:\n' + + ' task-master rules add cursor windsurf\n' + ) + ); + return; + } + + // Show what was detected + console.log(chalk.green('āœ“ Detected the following IDEs:\n')); + for (const ide of detected) { + console.log( + ` • ${ide.displayName} ${chalk.gray(`(${ide.markerPath})`)}` + ); + } + console.log(''); + + selectedRuleProfiles = detected.map((d) => d.profileName); + } else { + // Interactive mode: open setup with pre-selection + selectedRuleProfiles = await runInteractiveProfilesSetup(projectRoot); + } + + if (!selectedRuleProfiles || selectedRuleProfiles.length === 0) { + console.log(chalk.yellow('No profiles selected. Exiting.')); + return; + } + + console.log( + chalk.blue( + `Installing ${selectedRuleProfiles.length} selected profile(s)...` + ) + ); + + const addResults = await processRuleProfiles( + selectedRuleProfiles, + projectRoot, + options.mode + ); + + // Final summary + const { allSuccessfulProfiles, totalSuccess, totalFailed } = + categorizeProfileResults(addResults); + console.log( + chalk.green( + `\nāœ“ Successfully installed ${allSuccessfulProfiles.length} profile(s)` + ) + ); + if (totalSuccess > 0) { + console.log( + chalk.gray( + ` ${totalSuccess} files processed, ${totalFailed} failed` + ) + ); + } + return; + } + if (!profiles || profiles.length === 0) { console.error( 'Please specify at least one rule profile (e.g., windsurf, roo).' diff --git a/src/utils/profiles.js b/src/utils/profiles.js index 9cdc6800..28714f16 100644 --- a/src/utils/profiles.js +++ b/src/utils/profiles.js @@ -7,9 +7,14 @@ import path from 'path'; import boxen from 'boxen'; import chalk from 'chalk'; import inquirer from 'inquirer'; -import { log } from '../../scripts/modules/utils.js'; import { RULE_PROFILES } from '../constants/profiles.js'; -import { getRulesProfile } from './rule-transformer.js'; +import { + convertAllRulesToProfileRules, + getRulesProfile, + isValidProfile +} from './rule-transformer.js'; +import { getPreSelectedProfiles } from '@tm/profiles'; +import { getOperatingMode } from '../../scripts/modules/config-manager.js'; // ============================================================================= // PROFILE DETECTION @@ -95,13 +100,28 @@ export function wouldRemovalLeaveNoProfiles(projectRoot, profilesToRemove) { * 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. + * 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} Array of selected profile names (e.g., ['cursor', 'windsurf']) */ -export async function runInteractiveProfilesSetup() { +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); @@ -165,10 +185,14 @@ export async function runInteractiveProfilesSetup() { ); // 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: displayName, - value: profileName + name: preSelected.includes(profileName) + ? `${displayName} ${chalk.dim('(detected)')}` + : displayName, + value: profileName, + checked: preSelected.includes(profileName) }) ); @@ -185,6 +209,57 @@ export async function runInteractiveProfilesSetup() { 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>} 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 // =============================================================================