← Back to home@RubyCcll

create-dsh-bundle

No description

Stars
0
Language
JavaScript
Created
Sep 12, 2026
Updated
Sep 19, 2026

Introduction

create-dsh-bundle

中文 | 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.

  • The generator itself is not a DSH plugin and does not declare dsh.bundle (see "Why this package does not declare dsh.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-plugin is already taken by a third party, so it is not used here.

Install

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.

Usage

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

Full parameter list

OptionModeRequiredDescription
--name <pkg>generateyesPackage 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>generatenopackage.json description; a one-line default is used when omitted
--with-toolgeneratenoGenerate the greet tool example (defineTool + inject = ['tools'] + one real tool call). Omit it for a minimal skeleton
--out <dir>generatenoOutput directory, defaults to ./<name>. An existing directory is refused (exit 1); there is no force flag
--verify <dir>verifyyesVerify the generated files in that directory; read-only, exit code 0 = all PASS, 1 = any FAIL
-h, --help—noPrint 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.

Name derivation rules

Follows the three-part convention of the official dsh-hello template (example with --name dsh-approval-gate):

NameRuleResult
package nameas givendsh-approval-gate
export name (name in index.js)drop the dsh- prefixapproval-gate
cordis id (id in cordis.patch.yml)export name, drop a -plugin suffixapproval-gate

Generated file structure

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.

What --verify checks

create-dsh-bundle --verify <dir> re-reads the generated files without writing anything and without installing anything. It prints PASS / FAIL / WARN per item:

#CheckFAILs when
1All 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)
2package.json parsesFile missing / JSON.parse throws / root is not an object
3cordis.patch.yml exists and is well formedMissing; or the built-in YAML subset parser fails (flow style [{...}], wrong indentation, extra lines all count)
4package.json name equals the patch insert nameNo 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
5dsh.bundle.patch points at an existing filedsh.bundle.patch is not declared, or it is declared but the file does not exist
6index.js has no default exportexport default / export { x as default } / module.exports = / exports.default = found
6bEvery bare import in index.js is declared in package.jsonA 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
7In 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
8No 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
9Cross-check against --nameOnly runs when --name was also passed
6cindex.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 devNever 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.

Actual output (real runs, not examples)

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

Negative-case matrix (all of these were actually run: break a generated file on purpose, see whether it FAILs)

What was brokenResultExit code
Deleted README.md (or .gitignore)1 FAIL (item 1, names the missing file)1
Truncated package.json into invalid JSON4 FAIL (2/4/5/6b)1
Replace the insert name in the patch with another package name1 FAIL (item 4, marked startup-fatal) + 1 WARN1
Append export default { … } to index.js1 FAIL (item 6) + 1 WARN1
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 patch2 FAIL (items 3, 4)1
Re-insert @deepseek-ai/dsh-tools1 FAIL (item 8, startup-fatal) + 1 WARN1
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 only0 FAIL / 2 WARN (item 6b PASSes via devDependencies, one WARN says prefer peerDependencies)0
Verify the artifact the shipped 0.1.1 generator produces1 FAIL (item 6b) — 0.1.1's own gate reported it as 7 PASS / 0 FAIL / 1 WARN1 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).

Generation-side failure paths (real output; none of these is allowed to succeed silently)

$ 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.

Install and load verification (end-to-end, actually run)

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.

Route B — the published package (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:

  1. The # == 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 []).
  2. 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: [], so timeout had to kill it.
  3. 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 declare peerDependencies and not dependencies.

Route A — a local directory (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.

Relation to the official publish guide

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.

Dependency declarations in the generated bundle (peerDependencies)

--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.

  • 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 --verify 6b checks.

  • The || 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):

    rangeresolves 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). --verify 6b 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.

Why this package does not declare dsh.bundle

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.

Pitfalls

  1. Never mix a default export into a function plugin. With named exports name / inject / apply present, adding export default makes the Loader drop the whole namespace (the real incident in the official postmortem 0001). Check 6 of --verify exists for exactly this.
  2. Do not re-insert @deepseek-ai/dsh-tools / @deepseek-ai/dsh-system-prompt. dsh-base already provides the tools service; inserting it again gives service "tools" has been registered. To use tools, just write export const inject = ['tools'] in your plugin.
  3. 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 is dsh 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 --verify fails (not warns) on the two startup-fatal shapes it can see statically: an undeclared bare import (6b) and a patch insert name that is not the package name (4).
  4. The cordis.patch.yml insert name must equal your package name (SOP §5.6). The loader imports the insert name; a mismatch is failed to import loader entry … and startup exits 1 — the same fatal class as an unresolvable dependency, and equally invisible until you actually boot.
  5. --with-tool imports @deepseek-ai/dsh-tools: declare it. The generator writes it into peerDependencies; if you delete that block, --verify 6b FAILs by design. peerDependencies (not dependencies) matches the community plugins — see "Dependency declarations in the generated bundle".
  6. Know which resolution path you are on. A dependency resolves from where the file lives: with dsh plugin add <dir> (pnpm link:) the file is your source directory, which is usually outside DSH_HOME, so the import must be installed there (pnpm install, as the generated README says). With dsh 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 run pnpm dsh from a deepseek-harness check-out, never bare node" advice describes the check-out mode only (workspace packages live in the tsx launcher) — it is not a requirement for an installed global dsh, and it was never something an outside user could follow.
  7. dsh --profile demo does not exit on its own (the agent loop idles on agents: []); getting 124 from timeout is normal, not a failure.
  8. --dump-config output is long (it includes the whole dsh-base layer); use grep -A3 '<your-package-name>' to look at your own layer only.
  9. link: installs need no re-add — edit index.js and re-run dsh --profile <name> (this holds for route A only; route B installs a copied package, so re-publish and re-add).
  10. Isolate the home: always export DSH_HOME=/tmp/xxx so tests do not pollute ~/.dsh.
  11. 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.

Packaging and release-readiness verification

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.

⚠️ Hard prerequisite when publishing from this machine (its default registry is a mirror — measured 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.

Development

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 --help

Zero 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).

License

MIT