disk-space-saviour 0.13.0 → 0.15.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/CHANGELOG.md +34 -0
- package/README.md +88 -27
- package/bin/dss.js +12 -1
- package/package.json +5 -5
- package/src/audit.js +6 -0
- package/src/clean.js +166 -32
- package/src/cli.js +12 -2
- package/src/docker/cli.js +33 -5
- package/src/docker/containers.js +63 -7
- package/src/docker/scan.js +170 -46
- package/src/docker/snapshots.js +65 -0
- package/src/docker/writable.js +288 -0
- package/src/emergency.js +12 -3
- package/src/env/local.js +162 -78
- package/src/env/policy.js +155 -0
- package/src/env/proc.js +53 -0
- package/src/env/shell.js +190 -12
- package/src/exec.js +4 -1
- package/src/git-preserved.js +120 -0
- package/src/git.js +34 -6
- package/src/index.d.ts +14 -2
- package/src/liveness.js +160 -13
- package/src/options.js +5 -0
- package/src/paths.js +34 -2
- package/src/report.js +67 -7
- package/src/rules/apps.js +178 -0
- package/src/rules/ecosystems.js +4 -3
- package/src/rules/other.js +29 -5
- package/src/rules/versions.js +23 -0
- package/src/scan.js +86 -12
- package/src/scanners/agents.js +1 -1
- package/src/scanners/common.js +2 -1
- package/src/scanners/global.js +59 -7
- package/src/scanners/projects.js +4 -8
- package/src/scanners/system.js +4 -4
- package/src/scanners/versions.js +16 -4
package/src/rules/other.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
export const OTHER_RULES = [
|
|
19
19
|
{
|
|
20
20
|
id: 'playwright-browsers',
|
|
21
|
+
protectProfiles: true,
|
|
21
22
|
ecosystem: 'browsers',
|
|
22
23
|
description: 'old Playwright browser revisions',
|
|
23
24
|
paths: [
|
|
@@ -27,7 +28,7 @@ export const OTHER_RULES = [
|
|
|
27
28
|
],
|
|
28
29
|
versionPattern: /^(?<family>[a-z_]+)-(?<version>\d+)$/,
|
|
29
30
|
minAge: 'inactive',
|
|
30
|
-
tier: '
|
|
31
|
+
tier: 'safe',
|
|
31
32
|
busy: [
|
|
32
33
|
'playwright',
|
|
33
34
|
'playwright-mcp',
|
|
@@ -49,7 +50,7 @@ export const OTHER_RULES = [
|
|
|
49
50
|
],
|
|
50
51
|
versionPattern: /^(?<version>\d+(?:\.\d+){1,3})$/,
|
|
51
52
|
minAge: 'inactive',
|
|
52
|
-
tier: '
|
|
53
|
+
tier: 'safe',
|
|
53
54
|
busy: ['playwright'],
|
|
54
55
|
},
|
|
55
56
|
{
|
|
@@ -59,7 +60,7 @@ export const OTHER_RULES = [
|
|
|
59
60
|
paths: ['~/.cache/puppeteer/*/*'],
|
|
60
61
|
versionPattern: /^(?<family>[a-z0-9_-]+)-(?<version>\d[\d.]*)$/,
|
|
61
62
|
minAge: 'inactive',
|
|
62
|
-
tier: '
|
|
63
|
+
tier: 'safe',
|
|
63
64
|
busy: ['puppeteer', 'chrome', 'chromium', 'headless_shell'],
|
|
64
65
|
},
|
|
65
66
|
{
|
|
@@ -69,7 +70,7 @@ export const OTHER_RULES = [
|
|
|
69
70
|
paths: ['~/.cache/Cypress/*', '~/Library/Caches/Cypress/*'],
|
|
70
71
|
versionPattern: /^(?<version>\d+(?:\.\d+){1,3})$/,
|
|
71
72
|
minAge: 'inactive',
|
|
72
|
-
tier: '
|
|
73
|
+
tier: 'safe',
|
|
73
74
|
busy: ['Cypress', 'cypress'],
|
|
74
75
|
},
|
|
75
76
|
{
|
|
@@ -79,7 +80,7 @@ export const OTHER_RULES = [
|
|
|
79
80
|
paths: ['~/.cache/selenium/*/*/*'],
|
|
80
81
|
versionPattern: /^(?<version>\d+(?:\.\d+){1,3})$/,
|
|
81
82
|
minAge: 'inactive',
|
|
82
|
-
tier: '
|
|
83
|
+
tier: 'safe',
|
|
83
84
|
busy: ['chromedriver', 'geckodriver'],
|
|
84
85
|
},
|
|
85
86
|
{
|
|
@@ -464,6 +465,29 @@ export const OTHER_RULES = [
|
|
|
464
465
|
aggregate: true,
|
|
465
466
|
paths: ['/var/lib/systemd/coredump/*', '/var/crash/*', '/cores/*'],
|
|
466
467
|
},
|
|
468
|
+
{
|
|
469
|
+
id: 'start-command-isolation-logs',
|
|
470
|
+
ecosystem: 'agents',
|
|
471
|
+
scope: 'tmp',
|
|
472
|
+
description: 'old start-command Docker isolation logs',
|
|
473
|
+
tier: 'moderate',
|
|
474
|
+
paths: ['{TMP}/start-command/logs/isolation/docker/*.log'],
|
|
475
|
+
minAge: 'stale',
|
|
476
|
+
busy: [],
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
id: 'sanitized-upload-staging',
|
|
480
|
+
ecosystem: 'agents',
|
|
481
|
+
scope: 'tmp',
|
|
482
|
+
description: 'old sanitized log upload staging data',
|
|
483
|
+
tier: 'moderate',
|
|
484
|
+
paths: [
|
|
485
|
+
'{TMP}/log-tmp-hive-mind-log-upload-*-sanitized-*',
|
|
486
|
+
'{TMP}/log-home-box-hive-telegram-bot-*',
|
|
487
|
+
],
|
|
488
|
+
minAge: 'stale',
|
|
489
|
+
busy: [],
|
|
490
|
+
},
|
|
467
491
|
{
|
|
468
492
|
id: 'gh-issue-solver-workdirs',
|
|
469
493
|
ecosystem: 'tmp',
|
package/src/rules/versions.js
CHANGED
|
@@ -101,6 +101,29 @@ export function compareVersions(left, right) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export const VERSION_RULES = [
|
|
104
|
+
{
|
|
105
|
+
id: 'claude-code-versions',
|
|
106
|
+
ecosystem: 'agents',
|
|
107
|
+
manager: 'Claude Code',
|
|
108
|
+
description: 'Claude Code binary',
|
|
109
|
+
dirs: ['~/.local/share/claude/versions/*'],
|
|
110
|
+
installTypes: ['file'],
|
|
111
|
+
defaultLinks: ['~/.local/bin/claude'],
|
|
112
|
+
busy: ['claude'],
|
|
113
|
+
recheck: true,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'copilot-cli-versions',
|
|
117
|
+
ecosystem: 'agents',
|
|
118
|
+
manager: 'Copilot CLI',
|
|
119
|
+
description: 'Copilot CLI package',
|
|
120
|
+
dirs: ['~/.cache/copilot/pkg/*/*'],
|
|
121
|
+
defaultLinks: ['~/.local/bin/copilot'],
|
|
122
|
+
linkVersion: (target) => /\/pkg\/[^/]+\/([^/]+)/.exec(target ?? '')?.[1],
|
|
123
|
+
busy: ['copilot'],
|
|
124
|
+
recheck: true,
|
|
125
|
+
},
|
|
126
|
+
|
|
104
127
|
{
|
|
105
128
|
id: 'nvm-node',
|
|
106
129
|
ecosystem: 'javascript',
|
package/src/scan.js
CHANGED
|
@@ -3,14 +3,19 @@
|
|
|
3
3
|
* Docker, inside running containers and nested daemons. Never deletes.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import {
|
|
7
|
+
configureContainerHost,
|
|
8
|
+
resetWritableLayers,
|
|
9
|
+
} from './docker/writable.js';
|
|
6
10
|
import { scanDocker } from './docker/scan.js';
|
|
7
11
|
import { LocalEnv } from './env/local.js';
|
|
12
|
+
import { configureScanPolicy } from './env/policy.js';
|
|
8
13
|
import { block, tierTotals } from './items.js';
|
|
9
14
|
import { hostExecutor, trace } from './exec.js';
|
|
10
15
|
import { GitInspector } from './git.js';
|
|
11
16
|
import { LivenessProbe } from './liveness.js';
|
|
12
17
|
import { resolveOptions } from './options.js';
|
|
13
|
-
import { existingPaths, isWithin,
|
|
18
|
+
import { existingPaths, isWithin, matchesExcluded } from './paths.js';
|
|
14
19
|
import { scanAgents } from './scanners/agents.js';
|
|
15
20
|
import { scanGlobal } from './scanners/global.js';
|
|
16
21
|
import { scanProjects } from './scanners/projects.js';
|
|
@@ -27,6 +32,8 @@ export const SCANNERS = {
|
|
|
27
32
|
|
|
28
33
|
export const REPORT_SCHEMA = 1;
|
|
29
34
|
const trustedReports = new WeakMap();
|
|
35
|
+
const reportEnvironments = new WeakMap();
|
|
36
|
+
export const scannedEnvironment = (report) => reportEnvironments.get(report);
|
|
30
37
|
const RESCAN_OPTIONS = new Set([
|
|
31
38
|
'roots',
|
|
32
39
|
'scanners',
|
|
@@ -43,6 +50,7 @@ const RESCAN_OPTIONS = new Set([
|
|
|
43
50
|
'exclude',
|
|
44
51
|
'includeVolumes',
|
|
45
52
|
'removeStoppedContainers',
|
|
53
|
+
'investigationMaxAge',
|
|
46
54
|
'removeContainers',
|
|
47
55
|
'removeImages',
|
|
48
56
|
'allowDirtyRepos',
|
|
@@ -115,6 +123,10 @@ async function defaultRoots(env, homes, tmpDirs) {
|
|
|
115
123
|
* @param {{roots?: string[]|null}} [overrides]
|
|
116
124
|
*/
|
|
117
125
|
export async function environmentContext(env, options, overrides = {}) {
|
|
126
|
+
await configureScanPolicy(env, {
|
|
127
|
+
...options,
|
|
128
|
+
roots: overrides.roots ?? options.roots,
|
|
129
|
+
});
|
|
118
130
|
const liveness = new LivenessProbe(env, {
|
|
119
131
|
staleAgeMs: options.staleAgeMs,
|
|
120
132
|
now: options.now,
|
|
@@ -164,16 +176,56 @@ export async function scanEnvironment(env, options, extra = {}) {
|
|
|
164
176
|
}
|
|
165
177
|
trace('scanner', name, 'in', env.id, `${Date.now() - started}ms`);
|
|
166
178
|
}
|
|
179
|
+
await attributeStorage(env, items);
|
|
180
|
+
for (const item of items) {
|
|
181
|
+
for (const target of item.paths) {
|
|
182
|
+
block(item, env.scanPolicy.removalReason(target));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const message of env.accessErrors?.values() ?? []) {
|
|
186
|
+
errors.push({ env: env.id, scanner: 'filesystem', message });
|
|
187
|
+
}
|
|
167
188
|
return items;
|
|
168
189
|
}
|
|
169
190
|
|
|
191
|
+
async function attributeStorage(env, items) {
|
|
192
|
+
if (env.writableLayer) {
|
|
193
|
+
const usages = await env.usageMany([
|
|
194
|
+
...new Set(items.flatMap((item) => item.paths)),
|
|
195
|
+
]);
|
|
196
|
+
for (const item of items.filter((entry) => entry.paths.length > 0)) {
|
|
197
|
+
const measured = item.paths
|
|
198
|
+
.map((target) => usages.get(target))
|
|
199
|
+
.filter(Boolean);
|
|
200
|
+
item.bytes = measured.reduce((sum, usage) => sum + usage.bytes, 0);
|
|
201
|
+
item.totalBytes = measured.reduce(
|
|
202
|
+
(sum, usage) => sum + (usage.totalBytes ?? usage.bytes),
|
|
203
|
+
0
|
|
204
|
+
);
|
|
205
|
+
item.imageBytes = measured.reduce(
|
|
206
|
+
(sum, usage) => sum + (usage.imageBytes ?? 0),
|
|
207
|
+
0
|
|
208
|
+
);
|
|
209
|
+
item.imageRef =
|
|
210
|
+
measured.find((usage) => usage.imageRef)?.imageRef ?? null;
|
|
211
|
+
item.sizeUnknown = measured.some((usage) => usage.sizeUnknown);
|
|
212
|
+
if (!item.sizeUnknown && item.bytes === 0 && item.imageBytes > 0) {
|
|
213
|
+
block(
|
|
214
|
+
item,
|
|
215
|
+
'all bytes are in the image layer; remove the stopped container and its unused image after the task completes, or rebuild without caches'
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
170
222
|
/**
|
|
171
223
|
* Container ids of the container dss itself runs in (from mount sources
|
|
172
224
|
* such as `/var/lib/docker/containers/<id>/hostname`).
|
|
173
225
|
* @returns {Promise<string[]>}
|
|
174
226
|
*/
|
|
175
227
|
export async function selfContainerIds(env) {
|
|
176
|
-
if (env.platform !== 'linux') {
|
|
228
|
+
if (env.platform !== 'linux' || typeof env.readText !== 'function') {
|
|
177
229
|
return [];
|
|
178
230
|
}
|
|
179
231
|
const ids = new Set();
|
|
@@ -210,13 +262,13 @@ function matchesOnly(item, only) {
|
|
|
210
262
|
}
|
|
211
263
|
|
|
212
264
|
function excluded(item, exclude, pathApi) {
|
|
213
|
-
return item.paths.some(
|
|
214
|
-
|
|
215
|
-
(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
265
|
+
return item.paths.some(
|
|
266
|
+
(target) =>
|
|
267
|
+
matchesExcluded(target, exclude, pathApi) ||
|
|
268
|
+
exclude.some(
|
|
269
|
+
(pattern) =>
|
|
270
|
+
!/[*?[]/.test(pattern) && isWithin(pattern, target, pathApi)
|
|
271
|
+
)
|
|
220
272
|
);
|
|
221
273
|
}
|
|
222
274
|
|
|
@@ -237,7 +289,10 @@ function underDaemonRoot(item, daemons, pathApi) {
|
|
|
237
289
|
export function filterItems(items, options, daemons = [], pathApi) {
|
|
238
290
|
return items.filter(
|
|
239
291
|
(item) =>
|
|
240
|
-
item.bytes >= options.minSizeBytes
|
|
292
|
+
(item.bytes >= options.minSizeBytes ||
|
|
293
|
+
item.sizeUnknown ||
|
|
294
|
+
item.imageBytes >= options.minSizeBytes ||
|
|
295
|
+
item.container?.pinnedImage?.bytes >= options.minSizeBytes) &&
|
|
241
296
|
matchesOnly(item, options.only) &&
|
|
242
297
|
!excluded(item, options.exclude, pathApi) &&
|
|
243
298
|
!underDaemonRoot(item, daemons, pathApi)
|
|
@@ -245,7 +300,14 @@ export function filterItems(items, options, daemons = [], pathApi) {
|
|
|
245
300
|
}
|
|
246
301
|
|
|
247
302
|
function envDescriptor(env, depth, chain) {
|
|
248
|
-
return {
|
|
303
|
+
return {
|
|
304
|
+
id: env.id,
|
|
305
|
+
label: env.label,
|
|
306
|
+
kind: env.kind,
|
|
307
|
+
depth,
|
|
308
|
+
chain,
|
|
309
|
+
hint: env.probeHint ?? null,
|
|
310
|
+
};
|
|
249
311
|
}
|
|
250
312
|
|
|
251
313
|
async function withDisk(env, descriptor) {
|
|
@@ -269,6 +331,7 @@ async function dockerSection(env, options, errors) {
|
|
|
269
331
|
{ ...options, selfContainerIds: await selfContainerIds(env) },
|
|
270
332
|
(inner) => scanEnvironment(inner, options, { errors })
|
|
271
333
|
);
|
|
334
|
+
errors.push(...result.errors);
|
|
272
335
|
if (options.docker === true && result.daemons.length === 0) {
|
|
273
336
|
errors.push({
|
|
274
337
|
env: env.id,
|
|
@@ -314,6 +377,15 @@ export async function scan(input = {}) {
|
|
|
314
377
|
const env = input.env ?? new LocalEnv();
|
|
315
378
|
const startedAt = options.now();
|
|
316
379
|
const errors = [];
|
|
380
|
+
resetWritableLayers(env.executor);
|
|
381
|
+
env.writableLayer = null;
|
|
382
|
+
env.scanPolicy = null;
|
|
383
|
+
await configureScanPolicy(env, options);
|
|
384
|
+
await configureContainerHost(
|
|
385
|
+
env,
|
|
386
|
+
options.docker === false ? [] : await selfContainerIds(env),
|
|
387
|
+
options
|
|
388
|
+
);
|
|
317
389
|
const hostItems =
|
|
318
390
|
options.host === false
|
|
319
391
|
? []
|
|
@@ -331,7 +403,7 @@ export async function scan(input = {}) {
|
|
|
331
403
|
environments.push(inner);
|
|
332
404
|
}
|
|
333
405
|
environments[0] = await withDisk(env, environments[0]);
|
|
334
|
-
|
|
406
|
+
const report = buildReport({
|
|
335
407
|
options,
|
|
336
408
|
env,
|
|
337
409
|
items,
|
|
@@ -340,6 +412,8 @@ export async function scan(input = {}) {
|
|
|
340
412
|
errors,
|
|
341
413
|
startedAt,
|
|
342
414
|
});
|
|
415
|
+
reportEnvironments.set(report, env);
|
|
416
|
+
return report;
|
|
343
417
|
}
|
|
344
418
|
|
|
345
419
|
function reportOptions(options) {
|
package/src/scanners/agents.js
CHANGED
|
@@ -132,7 +132,7 @@ async function appItems(context, app, root) {
|
|
|
132
132
|
...verdict,
|
|
133
133
|
checks: { busy: [], cwd: null, mtime: true },
|
|
134
134
|
});
|
|
135
|
-
const busy = scanTimeBusy(context, item);
|
|
135
|
+
const busy = await scanTimeBusy(context, item);
|
|
136
136
|
if (busy) {
|
|
137
137
|
block(item, `busy: ${busy}`);
|
|
138
138
|
}
|
package/src/scanners/common.js
CHANGED
|
@@ -25,7 +25,8 @@ export async function listMany(env, dirs) {
|
|
|
25
25
|
* Busy reason at scan time (recent writes, running tool, open files).
|
|
26
26
|
* @returns {string|null}
|
|
27
27
|
*/
|
|
28
|
-
export function scanTimeBusy(context, item) {
|
|
28
|
+
export async function scanTimeBusy(context, item) {
|
|
29
|
+
await context.liveness?.resolve(item.paths);
|
|
29
30
|
return context.liveness ? context.liveness.busyReason(item) : null;
|
|
30
31
|
}
|
|
31
32
|
|
package/src/scanners/global.js
CHANGED
|
@@ -6,12 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
import { CACHE_RULES } from '../rules/ecosystems.js';
|
|
8
8
|
import { OTHER_RULES } from '../rules/other.js';
|
|
9
|
+
import { APP_RULES, expandChromiumCaches } from '../rules/apps.js';
|
|
9
10
|
import { compareVersions } from '../rules/versions.js';
|
|
10
11
|
import { block, makeItem } from '../items.js';
|
|
11
12
|
import { expandGlobPaths, expandRulePath } from '../paths.js';
|
|
12
13
|
import { olderThan, scanTimeBusy } from './common.js';
|
|
13
14
|
|
|
14
|
-
export const GLOBAL_RULES = [
|
|
15
|
+
export const GLOBAL_RULES = [
|
|
16
|
+
...CACHE_RULES,
|
|
17
|
+
...expandChromiumCaches(OTHER_RULES),
|
|
18
|
+
...APP_RULES,
|
|
19
|
+
];
|
|
15
20
|
|
|
16
21
|
function scopeBases(rule, homes, tmpDirs) {
|
|
17
22
|
const scope = rule.scope ?? 'home';
|
|
@@ -123,6 +128,9 @@ async function olderVersions(env, rule, paths) {
|
|
|
123
128
|
async function buildItem(context, rule, base, group) {
|
|
124
129
|
const { env } = context;
|
|
125
130
|
const paths = group.map((entry) => entry.path);
|
|
131
|
+
const appRoot = rule.markers
|
|
132
|
+
? env.path.dirname(paths[0].replace(/[/\\]Service Worker[/\\].*$/, ''))
|
|
133
|
+
: null;
|
|
126
134
|
const { action, blocker } = await nativeAction(context, rule, base, paths);
|
|
127
135
|
const scope = rule.scope ?? 'home';
|
|
128
136
|
const item = makeItem(env, {
|
|
@@ -138,11 +146,15 @@ async function buildItem(context, rule, base, group) {
|
|
|
138
146
|
reason: rule.minAge
|
|
139
147
|
? `untouched for longer than the ${rule.minAge === 'stale' ? 'activity' : 'inactivity'} window`
|
|
140
148
|
: 're-downloaded or re-created on demand',
|
|
149
|
+
recheck: rule.git === 'ifPresent' ? { type: 'scratch' } : undefined,
|
|
141
150
|
action: action ?? { type: 'none' },
|
|
142
151
|
checks: {
|
|
143
|
-
busy:
|
|
152
|
+
busy: [
|
|
153
|
+
...(rule.busy ?? []),
|
|
154
|
+
...(appRoot ? [env.path.basename(appRoot)] : []),
|
|
155
|
+
],
|
|
144
156
|
cwd: scope === 'tmp' ? paths[0] : null,
|
|
145
|
-
mtime: scope === 'tmp' || Boolean(rule.minAge),
|
|
157
|
+
mtime: rule.mtime ?? (scope === 'tmp' || Boolean(rule.minAge)),
|
|
146
158
|
},
|
|
147
159
|
});
|
|
148
160
|
block(item, blocker);
|
|
@@ -157,12 +169,14 @@ async function addBlockers(context, rule, item) {
|
|
|
157
169
|
}
|
|
158
170
|
if (rule.git) {
|
|
159
171
|
for (const target of item.paths) {
|
|
160
|
-
for (const reason of await git.treeBlockers(target
|
|
172
|
+
for (const reason of await git.treeBlockers(target, {
|
|
173
|
+
allowNoRepo: rule.git === 'ifPresent',
|
|
174
|
+
})) {
|
|
161
175
|
block(item, reason);
|
|
162
176
|
}
|
|
163
177
|
}
|
|
164
178
|
}
|
|
165
|
-
const busy = scanTimeBusy(context, item);
|
|
179
|
+
const busy = await scanTimeBusy(context, item);
|
|
166
180
|
if (busy) {
|
|
167
181
|
block(item, `busy: ${busy}`);
|
|
168
182
|
}
|
|
@@ -193,8 +207,16 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
193
207
|
const candidates = [
|
|
194
208
|
...new Set(patterns.flatMap((pattern) => matches.get(pattern) ?? [])),
|
|
195
209
|
].filter((target) => !seen.has(target));
|
|
196
|
-
|
|
197
|
-
const
|
|
210
|
+
|
|
211
|
+
const accepted = [];
|
|
212
|
+
for (const target of candidates) {
|
|
213
|
+
if (!(await acceptedPath(env, rule, target))) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
accepted.push(target);
|
|
217
|
+
seen.add(target);
|
|
218
|
+
}
|
|
219
|
+
const paths = await olderVersions(env, rule, accepted);
|
|
198
220
|
if (paths.length > 0) {
|
|
199
221
|
found.push({ rule, base, paths });
|
|
200
222
|
}
|
|
@@ -215,3 +237,33 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
215
237
|
}
|
|
216
238
|
return items;
|
|
217
239
|
}
|
|
240
|
+
|
|
241
|
+
async function acceptedPath(env, rule, target) {
|
|
242
|
+
const name = env.path.basename(target);
|
|
243
|
+
if (rule.excludeNames?.includes(name)) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
if (rule.directoryOnly && (await env.stat(target))?.type !== 'dir') {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
if (rule.fileOnly && (await env.stat(target))?.type !== 'file') {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
if (
|
|
253
|
+
rule.protectProfiles &&
|
|
254
|
+
/(?:mcp|profile|userdata|user-data)/i.test(name)
|
|
255
|
+
) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
if (!rule.markers) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
const parent = env.path.dirname(
|
|
262
|
+
target.replace(/[/\\]Service Worker[/\\].*$/, '')
|
|
263
|
+
);
|
|
264
|
+
return (
|
|
265
|
+
await Promise.all(
|
|
266
|
+
rule.markers.map((marker) => env.exists(env.path.join(parent, marker)))
|
|
267
|
+
)
|
|
268
|
+
).some(Boolean);
|
|
269
|
+
}
|
package/src/scanners/projects.js
CHANGED
|
@@ -169,7 +169,7 @@ function projectTier(context, rule, lastActivityMs) {
|
|
|
169
169
|
const { inactiveMs } = context.options;
|
|
170
170
|
if (olderThan(context, lastActivityMs, inactiveMs)) {
|
|
171
171
|
return {
|
|
172
|
-
tier: 'moderate',
|
|
172
|
+
tier: rule.id === 'node-modules' ? 'safe' : 'moderate',
|
|
173
173
|
reason: 'project inactive, re-created by the build or install step',
|
|
174
174
|
};
|
|
175
175
|
}
|
|
@@ -185,7 +185,7 @@ async function addBlockers(context, item, requireClean) {
|
|
|
185
185
|
})) {
|
|
186
186
|
block(item, reason);
|
|
187
187
|
}
|
|
188
|
-
const busy = scanTimeBusy(context, item);
|
|
188
|
+
const busy = await scanTimeBusy(context, item);
|
|
189
189
|
if (busy) {
|
|
190
190
|
block(item, `busy: ${busy}`);
|
|
191
191
|
}
|
|
@@ -209,7 +209,7 @@ async function projectActivity(context, project, candidate, usage, globs) {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
async function projectItem(context, { candidate, rule }, usage, globs) {
|
|
212
|
-
const { env
|
|
212
|
+
const { env } = context;
|
|
213
213
|
const project = rule.parentName
|
|
214
214
|
? env.path.dirname(candidate.parent)
|
|
215
215
|
: candidate.parent;
|
|
@@ -234,11 +234,7 @@ async function projectItem(context, { candidate, rule }, usage, globs) {
|
|
|
234
234
|
...projectTier(context, rule, lastActivityMs),
|
|
235
235
|
checks: { busy: rule.busy ?? [], cwd: project, mtime: true },
|
|
236
236
|
});
|
|
237
|
-
await addBlockers(
|
|
238
|
-
context,
|
|
239
|
-
item,
|
|
240
|
-
item.tier !== 'safe' && !options.allowDirtyRepos
|
|
241
|
-
);
|
|
237
|
+
await addBlockers(context, item, false);
|
|
242
238
|
return item;
|
|
243
239
|
}
|
|
244
240
|
|
package/src/scanners/system.js
CHANGED
|
@@ -55,8 +55,8 @@ export function parseBrewCleanup(output) {
|
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function withBusy(context, item) {
|
|
59
|
-
const busy = scanTimeBusy(context, item);
|
|
58
|
+
async function withBusy(context, item) {
|
|
59
|
+
const busy = await scanTimeBusy(context, item);
|
|
60
60
|
if (busy) {
|
|
61
61
|
block(item, `busy: ${busy}`);
|
|
62
62
|
}
|
|
@@ -126,7 +126,7 @@ async function snapItems(context, isRoot) {
|
|
|
126
126
|
if (!isRoot) {
|
|
127
127
|
block(item, 'system location, run as root to clean it');
|
|
128
128
|
}
|
|
129
|
-
items.push(withBusy(context, item));
|
|
129
|
+
items.push(await withBusy(context, item));
|
|
130
130
|
}
|
|
131
131
|
return items;
|
|
132
132
|
}
|
|
@@ -154,7 +154,7 @@ async function brewItems(context, isRoot) {
|
|
|
154
154
|
action: { type: 'command', argv: ['brew', 'cleanup'], measure: [] },
|
|
155
155
|
checks: { busy: ['brew'], cwd: null, mtime: false },
|
|
156
156
|
});
|
|
157
|
-
return [withBusy(context, item)];
|
|
157
|
+
return [await withBusy(context, item)];
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/**
|
package/src/scanners/versions.js
CHANGED
|
@@ -26,7 +26,11 @@ async function installDirs(env, rule, home) {
|
|
|
26
26
|
if ((rule.exclude ?? []).includes(name)) {
|
|
27
27
|
continue;
|
|
28
28
|
}
|
|
29
|
-
if (
|
|
29
|
+
if (
|
|
30
|
+
!(rule.installTypes ?? ['dir']).includes(
|
|
31
|
+
(await env.stat(candidate))?.type
|
|
32
|
+
)
|
|
33
|
+
) {
|
|
30
34
|
continue;
|
|
31
35
|
}
|
|
32
36
|
if (
|
|
@@ -86,7 +90,11 @@ async function managerRefs(env, rule, home, installs) {
|
|
|
86
90
|
refs.push(...(await aliasRefs(env, expand(rule.aliasDir))));
|
|
87
91
|
}
|
|
88
92
|
for (const link of rule.defaultLinks ?? []) {
|
|
89
|
-
|
|
93
|
+
const target = await env.readLink(expand(link));
|
|
94
|
+
refs.push(...linkRefs(target));
|
|
95
|
+
if (rule.linkVersion) {
|
|
96
|
+
refs.push(rule.linkVersion(target));
|
|
97
|
+
}
|
|
90
98
|
}
|
|
91
99
|
if (rule.currentLink) {
|
|
92
100
|
const parents = new Set(installs.map((dir) => env.path.dirname(dir)));
|
|
@@ -195,10 +203,14 @@ async function ruleItems(context, rule, home, pins) {
|
|
|
195
203
|
tier: 'moderate',
|
|
196
204
|
reason: `not the ${rule.manager} default, not pinned by a scanned project, not the newest`,
|
|
197
205
|
checks: { busy: rule.busy ?? [], cwd: dir, mtime: true },
|
|
198
|
-
recheck: rule.
|
|
206
|
+
recheck: rule.recheck
|
|
207
|
+
? { type: 'version', home }
|
|
208
|
+
: rule.id === 'pyenv-python'
|
|
209
|
+
? { type: 'pyenv-envs' }
|
|
210
|
+
: null,
|
|
199
211
|
});
|
|
200
212
|
await blockPyenvEnvironments(env, rule, dir, item);
|
|
201
|
-
const busy = scanTimeBusy(context, item);
|
|
213
|
+
const busy = await scanTimeBusy(context, item);
|
|
202
214
|
if (busy) {
|
|
203
215
|
block(item, `busy: ${busy}`);
|
|
204
216
|
}
|