repo-clean 1.0.2 → 1.2.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 CHANGED
@@ -22,10 +22,14 @@ repo-clean
22
22
  Use the command patterns below with either `npx repo-clean` or `repo-clean` (after global install).
23
23
 
24
24
  ```bash
25
- repo-clean [--dry-run] [--node-modules] [--pm-cache] [--logs] [--editor] [--tmp]
25
+ repo-clean [--dry-run] [--node-modules] [--pm-cache] [--logs] [--editor] [--tmp] [--force]
26
26
  repo-clean --all [--keep-nm] [--dry-run]
27
+ repo-clean --help | -h
28
+ repo-clean --version | -v
27
29
  ```
28
30
 
31
+ `repo-clean` accepts flags only. Positional arguments are rejected with an error.
32
+
29
33
  ### Default behavior (no flags)
30
34
 
31
35
  Removes:
@@ -43,6 +47,7 @@ Removes:
43
47
 
44
48
  - `--dry-run` Print what would be removed without deleting anything.
45
49
  - `--node-modules` Also remove `node_modules`.
50
+ - `--force` Skip confirmation prompt for `node_modules` removal.
46
51
  - `--pm-cache` Also remove package manager caches/stores:
47
52
  - `.yarn/cache`
48
53
  - `.yarn/unplugged`
@@ -59,6 +64,8 @@ Removes:
59
64
  - `--tmp` Also remove `tmp` and `temp`.
60
65
  - `--all` Include all optional cleanup categories above.
61
66
  - `--keep-nm` With `--all`, keep `node_modules`.
67
+ - `--help`, `-h` Print usage and flags.
68
+ - `--version`, `-v` Print the CLI version.
62
69
 
63
70
  ## Examples
64
71
 
@@ -67,10 +74,19 @@ Removes:
67
74
  npx repo-clean --dry-run
68
75
 
69
76
  # Remove default targets + node_modules
77
+ npx repo-clean --node-modules --force
78
+
79
+ # Prompted node_modules removal (confirm with y/yes)
70
80
  npx repo-clean --node-modules
71
81
 
72
82
  # Full cleanup except node_modules
73
83
  npx repo-clean --all --keep-nm
84
+
85
+ # Print installed version
86
+ npx repo-clean --version
87
+
88
+ # Invalid (positional argument): exits with code 2
89
+ npx repo-clean my-folder
74
90
  ```
75
91
 
76
92
  ## Issues and Support
@@ -101,6 +117,7 @@ Contributions are welcome.
101
117
  ```bash
102
118
  git clone https://github.com/arbenkryemadhi/repo-clean.git
103
119
  cd repo-clean
120
+ npm test
104
121
  node ./bin/repo-clean.js --help
105
122
  node ./bin/repo-clean.js --dry-run
106
123
  ```
package/bin/repo-clean.js CHANGED
@@ -1,30 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  /* repo-clean: remove common repo build outputs and caches */
3
3
 
4
- const fs = require("node:fs");
5
- const path = require("node:path");
4
+ const { parseCliArgs } = require("../lib/args");
5
+ const { buildCleanupPlan } = require("../lib/targets");
6
+ const { removeTargets, removeRootLogs } = require("../lib/cleanup");
7
+ const { confirmNodeModulesRemoval } = require("../lib/confirm");
8
+ const { version: packageVersion } = require("../package.json");
6
9
 
7
10
  const argv = process.argv.slice(2);
8
- const args = new Set(argv);
11
+ const parsed = parseCliArgs(argv);
9
12
 
10
- const dryRun = args.has("--dry-run");
11
- const help = args.has("--help") || args.has("-h");
13
+ if (!parsed.ok) {
14
+ console.error(parsed.error);
15
+ process.exit(parsed.exitCode);
16
+ }
12
17
 
13
- const flagAll = args.has("--all");
14
- const keepNm = args.has("--keep-nm");
18
+ const { dryRun, help, version, force, flagAll, keepNm } = parsed.options;
15
19
 
16
- const flagNodeModules = args.has("--node-modules");
17
- const flagPmCache = args.has("--pm-cache");
18
- const flagLogs = args.has("--logs");
19
- const flagEditor = args.has("--editor");
20
- const flagTmp = args.has("--tmp");
20
+ if (version) {
21
+ console.log(packageVersion);
22
+ process.exit(0);
23
+ }
21
24
 
22
25
  if (help) {
23
26
  console.log(`repo-clean
24
27
 
25
28
  Usage:
