156 lines
7.7 KiB
Markdown
156 lines
7.7 KiB
Markdown
# 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/<name>` 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 <name>`. 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 <name> <args...>` 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 <path>` 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#<sha>`) 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
|