description: "Plugin management over a dsh profile: the installer the dsh plugin command and the Web host share, and the manager over the booted profile that enables, disables, retries, edits user-layer rows, and reports every package."
English | 中文
dsh-plugin-manager changes a profile's plugins. PluginInstaller needs the profile on disk: it runs pnpm there, probes every package a run added in a child process, and removes what has no place in a profile — dsh plugin add uses it before any plugin starts. PluginManager needs the booted tree: it enables and disables bundles through profileRuntime, retries failed ones, adds and removes rows in the global or a preset's user layer, reports dependents, and folds manifest, probe record, and live tree into one view per package. Failures carry plugins/* codes; dsh-host-plugin-manager exposes the manager as the plugins Remote.
Build a PluginInstaller from the profile directory, the install anchor (the dsh app's package.json), a loadProfile that answers the profile's composed layers, the tooling bounds, a sink for pnpm's output, and whether pnpm colours it; then add(spec) or remove(name):
import { loadProfile } from '@deepseek-ai/dsh-app-boot'
import { PluginInstaller } from '@deepseek-ai/dsh-plugin-manager'
declare const profileDir: string
declare const installAnchor: string
const installer = new PluginInstaller({
profileDir, profileName: 'web', installAnchor,
loadProfile: () => loadProfile('dsh', 'web', installAnchor, undefined, { userLayer: false }),
config: { pnpmCommand: 'pnpm', installTimeoutMs: 600_000, probeTimeoutMs: 20_000, installLogTailBytes: 16_384 },
installLog: (chunk) => process.stdout.write(chunk.text),
color: process.stdout.isTTY,
})
const outcome = await installer.add('@acme/dsh-sql-tool')
console.log(outcome.installed, outcome.removed)
add takes a pnpm spec — a registry name, a github: or git URL, a tarball, an absolute path — runs pnpm add, records what pnpm wrote to dependencies, and probes every new package. A successful pnpm add is not yet an installed plugin: a package that declares neither a bundle nor a plugin module, or a bundle whose row id a composed layer already owns, is removed again with pnpm remove and listed under removed with the reason; a package the probe refused stays installed for a view to explain. New bundles are left disabled and listed under installedOnly, so the caller decides whether to enable them — the CLI always does, the Web host only when asked. A non-zero exit, a spawn failure, or the timeout fails the call with plugins/install-failed and the tail of the log, and the profile manifest is restored to what it was before the run.
Build a PluginManager over the Cordis context and readers for what it needs per call — the profile runtime, the preset roster's layers, and the running-agent count — so a composition that gains or lacks one of them is answered at call time rather than at mount:
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-agent'
import type {} from '@deepseek-ai/dsh-agent-presets'
import type {} from '@deepseek-ai/dsh-app-boot'
import { PluginManager, type PluginToolingConfig } from '@deepseek-ai/dsh-plugin-manager'
declare const ctx: Context
declare const config: PluginToolingConfig
const manager = new PluginManager(ctx, {
config,
runtime: () => ctx.get('profileRuntime'),
presets: () => ctx.get('agentPresets'),
runningAgents: () => (ctx.get('agents')?.list() ?? []).filter(agent => agent.status === 'running').length,
})
console.log(await manager.list())
list returns one view per package the profile knows: its template and installed bundles and every other installed dependency. A view carries the manifest facts (name, version, title, description, engines.dsh), what the package is (bundle, plugin, or library), who supplied it (builtin or external), when its rows mount (boot or runtime), whether it is installed and enabled, and a folded status: running, partial, or failed for an enabled bundle by how many of its rows are active; disabled for an installed bundle outside the layer list; not-enableable when the probe refused it, with the reason; restart-required when the manifest and the live tree disagree on a profile that applies changes at its next start; plain for a library or plugin module, which is added to a composition rather than enabled. Rows come from the live tree while the bundle is composed — phase, disabled-by, and the recorded failure of an isolated row — and from the probe record otherwise, under the ids their patches declare. addable lists the modules the package declares in dsh.plugins, each with its default config and the probe's verdict, and for a plugin module its main export as . — the entry addRow accepts without a declaration.
add is the installer's add guarded by the running-agent count, with enable putting every newly installed bundle into the layer list at once. enable puts an installed bundle into the layer list and, on a live profile, recomposes the tree with it through the profile runtime. The recomposition is the Loader's own transaction: a bundle the tree rejects — a boot-stage bundle whose row throws — rolls back, the layer list is restored, and the call fails with plugins/enable-failed naming the reason, while the tree that was running keeps running. A runtime-stage bundle whose row fails is isolated instead: the call succeeds, the view reports the row's failure, and retry composes the bundle again from scratch. disable is the reverse; a template bundle, which is not a dependency, cannot be disabled. On a profile whose patchReload is startup, both write the manifest and report effect: 'restart'. uninstall disables the bundle when enabled, drops every user-layer row that names one of the package's modules, runs pnpm remove, and forgets the probe record.
addRow inserts a row naming one of the package's modules — its main export for a plugin package, or a dsh.plugins entry — into the profile's global cordis.patch.yml (target: { kind: 'global' }) or an agent preset's user layer ({ kind: 'preset', preset }, through the roster's overlayPathFor). The row id derives from the package name and subpath unless given; a taken id fails with plugins/row-conflict. removeRow removes an inserted row and setRowDisabled writes or removes a disabled: true for any row — deny-only, so a bundle's own !!js gate is restored rather than overridden. The global layer is recomposed live on the spot; a preset's layer reaches its next standing generation. dependents says what disabling or removing a package would strand: services its rows provide that rows outside it inject, and user-layer rows naming its modules.
The manager runs one mutation at a time — a second call while one runs fails with plugins/busy naming the operation in flight — and add and uninstall refuse to change node_modules while a session is running, with plugins/agents-running. Every change is followed by a plugins/changed event on the context, and an install run emits pnpm's output as plugins/install-log chunks, each naming the command line it ran and the profile directory it ran in, and, with colour on, carrying pnpm's SGR escapes.
Every refusal or failure is a PluginOperationError with a stable code and details typed by it: plugins/unavailable (no profile runtime, or no roster for a preset target), plugins/not-installed, plugins/not-enableable, plugins/enable-failed, plugins/install-failed, plugins/row-conflict, plugins/busy, plugins/agents-running, and plugins/bad-request for a request that names nothing the profile has. pluginOperationFailureOf narrows a caught value to the code-discriminated union.
Read these when the manager's contract is not enough: the runtime it drives, the files it edits, and the surfaces that call it.
plugins Remote over this manager.dsh plugin command over the installer.None, as plugin management registers nothing model-facing; the rows it composes own every registration they make.
None; this package neither assembles nor sends a provider request.
These limits define what the manager will not do for a caller. They are current package constraints, not a task backlog.
pnpm update through add rewrites the files but the running tree keeps the old modules until the process restarts.inject edge, so dependents cannot name a row that only reads what the package registered.engines.dsh check yet — the range is reported, not enforced against the running harness version.