@happyvibess/node-cleaner
Version:
🧹 Interactive CLI tool to find and clean node_modules directories and free up space
179 lines (147 loc) • 5.04 kB
JavaScript
import { Command } from 'commander';
import chalk from 'chalk';
import inquirer from 'inquirer';
import ora from 'ora';
import bytes from 'bytes';
import fs from 'fs-extra';
import { glob } from 'glob';
import { createRequire } from 'module';
import path from 'path';
import { SingleBar, Presets } from 'cli-progress';
import boxen from 'boxen';
import { fileURLToPath } from 'url';
import figures from 'figures';
const require = createRequire(import.meta.url);
const packageJson = require('../package.json');
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const program = new Command();
// Beautiful CLI header
console.log(boxen(
chalk.bold.cyan(`Node Cleaner v${packageJson.version}`) + '\n' +
chalk.dim('Free up space by removing node_modules'),
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'cyan'
}
));
program
.version(packageJson.version)
.description('🧹 Interactive tool to clean node_modules directories')
.option('-p, --path <path>', 'Specify path to scan', process.cwd())
.option('-d, --depth <number>', 'Maximum depth to scan', '4')
.option('-f, --force', 'Skip confirmation', false)
.parse(process.argv);
const options = program.opts();
async function calculateDirSize(dir) {
const files = await glob('**/*', { cwd: dir, nodir: true });
let size = 0;
for (const file of files) {
const stats = await fs.stat(path.join(dir, file));
size += stats.size;
}
return size;
}
async function findNodeModules(scanPath, maxDepth) {
const spinner = ora('Scanning for node_modules directories...').start();
try {
const dirs = await glob('**/node_modules', {
cwd: scanPath,
ignore: ['**/node_modules/**/node_modules/**'],
maxDepth: parseInt(maxDepth)
});
if (dirs.length === 0) {
spinner.fail('No node_modules directories found.');
process.exit(0);
}
spinner.succeed(`Found ${chalk.green(dirs.length)} node_modules ${dirs.length === 1 ? 'directory' : 'directories'}`);
const progress = new SingleBar({
format: 'Calculating sizes |' + chalk.cyan('{bar}') + '| {percentage}% || {value}/{total} directories',
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591',
}, Presets.shades_classic);
progress.start(dirs.length, 0);
const dirDetails = await Promise.all(
dirs.map(async (dir, index) => {
const fullPath = path.join(scanPath, dir);
const size = await calculateDirSize(fullPath);
progress.update(index + 1);
return {
path: fullPath,
size,
formattedSize: bytes(size)
};
})
);
progress.stop();
const totalSize = dirDetails.reduce((acc, dir) => acc + dir.size, 0);
console.log('\n' + boxen(
chalk.bold('Space usage summary:') + '\n\n' +
chalk.cyan(`Total space used: ${bytes(totalSize)}`),
{ padding: 1, borderColor: 'yellow' }
));
return dirDetails;
} catch (error) {
spinner.fail('Error scanning directories');
console.error(chalk.red(error));
process.exit(1);
}
}
async function main() {
const nodeModules = await findNodeModules(options.path, options.depth);
if (!options.force) {
const { selected } = await inquirer.prompt([
{
type: 'checkbox',
name: 'selected',
message: 'Select node_modules directories to clean:',
choices: nodeModules.map(dir => ({
name: `${dir.path} ${chalk.cyan(`(${dir.formattedSize})`)}`,
value: dir.path,
short: path.basename(path.dirname(dir.path))
})),
pageSize: 15
}
]);
if (selected.length === 0) {
console.log(chalk.yellow('No directories selected. Exiting...'));
process.exit(0);
}
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: `Are you sure you want to delete ${chalk.red(selected.length)} node_modules ${selected.length === 1 ? 'directory' : 'directories'}?`,
default: false
}
]);
if (!confirm) {
console.log(chalk.yellow('Operation cancelled'));
process.exit(0);
}
const progress = new SingleBar({
format: 'Cleaning |' + chalk.cyan('{bar}') + '| {percentage}% || {value}/{total} directories',
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591',
}, Presets.shades_classic);
progress.start(selected.length, 0);
for (let i = 0; i < selected.length; i++) {
await fs.remove(selected[i]);
progress.update(i + 1);
}
progress.stop();
console.log(boxen(
chalk.green.bold(`${figures.tick} Cleanup complete!`) + '\n\n' +
`${chalk.cyan(selected.length)} directories cleaned\n` +
chalk.dim('Your project is now lighter and faster'),
{ padding: 1, borderColor: 'green' }
));
}
}
main().catch(error => {
console.error(chalk.red('Error:', error.message));
process.exit(1);
});