disk-space-saviour 0.15.2 → 0.17.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 +20 -0
- package/README.md +74 -1
- package/bin/dss.js +22 -5
- package/package.json +1 -1
- package/src/audit.js +19 -2
- package/src/clean.js +153 -11
- package/src/cli.js +102 -14
- package/src/docker/cli.js +11 -7
- package/src/docker/scan.js +25 -4
- package/src/docker/upperdir.js +44 -0
- package/src/docker/writable.js +90 -13
- package/src/emergency.js +37 -6
- package/src/env/batches.js +20 -0
- package/src/env/cache-roots.js +138 -0
- package/src/env/inspection.js +100 -0
- package/src/env/local.js +314 -96
- package/src/env/parallel.js +23 -0
- package/src/env/platform-index.js +87 -0
- package/src/env/proc.js +13 -1
- package/src/env/process-scripts.js +24 -0
- package/src/env/scope.js +95 -0
- package/src/env/shell.js +146 -108
- package/src/exec.js +79 -8
- package/src/fast-clean.js +39 -0
- package/src/health.js +16 -5
- package/src/index.d.ts +52 -1
- package/src/items.js +9 -3
- package/src/liveness.js +112 -25
- package/src/options.js +9 -1
- package/src/report.js +25 -3
- package/src/rules/toolchains.js +66 -0
- package/src/scan-state.js +239 -0
- package/src/scan.js +240 -19
- package/src/scanners/agents.js +3 -2
- package/src/scanners/common.js +6 -1
- package/src/scanners/global.js +91 -22
- package/src/scanners/projects.js +114 -20
- package/src/scanners/rust.js +7 -1
- package/src/scanners/system.js +7 -6
- package/src/scanners/versions.js +25 -13
- package/src/swap.js +69 -0
package/src/scanners/global.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scanItems } from '../env/scope.js';
|
|
1
2
|
/**
|
|
2
3
|
* Global per-user caches (ecosystem download and compilation caches, IDE and
|
|
3
4
|
* browser caches), system package caches, and leftovers in temporary
|
|
@@ -9,13 +10,16 @@ import { OTHER_RULES } from '../rules/other.js';
|
|
|
9
10
|
import { APP_RULES, expandChromiumCaches } from '../rules/apps.js';
|
|
10
11
|
import { compareVersions } from '../rules/versions.js';
|
|
11
12
|
import { block, makeItem } from '../items.js';
|
|
12
|
-
import { expandGlobPaths, expandRulePath } from '../paths.js';
|
|
13
|
-
import { olderThan, scanTimeBusy } from './common.js';
|
|
13
|
+
import { expandGlobPaths, expandRulePath, isWithin } from '../paths.js';
|
|
14
|
+
import { olderThan, scanTimeBusy, resolvePaths } from './common.js';
|
|
15
|
+
import { configuredPattern } from '../env/cache-roots.js';
|
|
16
|
+
import { TOOLCHAIN_CACHE_RULES } from '../rules/toolchains.js';
|
|
14
17
|
|
|
15
18
|
export const GLOBAL_RULES = [
|
|
16
19
|
...CACHE_RULES,
|
|
17
20
|
...expandChromiumCaches(OTHER_RULES),
|
|
18
21
|
...APP_RULES,
|
|
22
|
+
...TOOLCHAIN_CACHE_RULES,
|
|
19
23
|
];
|
|
20
24
|
|
|
21
25
|
function scopeBases(rule, homes, tmpDirs) {
|
|
@@ -32,15 +36,30 @@ function scopeBases(rule, homes, tmpDirs) {
|
|
|
32
36
|
function rulePatterns(env, rule, base) {
|
|
33
37
|
const patterns = [];
|
|
34
38
|
for (const pattern of rule.paths) {
|
|
39
|
+
if (pattern.includes('{GEM_PATH}')) {
|
|
40
|
+
if (base.home === env.currentHome) {
|
|
41
|
+
for (const root of (env.vars.GEM_PATH ?? '').split(
|
|
42
|
+
env.platform === 'win32' ? ';' : ':'
|
|
43
|
+
)) {
|
|
44
|
+
if (env.path.isAbsolute(root)) {
|
|
45
|
+
patterns.push(pattern.replace('{GEM_PATH}', root));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
35
51
|
const usesVars = /\{(?!TMP\})[A-Z_]+\}/.test(pattern);
|
|
36
52
|
if (usesVars && base.home !== env.currentHome) {
|
|
37
53
|
continue;
|
|
38
54
|
}
|
|
39
|
-
const expanded = expandRulePath(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
const expanded = expandRulePath(
|
|
56
|
+
configuredPattern(env, base.home, pattern),
|
|
57
|
+
{
|
|
58
|
+
home: base.home ?? '/',
|
|
59
|
+
vars: { ...env.vars, TMP: base.tmp ?? undefined },
|
|
60
|
+
pathApi: env.path,
|
|
61
|
+
}
|
|
62
|
+
);
|
|
44
63
|
if (expanded) {
|
|
45
64
|
patterns.push(expanded);
|
|
46
65
|
}
|
|
@@ -59,15 +78,26 @@ async function nativeAction(context, rule, base, paths) {
|
|
|
59
78
|
if (!native || options.noNative) {
|
|
60
79
|
return { action: removal, blocker: null };
|
|
61
80
|
}
|
|
62
|
-
const
|
|
63
|
-
(
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
const init = native.init
|
|
82
|
+
? expandRulePath(configuredPattern(env, base.home, native.init), {
|
|
83
|
+
home: base.home,
|
|
84
|
+
pathApi: env.path,
|
|
85
|
+
})
|
|
86
|
+
: null;
|
|
87
|
+
const nativeRoot = native.cacheRoot
|
|
88
|
+
? expandRulePath(configuredPattern(env, base.home, native.cacheRoot), {
|
|
89
|
+
home: base.home,
|
|
90
|
+
pathApi: env.path,
|
|
91
|
+
})
|
|
92
|
+
: null;
|
|
93
|
+
const usable = await nativeUsable(env, rule, base, paths, nativeRoot, init);
|
|
66
94
|
if (usable) {
|
|
67
95
|
return {
|
|
68
96
|
action: {
|
|
69
97
|
type: 'command',
|
|
70
|
-
argv:
|
|
98
|
+
argv: init
|
|
99
|
+
? [native.tool, '-c', `. "$1" && ${native.command}`, 'dss', init]
|
|
100
|
+
: native.argv,
|
|
71
101
|
measure: paths,
|
|
72
102
|
},
|
|
73
103
|
blocker: null,
|
|
@@ -82,6 +112,16 @@ async function nativeAction(context, rule, base, paths) {
|
|
|
82
112
|
return { action: removal, blocker: null };
|
|
83
113
|
}
|
|
84
114
|
|
|
115
|
+
async function nativeUsable(env, rule, base, paths, root, init) {
|
|
116
|
+
return (
|
|
117
|
+
(rule.scope === 'system' || base.home === env.currentHome) &&
|
|
118
|
+
(await env.which(rule.native.tool)) &&
|
|
119
|
+
(!rule.native.root || (await env.isRoot())) &&
|
|
120
|
+
(!root || paths.every((target) => isWithin(target, root, env.path))) &&
|
|
121
|
+
(!init || (await env.exists(init)))
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
85
125
|
function ageFilter(context, rule) {
|
|
86
126
|
const { options } = context;
|
|
87
127
|
if (rule.minAge === 'stale') {
|
|
@@ -95,14 +135,18 @@ function ageFilter(context, rule) {
|
|
|
95
135
|
return () => true;
|
|
96
136
|
}
|
|
97
137
|
|
|
138
|
+
function requiresMtime(rule) {
|
|
139
|
+
return rule.mtime ?? (rule.scope === 'tmp' || Boolean(rule.minAge));
|
|
140
|
+
}
|
|
141
|
+
|
|
98
142
|
/** Keep the latest installed revision of each browser and platform family. */
|
|
99
|
-
|
|
143
|
+
function olderVersions(env, rule, paths, stats) {
|
|
100
144
|
if (!rule.versionPattern) {
|
|
101
145
|
return paths;
|
|
102
146
|
}
|
|
103
147
|
const groups = new Map();
|
|
104
148
|
for (const target of paths) {
|
|
105
|
-
if (
|
|
149
|
+
if (stats.get(target)?.type !== 'dir') {
|
|
106
150
|
continue;
|
|
107
151
|
}
|
|
108
152
|
const match = rule.versionPattern.exec(env.path.basename(target));
|
|
@@ -141,6 +185,7 @@ async function buildItem(context, rule, base, group) {
|
|
|
141
185
|
path: paths[0],
|
|
142
186
|
paths,
|
|
143
187
|
bytes: group.reduce((sum, entry) => sum + entry.usage.bytes, 0),
|
|
188
|
+
sizeEstimated: group.some((entry) => entry.usage.estimated),
|
|
144
189
|
newestMtimeMs: Math.max(...group.map((e) => e.usage.newestMtimeMs ?? 0)),
|
|
145
190
|
tier: rule.tier ?? 'safe',
|
|
146
191
|
reason: rule.minAge
|
|
@@ -201,6 +246,14 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
201
246
|
env,
|
|
202
247
|
planned.flatMap((entry) => entry.patterns)
|
|
203
248
|
);
|
|
249
|
+
const typedPaths = planned
|
|
250
|
+
.filter(
|
|
251
|
+
({ rule }) => rule.directoryOnly || rule.fileOnly || rule.versionPattern
|
|
252
|
+
)
|
|
253
|
+
.flatMap(({ patterns }) =>
|
|
254
|
+
patterns.flatMap((pattern) => matches.get(pattern) ?? [])
|
|
255
|
+
);
|
|
256
|
+
const stats = await env.statMany([...new Set(typedPaths)]);
|
|
204
257
|
const found = [];
|
|
205
258
|
const seen = new Set();
|
|
206
259
|
for (const { rule, base, patterns } of planned) {
|
|
@@ -210,20 +263,32 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
210
263
|
|
|
211
264
|
const accepted = [];
|
|
212
265
|
for (const target of candidates) {
|
|
213
|
-
if (!(await acceptedPath(env, rule, target))) {
|
|
266
|
+
if (!(await acceptedPath(env, rule, target, stats))) {
|
|
214
267
|
continue;
|
|
215
268
|
}
|
|
216
269
|
accepted.push(target);
|
|
217
270
|
seen.add(target);
|
|
218
271
|
}
|
|
219
|
-
const paths =
|
|
272
|
+
const paths = olderVersions(env, rule, accepted, stats);
|
|
220
273
|
if (paths.length > 0) {
|
|
221
274
|
found.push({ rule, base, paths });
|
|
222
275
|
}
|
|
223
276
|
}
|
|
224
|
-
|
|
225
|
-
|
|
277
|
+
await resolvePaths(
|
|
278
|
+
context,
|
|
279
|
+
found.flatMap((entry) => entry.paths)
|
|
280
|
+
);
|
|
281
|
+
const items = scanItems(context);
|
|
282
|
+
// Deliver cheap native measurements before rules needing recursive activity
|
|
283
|
+
// timestamps, so an emergency goal can avoid the expensive work entirely.
|
|
284
|
+
found.sort(
|
|
285
|
+
(a, b) => Number(requiresMtime(a.rule)) - Number(requiresMtime(b.rule))
|
|
286
|
+
);
|
|
226
287
|
for (const { rule, base, paths } of found) {
|
|
288
|
+
const usages = await env.usageMany(paths, {
|
|
289
|
+
mtime: requiresMtime(rule),
|
|
290
|
+
cache: true,
|
|
291
|
+
});
|
|
227
292
|
const accept = ageFilter(context, rule);
|
|
228
293
|
const measured = paths
|
|
229
294
|
.map((target) => ({ path: target, usage: usages.get(target) }))
|
|
@@ -232,21 +297,25 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
232
297
|
? [measured]
|
|
233
298
|
: measured.map((entry) => [entry]);
|
|
234
299
|
for (const group of groups.filter((g) => g.length > 0)) {
|
|
235
|
-
|
|
300
|
+
const item = await buildItem(context, rule, base, group);
|
|
301
|
+
items.push(item);
|
|
302
|
+
if ((await context.ready?.([item])) === false) {
|
|
303
|
+
return items;
|
|
304
|
+
}
|
|
236
305
|
}
|
|
237
306
|
}
|
|
238
307
|
return items;
|
|
239
308
|
}
|
|
240
309
|
|
|
241
|
-
async function acceptedPath(env, rule, target) {
|
|
310
|
+
async function acceptedPath(env, rule, target, stats) {
|
|
242
311
|
const name = env.path.basename(target);
|
|
243
312
|
if (rule.excludeNames?.includes(name)) {
|
|
244
313
|
return false;
|
|
245
314
|
}
|
|
246
|
-
if (rule.directoryOnly &&
|
|
315
|
+
if (rule.directoryOnly && stats.get(target)?.type !== 'dir') {
|
|
247
316
|
return false;
|
|
248
317
|
}
|
|
249
|
-
if (rule.fileOnly &&
|
|
318
|
+
if (rule.fileOnly && stats.get(target)?.type !== 'file') {
|
|
250
319
|
return false;
|
|
251
320
|
}
|
|
252
321
|
if (
|
package/src/scanners/projects.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scanItems } from '../env/scope.js';
|
|
1
2
|
/**
|
|
2
3
|
* Per-project dependency, build and cache directories (node_modules, target,
|
|
3
4
|
* .venv, ...) found by walking the scan roots.
|
|
@@ -64,6 +65,14 @@ function ruleAccepts(env, rule, candidate, selfListing, grandListing) {
|
|
|
64
65
|
* Pairs every found directory with the first rule it satisfies (or null).
|
|
65
66
|
*/
|
|
66
67
|
async function matchRules(env, found, rules) {
|
|
68
|
+
// Refresh markers and activity; the discovery index only supplies hints.
|
|
69
|
+
const parents = await listMany(env, [
|
|
70
|
+
...new Set(found.map((candidate) => candidate.parent)),
|
|
71
|
+
]);
|
|
72
|
+
found = found.map((candidate) => ({
|
|
73
|
+
...candidate,
|
|
74
|
+
siblings: parents.get(candidate.parent) ?? [],
|
|
75
|
+
}));
|
|
67
76
|
const options = found.map((candidate) => ({
|
|
68
77
|
candidate,
|
|
69
78
|
rules: rules.filter((rule) =>
|
|
@@ -115,6 +124,11 @@ async function rootCandidates(env, roots) {
|
|
|
115
124
|
return candidates;
|
|
116
125
|
}
|
|
117
126
|
|
|
127
|
+
async function indexedMatches(env, roots, globs, maxDepth, rules) {
|
|
128
|
+
const candidates = (await env.indexedDirs?.(roots, globs, maxDepth)) ?? [];
|
|
129
|
+
return matchRules(env, candidates, rules);
|
|
130
|
+
}
|
|
131
|
+
|
|
118
132
|
/**
|
|
119
133
|
* Finds every rule match below the roots.
|
|
120
134
|
* @returns {Promise<Array<{candidate: object, rule: object}>>}
|
|
@@ -124,18 +138,34 @@ export async function findProjectDirs(env, roots, options = {}) {
|
|
|
124
138
|
const maxDepth = options.maxDepth ?? 6;
|
|
125
139
|
const globs = [...new Set(rules.flatMap((rule) => rule.names))];
|
|
126
140
|
const matches = [];
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
const accept = (match) => {
|
|
143
|
+
if (!match.rule || seen.has(match.candidate.path)) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
seen.add(match.candidate.path);
|
|
147
|
+
matches.push(match);
|
|
148
|
+
return match;
|
|
149
|
+
};
|
|
150
|
+
const indexed = await indexedMatches(env, roots, globs, maxDepth, rules);
|
|
151
|
+
await options.onMatches?.(indexed.map(accept).filter(Boolean));
|
|
127
152
|
if (options.includeRoots) {
|
|
128
153
|
const self = await matchRules(env, await rootCandidates(env, roots), rules);
|
|
129
|
-
|
|
154
|
+
const accepted = self.map(accept).filter(Boolean);
|
|
155
|
+
await options.onMatches?.(accepted);
|
|
130
156
|
}
|
|
131
157
|
let frontier = new Map([[maxDepth, roots]]);
|
|
132
158
|
while (frontier.size > 0) {
|
|
133
159
|
const next = new Map();
|
|
134
160
|
for (const [depth, dirs] of frontier) {
|
|
135
161
|
const found = await env.findDirs(dirs, { globs, maxDepth: depth });
|
|
162
|
+
const accepted = [];
|
|
136
163
|
for (const match of await matchRules(env, found, rules)) {
|
|
137
164
|
if (match.rule) {
|
|
138
|
-
|
|
165
|
+
const fresh = accept(match);
|
|
166
|
+
if (fresh) {
|
|
167
|
+
accepted.push(fresh);
|
|
168
|
+
}
|
|
139
169
|
continue;
|
|
140
170
|
}
|
|
141
171
|
const remaining = depth - depthBelow(env, dirs, match.candidate.path);
|
|
@@ -146,6 +176,7 @@ export async function findProjectDirs(env, roots, options = {}) {
|
|
|
146
176
|
]);
|
|
147
177
|
}
|
|
148
178
|
}
|
|
179
|
+
await options.onMatches?.(accepted);
|
|
149
180
|
}
|
|
150
181
|
frontier = next;
|
|
151
182
|
}
|
|
@@ -179,6 +210,29 @@ function projectTier(context, rule, lastActivityMs) {
|
|
|
179
210
|
};
|
|
180
211
|
}
|
|
181
212
|
|
|
213
|
+
/** Re-evaluate activity before cleaning a reused project plan. */
|
|
214
|
+
export async function currentProjectTier(context, item) {
|
|
215
|
+
const rule = PROJECT_RULES.find((entry) => entry.id === item.rule);
|
|
216
|
+
if (!rule || rule.kind === 'cache') {
|
|
217
|
+
return item.tier;
|
|
218
|
+
}
|
|
219
|
+
const globs = [...new Set(PROJECT_RULES.flatMap((entry) => entry.names))];
|
|
220
|
+
const parent = context.env.path.dirname(item.path);
|
|
221
|
+
const candidate = {
|
|
222
|
+
name: context.env.path.basename(item.path),
|
|
223
|
+
siblings: await context.env.list(parent),
|
|
224
|
+
};
|
|
225
|
+
context.git.invalidate();
|
|
226
|
+
const activity = await projectActivity(
|
|
227
|
+
context,
|
|
228
|
+
item.project,
|
|
229
|
+
candidate,
|
|
230
|
+
{ newestMtimeMs: item.newestMtimeMs },
|
|
231
|
+
globs
|
|
232
|
+
);
|
|
233
|
+
return projectTier(context, rule, activity).tier;
|
|
234
|
+
}
|
|
235
|
+
|
|
182
236
|
async function addBlockers(context, item, requireClean) {
|
|
183
237
|
for (const reason of await context.git.pathBlockers(item.path, {
|
|
184
238
|
requireClean,
|
|
@@ -197,12 +251,21 @@ async function projectActivity(context, project, candidate, usage, globs) {
|
|
|
197
251
|
const lastCommitMs = repo ? (await git.repoState(repo)).lastCommitMs : 0;
|
|
198
252
|
context.projectUsage ??= new Map();
|
|
199
253
|
if (!context.projectUsage.has(project)) {
|
|
200
|
-
context.projectUsage.set(
|
|
254
|
+
context.projectUsage.set(
|
|
255
|
+
project,
|
|
256
|
+
env.projectActivity
|
|
257
|
+
? await env.projectActivity(
|
|
258
|
+
project,
|
|
259
|
+
globs,
|
|
260
|
+
context.now - context.options.inactiveMs
|
|
261
|
+
)
|
|
262
|
+
: (await env.usage(project))?.newestMtimeMs
|
|
263
|
+
);
|
|
201
264
|
}
|
|
202
265
|
const projectUsage = context.projectUsage.get(project);
|
|
203
266
|
return Math.max(
|
|
204
267
|
usage?.newestMtimeMs ?? 0,
|
|
205
|
-
projectUsage
|
|
268
|
+
projectUsage ?? 0,
|
|
206
269
|
siblingActivity(candidate, globs),
|
|
207
270
|
lastCommitMs
|
|
208
271
|
);
|
|
@@ -227,6 +290,7 @@ async function projectItem(context, { candidate, rule }, usage, globs) {
|
|
|
227
290
|
description: `${ECOSYSTEM_NAMES.get(rule.ecosystem)} ${rule.description}`,
|
|
228
291
|
path: candidate.path,
|
|
229
292
|
bytes: usage?.bytes ?? 0,
|
|
293
|
+
sizeEstimated: Boolean(usage?.estimated),
|
|
230
294
|
newestMtimeMs: usage?.newestMtimeMs ?? 0,
|
|
231
295
|
lastActivityMs,
|
|
232
296
|
project,
|
|
@@ -261,7 +325,32 @@ const RUST_PARTS = {
|
|
|
261
325
|
*/
|
|
262
326
|
async function rustItems(context, targetItem, rule) {
|
|
263
327
|
const { env, liveness, options, now } = context;
|
|
264
|
-
const items =
|
|
328
|
+
const items = scanItems(context);
|
|
329
|
+
const semverPath = env.path.join(targetItem.path, 'semver-checks');
|
|
330
|
+
const semverUsage = await env.usage(semverPath, { cache: true });
|
|
331
|
+
if (semverUsage) {
|
|
332
|
+
const semver = makeItem(env, {
|
|
333
|
+
rule: 'cargo-semver-checks-cache',
|
|
334
|
+
kind: 'cache',
|
|
335
|
+
ecosystem: 'rust',
|
|
336
|
+
description: 'cargo-semver-checks baseline builds',
|
|
337
|
+
path: semverPath,
|
|
338
|
+
bytes: semverUsage.bytes,
|
|
339
|
+
sizeEstimated: Boolean(semverUsage.estimated),
|
|
340
|
+
newestMtimeMs: semverUsage.newestMtimeMs,
|
|
341
|
+
project: targetItem.project,
|
|
342
|
+
parentId: targetItem.id,
|
|
343
|
+
tier: 'safe',
|
|
344
|
+
reason: 'baseline rustdoc and builds are regenerated on demand',
|
|
345
|
+
checks: {
|
|
346
|
+
busy: ['cargo-semver-checks', ...rule.busy],
|
|
347
|
+
cwd: targetItem.project,
|
|
348
|
+
mtime: true,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
await addBlockers(context, semver, false);
|
|
352
|
+
items.push(semver);
|
|
353
|
+
}
|
|
265
354
|
for (const profile of await findRustProfiles(env, targetItem.path)) {
|
|
266
355
|
const result = await analyzeRustProfile(env, profile, {
|
|
267
356
|
staleAgeMs: options.staleAgeMs,
|
|
@@ -308,24 +397,29 @@ async function rustItems(context, targetItem, rule) {
|
|
|
308
397
|
*/
|
|
309
398
|
export async function scanProjects(context, { rules = PROJECT_RULES } = {}) {
|
|
310
399
|
const { env, options } = context;
|
|
311
|
-
const
|
|
400
|
+
const globs = [...new Set(rules.flatMap((rule) => rule.names))];
|
|
401
|
+
const items = scanItems(context);
|
|
402
|
+
await findProjectDirs(env, options.roots, {
|
|
312
403
|
rules,
|
|
313
404
|
maxDepth: options.maxDepth,
|
|
314
405
|
includeRoots: options.includeRoots,
|
|
406
|
+
onMatches: async (matches) => {
|
|
407
|
+
const usages = await env.usageMany(
|
|
408
|
+
matches.map((match) => match.candidate.path),
|
|
409
|
+
{ cache: true }
|
|
410
|
+
);
|
|
411
|
+
for (const match of matches) {
|
|
412
|
+
const usage = usages.get(match.candidate.path);
|
|
413
|
+
if (!usage) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const item = await projectItem(context, match, usage, globs);
|
|
417
|
+
items.push(item);
|
|
418
|
+
if (match.rule.id === 'cargo-target') {
|
|
419
|
+
items.push(...(await rustItems(context, item, match.rule)));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
},
|
|
315
423
|
});
|
|
316
|
-
const usages = await env.usageMany(matches.map((m) => m.candidate.path));
|
|
317
|
-
const globs = [...new Set(rules.flatMap((rule) => rule.names))];
|
|
318
|
-
const items = [];
|
|
319
|
-
for (const match of matches) {
|
|
320
|
-
const usage = usages.get(match.candidate.path);
|
|
321
|
-
if (!usage) {
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
const item = await projectItem(context, match, usage, globs);
|
|
325
|
-
items.push(item);
|
|
326
|
-
if (match.rule.id === 'cargo-target') {
|
|
327
|
-
items.push(...(await rustItems(context, item, match.rule)));
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
424
|
return items;
|
|
331
425
|
}
|
package/src/scanners/rust.js
CHANGED
|
@@ -37,7 +37,13 @@ const HASHED = /^(.+)-([0-9a-f]{16})(\..*)?$/;
|
|
|
37
37
|
const INCREMENTAL = /^(.+)-([0-9a-z]{8,20})$/;
|
|
38
38
|
const LIB_PREFIXED = new Set(['.rlib', '.rmeta', '.so', '.dylib', '.a']);
|
|
39
39
|
const KIND_ORDER = ['lib', 'dylib', 'staticlib', 'bin', 'rmeta', 'other'];
|
|
40
|
-
const NON_PROFILES = new Set([
|
|
40
|
+
const NON_PROFILES = new Set([
|
|
41
|
+
'doc',
|
|
42
|
+
'package',
|
|
43
|
+
'tmp',
|
|
44
|
+
'flycheck0',
|
|
45
|
+
'semver-checks',
|
|
46
|
+
]);
|
|
41
47
|
const EXECUTABLE = new Set(['', '.exe']);
|
|
42
48
|
export const DEFAULT_GENERATION_MS = 10 * 60 * 1000;
|
|
43
49
|
export const DEFAULT_LEAF_IDLE_MS = 3 * 60 * 60 * 1000;
|
package/src/scanners/system.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scanItems } from '../env/scope.js';
|
|
1
2
|
/**
|
|
2
3
|
* System reclaimers that only the owning tool can clean correctly:
|
|
3
4
|
* journald archives (`journalctl --vacuum-size`), disabled snap revisions
|
|
@@ -102,7 +103,7 @@ async function snapItems(context, isRoot) {
|
|
|
102
103
|
return [];
|
|
103
104
|
}
|
|
104
105
|
const result = await env.run(['snap', 'list', '--all']);
|
|
105
|
-
const items =
|
|
106
|
+
const items = scanItems(context);
|
|
106
107
|
for (const { name, revision } of parseDisabledSnaps(result.stdout)) {
|
|
107
108
|
const file = `/var/lib/snapd/snaps/${name}_${revision}.snap`;
|
|
108
109
|
const stat = await env.stat(file);
|
|
@@ -163,9 +164,9 @@ async function brewItems(context, isRoot) {
|
|
|
163
164
|
*/
|
|
164
165
|
export async function scanSystem(context) {
|
|
165
166
|
const isRoot = await context.env.isRoot();
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
...(await
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
const items = scanItems(context);
|
|
168
|
+
for (const scanner of [journalItems, snapItems, brewItems]) {
|
|
169
|
+
items.push(...(await scanner(context, isRoot)));
|
|
170
|
+
}
|
|
171
|
+
return items;
|
|
171
172
|
}
|
package/src/scanners/versions.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scanItems } from '../env/scope.js';
|
|
1
2
|
/**
|
|
2
3
|
* Toolchain versions installed by version managers (nvm, pyenv, rbenv,
|
|
3
4
|
* SDKMAN!, rustup, elan, ghcup, opam, swiftly, VS Code Server).
|
|
@@ -15,21 +16,25 @@ import {
|
|
|
15
16
|
} from '../rules/versions.js';
|
|
16
17
|
import { block, makeItem } from '../items.js';
|
|
17
18
|
import { expandGlobPath, expandRulePath } from '../paths.js';
|
|
18
|
-
import { scanTimeBusy } from './common.js';
|
|
19
|
+
import { scanTimeBusy, resolvePaths } from './common.js';
|
|
20
|
+
import { configuredPattern } from '../env/cache-roots.js';
|
|
19
21
|
|
|
20
22
|
async function installDirs(env, rule, home) {
|
|
21
23
|
const dirs = [];
|
|
22
24
|
for (const pattern of rule.dirs) {
|
|
23
|
-
const expanded = expandRulePath(
|
|
24
|
-
|
|
25
|
+
const expanded = expandRulePath(configuredPattern(env, home, pattern), {
|
|
26
|
+
home,
|
|
27
|
+
pathApi: env.path,
|
|
28
|
+
});
|
|
29
|
+
const candidates = await expandGlobPath(env, expanded);
|
|
30
|
+
const stats = await env.statMany(candidates);
|
|
31
|
+
for (const candidate of candidates) {
|
|
25
32
|
const name = env.path.basename(candidate);
|
|
26
33
|
if ((rule.exclude ?? []).includes(name)) {
|
|
27
34
|
continue;
|
|
28
35
|
}
|
|
29
36
|
if (
|
|
30
|
-
!(rule.installTypes ?? ['dir']).includes(
|
|
31
|
-
(await env.stat(candidate))?.type
|
|
32
|
-
)
|
|
37
|
+
!(rule.installTypes ?? ['dir']).includes(stats.get(candidate)?.type)
|
|
33
38
|
) {
|
|
34
39
|
continue;
|
|
35
40
|
}
|
|
@@ -80,7 +85,10 @@ function linkRefs(target) {
|
|
|
80
85
|
*/
|
|
81
86
|
async function managerRefs(env, rule, home, installs) {
|
|
82
87
|
const expand = (pattern) =>
|
|
83
|
-
expandRulePath(
|
|
88
|
+
expandRulePath(configuredPattern(env, home, pattern), {
|
|
89
|
+
home,
|
|
90
|
+
pathApi: env.path,
|
|
91
|
+
});
|
|
84
92
|
const refs = [];
|
|
85
93
|
for (const source of rule.defaults ?? []) {
|
|
86
94
|
const text = await env.readText(expand(source.path), 65536);
|
|
@@ -182,24 +190,28 @@ async function ruleItems(context, rule, home, pins) {
|
|
|
182
190
|
return [];
|
|
183
191
|
}
|
|
184
192
|
const refs = [...(await managerRefs(env, rule, home, installs)), ...pins];
|
|
185
|
-
const usages = await env.usageMany(installs
|
|
193
|
+
const usages = await env.usageMany(installs, {
|
|
194
|
+
cache: !context.options.freshSizes,
|
|
195
|
+
});
|
|
196
|
+
await resolvePaths(context, installs);
|
|
186
197
|
const newest = newestInstalls(env, rule, installs, usages);
|
|
187
198
|
const versionOf = rule.versionOf ?? ((dir) => env.path.basename(dir));
|
|
188
|
-
const items =
|
|
199
|
+
const items = scanItems(context);
|
|
189
200
|
for (const dir of installs) {
|
|
190
201
|
const version = versionOf(dir);
|
|
191
202
|
if (newest.has(dir) || refs.some((ref) => versionMatches(version, ref))) {
|
|
192
203
|
continue;
|
|
193
204
|
}
|
|
194
|
-
const usage = usages.get(dir);
|
|
205
|
+
const usage = usages.get(dir) ?? {};
|
|
195
206
|
const item = makeItem(env, {
|
|
196
207
|
rule: rule.id,
|
|
197
208
|
kind: 'toolchain',
|
|
198
209
|
ecosystem: rule.ecosystem,
|
|
199
210
|
description: `${rule.description} ${version}`,
|
|
200
211
|
path: dir,
|
|
201
|
-
bytes: usage
|
|
202
|
-
|
|
212
|
+
bytes: usage.bytes ?? 0,
|
|
213
|
+
sizeEstimated: Boolean(usage.estimated),
|
|
214
|
+
newestMtimeMs: usage.newestMtimeMs ?? 0,
|
|
203
215
|
tier: 'moderate',
|
|
204
216
|
reason: `not the ${rule.manager} default, not pinned by a scanned project, not the newest`,
|
|
205
217
|
checks: { busy: rule.busy ?? [], cwd: dir, mtime: true },
|
|
@@ -232,7 +244,7 @@ export async function scanVersions(context, { rules = VERSION_RULES } = {}) {
|
|
|
232
244
|
rules,
|
|
233
245
|
options.maxDepth ?? 6
|
|
234
246
|
);
|
|
235
|
-
const items =
|
|
247
|
+
const items = scanItems(context);
|
|
236
248
|
for (const rule of rules) {
|
|
237
249
|
for (const home of homes) {
|
|
238
250
|
items.push(...(await ruleItems(context, rule, home, pins.get(rule.id))));
|
package/src/swap.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Swap is a capacity diagnostic, never a deletion candidate. */
|
|
2
|
+
import { parseSize } from './units.js';
|
|
3
|
+
|
|
4
|
+
export function parseSwapUsage(text) {
|
|
5
|
+
const match =
|
|
6
|
+
/total\s*=\s*([\d.]+[KMGT]?B?)\s+used\s*=\s*([\d.]+[KMGT]?B?)\s+free\s*=\s*([\d.]+[KMGT]?B?)/i.exec(
|
|
7
|
+
text
|
|
8
|
+
);
|
|
9
|
+
return match
|
|
10
|
+
? {
|
|
11
|
+
total: parseSize(match[1]),
|
|
12
|
+
used: parseSize(match[2]),
|
|
13
|
+
free: parseSize(match[3]),
|
|
14
|
+
}
|
|
15
|
+
: null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseMemoryHolders(text) {
|
|
19
|
+
return text
|
|
20
|
+
.split('\n')
|
|
21
|
+
.map((line) =>
|
|
22
|
+
/^\s*(\d+)\s+(.+?)\s+([\d.]+[KMGT]?B?[+-]?)\s+([\d.]+[KMGT]?B?[+-]?)\s*$/i.exec(
|
|
23
|
+
line
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.map((match) => ({
|
|
28
|
+
pid: Number(match[1]),
|
|
29
|
+
command: match[2],
|
|
30
|
+
memoryBytes: parseSize(match[3].replace(/[+-]$/, '')),
|
|
31
|
+
compressedBytes: parseSize(match[4].replace(/[+-]$/, '')),
|
|
32
|
+
}))
|
|
33
|
+
.sort((a, b) => b.memoryBytes - a.memoryBytes)
|
|
34
|
+
.slice(0, 10);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function readSwap(env) {
|
|
38
|
+
if (env.kind !== 'host' || env.platform !== 'darwin') {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const usage = await env.run(['sysctl', 'vm.swapusage']);
|
|
43
|
+
const swap = usage.code === 0 ? parseSwapUsage(usage.stdout) : null;
|
|
44
|
+
if (!swap) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const top = await env.run([
|
|
48
|
+
'top',
|
|
49
|
+
'-l',
|
|
50
|
+
'1',
|
|
51
|
+
'-o',
|
|
52
|
+
'mem',
|
|
53
|
+
'-n',
|
|
54
|
+
'10',
|
|
55
|
+
'-stats',
|
|
56
|
+
'pid,command,mem,cmprs',
|
|
57
|
+
]);
|
|
58
|
+
return {
|
|
59
|
+
...swap,
|
|
60
|
+
holders: top.code === 0 ? parseMemoryHolders(top.stdout) : [],
|
|
61
|
+
attribution:
|
|
62
|
+
'Largest resident/compressed memory consumers; macOS does not expose per-process swap ownership here.',
|
|
63
|
+
advice:
|
|
64
|
+
'If Docker Desktop is consuming memory, restart it after saving running work or lower its VM memory limit. Swap files are managed by macOS; dss never deletes them.',
|
|
65
|
+
};
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|