feat(plugin): plugin marketplace, durable install persistence, skill manager

Add an in-app plugin marketplace fed by a remote web catalog: the host
plugin-inventory gateway gains marketplaceList/Install/Uninstall remotes,
a durable per-user install table reconciled against the actual profile, and
git/npm/tarball/bundle install paths with non-interactive git and vendored
pnpm. The Web Settings surface gains a sibling 插件市场 tab that lists the
catalog with recommended badges, repository links, and a combined sort
(recommended first, then catalog priority, then id).

Add a host + client skill manager: list local skills by direct filesystem
discovery, and install/uninstall/toggle/edit their SKILL.md from git/npm/
tarball/local sources in the writable user root.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pine
2026-08-15 17:01:48 +08:00
parent 5f348f9b1c
commit a53769955b
57 changed files with 5168 additions and 129 deletions
+8 -4
View File
@@ -2,13 +2,17 @@
English | [中文](README.zh.md)
Host projection of the current Cordis Loader tree with per-plugin enable/disable. `PluginInventoryGateway` registers the `pluginInventory` service and publishes two generated direct Remotes: `pluginInventory/list` and `pluginInventory/setEnabled`. `list` reads `ctx.loader.entries()` directly, skips structural group rows, and returns the remaining entries in Loader order with only their Loader entry id, module specifier, effective enablement, and current root Fiber phase.
Host projection of the current Cordis Loader tree with per-plugin enable/disable. `PluginInventoryGateway` registers the `pluginInventory` service and publishes generated direct Remotes (`list`, `setEnabled`, `availableBundles`, `installPlugin`, `installedBundles`, `uninstall`). `list` reads `ctx.loader.entries()` directly, skips structural group rows, and returns the remaining entries in Loader order with only their Loader entry id, module specifier, effective enablement, and current root Fiber phase.
The phase is `pending`, `loading`, `active`, `failed`, or `unloading`; it is `null` when the entry has no live root Fiber. The snapshot is intentionally point-in-time: Loader remains the sole lifecycle authority, while this package owns no cache, history, provenance model, or event stream. `setEnabled` toggles one entry live through `ctx.loader.update` and persists an explicit `disabled` override into the profile's user patch layer so the choice survives a restart (a bundle-default disable needs the `disabled: false` override to stick).
Every entry carries a `protected` flag. The guard in `src/required.ts` is default-open with two code-editable lists: `REQUIRED_PLUGINS` (the blacklist of load-bearing core that must never be disabled — the entry tree, the Remote RPC spine, the session and agent spines) and `USER_TOGGLEABLE_PLUGINS` (the whitelist, which overrides the blacklist for an explicitly toggleable plugin); a plugin on neither list is toggleable by default. The full dependency-derived taxonomy of the shipped base bundle is in [`docs/plugin-system.md`](../../../docs/plugin-system.md). `setEnabled` refuses to disable a required plugin and, after enabling, verifies the fiber becomes active (reverting a dependency-missing enable). The Web plugin-list tab renders one flat list of every entry: each shows its real enabled state, a toggleable plugin carries an enable or disable button (so a bundle-default-disabled plugin can be re-enabled), and a required plugin shows only a read-only note.
Every entry carries a `protected` flag. The guard in `src/required.ts` is default-open with two code-editable lists: `REQUIRED_PLUGINS` (the blacklist of load-bearing core that must never be disabled — the entry tree, the Remote RPC spine, the session and agent spines) and `USER_TOGGLEABLE_PLUGINS` (the whitelist, which overrides the blacklist for an explicitly toggleable plugin); a plugin on neither list is toggleable by default. The full dependency-derived taxonomy of the shipped base bundle is in [`docs/plugin-system.md`](../../../docs/plugin-system.md). `setEnabled` refuses to disable a required plugin; a plugin whose `apply` actually fails rejects through the loader's own start error, while a plugin merely pending on a dependency is left enabled and activates once the dependency resolves. The Web plugin-list tab renders one flat list of every entry: each shows its real enabled state, a toggleable plugin carries an enable or disable button (so a bundle-default-disabled plugin can be re-enabled), and a required plugin shows only a read-only note.
The gateway also manages installation through `availableBundles`/`installPlugin`/`uninstall`. `availableBundles` lists the curated offline-installable optional bundles in `src/bundles.ts` (`AVAILABLE_BUNDLES`); that catalog is empty until an optional bundle ships — the profile's default bundles (`dsh-base`, `dsh-web-app`, `dsh-image-recognition-bundle`) are part of the deployment, not optional add-ons, and `uninstall` refuses to remove them. `installPlugin` runs pnpm against the writable profile directory via the bundled Node and vendored pnpm for a registry package spec (the settings plugin-list tab offers this as the "install plugin" form); a registry install is gated behind the `dshAllowPluginInstall` context flag, which only the desktop boot sets. It tries the ordered `INSTALL_REGISTRIES` list (`src/install.ts`) until one succeeds, with the official npm registry last as the fallback, and errors only when every registry is unreachable. When the boot provides a `dshReloadProfile` handle, the gateway recomposes the running tree after the write so the plugin activates immediately (`restartRequired: false`); without it, the install persists the manifest and requires a restart (`restartRequired: true`). Its public payload types live under `./types`, and Typert generates the Host and Client Remote artifacts exposed by `./typert` and `./remote`.
The gateway also manages installation through `availableBundles`/`installPlugin`/`installedBundles`/`uninstall`. `availableBundles` lists the curated offline-installable optional bundles in `src/bundles.ts` (`AVAILABLE_BUNDLES`); that catalog is empty until an optional bundle ships — the profile's default bundles (`dsh-base`, `dsh-web-app`, `dsh-image-recognition-bundle`) are part of the deployment, not optional add-ons, and `uninstall` refuses to remove them. `installedBundles` lists the profile's user-installed dependencies (the ones the user can uninstall). `uninstall` fully removes a user-installed dependency with `pnpm remove` (dropping both the dependency and any bundle layer it declared, gated by `dshAllowPluginInstall` like install), un-composes an offline optional bundle that was composed, and refuses the in-box default bundles. `installPlugin` runs pnpm against the writable profile directory for any `pnpm add` specifier — via the pnpm vendored into the harness, or the `pnpm` on PATH when no vendored pnpm is present (a development checkout), failing loudly only when neither exists — a bare npm name, a tarball path or URL (including a GitHub archive `.tar.gz`), or a git/GitHub URL — so every community install source shares one path. A registry install is gated behind the `dshAllowPluginInstall` context flag, which only the desktop boot sets; a registry-name spec tries the ordered `INSTALL_REGISTRIES` list (`src/install.ts`) until one succeeds, with the official npm registry last as the fallback, and errors only when every registry is unreachable, while a git, tarball, or path spec runs once with no registry. A registry-name spec also passes `--minimum-release-age-exclude` so a freshly published plugin installs.
Because pnpm 11 blocks dependency build scripts by default, install is two-phase when one is blocked: the first call returns the blocked packages as `pendingBuilds` instead of failing, the settings tab shows them with a sandbox warning for the user's per-package consent, and the retry carries the approved set in `consentBuilds` — the host writes the per-package `allowBuilds` map (pnpm ≥11) and `onlyBuiltDependencies` array (pnpm 10) directly into the profile's `pnpm-workspace.yaml` for exactly those packages, then reinstalls. Writing the config directly, rather than running `pnpm approve-builds`, does not depend on the packages being in pnpm's pending-build state. When the boot provides a `dshReloadProfile` handle, the gateway recomposes the running tree after the write so the plugin activates immediately (`restartRequired: false`); without it, the install persists the manifest and requires a restart (`restartRequired: true`). Its public payload types live under `./types`, and Typert generates the Host and Client Remote artifacts exposed by `./typert` and `./remote`.
A plugin marketplace is exposed through `marketplaceList`/`marketplaceInstall`/`marketplaceUninstall`. The catalog lives on the static web host (default `https://deepseek.pinesound.cn/plugins/plugins.json`, overridable via `DSH_MARKETPLACE_URL`): one index JSON lists plugins, and a per-plugin JSON beside it prescribes the install method (`git`, `npm`, `tarball`, or `bundle`) with the pnpm specifier and the dependency name. `marketplaceList` overlays the durable install table — a small JSON document under `$DSH_HOME/plugin-marketplace/installed.json` — to mark each entry installed; that table is the authoritative installed check. `marketplaceInstall` maps a method onto the existing install path (git/npm/tarball via the registry Remote, bundle via the offline compose), records the install in the table on success, and honors the same two-phase build-consent flow; `marketplaceUninstall` resolves the dependency from the table, runs the existing uninstall, and drops the row. Per-plugin spec URLs are derived from the fixed catalog base plus the plugin id (never from catalog content), so a hostile catalog cannot point the app at an arbitrary URL.
The service is Remote-only and deliberately declares no same-process Cordis `Context` merge. Client packages consume it through the explicit [`api-remotes`](../../api/remotes/README.md) assembly rather than importing the Host implementation.
@@ -23,4 +27,4 @@ None; this package never assembles model input.
## Known Limitations and Deferred Work
- **Point-in-time state only** — the result contains no durable failure history or subscription; a missing root Fiber is reported as `null`, regardless of why no live root exists.
- **No provenance or add/remove** — the service does not identify which bundle, profile, or override introduced an entry, and it cannot add or remove plugins. Enable/disable persists to the profile's user patch layer; a row the profile does not mount (absent from every bundle) cannot be toggled from here.
- **No provenance** — the service does not identify which bundle, profile, or override introduced an entry, and a row the profile does not mount (absent from every bundle) cannot be toggled from here. Enable/disable persists to the profile's user patch layer; user-installed dependencies can be uninstalled (`installedBundles`/`uninstall`), but the in-box default bundles are part of the installation and are not removable.