notoken-core 1.5.1 → 2.0.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/config/chat-responses.json +767 -0
- package/config/concept-clusters.json +31 -0
- package/config/entities.json +93 -0
- package/config/image-prompts.json +20 -0
- package/config/intent-vectors.json +1 -0
- package/config/intents.json +5023 -65
- package/config/ollama-models.json +193 -0
- package/config/rules.json +32 -1
- package/dist/automation/discordPatchright.d.ts +35 -0
- package/dist/automation/discordPatchright.js +424 -0
- package/dist/automation/discordSetup.d.ts +31 -0
- package/dist/automation/discordSetup.js +338 -0
- package/dist/conversation/coreference.js +44 -4
- package/dist/conversation/pendingActions.d.ts +55 -0
- package/dist/conversation/pendingActions.js +127 -0
- package/dist/conversation/store.d.ts +72 -0
- package/dist/conversation/store.js +140 -1
- package/dist/conversation/topicTracker.d.ts +36 -0
- package/dist/conversation/topicTracker.js +141 -0
- package/dist/execution/ssh.d.ts +42 -1
- package/dist/execution/ssh.js +532 -3
- package/dist/handlers/executor.js +3981 -16
- package/dist/index.d.ts +25 -3
- package/dist/index.js +36 -2
- package/dist/nlp/batchParser.d.ts +30 -0
- package/dist/nlp/batchParser.js +77 -0
- package/dist/nlp/conceptExpansion.d.ts +54 -0
- package/dist/nlp/conceptExpansion.js +136 -0
- package/dist/nlp/conceptRouter.d.ts +49 -0
- package/dist/nlp/conceptRouter.js +302 -0
- package/dist/nlp/confidenceCalibrator.d.ts +62 -0
- package/dist/nlp/confidenceCalibrator.js +116 -0
- package/dist/nlp/correctionLearner.d.ts +45 -0
- package/dist/nlp/correctionLearner.js +207 -0
- package/dist/nlp/entitySpellCorrect.d.ts +35 -0
- package/dist/nlp/entitySpellCorrect.js +141 -0
- package/dist/nlp/knowledgeGraph.d.ts +70 -0
- package/dist/nlp/knowledgeGraph.js +380 -0
- package/dist/nlp/llmFallback.js +28 -1
- package/dist/nlp/multiClassifier.js +91 -6
- package/dist/nlp/multiIntent.d.ts +43 -0
- package/dist/nlp/multiIntent.js +154 -0
- package/dist/nlp/parseIntent.d.ts +6 -1
- package/dist/nlp/parseIntent.js +180 -5
- package/dist/nlp/ruleParser.js +315 -0
- package/dist/nlp/semanticSimilarity.d.ts +30 -0
- package/dist/nlp/semanticSimilarity.js +174 -0
- package/dist/nlp/vocabularyBuilder.d.ts +43 -0
- package/dist/nlp/vocabularyBuilder.js +224 -0
- package/dist/nlp/wikidata.d.ts +49 -0
- package/dist/nlp/wikidata.js +228 -0
- package/dist/policy/confirm.d.ts +10 -0
- package/dist/policy/confirm.js +39 -0
- package/dist/policy/safety.js +6 -4
- package/dist/utils/aliases.d.ts +5 -0
- package/dist/utils/aliases.js +39 -0
- package/dist/utils/analysis.js +71 -15
- package/dist/utils/browser.d.ts +64 -0
- package/dist/utils/browser.js +364 -0
- package/dist/utils/commandHistory.d.ts +20 -0
- package/dist/utils/commandHistory.js +108 -0
- package/dist/utils/completer.d.ts +17 -0
- package/dist/utils/completer.js +79 -0
- package/dist/utils/config.js +32 -2
- package/dist/utils/dbQuery.d.ts +25 -0
- package/dist/utils/dbQuery.js +248 -0
- package/dist/utils/discordDiag.d.ts +35 -0
- package/dist/utils/discordDiag.js +826 -0
- package/dist/utils/diskCleanup.d.ts +36 -0
- package/dist/utils/diskCleanup.js +775 -0
- package/dist/utils/entityResolver.d.ts +107 -0
- package/dist/utils/entityResolver.js +468 -0
- package/dist/utils/imageGen.d.ts +92 -0
- package/dist/utils/imageGen.js +2031 -0
- package/dist/utils/installTracker.d.ts +57 -0
- package/dist/utils/installTracker.js +160 -0
- package/dist/utils/multiExec.d.ts +21 -0
- package/dist/utils/multiExec.js +141 -0
- package/dist/utils/openclawDiag.d.ts +29 -0
- package/dist/utils/openclawDiag.js +1035 -0
- package/dist/utils/output.js +4 -0
- package/dist/utils/platform.js +2 -1
- package/dist/utils/progressReporter.d.ts +50 -0
- package/dist/utils/progressReporter.js +58 -0
- package/dist/utils/projectDetect.d.ts +44 -0
- package/dist/utils/projectDetect.js +319 -0
- package/dist/utils/projectScanner.d.ts +44 -0
- package/dist/utils/projectScanner.js +312 -0
- package/dist/utils/shellCompat.d.ts +78 -0
- package/dist/utils/shellCompat.js +186 -0
- package/dist/utils/smartArchive.d.ts +16 -0
- package/dist/utils/smartArchive.js +172 -0
- package/dist/utils/smartRetry.d.ts +26 -0
- package/dist/utils/smartRetry.js +114 -0
- package/dist/utils/updater.d.ts +1 -0
- package/dist/utils/updater.js +1 -1
- package/dist/utils/version.d.ts +20 -0
- package/dist/utils/version.js +212 -0
- package/package.json +6 -3
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disk cleanup scanner and executor.
|
|
3
|
+
*
|
|
4
|
+
* Scans known safe-to-clean locations, presents findings, and asks
|
|
5
|
+
* before deleting anything. Platform-aware: Windows (PowerShell) and Linux.
|
|
6
|
+
*/
|
|
7
|
+
import { exec, execSync } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import { detectLocalPlatform } from "./platform.js";
|
|
10
|
+
import { askForStrictConfirmation, askWithControl } from "../policy/confirm.js";
|
|
11
|
+
import { taskRunner } from "../agents/taskRunner.js";
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
const c = {
|
|
14
|
+
reset: "\x1b[0m",
|
|
15
|
+
bold: "\x1b[1m",
|
|
16
|
+
dim: "\x1b[2m",
|
|
17
|
+
green: "\x1b[32m",
|
|
18
|
+
yellow: "\x1b[33m",
|
|
19
|
+
red: "\x1b[31m",
|
|
20
|
+
cyan: "\x1b[36m",
|
|
21
|
+
};
|
|
22
|
+
// ─── Windows scan targets ────────────────────────────────────────────────────
|
|
23
|
+
function getWindowsScanTargets(userHome) {
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
name: "npm cache",
|
|
27
|
+
path: `${userHome}\\AppData\\Local\\npm-cache`,
|
|
28
|
+
description: "Cached package downloads — re-downloaded as needed",
|
|
29
|
+
cleanCommand: "npm cache clean --force",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "Temp files",
|
|
33
|
+
path: `${userHome}\\AppData\\Local\\Temp`,
|
|
34
|
+
description: "Temporary files from apps and system",
|
|
35
|
+
cleanCommand: `powershell -Command "Remove-Item '${userHome}\\AppData\\Local\\Temp\\*' -Recurse -Force -ErrorAction SilentlyContinue"`,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "Windows Temp",
|
|
39
|
+
path: "C:\\Windows\\Temp",
|
|
40
|
+
description: "System temporary files",
|
|
41
|
+
cleanCommand: `powershell -Command "Remove-Item 'C:\\Windows\\Temp\\*' -Recurse -Force -ErrorAction SilentlyContinue"`,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "pnpm store",
|
|
45
|
+
path: `${userHome}\\AppData\\Local\\pnpm`,
|
|
46
|
+
description: "Cached pnpm packages",
|
|
47
|
+
cleanCommand: "pnpm store prune",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "yarn cache",
|
|
51
|
+
path: `${userHome}\\AppData\\Local\\Yarn\\Cache`,
|
|
52
|
+
description: "Cached yarn packages",
|
|
53
|
+
cleanCommand: "yarn cache clean",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "npm global",
|
|
57
|
+
path: `${userHome}\\AppData\\Roaming\\npm`,
|
|
58
|
+
description: "NOT auto-deleted — shows installed global packages for manual review",
|
|
59
|
+
cleanCommand: `npm ls -g --depth=0`,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "NuGet cache",
|
|
63
|
+
path: `${userHome}\\.nuget\\packages`,
|
|
64
|
+
description: "Cached .NET packages",
|
|
65
|
+
cleanCommand: "dotnet nuget locals all --clear",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "pip cache",
|
|
69
|
+
path: `${userHome}\\AppData\\Local\\pip\\cache`,
|
|
70
|
+
description: "Cached Python packages",
|
|
71
|
+
cleanCommand: "pip cache purge",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "Windows Update",
|
|
75
|
+
path: "C:\\Windows\\SoftwareDistribution\\Download",
|
|
76
|
+
description: "Old Windows Update files",
|
|
77
|
+
cleanCommand: `powershell -Command "Remove-Item 'C:\\Windows\\SoftwareDistribution\\Download\\*' -Recurse -Force -ErrorAction SilentlyContinue"`,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
// ─── Linux scan targets ──────────────────────────────────────────────────────
|
|
82
|
+
function getLinuxScanTargets(userHome) {
|
|
83
|
+
return [
|
|
84
|
+
{
|
|
85
|
+
name: "npm cache",
|
|
86
|
+
path: `${userHome}/.npm`,
|
|
87
|
+
description: "Cached package downloads — re-downloaded as needed",
|
|
88
|
+
cleanCommand: "npm cache clean --force",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "Temp files",
|
|
92
|
+
path: "/tmp",
|
|
93
|
+
description: "Temporary files",
|
|
94
|
+
cleanCommand: "sudo rm -rf /tmp/* 2>/dev/null",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "apt cache",
|
|
98
|
+
path: "/var/cache/apt/archives",
|
|
99
|
+
description: "Downloaded .deb packages",
|
|
100
|
+
cleanCommand: "sudo apt-get clean",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "Journal logs",
|
|
104
|
+
path: "/var/log/journal",
|
|
105
|
+
description: "Systemd journal logs (keeps last 3 days)",
|
|
106
|
+
cleanCommand: "sudo journalctl --vacuum-time=3d",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "pnpm store",
|
|
110
|
+
path: `${userHome}/.local/share/pnpm`,
|
|
111
|
+
description: "Cached pnpm packages",
|
|
112
|
+
cleanCommand: "pnpm store prune",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "yarn cache",
|
|
116
|
+
path: `${userHome}/.cache/yarn`,
|
|
117
|
+
description: "Cached yarn packages",
|
|
118
|
+
cleanCommand: "yarn cache clean",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "pip cache",
|
|
122
|
+
path: `${userHome}/.cache/pip`,
|
|
123
|
+
description: "Cached Python packages",
|
|
124
|
+
cleanCommand: "pip cache purge",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "Trash",
|
|
128
|
+
path: `${userHome}/.local/share/Trash`,
|
|
129
|
+
description: "Deleted files in trash",
|
|
130
|
+
cleanCommand: `rm -rf ${userHome}/.local/share/Trash/*`,
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
// ─── WSL scan targets (Windows paths via /mnt/c/) ───────────────────────────
|
|
135
|
+
function getWSLScanTargets(winUser) {
|
|
136
|
+
const winHome = `/mnt/c/Users/${winUser}`;
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
name: "npm cache (Win)",
|
|
140
|
+
path: `${winHome}/AppData/Local/npm-cache`,
|
|
141
|
+
description: "Windows npm cache — re-downloaded as needed",
|
|
142
|
+
cleanCommand: `rm -rf "${winHome}/AppData/Local/npm-cache"`,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "Temp files (Win)",
|
|
146
|
+
path: `${winHome}/AppData/Local/Temp`,
|
|
147
|
+
description: "Windows user temp files",
|
|
148
|
+
cleanCommand: `find "${winHome}/AppData/Local/Temp" -mindepth 1 -maxdepth 1 -mtime +1 -exec rm -rf {} + 2>/dev/null`,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "pnpm store (Win)",
|
|
152
|
+
path: `${winHome}/AppData/Local/pnpm`,
|
|
153
|
+
description: "Windows pnpm cached packages",
|
|
154
|
+
cleanCommand: `rm -rf "${winHome}/AppData/Local/pnpm/store"`,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "yarn cache (Win)",
|
|
158
|
+
path: `${winHome}/AppData/Local/Yarn/Cache`,
|
|
159
|
+
description: "Windows yarn cached packages",
|
|
160
|
+
cleanCommand: `rm -rf "${winHome}/AppData/Local/Yarn/Cache"`,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "npm global (Win)",
|
|
164
|
+
path: `${winHome}/AppData/Roaming/npm`,
|
|
165
|
+
description: "NOT auto-deleted — shows installed global packages for manual review",
|
|
166
|
+
cleanCommand: `ls "${winHome}/AppData/Roaming/npm/node_modules"`,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "pip cache (Win)",
|
|
170
|
+
path: `${winHome}/AppData/Local/pip/cache`,
|
|
171
|
+
description: "Windows pip cached packages",
|
|
172
|
+
cleanCommand: `rm -rf "${winHome}/AppData/Local/pip/cache"`,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "NuGet cache (Win)",
|
|
176
|
+
path: `${winHome}/.nuget/packages`,
|
|
177
|
+
description: "Windows .NET NuGet packages",
|
|
178
|
+
cleanCommand: `rm -rf "${winHome}/.nuget/packages"`,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: "Windows Temp",
|
|
182
|
+
path: "/mnt/c/Windows/Temp",
|
|
183
|
+
description: "System temporary files",
|
|
184
|
+
cleanCommand: `find /mnt/c/Windows/Temp -mindepth 1 -maxdepth 1 -mtime +1 -exec rm -rf {} + 2>/dev/null`,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: "Win Update DL",
|
|
188
|
+
path: "/mnt/c/Windows/SoftwareDistribution/Download",
|
|
189
|
+
description: "Old Windows Update download files",
|
|
190
|
+
cleanCommand: `rm -rf /mnt/c/Windows/SoftwareDistribution/Download/* 2>/dev/null`,
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
function detectWindowsUser() {
|
|
195
|
+
try {
|
|
196
|
+
// Try cmd.exe interop first — most reliable
|
|
197
|
+
const whoamiRaw = execSync("cmd.exe /c echo %USERNAME% 2>/dev/null", { encoding: "utf-8", timeout: 5000 }).trim();
|
|
198
|
+
// cmd.exe may print UNC path warning lines before the actual output
|
|
199
|
+
const lastLine = whoamiRaw.split("\n").pop()?.trim() ?? "";
|
|
200
|
+
if (lastLine && !lastLine.includes("%") && !lastLine.includes("UNC")) {
|
|
201
|
+
return lastLine;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch { }
|
|
205
|
+
try {
|
|
206
|
+
// Fallback: scan /mnt/c/Users for real user directories
|
|
207
|
+
const dirs = execSync("ls /mnt/c/Users 2>/dev/null", { encoding: "utf-8" }).trim().split("\n");
|
|
208
|
+
const systemDirs = new Set([
|
|
209
|
+
"Public", "Default", "Default User", "All Users",
|
|
210
|
+
"desktop.ini", "TEMP", "UMFD-0", "UMFD-1",
|
|
211
|
+
"postgres", "ContainerAdministrator", "ContainerUser",
|
|
212
|
+
]);
|
|
213
|
+
const users = dirs.filter((d) => d && !systemDirs.has(d) && !d.startsWith("."));
|
|
214
|
+
return users[0] ?? null;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function scanDocker(isWin) {
|
|
221
|
+
const results = [];
|
|
222
|
+
// Check if Docker is available and responsive
|
|
223
|
+
try {
|
|
224
|
+
await execAsync("docker info", { timeout: 10_000 });
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return results; // Docker not running or not installed
|
|
228
|
+
}
|
|
229
|
+
// Stopped containers
|
|
230
|
+
try {
|
|
231
|
+
const { stdout } = await execAsync('docker ps -a --filter "status=exited" --filter "status=dead" --format "{{.Size}}"', { timeout: 15_000 });
|
|
232
|
+
const containerLines = stdout.trim().split("\n").filter(Boolean);
|
|
233
|
+
if (containerLines.length > 0) {
|
|
234
|
+
// Get count for description
|
|
235
|
+
const { stdout: countOut } = await execAsync('docker ps -a --filter "status=exited" --filter "status=dead" -q', { timeout: 10_000 });
|
|
236
|
+
const count = countOut.trim().split("\n").filter(Boolean).length;
|
|
237
|
+
if (count > 0) {
|
|
238
|
+
// Estimate size from docker system df
|
|
239
|
+
const sizeGB = await getDockerComponentSize("Containers");
|
|
240
|
+
if (sizeGB > 0.01) {
|
|
241
|
+
results.push({
|
|
242
|
+
name: "Stopped containers",
|
|
243
|
+
sizeGB,
|
|
244
|
+
description: `${count} stopped/dead container(s)`,
|
|
245
|
+
cleanCommand: "docker container prune -f",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch { }
|
|
252
|
+
// Dangling images (untagged, orphaned)
|
|
253
|
+
try {
|
|
254
|
+
const { stdout } = await execAsync('docker images --filter "dangling=true" -q', { timeout: 10_000 });
|
|
255
|
+
const danglingIds = stdout.trim().split("\n").filter(Boolean);
|
|
256
|
+
if (danglingIds.length > 0) {
|
|
257
|
+
const sizeGB = await getDockerImageSize("dangling=true");
|
|
258
|
+
if (sizeGB > 0.01) {
|
|
259
|
+
results.push({
|
|
260
|
+
name: "Dangling images",
|
|
261
|
+
sizeGB,
|
|
262
|
+
description: `${danglingIds.length} untagged/orphaned image(s)`,
|
|
263
|
+
cleanCommand: "docker image prune -f",
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch { }
|
|
269
|
+
// Unused images (not referenced by any container)
|
|
270
|
+
try {
|
|
271
|
+
const { stdout: allImages } = await execAsync("docker images -q", { timeout: 10_000 });
|
|
272
|
+
const { stdout: usedImages } = await execAsync('docker ps -a --format "{{.Image}}"', { timeout: 10_000 });
|
|
273
|
+
const allCount = allImages.trim().split("\n").filter(Boolean).length;
|
|
274
|
+
const usedSet = new Set(usedImages.trim().split("\n").filter(Boolean));
|
|
275
|
+
// Count unused non-dangling images
|
|
276
|
+
const { stdout: tagged } = await execAsync('docker images --format "{{.Repository}}:{{.Tag}}" --filter "dangling=false"', { timeout: 10_000 });
|
|
277
|
+
const taggedImages = tagged.trim().split("\n").filter(Boolean);
|
|
278
|
+
const unused = taggedImages.filter((img) => !usedSet.has(img) && img !== "<none>:<none>");
|
|
279
|
+
if (unused.length > 0) {
|
|
280
|
+
const sizeGB = await getDockerImageSize("dangling=false") * (unused.length / Math.max(taggedImages.length, 1));
|
|
281
|
+
if (sizeGB > 0.05) {
|
|
282
|
+
results.push({
|
|
283
|
+
name: "Unused images",
|
|
284
|
+
sizeGB: Math.round(sizeGB * 100) / 100,
|
|
285
|
+
description: `${unused.length} image(s) not used by any container`,
|
|
286
|
+
cleanCommand: "docker image prune -af",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch { }
|
|
292
|
+
// Dangling volumes
|
|
293
|
+
try {
|
|
294
|
+
const { stdout } = await execAsync("docker volume ls --filter dangling=true -q", { timeout: 10_000 });
|
|
295
|
+
const vols = stdout.trim().split("\n").filter(Boolean);
|
|
296
|
+
if (vols.length > 0) {
|
|
297
|
+
// Volumes can be huge — estimate via docker system df
|
|
298
|
+
const sizeGB = await getDockerComponentSize("Volumes");
|
|
299
|
+
results.push({
|
|
300
|
+
name: "Dangling volumes",
|
|
301
|
+
sizeGB: sizeGB > 0 ? sizeGB : 0.01,
|
|
302
|
+
description: `${vols.length} volume(s) not used by any container`,
|
|
303
|
+
cleanCommand: "docker volume prune -f",
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch { }
|
|
308
|
+
// Build cache
|
|
309
|
+
try {
|
|
310
|
+
const sizeGB = await getDockerComponentSize("Build Cache");
|
|
311
|
+
if (sizeGB > 0.05) {
|
|
312
|
+
results.push({
|
|
313
|
+
name: "Docker build cache",
|
|
314
|
+
sizeGB,
|
|
315
|
+
description: "Cached build layers",
|
|
316
|
+
cleanCommand: "docker builder prune -f",
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch { }
|
|
321
|
+
return results;
|
|
322
|
+
}
|
|
323
|
+
async function getDockerComponentSize(component) {
|
|
324
|
+
try {
|
|
325
|
+
const { stdout } = await execAsync("docker system df", { timeout: 15_000 });
|
|
326
|
+
for (const line of stdout.split("\n")) {
|
|
327
|
+
if (line.startsWith(component) || line.includes(component)) {
|
|
328
|
+
// Parse: TYPE TOTAL ACTIVE SIZE RECLAIMABLE
|
|
329
|
+
const parts = line.split(/\s{2,}/);
|
|
330
|
+
// Reclaimable is the last column, e.g. "2.5GB (100%)"
|
|
331
|
+
const reclaimable = parts[parts.length - 1];
|
|
332
|
+
const sizeMatch = reclaimable.match(/([\d.]+)\s*(B|KB|MB|GB|TB)/i);
|
|
333
|
+
if (sizeMatch) {
|
|
334
|
+
const num = parseFloat(sizeMatch[1]);
|
|
335
|
+
const unit = sizeMatch[2].toUpperCase();
|
|
336
|
+
if (unit === "TB")
|
|
337
|
+
return num * 1024;
|
|
338
|
+
if (unit === "GB")
|
|
339
|
+
return num;
|
|
340
|
+
if (unit === "MB")
|
|
341
|
+
return num / 1024;
|
|
342
|
+
if (unit === "KB")
|
|
343
|
+
return num / (1024 * 1024);
|
|
344
|
+
return num / 1073741824;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch { }
|
|
350
|
+
return 0;
|
|
351
|
+
}
|
|
352
|
+
async function getDockerImageSize(filter) {
|
|
353
|
+
try {
|
|
354
|
+
const { stdout } = await execAsync(`docker images --filter "${filter}" --format "{{.Size}}"`, { timeout: 10_000 });
|
|
355
|
+
let total = 0;
|
|
356
|
+
for (const line of stdout.trim().split("\n").filter(Boolean)) {
|
|
357
|
+
const match = line.match(/([\d.]+)\s*(B|KB|MB|GB|TB)/i);
|
|
358
|
+
if (match) {
|
|
359
|
+
const num = parseFloat(match[1]);
|
|
360
|
+
const unit = match[2].toUpperCase();
|
|
361
|
+
if (unit === "TB")
|
|
362
|
+
total += num * 1024;
|
|
363
|
+
else if (unit === "GB")
|
|
364
|
+
total += num;
|
|
365
|
+
else if (unit === "MB")
|
|
366
|
+
total += num / 1024;
|
|
367
|
+
else if (unit === "KB")
|
|
368
|
+
total += num / (1024 * 1024);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return Math.round(total * 100) / 100;
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// ─── Scanner ─────────────────────────────────────────────────────────────────
|
|
378
|
+
/** Convert a WSL /mnt/c/... path to a Windows C:\... path for PowerShell. */
|
|
379
|
+
function wslToWinPath(p) {
|
|
380
|
+
const match = p.match(/^\/mnt\/([a-z])\/(.*)$/);
|
|
381
|
+
if (!match)
|
|
382
|
+
return null;
|
|
383
|
+
return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
|
|
384
|
+
}
|
|
385
|
+
const POWERSHELL_EXE = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe";
|
|
386
|
+
async function getDirSizeGB(dirPath, platform) {
|
|
387
|
+
try {
|
|
388
|
+
if (platform.os === "windows") {
|
|
389
|
+
const { stdout } = await execAsync(`powershell -Command "(Get-ChildItem '${dirPath}' -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum / 1GB"`, { timeout: 60_000 });
|
|
390
|
+
const val = parseFloat(stdout.trim());
|
|
391
|
+
return isNaN(val) ? 0 : Math.round(val * 100) / 100;
|
|
392
|
+
}
|
|
393
|
+
// WSL + Windows path: use PowerShell for speed (du on /mnt/c is very slow)
|
|
394
|
+
const winPath = platform.isWSL ? wslToWinPath(dirPath) : null;
|
|
395
|
+
if (winPath) {
|
|
396
|
+
const { stdout } = await execAsync(`${POWERSHELL_EXE} -Command "(Get-ChildItem '${winPath}' -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum / 1GB"`, { timeout: 60_000 });
|
|
397
|
+
const val = parseFloat(stdout.trim());
|
|
398
|
+
return isNaN(val) ? 0 : Math.round(val * 100) / 100;
|
|
399
|
+
}
|
|
400
|
+
// Linux native path
|
|
401
|
+
const { stdout } = await execAsync(`du -sb "${dirPath}" 2>/dev/null | cut -f1`, { timeout: 30_000 });
|
|
402
|
+
const bytes = parseInt(stdout.trim());
|
|
403
|
+
return isNaN(bytes) ? 0 : Math.round((bytes / 1073741824) * 100) / 100;
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return 0;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function getUserHome(platform) {
|
|
410
|
+
if (platform.os === "windows") {
|
|
411
|
+
return process.env.USERPROFILE ?? "C:\\Users\\" + (process.env.USERNAME ?? "User");
|
|
412
|
+
}
|
|
413
|
+
return process.env.HOME ?? "/root";
|
|
414
|
+
}
|
|
415
|
+
export async function scanForCleanup(platform) {
|
|
416
|
+
const plat = platform ?? detectLocalPlatform();
|
|
417
|
+
const userHome = getUserHome(plat);
|
|
418
|
+
let targets;
|
|
419
|
+
if (plat.os === "windows") {
|
|
420
|
+
targets = getWindowsScanTargets(userHome);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
targets = getLinuxScanTargets(userHome);
|
|
424
|
+
// In WSL, also scan Windows-side paths via /mnt/c
|
|
425
|
+
if (plat.isWSL) {
|
|
426
|
+
const winUser = detectWindowsUser();
|
|
427
|
+
if (winUser) {
|
|
428
|
+
targets = [...targets, ...getWSLScanTargets(winUser)];
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const results = [];
|
|
433
|
+
// Scan in parallel batches of 4 to avoid overwhelming the system
|
|
434
|
+
for (let i = 0; i < targets.length; i += 4) {
|
|
435
|
+
const batch = targets.slice(i, i + 4);
|
|
436
|
+
const sizes = await Promise.all(batch.map((t) => getDirSizeGB(t.path, plat)));
|
|
437
|
+
for (let j = 0; j < batch.length; j++) {
|
|
438
|
+
if (sizes[j] > 0.01) {
|
|
439
|
+
results.push({
|
|
440
|
+
name: batch[j].name,
|
|
441
|
+
path: batch[j].path,
|
|
442
|
+
sizeGB: sizes[j],
|
|
443
|
+
safe: true,
|
|
444
|
+
description: batch[j].description,
|
|
445
|
+
cleanCommand: batch[j].cleanCommand,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Scan Docker separately (uses docker CLI, not directory sizes)
|
|
451
|
+
try {
|
|
452
|
+
const dockerTargets = await scanDocker(plat.os === "windows");
|
|
453
|
+
for (const dt of dockerTargets) {
|
|
454
|
+
results.push({
|
|
455
|
+
name: dt.name,
|
|
456
|
+
path: "[docker]",
|
|
457
|
+
sizeGB: dt.sizeGB,
|
|
458
|
+
safe: true,
|
|
459
|
+
description: dt.description,
|
|
460
|
+
cleanCommand: dt.cleanCommand,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
catch { }
|
|
465
|
+
// Sort by size descending
|
|
466
|
+
results.sort((a, b) => b.sizeGB - a.sizeGB);
|
|
467
|
+
return results;
|
|
468
|
+
}
|
|
469
|
+
// ─── Formatter ───────────────────────────────────────────────────────────────
|
|
470
|
+
export function formatCleanupTable(targets) {
|
|
471
|
+
if (targets.length === 0) {
|
|
472
|
+
return `${c.green}✓ No significant reclaimable space found.${c.reset}`;
|
|
473
|
+
}
|
|
474
|
+
const lines = [];
|
|
475
|
+
const totalGB = targets.reduce((sum, t) => sum + t.sizeGB, 0);
|
|
476
|
+
lines.push(`\n${c.bold}${c.cyan}── Disk Cleanup Scan ──${c.reset}\n`);
|
|
477
|
+
// Table header
|
|
478
|
+
const nameW = Math.max(12, ...targets.map((t) => t.name.length)) + 2;
|
|
479
|
+
const sizeW = 10;
|
|
480
|
+
lines.push(` ${c.bold}${"#".padEnd(4)}${"Location".padEnd(nameW)}${"Size".padStart(sizeW)} ${"Description"}${c.reset}`);
|
|
481
|
+
lines.push(` ${"─".repeat(4)}${"─".repeat(nameW)}${"─".repeat(sizeW)}${"─".repeat(2)}${"─".repeat(30)}`);
|
|
482
|
+
for (let i = 0; i < targets.length; i++) {
|
|
483
|
+
const t = targets[i];
|
|
484
|
+
const sizeStr = t.sizeGB >= 1
|
|
485
|
+
? `${t.sizeGB.toFixed(2)} GB`
|
|
486
|
+
: `${(t.sizeGB * 1024).toFixed(0)} MB`;
|
|
487
|
+
const color = t.sizeGB >= 1 ? c.yellow : c.dim;
|
|
488
|
+
lines.push(` ${String(i + 1).padEnd(4)}${t.name.padEnd(nameW)}${color}${sizeStr.padStart(sizeW)}${c.reset} ${c.dim}${t.description}${c.reset}`);
|
|
489
|
+
}
|
|
490
|
+
lines.push(`\n ${c.bold}Total reclaimable: ~${totalGB.toFixed(2)} GB${c.reset}`);
|
|
491
|
+
lines.push(`\n ${c.green}${c.bold}Nothing has been deleted yet.${c.reset} These are only caches and temp files.`);
|
|
492
|
+
lines.push(` ${c.green}None of your code, projects, documents, or settings will be touched.${c.reset}`);
|
|
493
|
+
lines.push(` ${c.dim}You will be asked to confirm each item individually before anything is removed.${c.reset}`);
|
|
494
|
+
// WSL note: mention restart option will be offered after cleanup
|
|
495
|
+
const plat = detectLocalPlatform();
|
|
496
|
+
if (plat.isWSL && targets.some((t) => t.path.startsWith("/mnt/"))) {
|
|
497
|
+
lines.push(` ${c.yellow}${c.bold}⚠ WSL:${c.reset} After cleanup, you'll be offered to restart WSL to clear I/O errors.`);
|
|
498
|
+
}
|
|
499
|
+
return lines.join("\n");
|
|
500
|
+
}
|
|
501
|
+
// ─── Interactive cleanup ─────────────────────────────────────────────────────
|
|
502
|
+
export async function runInteractiveCleanup(targets) {
|
|
503
|
+
if (targets.length === 0) {
|
|
504
|
+
return `${c.green}✓ Nothing to clean.${c.reset}`;
|
|
505
|
+
}
|
|
506
|
+
const lines = [];
|
|
507
|
+
let totalCleaned = 0;
|
|
508
|
+
let cleanAll = false;
|
|
509
|
+
console.log(`\n${c.bold}Reviewing each item — nothing is deleted without your approval:${c.reset}`);
|
|
510
|
+
console.log(`${c.dim} y = yes, N = no (default), all = clean remaining, stop = done${c.reset}\n`);
|
|
511
|
+
for (const target of targets) {
|
|
512
|
+
const sizeStr = target.sizeGB >= 1
|
|
513
|
+
? `${target.sizeGB.toFixed(2)} GB`
|
|
514
|
+
: `${(target.sizeGB * 1024).toFixed(0)} MB`;
|
|
515
|
+
let shouldClean = false;
|
|
516
|
+
if (cleanAll) {
|
|
517
|
+
shouldClean = true;
|
|
518
|
+
console.log(` ${c.cyan}→${c.reset} ${target.name} (${sizeStr}) — auto-cleaning`);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
const answer = await askWithControl(`Delete ${c.bold}${target.name}${c.reset} (${c.yellow}${sizeStr}${c.reset})? ${c.dim}${target.description}${c.reset}`);
|
|
522
|
+
if (answer === "stop") {
|
|
523
|
+
lines.push(` ${c.dim} Stopped — remaining items not deleted.${c.reset}`);
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
else if (answer === "all") {
|
|
527
|
+
shouldClean = true;
|
|
528
|
+
cleanAll = true;
|
|
529
|
+
console.log(` ${c.cyan}Cleaning all remaining items...${c.reset}`);
|
|
530
|
+
}
|
|
531
|
+
else if (answer === "yes") {
|
|
532
|
+
shouldClean = true;
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
lines.push(` ${c.dim} Skipped ${target.name} — not deleted${c.reset}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (shouldClean) {
|
|
539
|
+
try {
|
|
540
|
+
await execAsync(target.cleanCommand, { timeout: 120_000 });
|
|
541
|
+
lines.push(` ${c.green}✓${c.reset} Deleted ${target.name} (${sizeStr} freed)`);
|
|
542
|
+
totalCleaned += target.sizeGB;
|
|
543
|
+
}
|
|
544
|
+
catch (err) {
|
|
545
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
546
|
+
lines.push(` ${c.red}✗${c.reset} Failed to delete ${target.name}: ${msg.split("\n")[0]}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (totalCleaned > 0) {
|
|
551
|
+
lines.push(`\n${c.green}${c.bold}✓ Freed ~${totalCleaned.toFixed(2)} GB${c.reset}`);
|
|
552
|
+
// WSL: offer to restart WSL to clear I/O errors
|
|
553
|
+
const plat = detectLocalPlatform();
|
|
554
|
+
if (plat.isWSL) {
|
|
555
|
+
console.log(lines.join("\n"));
|
|
556
|
+
lines.length = 0;
|
|
557
|
+
console.log(`\n ${c.yellow}${c.bold}⚠ WSL:${c.reset} If you were seeing I/O errors, WSL needs to restart to clear them.`);
|
|
558
|
+
console.log(` ${c.red}${c.bold} This will shut down ALL WSL sessions and disconnect this terminal.${c.reset}`);
|
|
559
|
+
console.log(` ${c.dim} You can also do this manually later from PowerShell: wsl --shutdown${c.reset}\n`);
|
|
560
|
+
const confirmed = await askForStrictConfirmation(` ${c.bold}Restart WSL now?${c.reset}`, "RESTART_WSL");
|
|
561
|
+
if (confirmed) {
|
|
562
|
+
console.log(`\n ${c.cyan}Shutting down WSL...${c.reset}`);
|
|
563
|
+
try {
|
|
564
|
+
await execAsync("cmd.exe /c wsl --shutdown", { timeout: 30_000 });
|
|
565
|
+
// If we get here, WSL didn't actually kill us yet
|
|
566
|
+
lines.push(` ${c.green}✓${c.reset} WSL shutdown initiated. Reopen your terminal.`);
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
// Expected — the process gets killed when WSL shuts down
|
|
570
|
+
lines.push(` ${c.dim}WSL is shutting down...${c.reset}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
lines.push(` ${c.dim}Skipped WSL restart. Run manually if needed:${c.reset} ${c.cyan}wsl --shutdown${c.reset}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
lines.push(`\n${c.dim}No changes made.${c.reset}`);
|
|
580
|
+
}
|
|
581
|
+
return lines.join("\n");
|
|
582
|
+
}
|
|
583
|
+
export async function smartDriveScan(platform) {
|
|
584
|
+
const plat = platform ?? detectLocalPlatform();
|
|
585
|
+
if (plat.os === "windows") {
|
|
586
|
+
return scanWindowsDrives("powershell");
|
|
587
|
+
}
|
|
588
|
+
else if (plat.isWSL) {
|
|
589
|
+
return scanWSLDrives();
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
return scanLinuxDrives();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async function scanWindowsDrives(psExe) {
|
|
596
|
+
const drives = [];
|
|
597
|
+
try {
|
|
598
|
+
const { stdout } = await execAsync(`${psExe} -Command "Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' | ForEach-Object { \\$_.DeviceID + '|' + \\$_.VolumeName + '|' + \\$_.FileSystem + '|' + \\$_.Size + '|' + \\$_.FreeSpace }"`, { timeout: 15_000 });
|
|
599
|
+
for (const line of stdout.trim().split("\n").filter(Boolean)) {
|
|
600
|
+
const [device, label, fs, sizeRaw, freeRaw] = line.trim().split("|");
|
|
601
|
+
const sizeGB = Math.round(parseInt(sizeRaw) / 1073741824 * 10) / 10;
|
|
602
|
+
const freeGB = Math.round(parseInt(freeRaw) / 1073741824 * 10) / 10;
|
|
603
|
+
const usedGB = Math.round((sizeGB - freeGB) * 10) / 10;
|
|
604
|
+
const usePct = sizeGB > 0 ? Math.round((usedGB / sizeGB) * 100) : 0;
|
|
605
|
+
drives.push({ device, label: label || "", filesystem: fs || "", sizeGB, usedGB, freeGB, usePct, mount: device + "\\" });
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
catch { }
|
|
609
|
+
return drives;
|
|
610
|
+
}
|
|
611
|
+
async function scanWSLDrives() {
|
|
612
|
+
const drives = [];
|
|
613
|
+
// Get Windows drives via PowerShell (accurate labels + filesystem info)
|
|
614
|
+
try {
|
|
615
|
+
const winDrives = await scanWindowsDrives(POWERSHELL_EXE);
|
|
616
|
+
for (const d of winDrives) {
|
|
617
|
+
const letter = d.device.replace(":", "").toLowerCase();
|
|
618
|
+
d.mount = `/mnt/${letter}`;
|
|
619
|
+
drives.push(d);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
catch { }
|
|
623
|
+
// Also add the WSL root filesystem
|
|
624
|
+
try {
|
|
625
|
+
const { stdout } = await execAsync("df -B1 / | tail -1", { timeout: 5000 });
|
|
626
|
+
const parts = stdout.trim().split(/\s+/);
|
|
627
|
+
if (parts.length >= 5) {
|
|
628
|
+
const sizeGB = Math.round(parseInt(parts[1]) / 1073741824 * 10) / 10;
|
|
629
|
+
const usedGB = Math.round(parseInt(parts[2]) / 1073741824 * 10) / 10;
|
|
630
|
+
const freeGB = Math.round(parseInt(parts[3]) / 1073741824 * 10) / 10;
|
|
631
|
+
const usePct = parseInt(parts[4]);
|
|
632
|
+
drives.push({ device: parts[0], label: "WSL Root", filesystem: "ext4", sizeGB, usedGB, freeGB, usePct, mount: "/" });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
catch { }
|
|
636
|
+
return drives;
|
|
637
|
+
}
|
|
638
|
+
async function scanLinuxDrives() {
|
|
639
|
+
const drives = [];
|
|
640
|
+
try {
|
|
641
|
+
const { stdout } = await execAsync("df -B1 -x tmpfs -x devtmpfs -x squashfs -x overlay 2>/dev/null || df -B1 | grep -v tmpfs", { timeout: 10_000 });
|
|
642
|
+
for (const line of stdout.trim().split("\n").slice(1)) {
|
|
643
|
+
const parts = line.trim().split(/\s+/);
|
|
644
|
+
if (parts.length < 6 || parts[0] === "Filesystem" || parts[0].startsWith("snap"))
|
|
645
|
+
continue;
|
|
646
|
+
const sizeGB = Math.round(parseInt(parts[1]) / 1073741824 * 10) / 10;
|
|
647
|
+
const usedGB = Math.round(parseInt(parts[2]) / 1073741824 * 10) / 10;
|
|
648
|
+
const freeGB = Math.round(parseInt(parts[3]) / 1073741824 * 10) / 10;
|
|
649
|
+
const usePct = parseInt(parts[4]);
|
|
650
|
+
if (sizeGB < 0.1)
|
|
651
|
+
continue;
|
|
652
|
+
drives.push({ device: parts[0], label: "", filesystem: "", sizeGB, usedGB, freeGB, usePct, mount: parts[5] });
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
catch { }
|
|
656
|
+
return drives;
|
|
657
|
+
}
|
|
658
|
+
/** Scan top-level directories on a drive to find space hogs. */
|
|
659
|
+
async function scanTopDirs(drive, platform) {
|
|
660
|
+
const results = [];
|
|
661
|
+
if (platform.os === "windows" || (platform.isWSL && drive.mount.startsWith("/mnt/"))) {
|
|
662
|
+
const winDrive = platform.isWSL ? drive.device + "\\" : drive.mount;
|
|
663
|
+
const psExe = platform.isWSL ? POWERSHELL_EXE : "powershell";
|
|
664
|
+
try {
|
|
665
|
+
const { stdout } = await execAsync(`${psExe} -Command "Get-ChildItem '${winDrive}' -Directory -Force -ErrorAction SilentlyContinue | Where-Object { \\$_.Name -notmatch '^(\\$Recycle|System Volume|Recovery)' } | ForEach-Object { \\$s = 0; try { \\$s = (Get-ChildItem \\$_.FullName -Recurse -File -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum } catch {}; if(\\$s -gt 104857600) { '{0}|{1}' -f [math]::Round(\\$s/1GB,2),\\$_.Name } }"`, { timeout: 300_000 });
|
|
666
|
+
for (const line of stdout.trim().split("\n").filter(Boolean)) {
|
|
667
|
+
const [sizeStr, name] = line.trim().split("|");
|
|
668
|
+
if (name && sizeStr && !isNaN(parseFloat(sizeStr))) {
|
|
669
|
+
results.push({ path: name, sizeGB: parseFloat(sizeStr) });
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
catch { }
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
try {
|
|
677
|
+
const { stdout } = await execAsync(`du -B1 --max-depth=1 "${drive.mount}" 2>/dev/null | sort -rn | head -15`, { timeout: 30_000 });
|
|
678
|
+
for (const line of stdout.trim().split("\n").filter(Boolean)) {
|
|
679
|
+
const parts = line.split("\t");
|
|
680
|
+
if (parts.length < 2)
|
|
681
|
+
continue;
|
|
682
|
+
const sizeGB = Math.round(parseInt(parts[0]) / 1073741824 * 100) / 100;
|
|
683
|
+
const dirName = parts[1].replace(drive.mount === "/" ? "" : drive.mount, "").replace(/^\//, "") || "/";
|
|
684
|
+
if (sizeGB > 0.1 && dirName !== drive.mount && dirName !== "") {
|
|
685
|
+
results.push({ path: dirName, sizeGB });
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
catch { }
|
|
690
|
+
}
|
|
691
|
+
results.sort((a, b) => b.sizeGB - a.sizeGB);
|
|
692
|
+
return results.slice(0, 10);
|
|
693
|
+
}
|
|
694
|
+
function driveUsageBar(percent, width = 20) {
|
|
695
|
+
const filled = Math.round((percent / 100) * width);
|
|
696
|
+
const empty = width - filled;
|
|
697
|
+
const color = percent >= 95 ? c.red : percent >= 85 ? c.yellow : c.green;
|
|
698
|
+
return `${color}${"█".repeat(filled)}${"░".repeat(empty)}${c.reset}`;
|
|
699
|
+
}
|
|
700
|
+
export async function formatDriveScan(drives, deepScan = false) {
|
|
701
|
+
const plat = detectLocalPlatform();
|
|
702
|
+
const lines = [];
|
|
703
|
+
lines.push(`\n${c.bold}${c.cyan}── Drive Analysis ──${c.reset}\n`);
|
|
704
|
+
const sorted = [...drives].sort((a, b) => b.usePct - a.usePct);
|
|
705
|
+
for (const d of sorted) {
|
|
706
|
+
const bar = driveUsageBar(d.usePct);
|
|
707
|
+
const status = d.usePct >= 95 ? `${c.red}CRITICAL${c.reset}` :
|
|
708
|
+
d.usePct >= 85 ? `${c.yellow}WARNING${c.reset}` :
|
|
709
|
+
d.usePct >= 70 ? `${c.dim}MODERATE${c.reset}` :
|
|
710
|
+
`${c.green}OK${c.reset}`;
|
|
711
|
+
const labelStr = d.label ? ` ${c.dim}(${d.label})${c.reset}` : "";
|
|
712
|
+
const fsStr = d.filesystem ? ` ${c.dim}[${d.filesystem}]${c.reset}` : "";
|
|
713
|
+
lines.push(` ${c.bold}${d.device}${c.reset}${labelStr}${fsStr} ${d.mount}`);
|
|
714
|
+
lines.push(` ${bar} ${d.usePct}% used ${c.bold}${d.usedGB}G${c.reset} / ${d.sizeGB}G (${c.bold}${d.freeGB}G free${c.reset}) ${status}`);
|
|
715
|
+
// Deep scan: show top directories for critical/warning drives
|
|
716
|
+
if (deepScan && d.usePct >= 85) {
|
|
717
|
+
const topDirs = await scanTopDirs(d, plat);
|
|
718
|
+
if (topDirs.length > 0) {
|
|
719
|
+
lines.push(` ${c.dim}Top space usage:${c.reset}`);
|
|
720
|
+
for (const dir of topDirs.slice(0, 5)) {
|
|
721
|
+
const pct = d.sizeGB > 0 ? Math.round((dir.sizeGB / d.sizeGB) * 100) : 0;
|
|
722
|
+
const sizeStr = dir.sizeGB >= 1 ? `${dir.sizeGB.toFixed(1)}G` : `${(dir.sizeGB * 1024).toFixed(0)}M`;
|
|
723
|
+
lines.push(` ${c.yellow}${sizeStr.padStart(7)}${c.reset} ${dir.path} ${c.dim}(${pct}%)${c.reset}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
lines.push("");
|
|
728
|
+
}
|
|
729
|
+
const critCount = sorted.filter(d => d.usePct >= 95).length;
|
|
730
|
+
const warnCount = sorted.filter(d => d.usePct >= 85 && d.usePct < 95).length;
|
|
731
|
+
if (critCount > 0) {
|
|
732
|
+
lines.push(` ${c.red}${c.bold}${critCount} drive(s) critically full!${c.reset} Run "free up space" to clean.`);
|
|
733
|
+
}
|
|
734
|
+
if (warnCount > 0) {
|
|
735
|
+
lines.push(` ${c.yellow}${warnCount} drive(s) approaching full.${c.reset}`);
|
|
736
|
+
}
|
|
737
|
+
if (critCount === 0 && warnCount === 0) {
|
|
738
|
+
lines.push(` ${c.green}✓ All drives healthy.${c.reset}`);
|
|
739
|
+
}
|
|
740
|
+
return lines.join("\n");
|
|
741
|
+
}
|
|
742
|
+
// ─── Background deep scan ────────────────────────────────────────────────────
|
|
743
|
+
/**
|
|
744
|
+
* Kicks off a background deep scan of critical/warning drives.
|
|
745
|
+
* Shows top space-consuming directories when complete.
|
|
746
|
+
* Uses the TaskRunner so the interactive REPL picks up the notification.
|
|
747
|
+
*/
|
|
748
|
+
export function runDeepScanBackground(drives) {
|
|
749
|
+
const intent = {
|
|
750
|
+
intent: "disk.scan.deep",
|
|
751
|
+
confidence: 1,
|
|
752
|
+
rawText: "deep scan drives",
|
|
753
|
+
fields: {},
|
|
754
|
+
};
|
|
755
|
+
taskRunner.submit("Deep scanning drives for top space usage...", intent, async () => {
|
|
756
|
+
const plat = detectLocalPlatform();
|
|
757
|
+
const lines = [];
|
|
758
|
+
lines.push(`\n${c.bold}${c.cyan}── Deep Scan Results ──${c.reset}\n`);
|
|
759
|
+
for (const d of drives) {
|
|
760
|
+
const topDirs = await scanTopDirs(d, plat);
|
|
761
|
+
if (topDirs.length === 0)
|
|
762
|
+
continue;
|
|
763
|
+
const labelStr = d.label ? ` (${d.label})` : "";
|
|
764
|
+
lines.push(` ${c.bold}${d.device}${c.reset}${labelStr} ${d.mount} ${c.dim}(${d.usePct}% full, ${d.freeGB}G free)${c.reset}`);
|
|
765
|
+
for (const dir of topDirs.slice(0, 7)) {
|
|
766
|
+
const pct = d.sizeGB > 0 ? Math.round((dir.sizeGB / d.sizeGB) * 100) : 0;
|
|
767
|
+
const sizeStr = dir.sizeGB >= 1 ? `${dir.sizeGB.toFixed(1)}G` : `${(dir.sizeGB * 1024).toFixed(0)}M`;
|
|
768
|
+
lines.push(` ${c.yellow}${sizeStr.padStart(7)}${c.reset} ${dir.path} ${c.dim}(${pct}% of drive)${c.reset}`);
|
|
769
|
+
}
|
|
770
|
+
lines.push("");
|
|
771
|
+
}
|
|
772
|
+
lines.push(` ${c.dim}Tip: Run "free up space" to clean caches and temp files.${c.reset}`);
|
|
773
|
+
return lines.join("\n");
|
|
774
|
+
});
|
|
775
|
+
}
|