mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-29 22:02:04 +00:00
Ralph/feat/add.auto.rules.add (#1535)
This commit is contained in:
9
.changeset/gold-tigers-tap.md
Normal file
9
.changeset/gold-tigers-tap.md
Normal file
@@ -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
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -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": {
|
||||
|
||||
242
packages/tm-profiles/src/detection/detector.spec.ts
Normal file
242
packages/tm-profiles/src/detection/detector.spec.ts
Normal file
@@ -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');
|
||||
});
|
||||
});
|
||||
84
packages/tm-profiles/src/detection/detector.ts
Normal file
84
packages/tm-profiles/src/detection/detector.ts
Normal file
@@ -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;
|
||||
}
|
||||
20
packages/tm-profiles/src/detection/index.ts
Normal file
20
packages/tm-profiles/src/detection/index.ts
Normal file
@@ -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';
|
||||
108
packages/tm-profiles/src/detection/profiles-map.ts
Normal file
108
packages/tm-profiles/src/detection/profiles-map.ts
Normal file
@@ -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);
|
||||
}
|
||||
25
packages/tm-profiles/src/detection/types.ts
Normal file
25
packages/tm-profiles/src/detection/types.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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 <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).'
|
||||
|
||||
@@ -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<string[]>} 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<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
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user