dsh-disk-cleaner 1.6.2 → 1.6.4
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 +29 -25
- package/lib/catalog.js +47 -17
- package/lib/client.js +182 -78
- package/lib/index.js +3 -0
- package/package.json +1 -1
- package/smoke.mjs +85 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<!-- badges -->
|
|
6
6
|
| | |
|
|
7
7
|
| --- | --- |
|
|
8
|
-
| **version** | `1.6.
|
|
8
|
+
| **version** | `1.6.4` |
|
|
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** — thirty-two known
|
|
19
|
+
1. **Clean** — thirty-two known 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, and the ones that cannot be rebuilt are never preselected.
|
|
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.
|
|
@@ -29,7 +29,7 @@ Both the panel and the agent reach the *same* operations: the cleaning logic liv
|
|
|
29
29
|
| --- | --- |
|
|
30
30
|
| Scan | Measures all thirty-two 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` / `history` / `duplicate` / `database-backup` / `orphan`) 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` / `orphan` / `irreversible`) 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 |
|
|
@@ -58,7 +58,7 @@ Every path is built from this table — a caller selects an **id**, never a path
|
|
|
58
58
|
| `edge-cache` | same set, Edge profiles | purge | caution | rebuilt; next open is slower |
|
|
59
59
|
| `code-cache` | `%APPDATA%\Code\{Cache,CachedData,CachedExtensionVSIXs}` | purge | caution | rebuilt; next start is slower |
|
|
60
60
|
| `windows-update` | `C:\Windows\SoftwareDistribution\Download` | purge | caution | needs elevation |
|
|
61
|
-
| `recycle-bin` | `$Recycle.Bin\S-1-5-*` on every mounted volume | purge |
|
|
61
|
+
| `recycle-bin` | `$Recycle.Bin\S-1-5-*` on every mounted volume | purge | caution | emptying it cannot be undone; never preselected |
|
|
62
62
|
| `inet-cache` | `%LOCALAPPDATA%\Microsoft\Windows\INetCache` | purge | caution | rebuilt |
|
|
63
63
|
| `app-updaters` | `*-updater` folders under `%LOCALAPPDATA%` and `%APPDATA%` — the installers app updaters leave behind | aged, 3 days | caution | a download from the last three days may still be a pending update |
|
|
64
64
|
| `app-caches` | named cache folders inside `%APPDATA%`: the WPS CEF cache, FTNN `cef_cache`, and the `Cache` / `Code Cache` / `CachedData` / `GPUCache` of Qoder, Trae, the DSH desktop app, PersonalCenter, the WeChat devtools and QQ PC Manager | purge | caution | CEF and Chromium caches rebuild on next start |
|
|
@@ -166,16 +166,16 @@ These are enforced in `lib/catalog.js` and must not be relaxed:
|
|
|
166
166
|
|
|
167
167
|
A catalog entry has been submitted to the curated [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) list, which powers **dshmarket → Discover** ([PR #12970](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/12970), pending review) — once it merges, search **dsh-disk-cleaner** there and install it in one click. Until then, use a direct install below.
|
|
168
168
|
|
|
169
|
-
Published on npm as [`dsh-disk-cleaner`](https://www.npmjs.com/package/dsh-disk-cleaner) — latest `1.6.
|
|
169
|
+
Published on npm as [`dsh-disk-cleaner`](https://www.npmjs.com/package/dsh-disk-cleaner) — latest `1.6.4`:
|
|
170
170
|
|
|
171
171
|
```bash
|
|
172
172
|
npm i dsh-disk-cleaner
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
A prebuilt tarball is attached to the [v1.6.
|
|
175
|
+
A prebuilt tarball is attached to the [v1.6.3 Release](https://github.com/stallenlu/dsh-disk-cleaner/releases/tag/v1.6.4); installing from it skips DSH's build-approval step:
|
|
176
176
|
|
|
177
177
|
```bash
|
|
178
|
-
dsh plugin --profile <profile> add https://github.com/stallenlu/dsh-disk-cleaner/releases/download/v1.6.
|
|
178
|
+
dsh plugin --profile <profile> add https://github.com/stallenlu/dsh-disk-cleaner/releases/download/v1.6.4/dsh-disk-cleaner-1.6.4.tgz
|
|
179
179
|
```
|
|
180
180
|
|
|
181
181
|
Then enable it in the profile — in `$DSH_PROFILE_DIR/package.json`:
|
|
@@ -183,7 +183,7 @@ Then enable it in the profile — in `$DSH_PROFILE_DIR/package.json`:
|
|
|
183
183
|
```json
|
|
184
184
|
{
|
|
185
185
|
"dependencies": {
|
|
186
|
-
"dsh-disk-cleaner": "^1.6.
|
|
186
|
+
"dsh-disk-cleaner": "^1.6.4"
|
|
187
187
|
},
|
|
188
188
|
"dsh": {
|
|
189
189
|
"profile": {
|
|
@@ -204,7 +204,7 @@ The bundle's own `cordis.patch.yml` inserts the plugin row; no profile patch edi
|
|
|
204
204
|
### Development
|
|
205
205
|
|
|
206
206
|
```bash
|
|
207
|
-
node smoke.mjs #
|
|
207
|
+
node smoke.mjs # 98 assertions, zero dependencies
|
|
208
208
|
node --check lib/client.js # and lib/index.js, lib/catalog.js, lib/audit.js, lib/plan.js
|
|
209
209
|
```
|
|
210
210
|
|
|
@@ -258,22 +258,24 @@ What the smoke test deliberately does **not** cover: anything that walks a real
|
|
|
258
258
|
```
|
|
259
259
|
├─ package.json dsh.bundle.patch + dsh.client { platform: "web" }
|
|
260
260
|
├─ cordis.patch.yml - insert: id: dsh-disk-cleaner
|
|
261
|
-
├─ lib/index.js (
|
|
262
|
-
├─ lib/catalog.js (
|
|
261
|
+
├─ lib/index.js (434) Host: HTTP route + disk_cleaner tool, same run() for both; reveal
|
|
262
|
+
├─ lib/catalog.js (1222) The 32 targets, measurePath / purgePath / resolveTargets / drives
|
|
263
263
|
├─ lib/audit.js (365) Read-only assessment, 10-minute cache, areaPaths, exposes no deletion
|
|
264
264
|
├─ lib/plan.js (150) Pure: measurements → ordered steps
|
|
265
|
-
├─ lib/client.js (
|
|
265
|
+
├─ lib/client.js (1597) Client half: slots, panel, preferences, zh/en dictionaries, CSS
|
|
266
266
|
├─ locale/{en,zh}.json Display metadata (meta.title / meta.description)
|
|
267
267
|
├─ icon.svg Plugin icon
|
|
268
268
|
├─ README.md This file
|
|
269
269
|
├─ LICENSE MIT
|
|
270
|
-
└─ smoke.mjs
|
|
270
|
+
└─ smoke.mjs 98 assertions, zero dependencies
|
|
271
271
|
```
|
|
272
272
|
|
|
273
273
|
Client-half slots: `sidebar.panellist` (order 50, disposed in settings-only mode), `main` (keyed by the package name), `settings.section` (order 30, disposed in sidebar-only mode), `plugins.bundle.config` (keyed by the package name).
|
|
274
274
|
|
|
275
275
|
### What's new
|
|
276
276
|
|
|
277
|
+
- **1.6.4** — The panel now paints itself with [dsh-design-baseline](https://github.com/stallenlu/dsh-design-baseline): the stylesheet is inlined into the client bundle, the panel root carries `dsb-root`, and every colour resolves through a `--dsb-*` token instead of a host `--dsw-alias-*` name. That means it follows the host's light and dark themes (including `color-scheme`, so native `<select>` menus and scrollbars match), and colour is spent only on data that has state — the primary button and the active chip are now inverted neutrals rather than brand blue. No behaviour changes.
|
|
278
|
+
- **1.6.3** — Safety and honesty rather than new features. The Recycle Bin is no longer filed under `safe`: emptying it cannot be undone, so it is never preselected, never picked by "select all", and carries its own `irreversible` note instead of the generic "rebuilt; next start is slower". The confirm dialog now names each target and the total instead of asking a generic "delete these?". A target that names no environment root is bounded by the volume it sits on rather than skipping the boundary check. `measure` and `purge` now count entries in the same unit, and a walk that hits the 2,000,000-entry cap says so (`truncated`) instead of claiming exact totals. A directory removal refused for a reason other than "not empty" is counted as `blocked` instead of being swallowed, and a clean that left files behind reports a warning rather than a green success. The orphan target is named for what it actually measures (the QQGuild folder).
|
|
277
279
|
- **1.6.2** — Documentation only: the README, the in-source comment, the panel's zh/en explanations and the tool's note legend now describe `orphan-app-data` as it actually behaves (the QQGuild folder is offered for full deletion once no file in it has been written for 30 days), instead of claiming four independent signals and a two-year idle period. No behaviour change; the refreshed README ships in the npm package and the [v1.6.2 Release](https://github.com/stallenlu/dsh-disk-cleaner/releases/tag/v1.6.2) tarball.
|
|
278
280
|
- **1.6.1** — Fixed the `disk_cleaner` agent tool: `output.schema` now declares `type: 'string'`, matching the text `execute()` returns (previously every call was rejected by the host's output validator with `tool "disk_cleaner" returned invalid output: "value" must be an object`). A real delete through the agent tool now invalidates the assessment cache, the same way the panel's HTTP route does. Two regression assertions added; a prebuilt tarball ships on the [v1.6.1 Release](https://github.com/stallenlu/dsh-disk-cleaner/releases/tag/v1.6.1).
|
|
279
281
|
- **1.6.0** — The 32 targets are grouped under seven foldable headings.
|
|
@@ -285,7 +287,7 @@ Client-half slots: `sidebar.panellist` (order 50, disposed in settings-only mode
|
|
|
285
287
|
|
|
286
288
|
**dsh-disk-cleaner** 是 [DeepSeek Harness](https://github.com/deepseek-ai)(DSH)的磁盘空间插件。它做两件不同的事,并且刻意分开:
|
|
287
289
|
|
|
288
|
-
1. **清理** ——
|
|
290
|
+
1. **清理** —— 三十二个已知位置(临时目录、包管理器存储、浏览器缓存、崩溃转储、回收站、更新安装包残留、各应用的缓存与日志、被淘汰的插件版本与已应用的更新包)。只有这些会被删除,其中无法重建的项目永远不会被预选。
|
|
289
291
|
2. **评估** —— 所有本插件**不会**删的东西(已装程序、WSL 虚拟磁盘、Windows 组件库、聊天软件数据、你自己的文件夹)。只测量、只列出,绝不删除。
|
|
290
292
|
|
|
291
293
|
评估的最后是一份**处理方案**:同一批数字按「你要付出多少代价」排序,本插件做不到的步骤附上确切的 Windows 命令。
|
|
@@ -298,7 +300,7 @@ Client-half slots: `sidebar.panellist` (order 50, disposed in settings-only mode
|
|
|
298
300
|
| --- | --- |
|
|
299
301
|
| 扫描 | 并发测量全部三十二个目标,显示各自占用 |
|
|
300
302
|
| 按盘过滤 | 已挂载卷的胶囊按钮;扫描与清理都可限定单盘,该盘上为空的项会隐藏 |
|
|
301
|
-
| 风险标注 | 每个目标都标 `安全` 或 `注意`,并给一行理由(`in-use` / `rebuild` / `elevation` / `pending-update` / `rollback` / `superseded` / `history` / `duplicate` / `database-backup` / `orphan`),图例常驻,所以「注意」永远不是「说不清」 |
|
|
303
|
+
| 风险标注 | 每个目标都标 `安全` 或 `注意`,并给一行理由(`in-use` / `rebuild` / `elevation` / `pending-update` / `rollback` / `superseded` / `history` / `duplicate` / `database-backup` / `orphan` / `irreversible`),图例常驻,所以「注意」永远不是「说不清」 |
|
|
302
304
|
| 保留期删除 | `temp` 与 `windows-temp` 只删一天前的文件;被运行中程序占住的自动跳过 |
|
|
303
305
|
| 空间评估 | 只读遍历磁盘,按 Windows 自身 / 已装软件 / 你自己的文件 分组,另列占用最大的十个应用数据目录。每一行都能打开所在目录——单击、右键或回车 |
|
|
304
306
|
| 重新评估真的重测 | 「重新评估」会重新测量;普通调用仍然走十分钟缓存 |
|
|
@@ -327,7 +329,7 @@ Client-half slots: `sidebar.panellist` (order 50, disposed in settings-only mode
|
|
|
327
329
|
| `edge-cache` | 同上,Edge 的配置目录 | 全清 | 注意 | 会重建,下次打开更慢 |
|
|
328
330
|
| `code-cache` | `%APPDATA%\Code\{Cache,CachedData,CachedExtensionVSIXs}` | 全清 | 注意 | 会重建,下次启动更慢 |
|
|
329
331
|
| `windows-update` | `C:\Windows\SoftwareDistribution\Download` | 全清 | 注意 | 需要管理员权限 |
|
|
330
|
-
| `recycle-bin` | 各已挂载卷的 `$Recycle.Bin\S-1-5-*` | 全清 |
|
|
332
|
+
| `recycle-bin` | 各已挂载卷的 `$Recycle.Bin\S-1-5-*` | 全清 | 注意 | 清空不可恢复,永不被预选 |
|
|
331
333
|
| `inet-cache` | `%LOCALAPPDATA%\Microsoft\Windows\INetCache` | 全清 | 注意 | 会重建 |
|
|
332
334
|
| `app-updaters` | `%LOCALAPPDATA%` 与 `%APPDATA%` 下的 `*-updater` 目录——升级程序留下的安装包 | 保留期 3 天 | 注意 | 三天内的下载可能还是待安装的更新 |
|
|
333
335
|
| `app-caches` | `%APPDATA%` 里点名列出的缓存目录:WPS 的 CEF 缓存、FTNN 的 `cef_cache`,以及 Qoder、Trae、DSH 桌面端、PersonalCenter、微信开发者工具、腾讯电脑管家的 `Cache` / `Code Cache` / `CachedData` / `GPUCache` | 全清 | 注意 | CEF 与 Chromium 缓存会在下次启动时重建 |
|
|
@@ -434,16 +436,16 @@ Client-half slots: `sidebar.panellist` (order 50, disposed in settings-only mode
|
|
|
434
436
|
|
|
435
437
|
已向精选目录 [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) 提交收录,它是 **dshmarket → 发现** 的数据源([PR #12970](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/12970),审核中)——合并后即可在 dshmarket 里搜索 **dsh-disk-cleaner** 一键安装。在那之前,用下面任一种直接安装方式。
|
|
436
438
|
|
|
437
|
-
已发布到 npm:[`dsh-disk-cleaner`](https://www.npmjs.com/package/dsh-disk-cleaner),最新版 `1.6.
|
|
439
|
+
已发布到 npm:[`dsh-disk-cleaner`](https://www.npmjs.com/package/dsh-disk-cleaner),最新版 `1.6.4`。
|
|
438
440
|
|
|
439
441
|
```bash
|
|
440
442
|
npm i dsh-disk-cleaner
|
|
441
443
|
```
|
|
442
444
|
|
|
443
|
-
[v1.6.
|
|
445
|
+
[v1.6.3 Release](https://github.com/stallenlu/dsh-disk-cleaner/releases/tag/v1.6.4) 还附带一个预构建 tarball,从它安装可跳过 DSH 的构建授权步骤:
|
|
444
446
|
|
|
445
447
|
```bash
|
|
446
|
-
dsh plugin --profile <profile> add https://github.com/stallenlu/dsh-disk-cleaner/releases/download/v1.6.
|
|
448
|
+
dsh plugin --profile <profile> add https://github.com/stallenlu/dsh-disk-cleaner/releases/download/v1.6.4/dsh-disk-cleaner-1.6.4.tgz
|
|
447
449
|
```
|
|
448
450
|
|
|
449
451
|
然后在 profile 里启用 —— 在 `$DSH_PROFILE_DIR/package.json`:
|
|
@@ -451,7 +453,7 @@ dsh plugin --profile <profile> add https://github.com/stallenlu/dsh-disk-cleaner
|
|
|
451
453
|
```json
|
|
452
454
|
{
|
|
453
455
|
"dependencies": {
|
|
454
|
-
"dsh-disk-cleaner": "^1.6.
|
|
456
|
+
"dsh-disk-cleaner": "^1.6.4"
|
|
455
457
|
},
|
|
456
458
|
"dsh": {
|
|
457
459
|
"profile": {
|
|
@@ -472,7 +474,7 @@ bundle 自带的 `cordis.patch.yml` 会插入插件行,无需改 profile 的 p
|
|
|
472
474
|
### 开发
|
|
473
475
|
|
|
474
476
|
```bash
|
|
475
|
-
node smoke.mjs #
|
|
477
|
+
node smoke.mjs # 98 条断言,零依赖
|
|
476
478
|
node --check lib/client.js # 以及 lib/index.js、lib/catalog.js、lib/audit.js、lib/plan.js
|
|
477
479
|
```
|
|
478
480
|
|
|
@@ -526,22 +528,24 @@ node --check lib/client.js # 以及 lib/index.js、lib/catalog.js、li
|
|
|
526
528
|
```
|
|
527
529
|
├─ package.json dsh.bundle.patch + dsh.client { platform: "web" }
|
|
528
530
|
├─ cordis.patch.yml - insert: id: dsh-disk-cleaner
|
|
529
|
-
├─ lib/index.js (
|
|
530
|
-
├─ lib/catalog.js (
|
|
531
|
+
├─ lib/index.js (434) Host:HTTP 路由 + disk_cleaner 工具,共用同一个 run(),含 reveal
|
|
532
|
+
├─ lib/catalog.js (1222) 32 个目标,measurePath / purgePath / resolveTargets / drives
|
|
531
533
|
├─ lib/audit.js (365) 只读评估,10 分钟缓存,带 areaPaths,不暴露任何删除
|
|
532
534
|
├─ lib/plan.js (150) 纯函数:测量结果 → 有序步骤
|
|
533
|
-
├─ lib/client.js (
|
|
535
|
+
├─ lib/client.js (1597) Client 半面:slot、面板、偏好、zh/en 字典、CSS
|
|
534
536
|
├─ locale/{en,zh}.json 展示元数据(meta.title / meta.description)
|
|
535
537
|
├─ icon.svg 插件图标
|
|
536
538
|
├─ README.md 本文件
|
|
537
539
|
├─ LICENSE MIT
|
|
538
|
-
└─ smoke.mjs
|
|
540
|
+
└─ smoke.mjs 98 条断言,零依赖
|
|
539
541
|
```
|
|
540
542
|
|
|
541
543
|
Client 半面注册的 slot:`sidebar.panellist`(order 50,仅设置页模式下拆除)、`main`(以包名为 key)、`settings.section`(order 30,仅侧边栏模式下拆除)、`plugins.bundle.config`(以包名为 key)。
|
|
542
544
|
|
|
543
545
|
### 更新记录
|
|
544
546
|
|
|
547
|
+
- **1.6.4** — 面板改用 [dsh-design-baseline](https://github.com/stallenlu/dsh-design-baseline) 上色:样式表内联进 client bundle,根节点带 `dsb-root`,所有颜色都走 `--dsb-*` 令牌而不是宿主的 `--dsw-alias-*` 名字。因此它跟随宿主的明暗主题(含 `color-scheme`,原生 `<select>` 弹层与滚动条也跟着变),并且颜色只花在有状态的数据上——主按钮与选中项改成中性反色,不再是品牌蓝。行为没有变化。
|
|
548
|
+
- **1.6.3** — 改的是安全与诚实,不是新功能。回收站不再归入「安全」:清空不可恢复,所以它永不被预选、不被「全选」勾中,并单独带 `irreversible` 注意码,而不是笼统的「重建,下次启动慢一些」。确认框现在逐条列出目标名与合计字节,不再只问一句「删除选中缓存目录里的文件?」。没有声明根目录的目标,改为按所在卷做边界校验,而不是直接跳过检查。`measure` 与 `purge` 现在用同一单位计数,走查触到 200 万条上限时会如实标 `truncated`,不再声称精确总量。目录删除失败若原因不是「非空」会计入 `blocked`(此前被静默吞掉),清理后仍有残留的目标会报警告而不是绿色成功。orphan 目标按它实际测量的东西重新命名(QQ 频道目录)。
|
|
545
549
|
- **1.6.2** — 仅文档修正:README、源码注释、面板中英文说明和工具里的注意码释义,现在都按真实行为描述 `orphan-app-data`(QQGuild 目录内 30 天没有文件写入时会被整目录列为可全清),不再声称「四条独立证据 / 两年未写」。行为不变;更新后的 README 也随 npm 包与 [v1.6.2 Release](https://github.com/stallenlu/dsh-disk-cleaner/releases/tag/v1.6.2) 的 tarball 一起发布。
|
|
546
550
|
- **1.6.1** — 修复了 `disk_cleaner` agent 工具:`output.schema` 现在声明 `type: 'string'`,与 `execute()` 返回的文本一致(此前每次调用都被宿主的输出校验器拒绝,报 `tool "disk_cleaner" returned invalid output: "value" must be an object`)。经由 agent 工具真删后也会让评估缓存失效,与面板的 HTTP 路径一致。新增两条回归断言;[v1.6.1 Release](https://github.com/stallenlu/dsh-disk-cleaner/releases/tag/v1.6.1) 附带预构建 tarball。
|
|
547
551
|
- **1.6.0** — 32 个目标按七个可折叠分组归类。
|
package/lib/catalog.js
CHANGED
|
@@ -642,7 +642,7 @@ const CATALOG = [
|
|
|
642
642
|
// service still holds, which the age limit already handles. Calling it `elevation` sent
|
|
643
643
|
// the user to an elevated prompt that changed nothing and hid the bytes from the plan.
|
|
644
644
|
note: 'in-use',
|
|
645
|
-
roots: [],
|
|
645
|
+
roots: ['SystemRoot'],
|
|
646
646
|
paths: () => ['C:\\Windows\\Temp'],
|
|
647
647
|
},
|
|
648
648
|
{
|
|
@@ -716,13 +716,16 @@ const CATALOG = [
|
|
|
716
716
|
kind: 'purge',
|
|
717
717
|
risk: 'caution',
|
|
718
718
|
note: 'elevation',
|
|
719
|
-
roots: [],
|
|
719
|
+
roots: ['SystemRoot'],
|
|
720
720
|
paths: () => ['C:\\Windows\\SoftwareDistribution\\Download'],
|
|
721
721
|
},
|
|
722
722
|
{
|
|
723
723
|
id: 'recycle-bin',
|
|
724
724
|
kind: 'purge',
|
|
725
|
-
|
|
725
|
+
// Everything in here was already deleted once; emptying it is not a cache rebuild, so it
|
|
726
|
+
// is never preselected and never counted as a safe win.
|
|
727
|
+
risk: 'caution',
|
|
728
|
+
note: 'irreversible',
|
|
726
729
|
roots: [],
|
|
727
730
|
paths: () => recycleBins(),
|
|
728
731
|
},
|
|
@@ -965,8 +968,11 @@ export function resolveTargets(only, drive) {
|
|
|
965
968
|
if (tooBroad(candidate)) continue;
|
|
966
969
|
if (!isDir(candidate) && !isFile(candidate)) continue;
|
|
967
970
|
if (!onDrive(candidate, drive)) continue;
|
|
968
|
-
|
|
969
|
-
|
|
971
|
+
// A target that names no root is still bounded by the volume it sits on. An empty list
|
|
972
|
+
// used to skip this check outright, so any path that passed the earlier tests was taken.
|
|
973
|
+
const declared = entry.roots.map((name) => env(name)).filter(Boolean);
|
|
974
|
+
const roots = declared.length > 0 ? declared : [path.parse(path.resolve(candidate)).root];
|
|
975
|
+
if (!roots.some((root) => within(candidate, root))) continue;
|
|
970
976
|
if (!found.includes(candidate)) found.push(candidate);
|
|
971
977
|
}
|
|
972
978
|
// The volume each target sits on, so a caller can group or filter by drive.
|
|
@@ -996,10 +1002,10 @@ export async function measurePath(abs, cutoff) {
|
|
|
996
1002
|
const stat = statReal(abs);
|
|
997
1003
|
const eligible = cutoff === undefined || stat.mtimeMs <= cutoff;
|
|
998
1004
|
return eligible
|
|
999
|
-
? { bytes: stat.size, files: 1, totalBytes: stat.size, totalFiles: 1 }
|
|
1000
|
-
: { bytes: 0, files: 0, totalBytes: stat.size, totalFiles: 1 };
|
|
1005
|
+
? { bytes: stat.size, files: 1, totalBytes: stat.size, totalFiles: 1, truncated: false }
|
|
1006
|
+
: { bytes: 0, files: 0, totalBytes: stat.size, totalFiles: 1, truncated: false };
|
|
1001
1007
|
} catch {
|
|
1002
|
-
return { bytes: 0, files: 0, totalBytes: 0, totalFiles: 0 };
|
|
1008
|
+
return { bytes: 0, files: 0, totalBytes: 0, totalFiles: 0, truncated: false };
|
|
1003
1009
|
}
|
|
1004
1010
|
}
|
|
1005
1011
|
let bytes = 0;
|
|
@@ -1040,7 +1046,9 @@ export async function measurePath(abs, cutoff) {
|
|
|
1040
1046
|
/* an unreadable directory ends this branch */
|
|
1041
1047
|
}
|
|
1042
1048
|
}
|
|
1043
|
-
|
|
1049
|
+
// A tree larger than the cap was only partly measured, which the caller must say out loud:
|
|
1050
|
+
// the dry run no longer equals what a real run frees.
|
|
1051
|
+
return { bytes, files, totalBytes, totalFiles, truncated: stack.length > 0 };
|
|
1044
1052
|
}
|
|
1045
1053
|
|
|
1046
1054
|
/**
|
|
@@ -1058,14 +1066,16 @@ export async function scan(only, drive) {
|
|
|
1058
1066
|
let files = 0;
|
|
1059
1067
|
let totalBytes = 0;
|
|
1060
1068
|
let totalFiles = 0;
|
|
1069
|
+
let truncated = false;
|
|
1061
1070
|
for (const abs of target.paths) {
|
|
1062
1071
|
const size = await measurePath(abs, cutoff);
|
|
1063
1072
|
bytes += size.bytes;
|
|
1064
1073
|
files += size.files;
|
|
1065
1074
|
totalBytes += size.totalBytes;
|
|
1066
1075
|
totalFiles += size.totalFiles;
|
|
1076
|
+
truncated = truncated || size.truncated === true;
|
|
1067
1077
|
}
|
|
1068
|
-
return { ...target, bytes, files, totalBytes, totalFiles };
|
|
1078
|
+
return { ...target, bytes, files, totalBytes, totalFiles, truncated };
|
|
1069
1079
|
}),
|
|
1070
1080
|
);
|
|
1071
1081
|
return measured;
|
|
@@ -1077,7 +1087,10 @@ async function purgePath(abs, cutoff) {
|
|
|
1077
1087
|
let failed = 0;
|
|
1078
1088
|
const dirs = [];
|
|
1079
1089
|
const stack = [abs];
|
|
1080
|
-
|
|
1090
|
+
// The cap is counted in entries visited, the same unit measurePath uses; counting directories
|
|
1091
|
+
// here meant the two walks stopped at wildly different depths.
|
|
1092
|
+
let seen = 0;
|
|
1093
|
+
while (stack.length > 0 && seen < MAX_ENTRIES) {
|
|
1081
1094
|
const dir = stack.pop();
|
|
1082
1095
|
dirs.push(dir);
|
|
1083
1096
|
let handle;
|
|
@@ -1089,6 +1102,7 @@ async function purgePath(abs, cutoff) {
|
|
|
1089
1102
|
}
|
|
1090
1103
|
try {
|
|
1091
1104
|
for await (const entry of handle) {
|
|
1105
|
+
seen += 1;
|
|
1092
1106
|
const full = path.join(dir, entry.name);
|
|
1093
1107
|
if (entry.isSymbolicLink()) continue;
|
|
1094
1108
|
if (entry.isDirectory()) {
|
|
@@ -1111,14 +1125,19 @@ async function purgePath(abs, cutoff) {
|
|
|
1111
1125
|
}
|
|
1112
1126
|
}
|
|
1113
1127
|
// Deepest first; index 0 is the target itself, which stays as an empty directory.
|
|
1128
|
+
let blocked = 0;
|
|
1114
1129
|
for (let i = dirs.length - 1; i > 0; i -= 1) {
|
|
1115
1130
|
try {
|
|
1116
1131
|
await fsp.rmdir(dirs[i]);
|
|
1117
|
-
} catch {
|
|
1118
|
-
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
// A folder that still holds a file past the age limit is expected and costs nothing.
|
|
1134
|
+
// A folder Windows refuses to remove is not: its bytes stay on disk, so they are counted.
|
|
1135
|
+
const code = error && error.code;
|
|
1136
|
+
if (code === 'ENOTEMPTY' || code === 'EEXIST') continue;
|
|
1137
|
+
blocked += 1;
|
|
1119
1138
|
}
|
|
1120
1139
|
}
|
|
1121
|
-
return { bytes, removed, failed };
|
|
1140
|
+
return { bytes, removed, failed, blocked, truncated: stack.length > 0 };
|
|
1122
1141
|
}
|
|
1123
1142
|
|
|
1124
1143
|
/** Delete the contents of the selected targets, or with `dryRun` only report what it would delete. */
|
|
@@ -1131,17 +1150,27 @@ export async function clean(ids, options = {}) {
|
|
|
1131
1150
|
if (dryRun) {
|
|
1132
1151
|
let bytes = 0;
|
|
1133
1152
|
let files = 0;
|
|
1153
|
+
let failed = 0;
|
|
1154
|
+
let truncated = false;
|
|
1134
1155
|
for (const abs of target.paths) {
|
|
1156
|
+
// The real run refuses a path this broad, so the estimate has to refuse it too, or it
|
|
1157
|
+
// promises bytes the deletion will never free.
|
|
1158
|
+
if (tooBroad(abs)) {
|
|
1159
|
+
failed += 1;
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1135
1162
|
const size = await measurePath(abs, cutoff);
|
|
1136
1163
|
bytes += size.bytes;
|
|
1137
1164
|
files += size.files;
|
|
1165
|
+
truncated = truncated || size.truncated === true;
|
|
1138
1166
|
}
|
|
1139
|
-
results.push({ id: target.id, bytes, removed: files, failed
|
|
1167
|
+
results.push({ id: target.id, bytes, removed: files, failed, truncated });
|
|
1140
1168
|
continue;
|
|
1141
1169
|
}
|
|
1142
1170
|
let bytes = 0;
|
|
1143
1171
|
let removed = 0;
|
|
1144
1172
|
let failed = 0;
|
|
1173
|
+
let truncated = false;
|
|
1145
1174
|
for (const abs of target.paths) {
|
|
1146
1175
|
if (tooBroad(abs)) {
|
|
1147
1176
|
failed += 1;
|
|
@@ -1162,9 +1191,10 @@ export async function clean(ids, options = {}) {
|
|
|
1162
1191
|
const outcome = await purgePath(abs, cutoff);
|
|
1163
1192
|
bytes += outcome.bytes;
|
|
1164
1193
|
removed += outcome.removed;
|
|
1165
|
-
failed += outcome.failed;
|
|
1194
|
+
failed += outcome.failed + (outcome.blocked ?? 0);
|
|
1195
|
+
truncated = truncated || outcome.truncated === true;
|
|
1166
1196
|
}
|
|
1167
|
-
results.push({ id: target.id, bytes, removed, failed });
|
|
1197
|
+
results.push({ id: target.id, bytes, removed, failed, truncated });
|
|
1168
1198
|
}
|
|
1169
1199
|
return results;
|
|
1170
1200
|
}
|
package/lib/client.js
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
window.__ModuleLoader__.load({
|
|
2
2
|
id: 'dsh-disk-cleaner',
|
|
3
3
|
factory(require) {
|
|
4
|
+
// >>> dsh-design-baseline v1.1.0 sha256 76858a2ccf04445daf6a05a19bd8903ff09a8b34789dde945e4ac0cbd47a0a7a
|
|
5
|
+
// Generated by dsh-design-baseline/sync.mjs — edit baseline.css, run build.mjs,
|
|
6
|
+
// then sync.mjs. Everything between these two markers is overwritten.
|
|
7
|
+
const BASELINE_CSS = "/* ============================================================================\n DSH plugin design baseline · dsh-design-baseline v1\n ----------------------------------------------------------------------------\n Derived from the FreeLLMAPI router dashboard (:3001) theme, which is the\n look approved as the house style for every DSH plugin I write.\n\n The whole point of this file is one rule:\n\n Neutral surfaces come from the DSH host. Colour comes from nowhere else\n except the six accents below.\n\n The dashboard's own palette has chroma 0 for every neutral — pure grey\n (oklch(...% 0 0)). That is why it looks calm: no tint is competing. The only\n saturated pixels on the entire page are the three score bars and the state\n dots. Reproduce that and you get the same calm for free.\n\n Host tokens are referenced by name (--dsw-alias-*) so light/dark switching,\n including DSH's own theme overrides, is automatic. Every reference carries a\n literal fallback so a panel still renders if a token is ever renamed.\n ========================================================================= */\n\n/* IMPORTANT — why this is body/.dsb-root and not :root.\n The host declares its dark overrides on `body[data-ds-dark-theme]`, not on\n :root. A var(--dsw-alias-x, fallback) resolved at :root therefore never sees\n the dark values and silently sticks to the light fallback (this bit me: the\n whole panel rendered in light colours while the app was dark). Resolving the\n mapping on body — or on your plugin's own root element, which is the tidier\n option — puts it on the same element the host's dark block targets, so the\n cascade picks the right set in both themes. */\nbody,\n.dsb-root {\n /* --- accents: the ONLY saturated colours in the system ------------------ */\n --dsb-accent-stability: #22c55e; /* emerald-500 — reliability */\n --dsb-accent-speed: #3b82f6; /* blue-500 — throughput */\n --dsb-accent-intelligence: #a855f7; /* violet-500 — capability */\n --dsb-accent-warn: #f59e0b; /* amber-500 — rate limited */\n --dsb-accent-error: #f43f5e; /* rose-500 — invalid/error*/\n --dsb-accent-info: #06b6d4; /* cyan-500 — vision/info */\n\n /* Soft washes for badges and chips: 15% of the accent, per the dashboard. */\n --dsb-wash-stability: color-mix(in srgb, var(--dsb-accent-stability) 15%, transparent);\n --dsb-wash-warn: color-mix(in srgb, var(--dsb-accent-warn) 15%, transparent);\n --dsb-wash-error: color-mix(in srgb, var(--dsb-accent-error) 15%, transparent);\n --dsb-wash-info: color-mix(in srgb, var(--dsb-accent-info) 15%, transparent);\n --dsb-wash-brand: color-mix(in srgb, var(--dsb-accent-intelligence) 15%, transparent);\n\n /* --- neutrals: mapped from the host, never hardcoded ------------------- */\n --dsb-surface: var(--dsw-alias-bg-layer-1, #ffffff);\n --dsb-surface-2: var(--dsw-alias-bg-layer-2, #fafafa);\n --dsb-surface-3: var(--dsw-alias-bg-layer-3, #f5f5f5);\n --dsb-card: var(--dsw-alias-bg-module-platform, #f9fafb);\n --dsb-track: var(--dsw-alias-bg-layer-2, #f5f5f5);\n\n --dsb-line: var(--dsw-alias-border-l1, #0000000a);\n --dsb-line-strong: var(--dsw-alias-border-l2, #0000001a);\n\n --dsb-text: var(--dsw-alias-label-primary, #0f1115);\n --dsb-text-secondary: var(--dsw-alias-label-secondary, #61666b);\n --dsb-text-tertiary: var(--dsw-alias-label-tertiary, #81858c);\n --dsb-text-caption: var(--dsw-alias-label-caption, #adb2b8);\n\n --dsb-inverted-bg: var(--dsw-alias-label-primary, #0f1115);\n --dsb-inverted-fg: var(--dsw-alias-label-primary-foreground, #ffffff);\n --dsb-hover: var(--dsw-alias-interactive-bg-hover, #2631480f);\n --dsb-hover-strong: var(--dsw-alias-interactive-bg-hover-accent, #26314824);\n\n /* --- geometry ---------------------------------------------------------- */\n --dsb-radius: 12px; /* cards, grouped chips, tables */\n --dsb-radius-sm: 8px; /* buttons inside those, tabs */\n --dsb-radius-pill: 9999px; /* bars, chips, badges, dots */\n --dsb-gap: 12px;\n\n --dsb-font: var(--dsw-font-family, \"Geist Variable\", -apple-system,\n \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif);\n --dsb-font-mono: var(--ds-font-family-code, \"Geist Mono Variable\",\n ui-monospace, SFMono-Regular, Menlo, monospace);\n}\n\n/* ---------------------------------------------------------------------------\n 1. Surfaces\n ------------------------------------------------------------------------ */\n\n.dsb-card {\n border: 1px solid var(--dsb-line-strong);\n border-radius: var(--dsb-radius);\n background: var(--dsb-surface);\n padding: 16px;\n color: var(--dsb-text);\n font-family: var(--dsb-font);\n}\n\n.dsb-card--flush { padding: 0; }\n\n.dsb-card-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--dsb-gap);\n margin-bottom: 12px;\n}\n\n.dsb-title {\n margin: 0;\n font-size: 14px;\n font-weight: 600;\n letter-spacing: -0.01em;\n color: var(--dsb-text);\n}\n\n.dsb-subtitle {\n margin: 2px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsb-text-secondary);\n}\n\n.dsb-muted { color: var(--dsb-text-secondary); }\n.dsb-caption {\n font-size: 11px;\n color: var(--dsb-text-caption);\n}\n\n/* Section divider: a hairline, never a filled band. */\n.dsb-divider {\n height: 1px;\n border: 0;\n margin: 16px 0;\n background: var(--dsb-line-strong);\n}\n\n/* ---------------------------------------------------------------------------\n 2. The score bar — the signature element\n Track 48×6px, pill, --dsb-track. Fill: display:block, width %, pill.\n NOTE: the fill MUST be display:block. As an inline <span> its width and\n height are ignored and the bar silently disappears, leaving only the grey\n track. That exact bug shipped once; do not let it back in.\n ------------------------------------------------------------------------ */\n\n.dsb-bar {\n display: flex;\n align-items: center;\n gap: 6px;\n white-space: nowrap;\n}\n\n.dsb-bar-track {\n position: relative;\n width: 48px;\n height: 6px;\n flex: 0 0 48px;\n border-radius: var(--dsb-radius-pill);\n background: var(--dsb-track);\n overflow: hidden;\n}\n\n.dsb-bar-fill {\n display: block;\n height: 100%;\n border-radius: var(--dsb-radius-pill);\n background: var(--dsb-accent-speed);\n transition: width 150ms ease-out;\n}\n\n.dsb-bar-fill--stability { background: var(--dsb-accent-stability); }\n.dsb-bar-fill--speed { background: var(--dsb-accent-speed); }\n.dsb-bar-fill--intelligence { background: var(--dsb-accent-intelligence); }\n\n/* No data: a dashed outline and an en dash. Never draw a 0-width bar and let\n it read as \"scores zero\". */\n.dsb-bar--empty .dsb-bar-track {\n background: transparent;\n border: 1px dashed color-mix(in srgb, var(--dsb-text-tertiary) 40%, transparent);\n}\n\n.dsb-bar-num {\n width: 28px;\n text-align: right;\n font-family: var(--dsb-font-mono);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n color: var(--dsb-text-secondary);\n}\n\n/* ---------------------------------------------------------------------------\n 3. State dot\n ------------------------------------------------------------------------ */\n\n.dsb-dot {\n display: inline-block;\n width: 6px;\n height: 6px;\n flex: 0 0 6px;\n border-radius: var(--dsb-radius-pill);\n background: var(--dsb-text-tertiary);\n}\n\n.dsb-dot--healthy { background: var(--dsb-accent-stability); }\n.dsb-dot--warn { background: var(--dsb-accent-warn); }\n.dsb-dot--error { background: var(--dsb-accent-error); }\n.dsb-dot--idle { background: var(--dsb-text-tertiary); }\n\n/* ---------------------------------------------------------------------------\n 4. Segmented control — grouped options inside one pill-ish frame\n ------------------------------------------------------------------------ */\n\n.dsb-segmented {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 4px;\n border: 1px solid var(--dsb-line-strong);\n border-radius: var(--dsb-radius);\n}\n\n.dsb-seg {\n padding: 4px 10px;\n border: 0;\n border-radius: var(--dsb-radius-sm);\n background: transparent;\n color: var(--dsb-text-secondary);\n font-family: inherit;\n font-size: 12px;\n line-height: 1.4;\n cursor: pointer;\n transition: background-color 150ms, color 150ms;\n}\n\n.dsb-seg:hover { background: var(--dsb-hover); color: var(--dsb-text); }\n\n/* Selected = inverted, not coloured. Colour is reserved for data. */\n.dsb-seg[aria-pressed=\"true\"],\n.dsb-seg[data-selected=\"true\"] {\n background: var(--dsb-inverted-bg);\n color: var(--dsb-inverted-fg);\n font-weight: 500;\n}\n\n/* ---------------------------------------------------------------------------\n 5. Chips and badges\n ------------------------------------------------------------------------ */\n\n.dsb-chip {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 6px;\n border-radius: var(--dsb-radius-pill);\n background: var(--dsb-surface-3);\n color: var(--dsb-text-secondary);\n font-size: 10px;\n line-height: 1.6;\n white-space: nowrap;\n}\n\n.dsb-chip--active {\n background: var(--dsb-inverted-bg);\n color: var(--dsb-inverted-fg);\n font-weight: 500;\n}\n\n.dsb-chip--stability { background: var(--dsb-wash-stability); color: var(--dsb-accent-stability); }\n.dsb-chip--warn { background: var(--dsb-wash-warn); color: var(--dsb-accent-warn); }\n.dsb-chip--error { background: var(--dsb-wash-error); color: var(--dsb-accent-error); }\n.dsb-chip--info { background: var(--dsb-wash-info); color: var(--dsb-accent-info); }\n.dsb-chip--brand { background: var(--dsb-wash-brand); color: var(--dsb-accent-intelligence); }\n\n/* In dark mode the 15% wash over a dark surface needs a lighter source colour\n to stay legible; the host's dark flag drives the swap. */\n[data-ds-dark-theme] .dsb-chip--stability,\n.dark .dsb-chip--stability { color: color-mix(in srgb, var(--dsb-accent-stability) 70%, white); }\n[data-ds-dark-theme] .dsb-chip--warn,\n.dark .dsb-chip--warn { color: color-mix(in srgb, var(--dsb-accent-warn) 70%, white); }\n[data-ds-dark-theme] .dsb-chip--error,\n.dark .dsb-chip--error { color: color-mix(in srgb, var(--dsb-accent-error) 70%, white); }\n\n/* ---------------------------------------------------------------------------\n 6. Tabs — underline on the active one, no pill fill\n ------------------------------------------------------------------------ */\n\n.dsb-tabs {\n display: flex;\n align-items: center;\n gap: 20px;\n border-bottom: 1px solid var(--dsb-line-strong);\n}\n\n.dsb-tab {\n position: relative;\n padding: 8px 0 10px;\n border: 0;\n background: transparent;\n color: var(--dsb-text-secondary);\n font-family: inherit;\n font-size: 13px;\n cursor: pointer;\n}\n\n.dsb-tab:hover { color: var(--dsb-text); }\n\n.dsb-tab[aria-selected=\"true\"],\n.dsb-tab[data-selected=\"true\"] {\n color: var(--dsb-text);\n font-weight: 500;\n}\n\n.dsb-tab[aria-selected=\"true\"]::after,\n.dsb-tab[data-selected=\"true\"]::after {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n bottom: -1px;\n height: 2px;\n border-radius: 2px 2px 0 0;\n background: var(--dsb-text);\n}\n\n/* ---------------------------------------------------------------------------\n 7. Table row — hairline separators, transparent background\n ------------------------------------------------------------------------ */\n\n.dsb-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 12px;\n color: var(--dsb-text);\n}\n\n.dsb-table th {\n padding: 8px 12px 8px 0;\n text-align: left;\n font-size: 11px;\n font-weight: 500;\n color: var(--dsb-text-secondary);\n border-bottom: 1px solid var(--dsb-line-strong);\n white-space: nowrap;\n}\n\n.dsb-table td {\n padding: 8px 12px 8px 0;\n vertical-align: middle;\n border-bottom: 1px solid var(--dsb-line);\n}\n\n.dsb-table tr:last-child td { border-bottom: 0; }\n.dsb-table tbody tr:hover { background: var(--dsb-hover); }\n\n.dsb-num {\n font-family: var(--dsb-font-mono);\n font-variant-numeric: tabular-nums;\n}\n\n.dsb-row-index {\n width: 28px;\n color: var(--dsb-text-caption);\n font-family: var(--dsb-font-mono);\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n\n.dsb-row-id {\n font-family: var(--dsb-font-mono);\n font-size: 12px;\n color: var(--dsb-text);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dsb-row-dim { opacity: 0.55; }\n\n/* ---------------------------------------------------------------------------\n 8. Toggle\n ------------------------------------------------------------------------ */\n\n.dsb-switch {\n position: relative;\n width: 34px;\n height: 20px;\n flex: 0 0 34px;\n padding: 0;\n border: 0;\n border-radius: var(--dsb-radius-pill);\n background: color-mix(in srgb, var(--dsb-text-tertiary) 28%, transparent);\n cursor: pointer;\n transition: background-color 150ms;\n}\n\n.dsb-switch::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--dsb-inverted-fg);\n transition: transform 150ms ease-out;\n}\n\n.dsb-switch[aria-checked=\"true\"] { background: var(--dsb-accent-stability); }\n.dsb-switch[aria-checked=\"true\"]::after { transform: translateX(14px); }\n\n/* Disabled stays readable: dim the track, not the whole row. */\n.dsb-switch:disabled { opacity: 0.4; cursor: default; }\n\n/* ---------------------------------------------------------------------------\n 9. Buttons\n ------------------------------------------------------------------------ */\n\n.dsb-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 6px 12px;\n border: 1px solid var(--dsb-line-strong);\n border-radius: var(--dsb-radius-sm);\n background: transparent;\n color: var(--dsb-text);\n font-family: inherit;\n font-size: 12px;\n line-height: 1.5;\n text-decoration: none;\n cursor: pointer;\n transition: background-color 150ms;\n}\n\n.dsb-btn:hover { background: var(--dsb-hover); }\n.dsb-btn:disabled { opacity: 0.45; cursor: default; background: transparent; }\n\n.dsb-btn--primary {\n border-color: transparent;\n background: var(--dsb-inverted-bg);\n color: var(--dsb-inverted-fg);\n font-weight: 500;\n}\n\n.dsb-btn--primary:hover { background: var(--dsb-inverted-bg); opacity: 0.9; }\n\n.dsb-btn--danger { color: var(--dsb-accent-error); }\n\n/* ---------------------------------------------------------------------------\n 10. Empty state\n ------------------------------------------------------------------------ */\n\n.dsb-empty {\n padding: 32px 16px;\n text-align: center;\n font-size: 12px;\n color: var(--dsb-text-secondary);\n}\n\n.dsb-empty--error { color: var(--dsb-accent-error); }\n"
|
|
8
|
+
const injectBaseline = (doc, plugin) => {
|
|
9
|
+
const target = doc || (typeof document === "undefined" ? null : document)
|
|
10
|
+
if (!target || !target.head) return null
|
|
11
|
+
const key = plugin || "dsh-design-baseline"
|
|
12
|
+
const existing = target.querySelector(`style[data-dsb="${key}"]`)
|
|
13
|
+
if (existing) return existing
|
|
14
|
+
const style = target.createElement("style")
|
|
15
|
+
style.dataset.dsb = key
|
|
16
|
+
style.textContent = BASELINE_CSS
|
|
17
|
+
target.head.appendChild(style)
|
|
18
|
+
return style
|
|
19
|
+
}
|
|
20
|
+
const THEME = {
|
|
21
|
+
"text": "var(--dsw-alias-label-primary, #0f1115)",
|
|
22
|
+
"textSecondary": "var(--dsw-alias-label-secondary, #61666b)",
|
|
23
|
+
"textTertiary": "var(--dsw-alias-label-tertiary, #81858c)",
|
|
24
|
+
"textCaption": "var(--dsw-alias-label-caption, #adb2b8)",
|
|
25
|
+
"surface": "var(--dsw-alias-bg-layer-1, #ffffff)",
|
|
26
|
+
"surface2": "var(--dsw-alias-bg-layer-2, #fafafa)",
|
|
27
|
+
"surface3": "var(--dsw-alias-bg-layer-3, #f5f5f5)",
|
|
28
|
+
"card": "var(--dsw-alias-bg-module-platform, #f9fafb)",
|
|
29
|
+
"track": "var(--dsw-alias-bg-layer-2, #f5f5f5)",
|
|
30
|
+
"line": "var(--dsw-alias-border-l1, rgba(0,0,0,0.04))",
|
|
31
|
+
"lineStrong": "var(--dsw-alias-border-l2, rgba(0,0,0,0.10))",
|
|
32
|
+
"hover": "var(--dsw-alias-interactive-bg-hover, rgba(38,49,72,0.06))",
|
|
33
|
+
"invertedBg": "var(--dsw-alias-label-primary, #0f1115)",
|
|
34
|
+
"invertedFg": "var(--dsw-alias-label-primary-foreground, #ffffff)",
|
|
35
|
+
"primaryFill": "var(--dsw-alias-button-primary-fill, #3b82f6)",
|
|
36
|
+
"primaryFg": "var(--dsw-alias-label-primary-foreground, #ffffff)",
|
|
37
|
+
"accentStability": "#22c55e",
|
|
38
|
+
"accentSpeed": "#3b82f6",
|
|
39
|
+
"accentIntelligence": "#a855f7",
|
|
40
|
+
"accentWarn": "#f59e0b",
|
|
41
|
+
"accentError": "#f43f5e",
|
|
42
|
+
"accentInfo": "#06b6d4",
|
|
43
|
+
"radius": "12px",
|
|
44
|
+
"radiusSm": "8px",
|
|
45
|
+
"radiusPill": "9999px",
|
|
46
|
+
"gap": "12px"
|
|
47
|
+
}
|
|
48
|
+
const wash = (accent) => `color-mix(in srgb, ${accent} 15%, transparent)`
|
|
49
|
+
// <<< dsh-design-baseline
|
|
4
50
|
const React = require('react');
|
|
5
51
|
const h = React.createElement;
|
|
6
52
|
const { useCallback, useEffect, useMemo, useRef, useState } = React;
|
|
@@ -42,6 +88,7 @@ window.__ModuleLoader__.load({
|
|
|
42
88
|
why_rollback: '每个插件/程序只留版本号最高的一份,其余是回滚副本',
|
|
43
89
|
why_superseded: '版本号不高于已装版本的更新包,装着的那份已经生效',
|
|
44
90
|
why_orphan: 'QQ 频道数据目录:目录内 30 天没有任何文件写入时会被整目录清除',
|
|
91
|
+
why_irreversible: '回收站里的东西已经被删过一次,清空后无法恢复',
|
|
45
92
|
why_history: '每个会话的整文件备份,删了不影响会话本身',
|
|
46
93
|
why_duplicate: '内容完全相同的重复副本,保留最新那份',
|
|
47
94
|
'why_database-backup': '升级前的数据库副本,正在用的数据库还在旁边',
|
|
@@ -52,6 +99,9 @@ window.__ModuleLoader__.load({
|
|
|
52
99
|
scanError: '扫描失败',
|
|
53
100
|
cleanError: '清理失败',
|
|
54
101
|
cleanConfirm: '删除选中缓存目录里的文件?正在使用的文件会被跳过。',
|
|
102
|
+
cleanConfirmList: '即将清理 {n} 项,合计 {bytes}:\n\n{list}\n\n回收站等不可恢复的项目会单独标出。',
|
|
103
|
+
cleanConfirmIrreversible: '\n\n注意:其中「回收站」清空后无法恢复。',
|
|
104
|
+
truncatedHint: '这个目录太大,只统计/清理了前 {n} 条,实际释放可能少于这个数。',
|
|
55
105
|
'target_temp': '用户临时文件',
|
|
56
106
|
'target_windows-temp': 'Windows 临时文件',
|
|
57
107
|
'target_npm-cache': 'npm 缓存',
|
|
@@ -64,7 +114,7 @@ window.__ModuleLoader__.load({
|
|
|
64
114
|
'target_code-cache': 'VS Code 缓存',
|
|
65
115
|
'target_inet-cache': '系统网络缓存',
|
|
66
116
|
'target_windows-update': 'Windows 更新缓存',
|
|
67
|
-
'target_recycle-bin': '
|
|
117
|
+
'target_recycle-bin': '回收站(清空不可恢复)',
|
|
68
118
|
'target_app-updaters': '更新安装包残留',
|
|
69
119
|
'target_app-caches': '各应用缓存',
|
|
70
120
|
'target_app-logs': '各应用日志',
|
|
@@ -81,7 +131,7 @@ window.__ModuleLoader__.load({
|
|
|
81
131
|
'target_rewind-snapshots': '过期的回滚快照',
|
|
82
132
|
'target_rotated-logs': '轮转掉的日志',
|
|
83
133
|
'target_qoder-old-versions': '编辑器少用的旧版本目录',
|
|
84
|
-
'target_orphan-app-data': '
|
|
134
|
+
'target_orphan-app-data': 'QQ 频道数据(长期未写入)',
|
|
85
135
|
'target_app-data-caches': '已停用应用的缓存',
|
|
86
136
|
'target_office-update-cache': 'Office 更新暂存包',
|
|
87
137
|
group_system: '系统临时与回收站',
|
|
@@ -90,7 +140,7 @@ window.__ModuleLoader__.load({
|
|
|
90
140
|
group_app: '应用缓存与日志',
|
|
91
141
|
group_updates: '升级残留',
|
|
92
142
|
group_history: '历史备份与快照',
|
|
93
|
-
group_orphan: '
|
|
143
|
+
group_orphan: '长期未动的应用数据',
|
|
94
144
|
group_other: '其他',
|
|
95
145
|
groupExpanded: '收起这一类',
|
|
96
146
|
groupFolded: '展开这一类',
|
|
@@ -214,6 +264,7 @@ window.__ModuleLoader__.load({
|
|
|
214
264
|
why_rollback: 'only the highest version of each family stays; the rest are rollback copies',
|
|
215
265
|
why_superseded: 'the update package version is not above what is installed, so it is already applied',
|
|
216
266
|
why_orphan: 'the QQGuild data folder, deleted in full when no file in it has been written for 30 days',
|
|
267
|
+
why_irreversible: 'everything here was already deleted once; emptying it cannot be undone',
|
|
217
268
|
why_history: 'a whole-file backup per session; removing it does not touch the session',
|
|
218
269
|
why_duplicate: 'an identical copy; the newest one is kept',
|
|
219
270
|
'why_database-backup': 'a copy made before an upgrade; the live database is beside it',
|
|
@@ -224,6 +275,9 @@ window.__ModuleLoader__.load({
|
|
|
224
275
|
scanError: 'Scan failed',
|
|
225
276
|
cleanError: 'Clean failed',
|
|
226
277
|
cleanConfirm: 'Delete the files in the selected cache directories? Files in use are skipped.',
|
|
278
|
+
cleanConfirmList: 'About to clean {n} target(s), {bytes} in total:\n\n{list}\n\nAnything irreversible is called out separately.',
|
|
279
|
+
cleanConfirmIrreversible: '\n\nNote: emptying the Recycle Bin cannot be undone.',
|
|
280
|
+
truncatedHint: 'This folder is too large: only the first {n} entries were measured or removed, so less may be freed.',
|
|
227
281
|
'target_temp': 'User temp files',
|
|
228
282
|
'target_windows-temp': 'Windows temp files',
|
|
229
283
|
'target_npm-cache': 'npm cache',
|
|
@@ -236,7 +290,7 @@ window.__ModuleLoader__.load({
|
|
|
236
290
|
'target_code-cache': 'VS Code cache',
|
|
237
291
|
'target_inet-cache': 'System internet cache',
|
|
238
292
|
'target_windows-update': 'Windows update cache',
|
|
239
|
-
'target_recycle-bin': 'Recycle Bin',
|
|
293
|
+
'target_recycle-bin': 'Recycle Bin (emptying is permanent)',
|
|
240
294
|
'target_app-updaters': 'Leftover update installers',
|
|
241
295
|
'target_app-caches': 'App caches',
|
|
242
296
|
'target_app-logs': 'App logs',
|
|
@@ -253,7 +307,7 @@ window.__ModuleLoader__.load({
|
|
|
253
307
|
'target_rewind-snapshots': 'Expired rewind snapshots',
|
|
254
308
|
'target_rotated-logs': 'Rotated-out log files',
|
|
255
309
|
'target_qoder-old-versions': 'Old versions the editor no longer runs',
|
|
256
|
-
'target_orphan-app-data': '
|
|
310
|
+
'target_orphan-app-data': 'QQ Guild data (long untouched)',
|
|
257
311
|
'target_app-data-caches': 'Caches of apps that stopped writing',
|
|
258
312
|
'target_office-update-cache': 'Office update staging packages',
|
|
259
313
|
group_system: 'System temp and recycle bin',
|
|
@@ -262,7 +316,7 @@ window.__ModuleLoader__.load({
|
|
|
262
316
|
group_app: 'App caches and logs',
|
|
263
317
|
group_updates: 'Leftover updaters',
|
|
264
318
|
group_history: 'Backups and snapshots',
|
|
265
|
-
group_orphan: '
|
|
319
|
+
group_orphan: 'Long-untouched app data',
|
|
266
320
|
group_other: 'Other',
|
|
267
321
|
groupExpanded: 'Collapse this group',
|
|
268
322
|
groupFolded: 'Expand this group',
|
|
@@ -354,101 +408,106 @@ window.__ModuleLoader__.load({
|
|
|
354
408
|
};
|
|
355
409
|
|
|
356
410
|
const CSS = `
|
|
357
|
-
.dsc-root { box-sizing: border-box; height: 100%; overflow-y: auto; padding: 16px 20px 32px; font-size: 13px; color: var(--
|
|
411
|
+
.dsc-root { box-sizing: border-box; height: 100%; overflow-y: auto; padding: 16px 20px 32px; font-family: var(--dsb-font); font-size: 13px; color: var(--dsb-text); background: transparent; color-scheme: light; }
|
|
412
|
+
/* The host flags dark mode on body, not through prefers-color-scheme; matching that
|
|
413
|
+
flag is what makes native <select> menus and scrollbars render dark too. */
|
|
414
|
+
body[data-ds-dark-theme] .dsc-root, .dark .dsc-root { color-scheme: dark; }
|
|
358
415
|
.dsc-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
|
|
359
416
|
.dsc-head-spacer { margin-left: auto; }
|
|
360
|
-
.dsc-head-btn { box-sizing: border-box; display: inline-flex; align-items: center; gap: 6px; font: inherit; font-size: 12px; line-height: 1; padding: 6px 11px; border-radius:
|
|
361
|
-
.dsc-head-btn:hover { color: var(--
|
|
417
|
+
.dsc-head-btn { box-sizing: border-box; display: inline-flex; align-items: center; gap: 6px; font: inherit; font-size: 12px; line-height: 1; padding: 6px 11px; border-radius: var(--dsb-radius-pill); cursor: pointer; border: 1px solid var(--dsb-line-strong); background: var(--dsb-surface-2); color: var(--dsb-text-secondary); white-space: nowrap; }
|
|
418
|
+
.dsc-head-btn:hover { color: var(--dsb-text); background: var(--dsb-hover); border-color: var(--dsb-line-strong); }
|
|
362
419
|
.dsc-head h1 { margin: 0; font-size: 16px; font-weight: 600; line-height: 24px; }
|
|
363
|
-
.dsc-head-sub { color: var(--
|
|
364
|
-
.dsc-card { box-sizing: border-box; background: var(--
|
|
420
|
+
.dsc-head-sub { color: var(--dsb-text-secondary); font-size: 12px; }
|
|
421
|
+
.dsc-card { box-sizing: border-box; background: var(--dsb-surface); border: 1px solid var(--dsb-line-strong); border-radius: var(--dsb-radius); padding: 12px 14px; margin-bottom: 12px; }
|
|
365
422
|
.dsc-card-title { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; font-weight: 600; }
|
|
366
423
|
.dsc-drives { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr)); gap: 10px 18px; }
|
|
367
424
|
.dsc-drive-label { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-size: 12px; margin-bottom: 4px; }
|
|
368
425
|
.dsc-drive-label b { font-size: 13px; }
|
|
369
|
-
.dsc-drive-label span { color: var(--
|
|
370
|
-
.dsc-bar { height: 6px; border-radius:
|
|
371
|
-
.dsc-bar-fill { height: 100%; border-radius:
|
|
372
|
-
.dsc-bar-fill.dsc-warn { background: var(--
|
|
373
|
-
.dsc-bar-fill.dsc-crit { background: var(--
|
|
426
|
+
.dsc-drive-label span { color: var(--dsb-text-secondary); font-variant-numeric: tabular-nums; }
|
|
427
|
+
.dsc-bar { height: 6px; border-radius: var(--dsb-radius-pill); background: var(--dsb-surface-2); overflow: hidden; }
|
|
428
|
+
.dsc-bar-fill { height: 100%; border-radius: var(--dsb-radius-pill); background: var(--dsb-accent-intelligence); transition: width .25s ease; }
|
|
429
|
+
.dsc-bar-fill.dsc-warn { background: var(--dsb-accent-warn); }
|
|
430
|
+
.dsc-bar-fill.dsc-crit { background: var(--dsb-accent-error); }
|
|
374
431
|
.dsc-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 10px; }
|
|
375
|
-
.dsc-chips-label { color: var(--
|
|
376
|
-
.dsc-chip { font: inherit; font-size: 12px; line-height: 1; padding: 5px 10px; border-radius:
|
|
377
|
-
.dsc-chip:hover:not(.dsc-chip-active) { color: var(--
|
|
378
|
-
.dsc-chip-active { border-color: var(--
|
|
432
|
+
.dsc-chips-label { color: var(--dsb-text-secondary); font-size: 12px; margin-right: 2px; }
|
|
433
|
+
.dsc-chip { font: inherit; font-size: 12px; line-height: 1; padding: 5px 10px; border-radius: var(--dsb-radius-pill); cursor: pointer; border: 1px solid var(--dsb-line-strong); background: transparent; color: var(--dsb-text-secondary); }
|
|
434
|
+
.dsc-chip:hover:not(.dsc-chip-active) { color: var(--dsb-text); background: var(--dsb-hover); }
|
|
435
|
+
.dsc-chip-active { border-color: transparent; background: var(--dsb-inverted-bg); color: var(--dsb-inverted-fg); font-weight: 600; }
|
|
379
436
|
.dsc-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 10px; }
|
|
380
|
-
.dsc-btn { font: inherit; font-size: 12px; line-height: 1; padding: 6px 12px; border-radius:
|
|
381
|
-
.dsc-btn:hover:not(:disabled) { background: var(--
|
|
437
|
+
.dsc-btn { font: inherit; font-size: 12px; line-height: 1; padding: 6px 12px; border-radius: var(--dsb-radius-sm); cursor: pointer; border: 1px solid var(--dsb-line-strong); background: var(--dsb-surface); color: var(--dsb-text); }
|
|
438
|
+
.dsc-btn:hover:not(:disabled) { background: var(--dsb-surface-2); }
|
|
382
439
|
.dsc-btn:disabled { opacity: .5; cursor: default; }
|
|
383
|
-
.dsc-btn-primary { background: var(--
|
|
384
|
-
.dsc-btn-primary:hover:not(:disabled) {
|
|
385
|
-
.dsc-btn-danger { border-color: var(--
|
|
386
|
-
.dsc-btn-danger:hover:not(:disabled) { background: var(--
|
|
440
|
+
.dsc-btn-primary { background: var(--dsb-inverted-bg); border-color: transparent; color: var(--dsb-inverted-fg); font-weight: 600; }
|
|
441
|
+
.dsc-btn-primary:hover:not(:disabled) { opacity: .9; }
|
|
442
|
+
.dsc-btn-danger { border-color: var(--dsb-accent-error); color: var(--dsb-accent-error); }
|
|
443
|
+
.dsc-btn-danger:hover:not(:disabled) { background: var(--dsb-surface-2); }
|
|
387
444
|
.dsc-spacer { flex: 1; }
|
|
388
|
-
.dsc-total { color: var(--
|
|
389
|
-
.dsc-total b { color: var(--
|
|
445
|
+
.dsc-total { color: var(--dsb-text-secondary); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
446
|
+
.dsc-total b { color: var(--dsb-text); font-size: 13px; }
|
|
390
447
|
.dsc-list { display: flex; flex-direction: column; gap: 2px; }
|
|
391
|
-
.dsc-row { display: flex; align-items: center; gap: 10px; padding: 7px 8px; border-radius:
|
|
448
|
+
.dsc-row { display: flex; align-items: center; gap: 10px; padding: 7px 8px; border-radius: var(--dsb-radius-sm); cursor: pointer; }
|
|
392
449
|
.dsc-group { display: flex; flex-direction: column; gap: 2px; }
|
|
393
450
|
.dsc-group + .dsc-group { margin-top: 6px; }
|
|
394
|
-
.dsc-group-head { display: flex; align-items: center; gap: 8px; width: 100%; box-sizing: border-box; padding: 6px 8px; border: 0; border-radius:
|
|
395
|
-
.dsc-group-head:hover { background: var(--
|
|
396
|
-
.dsc-group-head:focus-visible { outline: 2px solid var(--
|
|
397
|
-
.dsc-caret { width: 10px; color: var(--
|
|
451
|
+
.dsc-group-head { display: flex; align-items: center; gap: 8px; width: 100%; box-sizing: border-box; padding: 6px 8px; border: 0; border-radius: var(--dsb-radius-sm); background: transparent; font: inherit; color: var(--dsb-text); cursor: pointer; text-align: left; }
|
|
452
|
+
.dsc-group-head:hover { background: var(--dsb-hover); }
|
|
453
|
+
.dsc-group-head:focus-visible { outline: 2px solid var(--dsb-text); outline-offset: 1px; }
|
|
454
|
+
.dsc-caret { width: 10px; color: var(--dsb-text-secondary); font-size: 10px; line-height: 1; }
|
|
398
455
|
.dsc-group-title { flex: 1; min-width: 0; font-size: 12px; font-weight: 600; letter-spacing: .01em; }
|
|
399
|
-
.dsc-group-meta { color: var(--
|
|
400
|
-
.dsc-row:hover { background: var(--
|
|
401
|
-
.dsc-row input { margin: 0; accent-color: var(--
|
|
456
|
+
.dsc-group-meta { color: var(--dsb-text-secondary); font-size: 11px; }
|
|
457
|
+
.dsc-row:hover { background: var(--dsb-hover); }
|
|
458
|
+
.dsc-row input { margin: 0; accent-color: var(--dsb-inverted-bg); }
|
|
402
459
|
.dsc-row-name { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
|
|
403
460
|
.dsc-row-label { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
404
|
-
.dsc-row-hint { color: var(--
|
|
405
|
-
.dsc-legend { display: flex; flex-direction: column; gap: 6px; border-top: 1px solid var(--
|
|
406
|
-
.dsc-legend-row { display: flex; align-items: flex-start; gap: 8px; color: var(--
|
|
461
|
+
.dsc-row-hint { color: var(--dsb-text-secondary); font-size: 11px; }
|
|
462
|
+
.dsc-legend { display: flex; flex-direction: column; gap: 6px; border-top: 1px solid var(--dsb-line-strong); margin-top: 10px; padding-top: 10px; }
|
|
463
|
+
.dsc-legend-row { display: flex; align-items: flex-start; gap: 8px; color: var(--dsb-text-secondary); font-size: 11px; line-height: 16px; }
|
|
407
464
|
.dsc-legend-row > span:last-child { flex: 1; min-width: 0; }
|
|
408
|
-
.dsc-badge { flex: none; font-size: 10px; line-height: 16px; padding: 0 6px; border-radius:
|
|
409
|
-
.dsc-badge.dsc-badge-caution { border-color: var(--
|
|
465
|
+
.dsc-badge { flex: none; font-size: 10px; line-height: 16px; padding: 0 6px; border-radius: var(--dsb-radius-sm); border: 1px solid var(--dsb-line-strong); color: var(--dsb-text-secondary); }
|
|
466
|
+
.dsc-badge.dsc-badge-caution { border-color: var(--dsb-accent-warn); color: var(--dsb-accent-warn); }
|
|
410
467
|
.dsc-size { flex: none; min-width: 72px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
|
|
411
|
-
.dsc-size.dsc-idle { color: var(--
|
|
412
|
-
.dsc-empty { color: var(--
|
|
413
|
-
.dsc-notice { margin-top: 10px; padding: 8px 10px; border-radius:
|
|
414
|
-
.dsc-notice.dsc-ok { border-left: 3px solid var(--
|
|
415
|
-
.dsc-notice.dsc-err { border-left: 3px solid var(--
|
|
416
|
-
.dsc-
|
|
417
|
-
.dsc-
|
|
418
|
-
.dsc-plan
|
|
468
|
+
.dsc-size.dsc-idle { color: var(--dsb-text-secondary); font-weight: 400; }
|
|
469
|
+
.dsc-empty { color: var(--dsb-text-secondary); padding: 18px 4px; text-align: center; line-height: 1.7; }
|
|
470
|
+
.dsc-notice { margin-top: 10px; padding: 8px 10px; border-radius: var(--dsb-radius-sm); font-size: 12px; line-height: 1.6; background: var(--dsb-surface-2); }
|
|
471
|
+
.dsc-notice.dsc-ok { border-left: 3px solid var(--dsb-accent-stability); }
|
|
472
|
+
.dsc-notice.dsc-err { border-left: 3px solid var(--dsb-accent-error); color: var(--dsb-accent-error); }
|
|
473
|
+
.dsc-notice.dsc-warn { border-left: 3px solid var(--dsb-accent-warn); }
|
|
474
|
+
.dsc-audit-hint { color: var(--dsb-text-secondary); font-size: 12px; line-height: 1.7; margin: -4px 0 10px; }
|
|
475
|
+
.dsc-plan { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--dsb-line-strong); }
|
|
476
|
+
.dsc-plan-step { display: flex; flex-direction: column; gap: 6px; padding: 10px 0; border-bottom: 1px solid var(--dsb-line-strong); }
|
|
419
477
|
.dsc-plan-step:last-child { border-bottom: 0; }
|
|
420
478
|
.dsc-plan-head { display: flex; align-items: baseline; gap: 8px; }
|
|
421
|
-
.dsc-plan-index { flex: none; width: 18px; height: 18px; border-radius:
|
|
479
|
+
.dsc-plan-index { flex: none; width: 18px; height: 18px; border-radius: var(--dsb-radius-pill); background: var(--dsb-surface-2); color: var(--dsb-text-secondary); font-size: 11px; line-height: 18px; text-align: center; font-variant-numeric: tabular-nums; }
|
|
422
480
|
.dsc-plan-title { flex: 1; min-width: 0; font-weight: 600; }
|
|
423
|
-
.dsc-plan-size { flex: none; font-variant-numeric: tabular-nums; color: var(--
|
|
424
|
-
.dsc-plan-detail { color: var(--
|
|
481
|
+
.dsc-plan-size { flex: none; font-variant-numeric: tabular-nums; color: var(--dsb-text-secondary); }
|
|
482
|
+
.dsc-plan-detail { color: var(--dsb-text-secondary); font-size: 12px; line-height: 1.7; padding-left: 26px; }
|
|
425
483
|
.dsc-plan-cmd { display: flex; align-items: flex-start; gap: 8px; padding-left: 26px; }
|
|
426
|
-
.dsc-plan-cmd pre { flex: 1; min-width: 0; margin: 0; padding: 8px 10px; border-radius:
|
|
484
|
+
.dsc-plan-cmd pre { flex: 1; min-width: 0; margin: 0; padding: 8px 10px; border-radius: var(--dsb-radius-sm); background: var(--dsb-surface); border: 1px solid var(--dsb-line-strong); font-family: var(--dsb-font-mono); font-size: 11px; line-height: 1.7; overflow-x: auto; white-space: pre; }
|
|
427
485
|
.dsc-plan-step .dsc-btn { align-self: flex-start; margin-left: 26px; }
|
|
428
486
|
.dsc-audit-group { margin-top: 12px; }
|
|
429
487
|
.dsc-audit-group:first-of-type { margin-top: 4px; }
|
|
430
|
-
.dsc-audit-group-title { font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--
|
|
431
|
-
.dsc-audit-row { display: flex; align-items: baseline; gap: 10px; padding: 6px 8px; border-radius:
|
|
432
|
-
.dsc-audit-row:hover { background: var(--
|
|
488
|
+
.dsc-audit-group-title { font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--dsb-text-secondary); margin-bottom: 4px; }
|
|
489
|
+
.dsc-audit-row { display: flex; align-items: baseline; gap: 10px; padding: 6px 8px; border-radius: var(--dsb-radius-sm); }
|
|
490
|
+
.dsc-audit-row:hover { background: var(--dsb-hover); }
|
|
433
491
|
.dsc-openable { cursor: pointer; }
|
|
434
|
-
.dsc-openable:hover .dsc-audit-label { text-decoration: underline; text-decoration-color: var(--
|
|
435
|
-
.dsc-openable:focus-visible { outline: 2px solid var(--
|
|
492
|
+
.dsc-openable:hover .dsc-audit-label { text-decoration: underline; text-decoration-color: var(--dsb-line); text-underline-offset: 3px; }
|
|
493
|
+
.dsc-openable:focus-visible { outline: 2px solid var(--dsb-text); outline-offset: 1px; }
|
|
436
494
|
.dsc-audit-name { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
437
495
|
.dsc-audit-label { font-weight: 600; }
|
|
438
|
-
.dsc-audit-why { color: var(--
|
|
496
|
+
.dsc-audit-why { color: var(--dsb-text-secondary); font-size: 11px; line-height: 16px; }
|
|
439
497
|
.dsc-audit-size { flex: none; min-width: 72px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
|
|
440
498
|
.dsc-settings { display: flex; flex-direction: column; gap: 12px; }
|
|
441
499
|
.dsc-settings .dsc-card { margin: 0; }
|
|
442
|
-
.dsc-embedded { border-top: 1px solid var(--
|
|
500
|
+
.dsc-embedded { border-top: 1px solid var(--dsb-line-strong); padding-top: 12px; }
|
|
443
501
|
.dsc-embedded > .dsc-root { height: auto; padding: 0; background: transparent; }
|
|
444
|
-
.dsc-cfg-group + .dsc-cfg-group { border-top: .5px solid var(--
|
|
445
|
-
.dsc-cfg-group-title { color: var(--
|
|
502
|
+
.dsc-cfg-group + .dsc-cfg-group { border-top: .5px solid var(--dsb-line-strong); margin-top: 18px; padding-top: 18px; }
|
|
503
|
+
.dsc-cfg-group-title { color: var(--dsb-text); font-size: 16px; font-weight: 600; line-height: 24px; letter-spacing: .01em; }
|
|
446
504
|
.dsc-cfg-row { display: flex; align-items: center; gap: 8px; padding: 8px 0; }
|
|
447
|
-
.dsc-cfg-label { flex: 1; min-width: 0; color: var(--
|
|
505
|
+
.dsc-cfg-label { flex: 1; min-width: 0; color: var(--dsb-text); font-size: 14px; font-weight: 400; }
|
|
448
506
|
.dsc-cfg-pick { position: relative; display: inline-flex; flex: none; }
|
|
449
|
-
.dsc-cfg-select { appearance: none; -webkit-appearance: none; box-sizing: border-box; height: 36px; max-width: 220px; padding: 0 34px 0 14px; border: none; border-radius:
|
|
450
|
-
.dsc-cfg-select:focus-visible { outline: 2px solid var(--
|
|
451
|
-
.dsc-cfg-
|
|
507
|
+
.dsc-cfg-select { appearance: none; -webkit-appearance: none; box-sizing: border-box; height: 36px; max-width: 220px; padding: 0 34px 0 14px; border: none; border-radius: var(--dsb-radius-pill); background: var(--dsb-card); color: var(--dsb-text); font: inherit; font-size: 14px; line-height: 22px; cursor: pointer; }
|
|
508
|
+
.dsc-cfg-select:focus-visible { outline: 2px solid var(--dsb-text); outline-offset: 1px; }
|
|
509
|
+
.dsc-cfg-select option { background: var(--dsb-surface); color: var(--dsb-text); }
|
|
510
|
+
.dsc-cfg-chevron { position: absolute; top: 50%; right: 13px; transform: translateY(-50%); pointer-events: none; color: var(--dsb-text-secondary); display: inline-flex; }
|
|
452
511
|
`;
|
|
453
512
|
|
|
454
513
|
function insertStyles(css) {
|
|
@@ -461,6 +520,12 @@ window.__ModuleLoader__.load({
|
|
|
461
520
|
};
|
|
462
521
|
}
|
|
463
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Targets a clean cannot undo. These are never preselected and never swept in by "select
|
|
525
|
+
* all": emptying the recycle bin is not a cache rebuild, it is the second half of a delete.
|
|
526
|
+
*/
|
|
527
|
+
const IRREVERSIBLE = new Set(['recycle-bin']);
|
|
528
|
+
|
|
464
529
|
function formatBytes(bytes) {
|
|
465
530
|
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
|
|
466
531
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
@@ -674,7 +739,7 @@ window.__ModuleLoader__.load({
|
|
|
674
739
|
drive.root,
|
|
675
740
|
h(
|
|
676
741
|
'span',
|
|
677
|
-
{ style: { color: 'var(--
|
|
742
|
+
{ style: { color: 'var(--dsb-text-secondary)', fontWeight: 400, marginLeft: 6 } },
|
|
678
743
|
`${Math.round(ratio * 100)}%`,
|
|
679
744
|
),
|
|
680
745
|
),
|
|
@@ -723,10 +788,14 @@ window.__ModuleLoader__.load({
|
|
|
723
788
|
const targets = Array.isArray(data.targets) ? data.targets : [];
|
|
724
789
|
setRows(targets);
|
|
725
790
|
const want = defaultPick.get();
|
|
791
|
+
// "all" means every rebuildable target; it never means the irreversible ones.
|
|
726
792
|
const preselect =
|
|
727
793
|
want === 'none'
|
|
728
794
|
? []
|
|
729
|
-
: targets
|
|
795
|
+
: targets
|
|
796
|
+
.filter((row) => row.bytes > 0 && !IRREVERSIBLE.has(row.id))
|
|
797
|
+
.filter((row) => want === 'all' || row.risk === 'safe')
|
|
798
|
+
.map((row) => row.id);
|
|
730
799
|
setPicked(new Set(preselect));
|
|
731
800
|
} catch (err) {
|
|
732
801
|
setError(`${t('scanError')}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -755,13 +824,29 @@ window.__ModuleLoader__.load({
|
|
|
755
824
|
|
|
756
825
|
const pickAll = (on) => {
|
|
757
826
|
if (!shown) return;
|
|
758
|
-
|
|
827
|
+
// "Select all" means every rebuildable, non-empty row on screen; it never reaches an
|
|
828
|
+
// irreversible target. One line, because the shape is asserted in smoke.mjs.
|
|
829
|
+
const eligible = on ? visibleRows().filter((row) => row.bytes > 0 && !IRREVERSIBLE.has(row.id)).map((row) => row.id) : [];
|
|
830
|
+
setPicked(new Set(eligible));
|
|
759
831
|
};
|
|
760
832
|
|
|
761
833
|
const runClean = async () => {
|
|
762
834
|
if (!rows || picked.size === 0) return;
|
|
763
|
-
|
|
764
|
-
|
|
835
|
+
const chosen = shown.filter((row) => picked.has(row.id));
|
|
836
|
+
if (confirmFirst.get() === 'always') {
|
|
837
|
+
// A confirm box that does not say what it deletes is not a confirmation. Name the
|
|
838
|
+
// targets and the total, and call out anything that cannot be undone.
|
|
839
|
+
const bytes = chosen.reduce((sum, row) => sum + (row.bytes || 0), 0);
|
|
840
|
+
const list = chosen.map((row) => `· ${targetName(t, row.id)} — ${formatBytes(row.bytes)}`).join('\n');
|
|
841
|
+
const irreversible = chosen.some((row) => IRREVERSIBLE.has(row.id));
|
|
842
|
+
const text =
|
|
843
|
+
t('cleanConfirmList')
|
|
844
|
+
.replace('{n}', String(chosen.length))
|
|
845
|
+
.replace('{bytes}', formatBytes(bytes))
|
|
846
|
+
.replace('{list}', list) + (irreversible ? t('cleanConfirmIrreversible') : '');
|
|
847
|
+
if (!window.confirm(text)) return;
|
|
848
|
+
}
|
|
849
|
+
const ids = chosen.map((row) => row.id);
|
|
765
850
|
setBusy('clean');
|
|
766
851
|
setError('');
|
|
767
852
|
setNotice(null);
|
|
@@ -771,10 +856,14 @@ window.__ModuleLoader__.load({
|
|
|
771
856
|
const bytes = results.reduce((sum, item) => sum + (item.bytes || 0), 0);
|
|
772
857
|
const files = results.reduce((sum, item) => sum + (item.removed || 0), 0);
|
|
773
858
|
const failed = results.reduce((sum, item) => sum + (item.failed || 0), 0);
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
859
|
+
const truncated = results.filter((item) => item.truncated === true).length > 0;
|
|
860
|
+
// A run that left files behind is not a success banner: an all-ok green line over
|
|
861
|
+
// "12 skipped" reads as done when it is not.
|
|
862
|
+
const kind = failed > 0 ? 'warn' : 'ok';
|
|
863
|
+
const parts = [`${t('done')} ${formatBytes(bytes)}`, `${files} ${t('files')}`];
|
|
864
|
+
if (failed > 0) parts.push(`${t('failed')} ${failed} (${t('failedHint')})`);
|
|
865
|
+
if (truncated) parts.push(t('truncatedHint').replace('{n}', String(results.length)));
|
|
866
|
+
setNotice({ kind, text: parts.join(' · ') });
|
|
778
867
|
await scan();
|
|
779
868
|
} catch (err) {
|
|
780
869
|
setError(`${t('cleanError')}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -800,7 +889,7 @@ window.__ModuleLoader__.load({
|
|
|
800
889
|
|
|
801
890
|
return h(
|
|
802
891
|
'div',
|
|
803
|
-
{ className: 'dsc-root' },
|
|
892
|
+
{ className: 'dsc-root dsb-root' },
|
|
804
893
|
h(
|
|
805
894
|
'div',
|
|
806
895
|
{ className: 'dsc-head' },
|
|
@@ -1152,7 +1241,19 @@ window.__ModuleLoader__.load({
|
|
|
1152
1241
|
|
|
1153
1242
|
/** Run a plan step the plugin is allowed to run, then re-measure so the plan is honest. */
|
|
1154
1243
|
const runStep = async (step) => {
|
|
1155
|
-
if (confirmFirst.get() === 'always'
|
|
1244
|
+
if (confirmFirst.get() === 'always') {
|
|
1245
|
+
// The same confirmation as the main button: a plan step is still a delete, and
|
|
1246
|
+
// `reclaim-caution` can hold an irreversible target.
|
|
1247
|
+
const ids = Array.isArray(step.ids) ? step.ids : [];
|
|
1248
|
+
const list = ids.map((id) => `· ${targetName(t, id)}`).join('\n');
|
|
1249
|
+
const irreversible = ids.some((id) => IRREVERSIBLE.has(id));
|
|
1250
|
+
const text =
|
|
1251
|
+
t('cleanConfirmList')
|
|
1252
|
+
.replace('{n}', String(ids.length))
|
|
1253
|
+
.replace('{bytes}', formatBytes(step.bytes || 0))
|
|
1254
|
+
.replace('{list}', list) + (irreversible ? t('cleanConfirmIrreversible') : '');
|
|
1255
|
+
if (!window.confirm(text)) return;
|
|
1256
|
+
}
|
|
1156
1257
|
setBusy(true);
|
|
1157
1258
|
setError('');
|
|
1158
1259
|
try {
|
|
@@ -1424,7 +1525,10 @@ window.__ModuleLoader__.load({
|
|
|
1424
1525
|
ctx.effect(() => locale.register(NS, 'zh', zh), 'dsh-disk-cleaner: locale zh');
|
|
1425
1526
|
ctx.effect(() => locale.register(NS, 'en', en), 'dsh-disk-cleaner: locale en');
|
|
1426
1527
|
}
|
|
1427
|
-
ctx.effect(() =>
|
|
1528
|
+
ctx.effect(() => {
|
|
1529
|
+
injectBaseline(document, PANEL_ID);
|
|
1530
|
+
return insertStyles(CSS);
|
|
1531
|
+
}, 'dsh-disk-cleaner: styles');
|
|
1428
1532
|
|
|
1429
1533
|
const label = () => {
|
|
1430
1534
|
if (!locale) return 'Disk Cleaner';
|
package/lib/index.js
CHANGED
|
@@ -164,6 +164,8 @@ async function run(action, payload) {
|
|
|
164
164
|
// it a folder holding 261 MB reads as "0.2 MB" and looks like nothing was found.
|
|
165
165
|
totalBytes: target.totalBytes,
|
|
166
166
|
totalFiles: target.totalFiles,
|
|
167
|
+
// Set when the walk hit its cap, so a caller can say the totals are a lower bound.
|
|
168
|
+
truncated: target.truncated === true,
|
|
167
169
|
})),
|
|
168
170
|
};
|
|
169
171
|
}
|
|
@@ -211,6 +213,7 @@ const NOTE_MEANING = {
|
|
|
211
213
|
duplicate: 'a byte-identical copy of a directory that is still present',
|
|
212
214
|
'database-backup': 'a copy of a database file taken before an upgrade; the live database is beside it',
|
|
213
215
|
orphan: 'the QQGuild data folder is deleted in full when no file in it has been written for 30 days',
|
|
216
|
+
irreversible: 'the recycle bin: everything in it was already deleted once, and emptying it cannot be undone',
|
|
214
217
|
};
|
|
215
218
|
|
|
216
219
|
const formatBytes = (bytes) => {
|
package/package.json
CHANGED
package/smoke.mjs
CHANGED
|
@@ -51,6 +51,9 @@ const window = {
|
|
|
51
51
|
|
|
52
52
|
const document = {
|
|
53
53
|
head,
|
|
54
|
+
// injectBaseline() de-duplicates through querySelector; the throwaway DOM has no
|
|
55
|
+
// stylesheet registry, so it simply reports "not injected yet".
|
|
56
|
+
querySelector: () => null,
|
|
54
57
|
createElement: () => ({ dataset: {}, setAttribute() {}, set textContent(v) {} }),
|
|
55
58
|
};
|
|
56
59
|
|
|
@@ -350,5 +353,87 @@ check('audit imports measuring only, never the deleter', Boolean(auditImport) &&
|
|
|
350
353
|
const pkg = JSON.parse(read('package.json'));
|
|
351
354
|
check('package lists every runtime file', ['lib/index.js', 'lib/client.js', 'lib/catalog.js', 'lib/audit.js', 'lib/plan.js'].every((f) => pkg.files.includes(f)));
|
|
352
355
|
|
|
356
|
+
// --- 1.6.3: the delete-safety regressions ------------------------------------------------
|
|
357
|
+
check(
|
|
358
|
+
'the recycle bin is never filed as a safe, preselected target',
|
|
359
|
+
/id: 'recycle-bin',[\s\S]{0,200}?risk: 'caution'/.test(catalog) &&
|
|
360
|
+
!/id: 'recycle-bin',[\s\S]{0,200}?risk: 'safe'/.test(catalog) &&
|
|
361
|
+
/const IRREVERSIBLE = new Set\(\['recycle-bin'\]\)/.test(source),
|
|
362
|
+
'emptying the recycle bin cannot be undone, so it must not sit under "safe" or in "select all"',
|
|
363
|
+
);
|
|
364
|
+
check(
|
|
365
|
+
'no target skips the boundary check by naming no root',
|
|
366
|
+
/const roots = declared\.length > 0 \? declared : \[path\.parse/.test(catalog) &&
|
|
367
|
+
catalog
|
|
368
|
+
.split(/\n \{\n/)
|
|
369
|
+
.filter((block) => /roots: \[\]/.test(block))
|
|
370
|
+
.every((block) => /id: 'recycle-bin'/.test(block)),
|
|
371
|
+
'an empty roots list used to disable `within` entirely; only the recycle bin, which spans volumes, may declare none',
|
|
372
|
+
);
|
|
373
|
+
check(
|
|
374
|
+
'preselection and select-all both exclude irreversible targets',
|
|
375
|
+
/!IRREVERSIBLE\.has\(row\.id\)/.test(source) &&
|
|
376
|
+
(source.match(/!IRREVERSIBLE\.has\(row\.id\)/g) ?? []).length >= 2,
|
|
377
|
+
);
|
|
378
|
+
check(
|
|
379
|
+
'every confirm box names the targets and the total',
|
|
380
|
+
(source.match(/t\('cleanConfirmList'\)/g) ?? []).length === 2 &&
|
|
381
|
+
(source.match(/t\('cleanConfirmIrreversible'\)/g) ?? []).length === 2 &&
|
|
382
|
+
!/window\.confirm\(t\('cleanConfirm'\)\)/.test(source),
|
|
383
|
+
'a bare "delete these?" is not a confirmation, and the plan step is still a delete',
|
|
384
|
+
);
|
|
385
|
+
check(
|
|
386
|
+
'a clean that left files behind is not reported as a green success',
|
|
387
|
+
/const kind = failed > 0 \? 'warn' : 'ok'/.test(source) && /\.dsc-notice\.dsc-warn/.test(source),
|
|
388
|
+
);
|
|
389
|
+
check(
|
|
390
|
+
'a truncated walk says so instead of claiming exact totals',
|
|
391
|
+
catalog.includes('truncated: stack.length > 0') &&
|
|
392
|
+
catalog.includes('truncated = truncated || size.truncated === true') &&
|
|
393
|
+
index.includes('truncated: target.truncated === true') &&
|
|
394
|
+
source.includes("t('truncatedHint')"),
|
|
395
|
+
);
|
|
396
|
+
check(
|
|
397
|
+
'measure and purge count entries in the same unit',
|
|
398
|
+
/let seen = 0;\n while \(stack\.length > 0 && seen < MAX_ENTRIES\)/.test(catalog) && /seen \+= 1;/.test(catalog),
|
|
399
|
+
);
|
|
400
|
+
check(
|
|
401
|
+
'a refused directory removal is counted, a legitimately full one is not',
|
|
402
|
+
/code === 'ENOTEMPTY' \|\| code === 'EEXIST'/.test(catalog) && /blocked \+= 1/.test(catalog),
|
|
403
|
+
);
|
|
404
|
+
check(
|
|
405
|
+
'the dry run applies the same too-broad guard as the real run',
|
|
406
|
+
(catalog.match(/if \(tooBroad\(abs\)\) \{/g) ?? []).length === 2,
|
|
407
|
+
'an estimate that skips the guard promises bytes the delete will refuse',
|
|
408
|
+
);
|
|
409
|
+
check(
|
|
410
|
+
'the orphan target is named for what it actually measures',
|
|
411
|
+
!/已卸载程序的残留数据|Leftover data from an uninstalled app|已卸载程序残留|Data from uninstalled apps/.test(source) &&
|
|
412
|
+
source.includes('QQ') ,
|
|
413
|
+
'it only ever measures the QQGuild folder',
|
|
414
|
+
);
|
|
415
|
+
check('the version badge and package agree', pkg.version === '1.6.4');
|
|
416
|
+
|
|
417
|
+
check(
|
|
418
|
+
'the panel carries dsh-design-baseline (full mode: css inlined, root tagged, tokens only)',
|
|
419
|
+
/>>> dsh-design-baseline v/.test(source) &&
|
|
420
|
+
/const BASELINE_CSS = "/.test(source) &&
|
|
421
|
+
/const injectBaseline = /.test(source) &&
|
|
422
|
+
/className: 'dsc-root dsb-root'/.test(source) &&
|
|
423
|
+
/injectBaseline\(document, PANEL_ID\)/.test(source),
|
|
424
|
+
'baseline stylesheet, root class and injector must all be present',
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
check(
|
|
428
|
+
'no colour reaches for a raw host token outside the baseline block',
|
|
429
|
+
(() => {
|
|
430
|
+
const from = source.indexOf('// >>> dsh-design-baseline');
|
|
431
|
+
const to = source.indexOf('// <<< dsh-design-baseline');
|
|
432
|
+
if (from < 0 || to < from) return false;
|
|
433
|
+
return !/--dsw-alias-[a-z0-9-]+/.test(source.slice(0, from) + source.slice(to));
|
|
434
|
+
})(),
|
|
435
|
+
'panel css must go through --dsb-* so the host theme is followed',
|
|
436
|
+
);
|
|
437
|
+
|
|
353
438
|
console.log(`\n${pass} passed, ${fail} failed`);
|
|
354
439
|
if (fail > 0) process.exit(1);
|