@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
JavaScript
#!/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')
.