A failed installation could show the generic sentence while its details
panel stayed empty. A pnpm run that printed nothing had its diagnostic
written to the complete log and the result but never streamed, and the
GitHub connection check that stops an installation before pnpm ran kept
its captured output only in the result. Both now reach the install-log
stream the dialog renders: the no-output diagnostic streams as the run's
output, and a network or timeout check failure streams the exact
git ls-remote command line and its output as the installation's only
run under the caller's request id, with no exit code when the check
timed out. A check failure pnpm may resolve streams nothing.
Profile templates and the default, optional, on-demand, retired, and
installation-owned bundle lists move to the new data-only package
@deepseek-ai/dsh-official-bundles, so editing a bundle list no longer
touches app-boot.
Read configured plugin market indexes on the Host, search them with fuzzy
multilingual matching, and install a chosen version from the Plugins page.
- dsh-plugin-market-index validates format-1 indexes and builds install
arguments; inline icons are capped at 32 KiB.
- The pluginMarket service fetches configured http(s) URLs, parses bodies in
a Worker Thread, caches documents, and pages filtered, ordered results with
installation status and engines.dsh compatibility.
- Discover is an opt-in experimental bundle; the Plugins page gains a
plugins.tab slot and confirmed installs.
- The plugin_market tool lists indexes, plugins, and versions for Creator mode.
- Plugin compatibility reads engines.dsh before DSH peer dependencies.
Remove the `{ preset, insert }` / `{ preset, id, ...override }` profile patch
operation added in #5773, together with the app-boot compiler and reload drain,
config-editor filtering, plugin-manager preset row inventory and retained-module
removal checks, the read-only preset rows on the Plugins page, and the
agent-preset-registry row identities that served them.
Session search, Ralph, persistent terminals, Codex, and Claude Code bundles now
insert ordinary Host rows whose tools register in the global tool layer, like the
earlier optional bundles. Every preset, including minimal, sees them, and toggling
a bundle updates live Agents without new composition generations.
Review found three ways the takeover could let two writers overlap.
- A claimant that read an exited PID could remove the lock of a new
holder that reused that PID before the claim. The claimant now probes
the PID again under the claim, after re-reading the record.
- The browser Worker's process shim reports its own PID as absent, so a
second writer in the Worker took over the first one's live lock. A
record naming the contender's own process is never taken over.
- Takeover proves only that the holder exited, while a pnpm run it
started can still be writing the profile. Each profile operation now
records its pnpm run in .plugin-manager/run.json and removes the record
when the run ends; an operation that finds a record waits for the run
within the existing tree bound and otherwise refuses, naming the
process and the record, without running pnpm.
A failed removal of the exited holder's lock now waits instead of
failing the acquisition, the probe range is stated as int32, and the
tests inject exited PIDs instead of reusing real ones, which Windows
reissues at once.
Compatibility checks now yield the refused packages as data instead of a
profile-specific sentence. runProfilePnpm returns them as
PackageResult.incompatible, and the plugin manager refuses with the
incompatible-version code carrying the same records. Each surface renders
them: the CLI prints the exact allow-version command for its profile, the
Web plugin manager words them through its locale dictionary, and app-boot
keeps one neutral English diagnostic for logs and startup stderr.
A bundle package is not a plugin row, so startup admission never read its
own DSH peers: a bundle installed outside DSH or before a DSH upgrade still
applied its patch layer. loadProfileDirectory now skips such a bundle like an
unreadable one unless the profile exempts that exact pair.
A post-install refusal in a profile without a lockfile reinstalls from the
restored manifest instead of failing a frozen install and leaving the
rejected packages in node_modules.
Compatibility warnings now name the `dsh plugin allow-version` command for
CLI-managed profiles, startup denials name the row id instead of a resolved
module URL, and the CLI no longer reports "pnpm failed" when pnpm never ran.
* fix(plugin-manager): bound pnpm runs so a silent child cannot hold the profile lock
A pnpm child that prints its completion line and then never exits holds the
profile write lock until the host is restarted, so every other session and host
on that profile fails after its own lock timeout (issue #4981). execa resolves
its promise only once the piped stdio has ended, so a run now completes on the
process's own exit and drains the pipes under a bound instead of awaiting their
end.
A service run whose captured output stays silent for idleTimeoutMs is
terminated, reports timedOut alongside its exit status, and is not retried on
the next registry; a run with inherited descriptors captures nothing and is
left unbounded for its operator.
* test(plugin-manager): type the mocked child and await the drain descendants
The inherited-descriptor fixture no longer needs its legacy `as unknown` cast,
because the mocked child is typed for the fields the operations read; the
retired baseline entry is pruned (verify-no-unknown-casts).
The drain specs wait for the killed descendant to exit before removing its
directory: Windows refuses to delete a directory a live process still holds.
* test(plugin-manager): stop the drain descendant deterministically
The descendant that holds the pipes now ends on a stop file the teardown
writes, and on its own after a minute, so the fixture leaves no process behind
when a kill cannot be delivered. Windows stops it through taskkill, and the
spec waits for the exit before removing the directory it ran in.
* test(plugin-manager): run the drain descendant from its own script file
Windows cannot carry a nested `-e` script through another spawn argument: the
descendant failed to start there, so the fixture asserted on a process that was
already gone. The descendant now runs from a script file the fixture writes.
* test(plugin-manager): cover the bounded drain everywhere and stage the real descendant on POSIX
Windows does not stage a descendant that keeps the inherited pipes open past
the direct child's exit, so the real-process cases run on POSIX only. The
mocked child now models execa's own completion rule — its promise settles only
once both pipes end — which covers the bounded drain on every platform and
still fails without the raw-exit race.
* test(plugin-manager): cover the drain failure path without a child process
An output consumer failure that the bounded drain cuts short is staged with the
mocked child as well, so the per-file coverage gate also holds where the
real-process cases are skipped.
* fix(plugin-manager): terminate the whole tree of a stalled run
A lifecycle script is a child of pnpm, so signalling pnpm alone left an
approved postinstall writing into the profile after the operation reported a
timeout and released the lock. A stalled run now stops its process group (or
Windows process tree) and waits for it to disappear before the caller restores
the files. The cut tail's close is told apart from a reading failure by where it
happened, so a failure from before the cut still surfaces.
* docs(plugin-manager): regenerate the config catalog after the rebase
The English catalog gains `idleTimeoutMs` from the regenerator, and the Chinese
counterpart carries the same interface line so the pair stays consistent.
* fix(plugin-manager): terminate a run's tree through execa, keeping exit cleanup
Spawning a service run with `detached` made execa skip the handler that kills
the child when the manager exits, so a crashed manager left the pnpm tree
running in the profile. `killDescendants` spawns the run as its own process
group while keeping that cleanup, and it addresses the tree on every kill path,
including cancellation. The module now only decides the probe target and waits
for quiescence.
* feat(bundle): accept an ordered patch file list in dsh.bundle.patch
`dsh.bundle.patch` now accepts `string | string[]`. A list names patch
files applied in order as one bundle layer; each file's relative plugin
paths still resolve beside that file. `bundlePatchPaths` in dsh-app-boot
is the one resolver from a declaration to absolute paths, and every
reader uses it: profile loading, plugin-manager reconcile and bundle
rows, the client-runtime roster, and the workspace, cordis-config, and
product-isolation gates. `ProfileLayer.patchPath` becomes `patchPaths`.
* fix(bundle): keep declared patch files package-relative in the workspace gate
path.join produced backslash entries on Windows, so the expected files
list did not match package.json files. bundlePatchFiles returns the
validated declaration as written; bundlePatchPaths joins it.
Review of #4539. A refusal pnpm view prints as JSON on stdout was read
as unknown, so no registry was ever asked after a dead one; stderr and
stdout are now read alike, and a lookup killed at its bound is a
network refusal. pnpm's own registry joins the fallback plan only once
pnpm config get registry names npm's own or a configured fallback, so
a company registry in .npmrc is never followed by a public one, and a
registry pnpm already names is asked once. The Host lays each failure
at the registry or the spec's own host, reading the host off error
lines only, and reports it as ChangeResult.failedAt; a stop that lands
between attempts is cancelled, not failed. The package publishes its
registry rule through ./registry so the dialog plans with the Host's
own function, starts from the registry the Host asks first until one
is remembered, tags that one as the default, labels pnpm's own by the
host it names, and words and offers a change of registry from the
Host's attribution. ERR_PNPM_NO_OFFLINE_META is no longer a network
failure.
Add registry and fallbackRegistries to the manager Config, ask them in
order from inspect and installBundle with --registry, and move on only
for failures another registry can change: unreachable, timed out, or a
copy the registry lacks. A git or tarball spec's own host is never
retried through a registry, and a registry outside the configured set
is asked alone. The lookup runs without pnpm's retries so a dead
registry is reported within inspectTimeoutMs; the install keeps pnpm's
defaults. Results carry the registries asked, the install-state event
names each attempt, and the plugin_manager tool takes a registry.
Master now carries the final #4182 plugin manager and the live client
module graph of #4189, so the web management layer is re-ported onto
them:
- The manager keeps master's `ManagementError` codes, `ChangeResult`
stages, `configure()` under `hmr.runExclusive`, `reload()` warnings
and `writePluginEnabled` name matching, and adds back `inspect`,
request-id installs with `plugin-manager/install-log` and
`plugin-manager/install-state`, `cancelInstall`, `listBundles` titles,
rows and overrides, `packageResult.kind`, `pnpmCommand` and
`inspectTimeoutMs`, and `plugin-manager/changed` after each change and
each `profile/reconciled`. A failed or cancelled installation restores
the snapshotted manifest and lockfile instead of master's one-shot
`pnpm remove` cleanup, so `cleanup` and `remainingDependencies` leave
the result.
- App boot keeps master's reconciliation diagnostics and emits
`profile/reconciled`; `watchUserPatches` goes with the launcher's
watcher now that HMR owns profile reloads.
- The settings Plugin list tab stays read-only with #4189's page-local
sync failures and retry; master's bundle form and row checkboxes are
dropped again. The sidebar page words the Host's codes through its
dictionary, and a client bundle change follows the live module graph
without a page reload.
- The web e2e scaffold no longer chooses a reload policy: the base
bundle's `hmr` row turns on with the profile context and application
readiness, and `profile.hmr: false` disables it through an overlay for
the startup-only scenario.
The sidebar Plugins page now drives `ctx.pluginManager` from #4182 and the
settings Plugin list tab goes back to read-only: its bundle form and row
checkboxes are removed, and the page joins `listBundles` with
`listPlugins` into one view per bundle, decides availability from the
inventory's `managementAvailable`, switches bundles and rows through
`setBundleEnabled` and `setPluginEnabled`, and confirms uninstalls.
The manager gains what the guided install dialog needs: `inspect(spec)`
reads a spec through the registry or a directory before anything installs
and refuses specs without a bundle; `installBundle` takes a request id,
streams each pnpm run as `plugin-manager/install-log`, reports its phases
as `plugin-manager/install-state`, classifies a failed run into
`packageResult.kind`, names the bundle it added, and restores
`package.json` and `pnpm-lock.yaml` when the run fails, is cancelled, or
adds a package without a bundle patch; `cancelInstall(requestId)` answers
only after pnpm exited and the files are back; `listBundles` carries
titles, one-liners, declared rows with their live entries, and the
built-in rows a bundle overrides; every completed operation and every
patch generation applied outside the manager emits
`plugin-manager/changed`. The config gains `pnpmCommand` and
`inspectTimeoutMs`. App boot emits `profile/reconciled` after each applied
patch generation and finds a root Include mounted under its pinned id by
another copy of the module.
Row diagnostics, dependency checks, and the non-bundle dependency group
are not carried over; the web e2e scaffold mounts a launcher-style
managed profile instead of the removed profile runtime.