dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
中文 | English
A zero-dependency, non-interactive scaffolder and verifier for DeepSeek Harness (DSH) plugin bundles — for people writing DSH plugins.
npx create-dsh-bundle --name dsh-my-plugin --with-tool
One command generates a plugin bundle that dsh plugin add accepts (package.json / index.js / cordis.patch.yml / README.md / .gitignore); the built-in --verify then re-reads those files and reports on them. Zero npm dependencies, no network access, no LLM calls, no API key.
dsh.bundle (see "Why this package does not declare dsh.bundle" below).util.parseArgs), with no readline/inquirer prompts, so it runs in headless environments, cron jobs and subagents without waiting for an answer.create-dsh-bundle: create-dsh-plugin is already taken by a third party, so it is not used here.npx create-dsh-bundle --help # run without installing
npm i -g create-dsh-bundle # or install globally
Requires Node ^22.19 || >=24 (same engines as DSH itself). No dependencies, no postinstall.
Two modes: passing only --verify selects verification mode; when --verify is present nothing is generated (--name may still be passed, in which case it is used only for check 9's cross-check and does not trigger generation).
# generate
create-dsh-bundle --name dsh-my-plugin --desc "What it does" --with-tool --out ./dsh-my-plugin
# verify an already generated bundle (read-only, writes nothing)
create-dsh-bundle --verify ./dsh-my-plugin
| Option | Mode | Required | Description |
|---|---|---|---|
--name <pkg> | generate | yes | Package name, dsh-<what-it-does>; lowercase letters/digits/hyphens only, first character must be a letter. Anything outside ^[a-z][a-z0-9-]*$ is rejected with exit code 1 |
--desc <text> | generate | no | package.json description; a one-line default is used when omitted |
--with-tool | generate | no | Generate the greet tool example (defineTool + inject = ['tools'] + one real tool call). Omit it for a minimal skeleton |
--out <dir> | generate | no | Output directory, defaults to ./<name>. An existing directory is refused (exit 1); there is no force flag |
--verify <dir> | verify | yes | Verify the generated files in that directory; read-only, exit code 0 = all PASS, 1 = any FAIL |
-h, --help | — | no | Print usage |
Options are parsed with Node's built-in util.parseArgs using strict: true + allowPositionals: false: unknown options and stray positional arguments both fail with exit code 1 instead of being silently ignored.
Follows the three-part convention of the official dsh-hello template (example with --name dsh-approval-gate):
| Name | Rule | Result |
|---|---|---|
| package name | as given | dsh-approval-gate |
export name (name in index.js) | drop the dsh- prefix | approval-gate |
cordis id (id in cordis.patch.yml) | export name, drop a -plugin suffix | approval-gate |
dsh-my-plugin/
├── package.json # declares dsh.bundle: {"dsh":{"bundle":{"patch":"./cordis.patch.yml"}}}
├── index.js # plugin entry: name + apply(ctx) (adds inject = ['tools'] with --with-tool)
├── cordis.patch.yml # bundle config layer: inserts only its own layer
├── .gitignore # .dsh-home/ + node_modules/
└── README.md # install / verify / pitfalls notes shipped with the bundle
The generated cordis.patch.yml (--name dsh-demo-x, byte for byte):
- insert:
- id: demo-x
name: dsh-demo-x
The generated package.json (byte for byte, --with-tool; the minimal skeleton omits the
peerDependencies block entirely — it imports nothing):
{
"name": "dsh-demo-x",
"version": "0.1.0",
"description": "demo",
"type": "module",
"main": "index.js",
"files": [
"index.js",
"cordis.patch.yml"
],
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
}
},
"peerDependencies": {
"@deepseek-ai/dsh-tools": "^0.1.6-alpha.1 || ^0.1.5-rc.2"
}
}
The generated index.js skeleton (--with-tool, function-plugin form, no default export):
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'demo-x'
export const inject = ['tools']
export function apply(ctx) {
console.log('[demo-x] plugin loaded!')
ctx.tools.register(defineTool({ name: 'greet', /* parameters / output.render / execute */ }))
// then drive one real tool call (as if the model issued it; no key needed)
void (async () => { /* ctx.tools.execute({ callId: 'demo-1', name: 'greet', … }) */ })()
}
Without --with-tool the minimal skeleton is generated instead: only export const name and export function apply(ctx), importing no @deepseek-ai/dsh-* package and registering no tool.
--verify checkscreate-dsh-bundle --verify <dir> re-reads the generated files without writing anything and without installing anything. It prints PASS / FAIL / WARN per item:
| # | Check | FAILs when |
|---|---|---|
| 1 | All five generated files present (SOP §3) | Any of package.json / index.js / cordis.patch.yml / README.md / .gitignore is missing (the output names the missing file) |
| 2 | package.json parses | File missing / JSON.parse throws / root is not an object |
| 3 | cordis.patch.yml exists and is well formed | Missing; or the built-in YAML subset parser fails (flow style [{...}], wrong indentation, extra lines all count) |
| 4 | package.json name equals the patch insert name | No name found in the insert, or it differs from the package name. Startup-fatal (SOP §5.6): the insert name is what the loader imports, so the whole profile dies with exit 1 |
| 5 | dsh.bundle.patch points at an existing file | dsh.bundle.patch is not declared, or it is declared but the file does not exist |
| 6 | index.js has no default export | export default / export { x as default } / module.exports = / exports.default = found |
| 6b | Every bare import in index.js is declared in package.json | A module specifier that is not relative, not absolute and not a built-in (node:-prefixed or bare builtin) appears in no dependencies / peerDependencies / optionalDependencies / devDependencies. Startup-fatal: the local (link:) route resolves imports from the plugin directory, so this is ERR_MODULE_NOT_FOUND → profile exit 1. This is the gate that keeps 0.1.1's blind spot closed |
| 7 | In tool mode inject contains 'tools' | Tool code is detected (--with-tool, or ctx.tools.register / dsh-tools in the source) but inject has no tools |
| 8 | No re-insert of a service package dsh-base already provides | @deepseek-ai/dsh-tools / @deepseek-ai/dsh-system-prompt appears as a cordis.patch.yml insert again. Startup-fatal: dsh-base already provides those services, so startup aborts with service "tools" has been registered and the profile exits 1. A WARN (exit code 0) in 0.1.1 — a gate that calls an unstartable bundle green is worse than no gate |
| 9 | Cross-check against --name | Only runs when --name was also passed |
| 6c | index.js runtime namespace (best effort) | It imports and the namespace has a default key; or the import fails with a hard error such as a syntax error. When the module is merely not installed it records a WARN and moves on — not installed is WARN (this check never installs anything), not declared is the FAIL on 6b |
| — | Declared for consumers, not only for local dev | Never FAIL, only WARN: emits when 6b passed via devDependencies, which resolves locally but is absent for a consumer installing the package |
A declaration in devDependencies only is therefore a WARN, not a FAIL: it is enough for
route A (which does a local install) but not the shape you want to publish.
The print order is the table order above as it happens in a run; the numbering is the actual
item count of that run (the denominator varies by mode: 6b/7/8/9 only appear when they
apply). A run without --name and without a duplicate insert has 8 items.
Exit code: any FAIL → 1; PASS/WARN only → 0. WARN does not affect the exit code.
⚠️ Scope of the claim: --verify is a static check of the generated files (plus one
best-effort local import); it is not the same as "installed into a DSH profile and loaded
successfully". Install/load verification is a separate thing — see "Install and load
verification" below. The @deepseek-ai/dsh-* imports in index.js cannot resolve from a
standalone directory, so 6c honestly reports WARN ... SKIPPED (ERR_MODULE_NOT_FOUND) — that
is not a failure (a static check never installs anything), but it is not "verified loadable"
either. What 6b guarantees is the thing that actually broke: the import is declared.
Three runs, same generator, dated 2026-09-19 (DSH 0.1.6-alpha.1 global, Node v22.23.1).
dsh-fix-tool is a --with-tool bundle from the current generator; dsh-old-tool is the
same bundle from the shipped 0.1.1 generator, kept as the regression control.
… marks elided lines — the full text is in every run's own output.
$ node cli.mjs --verify /tmp/cdb-fix-e2e/gen-new/dsh-fix-tool # current generator
create-dsh-bundle --verify /tmp/cdb-fix-e2e/gen-new/dsh-fix-tool
[1/9] PASS All five generated files present (SOP §3) — all 5 present: package.json, index.js, cordis.patch.yml, README.md, .gitignore
[2/9] PASS package.json parses — name=dsh-fix-tool version=0.1.0
[3/9] PASS cordis.patch.yml exists and is well formed — block sequence, 1 insert(s)
[4/9] PASS package.json name matches a patch insert name — dsh-fix-tool
[5/9] PASS dsh.bundle.patch points at an existing file — ./cordis.patch.yml -> …/cordis.patch.yml
[6/9] PASS index.js has no default export (function plugin) — static scan: no default-export form found
[7/9] PASS every bare import in index.js is declared in package.json — 1 bare import(s), all declared: @deepseek-ai/dsh-tools -> peerDependencies
[8/9] WARN index.js runtime namespace (best effort) — SKIPPED (ERR_MODULE_NOT_FOUND: Cannot find package '@deepseek-ai/dsh-tools' imported from …/index.js) — static scan only, nothing was executed. …
[9/9] PASS inject contains 'tools' (tool example) — inject=[tools]
Summary: 8 PASS / 0 FAIL / 1 WARN
verify exit code: 0
$ node cli.mjs --verify /tmp/cdb-fix-e2e/gen-old/dsh-old-tool # the 0.1.1 artifact
[6/9] PASS index.js has no default export (function plugin) — static scan: no default-export form found
[7/9] FAIL every bare import in index.js is declared in package.json — undeclared bare import(s): @deepseek-ai/dsh-tools — in the local (link:) route these resolve from the plugin directory, so the import fails with ERR_MODULE_NOT_FOUND and `dsh --profile <name>` exits 1 before anything loads. …
… (8/9 WARN runtime namespace, 9/9 PASS inject, [1]-[5] PASS)
Summary: 7 PASS / 1 FAIL / 1 WARN
verify exit code: 1
$ node /tmp/cdb-fix-e2e/old-cli.mjs --verify /tmp/cdb-fix-e2e/gen-old/dsh-old-tool # 0.1.1's own gate
Summary: 7 PASS / 0 FAIL / 1 WARN
verify exit code: 0 # <- the fixed blind spot: a bundle that kills the
# profile reported as green
The minimal skeleton (no --with-tool, so no import at all) — 8 items, all green, and 6c gets
to import the real file:
$ node cli.mjs --verify /tmp/cdb-fix-e2e/gen-new/dsh-fix-min
[6/8] PASS index.js has no default export (function plugin) — static scan: no default-export form found
[7/8] PASS every bare import in index.js is declared in package.json — no bare imports
[8/8] PASS index.js runtime namespace (best effort) — imported OK, no default export, apply() present, keys=[apply, name]
… ([1]-[5] PASS as above)
Summary: 8 PASS / 0 FAIL / 0 WARN
verify exit code: 0
| What was broken | Result | Exit code |
|---|---|---|
Deleted README.md (or .gitignore) | 1 FAIL (item 1, names the missing file) | 1 |
Truncated package.json into invalid JSON | 4 FAIL (2/4/5/6b) | 1 |
Replace the insert name in the patch with another package name | 1 FAIL (item 4, marked startup-fatal) + 1 WARN | 1 |
Append export default { … } to index.js | 1 FAIL (item 6) + 1 WARN | 1 |
Write the patch in flow style - insert: [{id: x, name: y}] | 2 FAIL (items 3, 4) | 1 |
Add a stray indented line 4 stray: 1 to the patch | 2 FAIL (items 3, 4) | 1 |
Re-insert @deepseek-ai/dsh-tools | 1 FAIL (item 8, startup-fatal) + 1 WARN | 1 |
Remove peerDependencies from a --with-tool package.json (the import stays) | 1 FAIL (item 6b, names the undeclared package) | 1 |
Turn that import into any other undeclared bare import (import { Service } from 'cordis') | 1 FAIL (item 6b) | 1 |
Move the declaration into devDependencies only | 0 FAIL / 2 WARN (item 6b PASSes via devDependencies, one WARN says prefer peerDependencies) | 0 |
| Verify the artifact the shipped 0.1.1 generator produces | 1 FAIL (item 6b) — 0.1.1's own gate reported it as 7 PASS / 0 FAIL / 1 WARN | 1 vs 0 |
The first seven rows were run against 0.1.1 and re-run against the current generator; the last
four are the new gate. Raw evidence for the current round: evidence-2026-09-19-fix/ in the
project knowledge base (files 23-, 25-, 06-).
One row changed in 0.1.2: the duplicate insert was 0 FAIL / 2 WARN, exit code 0, while the very
same artifact was measured to abort startup (service "tools" has been registered, exit 1). It
is a FAIL now, so that row shows the current gate's verdict.
Real output fragments (reproducible: each case is a copy under /tmp/rf-011/neg-*; the gate
cases are 23-verify-neg-bare-cordis, 06-verify-newcli-on-old in the fix evidence):
$ node cli.mjs --verify /tmp/cdb-fix-e2e/neg/neg-bare-cordis # undeclared bare import
[6/8] PASS index.js has no default export (function plugin) — static scan: no default-export form found
[7/8] FAIL every bare import in index.js is declared in package.json — undeclared bare import(s): cordis — in the local (link:) route these resolve from the plugin directory, so the import fails with ERR_MODULE_NOT_FOUND and `dsh --profile <name>` exits 1 before anything loads. Add them to dependencies/peerDependencies in package.json (the --with-tool skeleton declares '@deepseek-ai/dsh-tools' as a peerDependency for exactly this reason)
Summary: 6 PASS / 1 FAIL / 1 WARN
verify exit code: 1
$ node cli.mjs --verify /tmp/cdb-fix-e2e/neg/neg-name-mismatch # insert name changed
[4/9] FAIL package.json name matches a patch insert name — package.json name="dsh-fix-tool" is not among patch insert names ["dsh-other-tool"] — startup-fatal (SOP §5.6): the insert name is what the loader imports, so `dsh --profile <name>` exits 1
Summary: 7 PASS / 1 FAIL / 1 WARN
verify exit code: 1
$ node cli.mjs --verify /tmp/rf-011/neg-default-export # export default appended
[6/8] FAIL index.js has no default export (function plugin) — found export default — the Loader drops the namespace (postmortem 0001)
Summary: 6 PASS / 1 FAIL / 1 WARN
verify exit code: 1
$ node cli.mjs --verify /tmp/rf-011/neg-stray-line # patch line 4 has broken indentation
[3/8] FAIL cordis.patch.yml exists and is well formed — line 4: unexpected indentation near "stray: 1"
[4/8] FAIL package.json name matches a patch insert name — cannot compare: package.json or cordis.patch.yml unreadable
Summary: 5 PASS / 2 FAIL / 1 WARN
verify exit code: 1
$ node cli.mjs --verify /tmp/rf-011/neg-missing-file # cordis.patch.yml deleted (item 1 catches the missing file first)
[1/8] FAIL All five generated files present (SOP §3) — missing 1/5: cordis.patch.yml — expected all of [package.json, index.js, cordis.patch.yml, README.md, .gitignore]
Summary: 3 PASS / 4 FAIL / 1 WARN
verify exit code: 1
$ node cli.mjs --verify /tmp/cdb-rel-neg-reinsert # dsh-tools re-inserted
[10/10] FAIL no re-insert of a service package dsh-base already provides — @deepseek-ai/dsh-tools re-inserted — dsh-base already provides these; `dsh --profile <name>` exits 1 with `service "..." has been registered` (SOP §8.2)
Summary: 8 PASS / 1 FAIL / 1 WARN
verify exit code: 1
(neg-reinsert is the --with-tool artifact of the paragraph above (9 items) plus the duplicate
insert, which makes item 8 appear — hence 10. The remaining examples use minimal-skeleton
artifacts, which have 8.)
Also: comparing md5 snapshots of the target directory before and after --verify → no change at all (read-only).
$ node cli.mjs --out /tmp/never-gen
Error: --name is required.
$ echo $?
1
$ node cli.mjs --name Dsh-Bad --out /tmp/never-gen
Error: --name "Dsh-Bad" is not a valid package name (lowercase letters, digits, hyphens; must start with a letter).
$ echo $?
1
$ node cli.mjs --name dsh-demo-x --desc "demo" --with-tool --out /tmp/dsh-demo-x
Error: output directory already exists: /tmp/dsh-demo-x
refusing to overwrite — pass a different --out, or remove that path first.
$ echo $?
1
$ node cli.mjs --bogus
Error: Unknown option '--bogus'
(prints USAGE)
$ echo $?
1
--name 1bad and --name dsh_bad exit 1 as well. An existing directory is always refused; there is no --force: either pick another --out or delete the path yourself first.
This section is the evidence that the bundle really installs into DSH and loads; it is a
different thing from the static --verify above.
State the mode. pnpm dsh inside a deepseek-harness source check-out (the tsx launcher)
and a global dsh (fallback links inside DSH_HOME) are two different resolution paths,
and passing in the first says nothing about the second. Every run below is the mode that
matters for an outside user: global dsh + a clean, isolated DSH_HOME + an install route the
user actually has. Environment: dsh 0.1.6-alpha.1 (global), Node v22.23.1, pnpm 11.15.1.
dsh plugin add <name>): the recommended path$ cd dsh-fix-tool && npm pack --pack-destination /tmp/cdb-fix-e2e/routeB # stands in for the
# published tarball
$ export DSH_HOME=/tmp/cdb-fix-e2e/home-B # isolated home, ~/.dsh never touched
$ dsh plugin --profile demoB add /tmp/cdb-fix-e2e/routeB/dsh-fix-tool-0.1.0.tgz
$ echo $?
0
$ dsh --profile demoB --dump-config | grep -A3 dsh-fix-tool
# == dsh-fix-tool
- id: fix-tool
name: dsh-fix-tool
$ timeout 20 dsh --profile demoB
[fix-tool] plugin loaded!
[fix-tool] hello from my first plugin
[fix-tool] greet replied: [{"type":"text","text":"Hello, Cordis!"}]
$ echo $?
124
Read out of that:
# == dsh-fix-tool block in --dump-config means the generated cordis.patch.yml was
inserted as a layer (the profile's own patch is the empty []).agents: [], so timeout had to kill it.dsh plugin add <package> installs the
package inside $DSH_HOME/profiles/<name>/, where the harness's fallback links for
@deepseek-ai/* are reachable. The shipped 0.1.1 artifact (which declares nothing at all)
boots this way too, which is exactly why the community plugins declare peerDependencies and
not dependencies.link:): development only, one prerequisite$ cd /tmp/cdb-fix-e2e/routeA/dsh-fix-tool && pnpm install
dependencies:
+ @deepseek-ai/dsh-tools 0.1.6-alpha.2
Done in 657ms using pnpm v11.15.1
$ export DSH_HOME=/tmp/cdb-fix-e2e/home-A
$ dsh plugin --profile demoA add "$PWD"
$ timeout 20 dsh --profile demoA
[fix-tool] plugin loaded!
[fix-tool] hello from my first plugin
[fix-tool] greet replied: [{"type":"text","text":"Hello, Cordis!"}]
$ echo $?
124
pnpm install picked up the declared peerDependencies (pnpm auto-installs missing peers) and
put @deepseek-ai/dsh-tools into the plugin's own node_modules — which is the whole point:
the link: target is outside DSH_HOME, so that directory has to resolve the import itself.
Remove that one command and the profile does not start. Same artifact, same add, only the
prerequisite taken away:
$ dsh plugin --profile demoA0 add /tmp/cdb-fix-e2e/routeA0/dsh-fix-tool
$ timeout 20 dsh --profile demoA0
Error: dsh: plugin tree failed to load: failed to apply loader entry include (cordis:include):
failed to import loader entry fix-tool (dsh-fix-tool): Cannot find package
'@deepseek-ai/dsh-tools' imported from /private/tmp/cdb-fix-e2e/routeA0/dsh-fix-tool/index.js
Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@deepseek-ai/dsh-tools' imported from …
$ echo $?
1
That exit code 1 is the entire incident: one unresolvable import in one layer, and nothing
loads. It is why the generated README states the prerequisite instead of implying it, why
--verify check 6b is a FAIL rather than a WARN, and why the rescue command
(dsh plugin --profile <name> remove <package>) is documented in the generated README.
The official packaging/installation tutorial lives in the DSH source tree, not in this package:
docs/user/develop/basic/publish.md (Chinese: publish.zh.md) — "Packaging and installing plugins": covers the two concepts (bundle / profile), the dsh.bundle manifest, dsh plugin add, and layer order. One line from it is worth repeating here: "A bundle is what you write and distribute; a profile is what the user starts with dsh --profile <name>. Nothing is both at the same time." (translated from the Chinese edition — check the original wording in your checkout).docs/user/develop/basic/first-plugin.md / tool.md / config.md — minimal plugin skeleton, defineTool, config layers.docs/cordis-tutorial/ (7 chapters) — the underlying Cordis concepts.--with-tool writes this into the generated package.json:
"peerDependencies": {
"@deepseek-ai/dsh-tools": "^0.1.6-alpha.1 || ^0.1.5-rc.2"
}
peerDependencies, not dependencies — matching the community plugins (dsh-airdrop,
dsh-plugin-subscriptions): the harness provides the module at runtime, so the declaration
says which generation the plugin was written against without dragging a second copy into
every profile.@deepseek-ai/*). It is the prerequisite for a local install (route A), and it is what
--verify 6b checks.|| is not decoration. Every published @deepseek-ai/dsh-tools version is a
prerelease, and semver only lets a prerelease satisfy a range when some comparator carries a
prerelease on the same major.minor.patch. Measured against the registry (mirror):