Skip to content

dsh-windows-c-cleanup

Verified

dsh-windows-c-cleanup · v0.5.2 · MIT · Web UI

DSH 插件:Windows 系统盘清理能力 —— 扫描 → 五级分级(可安全删除/谨慎删除/建议迁移/保护名单/长期防护)→ 用户确认 → 分层执行与迁移

Install

dsh plugin add dsh-windows-c-cleanup

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Tags

Readme

dsh-windows-c-cleanup

English中文

Repository: https://github.com/runcat-tommy/dsh-windows-c-cleanup (npm: dsh-windows-c-cleanup)

A DeepSeek Harness (DSH) plugin that adds Windows system-drive (C:) cleanup as an auditable pipeline:

Scan → five-tier grading → visual report → user selection → tiered execution / migration to another drive

Why

A full C: drive is rarely caused by "junk files". It is usually caused by things that belong on another drive: package-manager caches, IDE indexes, on-device browser AI models, leftover installers, virtual disks.

Deleting those blindly is risky: rebuilding an IDE index takes hours, and removing a virtual disk destroys an entire Linux environment. This plugin instead:

  1. Measures before deleting — every candidate is sized against an externalized rule library, and a visual Markdown report is produced first;
  2. Grades into five tiers — each candidate carries a human-readable reason;
  3. Treats the protection list as a hard constraint — anything not in the rule library is treated as protected ("unknown means don't delete");
  4. Prefers migration over deletion — with a second drive present, caches are moved via junctions or app config so they never come back.

Five tiers

Tier Meaning Execution policy
🟢 Safe to delete Caches, temp files, leftover installers — no data loss Batch after a single confirmation
🟡 Delete with care Rebuildable but expensive (IDE indexes, on-device AI models, package repos), or admin-only (WinSxS via DISM) Confirm per item
🟠 Consider migrating Moves to another drive transparently via junction or app config Migrate + link + ledger, rollback supported
🔴 Protected User documents, credentials, chat data, virtual disks, IDE config, unknown paths Never deleted automatically (hard constraint, not overridable by user rules)
🔵 Long-term prevention Config actions: cache redirection, Storage Sense, virtual-disk location audit Do once, clean less forever

Install

dsh plugin is a thin pnpm forwarder: it installs the package into the named profile and then registers it in dsh.profile.bundles based on the actual installed state (this package declares dsh.bundle.patch, so it joins the layer stack). Pick any of the three routes below — all of them require a DSH restart to take effect.

Option 1: install straight from GitHub

dsh plugin --profile web add github:runcat-tommy/dsh-windows-c-cleanup
  • Best for: grabbing the newest commit on the repo when you already have git installed.
  • Note: lib/ and client/client.js are build artifacts and are not committed (they are produced by the prepare script), so this route runs a build and asks for your approval; dsh plugin prints the approval prompt.
  • Updating: run the same command again (pnpm re-fetches the latest commit of that git source).

Option 2: download from GitHub, then install from the local folder

# Step 1: get the code (git clone, or Code → Download ZIP and unzip)
git clone https://github.com/runcat-tommy/dsh-windows-c-cleanup.git D:\dsh-plugins\dsh-windows-c-cleanup

# Step 2: install from the local folder
dsh plugin --profile web add D:\dsh-plugins\dsh-windows-c-cleanup
  • Best for: reading the code before installing, making your own changes, or when the network is unreliable (download the ZIP instead of using git).
  • Note: a local folder is installed as a link:, so your edits apply immediately — host-side changes need a dsh web restart; client-only UI changes need npm run build:client plus a page refresh.
  • Updating: git pull in that folder, run npm run build, then restart dsh web.

Option 3: install from npm (recommended)

dsh plugin --profile web add dsh-windows-c-cleanup

# pin a version when you want to
dsh plugin --profile web add [email protected]
  • Best for: everyday use, and the least effort. The npm package already ships the build artifacts, so no local compiler, no git and no build approval are involved.
  • Updating: dsh plugin --profile web up dsh-windows-c-cleanup
  • Uninstalling (same for all three routes): dsh plugin --profile web remove dsh-windows-c-cleanup
  • Listing what is installed: dsh plugin --profile web list

Which route?

