UNPKG

autosort

Version:

A modern CLI tool to organize files in a directory.

345 lines (303 loc) 10.6 kB
#!/usr/bin/env node const fs = require('fs-extra'); const path = require('path'); const yargs = require('yargs'); const chalk = require('chalk'); const ora = require('ora'); const dayjs = require('dayjs'); const cliProgress = require('cli-progress'); const { writeFileSync } = require('fs'); const figlet = require('figlet'); const Table = require('cli-table3'); const chokidar = require('chokidar'); // For --active option // Command-line arguments const argv = yargs .scriptName('autosort') .usage('$0 <directory> [options]', 'Sort files in a directory', (yargs) => { return yargs .positional('directory', { describe: 'Directory to sort', type: 'string', default: '.', }) .option('name', { alias: 'n', type: 'boolean', description: 'Sort files by name (first letter).', }) .option('extension', { alias: 'e', type: 'boolean', description: 'Sort files by extension.', }) .option('date', { alias: 'd', type: 'boolean', description: 'Sort files by creation date.', }) .option('log', { alias: 'l', type: 'boolean', description: 'Generate a log file of the actions taken.', }) .option('dry', { type: 'boolean', description: 'Perform a dry run without making any changes.', }) .option('sub', { alias: 's', type: 'boolean', description: 'Recursively sort files in all subdirectories.', }) .option('active', { alias: 'a', type: 'boolean', description: 'Continuously watch for file changes and sort them in real-time.', }) .check((argv) => { const sortingOptions = ['name', 'extension', 'date']; const selected = sortingOptions.filter((opt) => argv[opt]); if (selected.length > 1) { throw new Error( `Options --${selected.join(', --')} are mutually exclusive. Please choose only one sorting method.` ); } return true; }) .help() .alias('help', 'h') .alias('version', 'v'); }) .argv; // Utility function to display the banner const displayBanner = () => { const banner = figlet.textSync('AutoSort', { font: 'Slant', horizontalLayout: 'default', verticalLayout: 'default', }); console.log(chalk.blue.bold(banner)); }; // Utility function to display section headers const displaySectionHeader = (title) => { const line = chalk.gray('─'.repeat(process.stdout.columns || 80)); console.log(`\n${chalk.bold.cyan(title)}\n${line}\n`); }; // Utility function to display file movement const displayMove = (from, to, dryRun) => { const arrow = chalk.yellow('→'); if (dryRun) { console.log( `${chalk.gray('[DRY RUN]')} ${chalk.magenta(from)} ${arrow} ${chalk.magenta(to)}` ); } else { console.log(`${chalk.green('Moved')} ${chalk.magenta(from)} ${arrow} ${chalk.magenta(to)}`); } }; // Determine the active sorting method const determineSortingMethod = (argv) => { if (argv.name) return 'name'; if (argv.date) return 'date'; return 'extension'; // default }; // Function to sort files const sortFiles = async (targetDir, options, logEntries, movedFiles) => { const getFiles = async (dir) => { let filesList = []; const files = await fs.readdir(dir); for (const file of files) { const filePath = path.join(dir, file); const stats = await fs.stat(filePath); if (stats.isDirectory()) { if (options.sub) { filesList = filesList.concat(await getFiles(filePath)); } } else { filesList.push(filePath); } } return filesList; }; const allFiles = await getFiles(targetDir); const progressBar = new cliProgress.SingleBar( { format: `${chalk.cyan('Sorting')} |${chalk.green('{bar}')}| {percentage}% || {value}/{total} Files`, barCompleteChar: '\u2588', barIncompleteChar: '\u2591', hideCursor: true, }, cliProgress.Presets.shades_classic ); progressBar.start(allFiles.length, 0); for (const filePath of allFiles) { const fileName = path.basename(filePath); let subDirName; if (options.byName) { subDirName = fileName[0].toUpperCase(); } else if (options.byExtension) { subDirName = path.extname(fileName).replace('.', '').toUpperCase() || 'NO_EXT'; } else if (options.byDate) { subDirName = dayjs(fs.statSync(filePath).birthtime).format('YYYY-MM-DD'); } const relativeDir = path.relative(targetDir, path.dirname(filePath)); const finalSubDir = relativeDir === '' ? subDirName : path.join(relativeDir, subDirName); const subDirPath = path.join(targetDir, finalSubDir); const targetPath = path.join(subDirPath, fileName); displayMove(filePath, targetPath, options.dryRun); if (options.dryRun) { logEntries.push(`[DRY RUN] Would move: ${filePath}${targetPath}`); } else { await fs.ensureDir(subDirPath); await fs.move(filePath, targetPath, { overwrite: false }); logEntries.push(`Moved: ${filePath}${targetPath}`); movedFiles.push({ from: filePath, to: targetPath }); } progressBar.increment(); } progressBar.stop(); }; // Function to display moved files in a table const displayMovedFiles = (movedFiles) => { if (movedFiles.length === 0) return; displaySectionHeader('Moved Files'); const table = new Table({ head: [chalk.blue('From'), chalk.blue('To')], colWidths: [50, 50], wordWrap: true, }); movedFiles.forEach(({ from, to }) => { table.push([chalk.magenta(from), chalk.magenta(to)]); }); console.log(table.toString()); }; // Function to generate log file const generateLog = (targetDir, logEntries, isDryRun) => { const timestamp = dayjs().format('YYYY-MM-DD_HH-mm-ss'); const logFileName = isDryRun ? `autosort_dryrun_${timestamp}.log` : `autosort_${timestamp}.log`; const logFilePath = path.join(targetDir, logFileName); const logContent = isDryRun ? `Dry Run Log - ${dayjs().format('YYYY-MM-DD HH:mm:ss')}\n\n` + logEntries.join('\n') : logEntries.join('\n'); writeFileSync(logFilePath, logContent, 'utf8'); console.log( `${chalk.green('Log file generated at:')} ${chalk.magenta(logFilePath)}\n` ); }; // Main function (async () => { displayBanner(); const spinner = ora({ text: 'Initializing...', color: 'cyan' }).start(); try { const targetDir = path.resolve(argv.directory || '.'); if (!fs.existsSync(targetDir)) { throw new Error(`Directory "${targetDir}" does not exist.`); } spinner.succeed(chalk.green('Initialization complete.')); displaySectionHeader('Scanning Files'); const scanSpinner = ora({ text: 'Scanning files...', color: 'cyan' }).start(); let allFiles = []; if (argv.active) { // For active mode, no initial sorting scanSpinner.stop(); console.log( `${chalk.blue('Active mode enabled. Watching for file changes...')}\n` ); } else { allFiles = argv.sub ? await fs.readdir(targetDir) : await fs.readdir(targetDir); scanSpinner.stop(); } const sortingMethod = determineSortingMethod(argv); const options = { byName: sortingMethod === 'name', byExtension: sortingMethod === 'extension', byDate: sortingMethod === 'date', dryRun: argv.dry, log: argv.log, sub: argv.sub, active: argv.active, }; console.log( `${chalk.blue( `Sorting method: ${sortingMethod.charAt(0).toUpperCase() + sortingMethod.slice(1)}` )}` ); if (options.dryRun) { console.log( `${chalk.blue('Dry run mode enabled. No changes will be made.')}\n` ); } if (!options.active) { displaySectionHeader('Sorting Files'); } const logEntries = []; const movedFiles = []; if (!options.active) { await sortFiles(targetDir, options, logEntries, movedFiles); } if (options.log) { generateLog(targetDir, logEntries, options.dryRun); } if (!options.active) { if (options.dryRun) { console.log( `${chalk.yellow('Dry run completed. No files were moved.')}\n` ); } else { displayMovedFiles(movedFiles); console.log( chalk.green(`\n${movedFiles.length} file(s) organized successfully!\n`) ); } } // Handle active mode if (options.active) { const watcher = chokidar.watch(targetDir, { ignored: /(^|[\/\\])\../, // ignore dotfiles persistent: true, ignoreInitial: true, depth: options.sub ? undefined : 0, }); const handleFileChange = async (filePath) => { const fileName = path.basename(filePath); let subDirName; if (options.byName) { subDirName = fileName[0].toUpperCase(); } else if (options.byExtension) { subDirName = path.extname(fileName).replace('.', '').toUpperCase() || 'NO_EXT'; } else if (options.byDate) { subDirName = dayjs(fs.statSync(filePath).birthtime).format('YYYY-MM-DD'); } const relativeDir = path.relative(targetDir, path.dirname(filePath)); const finalSubDir = relativeDir === '' ? subDirName : path.join(relativeDir, subDirName); const subDirPath = path.join(targetDir, finalSubDir); const targetPath = path.join(subDirPath, fileName); displayMove(filePath, targetPath, options.dryRun); if (options.log) { const logEntry = options.dryRun ? `[DRY RUN] Would move: ${filePath}${targetPath}` : `Moved: ${filePath}${targetPath}`; logEntries.push(logEntry); generateLog(targetDir, [logEntry], options.dryRun); } if (!options.dryRun) { await fs.ensureDir(subDirPath); await fs.move(filePath, targetPath, { overwrite: false }); movedFiles.push({ from: filePath, to: targetPath }); console.log( chalk.green(`\n${movedFiles.length} file(s) organized successfully!\n`) ); } }; watcher .on('add', handleFileChange) .on('change', handleFileChange) .on('error', (error) => console.log(chalk.red(`Watcher error: ${error}`))); console.log(chalk.green('Watching for file changes... Press Ctrl+C to exit.')); } } catch (error) { spinner.fail(chalk.red(`Error: ${error.message}`)); process.exit(1); } })();