dsh-disk-cleaner 1.2.0 → 1.3.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/README.md +32 -13
- package/lib/catalog.js +150 -2
- package/lib/client.js +16 -0
- package/lib/index.js +12 -2
- package/package.json +1 -1
- package/smoke.mjs +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<!-- badges -->
|
|
6
6
|
| | |
|
|
7
7
|
| --- | --- |
|
|
8
|
-
| **version** | `1.
|
|
8
|
+
| **version** | `1.3.0` |
|
|
9
9
|
| **DSH** | `0.2.1-rc.5`(开发与实测版本;用到 `plugins.bundle.config`、`settings.section`) |
|
|
10
10
|
| **license** | [MIT](LICENSE) |
|
|
11
11
|
| **Node** | `>= 22`(仅运行冒烟测试需要) |
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
**dsh-disk-cleaner** is a disk-space plugin for [DeepSeek Harness](https://github.com/deepseek-ai) (DSH). It does two different jobs, and keeps them apart on purpose:
|
|
18
18
|
|
|
19
|
-
1. **Clean** —
|
|
19
|
+
1. **Clean** — twenty-four known-safe locations (temp folders, package manager stores, browser caches, crash dumps, the Recycle Bin, leftover update installers, per-app caches and logs, superseded plugin versions and applied update packages). Only these are ever deleted.
|
|
20
20
|
2. **Assess** — everything the plugin will *not* delete (installed programs, the WSL virtual disk, the Windows component store, chat-app data, your own folders). Measured and listed, never removed.
|
|
21
21
|
|
|
22
22
|
The assessment ends with a **plan**: the same numbers ordered by what it costs you to act, with the exact Windows command for the steps the plugin cannot perform itself.
|
|
@@ -27,9 +27,9 @@ Both the panel and the agent reach the *same* operations: the cleaning logic liv
|
|
|
27
27
|
|
|
28
28
|
| Feature | Description |
|
|
29
29
|
| --- | --- |
|
|
30
|
-
| Scan | Measures all
|
|
30
|
+
| Scan | Measures all twenty-four targets concurrently and shows what each occupies |
|
|
31
31
|
| Per-drive filter | Chip row of mounted volumes; scanning or cleaning can be limited to one drive, and rows with nothing on that drive disappear |
|
|
32
|
-
| Risk labels | Every target is `safe` or `caution`, with a one-line reason (`in-use` / `rebuild` / `elevation` / `pending-update` / `rollback` / `superseded`) and a permanent legend, so "caution" never means "unspecified" |
|
|
32
|
+
| Risk labels | Every target is `safe` or `caution`, with a one-line reason (`in-use` / `rebuild` / `elevation` / `pending-update` / `rollback` / `superseded` / `history` / `duplicate` / `database-backup`) and a permanent legend, so "caution" never means "unspecified" |
|
|
33
33
|
| Aged deletion | `temp` and `windows-temp` only remove files older than one day; files a running program still holds are skipped |
|
|
34
34
|
| Space assessment | Walks the disk read-only and groups the result into Windows / installed software / your own files, plus the ten largest app-data folders. Any row opens its folder in Explorer — click, right click, or Enter |
|
|
35
35
|
| Honest re-run | Re-running the assessment re-measures; a plain call still answers from the ten-minute cache |
|
|
@@ -65,6 +65,11 @@ Every path is built from this table — a caller selects an **id**, never a path
|
|
|
65
65
|
| `app-temp` | scratch folders apps keep inside their own `%APPDATA%` folder instead of `%TEMP%`: `WorkDaddy\client-tmp`, `PersonalCenter\Temp` | aged, 2 days | caution | the owning app may be running and holding files |
|
|
66
66
|
| `plugin-old-versions` | every version but the newest of each family in the WPS plugin pool `kingsoft\wps\addons\pool\win-i386` | purge | caution | the manager loads the newest; the rest are rollback copies |
|
|
67
67
|
| `update-packages` | update packages already applied: FTNN `.spd` patches whose build number is below the newest `Program Files\FTNN\app\[version]`, and `WorkDaddy\update` installers below the installed version | purge | caution | decided by version comparison, not by age |
|
|
68
|
+
| `session-backups` | `modify_backup` folders under `%USERPROFILE%\.workbuddy\workspace\sessions\*` — a whole-file snapshot of edits per agent session | purge | caution | the session itself lives beside the snapshot and does not read it back |
|
|
69
|
+
| `agent-logs` | date-named log folders under `%USERPROFILE%\.workbuddy\logs` and `\.workbuddy-ai\logs` | aged, 3 days | safe | nothing reads an old log back |
|
|
70
|
+
| `toolchain-snapshots` | duplicate snapshots under `%USERPROFILE%\.dsh\dsh-ops\bash-ipc`: folders whose `.ops-ipc.json` carries the same `patchedSha256` collapse to the newest | purge | caution | a folder without that metadata is never touched |
|
|
71
|
+
| `editor-server-bundles` | every bundle but the newest under `%USERPROFILE%\vscode-remote-wsl\stable` | purge | caution | the editor re-downloads the one it needs |
|
|
72
|
+
| `database-backups` | `zotero.sqlite*.bak` under `%USERPROFILE%\Zotero` | purge | caution | the live `zotero.sqlite` sits beside the copies |
|
|
68
73
|
|
|
69
74
|
`aged` targets skip anything modified within the retention window; `purge` targets remove every entry. Both keep the target directory itself and leave locked entries alone, counting them as skipped.
|
|
70
75
|
|
|
@@ -139,6 +144,10 @@ These are enforced in `lib/catalog.js` and must not be relaxed:
|
|
|
139
144
|
- Symlinks and junctions met during a walk are skipped, never followed or deleted.
|
|
140
145
|
- Locked or denied entries are counted as skipped and the walk continues.
|
|
141
146
|
- The target directory itself is kept; only its contents go.
|
|
147
|
+
- Rules that pick "the newest of a group" fail closed: a family with only one member, a folder
|
|
148
|
+
whose metadata file is missing or whose hash is not a string, and a `vscode-remote-wsl\stable`
|
|
149
|
+
that holds a single bundle all resolve to *nothing to delete*. The newest member is never returned.
|
|
150
|
+
- `database-backups` matches `.sqlite*.bak` by name only; the live `zotero.sqlite` is never a candidate.
|
|
142
151
|
- The directory Windows refuses to let an unelevated process write (`SoftwareDistribution\Download`) is labelled `elevation` and excluded from "reclaimable now". `C:\Windows\Temp` is **not** one of them on a default ACL: it grants `BUILTIN\Users` create and delete, so only EBUSY on a live file stops a delete there, which the age limit covers.
|
|
143
152
|
|
|
144
153
|
### Install
|
|
@@ -199,6 +208,7 @@ What the smoke test deliberately does **not** cover: anything that walks a real
|
|
|
199
208
|
| Dry run deletes nothing | `confirm` omitted, byte counts unchanged |
|
|
200
209
|
| Drive filter rejects unmounted and malformed volumes | `C:extra`, `Z:`, `abc` → 400 |
|
|
201
210
|
| Plan excludes elevation bytes from `reclaimNow` | Live audit against this machine |
|
|
211
|
+
| "Keep only the newest of a group" fails closed | Throwaway fixture tree: a same-hash duplicate collapses to the newest, a folder with no metadata is left alone, a different hash is kept, the live database survives (9/9) |
|
|
202
212
|
| Visual layout | Checked by hand in the running Web UI. No browser automation here, so this one is a human check, not a test. |
|
|
203
213
|
|
|
204
214
|
### Implementation notes
|
|
@@ -223,11 +233,11 @@ What the smoke test deliberately does **not** cover: anything that walks a real
|
|
|
223
233
|
```
|
|
224
234
|
├─ package.json dsh.bundle.patch + dsh.client { platform: "web" }
|
|
225
235
|
├─ cordis.patch.yml - insert: id: dsh-disk-cleaner
|
|
226
|
-
├─ lib/index.js (
|
|
227
|
-
├─ lib/catalog.js (
|
|
236
|
+
├─ lib/index.js (411) Host: HTTP route + disk_cleaner tool, same run() for both; reveal
|
|
237
|
+
├─ lib/catalog.js (836) The 24 targets, measurePath / purgePath / resolveTargets / drives
|
|
228
238
|
├─ lib/audit.js (365) Read-only assessment, 10-minute cache, areaPaths, exposes no deletion
|
|
229
239
|
├─ lib/plan.js (150) Pure: measurements → ordered steps
|
|
230
|
-
├─ lib/client.js (
|
|
240
|
+
├─ lib/client.js (1371) Client half: slots, panel, preferences, zh/en dictionaries, CSS
|
|
231
241
|
├─ locale/{en,zh}.json Display metadata (meta.title / meta.description)
|
|
232
242
|
├─ icon.svg Plugin icon
|
|
233
243
|
├─ README.md This file
|
|
@@ -244,7 +254,7 @@ Client-half slots: `sidebar.panellist` (order 50, disposed in settings-only mode
|
|
|
244
254
|
|
|
245
255
|
**dsh-disk-cleaner** 是 [DeepSeek Harness](https://github.com/deepseek-ai)(DSH)的磁盘空间插件。它做两件不同的事,并且刻意分开:
|
|
246
256
|
|
|
247
|
-
1. **清理** ——
|
|
257
|
+
1. **清理** —— 二十四个已知安全的位置(临时目录、包管理器存储、浏览器缓存、崩溃转储、回收站、更新安装包残留、各应用的缓存与日志、被淘汰的插件版本与已应用的更新包)。只有这些会被删除。
|
|
248
258
|
2. **评估** —— 所有本插件**不会**删的东西(已装程序、WSL 虚拟磁盘、Windows 组件库、聊天软件数据、你自己的文件夹)。只测量、只列出,绝不删除。
|
|
249
259
|
|
|
250
260
|
评估的最后是一份**处理方案**:同一批数字按「你要付出多少代价」排序,本插件做不到的步骤附上确切的 Windows 命令。
|
|
@@ -255,9 +265,9 @@ Client-half slots: `sidebar.panellist` (order 50, disposed in settings-only mode
|
|
|
255
265
|
|
|
256
266
|
| 功能 | 说明 |
|
|
257
267
|
| --- | --- |
|
|
258
|
-
| 扫描 |
|
|
268
|
+
| 扫描 | 并发测量全部二十四个目标,显示各自占用 |
|
|
259
269
|
| 按盘过滤 | 已挂载卷的胶囊按钮;扫描与清理都可限定单盘,该盘上为空的项会隐藏 |
|
|
260
|
-
| 风险标注 | 每个目标都标 `安全` 或 `注意`,并给一行理由(`in-use` / `rebuild` / `elevation` / `pending-update` / `rollback` / `superseded`),图例常驻,所以「注意」永远不是「说不清」 |
|
|
270
|
+
| 风险标注 | 每个目标都标 `安全` 或 `注意`,并给一行理由(`in-use` / `rebuild` / `elevation` / `pending-update` / `rollback` / `superseded` / `history` / `duplicate` / `database-backup`),图例常驻,所以「注意」永远不是「说不清」 |
|
|
261
271
|
| 保留期删除 | `temp` 与 `windows-temp` 只删一天前的文件;被运行中程序占住的自动跳过 |
|
|
262
272
|
| 空间评估 | 只读遍历磁盘,按 Windows 自身 / 已装软件 / 你自己的文件 分组,另列占用最大的十个应用数据目录。每一行都能打开所在目录——单击、右键或回车 |
|
|
263
273
|
| 重新评估真的重测 | 「重新评估」会重新测量;普通调用仍然走十分钟缓存 |
|
|
@@ -293,6 +303,11 @@ Client-half slots: `sidebar.panellist` (order 50, disposed in settings-only mode
|
|
|
293
303
|
| `app-temp` | 应用自己放在 `%APPDATA%` 里的临时目录(不走 `%TEMP%`):`WorkDaddy\client-tmp`、`PersonalCenter\Temp` | 保留期 2 天 | 注意 | 应用可能正在运行并占用其中的文件 |
|
|
294
304
|
| `plugin-old-versions` | WPS 插件池 `kingsoft\wps\addons\pool\win-i386` 里,每个插件族除最高版本以外的全部版本 | 全清 | 注意 | 管理器加载最高版本,其余是回滚副本 |
|
|
295
305
|
| `update-packages` | 已应用的更新包:FTNN 的 `.spd` 补丁(build 号低于 `Program Files\FTNN\app\[版本]` 中最新者),以及 `WorkDaddy\update` 里低于已装版本的安装包 | 全清 | 注意 | 按版本号比较判断,不按时间 |
|
|
306
|
+
| `session-backups` | `%USERPROFILE%\.workbuddy\workspace\sessions\*` 下的 `modify_backup` 目录——每个 agent 会话的整文件修改快照 | 全清 | 注意 | 会话本体在快照旁边,不会读回这些文件 |
|
|
307
|
+
| `agent-logs` | `%USERPROFILE%\.workbuddy\logs` 与 `\.workbuddy-ai\logs` 下按日期命名的日志目录 | 保留期 3 天 | 安全 | 旧日志不会被读回 |
|
|
308
|
+
| `toolchain-snapshots` | `%USERPROFILE%\.dsh\dsh-ops\bash-ipc` 下重复的快照:`.ops-ipc.json` 里 `patchedSha256` 相同的目录只留最新一份 | 全清 | 注意 | 没有该元数据的目录永不触碰 |
|
|
309
|
+
| `editor-server-bundles` | `%USERPROFILE%\vscode-remote-wsl\stable` 下除最新以外的服务包 | 全清 | 注意 | 编辑器需要时会重新下载 |
|
|
310
|
+
| `database-backups` | `%USERPROFILE%\Zotero` 下的 `zotero.sqlite*.bak` | 全清 | 注意 | 正在用的 `zotero.sqlite` 就在副本旁边 |
|
|
296
311
|
|
|
297
312
|
保留期方式会跳过保留期内修改过的文件;全清方式清掉全部条目。两者都保留目标目录本身,并把锁定或无权限的条目跳过计数。
|
|
298
313
|
|
|
@@ -367,6 +382,9 @@ Client-half slots: `sidebar.panellist` (order 50, disposed in settings-only mode
|
|
|
367
382
|
- 遍历时遇到符号链接与 junction 一律跳过,不跟随也不删除。
|
|
368
383
|
- 锁定或无权限的条目计为跳过,遍历继续。
|
|
369
384
|
- 保留目标目录本身,只清内容。
|
|
385
|
+
- 「只留最新一份」这类规则一律选保守的一侧:族内只有一个成员、元数据文件缺失或哈希不是字符串、
|
|
386
|
+
`vscode-remote-wsl\stable` 下只有一个服务包,结果都是*没有可删项*。最新那一份永远不会被返回。
|
|
387
|
+
- `database-backups` 只按名字匹配 `.sqlite*.bak`,正在用的 `zotero.sqlite` 永不成为候选。
|
|
370
388
|
- 那个拒绝普通权限写入的 Windows 目录标为 `elevation`,并排除在「现在能回收」之外。
|
|
371
389
|
|
|
372
390
|
### 安装
|
|
@@ -431,6 +449,7 @@ node --check lib/client.js # 以及 lib/index.js、lib/catalog.js、li
|
|
|
431
449
|
| 试运行不删任何东西 | 不传 `confirm`,字节数不变 |
|
|
432
450
|
| 盘过滤拒绝未挂载与畸形卷名 | `C:extra`、`Z:`、`abc` → 400 |
|
|
433
451
|
| 方案不把提权目标的字节算进「现在能回收」 | 在本机跑真实 audit |
|
|
452
|
+
| 「只留最新一份」一律选保守一侧 | 一次性 fixture 目录:同哈希的重复项收敛到最新、无元数据的目录不动、不同哈希保留、正在用的数据库完好(9/9) |
|
|
434
453
|
| 视觉排版 | 在运行中的 Web UI 里手工确认过。本环境没有浏览器自动化,所以这条是人眼检查,不是测试。 |
|
|
435
454
|
|
|
436
455
|
### 实现要点
|
|
@@ -455,11 +474,11 @@ node --check lib/client.js # 以及 lib/index.js、lib/catalog.js、li
|
|
|
455
474
|
```
|
|
456
475
|
├─ package.json dsh.bundle.patch + dsh.client { platform: "web" }
|
|
457
476
|
├─ cordis.patch.yml - insert: id: dsh-disk-cleaner
|
|
458
|
-
├─ lib/index.js (
|
|
459
|
-
├─ lib/catalog.js (
|
|
477
|
+
├─ lib/index.js (411) Host:HTTP 路由 + disk_cleaner 工具,共用同一个 run(),含 reveal
|
|
478
|
+
├─ lib/catalog.js (836) 24 个目标,measurePath / purgePath / resolveTargets / drives
|
|
460
479
|
├─ lib/audit.js (365) 只读评估,10 分钟缓存,带 areaPaths,不暴露任何删除
|
|
461
480
|
├─ lib/plan.js (150) 纯函数:测量结果 → 有序步骤
|
|
462
|
-
├─ lib/client.js (
|
|
481
|
+
├─ lib/client.js (1371) Client 半面:slot、面板、偏好、zh/en 字典、CSS
|
|
463
482
|
├─ locale/{en,zh}.json 展示元数据(meta.title / meta.description)
|
|
464
483
|
├─ icon.svg 插件图标
|
|
465
484
|
├─ README.md 本文件
|
package/lib/catalog.js
CHANGED
|
@@ -274,14 +274,122 @@ const supersededUpdatePackages = () => [
|
|
|
274
274
|
...pooledOldVersions(path.join(env('APPDATA') ?? '', 'WorkDaddy', 'update'), /^(.+)-([0-9][0-9._]*)(\.[A-Za-z0-9]+)?$/),
|
|
275
275
|
];
|
|
276
276
|
|
|
277
|
+
/** The user profile, which roots every target built from a dot-directory in the home folder. */
|
|
278
|
+
const home = () => env('USERPROFILE') ?? '';
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Per-session backup dumps an editor's agent keeps beside a workspace, such as WorkBuddy's
|
|
282
|
+
* `sessions/<id>/modify_backup`. Each file is a whole-file snapshot ten-odd days old; the
|
|
283
|
+
* session's live state is elsewhere in the same folder, so the dumps are pure history.
|
|
284
|
+
*/
|
|
285
|
+
const sessionBackups = () => {
|
|
286
|
+
const sessions = path.join(home(), '.workbuddy', 'workspace', 'sessions');
|
|
287
|
+
const out = [];
|
|
288
|
+
for (const entry of listDir(sessions)) {
|
|
289
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
290
|
+
const backup = path.join(sessions, entry.name, 'modify_backup');
|
|
291
|
+
if (isDir(backup)) out.push(backup);
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/** `2026-11-04` and `20261104` both name one day; anything else is not a log archive. */
|
|
297
|
+
const LOG_DAY = /^(\d{4})-?(\d{2})-?(\d{2})$/;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Day-named log archives an app writes and never prunes. The age limit on the target does
|
|
301
|
+
* the pruning per file, so this only has to name the folders that look like a date.
|
|
302
|
+
*/
|
|
303
|
+
const datedLogDirs = (roots) => {
|
|
304
|
+
const out = [];
|
|
305
|
+
for (const root of roots) {
|
|
306
|
+
for (const entry of listDir(root)) {
|
|
307
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
308
|
+
if (LOG_DAY.exec(entry.name) === null) continue;
|
|
309
|
+
out.push(path.join(root, entry.name));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return out;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Sibling directories that repeat each other, decided by the content hash the owner writes
|
|
317
|
+
* into its own metadata file. A snapshot keeper leaves the previous extraction behind after
|
|
318
|
+
* an upgrade; two of them here were byte-identical. The newest stays, and a sibling with no
|
|
319
|
+
* metadata proves nothing, so it is never touched.
|
|
320
|
+
*/
|
|
321
|
+
const repeatedSiblings = (parent, metaName, key) => {
|
|
322
|
+
if (!isDir(parent)) return [];
|
|
323
|
+
const groups = new Map();
|
|
324
|
+
for (const entry of listDir(parent)) {
|
|
325
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
326
|
+
const abs = path.join(parent, entry.name);
|
|
327
|
+
let stamp;
|
|
328
|
+
try {
|
|
329
|
+
stamp = JSON.parse(fs.readFileSync(path.join(abs, metaName), 'utf8'))[key];
|
|
330
|
+
} catch {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (typeof stamp !== 'string' || stamp === '') continue;
|
|
334
|
+
if (!groups.has(stamp)) groups.set(stamp, []);
|
|
335
|
+
let mtimeMs = 0;
|
|
336
|
+
try {
|
|
337
|
+
mtimeMs = fs.statSync(abs).mtimeMs;
|
|
338
|
+
} catch {
|
|
339
|
+
/* unreadable sorts last, so a broken copy is the one dropped */
|
|
340
|
+
}
|
|
341
|
+
groups.get(stamp).push({ abs, mtimeMs });
|
|
342
|
+
}
|
|
343
|
+
const out = [];
|
|
344
|
+
for (const list of groups.values()) {
|
|
345
|
+
if (list.length < 2) continue;
|
|
346
|
+
list.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
347
|
+
for (const older of list.slice(1)) out.push(older.abs);
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/** Every child directory but the most recently written one; a lone child is left alone. */
|
|
353
|
+
const keepNewestChild = (parent) => {
|
|
354
|
+
if (!isDir(parent)) return [];
|
|
355
|
+
const stamped = listDir(parent)
|
|
356
|
+
.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink())
|
|
357
|
+
.map((entry) => {
|
|
358
|
+
const abs = path.join(parent, entry.name);
|
|
359
|
+
let mtimeMs = 0;
|
|
360
|
+
try {
|
|
361
|
+
mtimeMs = fs.statSync(abs).mtimeMs;
|
|
362
|
+
} catch {
|
|
363
|
+
/* unreadable sorts last */
|
|
364
|
+
}
|
|
365
|
+
return { abs, mtimeMs };
|
|
366
|
+
})
|
|
367
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
368
|
+
if (stamped.length < 2) return [];
|
|
369
|
+
return stamped.slice(1).map((item) => item.abs);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
/** The one-generation database copies Zotero writes beside the library it is using. */
|
|
373
|
+
const zoteroBackups = () => {
|
|
374
|
+
const data = path.join(home(), 'Zotero');
|
|
375
|
+
return listDir(data)
|
|
376
|
+
.filter((entry) => entry.isFile() && !entry.isSymbolicLink())
|
|
377
|
+
.filter((entry) => /\.sqlite[0-9.]*\.bak$/i.test(entry.name))
|
|
378
|
+
.map((entry) => path.join(data, entry.name));
|
|
379
|
+
};
|
|
380
|
+
|
|
277
381
|
/**
|
|
278
382
|
* `purge` targets drop every entry under the path. `aged` targets keep anything modified
|
|
279
383
|
* within `minAgeDays`, because those files may belong to a process that is still running.
|
|
280
384
|
*
|
|
281
385
|
* `note` says why a target carries the `caution` risk, in one stable code the UI
|
|
282
386
|
* translates: 'in-use' (a running program may hold these files), 'rebuild' (the owner
|
|
283
|
-
* regenerates them, so the first use afterwards is slower),
|
|
284
|
-
* refuses an unelevated delete)
|
|
387
|
+
* regenerates them, so the first use afterwards is slower), 'elevation' (Windows
|
|
388
|
+
* refuses an unelevated delete), 'pending-update' (a download that may not be applied
|
|
389
|
+
* yet), 'rollback' (an older version kept beside the live one), 'superseded' (an update
|
|
390
|
+
* package the installed version already contains), 'history' (a snapshot nothing reads
|
|
391
|
+
* back), 'duplicate' (a byte-identical copy of a live directory) or 'database-backup' (a
|
|
392
|
+
* copy of a database file made before an upgrade). Safe targets have none.
|
|
285
393
|
*/
|
|
286
394
|
const CATALOG = [
|
|
287
395
|
{
|
|
@@ -437,6 +545,46 @@ const CATALOG = [
|
|
|
437
545
|
roots: ['APPDATA'],
|
|
438
546
|
paths: () => supersededUpdatePackages(),
|
|
439
547
|
},
|
|
548
|
+
{
|
|
549
|
+
id: 'session-backups',
|
|
550
|
+
kind: 'purge',
|
|
551
|
+
risk: 'caution',
|
|
552
|
+
note: 'history',
|
|
553
|
+
roots: ['USERPROFILE'],
|
|
554
|
+
paths: () => sessionBackups(),
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
id: 'agent-logs',
|
|
558
|
+
kind: 'aged',
|
|
559
|
+
minAgeDays: 3,
|
|
560
|
+
risk: 'safe',
|
|
561
|
+
roots: ['USERPROFILE'],
|
|
562
|
+
paths: () => datedLogDirs([path.join(home(), '.workbuddy', 'logs'), path.join(home(), '.workbuddy-ai', 'logs')]),
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
id: 'toolchain-snapshots',
|
|
566
|
+
kind: 'purge',
|
|
567
|
+
risk: 'caution',
|
|
568
|
+
note: 'duplicate',
|
|
569
|
+
roots: ['USERPROFILE'],
|
|
570
|
+
paths: () => repeatedSiblings(path.join(home(), '.dsh', 'dsh-ops', 'bash-ipc'), '.ops-ipc.json', 'patchedSha256'),
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
id: 'editor-server-bundles',
|
|
574
|
+
kind: 'purge',
|
|
575
|
+
risk: 'caution',
|
|
576
|
+
note: 'superseded',
|
|
577
|
+
roots: ['USERPROFILE'],
|
|
578
|
+
paths: () => keepNewestChild(path.join(home(), 'vscode-remote-wsl', 'stable')),
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
id: 'database-backups',
|
|
582
|
+
kind: 'purge',
|
|
583
|
+
risk: 'caution',
|
|
584
|
+
note: 'database-backup',
|
|
585
|
+
roots: ['USERPROFILE'],
|
|
586
|
+
paths: () => zoteroBackups(),
|
|
587
|
+
},
|
|
440
588
|
];
|
|
441
589
|
|
|
442
590
|
const byId = new Map(CATALOG.map((entry) => [entry.id, entry]));
|
package/lib/client.js
CHANGED
|
@@ -41,6 +41,9 @@ window.__ModuleLoader__.load({
|
|
|
41
41
|
'why_pending-update': '只清 {n} 天前的下载,避免删掉还没装的更新',
|
|
42
42
|
why_rollback: '每个插件/程序只留版本号最高的一份,其余是回滚副本',
|
|
43
43
|
why_superseded: '版本号不高于已装版本的更新包,装着的那份已经生效',
|
|
44
|
+
why_history: '每个会话的整文件备份,删了不影响会话本身',
|
|
45
|
+
why_duplicate: '内容完全相同的重复副本,保留最新那份',
|
|
46
|
+
'why_database-backup': '升级前的数据库副本,正在用的数据库还在旁边',
|
|
44
47
|
pathCount: '{n} 处',
|
|
45
48
|
driveLabel: '清理范围',
|
|
46
49
|
allDrives: '全部磁盘',
|
|
@@ -66,6 +69,11 @@ window.__ModuleLoader__.load({
|
|
|
66
69
|
'target_app-temp': '各应用临时目录',
|
|
67
70
|
'target_plugin-old-versions': '插件旧版本',
|
|
68
71
|
'target_update-packages': '已应用的更新包',
|
|
72
|
+
'target_session-backups': '会话备份快照',
|
|
73
|
+
'target_agent-logs': '按天的日志归档',
|
|
74
|
+
'target_toolchain-snapshots': '重复的工具链快照',
|
|
75
|
+
'target_editor-server-bundles': '多余的编辑器服务包',
|
|
76
|
+
'target_database-backups': '数据库备份副本',
|
|
69
77
|
auditTitle: '空间评估(本插件不会删除)',
|
|
70
78
|
auditHint: '统计缓存之外真正占空间的东西:已装软件、WSL 虚拟磁盘、Windows 组件库,以及你自己的文件。这些一律不由本插件删除,只列出供你判断。',
|
|
71
79
|
auditRun: '开始评估',
|
|
@@ -185,6 +193,9 @@ window.__ModuleLoader__.load({
|
|
|
185
193
|
'why_pending-update': 'only downloads older than {n} day(s), so a pending update survives',
|
|
186
194
|
why_rollback: 'only the highest version of each family stays; the rest are rollback copies',
|
|
187
195
|
why_superseded: 'the update package version is not above what is installed, so it is already applied',
|
|
196
|
+
why_history: 'a whole-file backup per session; removing it does not touch the session',
|
|
197
|
+
why_duplicate: 'an identical copy; the newest one is kept',
|
|
198
|
+
'why_database-backup': 'a copy made before an upgrade; the live database is beside it',
|
|
188
199
|
pathCount: '{n} location(s)',
|
|
189
200
|
driveLabel: 'Limit to',
|
|
190
201
|
allDrives: 'All drives',
|
|
@@ -210,6 +221,11 @@ window.__ModuleLoader__.load({
|
|
|
210
221
|
'target_app-temp': 'App scratch folders',
|
|
211
222
|
'target_plugin-old-versions': 'Old plugin versions',
|
|
212
223
|
'target_update-packages': 'Applied update packages',
|
|
224
|
+
'target_session-backups': 'Session backup snapshots',
|
|
225
|
+
'target_agent-logs': 'Dated log archives',
|
|
226
|
+
'target_toolchain-snapshots': 'Duplicate toolchain snapshots',
|
|
227
|
+
'target_editor-server-bundles': 'Extra editor server bundles',
|
|
228
|
+
'target_database-backups': 'Database backup copies',
|
|
213
229
|
auditTitle: 'Space assessment (this plugin deletes none of it)',
|
|
214
230
|
auditHint: 'What actually takes up room outside caches: installed programs, WSL disk images, the Windows component store, and your own files. None of it is removed here; it is listed so you can decide.',
|
|
215
231
|
auditRun: 'Run assessment',
|
package/lib/index.js
CHANGED
|
@@ -197,6 +197,10 @@ const NOTE_MEANING = {
|
|
|
197
197
|
elevation: 'Windows refuses an unelevated delete here; run the plugin elevated to reach these bytes',
|
|
198
198
|
rollback: 'an older version the app keeps for rollback; what it runs now is the newest of that family',
|
|
199
199
|
superseded: 'an update package whose version is at or below what is already installed',
|
|
200
|
+
'pending-update': 'a downloaded update inside the age limit, so it may not be applied yet',
|
|
201
|
+
history: 'a backup snapshot of file edits that nothing reads back; the live state sits beside it',
|
|
202
|
+
duplicate: 'a byte-identical copy of a directory that is still present',
|
|
203
|
+
'database-backup': 'a copy of a database file taken before an upgrade; the live database is beside it',
|
|
200
204
|
};
|
|
201
205
|
|
|
202
206
|
const formatBytes = (bytes) => {
|
|
@@ -274,7 +278,9 @@ export function apply(ctx) {
|
|
|
274
278
|
description:
|
|
275
279
|
'Inspect and reclaim disk space on this Windows machine from known-safe cache locations only: ' +
|
|
276
280
|
'temp directories, package manager caches (npm, pnpm, pip, uv), crash dumps, browser and editor ' +
|
|
277
|
-
'caches,
|
|
281
|
+
'caches, the Windows internet cache, per-app caches, logs and scratch folders under %APPDATA%, ' +
|
|
282
|
+
'superseded plugin versions and applied update packages, and the backup snapshots, dated logs, ' +
|
|
283
|
+
'duplicate toolchain copies and database backups under the user profile. Call with action "overview" to list drives and target ids, ' +
|
|
278
284
|
'"scan" to measure them, and "clean" to delete. clean without confirm: true is a dry run that only ' +
|
|
279
285
|
'reports what it would free, so the user can approve the amount before anything is removed. ' +
|
|
280
286
|
'Action "audit" instead assesses the space that CANNOT be cleaned automatically — installed programs, ' +
|
|
@@ -358,7 +364,11 @@ export function apply(ctx) {
|
|
|
358
364
|
}
|
|
359
365
|
if (action === 'scan') {
|
|
360
366
|
const measured = await scan(readIds(args.ids), readDrive(args.drive));
|
|
361
|
-
const lines = measured.map((target) =>
|
|
367
|
+
const lines = measured.map((target) => {
|
|
368
|
+
// scan measures everything present; a retention limit means clean frees less.
|
|
369
|
+
const limit = target.minAgeDays > 0 ? ` (only files older than ${target.minAgeDays} day(s) go)` : '';
|
|
370
|
+
return `${target.id}: ${formatBytes(target.bytes)} in ${target.files} file(s)${limit}`;
|
|
371
|
+
});
|
|
362
372
|
const total = measured.reduce((sum, target) => sum + target.bytes, 0);
|
|
363
373
|
const notes = [...new Set(measured.map((target) => target.note).filter(Boolean))];
|
|
364
374
|
const meaning = notes.map((note) => `${note}: ${NOTE_MEANING[note] ?? note}`).join('\n');
|
package/package.json
CHANGED
package/smoke.mjs
CHANGED
|
@@ -262,9 +262,9 @@ check('tool description warns audit is read-only', /read-only: it never removes
|
|
|
262
262
|
check('every effect is labelled', applied.every((label) => typeof label === 'string' && label.startsWith('dsh-disk-cleaner:')));
|
|
263
263
|
|
|
264
264
|
const ids = [...catalog.matchAll(/^\s{4}id: '([a-z-]+)',$/gm)].map((m) => m[1]);
|
|
265
|
-
check('catalog has the documented
|
|
265
|
+
check('catalog has the documented 24 targets', ids.length === 24, `got ${ids.length}`);
|
|
266
266
|
check('every catalog id has a client label', ids.every((id) => zhKeys.includes(`target_${id}`) && enKeys.includes(`target_${id}`)));
|
|
267
|
-
check('every note code has an explanation', ['why_safe', 'why_in-use', 'why_rebuild', 'why_elevation', 'why_pending-update', 'why_rollback', 'why_superseded'].every((k) => zhKeys.includes(k) && enKeys.includes(k)));
|
|
267
|
+
check('every note code has an explanation', ['why_safe', 'why_in-use', 'why_rebuild', 'why_elevation', 'why_pending-update', 'why_rollback', 'why_superseded', 'why_history', 'why_duplicate', 'why_database-backup'].every((k) => zhKeys.includes(k) && enKeys.includes(k)));
|
|
268
268
|
const areaIds = [...auditSrc.matchAll(/^\s{6}id: '([a-z-]+)',$/gm)].map((m) => m[1]);
|
|
269
269
|
check('every assessment area has a translated label and reason', areaIds.length > 0 && areaIds.every((id) => zhKeys.includes(`area_${id}`) && zhKeys.includes(`area_${id}_why`) && enKeys.includes(`area_${id}`) && enKeys.includes(`area_${id}_why`)), `areas: ${areaIds.join(',')}`);
|
|
270
270
|
check('a reveal request names an area, never a path', index.includes("action === 'reveal'") && auditSrc.includes('export function areaPaths') && !/revealPath[\s\S]{0,200}payload\.path/.test(index));
|