Skip to main content
Glama

Curupira

by drzln
command-parser.ts9.07 kB
/** * @fileoverview Advanced command parser for Curupira CLI */ import { createLogger } from '@curupira/shared' import type { CliContext, CommandResult, BaseCommand, ParsedCommand, CommandDefinition, ValidationResult, HelpInfo } from '../types.js' const logger = createLogger({ level: 'info', name: 'command-parser' }) /** * Enhanced command parser with validation and help generation */ export class CommandParser { private commands: Map<string, CommandDefinition> = new Map() private aliases: Map<string, string> = new Map() /** * Register a command with the parser */ registerCommand(definition: CommandDefinition): void { logger.debug({ command: definition.name }, 'Registering command') this.commands.set(definition.name, definition) // Register aliases if (definition.aliases) { for (const alias of definition.aliases) { this.aliases.set(alias, definition.name) } } } /** * Parse command line arguments into structured command */ parseCommand(args: string[]): ParsedCommand { if (args.length < 3) { return { command: 'help', args: [], flags: {}, options: {}, raw: args } } const [, , commandName, ...rest] = args const resolvedCommand = this.resolveCommand(commandName) const parsed = this.parseArguments(rest) return { command: resolvedCommand, args: parsed.args, flags: parsed.flags, options: parsed.options, raw: args } } /** * Validate parsed command against command definition */ validateCommand(parsed: ParsedCommand): ValidationResult { const definition = this.commands.get(parsed.command) if (!definition) { return { valid: false, errors: [`Unknown command: ${parsed.command}`] } } const errors: string[] = [] // Validate required arguments if (definition.args) { const requiredArgs = definition.args.filter(arg => arg.required) if (parsed.args.length < requiredArgs.length) { errors.push(`Missing required arguments: ${requiredArgs.slice(parsed.args.length).map(a => a.name).join(', ')}`) } } // Validate options if (definition.options) { for (const [optionName, value] of Object.entries(parsed.options)) { const optionDef = definition.options.find(opt => opt.name === optionName) if (!optionDef) { errors.push(`Unknown option: --${optionName}`) continue } // Type validation if (optionDef.type === 'number' && isNaN(Number(value))) { errors.push(`Option --${optionName} must be a number`) } if (optionDef.type === 'boolean' && typeof value !== 'boolean') { errors.push(`Option --${optionName} must be true or false`) } // Choices validation if (optionDef.choices && !optionDef.choices.includes(String(value))) { errors.push(`Option --${optionName} must be one of: ${optionDef.choices.join(', ')}`) } } } // Validate flags if (definition.flags) { for (const flagName of Object.keys(parsed.flags)) { const flagDef = definition.flags.find(flag => flag.name === flagName) if (!flagDef) { errors.push(`Unknown flag: --${flagName}`) } } } return { valid: errors.length === 0, errors } } /** * Generate help information for a command */ generateHelp(commandName?: string): HelpInfo { if (commandName) { const command = this.commands.get(commandName) if (!command) { return { command: commandName, description: `Unknown command: ${commandName}`, usage: '', options: [], examples: [] } } return this.generateCommandHelp(command) } return this.generateOverallHelp() } /** * Get all registered commands */ getCommands(): CommandDefinition[] { return Array.from(this.commands.values()) } /** * Check if a command exists */ hasCommand(name: string): boolean { return this.commands.has(name) || this.aliases.has(name) } /** * Resolve command name or alias to actual command name */ private resolveCommand(name: string): string { if (this.commands.has(name)) { return name } if (this.aliases.has(name)) { return this.aliases.get(name)! } return name // Return as-is for validation to catch unknown commands } /** * Parse arguments into args, flags, and options */ private parseArguments(args: string[]): { args: string[] flags: Record<string, boolean> options: Record<string, string | boolean | number> } { const result = { args: [] as string[], flags: {} as Record<string, boolean>, options: {} as Record<string, string | boolean | number> } for (let i = 0; i < args.length; i++) { const arg = args[i] if (arg.startsWith('--')) { // Long option const [optionName, value] = arg.slice(2).split('=', 2) if (value !== undefined) { // --option=value result.options[optionName] = this.parseValue(value) } else if (i + 1 < args.length && !args[i + 1].startsWith('-')) { // --option value result.options[optionName] = this.parseValue(args[i + 1]) i++ // Skip next argument } else { // --flag result.flags[optionName] = true } } else if (arg.startsWith('-') && arg.length > 1) { // Short option(s) const shortOpts = arg.slice(1) if (shortOpts.length === 1 && i + 1 < args.length && !args[i + 1].startsWith('-')) { // -o value result.options[shortOpts] = this.parseValue(args[i + 1]) i++ // Skip next argument } else { // -abc (multiple flags) for (const char of shortOpts) { result.flags[char] = true } } } else { // Positional argument result.args.push(arg) } } return result } /** * Parse string value to appropriate type */ private parseValue(value: string): string | boolean | number { // Boolean if (value === 'true') return true if (value === 'false') return false // Number if (/^\d+$/.test(value)) return parseInt(value, 10) if (/^\d*\.\d+$/.test(value)) return parseFloat(value) // String return value } /** * Generate help for a specific command */ private generateCommandHelp(command: CommandDefinition): HelpInfo { const usage = this.generateUsage(command) const options = this.generateOptionsList(command) return { command: command.name, description: command.description, usage, options, examples: command.examples || [] } } /** * Generate overall help for all commands */ private generateOverallHelp(): HelpInfo { const commands = Array.from(this.commands.values()) const usage = 'curupira <command> [options]' return { command: 'curupira', description: 'AI-assisted React debugging tool', usage, options: commands.map(cmd => ({ name: cmd.name, description: cmd.description, type: 'command' })), examples: [ 'curupira init # Initialize Curupira in your project', 'curupira validate # Validate configuration', 'curupira dev # Start development server', 'curupira --help # Show this help message' ] } } /** * Generate usage string for a command */ private generateUsage(command: CommandDefinition): string { let usage = `curupira ${command.name}` if (command.args) { for (const arg of command.args) { if (arg.required) { usage += ` <${arg.name}>` } else { usage += ` [${arg.name}]` } } } if (command.options || command.flags) { usage += ' [options]' } return usage } /** * Generate options list for help display */ private generateOptionsList(command: CommandDefinition): Array<{ name: string description: string type: string }> { const options = [] if (command.flags) { for (const flag of command.flags) { options.push({ name: `--${flag.name}`, description: flag.description, type: 'flag' }) } } if (command.options) { for (const option of command.options) { let name = `--${option.name}` if (option.short) { name += `, -${option.short}` } if (option.type !== 'boolean') { name += ` <${option.type}>` } options.push({ name, description: option.description, type: option.type }) } } return options } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/drzln/curupira'

If you have feedback or need assistance with the MCP directory API, please join our Discord server