dsh-plugin-ployenv
Verifieddsh-plugin-ployenv · v0.3.4 · MIT
DSH host plugin that drives the ployenv local runtime version manager from the chat (list/install/use runtimes, ship a ployenv binary, and inject its path list into session shells).
Install
dsh plugin add dsh-plugin-ployenv Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-plugin-ployenv
DeepSeek Harness (DSH) host plugin that drives ployenv - the local runtime version manager for Node / Java / Go / Python - from the chat.
It contributes:
- a human slash command
/ployenv, - a model-callable tool
ployenv(same actions),
both of which shell out to the ployenv CLI with node:child_process, plus a
DSH_PLOYENV_* environment fact triple for session shell tools.
Written against ployenv 0.3.4 (the 0.2.0 session model is unchanged: no shim executables, no shim/root
commands, and nothing is ever written to PATH or the registry). Since 0.3.0 the package is self-contained -
it ships a ployenv binary for its platform, so installing the plugin is enough (REQ-003). Since REQ-003
FR-303 it can also install the released binary itself through its own setup action (SHA-256 verified,
into data/ployenv/); that action is plugin-side and is never passed to the CLI.
Layout
| File | Role |
|---|---|
package.json |
DSH bundle and plugin manifest (dsh.bundle.patch) |
cordis.patch.yml |
Bundle layer: inserts the ployenv Loader row and documents its config |
index.js |
The plugin: export apply(ctx, config), no build step, no dependencies |
stub-smoke.mjs |
Hermetic contract smoke test (no binary, no subprocess, no network) |
cordis.patch.yml is required - package.json declares it as the bundle's patch, and a package
that declares no dsh.bundle installs as a plain dependency instead of an active layer.
Requirements
Platform note. The bundled binary is Windows x64 (
bin/win32-x64/ployenv.exe); the package no longer declares"os"/"cpu", so npm installs it on any platform. Elsewhere, run the plugin-sidesetupaction - it downloads the released asset for this host (ployenv-<os>-<arch>[.exe]) and verifies its SHA-256 against the releaseSHA256SUMS.txt- or pointPLOYENV_BIN(orPLOYENV_ROOT) at a ployenv you provide; the resolution order below then finds it.
- Node.js >= 20 for the host (DSH already requires this).
- The ployenv CLI, resolved in this order (REQ-003 §3.2);
statusandbinalways name the stage that won, so you can tell which one is in use:- row config
bin- an explicit choice always wins, - the
PLOYENV_BINenvironment variable of the DSH process, $PLOYENV_ROOT\ployenv.exewhen that file exists (reuse a ployenv you installed yourself),- the copy installed by
setup:data/ployenv/bin/ployenv(.exe)inside this package - runningsetupis a deliberate choice by the operator, so it outranks the bundled copy, - the bundled copy:
bin/<platform>-<arch>/ployenv(.exe)inside this package, - the bare name
ployenv, resolved throughPATH.
- row config
- No package manager, no install scripts, no build: the plugin is plain ESM JavaScript.
Install
Two ways in, both end the same way: pnpm runs inside the target profile directory and the package
name lands in the profile's dsh.profile.bundles list.
| Path | Use it when |
|---|---|
npm package dsh-plugin-ployenv |
you just want the plugin (published package, binary included) |
| local directory (Options A-C below) | you are developing the plugin itself |
Option 0 - from npm (published package)
dsh plugin --profile desktop add dsh-plugin-ployenv
The published tarball ships the platform binary, so no separate ployenv download is needed
(REQ-003). Quit the Desktop application before touching the reserved desktop profile.
Option A - Web GUI (this machine's Desktop app)
Open the sidebar Plugins page.
Choose Install, and give the absolute path of your checkout's
dsh-plugindirectory:<path to your checkout>\dsh-pluginConfirm. A local directory install links the checkout, so edits to
index.jsare picked up by the next load (a fresh JavaScript module generation needs a restart; see Caveats).
Option B - the agent
Ask the agent to call the plugin_manager tool:
{ "action": "install_bundle", "target": "<path to your checkout>\\dsh-plugin" }
Option C - the dsh CLI
dsh forwards its remaining arguments to pnpm in the profile directory. A throwaway profile keeps
the experiment away from your daily desktop profile:
dsh plugin --profile ployenv-test add <path to your checkout>\dsh-plugin
--profile <name> is required and the profile is initialized on first use. The reserved desktop
profile must first be initialized by the Desktop application, and the application must be fully
quit before running package operations against it.
What success looks like
The profile's package.json gains a dependency on dsh-plugin-ployenv and lists it in
dsh.profile.bundles; the profile's cordis.patch.yml is untouched. In the running app the
Plugins page shows the bundle with no load error, and /ployenv status answers.
Rollback: remove the bundle from the profile and delete the linked directory under the profile's
node_modules (nothing else is touched - the plugin writes no PATH, registry, or config).
Uninstall
- Web GUI: Plugins page -> remove the
dsh-plugin-ployenvbundle. - Agent:
plugin_managerwith{ "action": "remove_bundle", "target": "dsh-plugin-ployenv" }. - CLI:
dsh plugin --profile <name> remove dsh-plugin-ployenv.
Removal deselects the bundle, unloads its runtime contributions, then runs pnpm remove. Nothing else needs undoing: the plugin never writes PATH, the registry, or any file outside its own logs.
Configuration
Row config in the profile's own cordis.patch.yml overrides the bundle's defaults. The plugin
exports no Config schema, so the config object is passed to apply unchanged - every field is
optional and unknown fields are ignored.
- id: ployenv
name: "dsh-plugin-ployenv"
config:
bin: "D:\\tools\\ployenv.exe"
commandName: ployenv
toolName: ployenv
timeoutMs: 180000
injectPathAtLoad: false
args:
list: ["list"]
use: ["use", "{runtime}@{version}", "--global"]
env: ["env", "--shell", "json"]
| Field | Default | Meaning |
|---|---|---|
bin |
unset | ployenv executable (absolute path or PATH name). Set it to pin your own build; it overrides the bundled copy. |
root |
unset | ployenv data root passed to the child as PLOYENV_ROOT. When the bundled binary is used the default is <package>/data/ployenv, so uninstalling the plugin removes the runtimes with it. Set it to keep data outside the package (it survives plugin updates). |
commandName |
ployenv |
Slash-command name; must match ^[a-z][a-z0-9_-]*$. |
toolName |
ployenv |
Model tool name. |
timeoutMs |
120000 |
Per-invocation deadline. Install downloads can exceed it; raise it if needed. |
injectPathAtLoad |
false |
Prepend the env --shell json path list to the harness process PATH as soon as it is known. |
args |
see below | argv templates per action; {runtime}, {version}, {tool} and {filter} are substituted, and a token that becomes empty is dropped. |
Default argv templates, matching the 0.2.0 CLI (internal/cli/cli.go):
list: ["list"]
available: ["available", "{runtime}"]
install: ["install", "{runtime}@{version}"]
uninstall: ["uninstall", "{runtime}@{version}"]
use: ["use", "{runtime}@{version}"]
current: ["current"]
which: ["which", "{tool}"]
env: ["env", "--shell", "json"]
config: ["config", "--json"]
doctor: ["doctor", "--json"]
version: ["--version"]
Usage
Slash command
/ployenv list installed runtimes and the current one
/ployenv list node only node
/ployenv available node lts list installable versions (filter optional)
/ployenv install [email protected] install an exact version
/ployenv install node@22 a major resolves to the newest match
/ployenv install node@22 --from D:\node take over an existing install instead of downloading
/ployenv install node@22 --force reinstall
/ployenv uninstall [email protected] uninstall
/ployenv use [email protected] set the global default
/ployenv use [email protected] --project pin the current project (.ployenv)
/ployenv current node effective version and its source
/ployenv which node the executable a tool would run
/ployenv env --shell json the 0.2.0 JSON contract (path + vars)
/ployenv env --shell pwsh paste-ready PATH export for this shell
/ployenv config root / defaults / mirrors as JSON
/ployenv status bin + data root + injectable directories + PATH state
/ployenv path on prepend those directories to the harness process PATH
/ployenv path off restore the PATH captured before the first injection
/ployenv doctor ployenv self-check as JSON
/ployenv bin the resolved executable and whether it exists
/ployenv raw doctor --json run an arbitrary ployenv argument vector
/ployenv help usage
Model tool
{ "action": "list" }
{ "action": "available", "runtime": "node", "filter": "lts" }
{ "action": "install", "runtime": "node", "version": "22.11.0" }
{ "action": "install", "runtime": "node", "version": "22", "flags": ["--force"] }
{ "action": "use", "runtime": "node", "version": "22.11.0", "flags": ["--project"] }
{ "action": "current" }
{ "action": "which", "tool": "npm" }
{ "action": "env" }
{ "action": "config" }
{ "action": "status" }
{ "action": "path_on" }
{ "action": "path_off" }
{ "action": "raw", "raw": ["doctor", "--json"] }
The tool returns one JSON string: { action, bin, runtime?, version?, argv?, exitCode, stdout, stderr, timedOut, aborted, spawnFailure?, note?, environment?, paths?, pathInjected?, ... }. A non-zero
exit, spawn failure, timeout, or abort is reported through those fields, never as a thrown tool error.
An unknown action reports exitCode: 1 with an explanatory error.
Environment facts for shell tools
When the profile mounts the shell-env registry (ctx.shellEnv, part of the base bundle), the plugin
declares three managed facts. Session shell tools rebuild the managed namespace for every call, so
these are always current:
| Variable | Value |
|---|---|
DSH_PLOYENV_BIN |
The resolved ployenv executable. |
DSH_PLOYENV_ROOT |
The ployenv data root, from config --json, once known. |
DSH_PLOYENV_PATHS |
Comma separated directories from env --shell json (path), once known. |
echo $env:DSH_PLOYENV_PATHS
These are facts, not a PATH change: they tell the shell where ployenv lives. To make bare
node / java / go / python resolve through ployenv, run /ployenv path on (or the
path_on tool action). It prepends exactly the directories from env --shell json, is
idempotent (a directory already present is never duplicated), and path off restores the
PATH captured before the first injection, byte for byte. Injection is process-wide: it affects every
session in the same harness process until path off or a restart.
Tests
npm run check # node --check index.js
npm run test # node stub-smoke.mjs
stub-smoke.mjs is hermetic: it patches node:child_process before importing the plugin, answers
every CLI invocation from a canned 0.2.0-shaped table and asserts the REQ-002 contract in thirteen
clauses (A1 apply/registration, A2 action list, argv, facts and flag set, path on/off semantics, unknown
action exit code, A4 no installer, A5 help, FR-305 status echo, and FR-303 setup failing loudly offline
without spawning the CLI). No binary, no subprocess, no network - so it gives the
same verdict in a confined session (where piped spawn is denied with EPERM), on a developer machine
and in CI (.github/workflows/ci.yml, job plugin-smoke).
Caveats
- PATH injection is process-wide and in-memory. DSH's only plugin-facing per-execution shell
environment extension point accepts
DSH_*keys exclusively, so a plugin cannot contributePATHto one session's shell calls.path ontherefore mutates the harness process environment, which every spawned child inherits, and nothing is written to the machine. - Machine-wide PATH is never touched. The plugin contains no registry,
setxor machine PATH writer, and exposes no such action. --dry-runis rejected. ployenv 0.3.0 and later refuse the flag (BUG-020) instead of pretending to dry-run while installing for real; older 0.2.0 builds accepted and ignored it. Either way the plugin passes the flag through verbatim and never treats it as a safety switch - confirm the runtime and version before installing.- Replacing the installed directory requires a restart to load a fresh JavaScript module generation. Newly installed bundles can activate through HMR.
- The plugin calls
node:child_processdirectly rather than a harness subprocess seam. Inside a session that runs under a confined sandbox, piped-stdio spawns are denied by the platform withEPERM; the DSH host process itself is normally unconfined. installcan download hundreds of megabytes;timeoutMsapplies to the whole call.
Self-contained install (REQ-003)
Shipped in v0.3.4. The bundled binary is built from the release commit (
-ldflags "-s -w"). v0.3.1 fixed the shell-script shapes (I-07 pwsh single-quoted literals, I-08 PATH separator follows the target shell) and stopped injecting a pythonScriptsdirectory that does not exist (I-09). v0.3.2 is a breaking change: ployenv now reads a single project file,.ployenv-.ployenv.localis no longer read at all.
Installing this package is meant to be enough - no separate ployenv download:
- a ployenv binary ships in the package (
bin/win32-x64/ployenv.exetoday; the samebin/<platform>-<arch>/layout is used for any other bundled copy later); - when that bundled binary is used, its data root is
<package>/data/ployenv(passed to the child asPLOYENV_ROOT), so uninstalling the plugin removes the installed runtimes too; - if you already have your own ployenv (row
bin/PLOYENV_BIN/PLOYENV_ROOT), the plugin reuses it and never touches the bundled copy - the plugin package and the ployenv distribution stay independent; - on a platform without a bundled binary, or when you want a newer release than the package ships, run
the plugin-side
setupaction - see the next section;
⚠️ Upgrade caveat. A package update replaces the whole package directory, so a
data/inside it goes away with it: the plugin recreates the directory on the next run, but the runtimes have to be installed again (NFR-307self-heal). Point row configrootat a directory outside the package (for exampleD:\ployenv-data) when the runtimes must survive plugin updates.
setup - install the released binary (REQ-003 FR-303)
setup fetches the released ployenv asset, verifies its SHA-256 and installs it under this package's
own data root. It is plugin-side: nothing is passed to the CLI, and no file is written outside
data/ (NFR-301).
/ployenv setup zero config: newest release, checksum from SHA256SUMS.txt
/ployenv setup --url <url> a mirror or a pinned release
/ployenv setup --url <url> --sha256 <hex> skip the checksum file entirely
{ "action": "setup" }
{ "action": "setup", "url": "http://mirror/ployenv-windows-amd64.exe", "sha256": "d95377df…" }
| Item | Value |
|---|---|
| Default URL | https://github.com/MOMIEDEJIYI/ployenv/releases/latest/download/ployenv-<os>-<arch>[.exe] for this host (Go-style names: windows/linux/darwin x amd64/arm64) - the fixed-name alias asset, so no version is hard-coded |
| Default digest | the entry for that file name in the SHA256SUMS.txt next to the URL; pass sha256 to skip that fetch |
| Writes | data/ployenv/bin/ployenv(.exe) and the provenance record data/ployenv/setup.json; on the Unix-likes the binary is marked executable (0755) |
| Verify order | the digest is checked before anything is written; a mismatch exits non-zero and leaves no file (NFR-302) |
| Platforms | every published target (windows/linux/darwin x amd64/arm64); a host without an asset fails loudly and tells you to pass url |
| After setup | bin/status report bin source: setup, and status also prints setup: <url> @ <time> |
| Data root | still <package>/data/ployenv unless row config root overrides it, so uninstalling the plugin removes both the binary and the runtimes |
Verified / unverified
node --check index.js- passes (Node 22.23.3).node stub-smoke.mjs- 16/16 clauses (hermetic contract check).setupresolves the released asset for this host (Go-style<os>-<arch>names) and refuses a host without one; the contract smoke drives a real download and SHA-256 check against a local source on a simulatedlinux/arm64host.setupend-to-end against a local HTTP source - 14/14 checks: install + provenance + resolution flip +statusecho, wrong digest (non-zero, nothing written, no temp left), missing checksum file, name missing from the checksum file, and an explicitsha256that skips the checksum fetch.- An independent acceptance harness (a fake CLI recording every argv, kept outside this directory) - re-run after any migration.
- The CLI grammar above was read from the 0.2.0 sources and confirmed by live runs of
env --shell json,current --json,config --jsonanddoctor --json. - No real-machine load through DSH has been performed yet. Load it once in a throwaway profile and record the outcome in your own notes.