UNPKG

create-roadkit

Version:

Beautiful Next.js roadmap website generator with full-screen kanban boards, dark/light mode, and static export

977 lines (869 loc) 31.4 kB
/** * Secure CLI implementation for RoadKit with comprehensive error handling and recovery * * This module provides a production-ready CLI system with: * - Comprehensive input validation and sanitization * - Proper error handling without process.exit calls * - Security logging and audit trails * - Recovery mechanisms for common failures * - Progress tracking and user feedback */ import { input, select, confirm, checkbox } from '@inquirer/prompts'; import chalk from 'chalk'; import ora from 'ora'; import boxen from 'boxen'; import { resolve, dirname } from 'path'; import { existsSync } from 'fs'; import { validateCLIOptions, validateProjectName, validateTemplateType, validateThemeType, sanitizePath, SecurityError } from '../utils/security.js'; import { Logger, logger, ErrorRecovery } from '../utils/logger.js'; import { SecureTemplateManager, SecureScaffoldResult } from '../core/secure-templates.js'; /** * CLI options that can be passed from command line arguments */ export interface SecureCLIOptions { name?: string; template?: 'basic' | 'advanced' | 'enterprise' | 'custom'; theme?: 'modern' | 'classic' | 'minimal' | 'corporate'; output?: string; skipPrompts?: boolean; verbose?: boolean; dryRun?: boolean; overwrite?: boolean; skipInstall?: boolean; skipGit?: boolean; } /** * Complete user choices for project generation */ interface SecureUserChoices { projectName: string; description: string; author: { name: string; email?: string; url?: string; }; template: 'basic' | 'advanced' | 'enterprise' | 'custom'; theme: 'modern' | 'classic' | 'minimal' | 'corporate'; outputDirectory: string; language: 'javascript' | 'typescript'; features: { analytics: boolean; seo: boolean; pwa: boolean; authentication: boolean; database: boolean; api: boolean; testing: boolean; deployment: boolean; }; customization: { primaryColor: string; secondaryColor: string; fontFamily: 'inter' | 'roboto' | 'open-sans' | 'poppins'; logoUrl?: string; faviconUrl?: string; }; technical: { typescript: boolean; eslint: boolean; prettier: boolean; tailwind: boolean; shadcnUi: boolean; }; overwrite: boolean; gitInit: boolean; installDependencies: boolean; } /** * CLI execution result with detailed information */ export interface SecureCLIResult { success: boolean; projectPath?: string; duration: number; errors: string[]; warnings: string[]; securityWarnings: string[]; filesCreated: string[]; filesSkipped: string[]; nextSteps: string[]; canRetry: boolean; retryInstructions?: string[]; } /** * Secure CLI implementation with comprehensive error handling */ export class SecureRoadKitCLI { private logger: Logger; private errorRecovery: ErrorRecovery; private templateManager: SecureTemplateManager; private spinner?: any; private startTime: number = 0; constructor(options: { verbose?: boolean; logDir?: string; templateDir?: string } = {}) { // Initialize secure logging this.logger = new Logger({ level: options.verbose ? 0 : 1, // DEBUG or INFO enableConsole: true, enableFile: true, logDir: options.logDir || './logs', verbose: options.verbose || false, colored: true }); this.errorRecovery = new ErrorRecovery(this.logger); // Initialize secure template manager const defaultTemplateDir = process.env.NODE_ENV === 'test' ? resolve(__dirname, '../../test-temp/templates') : resolve(process.cwd(), 'src/templates'); const templateDir = options.templateDir || defaultTemplateDir; this.templateManager = new SecureTemplateManager(templateDir, this.logger); this.logger.info('SecureRoadKitCLI initialized', 'CLI', { verbose: options.verbose, templateDir, logDir: options.logDir }); } /** * Main CLI execution method with comprehensive error handling */ async execute(options: SecureCLIOptions = {}): Promise<SecureCLIResult> { this.startTime = Date.now(); const result: SecureCLIResult = { success: false, duration: 0, errors: [], warnings: [], securityWarnings: [], filesCreated: [], filesSkipped: [], nextSteps: [], canRetry: false }; try { // Step 1: Validate and sanitize CLI options this.logger.startOperation('CLI Validation'); const validatedOptions = await this.validateCLIOptions(options); // Step 2: Display welcome and collect user input if (!validatedOptions.skipPrompts) { this.displayWelcome(); } const userChoices = await this.collectUserChoices(validatedOptions); // Step 3: Display configuration summary and confirm if (!validatedOptions.skipPrompts) { await this.confirmProjectGeneration(userChoices); } // Step 4: Generate project with security controls const scaffoldResult = await this.generateProjectSecurely(userChoices); // Step 5: Process results and provide feedback this.processScaffoldResult(scaffoldResult, result); result.duration = Date.now() - this.startTime; this.logger.completeOperation('CLI Execution', result.duration); return result; } catch (error) { return await this.handleCLIError(error, result); } finally { // Always stop spinner and flush logs if (this.spinner) { this.spinner.stop(); } await this.logger.flush(); } } /** * Validates and sanitizes CLI options */ private async validateCLIOptions(options: SecureCLIOptions): Promise<SecureCLIOptions> { this.logger.debug('Validating CLI options', 'Security', { options }); const validation = validateCLIOptions(options); if (!validation.isValid) { this.logger.security( `CLI options validation failed: ${validation.error}`, 'input_validation', 'high', JSON.stringify(options) ); throw new SecurityError( `Invalid CLI options: ${validation.error}`, 'input_validation' ); } if (validation.warnings && validation.warnings.length > 0) { validation.warnings.forEach(warning => { this.logger.security( `CLI options warning: ${warning}`, 'input_validation', 'low' ); }); } this.logger.debug('CLI options validated successfully', 'Security'); return validation.data || options; } /** * Displays welcome message with security notice */ private displayWelcome(): void { const welcomeMessage = `${chalk.bold.blue('🛣️ RoadKit CLI')} ${chalk.gray('Secure Next.js roadmap website generator')} Welcome to RoadKit! This tool will help you create a fully functional, secure, and customizable roadmap website built with Next.js, TypeScript, and Tailwind CSS. ${chalk.yellow('Security Notice:')} All inputs are validated and sanitized to ensure secure project generation. `; console.log(boxen(welcomeMessage, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'blue', })); } /** * Collects and validates user choices with comprehensive security checks */ private async collectUserChoices(options: SecureCLIOptions): Promise<SecureUserChoices> { this.logger.info('Collecting user input with security validation', 'CLI'); // Project name with validation let projectName: string; if (options.name) { const nameValidation = validateProjectName(options.name); if (!nameValidation.isValid) { throw new SecurityError(`Invalid project name: ${nameValidation.error}`, 'input_validation'); } projectName = nameValidation.data!; } else if (options.skipPrompts) { throw new SecurityError('Project name is required when skipping prompts', 'input_validation'); } else { projectName = await input({ message: 'What is your project name?', validate: (value: string) => { const validation = validateProjectName(value); return validation.isValid ? true : validation.error!; }, }); } // Description with length and content validation let description: string; if (options.skipPrompts) { description = `Generated roadmap project for ${projectName}`; } else { description = await input({ message: 'Provide a brief description of your roadmap:', validate: (value: string) => { if (!value.trim()) return 'Description is required'; if (value.length > 200) return 'Description must be 200 characters or less'; // Check for potentially dangerous content if (/<script|javascript:|data:|vbscript:/i.test(value)) { return 'Description contains prohibited content'; } return true; }, }); } // Author information with validation let authorName: string; if (options.skipPrompts) { authorName = 'Generated User'; } else { authorName = await input({ message: 'Author name:', validate: (value: string) => { if (!value.trim()) return 'Author name is required'; if (value.length > 100) return 'Author name too long'; if (/<script|javascript:|data:/i.test(value)) { return 'Author name contains prohibited content'; } return true; }, }); } let authorEmail: string | undefined; if (!options.skipPrompts) { const emailInput = await input({ message: 'Author email (optional):', validate: (value: string) => { if (!value.trim()) return true; const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; if (!emailRegex.test(value)) return 'Please enter a valid email address'; if (value.length > 254) return 'Email address too long'; return true; }, }); authorEmail = emailInput || undefined; } let authorUrl: string | undefined; if (!options.skipPrompts) { const urlInput = await input({ message: 'Author website URL (optional):', validate: (value: string) => { if (!value.trim()) return true; try { const url = new URL(value); if (!['http:', 'https:'].includes(url.protocol)) { return 'Only HTTP and HTTPS URLs are allowed'; } if (value.length > 2000) return 'URL too long'; return true; } catch { return 'Please enter a valid URL'; } }, }); authorUrl = urlInput || undefined; } // Template selection with validation let template: SecureUserChoices['template']; if (options.template) { const templateValidation = validateTemplateType(options.template); if (!templateValidation.isValid) { throw new SecurityError(`Invalid template: ${templateValidation.error}`, 'input_validation'); } template = templateValidation.data! as SecureUserChoices['template']; } else { template = await select({ message: 'Choose a project template:', choices: [ { name: 'Basic - Simple roadmap with essential features', value: 'basic' }, { name: 'Advanced - Feature-rich with analytics and SEO', value: 'advanced' }, { name: 'Enterprise - Full-featured with authentication and database', value: 'enterprise' }, { name: 'Custom - Minimal template for custom builds', value: 'custom' }, ], }) as SecureUserChoices['template']; } // Theme selection with validation let theme: SecureUserChoices['theme']; if (options.theme) { const themeValidation = validateThemeType(options.theme); if (!themeValidation.isValid) { throw new SecurityError(`Invalid theme: ${themeValidation.error}`, 'input_validation'); } theme = themeValidation.data! as SecureUserChoices['theme']; } else if (options.skipPrompts) { theme = 'modern'; // Default theme when skipping prompts } else { theme = await select({ message: 'Choose a visual theme:', choices: [ { name: 'Modern - Clean, contemporary design', value: 'modern' }, { name: 'Classic - Traditional, professional look', value: 'classic' }, { name: 'Minimal - Simple, focused design', value: 'minimal' }, { name: 'Corporate - Business-oriented styling', value: 'corporate' }, ], }) as SecureUserChoices['theme']; } // Output directory with path validation let outputDirectory: string; if (options.output) { const pathValidation = sanitizePath(options.output); if (!pathValidation.isValid) { throw new SecurityError(`Invalid output path: ${pathValidation.error}`, 'path_traversal'); } outputDirectory = pathValidation.sanitizedPath!; } else if (options.skipPrompts) { const defaultPath = `./${projectName}`; const pathValidation = sanitizePath(defaultPath); outputDirectory = pathValidation.sanitizedPath!; } else { const userPath = await input({ message: 'Output directory:', default: `./${projectName}`, validate: (value: string) => { const pathValidation = sanitizePath(value); return pathValidation.isValid ? true : pathValidation.error!; }, }); const pathValidation = sanitizePath(userPath); outputDirectory = pathValidation.sanitizedPath!; } // Language selection let language: 'javascript' | 'typescript'; if (options.skipPrompts) { language = 'typescript'; // Default to TypeScript when skipping prompts } else { language = await select({ message: 'Choose programming language:', choices: [ { name: 'TypeScript (recommended)', value: 'typescript' }, { name: 'JavaScript', value: 'javascript' }, ], default: 'typescript' }) as 'javascript' | 'typescript'; } // Feature selection based on template const features = await this.collectFeatureChoices(template, options.skipPrompts); // Customization options const customization = await this.collectCustomizationChoices(options.skipPrompts); // Technical configuration const technical = await this.collectTechnicalChoices(language, options.skipPrompts); // Additional options let overwrite: boolean; if (options.overwrite !== undefined) { overwrite = options.overwrite; } else if (options.skipPrompts) { overwrite = false; // Default to not overwriting when skipping prompts } else { overwrite = await confirm({ message: 'Overwrite existing directory if it exists?', default: false, }); } let gitInit: boolean; if (options.skipGit) { gitInit = false; } else if (options.skipPrompts) { gitInit = true; // Default to initializing git when skipping prompts } else { gitInit = await confirm({ message: 'Initialize git repository?', default: true, }); } let installDependencies: boolean; if (options.skipInstall) { installDependencies = false; } else if (options.skipPrompts) { installDependencies = true; // Default to installing dependencies when skipping prompts } else { installDependencies = await confirm({ message: 'Install dependencies after generation?', default: true, }); } const userChoices: SecureUserChoices = { projectName, description, author: { name: authorName, email: authorEmail || undefined, url: authorUrl || undefined, }, template, theme, outputDirectory, language, features, customization, technical, overwrite, gitInit, installDependencies, }; this.logger.info('User input collected successfully', 'CLI', { projectName, template, theme, hasEmail: !!authorEmail, hasUrl: !!authorUrl }); return userChoices; } /** * Collects feature choices with validation */ private async collectFeatureChoices(template: SecureUserChoices['template'], skipPrompts = false): Promise<SecureUserChoices['features']> { // Template-specific defaults const templateDefaults = { basic: ['seo', 'api', 'testing'], advanced: ['seo', 'api', 'testing', 'analytics', 'deployment'], enterprise: ['seo', 'api', 'testing', 'analytics', 'deployment', 'authentication', 'database'], custom: [], }; let selectedFeatures: string[]; if (skipPrompts) { // Use template defaults when skipping prompts selectedFeatures = templateDefaults[template]; } else { selectedFeatures = await checkbox({ message: 'Select additional features:', choices: [ { name: 'Analytics integration', value: 'analytics', checked: templateDefaults[template].includes('analytics') }, { name: 'SEO optimization', value: 'seo', checked: templateDefaults[template].includes('seo') }, { name: 'Progressive Web App (PWA)', value: 'pwa', checked: templateDefaults[template].includes('pwa') }, { name: 'Authentication system', value: 'authentication', checked: templateDefaults[template].includes('authentication') }, { name: 'Database integration', value: 'database', checked: templateDefaults[template].includes('database') }, { name: 'API routes', value: 'api', checked: templateDefaults[template].includes('api') }, { name: 'Testing setup', value: 'testing', checked: templateDefaults[template].includes('testing') }, { name: 'Deployment configuration', value: 'deployment', checked: templateDefaults[template].includes('deployment') }, ], }); } return { analytics: selectedFeatures.includes('analytics'), seo: selectedFeatures.includes('seo'), pwa: selectedFeatures.includes('pwa'), authentication: selectedFeatures.includes('authentication'), database: selectedFeatures.includes('database'), api: selectedFeatures.includes('api'), testing: selectedFeatures.includes('testing'), deployment: selectedFeatures.includes('deployment'), }; } /** * Collects customization options with validation */ private async collectCustomizationChoices(skipPrompts = false): Promise<SecureUserChoices['customization']> { let primaryColor: string; if (skipPrompts) { primaryColor = '#3b82f6'; // Default primary color } else { primaryColor = await input({ message: 'Primary color (hex code):', default: '#3b82f6', validate: (value: string) => { const hexRegex = /^#[0-9A-Fa-f]{6}$/; return hexRegex.test(value) ? true : 'Please enter a valid hex color (e.g., #3b82f6)'; }, }); } let secondaryColor: string; if (skipPrompts) { secondaryColor = '#64748b'; // Default secondary color } else { secondaryColor = await input({ message: 'Secondary color (hex code):', default: '#64748b', validate: (value: string) => { const hexRegex = /^#[0-9A-Fa-f]{6}$/; return hexRegex.test(value) ? true : 'Please enter a valid hex color (e.g., #64748b)'; }, }); } let fontFamily: SecureUserChoices['customization']['fontFamily']; if (skipPrompts) { fontFamily = 'inter'; // Default font family } else { fontFamily = await select({ message: 'Choose a font family:', choices: [ { name: 'Inter - Modern, clean sans-serif', value: 'inter' }, { name: 'Roboto - Google\'s signature font', value: 'roboto' }, { name: 'Open Sans - Friendly, readable font', value: 'open-sans' }, { name: 'Poppins - Geometric, modern look', value: 'poppins' }, ], }) as SecureUserChoices['customization']['fontFamily']; } let logoUrl: string | undefined; if (!skipPrompts) { const logoInput = await input({ message: 'Logo URL (optional):', validate: (value: string) => { if (!value.trim()) return true; try { const url = new URL(value); if (!['http:', 'https:'].includes(url.protocol)) { return 'Only HTTP and HTTPS URLs are allowed'; } if (value.length > 2000) return 'URL too long'; return true; } catch { return 'Please enter a valid URL'; } }, }); logoUrl = logoInput || undefined; } let faviconUrl: string | undefined; if (!skipPrompts) { const faviconInput = await input({ message: 'Favicon URL (optional):', validate: (value: string) => { if (!value.trim()) return true; try { const url = new URL(value); if (!['http:', 'https:'].includes(url.protocol)) { return 'Only HTTP and HTTPS URLs are allowed'; } if (value.length > 2000) return 'URL too long'; return true; } catch { return 'Please enter a valid URL'; } }, }); faviconUrl = faviconInput || undefined; } return { primaryColor, secondaryColor, fontFamily, logoUrl: logoUrl || undefined, faviconUrl: faviconUrl || undefined, }; } /** * Collects technical configuration */ private async collectTechnicalChoices(language: 'javascript' | 'typescript', skipPrompts = false): Promise<SecureUserChoices['technical']> { let technicalFeatures: string[]; if (skipPrompts) { // Use sensible defaults when skipping prompts technicalFeatures = ['eslint', 'prettier', 'tailwind', 'shadcnUi']; if (language === 'typescript') { technicalFeatures.push('typescript'); } } else { technicalFeatures = await checkbox({ message: 'Select technical features:', choices: [ { name: 'TypeScript', value: 'typescript', checked: language === 'typescript' }, { name: 'ESLint', value: 'eslint', checked: true }, { name: 'Prettier', value: 'prettier', checked: true }, { name: 'Tailwind CSS', value: 'tailwind', checked: true }, { name: 'shadcn/ui components', value: 'shadcnUi', checked: true }, ], }); } return { typescript: technicalFeatures.includes('typescript'), eslint: technicalFeatures.includes('eslint'), prettier: technicalFeatures.includes('prettier'), tailwind: technicalFeatures.includes('tailwind'), shadcnUi: technicalFeatures.includes('shadcnUi'), }; } /** * Confirms project generation with user */ private async confirmProjectGeneration(choices: SecureUserChoices): Promise<void> { const summary = ` ${chalk.bold('Project Configuration Summary:')} ${chalk.cyan('Basic Information:')} Name: ${choices.projectName} Description: ${choices.description} Author: ${choices.author.name}${choices.author.email ? ` <${choices.author.email}>` : ''} ${chalk.cyan('Template & Theme:')} Template: ${choices.template} Theme: ${choices.theme} Language: ${choices.language} ${chalk.cyan('Features:')} Analytics: ${choices.features.analytics ? chalk.green('Yes') : chalk.gray('No')} SEO: ${choices.features.seo ? chalk.green('Yes') : chalk.gray('No')} PWA: ${choices.features.pwa ? chalk.green('Yes') : chalk.gray('No')} Authentication: ${choices.features.authentication ? chalk.green('Yes') : chalk.gray('No')} Database: ${choices.features.database ? chalk.green('Yes') : chalk.gray('No')} ${chalk.cyan('Output:')} Directory: ${choices.outputDirectory} Install Dependencies: ${choices.installDependencies ? chalk.green('Yes') : chalk.gray('No')} Git Repository: ${choices.gitInit ? chalk.green('Yes') : chalk.gray('No')} ${chalk.yellow('Security:')} All inputs have been validated and sanitized. `; console.log(boxen(summary, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'cyan', })); const confirmed = await confirm({ message: 'Generate project with this configuration?', default: true, }); if (!confirmed) { throw new Error('Project generation cancelled by user'); } } /** * Generates project using secure template manager */ private async generateProjectSecurely(choices: SecureUserChoices): Promise<SecureScaffoldResult> { this.logger.startOperation('Project Generation'); this.spinner = ora('Initializing project generation...').start(); const projectConfig = { name: choices.projectName, template: choices.template, theme: choices.theme, path: dirname(choices.outputDirectory), language: choices.language, overwrite: choices.overwrite }; try { const result = await this.templateManager.scaffoldProject(projectConfig); if (this.spinner) { if (result.success) { this.spinner.succeed('Project generation completed'); } else { this.spinner.fail('Project generation failed'); } this.spinner = undefined; } return result; } catch (error) { if (this.spinner) { this.spinner.fail('Project generation error'); this.spinner = undefined; } throw error; } } /** * Processes scaffold result and updates CLI result */ private processScaffoldResult(scaffoldResult: SecureScaffoldResult, cliResult: SecureCLIResult): void { cliResult.success = scaffoldResult.success; cliResult.projectPath = scaffoldResult.projectPath; cliResult.filesCreated = scaffoldResult.createdFiles; cliResult.filesSkipped = scaffoldResult.skippedFiles; cliResult.warnings = scaffoldResult.warnings; cliResult.securityWarnings = scaffoldResult.securityWarnings; if (scaffoldResult.success) { // Generate next steps cliResult.nextSteps = this.generateNextSteps(scaffoldResult); this.displaySuccessResults(cliResult); } else { cliResult.errors.push(scaffoldResult.error || 'Unknown scaffolding error'); cliResult.canRetry = true; this.displayErrorResults(cliResult); } } /** * Handles CLI errors with recovery attempts */ private async handleCLIError(error: unknown, result: SecureCLIResult): Promise<SecureCLIResult> { result.duration = Date.now() - this.startTime; if (this.spinner) { this.spinner.fail('Operation failed'); this.spinner = undefined; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; result.errors.push(errorMessage); this.logger.failOperation('CLI Execution', error as Error); // Security incident logging if (error instanceof SecurityError) { this.logger.security( `Security error in CLI: ${errorMessage}`, 'cli_security_error', 'high', undefined, undefined, { securityType: error.securityType } ); result.securityWarnings.push(`Security error: ${errorMessage}`); } // Attempt error recovery result.canRetry = await this.attemptErrorRecovery(error, result); this.displayErrorResults(result); return result; } /** * Attempts to recover from errors */ private async attemptErrorRecovery(error: unknown, result: SecureCLIResult): Promise<boolean> { if (error instanceof SecurityError) { // Security errors cannot be automatically recovered result.retryInstructions = [ 'Review and correct the input that caused the security error', 'Ensure all paths and names follow the security guidelines', 'Run the command again with corrected inputs' ]; return true; } if (error instanceof Error && error.message.includes('EACCES')) { result.retryInstructions = [ 'Check file and directory permissions', 'Ensure you have write access to the target directory', 'Try running with elevated permissions if necessary' ]; return true; } if (error instanceof Error && error.message.includes('ENOSPC')) { result.retryInstructions = [ 'Free up disk space', 'Try using a different output directory', 'Run the command again after cleaning up space' ]; return true; } return false; } /** * Generates next steps for successful projects */ private generateNextSteps(scaffoldResult: SecureScaffoldResult): string[] { const steps = [ `cd ${scaffoldResult.projectPath}`, 'bun install', 'bun dev', ]; if (scaffoldResult.securityWarnings.length > 0) { steps.unshift('Review security warnings in the logs'); } return steps; } /** * Displays success results */ private displaySuccessResults(result: SecureCLIResult): void { const successMessage = `${chalk.bold.green('🎉 Project Generated Successfully!')} ${chalk.cyan('Project Details:')} Path: ${result.projectPath} Duration: ${result.duration}ms Files Created: ${result.filesCreated.length} Files Skipped: ${result.filesSkipped.length} ${result.securityWarnings.length > 0 ? `${chalk.yellow('Security Warnings:')} ${result.securityWarnings.map(warning => ` ⚠ ${warning}`).join('\n')} ` : ''}${chalk.cyan('Next Steps:')} ${result.nextSteps.map(step => ` ${chalk.gray('$')} ${step}`).join('\n')} ${chalk.green('Your secure Next.js roadmap project is ready!')} `; console.log(boxen(successMessage, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'green', })); } /** * Displays error results with recovery information */ private displayErrorResults(result: SecureCLIResult): void { let errorMessage = `${chalk.bold.red('❌ Project Generation Failed')} ${chalk.cyan('Error Details:')} ${result.errors.map(error => ` • ${error}`).join('\n')} Duration: ${result.duration}ms `; if (result.securityWarnings.length > 0) { errorMessage += ` ${chalk.yellow('Security Warnings:')} ${result.securityWarnings.map(warning => ` ⚠ ${warning}`).join('\n')} `; } if (result.canRetry && result.retryInstructions) { errorMessage += ` ${chalk.cyan('Recovery Instructions:')} ${result.retryInstructions.map(instruction => ` • ${instruction}`).join('\n')} `; } console.log(boxen(errorMessage, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'red', })); } } /** * Main entry point for secure CLI execution */ export async function runSecureCLI(options: SecureCLIOptions = {}): Promise<SecureCLIResult> { const cli = new SecureRoadKitCLI({ verbose: options.verbose, logDir: process.env.ROADKIT_LOG_DIR || './logs' }); return await cli.execute(options); } /** * Factory function for creating secure CLI instances */ export function createSecureCLI(options?: { verbose?: boolean; logDir?: string; templateDir?: string; }): SecureRoadKitCLI { return new SecureRoadKitCLI(options); }