UNPKG

@kadi.build/local-remote-file-manager-ability

Version:

Local & Remote File Management System with S3-compatible container registry, HTTP server provider, file streaming, comprehensive tunnel services (ngrok, serveo, localtunnel, localhost.run, pinggy), and comprehensive testing suite

1,325 lines (1,098 loc) • 92.2 kB
#!/usr/bin/env node import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; import path from 'path'; import { promisify } from 'util'; import { glob } from 'glob'; import readline from 'readline'; import fs from 'fs-extra'; import { LocalRemoteManager } from './src/localRemoteManager.js'; import { ConfigManager } from './src/configManager.js'; import { S3HttpServer } from './src/s3Server.js'; class LocalRemoteCLI { constructor() { this.program = new Command(); this.config = null; this.manager = null; this.activeWatchers = new Map(); // Track active watchers for CLI session this.activeServers = new Map(); // Track active S3 servers for CLI session this.setupCommands(); } async initialize() { try { this.config = new ConfigManager(); await this.config.load(); this.manager = new LocalRemoteManager(this.config); // Set up watch event handling this.setupWatchEventHandling(); } catch (error) { console.error(chalk.red(`āŒ Initialization failed: ${error.message}`)); process.exit(1); } } setupWatchEventHandling() { if (!this.manager) return; this.manager.on('fileEvent', (eventData) => { const icon = this.getEventIcon(eventData.type); const timestamp = new Date(eventData.timestamp).toLocaleTimeString(); const relativePath = eventData.relativePath || path.basename(eventData.path); console.log(chalk.cyan(`[${timestamp}] ${icon} ${eventData.type}: ${relativePath}`)); }); this.manager.on('watcherError', (errorData) => { console.log(chalk.red(`āŒ Watcher error on ${errorData.path}: ${errorData.error}`)); }); } getEventIcon(eventType) { const icons = { add: 'šŸ“„', change: 'āœļø', unlink: 'šŸ—‘ļø', addDir: 'šŸ“', unlinkDir: 'šŸ—‚ļø' }; return icons[eventType] || 'šŸ“‹'; } setupCommands() { this.program .name('local-remote-manager') .description('Local & Remote File Management System') .version('1.0.0'); // Connection and system commands this.addTestCommand(); this.addInfoCommand(); this.addValidateCommand(); // File operations this.addUploadCommand(); this.addDownloadCommand(); this.addListCommand(); this.addDeleteCommand(); this.addCopyCommand(); this.addMoveCommand(); this.addRenameCommand(); // Folder operations this.addMkdirCommand(); this.addRmdirCommand(); this.addListFoldersCommand(); // Search operations this.addSearchCommand(); // File watching commands (Bucket 2) this.addWatchCommand(); this.addWatchListCommand(); this.addWatchStopCommand(); this.addWatchStatusCommand(); // Compression commands (Bucket 3) this.addCompressCommand(); this.addDecompressCommand(); this.addCompressBatchCommand(); this.addDecompressBatchCommand(); this.addCompressionStatusCommand(); // Tunneling commands (Bucket 4) this.addShareCommand(); this.addShareCommand(); this.addTunnelStatusCommand(); this.addCleanupCommand(); // File Server commands (Bucket 5 - Phase 5: CLI Integration) this.addServeS3Command(); this.addServerStopCommand(); this.addServerStatusCommand(); this.addServerCleanupCommand(); } // ============================================================================ // CONNECTION AND SYSTEM COMMANDS // ============================================================================ addTestCommand() { this.program .command('test') .description('Test connection to providers') .option('-p, --provider <name>', 'Provider to test (local, watch, compression, tunnel)', 'local') .action(async (options) => { await this.initialize(); const spinner = ora(`Testing ${options.provider} provider connection...`).start(); try { const result = await this.manager.testConnection(options.provider); spinner.succeed(`Connection test successful!`); console.log(chalk.cyan('\nšŸ“Š Connection Details:')); console.log(` Provider: ${result.provider}`); if (result.provider === 'local') { console.log(` Local Root: ${result.localRoot}`); console.log(` Accessible: ${result.accessible ? 'āœ…' : 'āŒ'}`); console.log(` Writable: ${result.writable ? 'āœ…' : 'āŒ'}`); console.log(` Total Size: ${this.formatBytes(result.totalSize)}`); console.log(` Free Space: ${this.formatBytes(result.freeSpace)}`); } else if (result.provider === 'watch') { console.log(` Local Root: ${result.localRoot}`); console.log(` Enabled: ${result.enabled ? 'āœ…' : 'āŒ'}`); console.log(` Recursive: ${result.recursive ? 'āœ…' : 'āŒ'}`); console.log(` Active Watchers: ${result.activeWatchers}`); console.log(` Max Watchers: ${result.maxWatchers}`); console.log(` Debounce: ${result.debounceMs}ms`); } } catch (error) { spinner.fail(`Connection test failed: ${error.message}`); process.exit(1); } }); } addInfoCommand() { this.program .command('info') .description('Display system and configuration information') .action(async () => { await this.initialize(); const spinner = ora('Gathering system information...').start(); try { const systemInfo = this.manager.getSystemInfo(); const usageStats = await this.manager.getUsageStats(); spinner.succeed('System information gathered'); console.log(chalk.cyan('\nšŸ–„ļø System Information:')); console.log(` Platform: ${systemInfo.platform}`); console.log(` Architecture: ${systemInfo.arch}`); console.log(` Node Version: ${systemInfo.nodeVersion}`); console.log(` Available Providers: ${systemInfo.availableProviders.join(', ')}`); console.log(chalk.cyan('\nāš™ļø Configuration:')); console.log(` Local Root: ${systemInfo.config.local.localRoot}`); console.log(` Max File Size: ${this.formatBytes(systemInfo.config.local.maxFileSize)}`); console.log(` Chunk Size: ${this.formatBytes(systemInfo.config.local.chunkSize)}`); console.log(` Max Concurrent Operations: ${systemInfo.config.performance.maxConcurrentOperations}`); console.log(chalk.cyan('\nšŸ‘ļø Watch Configuration:')); console.log(` Enabled: ${systemInfo.config.watch.enabled ? 'āœ…' : 'āŒ'}`); console.log(` Recursive: ${systemInfo.config.watch.recursive ? 'āœ…' : 'āŒ'}`); console.log(` Ignore Dotfiles: ${systemInfo.config.watch.ignoreDotfiles ? 'āœ…' : 'āŒ'}`); console.log(` Debounce: ${systemInfo.config.watch.debounceMs}ms`); console.log(` Max Watchers: ${systemInfo.config.watch.maxWatchers}`); console.log(` Events: ${systemInfo.config.watch.events.join(', ')}`); console.log(chalk.cyan('\nšŸ“ Directory Usage:')); Object.entries(usageStats).forEach(([key, info]) => { if (key === 'watchStatus') { console.log(` Watch Status:`); console.log(` Active Watchers: ${info.activeWatchers}`); console.log(` Total Events: ${info.totalEvents}`); console.log(` Pending Debounces: ${info.pendingDebounces}`); } else { const status = info.exists ? 'āœ…' : 'āŒ'; console.log(` ${key}: ${status} ${info.path}`); if (info.exists) { console.log(` Files: ${info.fileCount}, Size: ${this.formatBytes(info.totalSize)}`); } } }); } catch (error) { spinner.fail(`Failed to gather system information: ${error.message}`); process.exit(1); } }); } addValidateCommand() { this.program .command('validate') .description('Validate configuration') .option('-p, --provider <name>', 'Provider to validate (local, watch)', 'local') .action(async (options) => { await this.initialize(); const spinner = ora(`Validating ${options.provider} configuration...`).start(); try { const result = await this.manager.validateProvider(options.provider); if (result.isValid) { spinner.succeed('Configuration is valid'); if (result.warnings && result.warnings.length > 0) { console.log(chalk.yellow('\nāš ļø Warnings:')); result.warnings.forEach(warning => { console.log(chalk.yellow(` • ${warning}`)); }); } } else { spinner.fail('Configuration is invalid'); console.log(chalk.red('\nāŒ Errors:')); result.errors.forEach(error => { console.log(chalk.red(` • ${error}`)); }); process.exit(1); } } catch (error) { spinner.fail(`Validation failed: ${error.message}`); process.exit(1); } }); } // ============================================================================ // FILE WATCHING COMMANDS (Bucket 2) // ============================================================================ addWatchCommand() { this.program .command('watch') .description('Start watching a directory or file for changes') .option('-d, --directory <path>', 'Directory or file to watch', './') .option('-r, --recursive', 'Watch recursively (default: true)') .option('--no-recursive', 'Disable recursive watching') .option('-i, --ignore-dotfiles', 'Ignore dotfiles (default: true)') .option('--no-ignore-dotfiles', 'Include dotfiles in watching') .option('-e, --events <events>', 'Comma-separated list of events to watch', 'add,change,unlink,addDir,unlinkDir') .option('-v, --verbose', 'Show detailed event information') .action(async (options) => { await this.initialize(); const watchPath = path.resolve(options.directory); console.log(chalk.cyan(`šŸ‘ļø Starting to watch: ${watchPath}`)); console.log(chalk.gray('Press Ctrl+C to stop watching\n')); try { const watchOptions = { recursive: options.recursive, ignoreDotfiles: options.ignoreDotfiles, events: options.events.split(',').map(e => e.trim()) }; const result = await this.manager.startWatching(watchPath, watchOptions); console.log(chalk.green(`āœ… Started watching (ID: ${result.watchId})`)); console.log(` Path: ${result.path}`); console.log(` Recursive: ${result.recursive ? 'āœ…' : 'āŒ'}`); console.log(` Events: ${result.events.join(', ')}`); console.log(` Started: ${result.startedAt}\n`); // Store watcher for cleanup this.activeWatchers.set(result.watchId, result); if (options.verbose) { console.log(chalk.cyan('šŸ“‹ Watching for file system events (verbose mode):')); } else { console.log(chalk.cyan('šŸ“‹ Watching for file system events:')); } // Set up graceful shutdown const cleanup = async () => { console.log(chalk.yellow('\n\nšŸ›‘ Stopping watcher...')); try { await this.manager.stopWatching(result.watchId); console.log(chalk.green('āœ… Watcher stopped successfully')); } catch (error) { console.log(chalk.red(`āŒ Error stopping watcher: ${error.message}`)); } process.exit(0); }; process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); // Keep the process alive await new Promise(() => { }); // Infinite promise } catch (error) { console.error(chalk.red(`āŒ Failed to start watching: ${error.message}`)); process.exit(1); } }); } addWatchListCommand() { this.program .command('watch-list') .alias('wls') .description('List active watchers') .option('-v, --verbose', 'Show detailed watcher information') .action(async (options) => { await this.initialize(); try { const watchers = this.manager.listActiveWatchers(); if (watchers.length === 0) { console.log(chalk.yellow('šŸ‘ļø No active watchers')); return; } console.log(chalk.cyan(`\nšŸ‘ļø Active Watchers (${watchers.length}):`)); watchers.forEach((watcher, index) => { console.log(chalk.green(`\n${index + 1}. ${watcher.path}`)); console.log(` ID: ${watcher.watchId}`); console.log(` Recursive: ${watcher.recursive ? 'āœ…' : 'āŒ'}`); console.log(` Events: ${watcher.events.join(', ')}`); console.log(` Started: ${new Date(watcher.startedAt).toLocaleString()}`); console.log(` Event Count: ${watcher.eventCount}`); console.log(` Duration: ${this.formatDuration(watcher.duration)}`); if (options.verbose) { try { const detailedInfo = this.manager.getWatcherInfo(watcher.watchId); console.log(` Normalized Path: ${detailedInfo.normalizedPath}`); console.log(` Has Callback: ${detailedInfo.hasCallback ? 'āœ…' : 'āŒ'}`); } catch (error) { console.log(chalk.red(` Error getting details: ${error.message}`)); } } }); } catch (error) { console.error(chalk.red(`āŒ Failed to list watchers: ${error.message}`)); process.exit(1); } }); } addWatchStopCommand() { this.program .command('watch-stop') .alias('wstop') .description('Stop watching a directory or file') .option('-i, --id <watchId>', 'Watch ID to stop') .option('-p, --path <path>', 'Path to stop watching') .option('-a, --all', 'Stop all watchers') .action(async (options) => { await this.initialize(); try { if (options.all) { const result = await this.manager.stopAllWatching(); console.log(chalk.green(`āœ… Stopped ${result.stopped} watcher(s)`)); if (result.failed > 0) { console.log(chalk.yellow(`āš ļø Failed to stop ${result.failed} watcher(s)`)); result.results.forEach(r => { if (r.error) { console.log(chalk.red(` Error: ${r.error}`)); } }); } } else if (options.id) { const result = await this.manager.stopWatching(options.id); console.log(chalk.green(`āœ… Stopped watcher: ${result.path}`)); console.log(` Events processed: ${result.eventCount}`); console.log(` Duration: ${this.formatDuration(result.duration)}`); } else if (options.path) { const resolvedPath = path.resolve(options.path); const result = await this.manager.stopWatching(resolvedPath); console.log(chalk.green(`āœ… Stopped watching: ${result.path}`)); console.log(` Events processed: ${result.eventCount}`); console.log(` Duration: ${this.formatDuration(result.duration)}`); } else { console.log(chalk.red('āŒ Must specify --id, --path, or --all')); process.exit(1); } } catch (error) { console.error(chalk.red(`āŒ Failed to stop watcher: ${error.message}`)); process.exit(1); } }); } addWatchStatusCommand() { this.program .command('watch-status') .alias('wstat') .description('Show file watching system status') .action(async () => { await this.initialize(); try { const status = this.manager.getWatchingStatus(); console.log(chalk.cyan('\nšŸ‘ļø File Watching Status:')); console.log(` Enabled: ${status.enabled ? 'āœ…' : 'āŒ'}`); console.log(` Active Watchers: ${status.activeWatchers}/${status.maxWatchers}`); console.log(` Total Events Processed: ${status.totalEvents}`); console.log(` Pending Debounces: ${status.pendingDebounces}`); console.log(chalk.cyan('\nāš™ļø Configuration:')); console.log(` Recursive Default: ${status.config.recursive ? 'āœ…' : 'āŒ'}`); console.log(` Ignore Dotfiles: ${status.config.ignoreDotfiles ? 'āœ…' : 'āŒ'}`); console.log(` Debounce Time: ${status.config.debounceMs}ms`); if (status.activeWatchers > 0) { const watchers = this.manager.listActiveWatchers(); console.log(chalk.cyan(`\nšŸ“‹ Active Watchers Summary:`)); watchers.forEach(watcher => { const duration = this.formatDuration(watcher.duration); console.log(` • ${watcher.path} (${watcher.eventCount} events, ${duration})`); }); } } catch (error) { console.error(chalk.red(`āŒ Failed to get watch status: ${error.message}`)); process.exit(1); } }); } // ============================================================================ // FUTURE BUCKET COMMANDS (UPDATED STUBS) // ============================================================================ // ============================================================================ // EXISTING FILE OPERATIONS (shortened for brevity) // ============================================================================ addUploadCommand() { this.program .command('upload') .description('Upload (copy) a file to target location') .requiredOption('-f, --file <path>', 'Source file path') .option('-t, --target <path>', 'Target file path (defaults to uploads directory)') .option('-p, --provider <name>', 'Provider to use', 'local') .action(async (options) => { await this.initialize(); const targetPath = options.target || path.join(this.config.get('DEFAULT_UPLOAD_DIRECTORY'), path.basename(options.file)); const spinner = ora(`Uploading ${path.basename(options.file)}...`).start(); try { const result = await this.manager.uploadFile(options.file, targetPath, options.provider); spinner.succeed(`Upload completed!`); console.log(chalk.green(` šŸ“¤ ${result.name}`)); console.log(` šŸ“ ${result.path}`); console.log(` šŸ“ ${this.formatBytes(result.size)}`); if (result.hash) { console.log(` šŸ”’ ${result.hash.substring(0, 16)}...`); } } catch (error) { spinner.fail(`Upload failed: ${error.message}`); process.exit(1); } }); } addDownloadCommand() { this.program .command('download') .description('Download (copy) a file from source location') .requiredOption('-s, --source <path>', 'Source file path') .option('-t, --target <path>', 'Target file path (defaults to downloads directory)') .option('-p, --provider <n>', 'Provider to use', 'local') .action(async (options) => { await this.initialize(); const targetPath = options.target || path.join(this.config.get('DEFAULT_DOWNLOAD_DIRECTORY'), path.basename(options.source)); const spinner = ora(`Downloading ${path.basename(options.source)}...`).start(); try { const result = await this.manager.downloadFile(options.source, targetPath, options.provider); spinner.succeed(`Download completed!`); console.log(chalk.green(` šŸ“„ ${result.name}`)); console.log(` šŸ“ ${result.path}`); console.log(` šŸ“ ${this.formatBytes(result.size)}`); } catch (error) { spinner.fail(`Download failed: ${error.message}`); process.exit(1); } }); } addListCommand() { this.program .command('list') .description('List files in directory') .option('-d, --directory <path>', 'Directory to list', '/') .option('-r, --recursive', 'List recursively') .option('-p, --provider <n>', 'Provider to use', 'local') .action(async (options) => { await this.initialize(); const spinner = ora(`Listing files in ${options.directory}...`).start(); try { const files = await this.manager.listFiles(options.directory, options.provider, { recursive: options.recursive }); spinner.succeed(`Found ${files.length} file(s)`); if (files.length === 0) { console.log(chalk.yellow(`šŸ“‚ Directory "${options.directory}" is empty`)); return; } console.log(chalk.cyan(`\nšŸ“‚ Files in "${options.directory}":`)); files.forEach((file, index) => { const icon = file.isDirectory ? 'šŸ“' : 'šŸ“„'; const size = file.isFile ? this.formatBytes(file.size) : ''; const modified = new Date(file.modifiedTime).toLocaleDateString(); console.log(` ${index + 1}. ${icon} ${file.name} ${size ? `(${size})` : ''} - ${modified}`); }); } catch (error) { spinner.fail(`List failed: ${error.message}`); process.exit(1); } }); } addDeleteCommand() { this.program .command('delete') .description('Delete a file') .requiredOption('-f, --file <path>', 'File path to delete') .option('-p, --provider <n>', 'Provider to use', 'local') .option('--yes', 'Skip confirmation prompt') .action(async (options) => { await this.initialize(); // Confirmation prompt if (!options.yes) { console.log(chalk.yellow(`āš ļø Are you sure you want to delete "${options.file}"?`)); console.log(chalk.gray('Use --yes to skip this confirmation')); return; } const spinner = ora(`Deleting ${path.basename(options.file)}...`).start(); try { await this.manager.deleteFile(options.file, options.provider); spinner.succeed(`File deleted successfully!`); console.log(chalk.green(` šŸ—‘ļø ${options.file}`)); } catch (error) { spinner.fail(`Delete failed: ${error.message}`); process.exit(1); } }); } addCopyCommand() { this.program .command('copy') .description('Copy a file to new location') .requiredOption('-s, --source <path>', 'Source file path') .requiredOption('-t, --target <path>', 'Target file path') .option('-p, --provider <n>', 'Provider to use', 'local') .action(async (options) => { await this.initialize(); const spinner = ora(`Copying ${path.basename(options.source)}...`).start(); try { const result = await this.manager.copyFile(options.source, options.target, options.provider); spinner.succeed(`Copy completed!`); console.log(chalk.green(` šŸ“‹ ${result.name}`)); console.log(` šŸ“ ${result.path}`); console.log(` šŸ“ ${this.formatBytes(result.size)}`); } catch (error) { spinner.fail(`Copy failed: ${error.message}`); process.exit(1); } }); } addMoveCommand() { this.program .command('move') .description('Move a file to new location') .requiredOption('-s, --source <path>', 'Source file path') .requiredOption('-t, --target <path>', 'Target file path') .option('-p, --provider <n>', 'Provider to use', 'local') .action(async (options) => { await this.initialize(); const spinner = ora(`Moving ${path.basename(options.source)}...`).start(); try { const result = await this.manager.moveFile(options.source, options.target, options.provider); spinner.succeed(`Move completed!`); console.log(chalk.green(` šŸ“¦ ${result.name}`)); console.log(` šŸ“ ${result.path}`); console.log(` šŸ“ ${this.formatBytes(result.size)}`); } catch (error) { spinner.fail(`Move failed: ${error.message}`); process.exit(1); } }); } addRenameCommand() { this.program .command('rename') .description('Rename a file') .requiredOption('-f, --file <path>', 'File path to rename') .requiredOption('-n, --name <name>', 'New file name') .option('-p, --provider <n>', 'Provider to use', 'local') .action(async (options) => { await this.initialize(); const spinner = ora(`Renaming ${path.basename(options.file)}...`).start(); try { const result = await this.manager.renameFile(options.file, options.name, options.provider); spinner.succeed(`Rename completed!`); console.log(chalk.green(` āœļø ${result.name}`)); console.log(` šŸ“ ${result.path}`); } catch (error) { spinner.fail(`Rename failed: ${error.message}`); process.exit(1); } }); } addMkdirCommand() { this.program .command('mkdir') .description('Create a directory') .requiredOption('-d, --directory <path>', 'Directory path to create') .option('-p, --provider <n>', 'Provider to use', 'local') .action(async (options) => { await this.initialize(); const spinner = ora(`Creating directory ${options.directory}...`).start(); try { const result = await this.manager.createFolder(options.directory, options.provider); spinner.succeed(`Directory created!`); console.log(chalk.green(` šŸ“ ${result.name}`)); console.log(` �� ${result.path}`); } catch (error) { spinner.fail(`Create directory failed: ${error.message}`); process.exit(1); } }); } addRmdirCommand() { this.program .command('rmdir') .description('Remove a directory') .requiredOption('-d, --directory <path>', 'Directory path to remove') .option('-r, --recursive', 'Remove directory and all contents') .option('-p, --provider <n>', 'Provider to use', 'local') .option('--yes', 'Skip confirmation prompt') .action(async (options) => { await this.initialize(); // Confirmation prompt if (!options.yes) { const action = options.recursive ? 'recursively delete' : 'delete'; console.log(chalk.yellow(`āš ļø Are you sure you want to ${action} "${options.directory}"?`)); console.log(chalk.gray('Use --yes to skip this confirmation')); return; } const spinner = ora(`Removing directory ${options.directory}...`).start(); try { await this.manager.deleteFolder(options.directory, options.recursive, options.provider); spinner.succeed(`Directory removed!`); console.log(chalk.green(` šŸ—‚ļø ${options.directory}`)); } catch (error) { spinner.fail(`Remove directory failed: ${error.message}`); process.exit(1); } }); } addListFoldersCommand() { this.program .command('ls-folders') .description('List folders in directory') .option('-d, --directory <path>', 'Directory to list', '/') .option('-p, --provider <n>', 'Provider to use', 'local') .action(async (options) => { await this.initialize(); const spinner = ora(`Listing folders in ${options.directory}...`).start(); try { const folders = await this.manager.listFolders(options.directory, options.provider); spinner.succeed(`Found ${folders.length} folder(s)`); if (folders.length === 0) { console.log(chalk.yellow(`šŸ“‚ No folders in "${options.directory}"`)); return; } console.log(chalk.cyan(`\nšŸ“‚ Folders in "${options.directory}":`)); folders.forEach((folder, index) => { const modified = new Date(folder.modifiedTime).toLocaleDateString(); console.log(` ${index + 1}. šŸ“ ${folder.name} - ${modified}`); }); } catch (error) { spinner.fail(`List folders failed: ${error.message}`); process.exit(1); } }); } addSearchCommand() { this.program .command('search') .description('Search for files') .requiredOption('-q, --query <term>', 'Search query') .option('-d, --directory <path>', 'Directory to search in', '/') .option('-r, --recursive', 'Search recursively', true) .option('-c, --case-sensitive', 'Case sensitive search') .option('-l, --limit <number>', 'Maximum number of results', '20') .option('-p, --provider <name>', 'Provider to use', 'local') .action(async (options) => { await this.initialize(); const spinner = ora(`Searching for "${options.query}"...`).start(); try { const results = await this.manager.searchFiles(options.query, options.provider, { directory: options.directory, recursive: options.recursive, caseSensitive: options.caseSensitive, limit: parseInt(options.limit) }); spinner.succeed(`Found ${results.length} result(s)`); if (results.length === 0) { console.log(chalk.yellow(`šŸ” No files found matching "${options.query}"`)); return; } console.log(chalk.cyan(`\nšŸ” Search Results for "${options.query}":`)); results.forEach((file, index) => { const icon = file.isDirectory ? 'šŸ“' : 'šŸ“„'; const size = file.isFile ? this.formatBytes(file.size) : ''; const modified = new Date(file.modifiedTime).toLocaleDateString(); console.log(` ${index + 1}. ${icon} ${file.name}`); console.log(` Path: ${file.path}`); if (size) console.log(` Size: ${size}`); console.log(` Modified: ${modified}`); console.log(''); }); } catch (error) { spinner.fail(`Search failed: ${error.message}`); process.exit(1); } }); } // ============================================================================ // COMPRESSION COMMANDS (Bucket 3) // ============================================================================ addCompressCommand() { this.program .command('compress') .description('Compress files or directories') .requiredOption('-f, --file <path>', 'File or directory to compress') .option('-o, --output <path>', 'Output archive path') .option('--format <type>', 'Compression format (zip, tar.gz)', 'zip') .option('--level <number>', 'Compression level (1-9)', '6') .option('--include-root', 'Include root directory in archive') .option('-p, --provider <name>', 'Provider to use', 'compression') .action(async (options) => { await this.initialize(); // Generate output path if not provided const outputPath = options.output || this.generateArchivePath(options.file, options.format); const spinner = ora(`Compressing ${path.basename(options.file)} to ${options.format.toUpperCase()}...`).start(); // Set up progress tracking let lastProgress = 0; this.manager.on('compressionProgress', (progress) => { if (progress.percentage >= lastProgress + 10) { spinner.text = `Compressing ${path.basename(options.file)}... ${progress.percentage}%`; lastProgress = progress.percentage; } }); try { const result = await this.manager.compressFile(options.file, outputPath, { format: options.format, level: parseInt(options.level), includeRoot: options.includeRoot }); spinner.succeed(`Compression completed!`); console.log(chalk.green(` šŸ“¦ ${result.name}`)); console.log(` šŸ“ ${result.outputPath}`); console.log(` šŸ“ Original: ${this.formatBytes(result.originalSize)}`); console.log(` šŸ“ Compressed: ${this.formatBytes(result.size)}`); console.log(` šŸ’¾ Space saved: ${(result.compressionRatio * 100).toFixed(1)}%`); if (result.hash) { console.log(` šŸ”’ Checksum: ${result.hash.substring(0, 16)}...`); } } catch (error) { spinner.fail(`Compression failed: ${error.message}`); process.exit(1); } finally { this.manager.removeAllListeners('compressionProgress'); } }); } addDecompressCommand() { this.program .command('decompress') .description('Decompress archive files') .requiredOption('-f, --file <path>', 'Archive file to decompress') .option('-d, --directory <path>', 'Output directory', './extracted') .option('--format <type>', 'Archive format (auto-detect if not specified)') .option('--overwrite', 'Overwrite existing files') .option('--preserve-permissions', 'Preserve file permissions (tar.gz only)') .option('-p, --provider <name>', 'Provider to use', 'compression') .action(async (options) => { await this.initialize(); const spinner = ora(`Decompressing ${path.basename(options.file)}...`).start(); // Set up progress tracking this.manager.on('decompressionProgress', (progress) => { if (progress.currentFile) { spinner.text = `Decompressing... ${path.basename(progress.currentFile)}`; } }); try { const result = await this.manager.decompressFile(options.file, options.directory, { format: options.format, overwrite: options.overwrite, preservePermissions: options.preservePermissions }); spinner.succeed(`Decompression completed!`); console.log(chalk.green(` šŸ“¦ ${path.basename(options.file)}`)); console.log(` šŸ“ Extracted to: ${result.outputDirectory}`); console.log(` šŸ“„ Files: ${result.extractedFiles}`); console.log(` šŸ“ Directories: ${result.extractedDirectories}`); console.log(` šŸ“ Total size: ${this.formatBytes(result.totalSize)}`); if (result.hash) { console.log(` šŸ”’ Archive checksum: ${result.hash.substring(0, 16)}...`); } } catch (error) { spinner.fail(`Decompression failed: ${error.message}`); process.exit(1); } finally { this.manager.removeAllListeners('decompressionProgress'); } }); } addCompressBatchCommand() { this.program .command('compress-batch') .description('Compress multiple files in batch') .requiredOption('-d, --directory <path>', 'Directory containing files to compress') .option('-o, --output <path>', 'Output directory for archives', './archives') .option('--format <type>', 'Compression format (zip, tar.gz)', 'zip') .option('--level <number>', 'Compression level (1-9)', '6') .option('--pattern <pattern>', 'File pattern to match', '*') .option('--naming <pattern>', 'Naming pattern for archives', '{name}.{format}') .option('-p, --provider <name>', 'Provider to use', 'compression') .action(async (options) => { await this.initialize(); const spinner = ora('Scanning files for batch compression...').start(); try { // Get list of files to compress const files = await this.getFilesForBatch(options.directory, options.pattern); if (files.length === 0) { spinner.warn('No files found matching the pattern'); return; } spinner.text = `Starting batch compression of ${files.length} file(s)...`; // Set up progress tracking let completedFiles = 0; this.manager.on('compressionProgress', (progress) => { spinner.text = `Batch compression: ${completedFiles}/${files.length} completed`; }); const result = await this.manager.compressMultipleFiles(files, options.output, { format: options.format, level: parseInt(options.level), namingPattern: options.naming }); spinner.succeed(`Batch compression completed!`); console.log(chalk.cyan('\nšŸ“Š Batch Results:')); console.log(` āœ… Successful: ${result.successful.length}`); console.log(` āŒ Failed: ${result.failed.length}`); console.log(` šŸ“ˆ Success rate: ${result.summary.successRate}`); if (result.successful.length > 0) { console.log(chalk.green('\nšŸ“¦ Created Archives:')); result.successful.forEach(item => { const sizeSaved = item.result.originalSize - item.result.size; console.log(` • ${path.basename(item.outputPath)} (saved ${this.formatBytes(sizeSaved)})`); }); } if (result.failed.length > 0) { console.log(chalk.red('\nāŒ Failed:')); result.failed.forEach(item => { console.log(` • ${path.basename(item.inputPath)}: ${item.error}`); }); } } catch (error) { spinner.fail(`Batch compression failed: ${error.message}`); process.exit(1); } finally { this.manager.removeAllListeners('compressionProgress'); } }); } addDecompressBatchCommand() { this.program .command('decompress-batch') .description('Decompress multiple archives in batch') .requiredOption('-d, --directory <path>', 'Directory containing archives') .option('-o, --output <path>', 'Output directory for extracted files', './extracted') .option('--pattern <pattern>', 'Archive pattern to match', '*.{zip,tar.gz}') .option('--overwrite', 'Overwrite existing files') .option('-p, --provider <name>', 'Provider to use', 'compression') .action(async (options) => { await this.initialize(); const spinner = ora('Scanning archives for batch decompression...').start(); try { // Get list of archives to decompress const archives = await this.getArchivesForBatch(options.directory, options.pattern); if (archives.length === 0) { spinner.warn('No archives found matching the pattern'); return; } spinner.text = `Starting batch decompression of ${archives.length} archive(s)...`; // Set up progress tracking let completedArchives = 0; this.manager.on('decompressionProgress', (progress) => { spinner.text = `Batch decompression: ${completedArchives}/${archives.length} completed`; }); const result = await this.manager.decompressMultipleFiles(archives, options.output, { overwrite: options.overwrite }); spinner.succeed(`Batch decompression completed!`); console.log(chalk.cyan('\nšŸ“Š Batch Results:')); console.log(` āœ… Successful: ${result.successful.length}`); console.log(` āŒ Failed: ${result.failed.length}`); console.log(` šŸ“ˆ Success rate: ${result.summary.successRate}`); if (result.successful.length > 0) { console.log(chalk.green('\nšŸ“ Extracted Archives:')); result.successful.forEach(item => { console.log(` • ${path.basename(item.inputPath)} → ${path.basename(item.outputDirectory)}/`); console.log(` Files: ${item.result.extractedFiles}, Size: ${this.formatBytes(item.result.totalSize)}`); }); } if (result.failed.length > 0) { console.log(chalk.red('\nāŒ Failed:')); result.failed.forEach(item => { console.log(` • ${path.basename(item.inputPath)}: ${item.error}`); }); } } catch (error) { spinner.fail(`Batch decompression failed: ${error.message}`); process.exit(1); } finally { this.manager.removeAllListeners('decompressionProgress'); } }); } addCompressionStatusCommand() { this.program .command('compression-status') .description('Show compression system status') .action(async () => { await this.initialize(); const spinner = ora('Getting compression status...').start(); try { const status = this.manager.getCompressionStatus(); const activeOps = this.manager.listActiveCompressionOperations(); spinner.succeed('Compression status retrieved'); console.log(chalk.cyan('\nšŸ“¦ Compression System Status:')); console.log(` Enabled: ${status.enabled ? 'āœ…' : 'āŒ'}`); console.log(` Supported Formats: ${status.supportedFormats.join(', ')}`); console.log(` Default Format: ${status.defaultFormat}`); console.log(` Compression Level: ${status.compressionLevel}/9`); console.log(` Active Operations: ${status.activeOperations}`); console.log(chalk.cyan('\nāš™ļø Configuration:')); console.log(` Max File Size: ${this.formatBytes(status.config.maxFileSize)}`); console.log(` Chunk Size: ${this.formatBytes(status.config.chunkSize)}`); console.log(` Progress Tracking: ${status.enableProgressTracking ? 'āœ…' : 'āŒ'}`); console.log(` Checksum Verification: ${status.enableChecksumVerification ? 'āœ…' : 'āŒ'}`); if (activeOps.length > 0) { console.log(chalk.cyan('\nšŸ”„ Active Operations:')); activeOps.forEach((op, index) => { const progress = op.progress.total > 0 ? Math.round((op.progress.processed / op.progress.total) * 100) : 0; console.log(` ${index + 1}. ${op.type}: ${path.basename(op.inputPath)}`); console.log(` Progress: ${progress}% (${this.formatDuration(op.duration)})`); }); } } catch (error) { spinner.fail(`Failed to get compression status: ${error.message}`); process.exit(1); } }); } addListFormatsCommand() { this.program .command('list-formats') .description('List supported compression formats') .action(async () => { await this.initialize(); try { const status = this.manager.getCompressionStatus(); console.log(chalk.cyan('šŸ—‚ļø Supported Compression Formats:')); console.log(''); status.supportedFormats.forEach(format => { const isDefault = format === status.defaultFormat; const icon = format === 'zip' ? 'šŸ“¦' : 'šŸ“š'; const defaultMarker = isDefault ? chalk.yellow(' (default)') : ''; console.log(` ${icon} ${format.toUpperCase()}${defaultMarker}`); if (format === 'zip') { console.log(' • Fast compression and decompression'); console.log(' • Wide compatibility'); console.log(' • Good for mixed file types'); } else if (format === 'tar.gz') { console.log(' • Better compression ratios'); console.log(' • Preserves file permissions'); console.log(' • Unix/Linux standard'); } console.log(''); }); console.log(chalk.gray('šŸ’” Tip: Use --format option to specify format in compress commands')); } catch (error) { console.error(chalk.red(`āŒ Failed to list formats: ${error.message}`)); process.exit(1); } }); } // ============================================================================ // COMPRESSION UTILITY METHODS // ============================================================================ generateArchivePath(inputPath, format) { const baseName = path.parse(inputPath).name; return `./${baseName}.${format}`; } async getFilesForBatch(directory, pattern) { try { const searchPattern = path.join(directory, pattern); const globPromise = promisify(glob); const files = await globPromise(searchPattern); // Filter out directories, only return files const fileList = []; for (const file of files) { const stats = await fs.stat(file); if (stats.isFile()) { fileList.push(file); } } return fileList; } catch (error) { throw new Error(`Failed to scan files: ${error.message}`); } } async getArchivesForBatch(directory, pattern) { try { const entries = await fs.readdir(directory); const archives = []; for (const entry of entries) { const fullPath = path.join(directory, entry); const stats = await fs.stat(fullPath); if (stats.isFile()) { const ext = path.extname(entry).toLowerCase(); const fileName = entry.toLowerCase(); if (ext === '.zip' || fileName.endsWith('.tar.gz') || fileName.endsWith('.tgz')) { archives.push(fullPath); } } } return archives; } catch (error) { throw new Error(`Failed to scan archives: ${error.message}`); } } formatDuration(milliseconds) { const seconds = Math.floor(milliseconds / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m ${seconds % 60}s`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } } // ============================================================================ // TUNNELING COMMANDS (Bucket 4) // ============================================================================ addShareCommand() { this.program .command('share <file>') .description('Create a temporary shareable URL for a file') .option('-e, --expires <time>', 'Expiration time (e.g., "30m", "2h", "7d")', '1h') .option('-p, --permissions <perms>', 'Comma-separated permissions', 'read') .option('-k, --keep-alive', 'Keep process alive after download (default: false)') .option('-t, --timeout <seconds>', 'Max time to wait for download in seconds', '1800') // 30 minutes default .option('-m, --multi-download', 'Allow multiple downloads before shutdown') .option('--no-auto-shutdown', 'Disable automatic shutdown (requires manual stop)') .action(async (filePath, options) => { await this.initialize(); const spinner = ora('Creating shareable URL...').start(); let shareResult = null; let downloadMonitor = null; try { // Parse expiration const expiresAt = this.parseExpirationTime(options.expires); const permissions = options.permissions.split(',').map(p => p.trim()); const timeoutSeconds = parseInt(options.timeout); const keepAliveAfterDownload = options.keepAlive; const allowMultipleDownloads = options.multiDownload; const autoShutdownDisabled = options.noAutoShutdown; // Create the shareable URL shareResult = await this.manager.createTemporaryUrl(filePath, { expiresAt, permissions }); spinner.succeed('Shareable URL created successfully!'); console.log(chalk.cyan('\nšŸ”— Shareable URL Details:')); console.log(` File: ${filePath}`); console.log(` URL: ${shareResult.shareableUrl}`); console.log(` Expires: ${new Date(shareResult.expiresAt).toLocaleString()}`); console.log(` Permissions: ${shareResult.permissions.join(', ')}`); console.log(` URL ID: ${shareResult.urlId}`); if (autoShutdownDisabled) { console.log(chalk.yellow('\nšŸ”„ Auto-shutdown disabled - process will run until manually stopped')); console.log(chalk.gray(' Press Ctrl+C to shutdown gracefully')); await this.setupGracefulShutdown(shareResult, 0, { monitorDownloads: false, keepAliveAfterDownload: true, allowMultipleDownloads: true }); } else { // Set up download monitoring downloadMonitor = this.setupDownloadMonitoring(shareResult, { keepAliveAfterDownload, allowMultipleDownloads, timeoutSeconds }); console.log(chalk.yellow('\nā³ Waiting for download...')); console.log(chalk.gray(` Will auto-shutdown after download${allowMultipleDownloads ? 's' : ''} or ${this.formatDuration(timeoutSeconds * 1000)} timeout`)); console.log(chalk.gray(' Press Ctrl+C to shutdown immediately')); await this.setupGracefulShutdown(shareResult, timeoutSeconds, { monitorDownloads: true, downloadMonitor, keepAliveAfterDownload, allowMultipleDownloads }); } } catch (error) { spinner.fail(`Failed to create shareable URL: ${error.message}`); if (shareResult) { try { await this.cleanup([shareResult.urlId]); } catch (cleanupError) { console.warn(chalk.yellow(`āš ļø Cleanup warning: ${cleanupError.message}`)); } } process.exit(1); } }); } addTunnelStatusCommand() { this.program .command('tunnel-status') .alias('tstatus') .