autosort
Version:
A modern CLI tool to organize files in a directory.
345 lines (303 loc) • 10.6 kB
JavaScript
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);
}
})();