UNPKG

scaffold-scripts

Version:

Simple CLI tool for managing and running your own scripts. Add any script, run it anywhere.

1,035 lines (910 loc) 36.3 kB
#!/usr/bin/env node import { Command } from 'commander' import chalk from 'chalk' import { readFileSync } from 'fs' import { join } from 'path' import prompts from 'prompts' import { ScaffoldDatabase, ScaffoldCommand } from './database.js' import { ScriptExecutor } from './scriptExecutor.js' import { ScriptValidator } from './scriptValidator.js' import { ScriptProcessor } from './scriptProcessor.js' import { sym } from './symbols.js' import { UsageHelper } from './usageHelper.js' // Get version from package.json const packageJsonPath = join(__dirname, '..', 'package.json') const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) const program = new Command() const db = new ScaffoldDatabase() const executor = new ScriptExecutor() const validator = new ScriptValidator() const processor = new ScriptProcessor() program .name('scaffold') .description('CLI tool for managing and executing scaffold scripts') .version(packageJson.version, '-v, --version', 'display version number') .helpOption('-h, --help', 'display help for command') .addHelpCommand('help [command]', 'display help for command') // Configure custom help program.configureHelp({ helpWidth: 80, sortSubcommands: true }); // Custom help display program.on('--help', () => { UsageHelper.displayMainHelp(); }); // Configure Commander to use our error handling program.exitOverride(); // Override Commander's error output program.configureOutput({ writeErr: () => {}, // Suppress all Commander error output writeOut: (str) => process.stdout.write(str) }); // Main scaffolding commands program .argument('[script-name]', 'run script with specified name') .option('--original', 'run the original script version') .option('--converted', 'run the converted script version') .option('--windows', 'run the Windows-specific script version') .option('--unix', 'run the Unix-specific script version') .option('--cross-platform', 'run the cross-platform script version') .action(async (scriptName, options) => { try { if (scriptName) { await handleScriptCommand(scriptName, false, options) } else { // Interactive mode when no script name provided const selectedScript = await selectScriptInteractively() if (!selectedScript) { return // User cancelled or no scripts available } // Handle special actions if (selectedScript === 'list') { await listCommands(false) return } if (selectedScript === 'add') { UsageHelper.displayCommandHelp('add'); return } if (selectedScript === 'clear') { await clearAllCommands() return } if (selectedScript === 'exit') { console.log(chalk.blue(`Goodbye! ${sym.party()}`)) return } // Run the selected script await handleScriptCommand(selectedScript, false) } } catch (error: any) { UsageHelper.displayError(error.message, 'Check your command syntax and try again'); process.exit(1) } }) // Add command program .command('add') .alias('a') .description('add a new script') .argument('<name>', 'script name') .argument('<scriptPath>', 'path to script file') .option('-p, --platform <platform>', 'target platform: all, windows, or unix', 'all') .option('--strict', 'use strict validation') .option('--no-validate', 'skip validation (use with caution)') .on('--help', () => { UsageHelper.displayCommandHelp('add'); }) .action(async (name, scriptPath, options) => { try { await addCommand(name, scriptPath, options) } catch (error: any) { UsageHelper.displayError(error.message, 'Check your script path and try again'); process.exit(1) } }) // Update command program .command('update') .alias('u') .description('update an existing script') .argument('<name>', 'script name') .argument('<scriptPath>', 'path to new script file') .option('-p, --platform <platform>', 'update target platform') .option('--strict', 'use strict validation') .on('--help', () => { UsageHelper.displayCommandHelp('update'); }) .action(async (name, scriptPath, options) => { try { await updateCommand(name, scriptPath, options) } catch (error: any) { UsageHelper.displayError(error.message, 'Check that the script exists and path is correct'); process.exit(1) } }) // Remove command program .command('remove') .alias('r') .description('remove a script') .argument('<name>', 'script name') .on('--help', () => { UsageHelper.displayCommandHelp('remove'); }) .action(async (name) => { try { await removeCommand(name) } catch (error: any) { UsageHelper.displayError(error.message, 'Check that the script name is correct'); process.exit(1) } }) // Clear all command program .command('clear') .description('clear all scripts from database') .option('-y, --yes', 'skip confirmation prompt') .on('--help', () => { console.log('') console.log('Examples:') console.log(' $ scripts clear # Clear all scripts with confirmation') console.log(' $ scripts clear -y # Clear all scripts without confirmation') }) .action(async (options) => { try { if (options.yes) { // Skip confirmation and clear directly const deletedCount = await db.clearAllCommands() if (deletedCount > 0) { console.log(chalk.green(`${sym.check()} Successfully cleared ${deletedCount} script(s) from the database.`)) } else { console.log(chalk.blue(`${sym.info()} No scripts were found to clear.`)) } } else { await clearAllCommands() } } catch (error: any) { UsageHelper.displayError(error.message, 'Unable to clear scripts'); process.exit(1) } }) // List commands program .command('list') .alias('l') .description('list available scripts') .option('-d, --detailed', 'show detailed information') .on('--help', () => { UsageHelper.displayCommandHelp('list'); }) .action(async (options) => { try { await listCommands(options.detailed) } catch (error: any) { UsageHelper.displayError(error.message, 'Unable to retrieve script list'); process.exit(1) } }) // View command program .command('view') .alias('s') .description('view script details') .argument('<name>', 'script name') .on('--help', () => { UsageHelper.displayCommandHelp('view'); }) .action(async (name) => { try { await handleScriptCommand(name, true) } catch (error: any) { UsageHelper.displayError(error.message, 'Check that the script name is correct'); process.exit(1) } }) // Export command program .command('export') .description('export all scripts to a directory') .argument('<directory>', 'directory to export scripts to') .on('--help', () => { UsageHelper.displayCommandHelp('export'); }) .action(async (directory) => { try { await exportCommand(directory) } catch (error: any) { UsageHelper.displayError(error.message, 'Check that the directory path is valid and writable'); process.exit(1) } }) // Uninstall command program .command('uninstall') .description('uninstall scaffold-scripts CLI') .action(async () => { try { await uninstallCommand() } catch (error: any) { UsageHelper.displayError(error.message, 'Try manual uninstallation or check the documentation'); process.exit(1) } }) // Interactive script selector async function selectScriptInteractively(): Promise<string | null> { const commands = await db.listCommands() if (commands.length === 0) { console.log(chalk.yellow('No scripts available.')) console.log(chalk.blue('Add one with: scripts add <name> <script-path>')) return null } console.log(chalk.blue(`\n${sym.rocket()} Available Scripts:`)) const choices = commands.map((cmd) => { const alias = cmd.alias ? ` (${cmd.alias})` : '' const platform = cmd.platform !== 'all' ? ` [${cmd.platform}]` : '' const description = cmd.description ? ` - ${cmd.description}` : '' return { title: `${cmd.name}${alias}${platform}${description}`, value: cmd.name, description: cmd.description || 'No description', } }) // Add management options choices.push( { title: '─'.repeat(50), value: 'separator', description: '─' }, { title: `${sym.list()} List all scripts`, value: 'list', description: 'Show all available scripts', }, { title: `${sym.package()} Add new script`, value: 'add', description: 'Get help adding a new script', }, { title: `${sym.warning()} Clear all scripts`, value: 'clear', description: 'Remove all scripts from the database', }, { title: `${sym.cross()} Exit`, value: 'exit', description: 'Exit interactive mode' }, ) try { const response = await prompts({ type: 'select', name: 'script', message: 'Select a script to run:', choices: choices, initial: 0, }) return response.script || null } catch (error) { // Handle Ctrl+C or other interruption console.log(chalk.yellow('\nOperation cancelled.')) return null } } // Clear all commands async function clearAllCommands() { console.log(chalk.yellow(`${sym.warning()} This will permanently delete ALL scripts from your database.`)) const confirmation = await prompts({ type: 'confirm', name: 'proceed', message: 'Are you sure you want to clear all scripts?', initial: false }) if (!confirmation.proceed) { console.log(chalk.blue('Operation cancelled.')) return } try { const deletedCount = await db.clearAllCommands() if (deletedCount > 0) { console.log(chalk.green(`${sym.check()} Successfully cleared ${deletedCount} script(s) from the database.`)) } else { console.log(chalk.blue(`${sym.info()} No scripts were found to clear.`)) } } catch (error: any) { console.error(chalk.red(`${sym.cross()} Error clearing scripts: ${error.message}`)) } } // Handle script commands async function handleScriptCommand(scriptName: string, viewOnly: boolean = false, versionOptions?: any) { // Try to find by name first, then by alias let command = await db.getCommand(scriptName) if (!command) { command = await db.getCommandByAlias(scriptName) } if (!command) { const commands = await db.listCommands() const scriptNames = commands.map(cmd => cmd.name) UsageHelper.displayScriptNotFound(scriptName, scriptNames); return } if (viewOnly) { displayCommandDetails(command) } else { console.log(chalk.blue(`Executing script: ${command.name}`)) if (command.description) { console.log(chalk.gray(command.description)) } // Determine which script version to execute based on flags let scriptToExecute = command.script_original; // default to original let versionUsed = 'original'; if (versionOptions) { if (versionOptions.original) { scriptToExecute = command.script_original; versionUsed = 'original'; } else if (versionOptions.converted) { scriptToExecute = processor.getBestScript(command); versionUsed = 'converted'; } else if (versionOptions.windows && command.script_windows) { scriptToExecute = command.script_windows; versionUsed = 'Windows'; } else if (versionOptions.unix && command.script_unix) { scriptToExecute = command.script_unix; versionUsed = 'Unix'; } else if (versionOptions.crossPlatform && command.script_cross_platform) { scriptToExecute = command.script_cross_platform; versionUsed = 'cross-platform'; } else if (versionOptions.windows && !command.script_windows) { console.log(chalk.yellow(`${sym.warning()} Windows version not available, using original`)); scriptToExecute = command.script_original; versionUsed = 'original (Windows not available)'; } else if (versionOptions.unix && !command.script_unix) { console.log(chalk.yellow(`${sym.warning()} Unix version not available, using original`)); scriptToExecute = command.script_original; versionUsed = 'original (Unix not available)'; } else if (versionOptions.crossPlatform && !command.script_cross_platform) { console.log(chalk.yellow(`${sym.warning()} Cross-platform version not available, using original`)); scriptToExecute = command.script_original; versionUsed = 'original (cross-platform not available)'; } } else { // Default behavior - auto-select best version if (['powershell', 'nodejs', 'python'].includes(command.script_type)) { scriptToExecute = command.script_original; versionUsed = 'original (interpreter-based)'; } else { scriptToExecute = processor.getBestScript(command); versionUsed = 'auto-selected'; } } console.log(chalk.gray(`Using ${versionUsed} version`)); const result = await executor.executeScript(scriptToExecute, command.platform, [], command.script_type) console.log(chalk.green(`\n${sym.check()} Script completed successfully!`)) // Output is now streamed in real-time, so we don't need to display it again // Only show stderr if there were any non-fatal errors if (result.stderr && result.stderr.trim()) { console.log(chalk.yellow('\n--- Warnings/Errors ---')) console.log(result.stderr) } } } // Add new command - Production Ready Version async function addCommand(name: string, scriptPath: string, options: any) { try { console.log(chalk.blue(`${sym.package()} Processing script: ${name}`)) // Process the script using the production-ready processor const processedScript = await processor.processScriptFile(scriptPath, { strict: options.strict || false, allowNetworkAccess: !options.strict, allowSystemModification: !options.strict, }) // Display validation results console.log(`\n${sym.list()} Validation Results:`) if (processedScript.validation.isValid) { console.log(chalk.green(`${sym.check()} Script validation passed`)) } else { console.log(chalk.red(`${sym.cross()} Script validation failed`)) } if (processedScript.validation.errors.length > 0) { console.log(chalk.red('\nErrors:')) processedScript.validation.errors.forEach((error) => { console.log(chalk.red(` • ${error}`)) }) } if (processedScript.validation.warnings.length > 0) { console.log(chalk.yellow('\nWarnings:')) processedScript.validation.warnings.forEach((warning) => { console.log(chalk.yellow(` • ${warning}`)) }) } // Fail if validation failed and not overridden if (!processedScript.validation.isValid && options.validate !== false) { console.error(chalk.red(`\n${sym.cross()} Cannot add command due to validation errors.`)) console.log(chalk.gray('Use --no-validate to skip validation (not recommended).')) return } // Display script processing info console.log(chalk.blue(`\n${sym.search()} Script Analysis:`)) console.log(chalk.gray(` Original Platform: ${processedScript.originalPlatform}`)) console.log(chalk.gray(` Script Type: ${processedScript.scriptType}`)) console.log( chalk.gray( ` Windows Version: ${processedScript.windows ? `${sym.check()} Generated` : `${sym.cross()} Not available`}`, ), ) console.log( chalk.gray(` Unix Version: ${processedScript.unix ? `${sym.check()} Generated` : `${sym.cross()} Not available`}`), ) console.log( chalk.gray( ` Cross-Platform: ${processedScript.crossPlatform ? `${sym.check()} Generated` : `${sym.cross()} Not available`}`, ), ) // Create the command object const command = processor.createCommand(name, processedScript, { platform: options.platform, }) // Save to database await db.addCommand(command) console.log(chalk.green(`${sym.check()} Added script "${name}"`)) // Show usage examples console.log(chalk.blue(`${sym.rocket()} Usage Examples:`)) console.log(chalk.cyan(` scripts ${name} # Run the script`)) console.log(chalk.cyan(` scripts ${name} arg1 arg2 # Run with arguments`)) console.log(chalk.cyan(` scripts view ${name} # View script details`)) console.log(chalk.cyan(` scripts remove ${name} # Remove this script`)) console.log(chalk.cyan(` scripts list # List all scripts`)) // Show platform compatibility info const compatibility = processor.validatePlatformCompatibility(command as any, process.platform) if (!compatibility.compatible || compatibility.warnings.length > 0) { console.log(chalk.yellow(`${sym.warning()} Platform Compatibility:`)) compatibility.warnings.forEach((warning) => { console.log(chalk.yellow(` • ${warning}`)) }) if (compatibility.recommendations.length > 0) { console.log(chalk.blue(`${sym.bulb()} Recommendations:`)) compatibility.recommendations.forEach((rec) => { console.log(chalk.blue(` • ${rec}`)) }) } } } catch (error: any) { console.error(chalk.red(`${sym.cross()} Failed to add command: ${error.message}`)) throw error } } // Update existing command - Production Ready Version async function updateCommand(name: string, scriptPath: string, options: any) { try { const existing = await db.getCommand(name) if (!existing) { console.error(chalk.red(`Script "${name}" not found.`)) return } console.log(chalk.blue(`${sym.package()} Updating script: ${name}`)) // Process the new script using the production-ready processor const processedScript = await processor.processScriptFile(scriptPath, { strict: options.strict || false, allowNetworkAccess: !options.strict, allowSystemModification: !options.strict, }) // Display validation results console.log(`\n${sym.list()} Validation Results:`) if (processedScript.validation.isValid) { console.log(chalk.green(`${sym.check()} Script validation passed`)) } else { console.log(chalk.red(`${sym.cross()} Script validation failed`)) } if (processedScript.validation.errors.length > 0) { console.log(chalk.red('\nErrors:')) processedScript.validation.errors.forEach((error) => { console.log(chalk.red(` • ${error}`)) }) } if (processedScript.validation.warnings.length > 0) { console.log(chalk.yellow('\nWarnings:')) processedScript.validation.warnings.forEach((warning) => { console.log(chalk.yellow(` • ${warning}`)) }) } // Fail if validation failed and not overridden if (!processedScript.validation.isValid && options.validate !== false) { console.error(chalk.red(`\n${sym.cross()} Cannot update command due to validation errors.`)) console.log(chalk.gray('Use --no-validate to skip validation (not recommended).')) return } // Display script processing info console.log(chalk.blue(`\n${sym.search()} Script Analysis:`)) console.log(chalk.gray(` Original Platform: ${processedScript.originalPlatform}`)) console.log(chalk.gray(` Script Type: ${processedScript.scriptType}`)) console.log( chalk.gray( ` Windows Version: ${processedScript.windows ? `${sym.check()} Generated` : `${sym.cross()} Not available`}`, ), ) console.log( chalk.gray(` Unix Version: ${processedScript.unix ? `${sym.check()} Generated` : `${sym.cross()} Not available`}`), ) console.log( chalk.gray( ` Cross-Platform: ${processedScript.crossPlatform ? `${sym.check()} Generated` : `${sym.cross()} Not available`}`, ), ) // Prepare updates with new multi-script fields const updates: Partial<ScaffoldCommand> = { script_original: processedScript.original, script_windows: processedScript.windows, script_unix: processedScript.unix, script_cross_platform: processedScript.crossPlatform, original_platform: processedScript.originalPlatform, script_type: processedScript.scriptType as any, } if (options.platform !== undefined) updates.platform = options.platform const success = await db.updateCommand(name, updates) if (success) { console.log(chalk.green(`\n${sym.check()} Updated script "${name}"`)) // Show usage examples console.log(chalk.blue(`\n${sym.rocket()} Usage Examples:`)) console.log(chalk.cyan(` scripts ${name} # Run the updated script`)) console.log(chalk.cyan(` scripts ${name} arg1 arg2 # Run with arguments`)) console.log(chalk.cyan(` scripts view ${name} # View script details`)) // Show platform compatibility info for updated command const updatedCommand = { ...existing, ...updates } as ScaffoldCommand const compatibility = processor.validatePlatformCompatibility( updatedCommand, process.platform, ) if (!compatibility.compatible || compatibility.warnings.length > 0) { console.log(chalk.yellow(`\n${sym.warning()} Platform Compatibility:`)) compatibility.warnings.forEach((warning) => { console.log(chalk.yellow(` • ${warning}`)) }) if (compatibility.recommendations.length > 0) { console.log(chalk.blue(`\n${sym.bulb()} Recommendations:`)) compatibility.recommendations.forEach((rec) => { console.log(chalk.blue(` • ${rec}`)) }) } } } else { console.error(chalk.red(`${sym.cross()} Failed to update command.`)) } } catch (error: any) { console.error(chalk.red(`${sym.cross()} Failed to update command: ${error.message}`)) throw error } } // Remove command async function removeCommand(name: string) { const success = await db.removeCommand(name) if (success) { console.log(chalk.green(`${sym.check()} Removed script "${name}"`)) } else { console.error(chalk.red(`Script "${name}" not found.`)) } } // List commands async function listCommands(detailed: boolean = false) { const commands = await db.listCommands() if (commands.length === 0) { console.log(chalk.gray('No scripts available.')) return } console.log(chalk.blue(`\n${sym.list()} Available Scripts:`)) commands.forEach((cmd) => { const alias = cmd.alias ? chalk.gray(` (${cmd.alias})`) : '' const platform = cmd.platform !== 'all' ? chalk.gray(` [${cmd.platform}]`) : '' if (detailed) { console.log(chalk.cyan(` • ${cmd.name}${alias}${platform}`)) if (cmd.description) { console.log(chalk.gray(` ${cmd.description}`)) } console.log(chalk.gray(` Updated: ${new Date(cmd.updatedAt).toLocaleDateString()}`)) } else { console.log(chalk.cyan(` • ${cmd.name}${alias}${platform}`)) } }) } // Display command details - Production Ready Version function displayCommandDetails(command: any) { console.log(chalk.blue(`\n${sym.page()} Command Details: ${command.name}`)) console.log(chalk.gray('═'.repeat(60))) // Basic info console.log(chalk.yellow('Type:'), chalk.cyan(command.type)) console.log(chalk.yellow('Name:'), chalk.cyan(command.name)) if (command.alias) { console.log(chalk.yellow('Alias:'), chalk.cyan(command.alias)) } if (command.description) { console.log(chalk.yellow('Description:'), command.description) } console.log(chalk.yellow('Platform Support:'), chalk.cyan(command.platform)) console.log(chalk.yellow('Created:'), new Date(command.createdAt).toLocaleString()) console.log(chalk.yellow('Updated:'), new Date(command.updatedAt).toLocaleString()) // Script metadata if (command.script_type) { console.log('\n' + chalk.blue(`${sym.list()} Script Information:`)) console.log(chalk.gray('─'.repeat(40))) console.log(chalk.yellow('Original Platform:'), chalk.cyan(command.original_platform)) console.log(chalk.yellow('Script Type:'), chalk.cyan(command.script_type)) } // Get version info using processor const versionInfo = processor.getScriptVersionInfo(command) console.log('\n' + chalk.blue(`${sym.page()} Script Versions:`)) console.log(chalk.gray('─'.repeat(40))) // Original script console.log( chalk.yellow( `\n${sym.diamond()} Original (${versionInfo.original.platform}, ${versionInfo.original.type}):`, ), ) console.log( chalk.gray( versionInfo.original.content.split('\n').slice(0, 5).join('\n') + (versionInfo.original.content.split('\n').length > 5 ? '\n...' : ''), ), ) // Windows version if (versionInfo.windows?.available) { console.log(chalk.yellow(`\n${sym.diamond()} Windows Version:`)) console.log( chalk.gray(versionInfo.windows.content.split('\n').slice(0, 3).join('\n') + '...'), ) } // Unix version if (versionInfo.unix?.available) { console.log(chalk.yellow(`\n${sym.diamond()} Unix Version:`)) console.log(chalk.gray(versionInfo.unix.content.split('\n').slice(0, 3).join('\n') + '...')) } // Cross-platform version if (versionInfo.crossPlatform?.available) { console.log(chalk.yellow(`\n${sym.diamond()} Cross-Platform Version:`)) console.log( chalk.gray(versionInfo.crossPlatform.content.split('\n').slice(0, 3).join('\n') + '...'), ) } // Best version for current platform console.log( '\n' + chalk.blue(`${sym.target()} Best for Current Platform (${versionInfo.bestForCurrent.version}):`), ) console.log(chalk.gray('─'.repeat(40))) console.log(versionInfo.bestForCurrent.content) // Platform compatibility const compatibility = processor.validatePlatformCompatibility(command, process.platform) if (!compatibility.compatible || compatibility.warnings.length > 0) { console.log('\n' + chalk.yellow(`${sym.warning()} Platform Compatibility:`)) console.log(chalk.gray('─'.repeat(40))) compatibility.warnings.forEach((warning) => { console.log(chalk.yellow(`• ${warning}`)) }) if (compatibility.recommendations.length > 0) { console.log('\n' + chalk.blue(`${sym.bulb()} Recommendations:`)) compatibility.recommendations.forEach((rec) => { console.log(chalk.blue(`• ${rec}`)) }) } } else { console.log('\n' + chalk.green(`${sym.check()} Fully compatible with current platform`)) } } // Export command async function exportCommand(directory: string) { const fs = await import('fs') const path = await import('path') console.log(chalk.blue(`${sym.package()} Exporting scripts to: ${directory}`)) // Create export directory if (!fs.existsSync(directory)) { fs.mkdirSync(directory, { recursive: true }) } // Get all commands const commands = await db.listCommands() if (commands.length === 0) { console.log(chalk.yellow('No scripts to export')) return } // Export as individual files let exported = 0 for (const command of commands) { const fileName = `${command.name}.sh` const filePath = path.join(directory, fileName) // Create a script file with metadata header const content = `#!/bin/bash # Scaffold Script: ${command.name} # Description: ${command.description || 'No description'} # Platform: ${command.platform} # Original Platform: ${command.original_platform} # Script Type: ${command.script_type} # Created: ${command.createdAt} # Updated: ${command.updatedAt} ${command.alias ? `# Alias: ${command.alias}` : ''} ${command.script_original}` fs.writeFileSync(filePath, content) // Make executable on Unix if (process.platform !== 'win32') { fs.chmodSync(filePath, '755') } exported++ } // Create README const readmeContent = `# Exported Scaffold Scripts Exported on: ${new Date().toISOString()} Total scripts: ${commands.length} ## Scripts: ${commands .map( (cmd) => `- **${cmd.name}** ${cmd.alias ? `(${cmd.alias})` : ''}: ${ cmd.description || 'No description' }`, ) .join('\n')} ## Usage: Each script is exported as an individual file. You can run them directly: \`\`\`bash ./${commands[0]?.name}.sh \`\`\` To recreate in scaffold-scripts (if you reinstall): \`\`\`bash scaffold add ${commands[0]?.name} ./${commands[0]?.name}.sh \`\`\` ` fs.writeFileSync(path.join(directory, 'README.md'), readmeContent) console.log(chalk.green(`${sym.check()} Exported ${exported} scripts as individual files`)) console.log(chalk.blue(`${sym.book()} See README.md in ${directory} for usage instructions`)) } // Helper function for user input async function askUser(question: string, defaultValue: string = 'n'): Promise<string> { return new Promise((resolve) => { console.log(chalk.yellow(question)) process.stdout.write('> ') process.stdin.once('data', (data) => { const response = data.toString().trim().toLowerCase() resolve(response || defaultValue) }) }) } // Uninstall command async function uninstallCommand() { const fs = await import('fs') const path = await import('path') const os = await import('os') const { exec } = await import('child_process') const { promisify } = await import('util') const execAsync = promisify(exec) console.log(chalk.blue(`${sym.trash()} Scaffold Scripts CLI Uninstaller`)) console.log(chalk.blue('====================================')) // Check if user has any scripts const commands = await db.listCommands() let exportDirectory: string | null = null let keepData = false if (commands.length > 0) { console.log(chalk.blue(`${sym.list()} You have ${commands.length} saved scripts:`)) commands.forEach((cmd) => { const alias = cmd.alias ? ` (${cmd.alias})` : '' console.log(chalk.cyan(` • ${cmd.name}${alias}`)) }) console.log('') // Ask about exporting const exportResponse = await askUser( 'Do you want to export your scripts before uninstalling? (y/N)', ) if (exportResponse === 'y' || exportResponse === 'yes') { const defaultDir = './my-scaffold-scripts' const dirResponse = await askUser(`Export directory (default: ${defaultDir}):`) exportDirectory = dirResponse.trim() || defaultDir console.log(chalk.blue(`${sym.package()} Exporting scripts to: ${exportDirectory}`)) await exportCommand(exportDirectory) console.log('') } else { // Ask about keeping data const keepResponse = await askUser('Keep your scripts in ~/.scaffold-scripts? (y/N)') keepData = keepResponse === 'y' || keepResponse === 'yes' } } console.log(chalk.blue(`${sym.trash()} Uninstalling Scaffold Scripts CLI...`)) try { // Uninstall package console.log(chalk.yellow(`${sym.package()} Removing scaffold-scripts package...`)) try { await execAsync('npm uninstall -g scaffold-scripts') console.log(chalk.green(`${sym.check()} Package removed`)) } catch (error) { console.log( chalk.yellow( `${sym.warning()} Could not remove via npm (this is normal if installed differently)`, ), ) } // Handle local data directory const dataDir = path.join(os.homedir(), '.scaffold-scripts') if (fs.existsSync(dataDir)) { if (!keepData && !exportDirectory) { console.log(chalk.yellow(`${sym.folder()} Removing local data directory...`)) fs.rmSync(dataDir, { recursive: true, force: true }) console.log(chalk.green(`${sym.check()} Local data directory removed`)) } else { console.log(chalk.blue(`${sym.info()} Local data preserved at: ` + dataDir)) } } else { console.log(chalk.blue(`${sym.info()} No local data directory found`)) } console.log(chalk.green(`${sym.party()} Uninstallation complete!`)) console.log('') console.log(chalk.blue(`${sym.memo()} Summary:`)) console.log(chalk.blue(` ${sym.check()} Removed scaffold command`)) if (exportDirectory) { console.log(chalk.blue(` ${sym.check()} Exported scripts to: ${exportDirectory}`)) } else if (keepData) { console.log(chalk.blue(` ${sym.info()} Scripts preserved in ~/.scaffold-scripts`)) } else if (commands.length > 0) { console.log(chalk.blue(` ${sym.check()} Removed local data directory`)) } console.log('') console.log(chalk.blue('Thank you for using Scaffold Scripts CLI!')) console.log(chalk.yellow('Please restart your terminal for changes to take effect')) process.exit(0) } catch (error: any) { console.error(chalk.red(`${sym.cross()} Error during uninstallation:`), error.message) console.log(chalk.yellow('You may need to remove manually or use the online uninstaller:')) console.log( chalk.blue( 'Unix: curl -fsSL https://raw.githubusercontent.com/ChrisColeTech/scaffold-scripts/main/uninstall.sh | bash', ), ) console.log( chalk.blue( 'Windows: irm https://raw.githubusercontent.com/ChrisColeTech/scaffold-scripts/main/uninstall.ps1 | iex', ), ) } } // Handle process termination process.on('SIGINT', () => { db.close() process.exit(0) }) process.on('SIGTERM', () => { db.close() process.exit(0) }) // Parse command line arguments with error handling try { program.parse() } catch (error: any) { // Handle Commander.js errors gracefully if (error.code === 'commander.missingArgument') { // Extract command name from argv or error let commandName = 'unknown'; if (process.argv.length > 2) { commandName = process.argv[2]; } if (error.command?.name()) { commandName = error.command.name(); } UsageHelper.displayError( `Missing required arguments`, `Use: scripts ${commandName} --help for usage information` ); UsageHelper.displayCommandHelp(commandName); process.exit(1); } else if (error.code === 'commander.unknownOption') { UsageHelper.displayError( `Unknown option: ${error.option}`, 'Use --help to see available options' ); process.exit(1); } else if (error.code === 'commander.unknownCommand') { UsageHelper.displayError( `Unknown command: ${error.command}`, 'Use --help to see available commands' ); process.exit(1); } else if (error.code === 'commander.version') { // Version command succeeded - just exit cleanly process.exit(0); } else if (error.code === 'commander.helpDisplayed' || error.message === '(outputHelp)') { // Help command succeeded - just exit cleanly process.exit(0); } else { // Only show error for actual errors if (error.message && error.message !== '(outputHelp)' && !error.message.match(/^\d+\.\d+\.\d+$/)) { UsageHelper.displayError(error.message, 'Check your command syntax and try again'); } process.exit(error.exitCode || 1); } }