clearnpmod
Version:
A CLI tool to find and clean up node_modules directories recursively
185 lines (157 loc) ⢠5.38 kB
JavaScript
/**
* clearnpmod - A CLI tool to find and clean up node_modules directories
*
* Usage:
* clearnpmod <path>
*
* Scans recursively for all "node_modules" folders under <path>,
* presents them in a checkbox list (all checked by default),
* and then deletes the ones you leave checked.
*/
const fs = require('fs');
const path = require('path');
/**
* Recursively find all node_modules directories in the given directory
* @param {string} dir - Directory to search
* @param {string[]} results - Array to store found paths
*/
async function findNodeModules(dir, results) {
let entries;
try {
entries = await fs.promises.readdir(dir, { withFileTypes: true });
} catch (err) {
// Skip folders we can't read (e.g. permissions)
return;
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const fullPath = path.join(dir, entry.name);
if (entry.name === 'node_modules') {
results.push(fullPath);
// Don't recurse into this node_modules; we want nested node_modules to be caught only by
// higher levels (if they exist), but not traverse inside each one.
continue;
}
// Skip common directories that shouldn't contain node_modules
if (['.git', '.vscode', '.idea', 'node_modules'].includes(entry.name)) {
continue;
}
await findNodeModules(fullPath, results);
}
}
/**
* Format file size in human readable format
* @param {number} bytes - Size in bytes
* @returns {string} Formatted size string
*/
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Get directory size (approximation for display purposes)
* @param {string} dirPath - Path to directory
* @returns {Promise<number>} Size in bytes
*/
async function getDirectorySize(dirPath) {
try {
const stats = await fs.promises.stat(dirPath);
return stats.size || 0;
} catch {
return 0;
}
}
async function main() {
const args = process.argv.slice(2);
// Handle help flag
if (args.includes('--help') || args.includes('-h')) {
console.log(`
clearnpmod - Clean up node_modules directories
Usage:
clearnpmod <path> Clean node_modules in specified directory
clearnpmod --help, -h Show this help message
clearnpmod --version, -v Show version
Examples:
clearnpmod . Clean current directory
clearnpmod ~/Development Clean all projects in Development folder
`);
process.exit(0);
}
// Handle version flag
if (args.includes('--version') || args.includes('-v')) {
const packageJson = require('./package.json');
console.log(packageJson.version);
process.exit(0);
}
if (args.length !== 1) {
console.error('ā Error: Please provide exactly one path argument.');
console.error('Usage: clearnpmod <path>');
console.error('Run "clearnpmod --help" for more information.');
process.exit(1);
}
const basePath = path.resolve(args[0]);
let stat;
try {
stat = await fs.promises.stat(basePath);
} catch (err) {
console.error(`ā Error: Path "${basePath}" does not exist or is not accessible.`);
process.exit(1);
}
if (!stat.isDirectory()) {
console.error(`ā Error: "${basePath}" is not a directory.`);
process.exit(1);
}
console.log(`š Scanning for node_modules folders under:\n ${basePath}\nā³ This may take a moment...`);
const nmPaths = [];
await findNodeModules(basePath, nmPaths);
if (nmPaths.length === 0) {
console.log('⨠No node_modules folders found. Your directory is already clean!');
process.exit(0);
}
console.log(`\nš Found ${nmPaths.length} node_modules director${nmPaths.length === 1 ? 'y' : 'ies'}:`);
// Import inquirer dynamically for ES module compatibility
const inquirer = (await import('inquirer')).default;
// Prompt user with a checkbox list (all checked by default)
const { toDelete } = await inquirer.prompt([
{
type: 'checkbox',
name: 'toDelete',
message: 'Select which node_modules directories you want to delete:',
choices: nmPaths.map(p => ({ name: p, value: p, checked: true })),
pageSize: 15,
loop: false,
},
]);
if (toDelete.length === 0) {
console.log('š« Nothing selected. Exiting without deleting anything.');
process.exit(0);
}
console.log(`\nšļø Deleting ${toDelete.length} folder(s)...`);
let successCount = 0;
let failCount = 0;
for (const dir of toDelete) {
try {
// Node 14+: fs.rm with recursive option
await fs.promises.rm(dir, { recursive: true, force: true });
console.log(`ā
Deleted: ${dir}`);
successCount++;
} catch (err) {
console.error(`ā Failed to delete: ${dir}`);
console.error(` Reason: ${err.message}`);
failCount++;
}
}
console.log(`\nš Done! Successfully deleted ${successCount} folder(s).`);
if (failCount > 0) {
console.log(`ā ļø Failed to delete ${failCount} folder(s).`);
}
console.log('š” Tip: You can reinstall dependencies with "npm install" or "pnpm install"');
}
main().catch(err => {
console.error('š„ Unexpected error:', err.message);
process.exit(1);
});