UNPKG

smart-file-organizer

Version:

Intelligent CLI tool for automatically organizing files by type with preview, logging, and undo functionality

523 lines (455 loc) • 13.9 kB
#!/usr/bin/env node import fs from "fs/promises"; import path from "path"; import readline from "readline"; // Default configuration - expandable and customizable const defaultConfig = { categories: { images: [ ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".tiff", ".ico", ".raw", ], documents: [ ".pdf", ".doc", ".docx", ".txt", ".rtf", ".odt", ".pages", ".tex", ], spreadsheets: [".xls", ".xlsx", ".csv", ".ods", ".numbers"], presentations: [".ppt", ".pptx", ".odp", ".key"], audio: [".mp3", ".wav", ".flac", ".aac", ".ogg", ".m4a", ".wma"], video: [".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm", ".m4v"], archives: [".zip", ".rar", ".7z", ".tar", ".gz", ".bz2", ".xz"], code: [ ".js", ".ts", ".html", ".css", ".py", ".java", ".cpp", ".c", ".php", ".rb", ".go", ], ebooks: [".epub", ".mobi", ".azw", ".fb2", ".pdf"], fonts: [".ttf", ".otf", ".woff", ".woff2", ".eot"], }, // Comprehensive list of files that should NEVER be moved excludeFiles: [ // Package managers "package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "composer.json", "composer.lock", "Gemfile", "Gemfile.lock", "requirements.txt", "poetry.lock", "Pipfile", "Pipfile.lock", // Git files ".gitignore", ".gitattributes", ".gitmodules", ".gitkeep", // Documentation "README.md", "README.txt", "README.rst", "CHANGELOG.md", "CHANGELOG.txt", "CONTRIBUTING.md", "AUTHORS.md", "CONTRIBUTORS.md", // License files "LICENSE", "LICENSE.md", "LICENSE.txt", "LICENCE", "LICENCE.md", "LICENCE.txt", // Environment and config files ".env", ".env.example", ".env.local", ".env.production", ".env.development", "tsconfig.json", "jsconfig.json", "webpack.config.js", "vite.config.js", "next.config.js", "nuxt.config.js", "vue.config.js", "angular.json", "tailwind.config.js", "postcss.config.js", "babel.config.js", // Linting and formatting ".eslintrc", ".eslintrc.js", ".eslintrc.json", ".eslintignore", ".prettierrc", ".prettierrc.js", ".prettierrc.json", ".prettierignore", // Docker "Dockerfile", "docker-compose.yml", "docker-compose.yaml", ".dockerignore", // CI/CD "netlify.toml", "vercel.json", ".travis.yml", "appveyor.yml", // Build tools "Makefile", "CMakeLists.txt", "build.gradle", "pom.xml", // App-specific files "file-organizer.log", "index.js", "app.js", // Archives and packages (avoid moving compressed files that might be important) "*.tgz", "*.tar.gz", ], // Folders to completely skip excludeFolders: [ "node_modules", ".git", ".vscode", ".idea", "dist", "build", "coverage", ".nyc_output", "target", "bin", "obj", ".gradle", ".next", ".nuxt", "__pycache__", ".pytest_cache", "venv", "env", ".svn", ".hg", "vendor", ], logFile: "file-organizer.log", }; class FileOrganizer { constructor(config = defaultConfig) { this.config = config; this.stats = { moved: 0, created: [], errors: [], skipped: [], startTime: new Date(), }; } async log(message, level = "INFO") { const timestamp = new Date().toISOString(); const logEntry = `[${timestamp}] ${level}: ${message}\n`; try { await fs.appendFile(this.config.logFile, logEntry); } catch (error) { console.warn("āš ļø Could not write to log file:", error.message); } } async getTargetFolder(fileExtension, fileName) { // Check for important files that should not be moved const importantPatterns = [ /^(readme|license|licence|changelog|contributing|authors|contributors)/i, /\.(md|txt|rst)$/i && fileName.toLowerCase().includes("readme"), /\.(env|config|conf)$/i, /^(makefile|dockerfile|rakefile|gemfile|podfile)$/i, ]; if ( importantPatterns.some( (pattern) => pattern.test && pattern.test(fileName) ) ) { return null; // Don't move this file } // Check categories for (const [category, extensions] of Object.entries( this.config.categories )) { if (extensions.includes(fileExtension.toLowerCase())) { return category; } } return "others"; } async createFolderIfNeeded(folderName) { try { await fs.access(folderName); } catch { await fs.mkdir(folderName, { recursive: true }); this.stats.created.push(folderName); console.log(`šŸ“‚ Created folder: ${folderName}/`); await this.log(`Created folder: ${folderName}`); } } async organizeFolder(targetPath = ".", preview = false) { try { console.log( `\nšŸ” ${preview ? "Preview" : "Organizing"}: ${path.resolve( targetPath )}\n` ); const files = await fs.readdir(targetPath); const filesToMove = []; // Analyze files first for (const file of files) { const filePath = path.join(targetPath, file); try { const stats = await fs.stat(filePath); // Skip directories, hidden files, excluded files and folders if ( stats.isDirectory() || file.startsWith(".") || this.config.excludeFiles.includes(file) || this.config.excludeFolders.includes(file) || // Skip files without extension (often important files) (!path.extname(file) && !file.includes(".")) ) { if (stats.isDirectory()) { this.stats.skipped.push(`${file}/ (directory)`); } else { this.stats.skipped.push(`${file} (excluded/important file)`); } continue; } const fileExtension = path.extname(file); const targetFolder = await this.getTargetFolder(fileExtension, file); // Skip files that should not be moved if (targetFolder === null) { console.log(`ā­ļø Skipping important file: ${file}`); this.stats.skipped.push(`${file} (important file)`); continue; } filesToMove.push({ name: file, path: filePath, targetFolder, size: stats.size, }); } catch (error) { this.stats.errors.push(`Could not read ${file}: ${error.message}`); } } if (filesToMove.length === 0) { console.log( "šŸŽ‰ Folder is already organized or contains no files to organize!" ); if (this.stats.skipped.length > 0) { console.log( `\nšŸ“‹ Skipped ${this.stats.skipped.length} items (directories, config files, etc.)` ); } return; } // Show overview const folderSummary = {}; filesToMove.forEach((file) => { if (!folderSummary[file.targetFolder]) { folderSummary[file.targetFolder] = []; } folderSummary[file.targetFolder].push(file.name); }); console.log("šŸ“‹ Overview of changes:"); for (const [folder, files] of Object.entries(folderSummary)) { console.log(` šŸ“ ${folder}/ (${files.length} files)`); files.slice(0, 3).forEach((file) => console.log(` • ${file}`)); if (files.length > 3) { console.log(` ... and ${files.length - 3} more`); } } if (this.stats.skipped.length > 0) { console.log( `\nā­ļø Skipped ${this.stats.skipped.length} items (config files, directories, etc.)` ); } if (preview) { console.log( "\n✨ This was just a preview. Run without --preview to execute the changes." ); return; } // Ask for confirmation const confirmed = await this.askConfirmation( "\nā“ Do you want to continue? (y/N): " ); if (!confirmed) { console.log("āŒ Cancelled by user"); return; } // Execute the moving console.log("\nšŸš€ Starting organization...\n"); for (const file of filesToMove) { try { await this.createFolderIfNeeded(file.targetFolder); const newPath = path.join(file.targetFolder, file.name); await fs.rename(file.path, newPath); console.log(`šŸ“„ ${file.name} → ${file.targetFolder}/`); await this.log(`Moved: ${file.name} to ${file.targetFolder}/`); this.stats.moved++; } catch (error) { const errorMsg = `Could not move ${file.name}: ${error.message}`; this.stats.errors.push(errorMsg); console.error(`āŒ ${errorMsg}`); await this.log(errorMsg, "ERROR"); } } this.showSummary(); } catch (error) { console.error("āŒ Critical error:", error.message); await this.log(`Critical error: ${error.message}`, "ERROR"); } } async askConfirmation(question) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); return new Promise((resolve) => { rl.question(question, (answer) => { rl.close(); resolve(["y", "yes"].includes(answer.toLowerCase())); }); }); } showSummary() { const duration = ((new Date() - this.stats.startTime) / 1000).toFixed(2); console.log("\nāœ… Organization completed!"); console.log("šŸ“Š Statistics:"); console.log(` • Files moved: ${this.stats.moved}`); console.log(` • Folders created: ${this.stats.created.length}`); console.log(` • Errors: ${this.stats.errors.length}`); console.log(` • Items skipped: ${this.stats.skipped.length}`); console.log(` • Time taken: ${duration}s`); if (this.stats.created.length > 0) { console.log(` • New folders: ${this.stats.created.join(", ")}`); } if (this.stats.errors.length > 0) { console.log("\nāš ļø Errors that occurred:"); this.stats.errors .slice(0, 5) .forEach((error) => console.log(` • ${error}`)); if (this.stats.errors.length > 5) { console.log(` ... and ${this.stats.errors.length - 5} more errors`); } } console.log(`\nšŸ“ Details saved to: ${this.config.logFile}`); } showHelp() { console.log(` šŸ—‚ļø Smart File Organizer CLI USAGE: organize-files [options] [folder] file-organizer [options] [folder] OPTIONS: --preview, -p Show what will happen without making changes --help, -h Show this help message --config, -c Show current configuration EXAMPLES: organize-files # Organize current folder organize-files --preview # Preview changes organize-files ~/Downloads # Organize Downloads folder organize-files -p ./test # Preview test folder āš ļø SAFETY FEATURES: • Always use --preview first to see what will happen • Automatically skips important files (package.json, README.md, etc.) • Best suited for Downloads, Desktop, and similar folders • Avoids moving configuration and project files šŸ›”ļø PROTECTED FILES: The script automatically skips: • package.json, README.md, LICENSE, etc. • Configuration files (.env, tsconfig.json, etc.) • Git files (.gitignore, etc.) • Folders like node_modules, .git, etc. SUPPORTED FILE TYPES: ${Object.entries(defaultConfig.categories) .map( ([cat, exts]) => ` šŸ“ ${cat}: ${exts.slice(0, 4).join(", ")}${ exts.length > 4 ? "..." : "" }` ) .join("\n")} `); } showConfig() { console.log("\nāš™ļø Current configuration:"); console.log(JSON.stringify(this.config, null, 2)); } } // Parse command line arguments function parseArgs() { const args = process.argv.slice(2); const options = { preview: false, help: false, config: false, targetPath: ".", }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === "--preview" || arg === "-p") { options.preview = true; } else if (arg === "--help" || arg === "-h") { options.help = true; } else if (arg === "--config" || arg === "-c") { options.config = true; } else if (!arg.startsWith("-")) { options.targetPath = arg; } } return options; } // Main function async function main() { console.log("šŸ—‚ļø Smart File Organizer CLI v1.0.0"); console.log("=====================================\n"); const options = parseArgs(); const organizer = new FileOrganizer(); if (options.help) { organizer.showHelp(); return; } if (options.config) { organizer.showConfig(); return; } await organizer.organizeFolder(options.targetPath, options.preview); } // Only run if file is called directly import { fileURLToPath } from "url"; if (fileURLToPath(import.meta.url) === process.argv[1]) { main().catch(console.error); } export { main };