26
- repo-clean [--dry-run] [--node-modules] [--pm-cache] [--logs] [--editor] [--tmp]
29
+ repo-clean [--dry-run] [--node-modules] [--pm-cache] [--logs] [--editor] [--tmp] [--force]
27
30
  repo-clean --all [--keep-nm] [--dry-run]
31
+ repo-clean --version | -v
28
32
 
29
33
  Defaults (no flags):
30
34
  Removes build outputs + framework caches:
@@ -39,134 +43,46 @@ Flags:
39
43
  --tmp Also remove tmp/temp folders
40
44
  --all Default + all flags above
41
45
  --keep-nm With --all, keep node_modules (do not remove it)
46
+ --force Skip confirmation prompt for node_modules removal
47
+ --version, -v Print CLI version
42
48
  `);
43
49
  process.exit(0);
44
50
  }
45
51
 
46
52
  const cwd = process.cwd();
47
53
 
48
- const defaultTargets = [
49
- "dist",
50
- "build",
51
- "coverage",
52
- ".next",
53
- ".turbo",
54
- ".vite",
55
- ".parcel-cache",
56
- ".cache",
57
- ];
58
-
59
- const editorTargets = [".vscode", ".idea"];
60
- const tmpTargets = ["tmp", "temp"];
61
-
62
- // Mix of directories + files that can exist in a repo for Yarn/Pnpm setups.
63
- // (No globs; exact known paths only.)
64
- const pmCacheTargets = [
65
- ".yarn/cache",
66
- ".yarn/unplugged",
67
- ".yarn/install-state.gz",
68
- ".pnp.cjs",
69
- ".pnp.loader.mjs",
70
- ".pnpm-store",
71
- ];
72
-
73
- // Log file prefixes to delete in repo root (covers e.g. npm-debug.log.123)
74
- const logPrefixes = [
75
- "npm-debug.log",
76
- "yarn-error.log",
77
- "pnpm-debug.log",
78
- "lerna-debug.log",
79
- ];
80
-
81
- function exists(p) {
82
- try {
83
- fs.accessSync(p);
84
- return true;
85
- } catch {
86
- return false;
87
- }
88
- }
89
-
90
- function removePath(absPath) {
91
- if (!exists(absPath)) return false;
92
-
93
- if (dryRun) {
94
- console.log(`[dry-run] would remove: ${absPath}`);
95
- return true;
96
- }
97
-
98
- fs.rmSync(absPath, {
99
- recursive: true,
100
- force: true,
101
- maxRetries: 2,
102
- retryDelay: 50,
103
- });
104
- console.log(`removed: ${absPath}`);
105
- return true;
106
- }
107
-
108
- function gatherRootLogs() {
109
- const found = [];
110
- let entries;
111
- try {
112
- entries = fs.readdirSync(cwd, { withFileTypes: true });
113
- } catch {
114
- return found;
115
- }
54
+ async function main() {
55
+ const cleanupPlan = buildCleanupPlan(parsed.options);
56
+ const { targets, includeLogs, includeNodeModules, logPrefixes } = cleanupPlan;
116
57
 
117
- for (const ent of entries) {
118
- if (!ent.isFile()) continue;
119
- const name = ent.name;
120
- if (logPrefixes.some((p) => name === p || name.startsWith(p))) {
121
- found.push(name);
58
+ if (includeNodeModules && !dryRun && !force) {
59
+ const confirmed = await confirmNodeModulesRemoval();
60
+ if (!confirmed) {
61
+ console.log("Aborted. No files were removed.");
62
+ process.exit(1);
122
63
  }
123
64
  }
124
- return found;
125
- }
126
-
127
- // Determine what to remove
128
- const targets = new Set(defaultTargets);
129
-
130
- const includePmCache = flagAll || flagPmCache;
131
- const includeLogs = flagAll || flagLogs;
132
- const includeEditor = flagAll || flagEditor;
133
- const includeTmp = flagAll || flagTmp;
134
65
 
135
- // node_modules inclusion logic:
136
- // - default: off
137
- // - --node-modules: on
138
- // - --all: on, unless --keep-nm is also present
139
- const includeNodeModules = (flagAll || flagNodeModules) && !(flagAll && keepNm);
66
+ let removedCount = removeTargets(cwd, targets, dryRun);
140
67
 
141
- if (includePmCache) for (const t of pmCacheTargets) targets.add(t);
142
- if (includeEditor) for (const t of editorTargets) targets.add(t);
143
- if (includeTmp) for (const t of tmpTargets) targets.add(t);
144
- if (includeNodeModules) targets.add("node_modules");
145
-
146
- // Remove fixed targets
147
- let removedCount = 0;
148
-
149
- for (const rel of Array.from(targets).sort()) {
150
- const abs = path.join(cwd, rel);
151
- if (removePath(abs)) removedCount += 1;
152
- }
153
-
154
- // Remove root log files (if enabled)
155
- if (includeLogs) {
156
- const logs = gatherRootLogs();
157
- for (const name of logs.sort()) {
158
- const abs = path.join(cwd, name);
159
- if (removePath(abs)) removedCount += 1;
68
+ if (includeLogs) {
69
+ removedCount += removeRootLogs(cwd, logPrefixes, dryRun);
160
70
  }
71
+
72
+ const nmNote =
73
+ flagAll && keepNm
74
+ ? " (kept node_modules)"
75
+ : includeNodeModules
76
+ ? " (includes node_modules)"
77
+ : "";
78
+ const mode = dryRun ? "done (dry-run)" : "done";
79
+ console.log(
80
+ `${mode}. ${dryRun ? "would remove" : "removed"} ${removedCount} item(s).${nmNote}`,
81
+ );
161
82
  }
162
83
 
163
- const nmNote =
164
- flagAll && keepNm
165
- ? " (kept node_modules)"
166
- : includeNodeModules
167
- ? " (includes node_modules)"
168
- : "";
169
- const mode = dryRun ? "done (dry-run)" : "done";
170
- console.log(
171
- `${mode}. ${dryRun ? "would remove" : "removed"} ${removedCount} item(s).${nmNote}`,
172
- );
84
+ main().catch((err) => {
85
+ const msg = err && err.message ? err.message : String(err);
86
+ console.error(`Error: ${msg}`);
87
+ process.exit(1);
88
+ });
package/lib/args.js ADDED
@@ -0,0 +1,135 @@
1
+ const allowedFlags = new Set([
2
+ "--dry-run",
3
+ "--help",
4
+ "-h",
5
+ "--version",
6
+ "-v",
7
+ "--force",
8
+ "--all",
9
+ "--keep-nm",
10
+ "--node-modules",
11
+ "--pm-cache",
12
+ "--logs",
13
+ "--editor",
14
+ "--tmp",
15
+ ]);
16
+
17
+ const EXIT_INVALID_ARGS = 2;
18
+
19
+ function isFlagToken(arg) {
20
+ return arg.startsWith("-");
21
+ }
22
+
23
+ function editDistance(a, b) {
24
+ const m = a.length;
25
+ const n = b.length;
26
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
27
+
28
+ for (let i = 0; i <= m; i += 1) dp[i][0] = i;
29
+ for (let j = 0; j <= n; j += 1) dp[0][j] = j;
30
+
31
+ for (let i = 1; i <= m; i += 1) {
32
+ for (let j = 1; j <= n; j += 1) {
33
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
34
+ dp[i][j] = Math.min(
35
+ dp[i - 1][j] + 1,
36
+ dp[i][j - 1] + 1,
37
+ dp[i - 1][j - 1] + cost,
38
+ );
39
+ }
40
+ }
41
+
42
+ return dp[m][n];
43
+ }
44
+
45
+ function getClosestFlag(input) {
46
+ let best = null;
47
+ let bestDistance = Number.POSITIVE_INFINITY;
48
+
49
+ for (const flag of allowedFlags) {
50
+ const dist = editDistance(input, flag);
51
+ if (dist < bestDistance) {
52
+ best = flag;
53
+ bestDistance = dist;
54
+ }
55
+ }
56
+
57
+ if (!best) return null;
58
+ if (bestDistance > 3) return null;
59
+ return best;
60
+ }
61
+
62
+ function formatUnknownFlagError(unknownFlags) {
63
+ const unknownList = unknownFlags.join(", ");
64
+ const flagLabel = unknownFlags.length === 1 ? "flag" : "flags";
65
+ const suggestions = unknownFlags
66
+ .map((flag) => {
67
+ const suggestion = getClosestFlag(flag);
68
+ return suggestion ? { flag, suggestion } : null;
69
+ })
70
+ .filter(Boolean);
71
+
72
+ let suggestionText = "";
73
+ if (suggestions.length === 1 && unknownFlags.length === 1) {
74
+ suggestionText = ` Did you mean ${suggestions[0].suggestion}?`;
75
+ } else if (suggestions.length > 0) {
76
+ const pairs = suggestions.map(
77
+ (item) => `${item.flag} -> ${item.suggestion}`,
78
+ );
79
+ suggestionText = ` Did you mean: ${pairs.join(", ")}?`;
80
+ }
81
+
82
+ return `Unknown ${flagLabel}: ${unknownList}. This looks like a typo.${suggestionText} Run repo-clean --help for valid options.`;
83
+ }
84
+
85
+ function formatPositionalArgError(positionalArgs) {
86
+ const argLabel = positionalArgs.length === 1 ? "argument" : "arguments";
87
+ const list = positionalArgs.join(", ");
88
+ return `Unexpected positional ${argLabel}: ${list}. repo-clean accepts flags only. Run repo-clean --help for usage.`;
89
+ }
90
+
91
+ function parseCliArgs(argv) {
92
+ const args = new Set(argv);
93
+
94
+ const unknownFlags = argv.filter(
95
+ (arg) => isFlagToken(arg) && !allowedFlags.has(arg),
96
+ );
97
+ if (unknownFlags.length > 0) {
98
+ return {
99
+ ok: false,
100
+ exitCode: EXIT_INVALID_ARGS,
101
+ error: formatUnknownFlagError(unknownFlags),
102
+ };
103
+ }
104
+
105
+ const positionalArgs = argv.filter((arg) => !isFlagToken(arg));
106
+ if (positionalArgs.length > 0) {
107
+ return {
108
+ ok: false,
109
+ exitCode: EXIT_INVALID_ARGS,
110
+ error: formatPositionalArgError(positionalArgs),
111
+ };
112
+ }
113
+
114
+ return {
115
+ ok: true,
116
+ options: {
117
+ dryRun: args.has("--dry-run"),
118
+ help: args.has("--help") || args.has("-h"),
119
+ version: args.has("--version") || args.has("-v"),
120
+ force: args.has("--force"),
121
+ flagAll: args.has("--all"),
122
+ keepNm: args.has("--keep-nm"),
123
+ flagNodeModules: args.has("--node-modules"),
124
+ flagPmCache: args.has("--pm-cache"),
125
+ flagLogs: args.has("--logs"),
126
+ flagEditor: args.has("--editor"),
127
+ flagTmp: args.has("--tmp"),
128
+ },
129
+ };
130
+ }
131
+
132
+ module.exports = {
133
+ allowedFlags,
134
+ parseCliArgs,
135
+ };
package/lib/cleanup.js ADDED
@@ -0,0 +1,85 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ function exists(filePath) {
5
+ try {
6
+ fs.accessSync(filePath);
7
+ return true;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ function removePath(absPath, dryRun) {
14
+ if (!exists(absPath)) return false;
15
+
16
+ if (dryRun) {
17
+ console.log(`[dry-run] would remove: ${absPath}`);
18
+ return true;
19
+ }
20
+
21
+ fs.rmSync(absPath, {
22
+ recursive: true,
23
+ force: true,
24
+ maxRetries: 2,
25
+ retryDelay: 50,
26
+ });
27
+ console.log(`removed: ${absPath}`);
28
+ return true;
29
+ }
30
+
31
+ function gatherRootLogs(cwd, logPrefixes) {
32
+ const found = [];
33
+ let entries;
34
+
35
+ try {
36
+ entries = fs.readdirSync(cwd, { withFileTypes: true });
37
+ } catch {
38
+ return found;
39
+ }
40
+
41
+ for (const entry of entries) {
42
+ if (!entry.isFile()) continue;
43
+ if (
44
+ logPrefixes.some(
45
+ (prefix) => entry.name === prefix || entry.name.startsWith(prefix),
46
+ )
47
+ ) {
48
+ found.push(entry.name);
49
+ }
50
+ }
51
+
52
+ return found;
53
+ }
54
+
55
+ function removeTargets(cwd, targets, dryRun) {
56
+ let removedCount = 0;
57
+
58
+ for (const rel of Array.from(targets).sort()) {
59
+ const absPath = path.join(cwd, rel);
60
+ if (removePath(absPath, dryRun)) {
61
+ removedCount += 1;
62
+ }
63
+ }
64
+
65
+ return removedCount;
66
+ }
67
+
68
+ function removeRootLogs(cwd, logPrefixes, dryRun) {
69
+ let removedCount = 0;
70
+ const logs = gatherRootLogs(cwd, logPrefixes);
71
+
72
+ for (const name of logs.sort()) {
73
+ const absPath = path.join(cwd, name);
74
+ if (removePath(absPath, dryRun)) {
75
+ removedCount += 1;
76
+ }
77
+ }
78
+
79
+ return removedCount;
80
+ }
81
+
82
+ module.exports = {
83
+ removeTargets,
84
+ removeRootLogs,
85
+ };
package/lib/confirm.js ADDED
@@ -0,0 +1,37 @@
1
+ const fs = require("node:fs");
2
+ const readline = require("node:readline/promises");
3
+
4
+ async function confirmNodeModulesRemoval() {
5
+ if (!process.stdin.isTTY) {
6
+ let input = "";
7
+ try {
8
+ input = fs.readFileSync(0, "utf8");
9
+ } catch {
10
+ input = "";
11
+ }
12
+ const answer = input.trim().toLowerCase();
13
+ return answer === "y" || answer === "yes";
14
+ }
15
+
16
+ const rl = readline.createInterface({
17
+ input: process.stdin,
18
+ output: process.stdout,
19
+ });
20
+
21
+ try {
22
+ const answer = (
23
+ await rl.question(
24
+ "Safety check: this will remove node_modules. Are you sure? [y/N]: ",
25
+ )
26
+ )
27
+ .trim()
28
+ .toLowerCase();
29
+ return answer === "y" || answer === "yes";
30
+ } finally {
31
+ rl.close();
32
+ }
33
+ }
34
+
35
+ module.exports = {
36
+ confirmNodeModulesRemoval,
37
+ };
package/lib/targets.js ADDED
@@ -0,0 +1,74 @@
1
+ const defaultTargets = [
2
+ "dist",
3
+ "build",
4
+ "coverage",
5
+ ".next",
6
+ ".turbo",
7
+ ".vite",
8
+ ".parcel-cache",
9
+ ".cache",
10
+ ];
11
+
12
+ const editorTargets = [".vscode", ".idea"];
13
+ const tmpTargets = ["tmp", "temp"];
14
+
15
+ const pmCacheTargets = [
16
+ ".yarn/cache",
17
+ ".yarn/unplugged",
18
+ ".yarn/install-state.gz",
19
+ ".pnp.cjs",
20
+ ".pnp.loader.mjs",
21
+ ".pnpm-store",
22
+ ];
23
+
24
+ const logPrefixes = [
25
+ "npm-debug.log",
26
+ "yarn-error.log",
27
+ "pnpm-debug.log",
28
+ "lerna-debug.log",
29
+ ];
30
+
31
+ function buildCleanupPlan(options) {
32
+ const targets = new Set(defaultTargets);
33
+
34
+ const includePmCache = options.flagAll || options.flagPmCache;
35
+ const includeLogs = options.flagAll || options.flagLogs;
36
+ const includeEditor = options.flagAll || options.flagEditor;
37
+ const includeTmp = options.flagAll || options.flagTmp;
38
+ const includeNodeModules =
39
+ (options.flagAll || options.flagNodeModules) &&
40
+ !(options.flagAll && options.keepNm);
41
+
42
+ if (includePmCache) {
43
+ for (const target of pmCacheTargets) {
44
+ targets.add(target);
45
+ }
46
+ }
47
+
48
+ if (includeEditor) {
49
+ for (const target of editorTargets) {
50
+ targets.add(target);
51
+ }
52
+ }
53
+
54
+ if (includeTmp) {
55
+ for (const target of tmpTargets) {
56
+ targets.add(target);
57
+ }
58
+ }
59
+
60
+ if (includeNodeModules) {
61
+ targets.add("node_modules");
62
+ }
63
+
64
+ return {
65
+ targets,
66
+ includeLogs,
67
+ includeNodeModules,
68
+ logPrefixes,
69
+ };
70
+ }
71
+
72
+ module.exports = {
73
+ buildCleanupPlan,
74
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repo-clean",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "Clean common build and cache folders from a repository.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -10,15 +10,26 @@
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/arbenkryemadhi/repo-clean.git"
12
12
  },
13
- "keywords": [],
13
+ "keywords": [
14
+ "cli",
15
+ "repo",
16
+ "workspace",
17
+ "cleanup",
18
+ "clean",
19
+ "cache"
20
+ ],
14
21
  "author": "Arben Kryemadhi",
15
22
  "type": "commonjs",
23
+ "scripts": {
24
+ "test": "node --test test/*.test.js"
25
+ },
16
26
  "bugs": {
17
27
  "url": "https://github.com/arbenkryemadhi/repo-clean/issues"
18
28
  },
19
29
  "homepage": "https://github.com/arbenkryemadhi/repo-clean#readme",
20
30
  "files": [
21
31
  "bin",
32
+ "lib",
22
33
  "README.md",
23
34
  "LICENSE"
24
35
  ]