From f97e1732f15a2f693cda25b3b98cae0bf16c4ed0 Mon Sep 17 00:00:00 2001 From: Joe Danziger Date: Mon, 9 Jun 2025 02:19:54 -0400 Subject: [PATCH] use --setup for rules interactive setup --- scripts/modules/commands.js | 46 +++++++++++------ tests/unit/commands.test.js | 99 +++++++++++++++++++++++++++++++++++-- 2 files changed, 127 insertions(+), 18 deletions(-) diff --git a/scripts/modules/commands.js b/scripts/modules/commands.js index b566a091..91fab6b6 100644 --- a/scripts/modules/commands.js +++ b/scripts/modules/commands.js @@ -2772,40 +2772,43 @@ Examples: // Add/remove profile rules command programInstance - .command('rules [profiles...]') + .command('rules [action] [profiles...]') .description( - `Add or remove rules for one or more profiles. Valid actions: ${Object.values(RULES_ACTIONS).join(', ')}, ${RULES_SETUP_ACTION} (e.g., task-master rules add windsurf roo)` + `Add or remove rules for one or more profiles. Valid actions: ${Object.values(RULES_ACTIONS).join(', ')} (e.g., task-master rules ${RULES_ACTIONS.ADD} windsurf roo)` ) .option( '-f, --force', 'Skip confirmation prompt when removing rules (dangerous)' ) + .option( + `--${RULES_SETUP_ACTION}`, + 'Run interactive setup to select rule profiles to add' + ) + .addHelpText( + 'after', + ` + Examples: + $ task-master rules ${RULES_ACTIONS.ADD} windsurf roo # Add Windsurf and Roo rule sets + $ task-master rules ${RULES_ACTIONS.REMOVE} windsurf # Remove Windsurf rule set + $ task-master rules --${RULES_SETUP_ACTION} # Interactive setup to select rule profiles` + ) .action(async (action, profiles, options) => { - // Validate action - handle setup separately since it's not in the enum - if (!isValidRulesAction(action) && action !== RULES_SETUP_ACTION) { - console.error( - chalk.red( - `Error: Invalid action '${action}'. Valid actions are: ${Object.values(RULES_ACTIONS).join(', ')}, ${RULES_SETUP_ACTION}` - ) - ); - process.exit(1); - } const projectDir = process.cwd(); /** - * 'task-master rules setup' action: + * 'task-master rules --setup' action: * * Launches an interactive prompt to select which rule profiles to add to the current project. * This does NOT perform project initialization or ask about shell aliases—only rules selection. * * Example usage: - * $ task-master rules setup + * $ task-master rules --setup * * Useful for adding rules after project creation. * * The list of profiles is always up-to-date with the available profiles. */ - if (action === RULES_SETUP_ACTION) { + if (options[RULES_SETUP_ACTION]) { // Run interactive rules setup ONLY (no project init) const selectedRuleProfiles = await runInteractiveProfilesSetup(); for (const profile of selectedRuleProfiles) { @@ -2829,6 +2832,21 @@ Examples: return; } + // Validate action for non-setup mode + if (!action || !isValidRulesAction(action)) { + console.error( + chalk.red( + `Error: Invalid or missing action '${action || 'none'}'. Valid actions are: ${Object.values(RULES_ACTIONS).join(', ')}` + ) + ); + console.error( + chalk.yellow( + `For interactive setup, use: task-master rules --${RULES_SETUP_ACTION}` + ) + ); + process.exit(1); + } + if (!profiles || profiles.length === 0) { console.error( 'Please specify at least one rule profile (e.g., windsurf, roo).' diff --git a/tests/unit/commands.test.js b/tests/unit/commands.test.js index 0fe6e56a..d76d7779 100644 --- a/tests/unit/commands.test.js +++ b/tests/unit/commands.test.js @@ -92,6 +92,10 @@ jest.mock('../../scripts/modules/utils.js', () => ({ import fs from 'fs'; import path from 'path'; import { setupCLI } from '../../scripts/modules/commands.js'; +import { + RULES_SETUP_ACTION, + RULES_ACTIONS +} from '../../src/constants/rules-actions.js'; describe('Commands Module - CLI Setup and Integration', () => { const mockExistsSync = jest.spyOn(fs, 'existsSync'); @@ -341,7 +345,9 @@ describe('rules command', () => { test('should handle rules add command', async () => { // Simulate: task-master rules add roo - await program.parseAsync(['rules', 'add', 'roo'], { from: 'user' }); + await program.parseAsync(['rules', RULES_ACTIONS.ADD, 'roo'], { + from: 'user' + }); // Expect some log output indicating success expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringMatching(/adding rules for profile: roo/i) @@ -355,9 +361,12 @@ describe('rules command', () => { test('should handle rules remove command', async () => { // Simulate: task-master rules remove roo --force - await program.parseAsync(['rules', 'remove', 'roo', '--force'], { - from: 'user' - }); + await program.parseAsync( + ['rules', RULES_ACTIONS.REMOVE, 'roo', '--force'], + { + from: 'user' + } + ); // Expect some log output indicating removal expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringMatching(/removing rules for profile: roo/i) @@ -370,4 +379,86 @@ describe('rules command', () => { // Should not exit with error expect(mockExit).not.toHaveBeenCalledWith(1); }); + + test(`should handle rules --${RULES_SETUP_ACTION} command`, async () => { + // For this test, we'll verify that the command doesn't crash and exits gracefully + // Since mocking ES modules is complex, we'll test the command structure instead + + // Create a spy on console.log to capture any output + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + + // Mock process.exit to prevent actual exit and capture the call + const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {}); + + try { + // The command should be recognized and not throw an error about invalid action + // We expect it to attempt to run the interactive setup, but since we can't easily + // mock the ES module, we'll just verify the command structure is correct + + // This test verifies that: + // 1. The --setup flag is recognized as a valid option + // 2. The command doesn't exit with error code 1 due to invalid action + // 3. The command structure is properly set up + + // Note: In a real scenario, this would call runInteractiveProfilesSetup() + // but for testing purposes, we're focusing on command structure validation + + expect(() => { + // Test that the command option is properly configured + const command = program.commands.find((cmd) => cmd.name() === 'rules'); + expect(command).toBeDefined(); + + // Check that the --setup option exists + const setupOption = command.options.find( + (opt) => opt.long === `--${RULES_SETUP_ACTION}` + ); + expect(setupOption).toBeDefined(); + expect(setupOption.description).toContain('interactive setup'); + }).not.toThrow(); + + // Verify the command structure is valid + expect(mockExit).not.toHaveBeenCalledWith(1); + } finally { + consoleSpy.mockRestore(); + exitSpy.mockRestore(); + } + }); + + test('should show error for invalid action', async () => { + // Simulate: task-master rules invalid-action + await program.parseAsync(['rules', 'invalid-action'], { from: 'user' }); + + // Should show error for invalid action + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringMatching(/Error: Invalid or missing action/i) + ); + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringMatching( + new RegExp( + `For interactive setup, use: task-master rules --${RULES_SETUP_ACTION}`, + 'i' + ) + ) + ); + expect(mockExit).toHaveBeenCalledWith(1); + }); + + test('should show error when no action provided', async () => { + // Simulate: task-master rules (no action) + await program.parseAsync(['rules'], { from: 'user' }); + + // Should show error for missing action + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringMatching(/Error: Invalid or missing action 'none'/i) + ); + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringMatching( + new RegExp( + `For interactive setup, use: task-master rules --${RULES_SETUP_ACTION}`, + 'i' + ) + ) + ); + expect(mockExit).toHaveBeenCalledWith(1); + }); });