Merge pull request #21 from eyaltoledano/fix-kebabe-case
fix: camelCase detection mechanism in global CLI
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user