disk-space-saviour 0.17.0 → 0.19.0
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/CHANGELOG.md +24 -0
- package/README.md +34 -14
- package/bin/dss.js +5 -1
- package/package.json +1 -1
- package/src/clean.js +26 -18
- package/src/cli.js +1 -1
- package/src/docker/scan.js +1 -0
- package/src/emergency.js +4 -0
- package/src/env/local.js +53 -29
- package/src/env/shell.js +0 -31
- package/src/git.js +101 -39
- package/src/index.d.ts +1 -1
- package/src/liveness.js +131 -9
- package/src/report.js +51 -0
- package/src/rules/apps.js +7 -3
- package/src/rules/downloads.js +175 -0
- package/src/rules/ecosystems.js +5 -4
- package/src/rules/other.js +7 -1
- package/src/rules/versions.js +23 -0
- package/src/scan.js +24 -5
- package/src/scanners/global.js +36 -15
- package/src/scanners/projects.js +67 -111
- package/src/scanners/versions.js +102 -3
- package/src/timings.js +54 -0
package/src/liveness.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* - a process holds a file, working directory, or executable inside the path
|
|
9
9
|
* (`/proc/<pid>/{fd,cwd,exe}` on Linux, `lsof` on macOS).
|
|
10
10
|
* - a command argument names a path inside the item (including scripts
|
|
11
|
-
* whose interpreter has already closed its input descriptor)
|
|
11
|
+
* whose interpreter has already closed its input descriptor);
|
|
12
|
+
* - a directory inside the item is on a process's `PATH`, so the process
|
|
13
|
+
* may start a program from it at any time.
|
|
12
14
|
*
|
|
13
15
|
* The cleaner refreshes the probe right before acting on each item.
|
|
14
16
|
*/
|
|
@@ -20,6 +22,44 @@ import { formatDuration } from './units.js';
|
|
|
20
22
|
|
|
21
23
|
const PROC_COMM_LIMIT = 15;
|
|
22
24
|
|
|
25
|
+
/** Bun subcommands that read or write the install cache. */
|
|
26
|
+
const BUN_PACKAGE_COMMANDS = new Set([
|
|
27
|
+
'install',
|
|
28
|
+
'i',
|
|
29
|
+
'add',
|
|
30
|
+
'a',
|
|
31
|
+
'remove',
|
|
32
|
+
'rm',
|
|
33
|
+
'update',
|
|
34
|
+
'upgrade',
|
|
35
|
+
'outdated',
|
|
36
|
+
'link',
|
|
37
|
+
'unlink',
|
|
38
|
+
'pm',
|
|
39
|
+
'x',
|
|
40
|
+
'create',
|
|
41
|
+
'c',
|
|
42
|
+
'init',
|
|
43
|
+
'patch',
|
|
44
|
+
'publish',
|
|
45
|
+
'audit',
|
|
46
|
+
'why',
|
|
47
|
+
'info',
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
function isBun(proc) {
|
|
51
|
+
return Boolean(matchingName(proc, ['bun', 'bunx']));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function bunPackageCommand(proc) {
|
|
55
|
+
const args = processArgs(proc);
|
|
56
|
+
if (path.posix.basename(args[0] ?? '') === 'bunx' || proc.name === 'bunx') {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const command = args.slice(1).find((arg) => !arg.startsWith('-'));
|
|
60
|
+
return BUN_PACKAGE_COMMANDS.has(command);
|
|
61
|
+
}
|
|
62
|
+
|
|
23
63
|
function uncertaintyRoots(proc) {
|
|
24
64
|
const roots = processArgs(proc).flatMap((arg) => {
|
|
25
65
|
const value = arg.includes('=') ? arg.slice(arg.indexOf('=') + 1) : arg;
|
|
@@ -182,6 +222,7 @@ export class LivenessProbe {
|
|
|
182
222
|
const processes = await this.env.processes().catch(() => null);
|
|
183
223
|
this.listed = processes !== null;
|
|
184
224
|
this.processes = processes ?? [];
|
|
225
|
+
this.bunCacheUsers = await this.#bunCacheUsers();
|
|
185
226
|
this.probeError =
|
|
186
227
|
(processes === null || this.openPaths === null) &&
|
|
187
228
|
(this.env.platform === 'linux' || this.env.platform === 'darwin');
|
|
@@ -191,6 +232,49 @@ export class LivenessProbe {
|
|
|
191
232
|
this.pathOwners.clear();
|
|
192
233
|
}
|
|
193
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Pids of Bun processes that may use the install cache: package commands,
|
|
237
|
+
* and anything else (scripts, tests, the REPL) without a `node_modules`
|
|
238
|
+
* directory above its working directory, because Bun then auto-installs
|
|
239
|
+
* from the cache. A long-running script of an installed project does not
|
|
240
|
+
* read the cache, so it does not keep the cache busy.
|
|
241
|
+
*/
|
|
242
|
+
async #bunCacheUsers() {
|
|
243
|
+
const users = new Set();
|
|
244
|
+
const installed = new Map();
|
|
245
|
+
const hasModules = async (dir) => {
|
|
246
|
+
if (!installed.has(dir)) {
|
|
247
|
+
installed.set(
|
|
248
|
+
dir,
|
|
249
|
+
Boolean(
|
|
250
|
+
await this.env.exists?.(this.env.path.join(dir, 'node_modules'))
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return installed.get(dir);
|
|
255
|
+
};
|
|
256
|
+
for (const proc of this.processes) {
|
|
257
|
+
if (!isBun(proc)) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (bunPackageCommand(proc) || !proc.cwd || !this.env.exists) {
|
|
261
|
+
users.add(proc.pid);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
let found = false;
|
|
265
|
+
for (const dir of [proc.cwd, ...ancestors(proc.cwd, this.env.path)]) {
|
|
266
|
+
if (await hasModules(dir)) {
|
|
267
|
+
found = true;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (!found) {
|
|
272
|
+
users.add(proc.pid);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return users;
|
|
276
|
+
}
|
|
277
|
+
|
|
194
278
|
/** Processes of the last refresh, or null when they could not be listed. */
|
|
195
279
|
processList() {
|
|
196
280
|
return this.listed ? this.processes : null;
|
|
@@ -269,6 +353,13 @@ export class LivenessProbe {
|
|
|
269
353
|
) {
|
|
270
354
|
continue;
|
|
271
355
|
}
|
|
356
|
+
if (
|
|
357
|
+
item.rule === 'bun-cache' &&
|
|
358
|
+
isBun(proc) &&
|
|
359
|
+
!this.bunCacheUsers?.has(proc.pid)
|
|
360
|
+
) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
272
363
|
const name = matchingName(proc, names);
|
|
273
364
|
if (!name) {
|
|
274
365
|
continue;
|
|
@@ -291,10 +382,7 @@ export class LivenessProbe {
|
|
|
291
382
|
return null;
|
|
292
383
|
}
|
|
293
384
|
const pathApi = this.env.path;
|
|
294
|
-
const targets =
|
|
295
|
-
const real = this.realPaths.get(target);
|
|
296
|
-
return real && real !== target ? [target, real] : [target];
|
|
297
|
-
});
|
|
385
|
+
const targets = this.withRealPaths(paths);
|
|
298
386
|
for (const open of this.openPaths) {
|
|
299
387
|
for (const target of targets) {
|
|
300
388
|
if (isWithin(open, target, pathApi)) {
|
|
@@ -340,6 +428,26 @@ export class LivenessProbe {
|
|
|
340
428
|
return null;
|
|
341
429
|
}
|
|
342
430
|
|
|
431
|
+
/** `paths` plus their resolved forms, as processes report paths. */
|
|
432
|
+
withRealPaths(paths) {
|
|
433
|
+
return paths.flatMap((target) => {
|
|
434
|
+
const real = this.realPaths.get(target);
|
|
435
|
+
return real && real !== target ? [target, real] : [target];
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
searchPathUsage(paths) {
|
|
440
|
+
const targets = this.withRealPaths(paths);
|
|
441
|
+
for (const proc of this.processes) {
|
|
442
|
+
for (const dir of proc.searchPath ?? []) {
|
|
443
|
+
if (targets.some((target) => isWithin(dir, target, this.env.path))) {
|
|
444
|
+
return `on the PATH of ${proc.name || 'a process'} (pid ${proc.pid}): ${dir}`;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
343
451
|
uncertainUsage(paths) {
|
|
344
452
|
for (const proc of this.env.unreadableProcesses ?? []) {
|
|
345
453
|
const args = processArgs(proc);
|
|
@@ -399,14 +507,28 @@ export class LivenessProbe {
|
|
|
399
507
|
const paths = checks.perPath ? [] : (item.paths ?? []);
|
|
400
508
|
return (
|
|
401
509
|
this.installationBlocker(item.paths ?? []) ??
|
|
402
|
-
|
|
403
|
-
? `cannot verify process and open-file activity${this.env.probeDiagnostic ? `: ${this.env.probeDiagnostic}` : ''}`
|
|
404
|
-
: null) ??
|
|
510
|
+
this.probeBlocker() ??
|
|
405
511
|
(checks.mtime === false ? null : this.recentWrite(item.newestMtimeMs)) ??
|
|
406
512
|
this.runningTool(checks.busy, checks.cwd, item) ??
|
|
513
|
+
this.processUsage(paths) ??
|
|
514
|
+
this.uncertainUsage(item.paths ?? [])
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
probeBlocker() {
|
|
519
|
+
if (!this.probeError) {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
const diagnostic = this.env.probeDiagnostic;
|
|
523
|
+
return `cannot verify process and open-file activity${diagnostic ? `: ${diagnostic}` : ''}`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Open files, command arguments and PATH entries inside the paths. */
|
|
527
|
+
processUsage(paths) {
|
|
528
|
+
return (
|
|
407
529
|
this.openInside(paths) ??
|
|
408
530
|
this.commandUsage(paths) ??
|
|
409
|
-
this.
|
|
531
|
+
this.searchPathUsage(paths)
|
|
410
532
|
);
|
|
411
533
|
}
|
|
412
534
|
|
package/src/report.js
CHANGED
|
@@ -305,6 +305,52 @@ function errorsSection(errors) {
|
|
|
305
305
|
];
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
const TIMING_LIMIT = 5;
|
|
309
|
+
|
|
310
|
+
function phaseList(phases) {
|
|
311
|
+
return phases
|
|
312
|
+
.map(
|
|
313
|
+
(phase) =>
|
|
314
|
+
`${phase.phase} ${(phase.ms / 1000).toFixed(1)}s${phase.count > 1 ? ` ×${phase.count}` : ''}`
|
|
315
|
+
)
|
|
316
|
+
.join(', ');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Slowest cleanup phases and items. `audit.timings` is a live PhaseTimer
|
|
321
|
+
* while cleaning and a plain array once read back from JSON.
|
|
322
|
+
*/
|
|
323
|
+
function auditTimingLines(audit, { verbose }) {
|
|
324
|
+
const phases = audit.timings?.toJSON?.() ?? audit.timings ?? [];
|
|
325
|
+
const items = audit.entries
|
|
326
|
+
.filter((entry) => entry.durationMs > 0)
|
|
327
|
+
.sort((a, b) => b.durationMs - a.durationMs);
|
|
328
|
+
const limit = verbose ? Infinity : TIMING_LIMIT;
|
|
329
|
+
const lines = [];
|
|
330
|
+
if (phases.length > 0) {
|
|
331
|
+
lines.push(` slowest phases: ${phaseList(phases.slice(0, limit))}`);
|
|
332
|
+
}
|
|
333
|
+
for (const entry of items.slice(0, limit)) {
|
|
334
|
+
lines.push(
|
|
335
|
+
` ${(entry.durationMs / 1000).toFixed(1)}s ${entry.rule} ${entry.path ?? entry.description}`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return lines.length > 0 ? ['', 'TIMINGS', ...lines] : [];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The slowest phases of each environment, so bottlenecks are visible
|
|
343
|
+
* without debug flags.
|
|
344
|
+
*/
|
|
345
|
+
function timingLines(report, { verbose }) {
|
|
346
|
+
return report.environments
|
|
347
|
+
.filter((env) => env.timings?.length > 0)
|
|
348
|
+
.map((env) => {
|
|
349
|
+
const phases = verbose ? env.timings : env.timings.slice(0, TIMING_LIMIT);
|
|
350
|
+
return ` ${env.label} slowest phases: ${phaseList(phases)}`;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
308
354
|
/**
|
|
309
355
|
* Renders a scan report.
|
|
310
356
|
* @param {object} report
|
|
@@ -352,6 +398,10 @@ export function formatReport(report, options = {}) {
|
|
|
352
398
|
lines.push(...dockerSection(report));
|
|
353
399
|
lines.push(...errorsSection(report.errors));
|
|
354
400
|
lines.push(...totalsSection(report));
|
|
401
|
+
const timings = timingLines(report, options);
|
|
402
|
+
if (timings.length > 0) {
|
|
403
|
+
lines.push('', 'TIMINGS', ...timings);
|
|
404
|
+
}
|
|
355
405
|
return lines.join('\n');
|
|
356
406
|
}
|
|
357
407
|
|
|
@@ -445,6 +495,7 @@ export function formatAudit(audit, options = {}) {
|
|
|
445
495
|
}
|
|
446
496
|
lines.push('', 'Per environment:', ...environmentLines(audit.environments));
|
|
447
497
|
lines.push(...healthLines(audit.health));
|
|
498
|
+
lines.push(...auditTimingLines(audit, options));
|
|
448
499
|
lines.push(
|
|
449
500
|
'',
|
|
450
501
|
audit.dryRun
|
package/src/rules/apps.js
CHANGED
|
@@ -167,9 +167,13 @@ export function expandChromiumCaches(rules) {
|
|
|
167
167
|
CACHE_LEAVES.map((leaf) => `${p}/${leaf}`)
|
|
168
168
|
),
|
|
169
169
|
...roots.flatMap((p) =>
|
|
170
|
-
[
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
[
|
|
171
|
+
'GrShaderCache',
|
|
172
|
+
'ShaderCache',
|
|
173
|
+
'GraphiteDawnCache',
|
|
174
|
+
'component_crx_cache',
|
|
175
|
+
'extensions_crx_cache',
|
|
176
|
+
].map((leaf) => `${p}/${leaf}`)
|
|
173
177
|
),
|
|
174
178
|
]),
|
|
175
179
|
],
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { CACHE_LEAVES } from './apps.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Files an application downloads or stages for itself and fetches again on
|
|
5
|
+
* demand: staged updates, language servers, toolchain archives, app bundle
|
|
6
|
+
* clones and caches outside the usual cache folders. Installed applications
|
|
7
|
+
* and the data they read are never matched.
|
|
8
|
+
*/
|
|
9
|
+
const openaiProfiles = [
|
|
10
|
+
'~/Library/Caches/com.openai.atlas/browser-data/host/*',
|
|
11
|
+
'~/Library/Caches/Codex/*',
|
|
12
|
+
];
|
|
13
|
+
const zedRoots = ['~/Library/Application Support/Zed', '~/.local/share/zed'];
|
|
14
|
+
|
|
15
|
+
export const DOWNLOAD_RULES = [
|
|
16
|
+
{
|
|
17
|
+
id: 'chromium-code-sign-clones',
|
|
18
|
+
ecosystem: 'browsers',
|
|
19
|
+
scope: 'tmp',
|
|
20
|
+
description:
|
|
21
|
+
'Chromium app bundle clones left by exited browsers (an update makes them full copies)',
|
|
22
|
+
// Chrome clones its bundle into the per-user `X` directory next to
|
|
23
|
+
// `$TMPDIR` at launch and removes the clone when it exits. A running
|
|
24
|
+
// browser maps the executables of its clone, so the open-file check
|
|
25
|
+
// keeps every clone in use: a browser name would block orphans forever.
|
|
26
|
+
paths: ['{TMP}/../X/*.code_sign_clone/code_sign_clone.*'],
|
|
27
|
+
directoryOnly: true,
|
|
28
|
+
minAge: 'stale',
|
|
29
|
+
busy: [],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'chrome-on-device-models',
|
|
33
|
+
ecosystem: 'browsers',
|
|
34
|
+
description: 'superseded Chrome on-device AI model versions',
|
|
35
|
+
paths: [
|
|
36
|
+
'~/Library/Application Support/Google/Chrome/OptGuideOnDeviceModel/*',
|
|
37
|
+
'~/.config/google-chrome/OptGuideOnDeviceModel/*',
|
|
38
|
+
'{LOCALAPPDATA}/Google/Chrome/User Data/OptGuideOnDeviceModel/*',
|
|
39
|
+
],
|
|
40
|
+
versionPattern: /^(?<version>\d+(?:\.\d+){1,3})$/,
|
|
41
|
+
minAge: 'stale',
|
|
42
|
+
busy: [],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'google-updater-downloads',
|
|
46
|
+
ecosystem: 'system',
|
|
47
|
+
description: 'Google Updater downloaded installers',
|
|
48
|
+
paths: [
|
|
49
|
+
'~/Library/Application Support/Google/GoogleUpdater/crx_cache',
|
|
50
|
+
'{LOCALAPPDATA}/Google/GoogleUpdater/crx_cache',
|
|
51
|
+
],
|
|
52
|
+
minAge: 'stale',
|
|
53
|
+
busy: ['GoogleUpdater'],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'openai-app-caches',
|
|
57
|
+
ecosystem: 'apps',
|
|
58
|
+
description: 'ChatGPT Atlas and Codex app browser caches',
|
|
59
|
+
paths: openaiProfiles.flatMap((profile) =>
|
|
60
|
+
CACHE_LEAVES.map((leaf) => `${profile}/${leaf}`)
|
|
61
|
+
),
|
|
62
|
+
busy: ['ChatGPT Atlas', 'Codex'],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'electron-downloads',
|
|
66
|
+
ecosystem: 'javascript',
|
|
67
|
+
description: 'Electron and electron-builder downloaded archives',
|
|
68
|
+
paths: [
|
|
69
|
+
'~/Library/Caches/electron',
|
|
70
|
+
'~/Library/Caches/electron-builder',
|
|
71
|
+
'~/.cache/electron',
|
|
72
|
+
'~/.cache/electron-builder',
|
|
73
|
+
'{LOCALAPPDATA}/electron/Cache',
|
|
74
|
+
'{LOCALAPPDATA}/electron-builder/Cache',
|
|
75
|
+
],
|
|
76
|
+
minAge: 'stale',
|
|
77
|
+
busy: ['electron-builder', 'electron-forge'],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'bun-transpiler-cache',
|
|
81
|
+
ecosystem: 'javascript',
|
|
82
|
+
description: 'Bun runtime transpiler cache',
|
|
83
|
+
// Bun validates every entry against its source and transpiles again on
|
|
84
|
+
// a miss, so only open files hold the cache back.
|
|
85
|
+
paths: ['~/Library/Caches/bun/@t@', '~/.cache/bun/@t@'],
|
|
86
|
+
busy: [],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'docker-desktop-update-downloads',
|
|
90
|
+
ecosystem: 'apps',
|
|
91
|
+
description: 'Docker Desktop updates downloaded but not installed',
|
|
92
|
+
paths: ['~/Library/Application Support/com.docker.install/in_progress'],
|
|
93
|
+
minAge: 'stale',
|
|
94
|
+
busy: ['Docker Desktop', 'com.docker.backend', 'Docker'],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'docker-desktop-update-images',
|
|
98
|
+
ecosystem: 'apps',
|
|
99
|
+
scope: 'tmp',
|
|
100
|
+
description: 'Docker Desktop update disk images',
|
|
101
|
+
paths: ['{TMP}/DockerDesktopUpdates/*'],
|
|
102
|
+
minAge: 'stale',
|
|
103
|
+
busy: ['Docker Desktop', 'com.docker.backend', 'Docker'],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'warp-staged-updates',
|
|
107
|
+
ecosystem: 'apps',
|
|
108
|
+
description: 'Warp updates staged for the next restart',
|
|
109
|
+
paths: ['~/Library/Application Support/dev.warp.Warp-*/autoupdate/*'],
|
|
110
|
+
minAge: 'stale',
|
|
111
|
+
// Warp applies the staged bundle when it quits, so it stays while Warp
|
|
112
|
+
// runs. The process names are those of the Stable and Preview builds.
|
|
113
|
+
busy: ['stable', 'preview', 'Warp'],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'zed-downloads',
|
|
117
|
+
ecosystem: 'ide',
|
|
118
|
+
description: 'Zed downloaded Node.js, language servers and Copilot',
|
|
119
|
+
paths: zedRoots.flatMap((root) =>
|
|
120
|
+
['node', 'languages', 'copilot'].map((leaf) => `${root}/${leaf}`)
|
|
121
|
+
),
|
|
122
|
+
busy: ['zed', 'Zed', 'zed-editor'],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'tree-sitter-downloads',
|
|
126
|
+
ecosystem: 'c-cpp',
|
|
127
|
+
description: 'tree-sitter CLI downloaded WASI SDK, Binaryen and parsers',
|
|
128
|
+
paths: ['~/.cache/tree-sitter', '~/Library/Caches/tree-sitter'],
|
|
129
|
+
busy: ['tree-sitter'],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'codex-plugin-staging',
|
|
133
|
+
ecosystem: 'agents',
|
|
134
|
+
description: 'abandoned Codex plugin and marketplace install staging',
|
|
135
|
+
paths: [
|
|
136
|
+
'~/.codex/plugins/..plugin-appserver.staging-*',
|
|
137
|
+
'~/.codex/plugins/.marketplace-plugin-source-staging/*',
|
|
138
|
+
'~/.codex/plugins/.remote-plugin-install-staging/*',
|
|
139
|
+
'~/.codex/.tmp/marketplaces/.staging/*',
|
|
140
|
+
],
|
|
141
|
+
// The Codex daemon runs for days; an install in progress writes to its
|
|
142
|
+
// staging directory, which the activity window and open files catch.
|
|
143
|
+
minAge: 'stale',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'clang-module-cache',
|
|
147
|
+
ecosystem: 'c-cpp',
|
|
148
|
+
description: 'Clang and Swift implicit module cache',
|
|
149
|
+
scope: 'tmp',
|
|
150
|
+
paths: [
|
|
151
|
+
'{TMP}/../C/clang/ModuleCache',
|
|
152
|
+
'{TMP}/../C/org.llvm.clang*/ModuleCache',
|
|
153
|
+
],
|
|
154
|
+
// A build in another directory still reads the cache, which the
|
|
155
|
+
// activity window and open files catch.
|
|
156
|
+
minAge: 'stale',
|
|
157
|
+
busy: ['clang', 'clang++', 'swift-frontend', 'swiftc', 'xcodebuild'],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 'crashpad-reports',
|
|
161
|
+
ecosystem: 'system',
|
|
162
|
+
description: 'Chromium and Electron app crash reports already processed',
|
|
163
|
+
tier: 'moderate',
|
|
164
|
+
aggregate: true,
|
|
165
|
+
paths: [
|
|
166
|
+
'~/Library/Application Support/*/Crashpad/completed/*',
|
|
167
|
+
...['Google', 'Yandex', 'BraveSoftware'].map(
|
|
168
|
+
(vendor) =>
|
|
169
|
+
`~/Library/Application Support/${vendor}/*/Crashpad/completed/*`
|
|
170
|
+
),
|
|
171
|
+
'~/.config/*/Crashpad/completed/*',
|
|
172
|
+
],
|
|
173
|
+
minAge: 'stale',
|
|
174
|
+
},
|
|
175
|
+
];
|
package/src/rules/ecosystems.js
CHANGED
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* `projects` are directories inside project trees, recognised by name plus a
|
|
10
10
|
* marker file next to them (`markers`), inside them (`selfMarkers`), or next
|
|
11
|
-
* to their parent (`parentMarkers`, with `parentName`).
|
|
12
|
-
* `
|
|
13
|
-
* inactive for the configured number of days and `aggressive` otherwise.
|
|
11
|
+
* to their parent (`parentMarkers`, with `parentName`). All kinds (`cache`,
|
|
12
|
+
* `build`, `dependencies`) are `safe` whenever nothing is using them.
|
|
14
13
|
*
|
|
15
14
|
* `busy` lists process names that, when running (inside the project for
|
|
16
15
|
* project directories), mean the tool may be using the directory right now.
|
|
@@ -71,7 +70,9 @@ export const ECOSYSTEMS = [
|
|
|
71
70
|
description: 'Bun install cache',
|
|
72
71
|
paths: ['~/.bun/install/cache'],
|
|
73
72
|
native: { tool: 'bun', argv: ['bun', 'pm', 'cache', 'rm'] },
|
|
74
|
-
|
|
73
|
+
// Only package commands and auto-installing scripts read the cache
|
|
74
|
+
// (see LivenessProbe.refresh).
|
|
75
|
+
busy: ['bun', 'bunx'],
|
|
75
76
|
},
|
|
76
77
|
{
|
|
77
78
|
id: 'deno-cache',
|
package/src/rules/other.js
CHANGED
|
@@ -107,6 +107,9 @@ export const OTHER_RULES = [
|
|
|
107
107
|
'~/Library/Application Support/Code/Cache',
|
|
108
108
|
'~/Library/Application Support/Code/CachedData',
|
|
109
109
|
'~/Library/Application Support/Code/CachedExtensionVSIXs',
|
|
110
|
+
'~/Library/Application Support/Code/WebStorage/*/CacheStorage',
|
|
111
|
+
'~/.config/Code/WebStorage/*/CacheStorage',
|
|
112
|
+
'{APPDATA}/Code/WebStorage/*/CacheStorage',
|
|
110
113
|
'~/.vscode-server/data/CachedExtensionVSIXs',
|
|
111
114
|
'~/.vscode-server/data/logs',
|
|
112
115
|
'~/.cursor-server/data/CachedExtensionVSIXs',
|
|
@@ -349,8 +352,11 @@ export const OTHER_RULES = [
|
|
|
349
352
|
ecosystem: 'agents',
|
|
350
353
|
description: 'abandoned Codex runtime install staging directories',
|
|
351
354
|
paths: ['~/.cache/codex-runtimes/codex-runtime-install-*'],
|
|
355
|
+
// The Codex daemon runs for days, so a running `codex` says nothing
|
|
356
|
+
// about a staging directory: an install in progress writes to it and
|
|
357
|
+
// holds its files open, which the activity window and open-file checks
|
|
358
|
+
// catch.
|
|
352
359
|
minAge: 'stale',
|
|
353
|
-
busy: ['codex'],
|
|
354
360
|
},
|
|
355
361
|
{
|
|
356
362
|
id: 'claude-cli-mcp-logs',
|
package/src/rules/versions.js
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* - the newest installed version,
|
|
7
7
|
* - in use by a running process (checked by the liveness layer).
|
|
8
8
|
*
|
|
9
|
+
* `refsPickNewest` makes a partial ref keep only the newest install it
|
|
10
|
+
* matches (nvm runs the newest 20.x for `20`); otherwise it keeps them all.
|
|
11
|
+
* `globalPackages` blocks versions holding packages the user installed
|
|
12
|
+
* globally, which reinstalling the version would not bring back.
|
|
13
|
+
*
|
|
9
14
|
* `versionOf(dirPath)` maps an install directory to the version string that
|
|
10
15
|
* defaults and pin files are compared with (see `versionMatches`).
|
|
11
16
|
*/
|
|
@@ -112,6 +117,16 @@ export const VERSION_RULES = [
|
|
|
112
117
|
busy: ['claude'],
|
|
113
118
|
recheck: true,
|
|
114
119
|
},
|
|
120
|
+
{
|
|
121
|
+
id: 'codex-daemon-releases',
|
|
122
|
+
ecosystem: 'agents',
|
|
123
|
+
manager: 'Codex',
|
|
124
|
+
description: 'Codex app-server daemon release',
|
|
125
|
+
dirs: ['~/.codex/packages/app-server-daemon/releases/*'],
|
|
126
|
+
defaultLinks: ['~/.codex/packages/app-server-daemon/current'],
|
|
127
|
+
busy: ['codex'],
|
|
128
|
+
recheck: true,
|
|
129
|
+
},
|
|
115
130
|
{
|
|
116
131
|
id: 'copilot-cli-versions',
|
|
117
132
|
ecosystem: 'agents',
|
|
@@ -133,6 +148,14 @@ export const VERSION_RULES = [
|
|
|
133
148
|
aliasDir: '~/.nvm/alias',
|
|
134
149
|
defaults: [{ path: '~/.nvm/alias/default', parse: firstLines }],
|
|
135
150
|
projectFiles: { '.nvmrc': firstLines, '.node-version': firstLines },
|
|
151
|
+
refsPickNewest: true,
|
|
152
|
+
globalPackages: {
|
|
153
|
+
dir: 'lib/node_modules',
|
|
154
|
+
// Bundled with Node.js, or a use-m cache (`<name>-v-<version>`) that
|
|
155
|
+
// use-m installs again on first use.
|
|
156
|
+
regenerable: (name) =>
|
|
157
|
+
name === 'npm' || name === 'corepack' || /^[^@][^/]*-v-./.test(name),
|
|
158
|
+
},
|
|
136
159
|
busy: ['node'],
|
|
137
160
|
},
|
|
138
161
|
{
|
package/src/scan.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
ScanIndex,
|
|
33
33
|
scopeKey,
|
|
34
34
|
} from './scan-state.js';
|
|
35
|
+
import { PhaseTimer, timed } from './timings.js';
|
|
35
36
|
|
|
36
37
|
export const SCANNERS = {
|
|
37
38
|
projects: scanProjects,
|
|
@@ -266,6 +267,12 @@ export async function scanEnvironment(env, options, extra = {}) {
|
|
|
266
267
|
}
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
/** Phase timings of an environment, started on its first scan. */
|
|
271
|
+
function environmentTimings(env) {
|
|
272
|
+
env.timings ??= new PhaseTimer();
|
|
273
|
+
return env.timings;
|
|
274
|
+
}
|
|
275
|
+
|
|
269
276
|
async function scanEnvironmentScoped(
|
|
270
277
|
env,
|
|
271
278
|
options,
|
|
@@ -275,11 +282,14 @@ async function scanEnvironmentScoped(
|
|
|
275
282
|
stage
|
|
276
283
|
) {
|
|
277
284
|
const errors = extra.errors ?? [];
|
|
278
|
-
const context = await
|
|
285
|
+
const context = await environmentTimings(env).measure('context', () =>
|
|
286
|
+
environmentContext(env, options, extra)
|
|
287
|
+
);
|
|
288
|
+
context.timings = env.timings;
|
|
279
289
|
context.emit = (item) => found.set(item.id, item);
|
|
280
290
|
let stopped = false;
|
|
281
291
|
const ready = async (items) => {
|
|
282
|
-
await attributeStorage(env, items);
|
|
292
|
+
await env.timings.measure('storage', () => attributeStorage(env, items));
|
|
283
293
|
for (const item of items) {
|
|
284
294
|
for (const target of item.paths) {
|
|
285
295
|
block(item, env.scanPolicy.removalReason(target));
|
|
@@ -324,7 +334,9 @@ async function scanEnvironmentScoped(
|
|
|
324
334
|
const started = Date.now();
|
|
325
335
|
const previous = new Set(found.keys());
|
|
326
336
|
try {
|
|
327
|
-
const entries = await scanner(
|
|
337
|
+
const entries = await env.timings.measure(`scanner:${name}`, () =>
|
|
338
|
+
scanner(context)
|
|
339
|
+
);
|
|
328
340
|
for (const item of entries) {
|
|
329
341
|
context.emit(item);
|
|
330
342
|
}
|
|
@@ -480,6 +492,7 @@ function envDescriptor(env, depth, chain) {
|
|
|
480
492
|
hint: env.probeHint ?? null,
|
|
481
493
|
scanStatus: env.scanStatus ?? null,
|
|
482
494
|
layerSource: env.writableLayer?.source ?? null,
|
|
495
|
+
timings: env.timings?.toJSON() ?? [],
|
|
483
496
|
};
|
|
484
497
|
}
|
|
485
498
|
|
|
@@ -590,6 +603,7 @@ async function scanScoped(input) {
|
|
|
590
603
|
resetWritableLayers(env.executor);
|
|
591
604
|
env.writableLayer = null;
|
|
592
605
|
env.scanPolicy = null;
|
|
606
|
+
env.timings = new PhaseTimer();
|
|
593
607
|
await configureScanPolicy(env, options);
|
|
594
608
|
await configureContainerHost(
|
|
595
609
|
env,
|
|
@@ -603,7 +617,9 @@ async function scanScoped(input) {
|
|
|
603
617
|
const docker =
|
|
604
618
|
env.scanStatus === 'goal-reached'
|
|
605
619
|
? null
|
|
606
|
-
: await
|
|
620
|
+
: await timed(env.timings, 'docker', () =>
|
|
621
|
+
dockerSection(env, options, errors)
|
|
622
|
+
);
|
|
607
623
|
blockMountedHostItems(hostItems, docker?.bindMounts ?? [], env.path);
|
|
608
624
|
const items = filterItems(
|
|
609
625
|
[...hostItems, ...(docker?.items ?? [])],
|
|
@@ -615,7 +631,10 @@ async function scanScoped(input) {
|
|
|
615
631
|
for (const inner of docker?.environments ?? []) {
|
|
616
632
|
environments.push(inner);
|
|
617
633
|
}
|
|
618
|
-
environments[0] = await
|
|
634
|
+
environments[0] = await timed(env.timings, 'disk', () =>
|
|
635
|
+
withDisk(env, environments[0])
|
|
636
|
+
);
|
|
637
|
+
environments[0].timings = env.timings.toJSON();
|
|
619
638
|
const report = buildReport({
|
|
620
639
|
options,
|
|
621
640
|
env,
|