From a1286191d60d7ba7aca269dbb22133a191b890ed Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Sun, 9 Aug 2026 23:49:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20plugin-registry=20=E6=A0=87=E5=87=86?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E9=9D=A2=EF=BC=88dsh.plugin.json=20+=20?= =?UTF-8?q?=E5=8F=8C=20client=20bundle=20+=20registry=20=E6=89=93=E5=8C=85?= =?UTF-8?q?=EF=BC=89=EF=BC=8Cv0.1.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dsh.plugin.json 原生清单:id dsh-external/dsh-better-sidebar,version 0.1.2, main = lib/index.js,client.main = lib/client-registry.js,contributes 空声明 - tsdown client 构建参数化:lib/client.js(官方通道,id = 包名)+ lib/client-registry.js (registry 通道,id = 清单 id),同源不漂移 - scripts/package-registry.mjs 组装 registry/ 安装根(gitignore,不入库) - tests/manifest-consistency.spec.ts:id 两段校验 / version 同步 / 入口存在 / 双 bundle id 不互换 / contributes 空 - README:新增「通过 plugin-registry 安装(标准)」章节 + 保留官方 profile 安装 方案 + 通道互斥说明;架构表补 manifest 与双 client 行 --- .gitignore | 3 ++ README.md | 64 +++++++++++++++++++++-- dsh.plugin.json | 16 ++++++ package.json | 2 +- scripts/package-registry.mjs | 49 +++++++++++++++++ tests/manifest-consistency.spec.ts | 79 ++++++++++++++++++++++++++++ tsdown.config.ts | 84 +++++++++++++++++++----------- 7 files changed, 262 insertions(+), 35 deletions(-) create mode 100644 dsh.plugin.json create mode 100644 scripts/package-registry.mjs create mode 100644 tests/manifest-consistency.spec.ts diff --git a/.gitignore b/.gitignore index 9d7e74c..58318a1 100644 --- a/.gitignore +++ b/.gitignore @@ -142,3 +142,6 @@ vite.config.js.timestamp-* vite.config.ts.timestamp-* .vite/ lib/ + +# plugin-registry install staging (assembled by scripts/package-registry.mjs) +registry/ diff --git a/README.md b/README.md index 1d70468..640bfd2 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,31 @@ image image +## 🚀 通过 plugin-registry 安装(标准) + +前置条件:DSH 已集成 [plugin-registry](https://github.com/dsh-external/plugin-registry)(`dsh registry` 命令可用;集成步骤见其 README)。 + +```sh +git clone https://github.com/dsh-external/DSH-better-sidebar.git +cd DSH-better-sidebar +pnpm install && pnpm build # 产物: lib/index.js + lib/invariant.js (host) + lib/client.js + lib/client-registry.js (client) + lib/types +node scripts/package-registry.mjs # 组装 registry/ 暂存目录(只含清单 + 构建产物 + README,不入库) +dsh registry install ./registry # 安装(默认禁用——信任边界) +dsh registry enable dsh-external/dsh-better-sidebar # 启用 +``` + +> **通道互斥**:registry 安装与下方「不依赖 plugin-registry 的官方 profile 安装」是同一插件的两种安装方式,**每个部署二选一**——同时启用会双挂载(Node 半挂两次、页面渲染两个侧边栏)。已用官方方式装过时,先移除 `~/.dsh/profiles/web/cordis.patch.yml` 里的 better-sidebar 挂载行与 `package.json` 的 `link:` 依赖,再走 registry。 + +### 更新(registry 通道) + +```sh +cd DSH-better-sidebar && git pull && pnpm install && pnpm build +node scripts/package-registry.mjs +dsh registry uninstall dsh-external/dsh-better-sidebar +dsh registry install ./registry +dsh registry enable dsh-external/dsh-better-sidebar +``` + ## 🚀 一键安装(把提示词发给 DSH) 把下面提示词**整段**发给 DSH,它会自动完成克隆、构建、注册与安装(前置条件:已安装 DSH 且 `dsh web` 可运行,Node.js ≥ 20、pnpm ≥ 10): @@ -139,7 +164,7 @@ git clone https://github.com/dsh-external/DSH-better-sidebar.git cd DSH-better-sidebar pnpm install -pnpm build # 产物: lib/index.js + lib/invariant.js (host) + lib/client.js (client) + lib/types/*.d.ts +pnpm build # 产物: lib/index.js + lib/invariant.js (host) + lib/client.js + lib/client-registry.js (client) + lib/types/*.d.ts ``` > 运行期依赖(cordis、react、`@deepseek-ai/dsh-client-*` 等)按官方插件清单规范声明在 `peerDependencies`——web profile 已内置全部 peer 依赖,安装本插件无需额外装包;`dependencies` 只保留插件自有的运行时依赖(node-pty、ws、xterm、CodeMirror、schemastery 等)。 @@ -177,22 +202,49 @@ git pull && pnpm install && pnpm build # 在插件仓库目录执行 `link:` 引用无需重新 install。 +### 通过 registry 安装(手动,与上面的官方方案二选一) + +前置条件:`dsh registry` 命令可用(DSH 已集成 plugin-registry)。安装源是 `scripts/package-registry.mjs` 组装的 `registry/` 暂存目录——只含清单 + 构建产物 + README,避免把 `node_modules/`(本机 `link:` 符号链接、node-pty 二进制)与 `.git/` 一并装进 `/plugins`。 + +```sh +pnpm build # 先构建(产物见上) +node scripts/package-registry.mjs # 组装 registry/(重新运行会整体重建) +dsh registry install ./registry # 安装(默认禁用) +dsh registry enable dsh-external/dsh-better-sidebar +``` + +更新: + +```sh +git pull && pnpm install && pnpm build +node scripts/package-registry.mjs +dsh registry uninstall dsh-external/dsh-better-sidebar +dsh registry install ./registry +dsh registry enable dsh-external/dsh-better-sidebar +``` + +- 只改了 **client** 代码 → 硬刷新页面即可(registry 通道的 bundle 由服务器按请求读取) +- 改了 **host** 代码 → 重启 DSH + 硬刷新 +- 与官方方案的互斥见上文「通道互斥」说明 + ## 🛠️ 开发 ```sh pnpm typecheck # tsc --noEmit -pnpm test # vitest(单元 + 冒烟 + 插件形态 guard:真实 git/fs/node-pty 交互) -pnpm build # rm -rf lib && tsc(声明) + tsdown → lib/index.js + lib/invariant.js + lib/client.js + lib/types +pnpm build # rm -rf lib && tsc(声明) + tsdown → lib/index.js + lib/invariant.js + lib/client.js + lib/client-registry.js + lib/types +pnpm test # vitest(单元 + 冒烟 + 插件形态 guard:真实 git/fs/node-pty 交互;manifest 一致性测试读取 lib/,需先 build) pnpm watch # tsdown --watch(client bundle 热重建) ``` ### 规范符合性 -插件按 DSH 官方插件规范组织(参考 [dsh-external/turtle-ui](https://github.com/dsh-external/turtle-ui) 与 mainline `packages/client/AGENTS.md`)。**安装通道与渲染方式是两个独立概念**:安装始终走 profile 清单协议(`dsh plugin` / cordis.yml 行,见[一键安装](#一键安装把提示词发给-dsh));下面的 portal 条目只描述面板在页面上的渲染方式。 +插件按 DSH 官方插件规范组织(参考 [dsh-external/turtle-ui](https://github.com/dsh-external/turtle-ui) 与 mainline `packages/client/AGENTS.md`),并额外提供 plugin-registry 标准发布面(`dsh.plugin.json`)。**安装通道与渲染方式是两个独立概念**:安装走 profile 清单协议(`dsh plugin` / cordis.yml 行,见[一键安装](#一键安装把提示词发给-dsh))**或** registry 通道(`dsh registry`,见[通过 plugin-registry 安装](#通过-plugin-registry-安装标准)),二者互斥;下面的 portal 条目只描述面板在页面上的渲染方式。 - **插件形态**:`export const name / inject / Config / apply`,无 default 导出;`tests/plugin-shape.spec.ts` 通过 `Loader.unwrapExports` 守卫该形态 - **清单**:`types` + `exports`(`.` / `./invariant` / `./client` / `./src/*` / `./package.json`)、`dshClient`(`platform: 'web'` + 信息性 `inject` 边)、peerDependencies、`engines`、`files` 产物明细、`prepare`(消费者侧 `tsdown`,git 安装可用) - **client 契约**:仅导出 `apply`/`inject`(+ 类型);store 为 `createSidebarStore()` 工厂,实例归 `apply` 所有;`src/invariant.ts` 伴生;client bundle 复刻官方 preset(externals = 平台模块表 + runtime/client 豁免、纯度门、CSS Modules 内联) +- **registry 形态(标准发布面)**:仓库根 `dsh.plugin.json`(原生清单:id `dsh-external/dsh-better-sidebar`、`main` = host 构建产物、`client.main` = registry client bundle、`contributes` 空声明);`tests/manifest-consistency.spec.ts` 守卫清单与构建产物一致性(id 严格两段、version 与 package.json 同步、bundle id = 清单 id) +- **双 client bundle**:同一 `src/client/index.tsx` 构建出 `lib/client.js`(官方通道,id = 包名 `dsh-better-sidebar`)与 `lib/client-registry.js`(registry 通道,id = 清单 id)——两通道的 bundle-id 契约互不相同,各产出一份、同源不漂移;每个部署二选一(见安装章节「通道互斥」) - **已知偏差**:侧边栏面板经 portal(`document.body` + `createRoot`)挂载而非 slot——官方 slot 系统的 `'root'` 由 ui-layout 独占声明,重复声明 fail loud,外部插件无整面板 slot 可用;与 shell 的集成点(`conversation.chat.turnTail`)走官方 chain-slot 机制 ## 🏗️ 架构 @@ -203,7 +255,9 @@ pnpm watch # tsdown --watch(client bundle 热重建) |---|---|---| | host | `src/index.ts` → `lib/index.js` | cordis 插件(`Config` 为 schemastery schema,可调 readLimit/mediaLimit/listLimit/terminalsPerSession/reconnectGraceMs):`/sidebar/api/*` JSON API、`/sidebar/file` 媒体路由、`/sidebar/ws/terminal` WebSocket;fs / git / pty 服务 | | invariant | `src/invariant.ts` → `lib/invariant.js` | 包属 invariant 伴生(注册包名,无运行时断言) | -| client | `src/client/index.tsx` → `lib/client.js` | 浏览器 bundle(`__ModuleLoader__.load` 闭包工厂):portal 侧边栏 + 各视图 + turnTail 拦截 | +| client(官方通道) | `src/client/index.tsx` → `lib/client.js` | 浏览器 bundle(`__ModuleLoader__.load` 闭包工厂,id = 包名):portal 侧边栏 + 各视图 + turnTail 拦截 | +| client(registry 通道) | `src/client/index.tsx` → `lib/client-registry.js` | 同一源码的 registry 版本(id = 清单 id `dsh-external/dsh-better-sidebar`,`dsh.plugin.json` 的 `client.main` 指向它) | +| manifest | `dsh.plugin.json`(仓库根;`scripts/package-registry.mjs` 组装 `registry/` 安装根) | registry 发布面:id / version / main / client.main;仅 registry 通道读取 | - 所有 API 携带 `sessionId`;cwd 权威值取自会话 header,会话未附加(页面加载竞态)时回退客户端摘要 cwd,再回退进程 cwd;终端按 `${sessionId}:${tabId}` 键控,重连时若权威 cwd 变化则重启 shell 到正确目录 - 路由与 `/api` 同款信任围栏(Host 头 loopback / `connection.trustedHosts`,`src/trust-fence.ts`,拷贝自 `@deepseek-ai/dsh-client-connection`,BSD-3-Clause) diff --git a/dsh.plugin.json b/dsh.plugin.json new file mode 100644 index 0000000..d736673 --- /dev/null +++ b/dsh.plugin.json @@ -0,0 +1,16 @@ +{ + "id": "dsh-external/dsh-better-sidebar", + "version": "0.1.2", + "main": "./lib/index.js", + "description": "VSCode 风格右侧侧边栏:文件资源管理器 / 编辑器 / 终端 / Git,按会话隔离", + "engines": { + "dsh": ">=0.0.1" + }, + "contributes": { + "tools": [], + "skills": [] + }, + "client": { + "main": "./lib/client-registry.js" + } +} diff --git a/package.json b/package.json index c78eb69..dda7bd3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsh-better-sidebar", - "version": "0.1.1", + "version": "0.1.2", "description": "DSH web plugin: a VSCode-like right sidebar (explorer / editor / terminal / git), isolated per conversation session.", "private": true, "type": "module", diff --git a/scripts/package-registry.mjs b/scripts/package-registry.mjs new file mode 100644 index 0000000..b13bade --- /dev/null +++ b/scripts/package-registry.mjs @@ -0,0 +1,49 @@ +#!/usr/bin/env node +/** + * Assemble the plugin-registry installable root (`registry/`) from the built + * `lib/` output. Run after `pnpm build`: + * + * pnpm build + * node scripts/package-registry.mjs + * dsh registry install ./registry + * dsh registry enable dsh-external/dsh-better-sidebar + * + * The registry install copies the whole source directory (`cp -r`), so + * installing the repo root would drag in `node_modules/` (machine-local + * `link:` devDependency symlinks, node-pty binaries) and `.git/`. This + * staging directory carries exactly the files the manifest references plus + * the docs — same layout as the dsh-subagent-tree `registry/` pattern. It is + * gitignored and rebuilt from scratch on every run. + */ +import { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = resolve(fileURLToPath(import.meta.url), '..', '..') +const out = join(root, 'registry') + +/** Files copied into registry/, preserving relative paths (lib/ stays lib/). */ +const files = [ + 'dsh.plugin.json', + 'lib/index.js', + 'lib/invariant.js', + 'lib/client-registry.js', + 'lib/client-registry.js.map', + 'README.md', + 'LICENSE', +] + +rmSync(out, { recursive: true, force: true }) +mkdirSync(out, { recursive: true }) +for (const file of files) { + const source = join(root, file) + if (!existsSync(source)) { + console.error(`missing ${file} — run \`pnpm build\` first`) + process.exit(1) + } + const target = join(out, file) + mkdirSync(dirname(target), { recursive: true }) + cpSync(source, target) +} +console.log(`registry/ assembled (${files.length} files)`) +console.log('next: dsh registry install ./registry && dsh registry enable dsh-external/dsh-better-sidebar') diff --git a/tests/manifest-consistency.spec.ts b/tests/manifest-consistency.spec.ts new file mode 100644 index 0000000..15ab1b5 --- /dev/null +++ b/tests/manifest-consistency.spec.ts @@ -0,0 +1,79 @@ +/** + * Registry manifest consistency spec: guards the plugin-registry release form + * (`dsh.plugin.json`) against the build output and package.json, so the repo + * cannot drift into an uninstallable / uneable state: + * - `id` passes the registry's strict two-segment validation (native form), + * - `version` stays in sync with package.json, + * - the manifest entry files exist in the build output, + * - the registry client bundle registers exactly the manifest id (the + * browser-side `arrive()` contract), and the official client bundle keeps + * the package-name id (the client-modules compose contract) — the two + * bundles must never be swapped. + * + * Reads the built lib/ output, so run `pnpm build` first (the registry + * install validates main/client.main existence the same way). + */ +import { describe, expect, it } from 'vitest' +import { existsSync, readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = resolve(fileURLToPath(new URL('..', import.meta.url))) + +/** The registry manifest face this spec touches (mirror of the registry's PluginManifest). */ +interface PluginManifest { + id: string + version: string + main: string + description?: string + engines?: { dsh?: string } + contributes?: { tools?: string[]; skills?: string[] } + client?: { main?: string; inject?: string[]; immediately?: boolean } +} + +interface PackageJson { + name: string + version: string +} + +const manifest = JSON.parse(readFileSync(resolve(ROOT, 'dsh.plugin.json'), 'utf8')) as PluginManifest +const pkg = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf8')) as PackageJson + +/** The registry's strict id validation (manifest.ts / client-modules registerExternal): exactly two lowercase slash-separated segments. */ +const ID_PATTERN = /^(?!node_modules\/)(?:@[a-z0-9][a-z0-9-.]*\/[a-z0-9][a-z0-9-.]*|[a-z0-9][a-z0-9-.]*\/[a-z0-9][a-z0-9-.]*)$/ + +/** The registered `__ModuleLoader__.load({ id })` value of a built client bundle. */ +function bundleId(file: string): string { + const source = readFileSync(resolve(ROOT, file), 'utf8') + const match = /load\(\{\s*id:\s*"([^"]+)"/.exec(source) + if (match === null) throw new Error(`${file} registers no __ModuleLoader__.load id — run \`pnpm build\` first`) + return match[1]! +} + +describe('registry manifest consistency (dsh.plugin.json)', () => { + it('id is a valid two-segment registry id (native form)', () => { + expect(manifest.id).toMatch(ID_PATTERN) + }) + + it('version matches package.json', () => { + expect(manifest.version).toBe(pkg.version) + }) + + it('main and client.main exist in the build output', () => { + expect(existsSync(resolve(ROOT, manifest.main))).toBe(true) + expect(manifest.client?.main).toBeDefined() + expect(existsSync(resolve(ROOT, manifest.client!.main!))).toBe(true) + }) + + it('the registry bundle registers exactly the manifest id and the official bundle the package name (no swap)', () => { + const registryId = bundleId(manifest.client!.main!) + expect(registryId).toBe(manifest.id) + expect(bundleId('lib/client.js')).toBe(pkg.name) + expect(registryId).not.toBe(pkg.name) + }) + + it('contributes declares no tools or skills (the host half registers none)', () => { + expect(manifest.contributes?.tools).toEqual([]) + expect(manifest.contributes?.skills).toEqual([]) + }) +}) diff --git a/tsdown.config.ts b/tsdown.config.ts index 4506cbf..5916903 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -1,10 +1,21 @@ /** * tsdown build for dsh-better-sidebar: the host-half lib (lib/index.js and - * the lib/invariant.js companion, ESM node) plus the browser client bundle - * (lib/client.js, CJS closure factory). + * the lib/invariant.js companion, ESM node) plus the two browser client + * bundles (lib/client.js and lib/client-registry.js, CJS closure factory) — + * one per install channel: * - * The client bundle replicates the official DSH client-bundle preset - * (packages/client/tsdown.client.ts): + * - `lib/client.js` serves the official profile channel, registering with + * the package-name id `dsh-better-sidebar` (the client-modules compose + * keys on the package name; keep it in sync with package.json `name`), + * - `lib/client-registry.js` serves the plugin-registry channel + * (dsh.plugin.json), registering with the manifest id + * `dsh-external/dsh-better-sidebar` (the registry browser-side `arrive()` + * check requires bundle id === plugin id). + * + * Both bundles replicate the official DSH client-bundle preset + * (packages/client/tsdown.client.ts) and are compiled from the same + * src/client/index.tsx source — only the registered id and the output file + * name differ, so they cannot drift: * - externals resolve through the loader module table at runtime (the * PLATFORM_MODULES seed list from apps/web's platform.ts, plus the * runtime/client exemption), @@ -13,7 +24,7 @@ * collaboration goes through cordis services, never value imports, * - CSS Modules compile to hashed class maps and inject