cleanout 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # ๐Ÿงน cleanout
2
+
3
+ > *Clean out the clutter, ship the code; one command and your project is clean.*
4
+
5
+ **`cleanout`** is a lightning-fast CLI tool designed to help you quickly scan and remove unnecessary files and folders from your project directories, freeing up valuable disk space. It automatically targets `node_modules`, build outputs, and log files, while being fully configurable to suit your needs.
6
+
7
+ ---
8
+
9
+ ## โšก Features
10
+
11
+ - **๐Ÿš€ Fast & Efficient**: Rapidly scans your project to find junk files.
12
+ - **๐Ÿ›ก๏ธ Safe by Default**: Always prompts for confirmation before deleting anything (unless you use `--yes`).
13
+ - **๐Ÿ” Dry Run Mode**: Preview exactly what will be deleted without touching any files using `--dry-run`.
14
+ - **โš™๏ธ Highly Configurable**: Use a `cleanout.config.json` file or CLI arguments to customize what gets cleaned.
15
+ - **๐ŸŽจ Beautiful Output**: Colorful and readable CLI interface to easily understand what is using your disk space.
16
+
17
+ ---
18
+
19
+ ## ๐Ÿ“ฆ Installation
20
+
21
+ Since it's an NPM package, you can run it via `npx` directly, or install it globally to use anywhere on your system.
22
+
23
+ **Install globally (Recommended):**
24
+ ```bash
25
+ npm install -g cleanout
26
+ ```
27
+
28
+ **Run via npx (Without installing):**
29
+ ```bash
30
+ npx cleanout
31
+ ```
32
+
33
+ ---
34
+
35
+ ## ๐Ÿ’ป Usage
36
+
37
+ Run `cleanout` in any project directory:
38
+
39
+ ```bash
40
+ cd my-project
41
+ cleanout
42
+ ```
43
+
44
+ ### ๐Ÿ› ๏ธ Options & Flags
45
+
46
+ | Flag | Alias | Description |
47
+ |---|---|---|
48
+ | `--dry-run` | `-dr` | Preview actions without deleting anything. |
49
+ | `--yes` | `-y` | Skip confirmation prompts (Warning: Deletes immediately). |
50
+ | `--depth=<n>` | `-d` | Set maximum folder scan depth (Default: Infinity). |
51
+ | `--include=<list>`| `-i` | Comma-separated extra patterns to clean (e.g., `dist,tmp,*.log`). |
52
+ | `--exclude=<list>`| `-e` | Comma-separated patterns to ignore (e.g., `node_modules,build`). |
53
+ | `--stats` | `-st` | Display folder statistics. |
54
+ | `--help` | `-h` | Show help information. |
55
+
56
+ ### ๐Ÿ’ก Examples
57
+
58
+ ```bash
59
+ # Preview what would be deleted in the src folder without actually deleting it
60
+ cleanout src --dry-run
61
+
62
+ # Quick cleanup in the current directory without prompts, limiting depth to 3 levels
63
+ cleanout . --yes --depth=3
64
+
65
+ # Clean the current directory but skip node_modules and dist
66
+ cleanout . --exclude=node_modules,dist
67
+
68
+ # Also clean up *.log and tmp files
69
+ cleanout . --include=*.log,tmp
70
+ ```
71
+
72
+ ---
73
+
74
+ ## โš™๏ธ Configuration (Optional)
75
+
76
+ You can create a `cleanout.config.json` file in your project root to define default behaviors.
77
+
78
+ CLI arguments will always override the values in your config file.
79
+
80
+ **Sample `cleanout.config.json`:**
81
+ ```json
82
+ {
83
+ "dryRun": false,
84
+ "yes": false,
85
+ "depth": 5,
86
+ "include": [
87
+ "*.log",
88
+ "tmp",
89
+ ".cache"
90
+ ],
91
+ "exclude": [
92
+ "dist",
93
+ "important_logs"
94
+ ]
95
+ }
96
+ ```
97
+
98
+ ---
99
+
100
+ ## ๐Ÿ“„ License
101
+
102
+ MIT ยฉ Jaswan Reddy Ch
@@ -0,0 +1,14 @@
1
+ {
2
+ "dryRun": false,
3
+ "yes": false,
4
+ "depth": 5,
5
+ "include": [
6
+ "*.log",
7
+ "tmp",
8
+ ".cache"
9
+ ],
10
+ "exclude": [
11
+ "dist",
12
+ "important_logs"
13
+ ]
14
+ }
@@ -0,0 +1 @@
1
+ {}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "cleanout",
3
+ "version": "1.0.0",
4
+ "description": "clean out the clutter, ship the code; one command your project is clean.",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "cleanout": "src/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/index.js"
12
+ },
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "keywords": [
17
+ "cli",
18
+ "clean",
19
+ "node_modules",
20
+ "build",
21
+ "cleanup"
22
+ ],
23
+ "author": "Jaswan Reddy Ch",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/jaswanreddych/cleanout"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/jaswanreddych/cleanout/issues"
31
+ },
32
+ "homepage": "https://github.com/jaswanreddych/cleanout#readme"
33
+ }
@@ -0,0 +1,72 @@
1
+ import fs from "fs"
2
+ import { c } from "../constants/color.js"
3
+ import { getSize, scan } from "../utils/scanner.js"
4
+ import { confirm, printDone, printDryRun, printEmptyMatches, printMatches, printStats } from "../utils/ui.js"
5
+
6
+ export async function cleanout(targetDir, config) {
7
+ const { dryRun, yes, stats, depth, include, exclude } = config
8
+
9
+ const matches = await scan(targetDir, depth, include, exclude)
10
+
11
+ for (const item of matches) {
12
+ item['size'] = await getSize(item.path)
13
+ }
14
+
15
+ if (!(matches.length)) {
16
+ printEmptyMatches()
17
+ return
18
+ }
19
+
20
+ let grandTotal = 0
21
+ const groupMatches = new Map()
22
+
23
+ for (const item of matches) {
24
+ const category = item.category
25
+ if (!(groupMatches.has(category))) {
26
+ groupMatches.set(category, {
27
+ category: category,
28
+ color: item.color,
29
+ items: [],
30
+ total: 0
31
+ })
32
+ }
33
+
34
+ const entry = groupMatches.get(category)
35
+ entry.items.push(item)
36
+ entry.total += item.size
37
+ grandTotal += item.size
38
+ }
39
+
40
+ const result = [...groupMatches.values()]
41
+ printMatches(result, grandTotal)
42
+
43
+ if (stats) {
44
+ printStats()
45
+ return
46
+ }
47
+
48
+ if (dryRun) {
49
+ printDryRun()
50
+ return
51
+ }
52
+
53
+ if (!yes) {
54
+ const confirmation = await confirm(`Are you sure you want to clean all the files and folders (${matches.length})?`)
55
+ if (!confirmation) {
56
+ console.log(`Operation cancelled (No files/folders are untouched) `)
57
+ return
58
+ }
59
+ }
60
+
61
+ let space = 0
62
+ for (const item of matches) {
63
+ try {
64
+ fs.rmSync(item.path, { recursive: true, force: true })
65
+ space += item.size
66
+ } catch (error) {
67
+ console.log(`${c.red}โœ– failed to delete ${item.path}: ${error.message}${c.reset}`)
68
+ }
69
+ }
70
+
71
+ printDone(space)
72
+ }
@@ -0,0 +1,15 @@
1
+ export const c = {
2
+ reset: '\x1b[0m',
3
+ bold: '\x1b[1m',
4
+ dim: '\x1b[2m',
5
+ green: '\x1b[32m',
6
+ yellow: '\x1b[33m',
7
+ red: '\x1b[31m',
8
+ cyan: '\x1b[36m',
9
+ gray: '\x1b[90m',
10
+ white: '\x1b[97m',
11
+ error: "\x1b[31m",
12
+ success: "\x1b[32m",
13
+ info: "\x1b[33m",
14
+ warning: "\x1b[33m",
15
+ }
@@ -0,0 +1,57 @@
1
+ export const EXAMPLES = [
2
+ {
3
+ cmd: "cleanout",
4
+ desc: "Scan current directory",
5
+ },
6
+ {
7
+ cmd: "cleanout src --dry-run",
8
+ desc: "Preview cleanup without deleting files",
9
+ },
10
+ {
11
+ cmd: "cleanout . --yes --depth=3",
12
+ desc: "Quick cleanup with depth limit",
13
+ },
14
+ {
15
+ cmd: "cleanout . --exclude=node_modules,dist",
16
+ desc: "Skip build folders",
17
+ },
18
+ {
19
+ cmd: "cleanout . --include=*.log,tmp",
20
+ desc: "Include extra cleanup patterns",
21
+ }
22
+ ];
23
+
24
+ export const OPTIONS = [
25
+ {
26
+ flags: "-h, --help",
27
+ desc: "Show help information",
28
+ },
29
+ {
30
+ flags: "-v, --version",
31
+ desc: "Show current version",
32
+ },
33
+ {
34
+ flags: "-dr, --dry-run",
35
+ desc: "Preview actions without deleting files",
36
+ },
37
+ {
38
+ flags: "-y, --yes",
39
+ desc: "Skip confirmation prompts",
40
+ },
41
+ {
42
+ flags: "-st, --stats",
43
+ desc: "Display folder statistics",
44
+ },
45
+ {
46
+ flags: "-d, --depth=<number>",
47
+ desc: "Maximum folder scan depth (Default: Infinity)",
48
+ },
49
+ {
50
+ flags: "-i, --include=<list>",
51
+ desc: "Additional patterns to include (e.g. dist,tmp,*.log)",
52
+ },
53
+ {
54
+ flags: "-e, --exclude=<list>",
55
+ desc: "Patterns to ignore (e.g. node_modules,build,vendor)",
56
+ }
57
+ ];
@@ -0,0 +1,32 @@
1
+ export const DEFAULT_TARGETS = [
2
+ {
3
+ "name": "node_modules",
4
+ "patterns": [
5
+ "node_modules"
6
+ ],
7
+ "color": "\x1b[36m"
8
+ },
9
+ {
10
+ "name": "Build output",
11
+ "patterns": [
12
+ "dist",
13
+ "build",
14
+ "out",
15
+ ".next",
16
+ ".nuxt",
17
+ ".svelte-kit",
18
+ ".vite",
19
+ "__pycache__"
20
+ ],
21
+ "color": "\x1b[33m"
22
+ },
23
+ {
24
+ "name": "Log files",
25
+ "patterns": [
26
+ "*.log",
27
+ "npm-debug.log*",
28
+ "yarn-error.log"
29
+ ],
30
+ "color": "\x1b[90m"
31
+ },
32
+ ]
package/src/index.js ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "path"
4
+ import { cleanout } from "./commands/cleanout.js"
5
+ import { c } from "./constants/color.js"
6
+ import { getVersion, loadConfig } from "./utils/config.js"
7
+ import { parseArgs } from "./utils/parseArgs.js"
8
+ import { printBanner, printHelp, printVersion } from "./utils/ui.js"
9
+
10
+ async function main() {
11
+ const args = parseArgs(process.argv.slice(2))
12
+ printBanner()
13
+ if (args.version) {
14
+ printVersion(getVersion())
15
+ process.exit(0)
16
+ }
17
+
18
+ if (args.help) {
19
+ printHelp()
20
+ process.exit(0)
21
+ }
22
+
23
+ const targetDir = args.path !== "." ? path.resolve(args.path) : process.cwd()
24
+ const config = loadConfig(targetDir, args)
25
+ await cleanout(targetDir, config)
26
+ process.exit(0)
27
+ }
28
+
29
+ main().catch(err => {
30
+ console.log(`\n ${c.error} ${err.message} ${c.reset}\n`)
31
+ process.exit(1)
32
+ })
@@ -0,0 +1,42 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+ import { fileURLToPath } from "url"
4
+
5
+ export function getVersion() {
6
+ try {
7
+ const packagePath = fileURLToPath(new URL("../../package.json", import.meta.url))
8
+ const packageJSon = JSON.parse(fs.readFileSync(packagePath, "utf-8"))
9
+ return packageJSon.version ?? "0.0.0"
10
+ } catch {
11
+ return "0.0.0"
12
+ }
13
+ }
14
+
15
+ export function loadConfig(targetDir, args) {
16
+ const userConfigPath = path.join(targetDir, "cleanout.config.json");
17
+
18
+ let userConfig = {};
19
+
20
+ if (fs.existsSync(userConfigPath)) {
21
+ try {
22
+ userConfig = JSON.parse(
23
+ fs.readFileSync(userConfigPath, "utf-8")
24
+ );
25
+ } catch {
26
+ throw new Error("Invalid cleanout.config.json file");
27
+ }
28
+ }
29
+
30
+ return {
31
+ ...userConfig,
32
+ ...args,
33
+ include: [
34
+ ...(userConfig.include ?? []),
35
+ ...(args.include ?? [])
36
+ ],
37
+ exclude: [
38
+ ...(userConfig.exclude ?? []),
39
+ ...(args.exclude ?? [])
40
+ ]
41
+ };
42
+ }
@@ -0,0 +1,29 @@
1
+ function parseList(value) {
2
+ return value
3
+ .split(",")
4
+ .map(item => item.trim())
5
+ .filter(Boolean)
6
+ }
7
+
8
+ function requireValue(value, message) {
9
+ if (!value) {
10
+ throw new Error(message)
11
+ }
12
+ return value
13
+ }
14
+
15
+ function formatSize(bytes) {
16
+ try {
17
+ if (!Number.isInteger(bytes) || bytes < 0) {
18
+ return "0 b"
19
+ }
20
+ const SIZES = ["b", "kb", "mb", "gb", "tb", "pb"]
21
+ const i = Math.floor(Math.log(bytes) / Math.log(1024))
22
+ return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${SIZES[i]}`
23
+ } catch {
24
+ return "0 b"
25
+ }
26
+ }
27
+
28
+ export { formatSize, parseList, requireValue }
29
+
@@ -0,0 +1,56 @@
1
+ import { parseList, requireValue } from "./helper.js"
2
+
3
+ export function parseArgs(args) {
4
+ const result = {
5
+ path: ".",
6
+ dryRun: false,
7
+ help: false,
8
+ version: false,
9
+ yes: false,
10
+ stats: false,
11
+ depth: Infinity,
12
+ include: [],
13
+ exclude: [],
14
+ }
15
+
16
+ const aliases = {
17
+ "--dry-run": "dryRun",
18
+ "-dr": "dryRun",
19
+
20
+ "--help": "help",
21
+ "-h": "help",
22
+
23
+ "--version": "version",
24
+ "-v": "version",
25
+
26
+ "--yes": "yes",
27
+ "-y": "yes",
28
+
29
+ "--stats": "stats",
30
+ "-st": "stats"
31
+ }
32
+
33
+ for (const arg of args) {
34
+ if (aliases[arg]) {
35
+ result[aliases[arg]] = true
36
+ } else if (arg.startsWith("--depth") || arg.startsWith("-d")) {
37
+ const value = requireValue(arg.split("=")[1], "Invalid depth value")
38
+ const depth = parseInt(value, 10)
39
+ if (!Number.isInteger(depth) || depth < 1) {
40
+ throw new Error("Depth must be a positive integer");
41
+ }
42
+ result.depth = depth
43
+ } else if (arg.startsWith("--exclude") || arg.startsWith("-e")) {
44
+ const value = requireValue(arg?.split("=")[1], "Invalid exclude pattern");
45
+ result.exclude.push(...parseList(value))
46
+ } else if (arg.startsWith("--include") || arg.startsWith("-i")) {
47
+ const value = requireValue(arg?.split("=")[1], "Invalid include patterns");
48
+ result.include.push(...parseList(value))
49
+ } else if (!arg.startsWith("-")) {
50
+ result.path = arg.trim()
51
+ } else {
52
+ throw new Error(`Invalid argument: ${arg}. Use --help to see available options.`)
53
+ }
54
+ }
55
+ return result
56
+ }
@@ -0,0 +1,104 @@
1
+ import { DEFAULT_TARGETS } from "../constants/targets.js";
2
+ import { c } from "../constants/color.js";
3
+ import fsp from "fs/promises";
4
+ import path from "path";
5
+
6
+ function globToRegex(pattern) {
7
+ const escaped = pattern
8
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
9
+ .replace(/\*/g, ".*");
10
+
11
+ return new RegExp(`^${escaped}$`, "i");
12
+ }
13
+
14
+ async function searchDir(dir, matchers, depth, maxDepth) {
15
+ if (depth > maxDepth) {
16
+ return [];
17
+ }
18
+
19
+ let entries;
20
+
21
+ try {
22
+ entries = await fsp.readdir(dir, { withFileTypes: true });
23
+ } catch {
24
+ return [];
25
+ }
26
+
27
+ const results = [];
28
+
29
+ for (const entry of entries) {
30
+ const fullPath = path.join(dir, entry.name);
31
+
32
+ const matched = matchers.find(({ regex }) =>
33
+ regex.test(entry.name)
34
+ );
35
+
36
+ if (matched) {
37
+ results.push({
38
+ name: entry.name,
39
+ path: fullPath,
40
+ isDirectory: entry.isDirectory(),
41
+ category: matched.category,
42
+ pattern: matched.pattern,
43
+ color: matched.color
44
+ });
45
+ continue;
46
+ }
47
+
48
+ if (entry.isDirectory()) {
49
+ const nestedResults = await searchDir(
50
+ fullPath,
51
+ matchers,
52
+ depth + 1,
53
+ maxDepth
54
+ );
55
+
56
+ results.push(...nestedResults);
57
+ }
58
+ }
59
+
60
+ return results;
61
+ }
62
+
63
+ export async function scan(targetDir, depth, include, exclude) {
64
+ const targets = [
65
+ ...DEFAULT_TARGETS,
66
+ {
67
+ name: "Custom Includes",
68
+ patterns: include,
69
+ color: c.yellow,
70
+ },
71
+ ];
72
+
73
+ const matchers = targets.flatMap(target =>
74
+ target.patterns
75
+ .filter(pattern => pattern && !exclude.includes(pattern))
76
+ .map(pattern => ({
77
+ category: target.name,
78
+ color: target.color,
79
+ pattern,
80
+ regex: globToRegex(pattern),
81
+ }))
82
+ );
83
+
84
+ return searchDir(
85
+ targetDir,
86
+ matchers,
87
+ 0,
88
+ depth
89
+ );
90
+ }
91
+
92
+ export async function getSize(targetPath) {
93
+ try {
94
+ const stat = await fsp.stat(targetPath)
95
+ if (!stat.isDirectory()) return stat.size
96
+ const files = await fsp.readdir(targetPath)
97
+ const sizes = await Promise.all(
98
+ files.map(file => getSize(path.join(targetPath, file)))
99
+ )
100
+ return sizes.reduce((acc, s) => acc + s, 0)
101
+ } catch {
102
+ return 0
103
+ }
104
+ }
@@ -0,0 +1,110 @@
1
+ import { c } from "../constants/color.js";
2
+ import { EXAMPLES, OPTIONS } from "../constants/common.js";
3
+ import { formatSize } from "./helper.js";
4
+
5
+ function printOptions(options) {
6
+ const pad = Math.max(...options.map(o => o.flags.length));
7
+
8
+ for (const opt of options) {
9
+ const spacing = " ".repeat(pad - opt.flags.length + 2);
10
+
11
+ console.log(
12
+ ` ${c.yellow}${opt.flags}${c.reset}${spacing}${opt.desc}`
13
+ );
14
+ }
15
+ }
16
+
17
+ function printExamples(examples) {
18
+ for (const ex of examples) {
19
+ console.log(` ${c.gray}${ex.cmd}${c.reset}`);
20
+ console.log(` ${c.dim}${ex.desc}${c.reset}`);
21
+ console.log();
22
+ }
23
+ }
24
+
25
+
26
+ export function printBanner() {
27
+ console.log(`\n\t${c.cyan}${c.bold}cleanout${c.reset} ${c.gray}โ€” clean out the clutter, ship the code; one command your project is clean.${c.reset}`)
28
+ console.log(`\t${c.gray}${'โ”€'.repeat(85)}${c.reset}\n`)
29
+ }
30
+
31
+ export function printVersion(version) {
32
+ console.log(`${c.bold}Version:${c.reset} ${c.dim}${version}${c.reset}`)
33
+ }
34
+
35
+ export function printHelp() {
36
+ console.log(`
37
+ ${c.bold}Usage:${c.reset}
38
+ cleanout ${c.yellow}[path] [options]${c.reset}
39
+
40
+ ${c.bold}Description:${c.reset}
41
+ Clean and analyze project folders by finding unnecessary files,
42
+ generating statistics, and producing reports.
43
+
44
+ ${c.bold}Arguments:${c.reset}
45
+ ${c.yellow}path${c.reset} Project directory to scan (Default: current directory)
46
+ `);
47
+
48
+ console.log(`${c.bold}Options:${c.reset}`);
49
+ printOptions(OPTIONS);
50
+
51
+ console.log(`\n${c.bold}Examples:${c.reset}`);
52
+ printExamples(EXAMPLES);
53
+ }
54
+
55
+ export function printEmptyMatches() {
56
+ console.log(`${c.green}โœ” Nothing found to clean!${c.reset}\n`)
57
+ }
58
+
59
+ function printCategory(group) {
60
+ const { category, items, color, total } = group
61
+ const noOfItems = items.length
62
+ console.log(`${color ?? c['white']}${category} (${noOfItems}) Total Size : ${formatSize(total)} ${c.reset}`)
63
+ }
64
+
65
+ function printItem(item) {
66
+ const { name, path, isDirectory, size } = item
67
+ const icon = isDirectory ? "๐Ÿ“" : "๐Ÿ“„"
68
+ console.log(` -> ${icon} ${c.white}${name}${c.reset}${c.gray} (${formatSize(size)}) ${path}${c.reset}`)
69
+ }
70
+
71
+
72
+ function printTotal(total) {
73
+ console.log(`${c.bold}Total: ${c.reset} ${formatSize(total)}\n`)
74
+ }
75
+
76
+ export function printMatches(groupMatches, grandTotal) {
77
+ for (const group of groupMatches) {
78
+ printCategory(group)
79
+ console.log()
80
+ for (const item of group.items) {
81
+ printItem(item)
82
+ }
83
+ console.log()
84
+ }
85
+ printTotal(grandTotal)
86
+ }
87
+
88
+ export function printStats() {
89
+ console.log(`${c.green}This is report of scan (files or folder) found to clean up ${c.reset}\n`)
90
+ }
91
+
92
+ export function printDryRun() {
93
+ console.log(`${c.green}DRY RUN - Completed (no files or folder are deleted) ${c.reset}\n`)
94
+ }
95
+
96
+ export function confirm(question) {
97
+ process.stdout.write(` ${c.white}${question}${c.reset} ${c.gray}(y/N)${c.reset} `)
98
+ return new Promise((resolve) => {
99
+ process.stdin.setEncoding("utf8")
100
+ process.stdin.once("data", (data) => {
101
+ const answer = data.trim().toLowerCase()
102
+ process.stdin.pause()
103
+ resolve(answer === "y" || answer === "yes")
104
+ })
105
+ })
106
+ }
107
+
108
+ export function printDone(space) {
109
+ console.log(`\n${c.green}${c.bold}โœ” Done!${c.reset} Total ${c.yellow}${formatSize(space)}${c.reset} freed`)
110
+ }