From 20acfdb71d6cac1f8167f2b693c83d2b3d6c0b46 Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 6 Aug 2026 17:28:40 +0800 Subject: [PATCH] docs: tutorial for packaging and installing a plugin bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds docs/user/develop/basic/publish.md (+ zh pair, website entry) to the basics path: the bundle-vs-profile manifest split, dsh plugin add into a profile, the five-layer loading order, and the GitHub-install build-script catch — git specs ship sources, so the author owns a self-contained prepare script and the user owns an allowBuilds allowance that is install-time code execution; built tarballs and npm need neither. --- docs/user/develop/basic/config.i18n.yaml | 4 +- docs/user/develop/basic/config.md | 1 + docs/user/develop/basic/config.zh.md | 1 + docs/user/develop/basic/publish.i18n.yaml | 6 + docs/user/develop/basic/publish.md | 140 ++++++++++++++++++++++ docs/user/develop/basic/publish.zh.md | 140 ++++++++++++++++++++++ website/docs.ts | 8 ++ 7 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 docs/user/develop/basic/publish.i18n.yaml create mode 100644 docs/user/develop/basic/publish.md create mode 100644 docs/user/develop/basic/publish.zh.md diff --git a/docs/user/develop/basic/config.i18n.yaml b/docs/user/develop/basic/config.i18n.yaml index 7fca045189..6bf0ddea72 100644 --- a/docs/user/develop/basic/config.i18n.yaml +++ b/docs/user/develop/basic/config.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/user/develop/basic/config.md -config.md: 11a2311464789f74537cc7c4435f83ec07ca26fd -config.zh.md: 4e827ecafa6bfaf87c3e3f118425e656e1254787 +config.md: 02998c32415b5ba7acf82700034cabc1f7314f33 +config.zh.md: 42af432b36d8f82871aa7d7b6a3a2eaf7427cdae diff --git a/docs/user/develop/basic/config.md b/docs/user/develop/basic/config.md index 11a2311464..02998c3241 100644 --- a/docs/user/develop/basic/config.md +++ b/docs/user/develop/basic/config.md @@ -101,5 +101,6 @@ A configuration edit hot-replaces the plugin: the framework unloads the old inst ## Next steps +- [Package and install a plugin](./publish.md) — ship the plugin as an installable package - [Plugins and lifecycle](../framework/) — understand the full plugin lifecycle - [Services and dependencies](../framework/service.md) — provide a service to other plugins diff --git a/docs/user/develop/basic/config.zh.md b/docs/user/develop/basic/config.zh.md index 4e827ecafa..42af432b36 100644 --- a/docs/user/develop/basic/config.zh.md +++ b/docs/user/develop/basic/config.zh.md @@ -101,5 +101,6 @@ export interface Config { ## 下一步 +- [打包与安装插件](./publish.md) — 把插件以可安装包的形式交付 - [插件与生命周期](../framework/) — 深入了解插件的完整生命周期 - [服务与依赖](../framework/service.md) — 让你的插件对外提供服务 diff --git a/docs/user/develop/basic/publish.i18n.yaml b/docs/user/develop/basic/publish.i18n.yaml new file mode 100644 index 0000000000..da74ce8d7c --- /dev/null +++ b/docs/user/develop/basic/publish.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/user/develop/basic/publish.md +publish.md: 1d1179a78c4d3a7e9e7055e3f5ee41381e28147e +publish.zh.md: 26d1c2b737a3498bce0757d5b2d14e254fc13527 diff --git a/docs/user/develop/basic/publish.md b/docs/user/develop/basic/publish.md new file mode 100644 index 0000000000..1d1179a78c --- /dev/null +++ b/docs/user/develop/basic/publish.md @@ -0,0 +1,140 @@ +# 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 has the same shape as the `--patch` overlays you have been writing — a YAML array of patch entries — 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. That is the correct shape 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. +5. Launcher flag patches (for example `dsh web --port`). + +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. + +## 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 diff --git a/docs/user/develop/basic/publish.zh.md b/docs/user/develop/basic/publish.zh.md new file mode 100644 index 0000000000..26d1c2b737 --- /dev/null +++ b/docs/user/develop/basic/publish.zh.md @@ -0,0 +1,140 @@ +# 打包与安装插件 + +[English](publish.md) | 中文 + +前几篇教程通过 `--patch` overlay 加载本地插件。本教程把它打包成可安装的**组合包**(bundle),用 `dsh plugin add` 安装进一个 **profile**,并解释决定组合后配置的层顺序。请先完成[插件配置](./config.md)。 + +## 两个概念,两种 manifest + +安装机制建立在两个概念之上。二者都由一份 `package.json` 描述,但它们在 `dsh` 键下携带的 manifest(元数据清单)种类不同,回答的问题也不同: + +- **组合包**是附带一个配置层的 npm 包。它的 manifest 声明 `dsh.bundle`,回答的是"这个包贡献什么?":一个插入或覆盖插件行的 patch 文件。 +- **profile** 是位于 `$DSH_HOME/profiles/` 下、描述一份可启动组合的目录。它的 manifest 声明 `dsh.profile`,回答的是"这套配置由哪些组合包按什么顺序组成?"。 + +组合包是你编写并分发的东西;profile 是用户用 `dsh --profile ` 启动的东西。没有东西同时是两者。 + +### 组合包 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" } } +} +``` + +patch 文件的形状与你一直在写的 `--patch` overlay 相同——一个 patch 条目的 YAML 数组——只是插件行按包名而不是相对源码路径引用这个包,这样 Node 的模块解析才能找到已安装的代码: + +```yaml +- insert: + - id: hello + name: dsh-hello-plugin +``` + +没有 `dsh.bundle` 声明的包仍然可以安装,但只作为普通依赖:`dsh plugin` 会打印警告,且不激活任何层。这正是"供插件包 import 的库"应有的形状,区别于"供用户启用的插件"。 + +### profile manifest + +profile 目录包含两个文件: + +- `package.json` — profile 的树外插件依赖(由 pnpm 管理),加上 `dsh.profile` manifest 及其有序的 `bundles` 列表。 +- `cordis.patch.yml` — 用户自己的 patch 层,在每个组合包层之后应用。 + +profile manifest 从不需要手写:`dsh plugin` 负责创建和维护它。下一节展示其结果。 + +## 安装进 profile + +`dsh plugin --profile ` 在 profile 目录内转发给 pnpm,因此所有 pnpm 子命令都可用。从 checkout 安装你的包: + +```sh +cd hello-plugin +dsh plugin --profile demo add . +``` + +首次使用会初始化 profile(`@deepseek-ai/dsh-base` 作为它的第一个组合包),pnpm 链接该 checkout,而 `dsh` 因为这个包声明了 `dsh.bundle`,把它追加进 `dsh.profile.bundles`: + +```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" + ] + } + } +} +``` + +先不启动、只验证该层,再启动: + +```sh +dsh --profile demo --dump-config # shows a "# == dsh-hello-plugin" layer +dsh --profile demo +``` + +`dsh plugin --profile demo remove dsh-hello-plugin` 会同时移除依赖和对应的层。 + +## 加载顺序 + +生效配置在空根之上按以下顺序逐层组合: + +1. profile 的 `dsh.profile.bundles` 列表所列的各个组合包 patch,按列表顺序——先是 `@deepseek-ai/dsh-base`,然后是每个已安装组合包,按其加入顺序。 +2. profile 自己的 `cordis.patch.yml`。 +3. home 级的 `$DSH_HOME/cordis.patch.yml`——各 profile 共享的机器本地偏好。 +4. 每个 `--patch ` overlay,按 argv 顺序。 +5. 启动器 flag patch(例如 `dsh web --port`)。 + +后应用的层按行胜出,且 patch 会替换目标行的整个 `config` 值,而不是深度合并各键。这给组合包作者带来两个推论: + +- 你的 patch 可以按 `id` 覆盖前面各层的行——就像 [`dsh-web-app` 组合包](../../../../packages/bundle/web-app/cordis.patch.yml)覆盖 `dsh-base` 的行那样——但必须重述该行需要的每一个键,而不是只写改动的那个。 +- 用户可以在自己 profile 的 `cordis.patch.yml` 中覆盖你的行,无需改动你的包,所以优先给出用户大概率会保留的配置默认值,其余交给 schema 承担。 + +内置组合包名称始终从 dsh 安装目录本身解析;pnpm 只管理树外的包,所以你的组合包可以放心依赖 `@deepseek-ai/dsh-base` 存在且与安装保持一致。 + +## 从 GitHub 安装:构建脚本这道坎 + +发布到注册表不是必须的——用户可以直接从 git 托管安装: + +```sh +dsh plugin --profile demo add github:you/hello-plugin +``` + +但 git 安装拉取的是**源码,不是构建产物**:没有任何环节运行你的 `build` 脚本,因此 TypeScript 包到手时没有 `lib/` 输出,加载会失败。必须两边各做一件事: + +- **作者**提供一个 `prepare` 脚本——pnpm 在 git 安装后运行它——从源码构建出发布入口,且必须自包含:不能假设仅开发环境才有的上下文,例如旁边有一份 monorepo checkout。[turtle-ui](https://github.com/deepseek-harness/turtle-ui) 是一个可用的例子:它的 `prepare` 运行一份专用的 tsdown 配置,直接转译 `src/`,不用项目引用,也不做类型检查。 +- **用户**为构建授权。pnpm ≥10 在得到显式允许之前拒绝运行 git 依赖的 `prepare` 脚本,所以第一次 `add` 会失败;`dsh` 会指出修法——把 pnpm 打印的确切包键复制进该 profile 的 `pnpm-workspace.yaml`: + + ```yaml + allowBuilds: + dsh-hello-plugin: true + ``` + + 然后重新执行 `add`。 + +请如实看待这项授权:**允许该包的代码在安装时于你的机器上执行**,且不在 agent 运行的任何沙箱之内。只对源码可信的包授权,并锁定 commit(`github:you/hello-plugin#`),让后续推送无法悄悄改变实际运行的内容。 + +如果不想让用户做这项授权,就改为分发构建产物——以下两种形式都不需要任何构建权限: + +- **发布到 npm**,在 `pnpm publish` 时构建好 `lib/`;`dsh plugin add your-package` 安装的就是预构建代码。 +- **交付 tarball**:用 `pnpm pack` 打包;用户执行 `dsh plugin add ./hello-plugin-0.1.0.tgz`。 + +## 下一步 + +- [插件与生命周期](../framework/) — 插件的完整生命周期 +- [CLI 行为参考](../../../../apps/cli/reference/README.md) — 确切的层优先级、flag 与 profile 机制 diff --git a/website/docs.ts b/website/docs.ts index 8d42ae3209..1a9b20b5be 100644 --- a/website/docs.ts +++ b/website/docs.ts @@ -166,6 +166,14 @@ const develop = pairedPages([ section: { root: '基础', en: 'Basics' }, order: 3, }, + { + source: 'docs/user/develop/basic/publish.md', + route: 'develop/basic/publish.md', + label: { root: '打包与安装插件', en: 'Package and install' }, + sidebar: { root: 'zh-develop', en: 'en-develop' }, + section: { root: '基础', en: 'Basics' }, + order: 4, + }, { source: 'docs/user/develop/framework/index.md', route: 'develop/framework/index.md',