UNPKG

@api-buddy/plugin-utils

Version:

Shared utilities for API Buddy plugins

183 lines (153 loc) 5.03 kB
import { Command } from 'commander'; import chalk from 'chalk'; import inquirer from 'inquirer'; // Define a basic Logger interface if @api-buddy/types is not available interface Logger { info: (message: string) => void; warn: (message: string) => void; error: (message: string) => void; debug: (message: string) => void; level?: 'info' | 'warn' | 'error' | 'debug'; } export interface CommandContext { logger: Logger; cwd: string; } export interface CommandArgument { name: string; description?: string; required?: boolean; defaultValue?: unknown; } export interface CommandOption { flags: string; description?: string; required?: boolean; defaultValue?: unknown; } export interface CommandMeta { name: string; description?: string; arguments?: CommandArgument[]; options?: CommandOption[]; } export abstract class BaseCommand<T = Record<string, unknown>> { protected options: T; protected context: CommandContext; protected meta: CommandMeta; protected program: Command; constructor(context: CommandContext) { this.context = context; this.meta = this.getMeta(); this.options = {} as T; this.program = new Command(); this.program .name(this.meta.name) .description(this.meta.description || ''); // Add common options this.program .option('-v, --verbose', 'Enable verbose logging', false) .option('--dry-run', 'Perform a dry run without making changes', false); // Add command-specific arguments this.meta.arguments?.forEach((arg: CommandArgument) => { this.program.argument( arg.required ? `<${arg.name}>` : `[${arg.name}]`, arg.description || '' ); }); // Add command-specific options with proper type handling this.meta.options?.forEach((opt: CommandOption) => { // For commander v9+, we'll use the option's default value in the flags const flags = opt.defaultValue !== undefined ? `${opt.flags} [${typeof opt.defaultValue}]` : opt.flags; // Add the option with description this.program.option( flags, opt.description || '' ); // Store the default value in the options object if (opt.defaultValue !== undefined) { const optionName = this.getOptionName(opt.flags); if (optionName) { (this.options as Record<string, unknown>)[optionName] = opt.defaultValue; } } }); } abstract getMeta(): CommandMeta; abstract run(options: T): Promise<void> | void; async execute(args: string[] = process.argv): Promise<void> { this.program.parse(args); const options = this.program.opts(); // Set options from command line this.setOptions(options as Partial<T>); // Enable verbose logging if requested if (options.verbose && this.context.logger) { this.context.logger.level = 'debug'; } try { await this.run(this.options); } catch (error) { const err = error as Error; this.context.logger.error(`Error: ${err.message}`); if (options.verbose && err.stack) { this.context.logger.debug(err.stack); } process.exit(1); } } /** * Extracts the option name from command line flags * @param flags - The command line flags (e.g., '-n, --name') * @returns The option name or null if not found */ private getOptionName(flags: string): string | null { const match = flags.match(/--([a-zA-Z0-9-]+)/); return match ? match[1] : null; } protected setOptions(options: Partial<T>): void { this.options = { ...this.options, ...options } as T; } protected setOptionsFromEnv(prefix = 'API_BUDDY_'): void { const env = process.env; const envOptions: Record<string, unknown> = {}; Object.entries(env).forEach(([key, value]) => { if (key.startsWith(prefix) && value !== undefined) { const optionKey = key .substring(prefix.length) .toLowerCase() .replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); try { // Try to parse JSON values envOptions[optionKey] = value ? JSON.parse(value) : null; } catch { // Fall back to string value if not valid JSON envOptions[optionKey] = value; } } }); this.setOptions(envOptions as Partial<T>); } protected success(message: string): void { this.context.logger.info(chalk.green(`✓ ${message}`)); } protected log(message: string): void { this.context.logger.info(message); } protected warn(message: string): void { this.context.logger.warn(chalk.yellow(`⚠ ${message}`)); } protected async confirm(message: string, defaultValue = false): Promise<boolean> { if (process.env.CI) { return defaultValue; } const { confirmed } = await inquirer.prompt([{ type: 'confirm', name: 'confirmed', message, default: defaultValue, }]); return confirmed; } }