# Package and install a plugin English | [中文](publish.zh.md) The previous tutorials loaded a local plugin through a `--patch` overlay. This tutorial packages it as an installable **bundle**, installs it into a **profile** with `dsh plugin add`, and explains the layer order that determines the composed configuration. Complete [plugin configuration](./config.md) first. ## Two concepts, two manifests Installation is built on two concepts. Both are described by a `package.json`, but they carry different kinds of manifest under the `dsh` key, and they answer different questions: - A **bundle** is an npm package that ships a configuration layer. Its manifest declares `dsh.bundle`, answering "what does this package contribute?": a patch file that inserts or overrides plugin rows. - A **profile** is a directory under `$DSH_HOME/profiles/` describing one runnable composition. Its manifest declares `dsh.profile`, answering "which bundles compose this setup, in what order?". A bundle is what you author and distribute; a profile is what a user boots with `dsh --profile `. Nothing is both. ### The bundle manifest ``` hello-plugin/ ├── package.json # declares dsh.bundle ├── cordis.patch.yml # the layer applied when a profile lists this bundle └── index.js # plugin modules the patch rows reference ``` ```json { "name": "dsh-hello-plugin", "version": "0.1.0", "type": "module", "main": "index.js", "files": ["index.js", "cordis.patch.yml"], "dsh": { "bundle": { "patch": "./cordis.patch.yml" } } } ``` The patch file is a YAML array of patch entries, like the `--patch` overlays you have been writing, except plugin rows reference the package by name instead of a relative source path so Node resolution finds the installed code: ```yaml - insert: - id: hello name: dsh-hello-plugin ``` A package without the `dsh.bundle` declaration still installs, but only as a plain dependency: `dsh plugin` prints a warning and activates no layer. Use that package format for a library that plugin packages import rather than a plugin users enable. ### The profile manifest A profile directory holds two files: - `package.json` — the profile's out-of-tree plugin dependencies (managed by pnpm) plus the `dsh.profile` manifest with its ordered `bundles` list. - `cordis.patch.yml` — the user's own patch layer, applied after every bundle layer. You never write a profile manifest by hand: `dsh plugin` creates and maintains it. The next section shows the result. ## Install into a profile `dsh plugin --profile ` forwards to pnpm in the profile directory, so every pnpm verb works. Install your package from its checkout: ```sh cd hello-plugin dsh plugin --profile demo add . ``` The first use initializes the profile (with `@deepseek-ai/dsh-base` as its first bundle), pnpm links the checkout, and `dsh` appends the bundle to `dsh.profile.bundles` because the package declares `dsh.bundle`: ```json { "name": "dsh-profile-demo", "private": true, "dependencies": { "dsh-hello-plugin": "link:/path/to/hello-plugin" }, "dsh": { "profile": { "bundles": [ "@deepseek-ai/dsh-base", "dsh-hello-plugin" ] } } } ``` Verify the layer without booting, then boot: ```sh dsh --profile demo --dump-config # shows a "# == dsh-hello-plugin" layer dsh --profile demo ``` `dsh plugin --profile demo remove dsh-hello-plugin` removes both the dependency and the layer. ## The loading order The effective configuration composes over an empty root by applying, in order: 1. Each bundle patch named in the profile's `dsh.profile.bundles` list, in list order — `@deepseek-ai/dsh-base` first, then each installed bundle in the order it was added. 2. The profile's own `cordis.patch.yml`. 3. The home-level `$DSH_HOME/cordis.patch.yml` — machine-local preferences shared by every profile. 4. Each `--patch ` overlay, in argv order. App arguments are not another patch layer. A surface bundle can resolve them through a startup service, described below. Later layers win per row, and a patch replaces a row's entire `config` value rather than deep-merging keys. Two consequences for bundle authors: - Your patch can override rows from earlier layers by `id` — the same way [the `dsh-web-app` bundle](../../../../packages/bundle/web-app/cordis.patch.yml) overrides `dsh-base` rows — but must restate every key the row needs, not just the changed one. - Users can override your rows in their profile's `cordis.patch.yml` without touching your package, so prefer configuration defaults users are likely to keep and let the schema carry the rest. In-box bundle names always resolve from the dsh installation itself; pnpm manages only out-of-tree packages, so your bundle can rely on `@deepseek-ai/dsh-base` being present and current. ## Give a surface bundle its own command line A bundle that defines a runnable app marks its startup row through the injection it already requires: ```yaml - id: hello-startup name: 'dsh-hello-plugin/startup' inject: [cmdlineArgs] ``` That row calls `runStartup` from [`@deepseek-ai/dsh-cmdline`](../../../../packages/ui/cmdline/README.md) with the app's own commander program. The launcher hands it every argument after the launcher flags, so app-specific flags need no launcher change. Loader mounts the composition once, waits for each row's injections, and only then evaluates that row's `!!js` config against its injected context. Rows configured by those arguments inject the startup service and read it from their own `!!js` options, with the deployment value beside it as the fallback. On `--help`, the service is not provided, so those rows never activate. An app layered over another app disables the lower startup row, because one composition has one command-line owner. ## Installing from GitHub: the build-script catch Publishing to a registry is not required — users can install straight from a git host: ```sh dsh plugin --profile demo add github:you/hello-plugin ``` But a git install fetches **sources, not built artifacts**: nothing runs your `build` script, so a TypeScript package arrives without its `lib/` output and fails to load. Two things must happen, one on each side: - **The author** ships a `prepare` script — pnpm runs it after a git install — that builds the published entry points from source, self-contained: it must not assume dev-only context such as a sibling monorepo checkout. [turtle-ui](https://github.com/deepseek-harness/turtle-ui) is a working example: its `prepare` runs a dedicated tsdown config that transpiles `src/` without project references or type checking. - **The user** allowlists the build. pnpm ≥10 refuses to run a git dependency's `prepare` script until it is explicitly allowed, so the first `add` fails; `dsh` points at the fix — copy the exact package key pnpm printed into the profile's `pnpm-workspace.yaml`: ```yaml allowBuilds: dsh-hello-plugin: true ``` and re-run the `add`. Treat that allowance as what it is: **permission to execute the package's code on your machine at install time**, outside any sandbox the agent runs under. Only allow packages whose source you trust, and pin a commit (`github:you/hello-plugin#`) so a later push cannot silently change what runs. If you would rather not ask users for the allowance, distribute built artifacts instead — neither form needs any build permission: - **Publish to npm** with `lib/` built at `pnpm publish` time; `dsh plugin add your-package` then installs prebuilt code. - **Ship a tarball** from `pnpm pack`; users run `dsh plugin add ./hello-plugin-0.1.0.tgz`. ## Next steps - [Plugins and lifecycle](../framework/) — the full plugin lifecycle - [CLI behavior reference](../../../../apps/cli/reference/README.md) — exact layer precedence, flags, and profile mechanics