Command What it needs What you get Updating
1. Straight from GitHub add github:runcat-tommy/dsh-windows-c-cleanup git installed; one build approval the newest commit (built at install time) run the same command again
2. Download to a folder add D:\dsh-plugins\dsh-windows-c-cleanup nothing (a ZIP works too) the local folder (link:, edits apply at once) git pull + npm run build + restart
3. npm add dsh-windows-c-cleanup nothing the published release (artifacts included) up dsh-windows-c-cleanup

In one line: npm is the least effort, GitHub-straight is the quickest (but needs a build approval), local-folder is the most flexible (edits show up immediately).

Restart after installing

All three routes are the same here: the plugin is only loaded after DSH restarts.

dsh web

Then open any session — the "Disk cleanup" tab appears at the top of the conversation view.

For development you can skip installation and mount an overlay instead (on Windows the path must be a file:// URL):

node -e "const{pathToFileURL}=require('node:url');console.log(pathToFileURL(process.argv[1]).href)" "$PWD\src\index.ts"
dsh web --patch .\dev.cordis.yml

Usage

The plugin registers one tool, disk_cleanup:

Parameter Value Description
action scan | plan | apply | migrate | rollback | trash Required. scan/plan are read-only; apply/trash execute cleanup (M2); migrate/rollback move and restore data (M3)
scope hotspots | full hotspots measures rule-library hotspots only (fast); full adds a whole-drive Top-N pass (default)
reportPath path Where the report is written; defaults to <cwd>/C盘清理报告-<timestamp>.md
format markdown | json | both Report format; defaults to the defaultReportFormat config. json writes a machine-readable report (passing x.md also writes a sibling x.json)
items paths Concrete paths to clean (required for the caution tier: only per-item user-confirmed paths)
grade safe | caution | migrate Select by tier: safe may be batched, caution must also pass items, migrate drives action=migrate
mode permanent | trash trash moves items to the staging area for recovery (default); permanent deletes outright
trashPath path Staging root, must be on another drive (same-volume staging frees nothing); defaults to <largest non-system drive>:\to_delete
migrationRoot path Migration root; the ledger.jsonl ledger lives alongside it. Defaults to <largest non-system drive>:\dsh-cc-migrated
historyPath path Scan history (the trend data source). Defaults to <DSH_HOME>\windows-c-cleanup\history.jsonl, deliberately outside the caches being cleaned
defaultReportFormat markdown Default report format: markdown / json / both
schedule.enabled false Enable scheduled scans (off by default — it needs your explicit consent)
schedule.intervalHours 24 Scan interval in hours
schedule.alertFreePercent 10 Write an alert when free space drops below this percentage
schedule.initialDelayMinutes 1 First-run delay so startup I/O is not contended
schedule.scope hotspots Scheduled scan scope (faster and lighter than full)
dryRun boolean Defaults to true: lists the actions without deleting anything; pass false only after the user confirms
elevation none | dism | cleanmgr | dism+cleanmgr Whether to also run admin-level system cleanup (triggers UAC)
targetDrive e.g. D: Migration target; defaults to the non-system drive with the most free space
extraRulesFile path Extra user rule file layered on top of the built-in library

Example prompt:

Show me what is eating my C: drive

The tool returns drive info, reclaimable bytes per tier, the biggest top-N consumers, and a migration target suggestion — plus a Markdown report containing: summary → 🟥 biggest consumers → 🟢/🟡/🟠/🔴/🔵 tiers → execution results.

Cleanup panel (M5)

The plugin registers a conversation view tab in the Web GUI (conversation.view, an additive list slot — it never replaces or destroys any existing surface). Open any session, switch to "Disk cleanup", and go from "understand" to "select → preview → execute" without leaving the page.

Real screenshot (English UI, matching this document's language; the Chinese UI is in README.md):

English UI
Disk cleanup panel, English UI

The panel is deliberately thin: it makes no judgement of its own. Tiering, the safety gate, measurement and freed-bytes accounting all reuse the existing host modules, and the panel's preview and real run call the same executeCleanup (only dryRun differs) — so what you preview is exactly what would happen.

Module map (every block carries its name as a label)

Top to bottom, each block has a small fixed-name label in its top-left corner (one set per language, guarded by checks 10.1–10.6). To point at a block, just use its name:

Module Where What is inside
Overview the header, top of the panel drive free/total and used percentage; two chips on the right: migration target (letter + free space) and the scheduled-scan switch
Controls (scan → select → preview) under the header three tight groups with right-pointing arrows between them: ① Scope + Scan C: ② "n selected" + Clear selection ③ Delete mode + Preview + ?. There is no "Confirm and run" here — the run entry point lives only in "Preview result"
Preview result directly under the controls (appears after clicking Preview) what would happen to each item (move to staging / permanent delete / needs elevation / refused), the total that would be freed, and the refusal list; the panel's only "Confirm and run" button sits here, next to "Preview again (dry run)". The dynamic text beside the title is what this particular preview worked out
Status & notices under the preview result what is running now (⏳), the result notice of the last action, errors, and the warning shown when the scan hit its time budget
Cleanup candidates (🟢/🟡/🟠/🔴) the four cards in the middle Safe to delete / Delete with care / Better migrated / Protected list, each item with path, size and reason; protected entries cannot be selected
Long-term protections under the cards (collapsible) the 6 long-term measures where changing settings or habits beats repeated cleanup
Run progress appears after clicking Confirm and run (under the cards) job id and status, progress bar (from the host's per-item accounting), item-by-item detail, freed bytes, cancel button
Migration preview appears after clicking a card's "Preview migration" source → destination mapping, file count, whether the target drive has room, and app config you must change yourself; the migrate buttons sit at the bottom
Records & artifacts the very bottom of the panel absolute paths of the history ledger and the report directory (reports are written to disk and stay in Chinese)

Two things are worth noting:

  • "Preview result" is the only dynamic block that moved up to sit right under the controls (the other dynamic blocks stay below the candidate cards). So after clicking Preview, the result and its run button appear directly beneath the buttons you just clicked — no scrolling past four columns of cards; when there is no preview, that spot goes to "Status & notices", so a finished scan reports right under the controls too.
  • There is exactly one run entry point: "Confirm and run" exists only inside "Preview result". The toolbar no longer offers one, so "run without previewing" has no entry point in the UI at all (the host-side guardTargets and dryRun contract is unchanged and remains the real defence). Changing the selection or delete mode after a preview voids it, the whole block disappears and you preview again; with permanent deletion and no confirmation tick the button is disabled and the reason is written next to it (checks 8.1–8.9).
  • On request, the former "Trend" module (time since the previous scan, directories that grew back) was removed from the UI; the host still returns state.trend, so it can be wired back whenever you want it.

The toolbar is laid out as three tight groups with a right-pointing arrow after the first two, left to right:

Group Controls Purpose
① Scan Scope select + Scan C: Pick hotspots/full, then scan (shows "Rescan" once a scan exists) →
② Selection <n> selected + Clear selection Live count and one-click reset →
③ Preview Delete mode select + Preview + ? The ? help button sits right next to "Preview"; the run button is not here (see "Preview result")

The arrows are purely decorative (aria-hidden, skipped by screen readers) and carry 12px of margin on each side on top of the group's 6px gap, so the flow reads clearly as "scan first → then select → finally preview".

Visual affordance (deliberately tuned, not the default look):

  • The three action buttons "Scan C:", "Clear selection" and "Preview" get a thick brand-coloured outline, a tinted fill, a drop shadow and semi-bold text while they are enabled, so they read as buttons at a glance. While disabled they keep the original flat look (pale background, grey border, translucent) — a button you cannot press must never be drawn as if you could;

  • "Confirm and run" (inside "Preview result") stays solid brand-coloured: solid means the final action, outlined means an ordinary clickable action, so the hierarchy never blurs;

  • The ? next to "Preview" is a 24px round button (2px brand outline, tinted fill, bold question mark, grows on hover). Opening it explains in place, in deliberately plain words: the first line is "Clicking Preview runs a dry run — nothing is deleted", then three bullet points (it really checks what may be deleted / it really counts permissions and space / it says what would happen to each item), and only then two caveats (the numbers are estimates, and locked files are invisible to a preview).

  • Five-tier cards (🟢 safe / 🟡 caution / 🟠 migrate / 🔴 protected) with path, size and the reason for each verdict; protected entries cannot be selected;

  • Two-step execution: there is exactly one run entry point, inside "Preview result" — so a preview is a precondition, not a suggestion. Any change to the selection or mode voids the preview, the block disappears and you preview again. The only remaining gate is "permanent deletion without the confirmation tick", and the reason is written right next to the button (a tooltip on a disabled button never appears in most browsers, so a tooltip alone explains nothing) — decided by the pure runGate() function, pinned by checks 8.1–8.9;

  • Real progress from the host's per-item accounting callback (not parsed log text), with a working "cancel job";

  • Migration preview showing source → destination, file count and whether the target volume has room; app config changes are only suggested, never applied silently.

Switching to the chat view and back does not lose work in progress

The panel lives inside the conversation view, so switching away unmounts its component (and with it the local React state). Anything "currently happening" is therefore reclaimed from the host as the source of truth, never remembered locally:

What you switched away from The host's authoritative state What you see on return
A running "Scan C:" panelState().scan = {running, scope, startedAt} (the scan runs host-side, so unmounting changes nothing) The "Scanning C:" ⏳ comes right back, and the panel asks the host every 1.2 s
A scan that finished while you were away The result in the host cache (scan-view) The candidate cards appear immediately, plus a "Scan complete" notice — no need to scan again
A real cleanup / migration in progress panelState().runningJobIds The panel reattaches to the job by id, so the progress bar and per-item detail keep going

The host also scans only one at a time: if you click "Scan" again after coming back, it joins the scan already running (saving a full disk walk and keeping two results from overwriting each other; checks 12.3–12.5).

One honest limitation: the selection and the preview are not preserved across unmounts (changing the selection or mode invalidates a preview anyway, so on return the flow is "select again → preview again"). The scan and runningJobIds fields are new in 0.5.1; an older host that has not been restarted (dsh web) lacks them, and the panel degrades to its previous behaviour without erroring (check 11.6).

Bilingual UI (Chinese / English)

Both the panel and the host text exist as two complete sets and follow the DSH locale automatically — there is no language switcher to click:

  • UI strings live in the client dictionary (client/src/i18n.ts, 120 keys per language, key sets identical). The tab label is a thunk, so switching the locale retitles the tab without re-registering anything;
  • Every panel call on the host RPC carries the current locale, so host-generated text switches too: every rule reason, all 12 safety-gate refusal sentences, the executor's per-item planned actions, migration config hints, and the scheduler status. English strings live in src/rules/default-rules.en.json, aligned with Chinese strictly by rule id (101 rules + 6 long-term actions, guarded by tests that check coverage and that no Han character leaks into the English file);
  • Switching the locale never makes you rescan (the scan-view endpoint): rule reasons and truncation warnings are strings the host rendered at scan time, so they would normally stay in the old language. The host therefore exposes "re-render the cached scan in the target language" (no disk access, no measurements — measured at 0 ms), and the panel calls it whenever the locale changes, re-running the preview and migration preview alongside it so the UI is never half-translated (a real screenshot exposed exactly this);
  • Notices store a key + params, not a rendered string (Notice in client/src/panel.tsx): the text is produced at render time with the current t, so "Scan complete…" / "Preview ready…" follow the locale instead of freezing in the language they were created in (the type also forbids passing a plain string);
  • Chinese is the default: the model tool path (disk_cleanup) never passes locale, so tool output is byte-for-byte what it always was; only the Web panel sends en;
  • A missing English entry always falls back to the Chinese original — never to an empty string;
  • Wiring has an ordering requirement: dictionaries must be registered before the framework renders a registration that declares locale:, so the client plugin waits for the locale service with ctx.inject(['locale']). dsh.client.inject therefore lists @deepseek-ai/dsh-client-locale too — that is a package-metadata change, so the first upgrade needs a dsh web restart; after that, text changes only need a page refresh.

Browser and host talk over the generic Connection RPC channel /dsh-c-cleanup (authority: loopback, local callers only) with endpoints state / scan / scan-view / preview / execute / migrate / progress / cancel / history and friends. The job table lives in host memory and is cleared on host restart.

The state endpoint is the panel's "remount entry point": besides drives and scheduler state it truthfully reports whether a scan is running right now (scan) and which jobs are running (runningJobIds) — that is how the panel reattaches its UI after you switch views and come back (see the previous section). The scan and the jobs themselves run host-side and are unaffected by the component unmounting.

What it takes to go live (platform mechanics, not a plugin choice):

Change What you must do
Add/remove a plugin package, or change dsh.client fields Restart dsh web (package metadata verdicts are cached forever)
Change only the contents of client/client.js Refresh the page (bundles are served no-cache; HMR is disabled in this profile)

Configuration

Override the row in your profile's cordis.patch.yml (a patch replaces the whole config, it does not deep-merge):

- id: windows-c-cleanup
  name: 'dsh-windows-c-cleanup'
  config:
    reportDir: 'D:\reports'
    defaultScope: full
    hotspotTimeBudgetMs: 60000
    topTreeTimeBudgetMs: 45000
    topTreeMaxDepth: 3
    bigItemThresholdBytes: 2147483648
    allowProtectedOverride: false
    extraRulesFile: 'D:\my-rules.json'
Option Default Description
reportDir current working directory Report output directory
defaultScope full Default scan scope
hotspotTimeBudgetMs 70000 Hotspot time budget (truncates and flags partial)
topTreeTimeBudgetMs 70000 Whole-drive Top-N time budget (both scans run in parallel, so wall time tracks the larger one)
topTreeMaxDepth 3 Top-N traversal depth
bigItemThresholdBytes 2 GiB "Big item" threshold
allowProtectedOverride false Whether user rules may override the protection list
extraRulesFile none Extra user rule file

Rule library

The built-in library (100+ rules) lives in src/rules/default-rules.json. Every path is written with placeholders (%LOCALAPPDATA%, %APPDATA%, %USERPROFILE%, %WINDIR%, …) — no hardcoded user names, so it is portable across machines.

{
  "id": "npm-cache-local",
  "path": "%LOCALAPPDATA%\\npm-cache",
  "grade": "migrate",
  "reason": "npm package cache, often several GB; better migrated to another drive",
  "migrate": {
    "method": "app-config",
    "targetHint": "<other drive>:\\npm-cache",
    "configHint": "npm config set cache \"<target>\""
  }
}

Matching: a candidate hits a rule when it equals the rule path or sits beneath it; * matches a single path segment (e.g. %LOCALAPPDATA%\*-updater). Priority: the most specific path wins; on ties protected wins; no match at all → treated as protected.

Custom rules use the same shape:

{ "rules": [{ "id": "my-cache", "path": "%LOCALAPPDATA%\\my-app\\cache", "grade": "safe", "reason": "self-built app cache" }] }

Attempts to override a overridable: false protected entry are rejected with a warning (unless allowProtectedOverride is explicitly enabled).

Safety

  • Protection list as a hard constraint: user documents, desktop, downloads, SSH/cloud credentials, .dsh config, chat data (WeCom/Lark/WeChat), IDE config, pagefile.sys / hiberfil.sys / virtual disks, Program Files, Windows — never deleted automatically.
  • Unknown means protected: paths absent from the rule library are never deleted.
  • Tiered confirmation: the safe tier can be confirmed once; the caution tier is confirmed item by item; the protection tier has no execution entry point at all.
  • Time budgets: a truncated scan is explicitly flagged as partial in both the report and the tool result — never a silently incomplete verdict.
  • No link following: junctions and symlinks are never followed, avoiding double counting and recursion traps.
  • Reversible migration: migrations use junctions or app config and are recorded in a ledger (rollback from M3).

Status (M1 + M2 + M3 + M4 + M5)

  • Rule library (100+ rules, placeholder-based) + long-term prevention list
  • Scanning: drive info, hotspot list, whole-drive Top-N, junction-safe measurement, time budgets
  • Five-tier grading with "unknown means protected"
  • Visual Markdown report
  • DSH tool registration (disk_cleanup, parameter/output schemas validated)
  • M2 Execution: deletion (batch for the safe tier, per-item for caution), staging area with ledger, UAC elevation (Windows\Temp / WinSxS / DISM / cleanmgr), execution report, dry run by default
  • M3 Migration: junction moves (transparent to applications), JSONL ledger and rollback, refusal and rollback on same-volume / existing-destination / insufficient-space / busy-source, app-config suggestions
  • M4 Polish: scan history with trend comparison, machine-readable JSON reports, scheduled scans with alerts (cleanmgr/DISM elevation already shipped in M2)
  • M5 Client GUI panel: conversation.view five-tier cards, selection, two-step execution (preview → confirm), per-item progress with cancel, migration preview
  • M6 Release: npm + community marketplace (GitHub done)

Known limitations (M1 + M2 + M3 + M4 + M5)

  • Execution requires explicit authorization: apply / trash dry-run by default; a real run should follow a scan the user has reviewed. migrate / rollback still return not-implemented (M3).
  • Admin-level cleanup depends on the UAC prompt: DSH's permission stack has no UAC primitive, so the plugin spawns the system prompt via Start-Process -Verb RunAs (the script lands in %TEMP%\dsh-cc-elevated-*.ps1). If the user declines, Windows\Temp, SoftwareDistribution, and WinSxS cannot be cleaned and the result is explicitly marked as cancelled.
  • Files locked by running apps cannot be deleted: an open browser, IDE, or chat client locks its own caches; those items are reported as "partial" with the residual size. That is normal Windows behaviour, not a plugin failure.
  • Whole-drive Top-N coverage is I/O bound: Node file operations go through the libuv thread pool (4 threads by default) and directory measurement cost is essentially driven by file count, so raising concurrency does not help. Within the 70-second budget, the hotspot rules reach 100% coverage on a 153 GB used drive, but the whole-drive Top-N pass covers only part of the tree (in practice it truncates after roughly 160 top/shallow-level directories). Truncation is always flagged as partial with a reason in both the report and the tool result — never disguised as a complete verdict. Raise topTreeTimeBudgetMs for more coverage at the cost of waiting longer. Actionable findings come from the hotspot rules; Top-N is a safety net, so truncation does not affect the usability of the five-tier list.
  • Two released-space metrics: for small cleanups (tens of MB) the drive free-space delta can read 0 because other processes keep writing to the drive; the per-item measured total is authoritative there, and both are reported side by side.
  • No interactive confirmation UI yet in the tool path: the model hands the report to the user, and the user's selection feeds the execution path. The M5 panel provides the interactive route (preview → confirm → progress).
  • Panel availability is a platform matter: adding/removing a package or changing dsh.client requires a dsh web restart (metadata verdicts are cached forever); changing only the bundle needs a page refresh (HMR is off in this profile).
  • The panel shares the tool's judgement but not its configuration surface: it can scan, preview, execute, migrate and roll back, while settings such as schedule.enabled or reportDir stay in cordis.yml — the panel reports state, it does not persist settings.
  • Panel jobs live in host memory: after a host restart the panel shows "job vanished with the host restart" and any running job stops (reports and the migration ledger already on disk are unaffected).

Development

npm install --legacy-peer-deps   # DSH type packages have peer conflicts; local install uses legacy resolution
npm run deps:link                 # link the host's own @deepseek-ai/* copies (see note below)
npm run typecheck                 # type check
npm run smoke                     # fast self-check: rule matching / drive info / budgeted measurement
npm run m2                        # isolated M2 execution tests (sandboxed under %TEMP%)
npm run m3                        # isolated M3 migration tests (sandbox + D:\dsh-cc-m3-test)
npm run m4                        # M4 history/trend/JSON/scheduling semantics (fake scans, seconds; includes one real hotspot scan)
npm run m4:live                   # M4 scheduled scan end to end (real scan, ~1 minute; history numbers checked against fs.statfs)
npm run m5                        # M5 panel host half: tiering, preview == execute, real staging area, real migration, cancel, RPC endpoints
npm run m5:client                 # M5 client bundle contract: replay the browser module load and really render the panel once (offline, seconds)
npm run m5:live                   # M5 live check: ask the running dsh web for its boot manifest and the served bundle (hash-compared with the local build)
npm run market:check              # Release health check: pins the hard requirements for automatic DSH marketplace collection (offline, seconds)
npx tsx tests/tool-run.ts full     # headless full scan producing a real report
npm run build                      # compile to lib/ and bundle client/client.js (publishable artifacts)
npm run build:client               # re-bundle the client half only (after editing client/src)

⚠️ @deepseek-ai/dsh-tools and friends declare their runtime dependencies as peerDependencies. npm install --legacy-peer-deps therefore never installs them (tests die with ERR_MODULE_NOT_FOUND: Cannot find package '@deepseek-ai/dsh-xxx'), and a plain npm install prunes them away again. npm run deps:link junctions the host's own copies into node_modules, so tests run against exactly the packages the host loads. It is idempotent; re-run it after any install. Always re-verify npm run build after touching dependencies.

License

MIT