Skip to content
dsh.fish
Bundle

dsh-bundle-dedup-guard

DSH plugin guard: on every plugin load checks profile bundle lists for duplicate loader entries (aggregate bundle + sub-plugins listed together → 'duplicate loader entry id' boot crash) and runs site-level health checks — known conflict pairs, vendor @deepseek-ai tree integrity vs lockfile, plugin @deepseek-ai junction integrity — guarding the 2026-08-18 and 2026-08-21 incidents. Read-only diagnostics.

Source
Lstalu
stars
2 stars
License
MIT
Updated
Updated 4 days ago

Readme

# dsh-bundle-dedup-guard

[**English**](README.md) | [**简体中文**](README.zh-CN.md)

[![CI](https://github.com/Lstalu/dsh-bundle-dedup-guard/actions/workflows/ci.yml/badge.svg)](https://github.com/Lstalu/dsh-bundle-dedup-guard/actions/workflows/ci.yml)
[![npm version](https://img.shields.io/npm/v/dsh-bundle-dedup-guard)](https://www.npmjs.com/package/dsh-bundle-dedup-guard)
[![node](https://img.shields.io/badge/node-%3E%3D22.13-green)]()
[![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)

**A DeepSeek Harness plugin that catches duplicate loader entry ids in profile bundle lists — and audits the whole plugin environment (vendor tree, plugin junctions, known conflict pairs) — on every plugin load.**

If a profile's `dsh.profile.bundles` lists an **aggregate bundle** (a bundle whose patch inserts all of its sub-plugins, e.g. `@linxin666/dsh-web-ui-all`) **and its sub-plugins individually**, the loader receives the same loader-entry `id` twice. `EntryGroup.update` throws `duplicate loader entry id: <id>` before any plugin starts, and the whole profile fails to boot. This plugin exists so that never happens silently again.

---

## Why this exists

**Incident, 2026-08-18:** a web profile listed `@linxin666/dsh-web-ui-all` (which aggregates 13 sub-plugins into one patch) *and* all 13 sub-plugins separately. Every sub-plugin id was inserted twice; the first collision reported was `duplicate loader entry id: ui-dsh-aionui-panel`. Fixing only the `bundles` list was not enough — the `dsh plugin` command's `reconcilePlugins` re-appends **every `dependencies` entry that declares `dsh.bundle`** to the bundle list after each pnpm operation, so the sub-plugins came back an hour later and crashed the next boot.

**Incident, 2026-08-21:** a bulk plugin update wiped plugin `node_modules` (8 plugins failed to boot with `Cannot find package`); a repair `npm install` followed an `@deepseek-ai` junction into the vendor tree and corrupted it; and once dependencies were restored, `dsh-better-sidebar` ended up mounted twice (`/sidebar/api` duplicate-route crash). v0.2.0 turns these red lines into automatic site-level checks — see [Site-level health checks](#site-level-health-checks-v020) below.

Full incident records: [docs/KNOWN-ISSUE-bundle-duplicate.md](docs/KNOWN-ISSUE-bundle-duplicate.md) (2026-08-18) and the 2026-08-21 self-repair manual surfaced by the site audit.

## How it works

The loader's failure path is: `cordis-plugin-include`'s `applyEntryPatches` **flattens** every bundle's insert entries without deduplicating, then `cordis-plugin-loader`'s `EntryGroup.update` dedups by id and **throws on the first duplicate** — before any plugin entry is created. This plugin re-implements exactly that "flatten + dedup by id" semantics in pure Node, and reports the offending ids, their sources (which bundle/patch inserted each), and the fix.

### Checks on every plugin load

| Trigger | When | Notes |
| --- | --- | --- |
| Apply | every boot | instant health check as the plugin mounts |
| Loader events | `loader/entry-init` / `loader/partial-dispose` | runtime hot loads / plugin additions, debounced 800 ms |
| Manifest watch | `fs.watch` on the profile dir | the moment `package.json` or `cordis.patch.yml` changes — i.e. `dsh plugin add`, marketplace installs, or hand edits — warn immediately, before the next restart |

### What it reports

- **Duplicate loader entry ids** — each id inserted by more than one source, with the full source chain (e.g. `ui-dsh-aionui-panel: @linxin666/dsh-web-ui-all ← @linxin666/dsh-client-ui-aionui-panel`).
- **Unresolved bundles** — listed in `bundles` but not resolvable (the loader would loud-fail too).
- **Bundle-less packages** — listed but without a `dsh.bundle.patch` (a misconfiguration per the loader contract).
- **Predictive reconcile warning** — a `dependencies` entry that declares `dsh.bundle` but is *not* in `bundles`. `dsh plugin`'s reconcile will append it on the next install/update; if it's a sub-plugin covered by an aggregate, that re-creates the crash. The warning names the covered ids. Fix: move such packages to `devDependencies` (reconcile only reads `dependencies`).

Reports are written to `$DSH_HOME/dsh-bundle-dedup-guard/reports/<profile>-<timestamp>.json` and `<profile>.latest.json`.

### Site-level health checks (v0.2.0)

Beyond the bundle-list checks, every run also audits the whole plugin environment,
turning the 2026-08-21 incident's red lines into automatic checks (`lib/site-health.mjs`,
zero-dependency, read-only):

| Check | Detects | Incident |
| --- | --- | --- |
| Known conflict pairs | `bundles` lists `@linxin666/dsh-web-ui-all` **and** `dsh-better-sidebar` together — both entries execute the same `lib/index.js` and register the same `/sidebar/api` route (`duplicate prefix route` crash at apply time) | 2026-08-21 |
| Vendor tree integrity | every `@deepseek-ai/*` package vs `resources/vendor/dsh/node_modules/.package-lock.json`: missing / empty dir / `package.json` `name` mismatch (wrong content installed) / version mismatch; `.name-*` temp-dir leftovers (informational) | 2026-08-21 |
| Plugin junction integrity | plugins whose runtime code imports `@deepseek-ai/*` but whose `package/node_modules/@deepseek-ai` junction is missing (the exact `Cannot find package` boot crash), points at the wrong target, or dangles; real-dir copies (works, informational) and `.npmbak` leftovers (an `npm install` ran inside a junction dir) | 2026-08-21 |
| Incident manual pointer | surfaces the latest `$DSH_HOME/incidents/<date>/README.md` self-repair manual | — |

Every problem is reported with a copy-paste fix command (recreate the junction / run
`repair-vendor.ps1` / remove the `bundles` entry). The guard never modifies anything itself.

> The junction check decides "does this plugin need a junction" by **source-scanning** runtime
> `import`/`require` of `@deepseek-ai/*` (not by `package.json` declarations), and only audits
> plugins actually listed in some profile's `bundles` — dormant plugin dirs are skipped.

### Known limitation

The loader deduplicates **before** creating any plugin entry, so when duplicates already exist at boot, an in-process check cannot run — the tree never mounts. For that case use the standalone CLI below: it is pure disk reads and works even when boot is broken.

## Installation

**As a profile bundle (recommended while in development):**

1. Add to the profile's `package.json` `dependencies`:
   ```json
   "dsh-bundle-dedup-guard": "link:F:/path/to/dsh-bundle-dedup-guard"
   ```
2. Add `"dsh-bundle-dedup-guard"` to `dsh.profile.bundles` (first entry is fine).
3. Link it into the profile's `node_modules` (pnpm does this for `dsh plugin add`).

**From npm:**

```bash
dsh plugin --profile web add dsh-bundle-dedup-guard
```

## Usage

The plugin checks automatically — no interaction needed. For manual diagnosis (including when boot already crashed):

```bash
# check all profiles + site-level health (DSH_HOME defaults to ~/.dsh)
node bin/check.mjs

# a specific profile
node bin/check.mjs --profile web

# a specific manifest file (e.g. a pre-fix backup, for testing)
node bin/check.mjs --manifest <path-to-package.json>

# machine-readable JSON, skip report files
node bin/check.mjs --profile web --json --no-write

# skip the site-level health audit
node bin/check.mjs --no-site
```

Exit codes: `0` = healthy, `1` = duplicates / unresolved bundles / bundle-less packages /
site-level problems found (useful as a CI gate).

## Fixing duplicates

Edit `dsh.profile.bundles` so each id has exactly one source. The common shape is "aggregate + sub-plugins":

- keep the aggregate (e.g. `@linxin666/dsh-web-ui-all`)
- remove the individually listed sub-plugin entries
- **also move the sub-plugins from `dependencies` to `devDependencies`** — otherwise `dsh plugin` reconcile re-appends them on the next install/update (the exact recurrence from 2026-08-18)

Then re-run `node bin/check.mjs --profile <name>` until green, and restart.

## Development

```bash
npm test          # node --test, zero dependencies
npm run check     # run the guard against your local profiles
```

- `lib/check.mjs` — the check core (pure Node, no third-party deps)
- `lib/site-health.mjs` — site-level health checks (v0.2.0: conflict pairs, vendor tree, junctions)
- `index.mjs` — the Cordis plugin entry (`apply` + listeners)
- `bin/check.mjs` — standalone CLI (works without a booted tree)
- `test/` — unit tests with fixture profiles

## License

MIT

Install

dsh plugin --profile web add github:Lstalu/dsh-bundle-dedup-guard

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source