UNPKG

@jasondark/proompt

Version:

CLI tool for running AI prompts with structure and repeatability

209 lines 9.13 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.registerCommandModule = exports.replaceVariables = exports.createCommandModule = void 0; const child_process_1 = require("child_process"); const zod_1 = require("zod"); const resolver_1 = require("./config/resolver"); const constants_1 = require("./constants"); /** * Create a command module with type safety and schema validation */ const createCommandModule = (config, argsSchema, proomptContent, customHandler) => { const handler = customHandler || createDefaultHandler(proomptContent, argsSchema); return { config: { name: config.name, description: config.description, arguments: config.arguments || [], }, argsSchema, handler, proomptContent, }; }; exports.createCommandModule = createCommandModule; /** * Create default handler that executes proompt with schema-validated arguments */ const createDefaultHandler = (proomptContent, argsSchema) => { return async (args) => { // Validate arguments using schema const validatedArgs = argsSchema.parse(args); // Use direct execution with proompt content instead of file path await executeProomptContent(proomptContent, validatedArgs); }; }; /** * Execute proompt with content directly (not from file) */ const executeProomptContent = async (content, args) => { // Resolve settings using hierarchy: args > user settings > defaults const settingsOverride = { llmCli: args?.llmCli, outputFormat: args?.outputFormat, }; const resolvedSettings = (0, resolver_1.resolveSettings)(settingsOverride); const llmCli = resolvedSettings.llmCli; const outputFormat = resolvedSettings.outputFormat; // Generate output file names based on configured formats const outputFileNames = outputFormat.map((format) => constants_1.OUTPUT_FILE_NAMES[format]); const isPlural = outputFileNames.length > 1; // Inject output format configuration into template variables const enhancedArgs = { ...args, outputFiles: outputFileNames.map((name) => `\`${name}\``).join(' and '), outputFileList: outputFileNames.join(' and '), outputAction: isPlural ? 'Create identical' : 'Create a', fileOrFiles: isPlural ? 'files' : 'file', requiredDocFiles: outputFileNames.map((name) => `"${name}"`).join(', '), allRequiredFilesExist: `all required files (${outputFileNames.join(', ')}) exist`, }; // Replace variables with provided arguments const processedContent = (0, exports.replaceVariables)(content, enhancedArgs); // Launch interactive CLI with initial prompt let child; if (llmCli === 'claude') { // Claude accepts prompt as argument child = (0, child_process_1.spawn)('claude', [processedContent], { stdio: 'inherit', }); } else if (llmCli === 'gemini') { // Gemini uses -i flag for interactive with initial prompt child = (0, child_process_1.spawn)('gemini', ['-i', processedContent], { stdio: 'inherit', }); } else { throw new Error(`Unsupported LLM CLI: ${llmCli}`); } return new Promise((resolve, reject) => { child.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`${llmCli} command failed with exit code ${code}`)); } }); child.on('error', (error) => { reject(error); }); }); }; /** * Replace template variables in content with provided arguments */ const replaceVariables = (content, args) => { let result = content; for (const [key, value] of Object.entries(args)) { const regex = new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g'); result = result.replace(regex, String(value)); } return result; }; exports.replaceVariables = replaceVariables; /** * Add command module to Commander program with automatic option generation */ const registerCommandModule = (program, module) => { const command = program .command(module.config.name) .description(module.config.description); // TODO: This is gross, need to find a better way to conditionally add options to commands // Add global settings options to all commands (except config) if (module.config.name !== 'config') { // All commands get the LLM CLI option command.option('-L, --llm-cli <cli>', 'override LLM CLI (claude|gemini)'); // Planning commands get the output file path option (required) if (module.config.name === 'generate-plan' || module.config.name === 'validate-plan') { command.requiredOption('-o, --output-file-path <path>', 'specify output file path for generated content'); } // Documentation commands get the output format option if (module.config.name.startsWith('document-')) { command.option('-F, --output-format <formats>', 'override output format (claude,gemini or any combination)'); } } // Add positional arguments and options based on command arguments for (const arg of module.config.arguments) { if (arg.positional) { // Handle positional arguments const argName = arg.required ? `<${arg.name}>` : `[${arg.name}]`; command.argument(argName, arg.description); } else { // Handle options (flags) const optionFlag = arg.alias ? `-${arg.alias}, --${arg.name}` : `--${arg.name}`; const optionDescription = arg.required ? `${arg.description} (required)` : arg.description; if (arg.type === 'boolean') { command.option(optionFlag, optionDescription); } else { const valueDesc = arg.type === 'number' ? '<number>' : '<value>'; command.option(`${optionFlag} ${valueDesc}`, optionDescription); } } } // Set up action handler command.action(async (...args) => { try { // Commander.js passes positional arguments first, then options object as the last argument const commandObject = args[args.length - 1]; const positionalArgs = args.slice(0, -1); // Extract option values from Commander object const options = commandObject._optionValues || {}; // Map positional arguments to their names based on the command configuration const positionalArguments = module.config.arguments.filter((arg) => arg.positional); const combinedArgs = { ...options }; // Convert option keys from kebab-case to camelCase to match schema expectations for (const [key, value] of Object.entries(options)) { const camelKey = key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); if (camelKey !== key) { combinedArgs[camelKey] = value; delete combinedArgs[key]; } } for (let i = 0; i < positionalArguments.length && i < positionalArgs.length; i++) { const argName = positionalArguments[i].name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); combinedArgs[argName] = positionalArgs[i]; } await module.handler(combinedArgs); } catch (error) { if (error instanceof zod_1.z.ZodError) { console.error(`Error: Missing required arguments for command '${module.config.name}':`); for (const issue of error.issues) { const argName = String(issue.path[0]); const arg = module.config.arguments.find((a) => a.name.replace(/-/g, '') === argName || a.name.replace(/-/g, '').toLowerCase() === argName.toLowerCase()); if (arg) { if (arg.positional) { console.error(` ${arg.name}: ${arg.description} (positional argument)`); } else { const flag = arg.alias ? `-${arg.alias}, --${arg.name}` : `--${arg.name}`; console.error(` ${flag}: ${arg.description}`); } } else { console.error(` --${argName}: ${issue.message}`); } } console.error(`\nUse 'proompt ${module.config.name} --help' for more information.`); } else { console.error(`Error running command ${module.config.name}: ${error.message}`); } process.exit(1); } }); }; exports.registerCommandModule = registerCommandModule; //# sourceMappingURL=command-utils.js.map