@api-buddy/plugin-utils
Version:
Shared utilities for API Buddy plugins
183 lines (153 loc) • 5.03 kB
text/typescript
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;
}
}