中文 | 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-toolOne 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.
- The generator itself is not a DSH plugin and does not declare
dsh.bundle(see "Why this package does not declaredsh.bundle" below). - Non-interactive is a hard requirement: everything is parameter-driven (Node's built-in
util.parseArgs), with no readline/inquirer prompts, so it runs in headless environments, cron jobs and subagents without waiting for an answer. - Package/binary name
create-dsh-bundle:create-dsh-pluginis 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 globallyRequires 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-xThe 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.
create-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.
--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 greenThe 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: 0Negative-case matrix (all of these were actually run: break a generated file on purpose, see whether it FAILs)
| 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.
$ 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 $?
124Read out of that:
- The
# == dsh-fix-toolblock in--dump-configmeans the generatedcordis.patch.ymlwas inserted as a layer (the profile's own patch is the empty[]). - The three log lines are "loaded + the tool really ran"; exit code 124 is normal — the
plugin finished and the agent loop idled on
agents: [], sotimeouthad to kill it. - Route B needs no dependency work from the author:
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 declarepeerDependenciesand notdependencies.
$ 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 $?
124pnpm 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 $?
1That 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), thedsh.bundlemanifest,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 withdsh --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, notdependencies— 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. -
Route B does not actually need the declaration (the profile's fallback links resolve
@deepseek-ai/*). It is the prerequisite for a local install (route A), and it is what--verify6b checks. -
The
||is not decoration. Every published@deepseek-ai/dsh-toolsversion is a prerelease, and semver only lets a prerelease satisfy a range when some comparator carries a prerelease on the samemajor.minor.patch. Measured against the registry (mirror):range resolves to *0.0.1-rc.1>=0.1.1-rc.20.1.1-rc.2^0.1.5-rc.20.1.5-rc.2^0.1.6-alpha.1 || ^0.1.5-rc.20.1.5-rc.2,0.1.6-alpha.1,0.1.6-alpha.2(
npm view @deepseek-ai/dsh-tools@'<range>' version.) A "reasonable looking" caret range on a prerelease line silently resolves to a single old version — which is how a plugin ends up pointing at a generation nobody is running. -
Refresh the range when the official line moves (
npm view @deepseek-ai/dsh-tools dist-tags).--verify6b only checks that the import is declared; keeping the range honest is the author's call. -
The minimal skeleton imports nothing and therefore declares nothing.
create-dsh-bundle is a scaffolder + verifier, not a Cordis plugin: it has no index.js plugin entry, provides no service or tool, and has no cordis.patch.yml. By the official definition, dsh.bundle is the declaration of "which config layer this package contributes"; we contribute no layer. Forcing a dsh.bundle.patch in would mean a user's dsh plugin add create-dsh-bundle tries to load a nonexistent or meaningless plugin layer — that is writing a broken config into a profile. So this package declares bin and no dsh.bundle.
- Never mix a
default exportinto a function plugin. With named exportsname/inject/applypresent, addingexport defaultmakes the Loader drop the whole namespace (the real incident in the official postmortem 0001). Check 6 of--verifyexists for exactly this. - Do not re-insert
@deepseek-ai/dsh-tools/@deepseek-ai/dsh-system-prompt. dsh-base already provides thetoolsservice; inserting it again givesservice "tools" has been registered. To use tools, just writeexport const inject = ['tools']in your plugin. - A plugin that fails to load takes the whole profile down — exit code 1, nothing starts. There is no partial start: one unresolvable import or one bad layer and
dsh --profile <name>terminates. The rescue command isdsh plugin --profile <name> remove <package>; run it from a shell, the profile does not need to boot first. This is why the generated README ships it and why--verifyfails (not warns) on the two startup-fatal shapes it can see statically: an undeclared bare import (6b) and a patch insertnamethat is not the package name (4). - The
cordis.patch.ymlinsertnamemust equal your packagename(SOP §5.6). The loader imports the insert name; a mismatch isfailed to import loader entry …and startup exits 1 — the same fatal class as an unresolvable dependency, and equally invisible until you actually boot. --with-toolimports@deepseek-ai/dsh-tools: declare it. The generator writes it intopeerDependencies; if you delete that block,--verify6b FAILs by design.peerDependencies(notdependencies) matches the community plugins — see "Dependency declarations in the generated bundle".- Know which resolution path you are on. A dependency resolves from where the file lives: with
dsh plugin add <dir>(pnpmlink:) the file is your source directory, which is usually outsideDSH_HOME, so the import must be installed there (pnpm install, as the generated README says). Withdsh plugin add <published-package>the files land inside$DSH_HOME/profiles/, where the harness's fallback links resolve@deepseek-ai/*for you. The old "must runpnpm dshfrom a deepseek-harness check-out, never barenode" advice describes the check-out mode only (workspace packages live in the tsx launcher) — it is not a requirement for an installed globaldsh, and it was never something an outside user could follow. dsh --profile demodoes not exit on its own (the agent loop idles onagents: []); getting 124 fromtimeoutis normal, not a failure.--dump-configoutput is long (it includes the whole dsh-base layer); usegrep -A3 '<your-package-name>'to look at your own layer only.link:installs need no re-add — editindex.jsand re-rundsh --profile <name>(this holds for route A only; route B installs a copied package, so re-publish and re-add).- Isolate the home: always
export DSH_HOME=/tmp/xxxso tests do not pollute~/.dsh. - Stay non-interactive: this tool (and any generator you write from it) must not introduce readline/inquirer — in a headless run nobody is there to answer and it will hang.
This package has been through full packaging verification (npm pack → install the tarball into an isolated directory → npx create-dsh-bundle --help → npm publish --dry-run); the raw output for each step is in VERIFICATION.md in the repository (that file is not part of the published artifact — files lists only cli.mjs, README.md, CHANGELOG.md and docs/zh-CN.md, and the Chinese translation deliberately lives outside the root README* namespace). create-dsh-bundle@0.1.0 is published on npm (repository pushed to GitHub); the release status is whatever the npm page says: https://www.npmjs.com/package/create-dsh-bundle
0.1.2 is prepared but not published — it carries the dependency gate, the peerDependencies
declaration and the rewritten generated README described above (see CHANGELOG.md). Publishing is
a separate, outward-facing step.
npm config get registry = https://registry.npmmirror.com): always pass the registry explicitly, otherwise you publish to the Chinese mirror instead: npm login --registry=https://registry.npmjs.org followed by npm publish --registry=https://registry.npmjs.org. Commands and measured output: VERIFICATION.md §9.1.
sh test/smoke.sh # or npm test: generate → assert the file list (test -f per file) → verify → negative case (default export) → three failure paths → minimal skeleton
node cli.mjs --helpZero dependencies, a single cli.mjs (containing the built-in YAML subset parser and the verification logic). After changing cli.mjs, run test/smoke.sh: it asserts not only exit codes but the generated file list file by file (a missing output file fails the run instead of staying silently green).
MIT