UNPKG

empty-folder-manager

Version:

Interactive CLI tool for analyzing and removing empty folders from filesystem with .gitignore support and custom glob patterns

999 lines (834 loc) 33.6 kB
#!/usr/bin/env node const fs = require('fs').promises; const path = require('path'); const crypto = require('crypto'); const readline = require('readline'); const { spawn } = require('child_process'); // Configuration const CONFIG_FILE = '.empty-folders-config.json'; const ANALYSIS_DIR = '.analysis'; // Command line argument parsing const args = process.argv.slice(2); const hasUIFlag = args.includes('--ui') || args.includes('-u'); const hasHelpFlag = args.includes('--help') || args.includes('-h'); // Show help if requested if (hasHelpFlag) { console.log(` 🗂️ Empty Folder Manager - CLI Tool Usage: empty-folder-manager [options] node remove-empty-folders.js [options] Options: --ui, -u Launch web UI instead of CLI interface --help, -h Show this help message Examples: empty-folder-manager # Run interactive CLI empty-folder-manager --ui # Launch web UI empty-folder-manager -u # Launch web UI (short form) Troubleshooting: If you get "port already in use" errors, run: npm run kill-webui # Kill any orphaned web UI processes For more information, visit: https://github.com/your-username/empty-folder-manager `); process.exit(0); } const SPEED_PRESETS = { 'turbo': { name: '🚀 Turbo (Fastest)', sleepBetweenOps: 0, batchSize: 500, progressInterval: 100 }, 'fast': { name: '⚡ Fast', sleepBetweenOps: 10, batchSize: 200, progressInterval: 75 }, 'normal': { name: '🐢 Normal (Balanced)', sleepBetweenOps: 50, batchSize: 100, progressInterval: 50 }, 'gentle': { name: '🌱 Gentle (Disk-friendly)', sleepBetweenOps: 100, batchSize: 50, progressInterval: 25 }, 'ultra-gentle': { name: '🐌 Ultra Gentle (Very slow, very safe)', sleepBetweenOps: 250, batchSize: 25, progressInterval: 10 } }; // Function to launch web UI async function launchWebUI() { console.log('🚀 Launching Empty Folder Manager Web UI...'); console.log(); const webuiPath = path.join(__dirname, 'webui'); try { await fs.access(webuiPath); } catch (error) { console.log('❌ Web UI directory not found. Please ensure the webui directory exists.'); console.log('💡 You can download the complete package from the repository or run `git submodule update --init --recursive`.'); throw new Error('Web UI directory not found'); } const nodeModulesPath = path.join(webuiPath, 'node_modules'); try { await fs.access(nodeModulesPath); } catch (error) { console.log('📦 Web UI dependencies not found. Installing...'); console.log(); const installProcess = spawn('npm', ['install'], { cwd: webuiPath, stdio: 'inherit' }); const installResultCode = await new Promise((resolve, reject) => { installProcess.on('close', resolve); installProcess.on('error', reject); }); if (installResultCode !== 0) { throw new Error(`npm install for web UI failed with code ${installResultCode}`); } console.log(); console.log('✅ Web UI dependencies installed successfully!'); console.log(); } console.log('🌐 Starting web server...'); console.log(); return new Promise((resolve, reject) => { const serverProcess = spawn('npm', ['start'], { cwd: webuiPath, stdio: 'inherit', // Show server logs in the current terminal detached: false // Keep it attached }); serverProcess.on('close', (code) => { console.log(code === 0 || code === null ? '✅ Web server stopped.' : `❌ Web server exited with code ${code}.`); resolve({ exitCode: code === null ? 0 : code }); // SIGINT often results in null code }); serverProcess.on('error', (err) => { console.error('❌ Failed to start web server process:', err.message); console.log(`remove-empty-folders.js launchWebUI serverProcess error`,{message:err.message,stack:err.stack}); reject(err); }); }); } // Simple glob pattern matcher class GlobMatcher { constructor(pattern) { this.pattern = pattern; this.regex = this.globToRegex(pattern); } globToRegex(glob) { // Escape special regex characters except * and ? let regex = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&'); // Convert glob patterns to regex regex = regex.replace(/\*/g, '.*'); // * matches any characters regex = regex.replace(/\?/g, '.'); // ? matches single character // Handle directory patterns if (glob.endsWith('/')) { regex += '.*'; // Directory pattern matches everything inside } return new RegExp('^' + regex + '$'); } test(str) { return this.regex.test(str); } } // GitIgnore parser and matcher class GitIgnoreParser { constructor() { this.patterns = []; this.negativePatterns = []; } addPattern(pattern) { if (!pattern || pattern.startsWith('#')) return; pattern = pattern.trim(); if (!pattern) return; if (pattern.startsWith('!')) { // Negative pattern (don't ignore) this.negativePatterns.push(new GlobMatcher(pattern.slice(1))); } else { // Positive pattern (ignore) this.patterns.push(new GlobMatcher(pattern)); } } async loadFromFile(filePath) { try { const content = await fs.readFile(filePath, 'utf8'); const lines = content.split('\n'); for (const line of lines) { this.addPattern(line); } return true; } catch (error) { return false; } } shouldIgnore(relativePath) { // Check if any positive pattern matches const shouldIgnore = this.patterns.some(pattern => pattern.test(relativePath)); if (!shouldIgnore) return false; // Check if any negative pattern matches (overrides ignore) const shouldNotIgnore = this.negativePatterns.some(pattern => pattern.test(relativePath)); return !shouldNotIgnore; } } class EmptyFolderCLI { constructor() { this.config = {}; this.rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // Default speed settings this.speedSettings = SPEED_PRESETS.normal; // GitIgnore support this.gitIgnore = new GitIgnoreParser(); this.useGitIgnore = true; this.customPatterns = []; } // Utility methods async sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } generatePathHash(fullPath) { return crypto.createHash('sha256').update(fullPath).digest('hex').substring(0, 16); } async ensureAnalysisDir() { try { await fs.mkdir(ANALYSIS_DIR, { recursive: true }); } catch (error) { // Directory might already exist } } async loadConfig() { try { const configData = await fs.readFile(CONFIG_FILE, 'utf8'); this.config = JSON.parse(configData); // Load speed settings if saved if (this.config.speedPreset && SPEED_PRESETS[this.config.speedPreset]) { this.speedSettings = SPEED_PRESETS[this.config.speedPreset]; } // Load filtering settings if (this.config.useGitIgnore !== undefined) { this.useGitIgnore = this.config.useGitIgnore; } if (this.config.customPatterns) { this.customPatterns = this.config.customPatterns; } } catch (error) { this.config = {}; } } async saveConfig() { // Save filtering settings this.config.useGitIgnore = this.useGitIgnore; this.config.customPatterns = this.customPatterns; await fs.writeFile(CONFIG_FILE, JSON.stringify(this.config, null, 2)); } async question(prompt) { return new Promise((resolve) => { this.rl.question(prompt, resolve); }); } // Display methods displayHeader() { console.clear(); console.log('╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🗂️ Empty Folder Manager 🗂️ ║'); console.log('║ ║'); console.log('║ Clean up your filesystem efficiently! 🚀 ║'); console.log('╚══════════════════════════════════════════════════════════════╝'); console.log(); } displayMenu() { console.log('📋 Available Options:'); console.log(); console.log(' 1️⃣ Configure Path - Set the directory to analyze'); console.log(' 2️⃣ Configure Speed - Adjust processing speed'); console.log(' 3️⃣ Configure Filters - Set up .gitignore and custom patterns'); console.log(' 4️⃣ Run Analysis - Find all empty folders'); console.log(' 5️⃣ Show Stats - Display analysis results'); console.log(' 6️⃣ Remove Folders - Clean up empty folders'); console.log(' 7️⃣ Launch Web UI - Open web interface'); console.log(' 8️⃣ Exit - Quit the application'); console.log(); if (this.config.currentPath) { console.log(`📁 Current Path: ${this.config.currentPath}`); } console.log(`⚡ Current Speed: ${this.speedSettings.name}`); console.log(`🔍 GitIgnore: ${this.useGitIgnore ? '✅ Enabled' : '❌ Disabled'}`); console.log(`🎯 Custom Patterns: ${this.customPatterns.length} pattern(s)`); console.log(); } async displayStats(analysisData) { if (!analysisData || !analysisData.emptyFolders) { console.log('❌ No analysis data found. Please run analysis first.'); return; } const totalFolders = analysisData.emptyFolders.length; const analysisDate = new Date(analysisData.timestamp).toLocaleString(); console.log('📊 Empty Folder Analysis Results'); console.log('═'.repeat(50)); console.log(); console.log(`🗂️ Total Empty Folders: ${totalFolders}`); console.log(`📅 Analysis Date: ${analysisDate}`); console.log(`📁 Analyzed Path: ${analysisData.basePath}`); if (analysisData.filteringEnabled) { console.log(`🔍 Filtering: ${analysisData.filteringEnabled ? 'Enabled' : 'Disabled'}`); if (analysisData.ignoredCount) { console.log(`🚫 Ignored Folders: ${analysisData.ignoredCount}`); } } console.log(); if (totalFolders > 0) { console.log('📋 Sample Empty Folders:'); const sampleSize = Math.min(10, totalFolders); for (let i = 0; i < sampleSize; i++) { console.log(` ${i + 1}. ${analysisData.emptyFolders[i]}`); } if (totalFolders > sampleSize) { console.log(` ... and ${totalFolders - sampleSize} more folders`); } } console.log(); } // Core functionality async configurePath() { console.log('🔧 Configure Analysis Path'); console.log('═'.repeat(30)); console.log(); const currentPath = this.config.currentPath || ''; if (currentPath) { console.log(`Current path: ${currentPath}`); console.log(); } const newPath = await this.question('Enter the full path to analyze: '); if (!newPath.trim()) { console.log('❌ Path cannot be empty!'); await this.question('Press Enter to continue...'); return; } try { const stats = await fs.stat(newPath); if (!stats.isDirectory()) { console.log('❌ Path must be a directory!'); await this.question('Press Enter to continue...'); return; } this.config.currentPath = path.resolve(newPath); await this.saveConfig(); console.log('✅ Path configured successfully!'); console.log(`📁 Set to: ${this.config.currentPath}`); } catch (error) { console.log(`❌ Error accessing path: ${error.message}`); } await this.question('Press Enter to continue...'); } async configureSpeed() { console.log('⚡ Configure Processing Speed'); console.log('═'.repeat(35)); console.log(); console.log('Choose a speed preset:'); console.log(); const presetKeys = Object.keys(SPEED_PRESETS); presetKeys.forEach((key, index) => { const preset = SPEED_PRESETS[key]; const current = this.speedSettings === preset ? ' (current)' : ''; console.log(` ${index + 1}️⃣ ${preset.name}${current}`); console.log(` Delay: ${preset.sleepBetweenOps}ms, Batch: ${preset.batchSize}, Progress: every ${preset.progressInterval}`); console.log(); }); const choice = await this.question(`Select speed preset (1-${presetKeys.length}): `); const choiceIndex = parseInt(choice) - 1; if (choiceIndex >= 0 && choiceIndex < presetKeys.length) { const selectedKey = presetKeys[choiceIndex]; this.speedSettings = SPEED_PRESETS[selectedKey]; this.config.speedPreset = selectedKey; await this.saveConfig(); console.log(`✅ Speed configured to: ${this.speedSettings.name}`); console.log(` Delay between operations: ${this.speedSettings.sleepBetweenOps}ms`); console.log(` Batch size: ${this.speedSettings.batchSize} folders`); console.log(` Progress updates: every ${this.speedSettings.progressInterval} folders`); } else { console.log('❌ Invalid choice!'); } await this.question('Press Enter to continue...'); } async configureFilters() { console.log('🎯 Configure Filtering Options'); console.log('═'.repeat(35)); console.log(); while (true) { console.log('Current Filter Settings:'); console.log(`🔍 GitIgnore Support: ${this.useGitIgnore ? '✅ Enabled' : '❌ Disabled'}`); console.log(`🎯 Custom Patterns: ${this.customPatterns.length} pattern(s)`); if (this.customPatterns.length > 0) { console.log(' Current patterns:'); this.customPatterns.forEach((pattern, index) => { console.log(` ${index + 1}. ${pattern}`); }); } console.log(); console.log('Filter Options:'); console.log(' 1️⃣ Toggle GitIgnore support'); console.log(' 2️⃣ Add custom ignore pattern'); console.log(' 3️⃣ Remove custom pattern'); console.log(' 4️⃣ Clear all custom patterns'); console.log(' 5️⃣ Test pattern against path'); console.log(' 6️⃣ Back to main menu'); console.log(); const choice = await this.question('Select option (1-6): '); switch (choice.trim()) { case '1': this.useGitIgnore = !this.useGitIgnore; console.log(`✅ GitIgnore support ${this.useGitIgnore ? 'enabled' : 'disabled'}`); await this.saveConfig(); break; case '2': const pattern = await this.question('Enter ignore pattern (e.g., node_modules/, *.log, .git/): '); if (pattern.trim()) { this.customPatterns.push(pattern.trim()); console.log(`✅ Added pattern: ${pattern.trim()}`); await this.saveConfig(); } break; case '3': if (this.customPatterns.length === 0) { console.log('❌ No custom patterns to remove'); } else { console.log('Select pattern to remove:'); this.customPatterns.forEach((p, i) => { console.log(` ${i + 1}. ${p}`); }); const removeChoice = await this.question(`Enter number (1-${this.customPatterns.length}): `); const removeIndex = parseInt(removeChoice) - 1; if (removeIndex >= 0 && removeIndex < this.customPatterns.length) { const removed = this.customPatterns.splice(removeIndex, 1)[0]; console.log(`✅ Removed pattern: ${removed}`); await this.saveConfig(); } else { console.log('❌ Invalid choice'); } } break; case '4': if (this.customPatterns.length > 0) { this.customPatterns = []; console.log('✅ Cleared all custom patterns'); await this.saveConfig(); } else { console.log('❌ No custom patterns to clear'); } break; case '5': const testPath = await this.question('Enter path to test: '); if (testPath.trim()) { const shouldIgnore = await this.shouldIgnorePath(testPath.trim(), '/test/base'); console.log(`Result: ${shouldIgnore ? '🚫 IGNORED' : '✅ INCLUDED'}`); } break; case '6': return; default: console.log('❌ Invalid option. Please select 1-6.'); } console.log(); await this.question('Press Enter to continue...'); console.log(); } } async loadGitIgnoreFiles(basePath) { this.gitIgnore = new GitIgnoreParser(); if (!this.useGitIgnore) return; // Load .gitignore files from current directory up to basePath let currentDir = basePath; const loadedFiles = []; while (true) { const gitIgnorePath = path.join(currentDir, '.gitignore'); const loaded = await this.gitIgnore.loadFromFile(gitIgnorePath); if (loaded) { loadedFiles.push(gitIgnorePath); } const parentDir = path.dirname(currentDir); if (parentDir === currentDir) break; // Reached root currentDir = parentDir; } // Add custom patterns for (const pattern of this.customPatterns) { this.gitIgnore.addPattern(pattern); } return loadedFiles; } async shouldIgnorePath(fullPath, basePath) { if (!this.useGitIgnore && this.customPatterns.length === 0) { return false; } // Get relative path from base const relativePath = path.relative(basePath, fullPath); // Always ignore some common patterns const commonIgnorePatterns = [ '.git', '.svn', '.hg', 'node_modules', '.DS_Store', 'Thumbs.db' ]; // Check common patterns first for (const pattern of commonIgnorePatterns) { if (relativePath.includes(pattern)) { return true; } } // Check gitignore patterns return this.gitIgnore.shouldIgnore(relativePath); } async isDirectoryEmpty(dirPath) { try { const entries = await fs.readdir(dirPath); return entries.length === 0; } catch (error) { return false; // If we can't read it, assume it's not empty } } async findEmptyFolders(basePath, emptyFolders = [], processedCount = { value: 0 }, ignoredCount = { value: 0 }) { try { const entries = await fs.readdir(basePath, { withFileTypes: true }); const directories = entries.filter(entry => entry.isDirectory()); // Process directories in batches based on speed settings for (let i = 0; i < directories.length; i += this.speedSettings.batchSize) { const batch = directories.slice(i, i + this.speedSettings.batchSize); for (const dir of batch) { const fullPath = path.join(basePath, dir.name); processedCount.value++; // Show progress based on speed settings if (processedCount.value % this.speedSettings.progressInterval === 0) { process.stdout.write(`\r🔍 Processed ${processedCount.value} folders (ignored ${ignoredCount.value})...`); } try { // Check if this path should be ignored const shouldIgnore = await this.shouldIgnorePath(fullPath, this.config.currentPath); if (shouldIgnore) { ignoredCount.value++; continue; // Skip this directory and its subdirectories } // Recursively check subdirectories first await this.findEmptyFolders(fullPath, emptyFolders, processedCount, ignoredCount); // Then check if this directory is empty if (await this.isDirectoryEmpty(fullPath)) { emptyFolders.push(fullPath); } // Delay based on speed settings if (this.speedSettings.sleepBetweenOps > 0) { await this.sleep(this.speedSettings.sleepBetweenOps); } } catch (error) { // Skip directories we can't access continue; } } } } catch (error) { // Skip directories we can't read } return emptyFolders; } async runAnalysis() { if (!this.config.currentPath) { console.log('❌ Please configure a path first!'); await this.question('Press Enter to continue...'); return; } console.log('🔍 Running Empty Folder Analysis'); console.log('═'.repeat(35)); console.log(); console.log(`📁 Analyzing: ${this.config.currentPath}`); console.log(`⚡ Speed: ${this.speedSettings.name}`); console.log(`🔍 GitIgnore: ${this.useGitIgnore ? 'Enabled' : 'Disabled'}`); console.log(`🎯 Custom Patterns: ${this.customPatterns.length}`); console.log('⏳ This may take a while for large directories...'); console.log(); // Load gitignore files const loadedGitIgnoreFiles = await this.loadGitIgnoreFiles(this.config.currentPath); if (loadedGitIgnoreFiles.length > 0) { console.log(`📄 Loaded .gitignore files: ${loadedGitIgnoreFiles.length}`); loadedGitIgnoreFiles.forEach(file => { console.log(` - ${file}`); }); console.log(); } const startTime = Date.now(); const emptyFolders = []; const processedCount = { value: 0 }; const ignoredCount = { value: 0 }; try { await this.findEmptyFolders(this.config.currentPath, emptyFolders, processedCount, ignoredCount); const endTime = Date.now(); const duration = ((endTime - startTime) / 1000).toFixed(2); console.log(`\r✅ Analysis complete! Processed ${processedCount.value} folders (ignored ${ignoredCount.value}) in ${duration}s`); console.log(); // Save analysis results await this.ensureAnalysisDir(); const pathHash = this.generatePathHash(this.config.currentPath); const analysisFile = path.join(ANALYSIS_DIR, `analysis-${pathHash}.json`); const analysisData = { basePath: this.config.currentPath, timestamp: Date.now(), emptyFolders: emptyFolders.sort(), totalProcessed: processedCount.value, ignoredCount: ignoredCount.value, duration: duration, speedSettings: this.speedSettings.name, filteringEnabled: this.useGitIgnore || this.customPatterns.length > 0, gitIgnoreEnabled: this.useGitIgnore, customPatterns: [...this.customPatterns], loadedGitIgnoreFiles: loadedGitIgnoreFiles || [] }; await fs.writeFile(analysisFile, JSON.stringify(analysisData, null, 2)); console.log(`📊 Found ${emptyFolders.length} empty folders`); console.log(`🚫 Ignored ${ignoredCount.value} folders due to filtering`); console.log(`💾 Results saved to: ${analysisFile}`); } catch (error) { console.log(`❌ Analysis failed: ${error.message}`); } await this.question('Press Enter to continue...'); } async showStats() { if (!this.config.currentPath) { console.log('❌ Please configure a path first!'); await this.question('Press Enter to continue...'); return; } const pathHash = this.generatePathHash(this.config.currentPath); const analysisFile = path.join(ANALYSIS_DIR, `analysis-${pathHash}.json`); try { const analysisData = JSON.parse(await fs.readFile(analysisFile, 'utf8')); await this.displayStats(analysisData); if (analysisData.speedSettings) { console.log(`⚡ Analysis was run with: ${analysisData.speedSettings}`); } if (analysisData.loadedGitIgnoreFiles && analysisData.loadedGitIgnoreFiles.length > 0) { console.log(`📄 GitIgnore files used: ${analysisData.loadedGitIgnoreFiles.length}`); } console.log(); } catch (error) { console.log('❌ No analysis data found for current path.'); console.log('💡 Please run analysis first!'); console.log(); } await this.question('Press Enter to continue...'); } async removeFolders() { if (!this.config.currentPath) { console.log('❌ Please configure a path first!'); await this.question('Press Enter to continue...'); return; } const pathHash = this.generatePathHash(this.config.currentPath); const analysisFile = path.join(ANALYSIS_DIR, `analysis-${pathHash}.json`); let analysisData; try { analysisData = JSON.parse(await fs.readFile(analysisFile, 'utf8')); } catch (error) { console.log('❌ No analysis data found for current path.'); console.log('💡 Please run analysis first!'); await this.question('Press Enter to continue...'); return; } if (!analysisData.emptyFolders || analysisData.emptyFolders.length === 0) { console.log('🎉 No empty folders found! Your directory is already clean.'); await this.question('Press Enter to continue...'); return; } console.log('🗑️ Remove Empty Folders'); console.log('═'.repeat(25)); console.log(); await this.displayStats(analysisData); // Ask for dry run or actual removal const dryRunChoice = await this.question('Run in dry-run mode? (y/n): '); const isDryRun = dryRunChoice.toLowerCase().startsWith('y'); if (!isDryRun) { console.log('⚠️ WARNING: This will permanently delete empty folders!'); const confirm = await this.question('Are you sure you want to proceed? (yes/no): '); if (confirm.toLowerCase() !== 'yes') { console.log('❌ Operation cancelled.'); await this.question('Press Enter to continue...'); return; } } console.log(); console.log(isDryRun ? '🔍 DRY RUN - No folders will be deleted' : '🗑️ REMOVING FOLDERS'); console.log(`⚡ Speed: ${this.speedSettings.name}`); console.log(); let removedCount = 0; let errorCount = 0; const startTime = Date.now(); for (let i = 0; i < analysisData.emptyFolders.length; i++) { const folderPath = analysisData.emptyFolders[i]; // Show progress based on speed settings if (i % Math.max(1, Math.floor(this.speedSettings.progressInterval / 5)) === 0) { process.stdout.write(`\r${isDryRun ? '🔍' : '🗑️'} Processing ${i + 1}/${analysisData.emptyFolders.length}...`); } try { // Check if folder still exists and is still empty const exists = await fs.access(folderPath).then(() => true).catch(() => false); if (!exists) continue; const isEmpty = await this.isDirectoryEmpty(folderPath); if (!isEmpty) continue; if (!isDryRun) { await fs.rmdir(folderPath); } removedCount++; // Delay based on speed settings if (this.speedSettings.sleepBetweenOps > 0) { await this.sleep(this.speedSettings.sleepBetweenOps); } } catch (error) { errorCount++; } } const endTime = Date.now(); const duration = ((endTime - startTime) / 1000).toFixed(2); console.log(`\r✅ Operation complete in ${duration}s`); console.log(); if (isDryRun) { console.log(`🔍 DRY RUN RESULTS:`); console.log(` 📁 Would remove: ${removedCount} folders`); console.log(` ❌ Errors: ${errorCount}`); } else { console.log(`🗑️ REMOVAL RESULTS:`); console.log(` ✅ Removed: ${removedCount} folders`); console.log(` ❌ Errors: ${errorCount}`); console.log(` 🎉 Your filesystem is now cleaner!`); } await this.question('Press Enter to continue...'); } async run() { await this.loadConfig(); while (true) { this.displayHeader(); this.displayMenu(); const choice = await this.question('Select an option (1-8): '); switch (choice.trim()) { case '1': await this.configurePath(); break; case '2': await this.configureSpeed(); break; case '3': await this.configureFilters(); break; case '4': await this.runAnalysis(); break; case '5': await this.showStats(); break; case '6': await this.removeFolders(); break; case '7': await this.launchWebUIFromMenu(); // CLI will wait here, then loop continues break; case '8': console.log('👋 Goodbye! Thanks for using Empty Folder Manager!'); this.rl.close(); return; default: console.log('❌ Invalid option. Please select 1-8.'); await this.question('Press Enter to continue...'); } } } async launchWebUIFromMenu() { console.log('🚀 Launching Web UI...'); console.log(); console.log('💡 The web interface will run in the foreground of this terminal session.'); console.log('💡 Close the web server (e.g., by pressing Ctrl+C in its terminal output) to return to this CLI menu.'); console.log(); const confirm = await this.question('Do you want to launch the web UI? (y/n): '); if (confirm.toLowerCase().startsWith('y')) { console.log('🌐 Starting web interface...'); console.log('📝 Note: This CLI will wait for the Web UI to close.'); console.log(); const tempSigintHandler = () => { // This handler is for the main CLI process. // When Ctrl+C is pressed, the child server process also gets SIGINT. // The child should handle it and exit, which will resolve/reject the launchWebUI() promise. // This handler simply prevents the main CLI from exiting due to this SIGINT. console.log('\nℹ️ SIGINT received by CLI. Web server is expected to close. Once it has, you will be prompted to continue.'); }; process.on('SIGINT', tempSigintHandler); try { await launchWebUI(); // This waits for the server to stop console.log('✅ Web UI session finished.'); } catch (error) { console.error('❌ Error during Web UI session:', error.message); console.log(`remove-empty-folders.js launchWebUIFromMenu error`,{message:err.message,stack:err.stack}); console.log('ℹ️ An error occurred with the Web UI.'); } finally { process.removeListener('SIGINT', tempSigintHandler); } // Prompt user before returning to menu, only if readline is still open. if (!this.rl.closed) { try { await this.question('Press Enter to return to the menu...'); } catch (e) { // If readline.question errors (e.g., if rl was closed concurrently by SIGINT effects) console.log('\nℹ️ CLI prompt was interrupted. Returning to main loop or exiting.'); } } else { console.log('\nℹ️ Readline interface was closed. CLI session will end.'); } } else { console.log('❌ Web UI launch cancelled.'); await this.question('Press Enter to continue...'); } // No return value needed, CLI loop will continue } } // Main execution block if (require.main === module) { if (hasHelpFlag) { // Help message is shown at the top and process.exit(0) is called there. // This block is effectively handled earlier. } else if (hasUIFlag) { (async () => { try { // Process SIGINT to allow graceful shutdown if user Ctrl+C the main script process.on('SIGINT', () => { console.log('\n👋 SIGINT received in --ui mode. Web server should be shutting down.'); // The web server (child process) should handle SIGINT and exit gracefully. // launchWebUI() promise will resolve/reject accordingly. }); const { exitCode } = await launchWebUI(); console.log(`remove-empty-folders.js --ui mode: Web UI exited with code ${exitCode}. Script will now exit.`); process.exit(exitCode); } catch (error) { console.error('❌ Failed to launch or run web UI in --ui mode:', error.message); console.log(`remove-empty-folders.js --ui mode error`,{message:err.message,stack:err.stack}); process.exit(1); } })(); } else { // Run the interactive CLI const cli = new EmptyFolderCLI(); cli.run().catch(error => { // Check if the error is due to readline being closed, which might happen after SIGINT if (error.message && error.message.includes('readline was closed')) { console.log('\n👋 CLI session terminated.'); } else { console.error('❌ Fatal error in CLI:', error.message); console.log(`remove-empty-folders.js cli.run error`, { message: error.message, stack: error.stack }); } process.exit(1); }); } } module.exports = EmptyFolderCLI;