From 33bcb0114a4ccbcac783be8e3642f3d9c1e01339 Mon Sep 17 00:00:00 2001 From: Eyal Toledano Date: Tue, 25 Mar 2025 00:12:29 -0400 Subject: [PATCH 1/2] fix: camelCase detection mechanism in global CLI --- bin/task-master.js | 194 ++++++++++++++++++++++++++++----------------- scripts/dev.js | 5 ++ 2 files changed, 128 insertions(+), 71 deletions(-) diff --git a/bin/task-master.js b/bin/task-master.js index e3284f0e..03fe1728 100755 --- a/bin/task-master.js +++ b/bin/task-master.js @@ -27,6 +27,22 @@ const initScriptPath = resolve(__dirname, '../scripts/init.js'); // Helper function to run dev.js with arguments function runDevScript(args) { + // Debug: Show the transformed arguments when DEBUG=1 is set + if (process.env.DEBUG === '1') { + console.error('\nDEBUG - CLI Wrapper Analysis:'); + console.error('- Original command: ' + process.argv.join(' ')); + console.error('- Transformed args: ' + args.join(' ')); + console.error('- dev.js will receive: node ' + devScriptPath + ' ' + args.join(' ') + '\n'); + } + + // For testing: If TEST_MODE is set, just print args and exit + if (process.env.TEST_MODE === '1') { + console.log('Would execute:'); + console.log(`node ${devScriptPath} ${args.join(' ')}`); + process.exit(0); + return; + } + const child = spawn('node', [devScriptPath, ...args], { stdio: 'inherit', cwd: process.cwd() @@ -44,93 +60,128 @@ function runDevScript(args) { */ function createDevScriptAction(commandName) { return (options, cmd) => { - // Start with the command name + // Helper function to detect camelCase and convert to kebab-case + const toKebabCase = (str) => str.replace(/([A-Z])/g, '-$1').toLowerCase(); + + // Check for camelCase flags and error out with helpful message + const camelCaseFlags = []; + for (const arg of process.argv) { + if (arg.startsWith('--') && /[A-Z]/.test(arg)) { + const flagName = arg.split('=')[0].slice(2); // Remove -- and anything after = + const kebabVersion = toKebabCase(flagName); + camelCaseFlags.push({ + original: flagName, + kebabCase: kebabVersion + }); + } + } + + // If camelCase flags were found, show error and exit + if (camelCaseFlags.length > 0) { + console.error('\nError: Please use kebab-case for CLI flags:'); + camelCaseFlags.forEach(flag => { + console.error(` Instead of: --${flag.original}`); + console.error(` Use: --${flag.kebabCase}`); + }); + console.error('\nExample: task-master parse-prd --num-tasks=5 instead of --numTasks=5\n'); + process.exit(1); + } + + // Since we've ensured no camelCase flags, we can now just: + // 1. Start with the command name const args = [commandName]; - // Handle direct arguments (non-option arguments) - if (cmd && cmd.args && cmd.args.length > 0) { - args.push(...cmd.args); - } + // 3. Get positional arguments and explicit flags from the command line + const commandArgs = []; + const positionals = new Set(); // Track positional args we've seen - // Get the original CLI arguments to detect which options were explicitly specified - const originalArgs = process.argv; - - // Special handling for parent parameter which seems to have issues - const parentArg = originalArgs.find(arg => arg.startsWith('--parent=')); - if (parentArg) { - args.push('-p', parentArg.split('=')[1]); - } else if (options.parent) { - args.push('-p', options.parent); - } - - // Add all options - Object.entries(options).forEach(([key, value]) => { - // Skip the Command's built-in properties and parent (special handling) - if (['parent', 'commands', 'options', 'rawArgs'].includes(key)) { - return; - } - - // Special case: handle the 'generate' option which is automatically set to true - // We should only include it if --no-generate was explicitly specified - if (key === 'generate') { - // Check if --no-generate was explicitly specified - if (originalArgs.includes('--no-generate')) { - args.push('--no-generate'); + // Find the command in raw process.argv to extract args + const commandIndex = process.argv.indexOf(commandName); + if (commandIndex !== -1) { + // Process all args after the command name + for (let i = commandIndex + 1; i < process.argv.length; i++) { + const arg = process.argv[i]; + + if (arg.startsWith('--')) { + // It's a flag - pass through as is + commandArgs.push(arg); + // Skip the next arg if this is a flag with a value (not --flag=value format) + if (!arg.includes('=') && + i + 1 < process.argv.length && + !process.argv[i+1].startsWith('--')) { + commandArgs.push(process.argv[++i]); + } + } else if (!positionals.has(arg)) { + // It's a positional argument we haven't seen + commandArgs.push(arg); + positionals.add(arg); } + } + } + + // Add all command line args we collected + args.push(...commandArgs); + + // 4. Add default options from Commander if not specified on command line + // Track which options we've seen on the command line + const userOptions = new Set(); + for (const arg of commandArgs) { + if (arg.startsWith('--')) { + // Extract option name (without -- and value) + const name = arg.split('=')[0].slice(2); + userOptions.add(name); + + // Add the kebab-case version too, to prevent duplicates + const kebabName = name.replace(/([A-Z])/g, '-$1').toLowerCase(); + userOptions.add(kebabName); + + // Add the camelCase version as well + const camelName = kebabName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); + userOptions.add(camelName); + } + } + + // Add Commander-provided defaults for options not specified by user + Object.entries(options).forEach(([key, value]) => { + // Skip built-in Commander properties and options the user provided + if (['parent', 'commands', 'options', 'rawArgs'].includes(key) || userOptions.has(key)) { return; } - // Look for how this parameter was passed in the original arguments - // Find if it was passed as --key=value - const equalsFormat = originalArgs.find(arg => arg.startsWith(`--${key}=`)); - - // Check for kebab-case flags - // Convert camelCase back to kebab-case for command line arguments + // Also check the kebab-case version of this key const kebabKey = key.replace(/([A-Z])/g, '-$1').toLowerCase(); - - // Check if it was passed with kebab-case - const foundInOriginal = originalArgs.find(arg => - arg === `--${key}` || - arg === `--${kebabKey}` || - arg.startsWith(`--${key}=`) || - arg.startsWith(`--${kebabKey}=`) - ); - - // Determine the actual flag name to use (original or kebab-case) - const flagName = foundInOriginal ? - (foundInOriginal.startsWith('--') ? foundInOriginal.split('=')[0].slice(2) : key) : - key; - - if (equalsFormat) { - // Preserve the original format with equals sign - args.push(equalsFormat); + if (userOptions.has(kebabKey)) { return; } - // Handle boolean flags - if (typeof value === 'boolean') { - if (value === true) { - // For non-negated options, add the flag - if (!flagName.startsWith('no-')) { - args.push(`--${flagName}`); + // Add default values + if (value !== undefined) { + if (typeof value === 'boolean') { + if (value === true) { + args.push(`--${key}`); + } else if (value === false && key === 'generate') { + args.push('--no-generate'); } } else { - // For false values, use --no-X format - if (flagName.startsWith('no-')) { - // If option is already in --no-X format, it means the user used --no-X explicitly - // We need to pass it as is - args.push(`--${flagName}`); - } else { - // If it's a regular option set to false, convert to --no-X - args.push(`--no-${flagName}`); - } + args.push(`--${key}=${value}`); } - } else if (value !== undefined) { - // For non-boolean values, pass as --key value (space-separated) - args.push(`--${flagName}`, value.toString()); } }); + // Special handling for parent parameter (uses -p) + if (options.parent && !args.includes('-p') && !userOptions.has('parent')) { + args.push('-p', options.parent); + } + + // Debug output for troubleshooting + if (process.env.DEBUG === '1') { + console.error('DEBUG - Command args:', commandArgs); + console.error('DEBUG - User options:', Array.from(userOptions)); + console.error('DEBUG - Commander options:', options); + console.error('DEBUG - Final args:', args); + } + + // Run the script with our processed args runDevScript(args); }; } @@ -214,7 +265,8 @@ tempProgram.commands.forEach(cmd => { // Create a new command with the same name and description const newCmd = program .command(cmd.name()) - .description(cmd.description()); + .description(cmd.description()) + .allowUnknownOption(); // Allow any options, including camelCase ones // Copy all options cmd.options.forEach(opt => { diff --git a/scripts/dev.js b/scripts/dev.js index 3e2bf9e9..8d2aad73 100755 --- a/scripts/dev.js +++ b/scripts/dev.js @@ -8,6 +8,11 @@ * It imports functionality from the modules directory and provides a CLI. */ +// Add at the very beginning of the file +if (process.env.DEBUG === '1') { + console.error('DEBUG - dev.js received args:', process.argv.slice(2)); +} + import { runCLI } from './modules/commands.js'; // Run the CLI with the process arguments From d4f767c9b51e8eb3df6d1e1756fe2c049bd378c1 Mon Sep 17 00:00:00 2001 From: Eyal Toledano Date: Tue, 25 Mar 2025 00:22:43 -0400 Subject: [PATCH 2/2] adjusts rule to use kebab-case for long form option flags. --- .cursor/rules/commands.mdc | 2 +- tasks/task_024.txt | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.cursor/rules/commands.mdc b/.cursor/rules/commands.mdc index ddf0959b..534a44b9 100644 --- a/.cursor/rules/commands.mdc +++ b/.cursor/rules/commands.mdc @@ -35,7 +35,7 @@ alwaysApply: false - ✅ DO: Use descriptive, action-oriented names - **Option Names**: - - ✅ DO: Use camelCase for long-form option names (`--outputFormat`) + - ✅ DO: Use kebab-case for long-form option names (`--output-format`) - ✅ DO: Provide single-letter shortcuts when appropriate (`-f, --file`) - ✅ DO: Use consistent option names across similar commands - ❌ DON'T: Use different names for the same concept (`--file` in one command, `--path` in another) diff --git a/tasks/task_024.txt b/tasks/task_024.txt index 53ed453a..aec6f810 100644 --- a/tasks/task_024.txt +++ b/tasks/task_024.txt @@ -59,30 +59,30 @@ Testing approach: - Test error handling for non-existent task IDs - Test basic command flow with a mock task store -## 2. Implement AI prompt construction [pending] +## 2. Implement AI prompt construction and FastMCP integration [pending] ### Dependencies: 24.1 -### Description: Develop the logic to analyze tasks, construct appropriate AI prompts, and interact with the AI service using the existing ai-service.js to generate test content. +### Description: Develop the logic to analyze tasks, construct appropriate AI prompts, and interact with the AI service using FastMCP to generate test content. ### Details: Implementation steps: 1. Create a utility function to analyze task descriptions and subtasks for test requirements 2. Implement a prompt builder that formats task information into an effective AI prompt -3. Use ai-service.js as needed to send the prompt and receive the response (streaming) -4. Process the response to extract the generated test code -5. Implement error handling for failures, rate limits, and malformed responses -6. Add appropriate logging for the test generation process +3. Use FastMCP to send the prompt and receive the response +4. Process the FastMCP response to extract the generated test code +5. Implement error handling for FastMCP failures, rate limits, and malformed responses +6. Add appropriate logging for the FastMCP interaction process Testing approach: - Test prompt construction with various task types -- Test ai services integration with mocked responses -- Test error handling for ai service failures -- Test response processing with sample ai-services.js outputs +- Test FastMCP integration with mocked responses +- Test error handling for FastMCP failures +- Test response processing with sample FastMCP outputs ## 3. Implement test file generation and output [pending] ### Dependencies: 24.2 ### Description: Create functionality to format AI-generated tests into proper Jest test files and save them to the appropriate location. ### Details: Implementation steps: -1. Create a utility to format the ai-services.js response into a well-structured Jest test file +1. Create a utility to format the FastMCP response into a well-structured Jest test file 2. Implement naming logic for test files (task_XXX.test.ts for parent tasks, task_XXX_YYY.test.ts for subtasks) 3. Add logic to determine the appropriate file path for saving the test 4. Implement file system operations to write the test file @@ -93,7 +93,7 @@ Implementation steps: Testing approach: - Test file naming logic for various task/subtask combinations -- Test file content formatting with sample ai-services.js outputs +- Test file content formatting with sample FastMCP outputs - Test file system operations with mocked fs module - Test the complete flow from command input to file output - Verify generated tests can be executed by Jest