diff --git a/AGENTS.md b/AGENTS.md index 24b7a9f..e575ac4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,7 @@ better-sidebar 从 v0.4.0 起暴露 `ctx.betterSidebar` 服务(Cordis context - **禁止修改 DeepSeek Harness (DSH) 源码**:对官方源码 checkout(`~/.dsh/source/current`)零写入——不得改 harness 包、不得把 harness 改动提交到它的分支。 - **代码改动必须走 PR**:功能 / 修复 / 测试等非文档改动一律在分支上开发(`feat/*` / `fix/*`),用 `gh pr create` 发起 PR,review 合并后才进 main;**仅纯文档类改动**(README / AGENTS.md / docs/ 等)允许直接推送到 main。 - **挂载只走 `cordis.patch.yml` + profile 机制**(`~/.dsh/profiles//`),插件永远作为独立包被 profile 引用,不反向侵入 DSH。 +- **DSH 市场受管安装兼容约束**:发布清单的 `dependencies` / `peerDependencies` / `optionalDependencies` 三字段**一律不得出现 `cordis`**(市场预览按名硬拒,optional 无效),且 `scripts` 不得含 `preinstall` / `install` / `postinstall` / `prepare`。回归由 `tests/market-manifest.spec.ts` 守护——违反即市场目录拿不到 `repository_backlink` 验证目标。 - 需要 harness 没有的能力时,用 DSH **现成的只读/公开 API** 或插件自有路由实现(参考 §7 的 `jobs.output` 事件回放:读会话事件日志而非动注册表);如果确实做不到,先向用户说明取舍,而不是直接改 DSH。 ### CI 挂载冒烟(`plugin-mount` job / `pnpm test:mount`) @@ -39,7 +40,7 @@ better-sidebar 从 v0.4.0 起暴露 `ctx.betterSidebar` 服务(Cordis context - **服务名**:`betterSidebar`(即 `ctx.betterSidebar`) - **发布侧**:better-sidebar 的 client half(`src/client/index.tsx`,通过 `ctx.provide('betterSidebar', service)` 发布) - **消费侧**:你的插件的 client half(`inject = ['betterSidebar', ...]`,然后 `ctx.betterSidebar.registerTab(...)`) -- **类型合并**:`declare module 'cordis' { interface Context { betterSidebar: BetterSidebarService } }` 由 `dsh-better-sidebar` 包导出;消费插件 `import type {} from 'dsh-better-sidebar'` 即触发类型合并 +- **类型合并**:`declare module '@deepseek-ai/cordis' { interface Context { betterSidebar: BetterSidebarService } }` 由 `dsh-better-sidebar` 包导出;消费插件 `import type {} from 'dsh-better-sidebar'` 即触发类型合并 > ⚠️ **host 半不发布此服务**:`ctx.betterSidebar` 只在 client 侧存在。如果你的插件 host 半需要读 better-sidebar 状态,走 better-sidebar 自己的 HTTP/WS 路由(`/sidebar/api/*`),不走服务。 @@ -53,7 +54,7 @@ better-sidebar 从 v0.4.0 起暴露 `ctx.betterSidebar` 服务(Cordis context { "name": "my-plugin", "peerDependencies": { - "cordis": "^4.0.0-rc.8", + "@deepseek-ai/cordis": "^4.0.1", "dsh-better-sidebar": "workspace:*" }, "peerDependenciesMeta": { @@ -115,7 +116,7 @@ import type { } from 'dsh-better-sidebar/client/service' ``` -> 💡 **类型合并触发路径**:`import type {} from 'dsh-better-sidebar/client/service'` 同样会加载 `Context` 的 augmentation(`declare module 'cordis'` 在 context-types.d.ts 中,service 声明会拉入它)——纯浏览器侧插件建议走 `client/service` 路径,避免拉进宿主半的 Node 类型图(主入口 `dsh-better-sidebar` 的声明面含宿主代码,宿主消费者本就处于 Node 环境)。client 可达声明图(`client/*` + context-types + html-route + prefs-shared)自 v0.12.0 起**零 Node 依赖**(`scripts/check-consumer-types.sh` 守护),无 `@types/node`、`skipLibCheck: false` 也能编译。 +> 💡 **类型合并触发路径**:`import type {} from 'dsh-better-sidebar/client/service'` 同样会加载 `Context` 的 augmentation(`declare module '@deepseek-ai/cordis'` 在 context-types.d.ts 中,service 声明会拉入它)——纯浏览器侧插件建议走 `client/service` 路径,避免拉进宿主半的 Node 类型图(主入口 `dsh-better-sidebar` 的声明面含宿主代码,宿主消费者本就处于 Node 环境)。client 可达声明图(`client/*` + context-types + html-route + prefs-shared)自 v0.12.0 起**零 Node 依赖**(`scripts/check-consumer-types.sh` 守护),无 `@types/node`、`skipLibCheck: false` 也能编译。 --- @@ -588,7 +589,7 @@ interface OpenTabSeed { | 陷阱 | 说明 | |---|---| | **构建纯度门** | client bundle 禁止 value-import `@dsh-external/*` 或非白名单的 `@deepseek-ai/*`;类型 `import type {}` 会被擦除,不触发门禁 | -| **双 cordis 实例** | 外部插件解析不到 DSH monorepo 的 cordis augmentation;better-sidebar 自己重述了 `interface Context { betterSidebar: ... }`,你 `import type {}` 即拿到类型 | +| **统一 cordis 分支(v0.15.2+)** | 类型基底与 augmentation 全部落在 DSH 运行时正主 `@deepseek-ai/cordis`(`src/context-types.ts`:真实 cordis `Context` 与结构化服务面做**交集**,不再重述 `declare module 'cordis'`——DSH host/client 包对同一成员声明不同类型(如 `sessions`),合并会 TS2717 冲突)。消费者 `import type {} from 'dsh-better-sidebar'` 即拿到 `ctx.betterSidebar` augmentation,或直接 `import type { Context } from 'dsh-better-sidebar'`。**公开版 `cordis` 不再被依赖**(市场规则拒绝依赖字段出现 `cordis`,见 §0 硬约束) | | **ModuleLoader 不跨插件** | 运行时 `require()` 虽支持跨 bundle,但被构建门挡;所有交互走 `ctx.betterSidebar` 方法调用 | | **host 半无此服务** | `ctx.betterSidebar` 只在 client 侧存在;host 半需要 better-sidebar 数据走 `/sidebar/api/*` HTTP 路由 | | **portal 限制** | 整面板 slot 由 ui-layout 独占,外部 tab 只能进入 better-sidebar 的 portal 内部,无法全屏替换 | @@ -641,7 +642,7 @@ interface OpenTabSeed { "./client": { "types": "./lib/types/client/index.d.ts", "default": "./lib/client.js" } }, "peerDependencies": { - "cordis": "^4.0.0-rc.8", + "@deepseek-ai/cordis": "^4.0.1", "dsh-better-sidebar": "workspace:*", "@deepseek-ai/dsh-client-runtime": "^0.0.1", "react": "^18.2.0" @@ -656,7 +657,7 @@ interface OpenTabSeed { ```tsx import { createElement } from 'react' import type {} from 'dsh-better-sidebar' // 触发 ctx.betterSidebar 类型合并 -import type { Context } from 'cordis' +import type { Context } from '@deepseek-ai/cordis' export const inject = ['betterSidebar'] diff --git a/README.md b/README.md index 52353c1..f533a7a 100644 --- a/README.md +++ b/README.md @@ -305,6 +305,12 @@ GitHub topic [`dsh-better-sidebar`](https://github.com/topics/dsh-better-sidebar **支持的 DSH 版本**:支持的 DSH 版本:0.1.0-rc.8 · 0.1.1-rc.1 · 0.1.1-rc.2 · 完整发布历史见 [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases) +### v0.15.2 + +- 🛒 **DSH 市场受管安装兼容**:移除 `peerDependencies` 里的公开版 `cordis`(市场预览硬拒依赖字段出现 `cordis`,optional 无效),使 npm 包满足 [dsh-community-market 安装规范](https://github.com/anywhere-labs/deepseek-harness-desktop/blob/master/dsh-community-market/docs/install-and-uninstall.zh.md)——目录(dshfind / 1024Store)里的本插件条目将重新获得 `repository_backlink` 验证目标,可直接从 Desktop 市场受管安装 +- 🔤 **类型基底迁移到 `@deepseek-ai/cordis`**:声明面(`src/context-types.ts`)不再依赖/重述公开版 cordis——`Context` = 真实 vendored cordis Context 与结构化服务面的**交集**,`ctx.betterSidebar` 类型合并改挂在 `@deepseek-ai/cordis` 上。**消费者迁移**:`import type { Context } from 'cordis'` 改为 `import type { Context } from '@deepseek-ai/cordis'`(`import type {} from 'dsh-better-sidebar'` 的类型合并方式不变);对未使用该导入的插件无影响 +- 🍃 **`ctx.effect` 严格化顺手修了 4 处**:拦截注册失败时 effect 体返回 `undefined` 改为 no-op disposer(vendored cordis 的 effect 契约要求返回 disposer,返回 `undefined` 属非法形状) + ### v0.15.1 自 v0.15.0 以来的全部更改: diff --git a/README_EN.md b/README_EN.md index 4412e2b..9200685 100644 --- a/README_EN.md +++ b/README_EN.md @@ -310,6 +310,12 @@ The GitHub topic [`dsh-better-sidebar`](https://github.com/topics/dsh-better-sid **Supported DSH versions**: Supported DSH versions: 0.1.0-rc.8 · 0.1.1-rc.1 · 0.1.1-rc.2 · full release history on the [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases) page +### v0.15.2 + +- 🛒 **DSH marketplace managed-install compatibility**: the public `cordis` entry was removed from `peerDependencies` (the market preview hard-rejects `cordis` in any dependency field — optional does not help), so the npm package now satisfies the [dsh-community-market install rules](https://github.com/anywhere-labs/deepseek-harness-desktop/blob/master/dsh-community-market/docs/install-and-uninstall.zh.md) — catalog sources (dshfind / 1024Store) can re-issue the `repository_backlink` verified target and the plugin becomes installable through the Desktop market +- 🔤 **Type base migrated to `@deepseek-ai/cordis`**: the declaration surface (`src/context-types.ts`) no longer depends on or restates the public `cordis` — `Context` is now an **intersection** of the real vendored cordis Context with the structural service faces, and the `ctx.betterSidebar` type merge lives on `@deepseek-ai/cordis`. **Consumer migration**: change `import type { Context } from 'cordis'` to `import type { Context } from '@deepseek-ai/cordis'` (the `import type {} from 'dsh-better-sidebar'` merge path is unchanged); plugins that never used that import are unaffected +- 🍃 **Strict `ctx.effect` also fixed 4 latent sites**: interception/IME-guard effect bodies returned `undefined` on failure instead of a disposer (invalid shape under the vendored cordis effect contract) — now a no-op disposer + ### v0.15.1 All changes since v0.15.0: diff --git a/docs/external-plugin-guide.md b/docs/external-plugin-guide.md index b0cbf30..190280b 100644 --- a/docs/external-plugin-guide.md +++ b/docs/external-plugin-guide.md @@ -24,12 +24,12 @@ better-sidebar 从 v0.4.0 起把自己改造成一个**注册表服务**: ## 2. 前置:类型合并与依赖声明 -### 2.1 双 cordis 实例问题 +### 2.1 类型合并(统一 `@deepseek-ai/cordis`) -外部插件在 DSH monorepo 之外解析,拿不到官方 cordis 的 augmentation,因此 `ctx.betterSidebar` 不会自动出现在你的 `Context` 类型上。解法由 better-sidebar 自己提供: +DSH 运行时和生态的类型正主是 vendored `@deepseek-ai/cordis`。你的插件解析到它的 `Context` 时,`ctx.betterSidebar` 不会自动出现——由 better-sidebar 用 `declare module '@deepseek-ai/cordis'` 补上: ```ts -import type {} from 'dsh-better-sidebar' // 触发 declare module 'cordis' 类型合并 +import type {} from 'dsh-better-sidebar' // 触发 declare module '@deepseek-ai/cordis' 类型合并 ``` 这个 **type-only import** 在编译时被擦除,不产生任何运行时依赖,也不会触发构建纯度门(见 §10)。 @@ -40,7 +40,7 @@ import type {} from 'dsh-better-sidebar' // 触发 declare module 'cordis' 类 { "name": "my-plugin", "peerDependencies": { - "cordis": "^4.0.0-rc.8", + "@deepseek-ai/cordis": "^4.0.1", "dsh-better-sidebar": "workspace:*" }, "peerDependenciesMeta": { @@ -78,7 +78,7 @@ import type { } from 'dsh-better-sidebar/client/service' ``` -> 💡 **类型合并触发路径**:`import type {} from 'dsh-better-sidebar/client/service'` 同样会加载 `Context` 的 augmentation(`declare module 'cordis'` 在 context-types.d.ts 中)——**纯浏览器侧插件建议走 `client/service` 路径**,避免拉进宿主半的 Node 类型图(主入口 `dsh-better-sidebar` 的声明面含宿主代码;宿主消费者本就处于 Node 环境则无所谓)。client 可达声明图(`client/*` + context-types + html-route + prefs-shared)自 v0.12.0 起**零 Node 依赖**(`scripts/check-consumer-types.sh` 守护),没有 `@types/node`、`skipLibCheck: false` 也能编译。 +> 💡 **类型合并触发路径**:`import type {} from 'dsh-better-sidebar/client/service'` 同样会加载 `Context` 的 augmentation(`declare module '@deepseek-ai/cordis'` 在 context-types.d.ts 中)——**纯浏览器侧插件建议走 `client/service` 路径**,避免拉进宿主半的 Node 类型图(主入口 `dsh-better-sidebar` 的声明面含宿主代码;宿主消费者本就处于 Node 环境则无所谓)。client 可达声明图(`client/*` + context-types + html-route + prefs-shared)自 v0.12.0 起**零 Node 依赖**(`scripts/check-consumer-types.sh` 守护),没有 `@types/node`、`skipLibCheck: false` 也能编译。 --- @@ -87,7 +87,7 @@ import type { ```ts // my-plugin/src/client/index.ts import type {} from 'dsh-better-sidebar' // 触发 ctx.betterSidebar 类型合并 -import type { Context } from 'cordis' +import type { Context } from '@deepseek-ai/cordis' export const inject = ['betterSidebar', 'slots'] // 声明服务依赖(slots 可选,按需) @@ -664,7 +664,7 @@ ctx.effect(() => | 陷阱 | 说明 | |---|---| | **构建纯度门** | client bundle 禁止 value-import `@dsh-external/*` 或非白名单的 `@deepseek-ai/*`(`tsdown.config.ts` 的 `dsh-client-bundle-purity` 插件);**类型 `import type {}` 会被擦除,不触发门禁**——类型可自由共享,运行时符号不行。所有跨插件交互走 `ctx.betterSidebar` 方法调用 | -| **双 cordis 实例** | 外部插件解析不到 DSH monorepo 的 cordis augmentation;better-sidebar 自己重述了 `interface Context { betterSidebar: ... }`(`src/context-types.ts`),你 `import type {} from 'dsh-better-sidebar'` 即拿到类型 | +| **统一 cordis 分支(v0.15.2+)** | 类型基底与 augmentation 全部在 DSH 运行时正主 `@deepseek-ai/cordis` 上(`src/context-types.ts`:真实 cordis `Context` 与结构化服务面做**交集**,不再重述 `declare module 'cordis'`)。消费者 `import type {} from 'dsh-better-sidebar'` 即拿到 `ctx.betterSidebar`,或直接 `import type { Context } from 'dsh-better-sidebar'`。公开版 `cordis` 不再被依赖 | | **ModuleLoader 不跨插件** | 运行时 `require()` 虽支持跨 bundle,但被构建门挡;所有交互走 `ctx.betterSidebar` 方法调用 | | **host 半无此服务** | `ctx.betterSidebar` 只在 client 侧存在;host 半需要 better-sidebar 数据走 `/sidebar/api/*` HTTP 路由 | | **portal 限制** | 整面板 slot 由 ui-layout 独占,外部 tab 只能进入 better-sidebar 的 portal 内部,无法全屏替换整个面板 | @@ -690,7 +690,7 @@ ctx.effect(() => "./client": { "types": "./lib/types/client/index.d.ts", "default": "./lib/client.js" } }, "peerDependencies": { - "cordis": "^4.0.0-rc.8", + "@deepseek-ai/cordis": "^4.0.1", "dsh-better-sidebar": "workspace:*", "@deepseek-ai/dsh-client-runtime": "^0.0.1", "react": "^18.2.0" @@ -706,7 +706,7 @@ ctx.effect(() => ```tsx import { createElement } from 'react' import type {} from 'dsh-better-sidebar' // 触发 ctx.betterSidebar 类型合并 -import type { Context } from 'cordis' +import type { Context } from '@deepseek-ai/cordis' export const inject = ['betterSidebar'] diff --git a/dsh.plugin.json b/dsh.plugin.json index f067d21..a230943 100644 --- a/dsh.plugin.json +++ b/dsh.plugin.json @@ -1,6 +1,6 @@ { "id": "dsh-external/dsh-better-sidebar", - "version": "0.15.1", + "version": "0.15.2", "main": "./lib/index.js", "description": "VSCode 风格右侧侧边栏:文件资源管理器 / 编辑器 / 终端 / Git / 浏览器,按会话隔离。暴露服务供其他插件注册侧边栏页面和文件预览器", "engines": { diff --git a/package.json b/package.json index 8dce638..e9a43f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsh-better-sidebar", - "version": "0.15.1", + "version": "0.15.2", "description": "DSH web plugin: a VSCode-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes a service for other plugins to register sidebar tabs and file viewers.", "type": "module", "repository": { @@ -99,15 +99,9 @@ "@deepseek-ai/dsh-settings": "^0.1.0-rc.8", "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8", "@deepseek-ai/dsh-tools": "^0.1.0-rc.8", - "cordis": "^4.0.0-rc.8", "react": "^18.2.0", "react-dom": "^18.2.0" }, - "peerDependenciesMeta": { - "cordis": { - "optional": true - } - }, "dependencies": { "@codemirror/commands": "^6.10.4", "@codemirror/lang-cpp": "^6.0.3", diff --git a/scripts/check-consumer-types.sh b/scripts/check-consumer-types.sh index 300c88a..2e3f22e 100755 --- a/scripts/check-consumer-types.sh +++ b/scripts/check-consumer-types.sh @@ -22,6 +22,10 @@ trap 'rm -rf "$WORK"' EXIT mkdir -p "$WORK/node_modules" ln -s "$(pwd)" "$WORK/node_modules/dsh-better-sidebar" +# The vendored cordis base (and, through its realpath, cosmokit / standard-schema) +# must resolve so the consumer can type `Context` against the DSH runtime scope. +mkdir -p "$WORK/node_modules/@deepseek-ai" +ln -s "$(pwd)/node_modules/@deepseek-ai/cordis" "$WORK/node_modules/@deepseek-ai/cordis" cat > "$WORK/check.ts" <<'EOF' import { SIDEBAR_FEATURES, SIDEBAR_SERVICE_VERSION } from 'dsh-better-sidebar/client/service' @@ -32,6 +36,10 @@ import type { } from 'dsh-better-sidebar/client/service' import type { SessionScope, SidebarSnapshot, SidebarState, SidebarStore, SidebarTab } from 'dsh-better-sidebar/client/service' import type { SidebarPrefs } from 'dsh-better-sidebar/client/service' +// The vendored-cordis path: `Context` from '@deepseek-ai/cordis' plus the +// side-effect type import above must expose `ctx.betterSidebar` — the consumer +// never needs to import this package's own Context type. +import type { Context as VendoredContext } from '@deepseek-ai/cordis' declare const ctx: { betterSidebar: BetterSidebarService } const tab: TabDescriptor = { @@ -65,6 +73,11 @@ const prefs: SidebarPrefs = snap.prefs const store: SidebarStore = null as unknown as SidebarStore void prefs; void store; void ctx.betterSidebar.version; void ctx.betterSidebar.features void SIDEBAR_SERVICE_VERSION; void SIDEBAR_FEATURES +// Vendored-cordis augmentation path. +declare const vctx: VendoredContext +vctx.betterSidebar.registerTab(tab) +vctx.betterSidebar.registerFileViewer(viewer) +void vctx.betterSidebar.getSnapshot() EOF cat > "$WORK/tsconfig.json" <<'EOF' @@ -88,14 +101,16 @@ set +e STATUS=$? set -e # Fail only on errors that mention OUR package / declarations / the fixture. -if grep -E "dsh-better-sidebar|lib/types|check\.ts" "$WORK/strict.log" | grep -v "node_modules/cordis\|node_modules/cosmokit"; then +# Upstream noise (vendored cordis / cosmokit declaration warnings under the +# repo's pinned TS lib) lives anywhere under node_modules and is filtered. +if grep -E "dsh-better-sidebar|lib/types|check\.ts" "$WORK/strict.log" | grep -vE "node_modules/[^ ]*(cordis|cosmokit)"; then echo "[check-consumer-types] FAIL: errors in the dsh-better-sidebar declaration surface:" >&2 grep -E "dsh-better-sidebar|lib/types|check\.ts" "$WORK/strict.log" >&2 exit 1 fi if [ "$STATUS" -ne 0 ]; then echo "[check-consumer-types] pass 2 note: strict mode only reports upstream declaration noise:" - grep -v "dsh-better-sidebar\|lib/types\|check\.ts" "$WORK/strict.log" | head -5 + grep -v "dsh-better-sidebar\|lib/types\|check\.ts" "$WORK/strict.log" | head -5 || true fi echo "[check-consumer-types] OK: the client/service declaration surface is node-free and self-contained." diff --git a/src/client/index.tsx b/src/client/index.tsx index c87fbd1..03cd74f 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -259,7 +259,7 @@ export function apply(ctx: Context): void { return registerTurnTailInterception(ctx, sidebarStore) } catch (error) { fail('interception', error) - return undefined + return () => {} } }, 'dsh-better-sidebar: turn-tail interception', @@ -271,7 +271,7 @@ export function apply(ctx: Context): void { return registerOpenPathInterception(ctx, sidebarStore) } catch (error) { fail('interception', error) - return undefined + return () => {} } }, 'dsh-better-sidebar: open-path interception', @@ -315,7 +315,7 @@ export function apply(ctx: Context): void { }) } catch (error) { fail('interception', error) - return undefined + return () => {} } }, 'dsh-better-sidebar: link interception', @@ -335,7 +335,7 @@ export function apply(ctx: Context): void { return registerImeGuard() } catch (error) { fail('ime guard', error) - return undefined + return () => {} } }, 'dsh-better-sidebar: IME composition guard', diff --git a/src/client/service.ts b/src/client/service.ts index c16e67d..af9ac1d 100644 --- a/src/client/service.ts +++ b/src/client/service.ts @@ -470,7 +470,7 @@ export function matchUrlTarget(tabs: readonly TabDescriptor[], url: URL): TabDes * The plugin version this service instance reports. Keep in lockstep with * `package.json`'s version — `tests/service.spec.ts` asserts the pair. */ -export const SIDEBAR_SERVICE_VERSION = '0.15.1' +export const SIDEBAR_SERVICE_VERSION = '0.15.2' /** * Monotonic capability list consumers use to gate new API usage (features diff --git a/src/context-types.ts b/src/context-types.ts index 5686795..a62333b 100644 --- a/src/context-types.ts +++ b/src/context-types.ts @@ -1,31 +1,34 @@ /** * Structural types for the cordis services this plugin consumes, plus the - * Context augmentation both halves share. A third-party plugin resolves - * outside the DSH monorepo's single cordis instance, so the upstream - * `declare module 'cordis'` augmentations do not reach this Context — and - * the npm cordis package does not declare the DSH-vendored runtime members - * (`ctx.effect`, service properties). The members below mirror the actual - * runtime shapes this plugin touches: - * - webServer: @deepseek-ai/dsh-host-webserver (the WebServer) - * - sessions: host side @deepseek-ai/dsh-session (SessionStore), client - * side the runtime ISessions list feed - * - conversation: client side ui-conversation's IConversation (composer - * draft), read lazily through `ctx.get` — cross-plugin service reads need - * an inject declaration, so the direct property is never typed here - * - webRuntime: @deepseek-ai/dsh-web-app (bind-derived trusted hosts) - * - slots: the client runtime SlotRegistry - * - effect: the DSH-vendored cordis lifecycle helper - * Drift from upstream is contained to this file. + * Context face both halves share. + * + * The type base is the vendored `@deepseek-ai/cordis` Context (the runtime + * DSH actually runs); the service members this plugin touches are restated + * below as structural mirrors and combined with the base by INTERSECTION. + * Intersection (not `declare module` augmentation) is deliberate: DSH's own + * packages already augment `@deepseek-ai/cordis`, and the host and client + * packages declare *different* types for the same member — host + * `sessions: SessionStore` vs client runtime `sessions: ISessions` — so a + * single program that re-declares them would fail interface merging + * (TS2717). Intersecting keeps every face available and lets each call site + * resolve against the member it needs without any module-level conflict. + * + * `effect`, `get`, `provide`, `inject`, `logger`, `emit`, `isolate` and the + * event helpers come from the vendored cordis base and are intentionally NOT + * restated here: their strict shapes are the runtime contract (e.g. an + * effect body must return a disposer). Only the string-keyed session-feed + * `on` overload is added, because the cordis `on` is keyed to its own + * typed `Events` map and the harness session feed is a plain string event. * * This file must stay FREE of Node.js types (`node:http`, `node:stream`, * `Buffer`): it is part of the CLIENT-reachable declaration graph (the - * `Context` in `TabComponentProps` and the `declare module` augmentation), + * `Context` in `TabComponentProps` and the `betterSidebar` augmentation), * so a Node import here would leak into browser-only consumer builds. The * webServer faces below are therefore structural mirrors with plain * interfaces (the host casts to real Node types at the few boundaries that * need them — e.g. the `ws` upgrade hook in src/index.ts). */ -import type { Context } from 'cordis' +import type { Context as CordisContext } from '@deepseek-ai/cordis' import type { BetterSidebarService } from './client/service.ts' /** The request face route handlers see (structural subset of node's @@ -514,97 +517,78 @@ export interface SidebarAgent { } } -declare module 'cordis' { - interface Context { - webServer: SidebarWebServer - sessions: SidebarSessionStore & SidebarSessionsService - connection: SidebarConnectionHandle - webRuntime: SidebarWebRuntime - slots: SidebarSlotsService - workspaces: SidebarWorkspacesService - settings: SidebarSettingsService - invariants: SidebarInvariantsService - tools: SidebarToolsService - /** - * The client locale service (`@deepseek-ai/dsh-client-locale`): the - * sidebar's copy follows its active locale and registers its - * dictionaries under the `betterSidebar` namespace. Client side only. - */ - locale: SidebarLocaleService - /** - * The client module system (rc.8+): resolves module-table specifiers - * (seed words, graph rows) — the chunk loader's externals go through it - * (`ctx.modules.import`). rc.7 exposed the same surface as the - * `window.__DSH_MODULES__` page global; the loader keeps that fallback. - */ - modules: { import(specifier: string): Promise } - /** - * The host background-job registry (`ctx.get('jobs')`; optional — the - * sidebar routes degrade to a 503 when the deployment lacks it). - */ - jobs: SidebarJobsService - /** - * The host live-agent registry (`ctx.get('agents')`; optional — used to - * resolve the caller the jobs fence compares against, and to create / - * resume the Side Chat thread agents). - */ - agents: SidebarAgentsService - /** - * The host subagent runtime (`ctx.subagents`; optional — the Subagent - * page's live batch route reads descendant catalogs through it). - */ - subagents: SidebarSubagentsService - /** - * The host agent-presets service (`ctx.get('agentPresets')`; optional — - * absent deployments compose nothing and every session shares the host - * composition). - */ - agentPresets: SidebarAgentPresetsService - /** - * The host session-title service (`ctx.get('sessionTitle')`; optional — - * the Side Chat thread label pin degrades to the auto-generated title). - */ - sessionTitle: SidebarSessionTitleService - /** - * The host session-persistence service (`ctx.get('sessionPersistence')`; - * optional — needed only for the Side Chat cold-resume composition). - */ - sessionPersistence: SidebarSessionPersistenceService - /** - * The client-side sidebar registry: external plugins register tab types - * and file previewers here. Provided by the client half (see - * {@link ./client/index.tsx}); undefined on the host side. - */ - betterSidebar: BetterSidebarService - /** - * Subscribe to the session append feed (mirror of the cordis event API): - * the listener receives every appended session event with the LIVE - * Session instance that appended it. The api-proxy pushes the same feed - * to browsers; the sidebar uses it to mirror job_output events the - * session store's own log can lag behind (restart divergence). Returns - * the disposer. - */ - on(event: string, listener: (session: unknown, event: SidebarSessionEvent) => void): () => void - /** - * Register a lifecycle callback (DSH-vendored cordis): runs at plugin - * activation; its returned cleanup runs at disposal. - */ - effect(fn: () => void | (() => void), label?: string): void - } +/** + * The shape this plugin actually consumes, intersected with the vendored + * cordis `Context` below (see the file header for why intersection is used + * instead of module augmentation). + */ +export interface SidebarContextShape { + /** The webServer service face this plugin uses. */ + webServer: SidebarWebServer + /** The session store (host `.get`) and the client list feed (`.list`) faces. */ + sessions: SidebarSessionStore & SidebarSessionsService + /** The wire handle the Side Chat transcript polls through. */ + connection: SidebarConnectionHandle + /** The web runtime trust list (bind-derived). */ + webRuntime: SidebarWebRuntime + /** The client slot registry (register/inject). */ + slots: SidebarSlotsService + /** The client workspaces service face (file-open funnel). */ + workspaces: SidebarWorkspacesService + /** The settings service face (prefs persistence + namespace reads). */ + settings: SidebarSettingsService + /** The invariant registry face. */ + invariants: SidebarInvariantsService + /** The tool registry face. */ + tools: SidebarToolsService + /** The client locale service face. */ + locale: SidebarLocaleService + /** The client module system (rc.8+ chunk-loader externals). */ + modules: { import(specifier: string): Promise } + /** The host background-job registry (optional; routes degrade to 503). */ + jobs: SidebarJobsService + /** The host live-agent registry (optional; side chat thread agents). */ + agents: SidebarAgentsService + /** The host subagent runtime (optional; live topology batch route). */ + subagents: SidebarSubagentsService + /** The host agent-presets service (optional; side chat cold resume). */ + agentPresets: SidebarAgentPresetsService + /** The host session-title service (optional; side chat thread label pin). */ + sessionTitle: SidebarSessionTitleService + /** The host session-persistence service (optional; side chat cold resume). */ + sessionPersistence: SidebarSessionPersistenceService + /** The composer draft face (client ui-conversation, lazy `ctx.get` probe). */ + conversation: SidebarConversation + /** + * The client-side sidebar registry: external plugins register tab types + * and file previewers here. Provided by the client half (see + * {@link ./client/index.tsx}); undefined on the host side. + */ + betterSidebar: BetterSidebarService + /** + * String-keyed session feed subscribe (the vendored cordis `on` is keyed + * to its typed Events map; the harness session feed is a plain string + * event). The listener receives every appended session event with the + * LIVE Session instance that appended it. + */ + on(event: string, listener: (session: unknown, event: SidebarSessionEvent) => void): () => void } /** - * Dual cordis-scope augmentation: DSH's runtime (and the public packages) - * resolve against the vendored `@deepseek-ai/cordis` scope, which DSH's own - * packages already augment with `effect`/`on`/service members. This side only - * adds the sidebar service member so consumers importing `Context` from - * `@deepseek-ai/cordis` (instead of the public `cordis` above) still see - * `ctx.betterSidebar`. + * The Context this plugin sees: the vendored cordis Context intersected with + * the structural service faces above. Re-exported from the package root so a + * consumer can `import type { Context } from 'dsh-better-sidebar'`. + */ +export type Context = CordisContext & SidebarContextShape + +/** + * Consumer-facing augmentation (deliberately the only one kept): a plugin + * that imports `Context` from `@deepseek-ai/cordis` and does + * `import type {} from 'dsh-better-sidebar'` sees `ctx.betterSidebar` + * without importing this package's own Context type. */ declare module '@deepseek-ai/cordis' { interface Context { betterSidebar: BetterSidebarService } } - -export type { Context } diff --git a/src/index.ts b/src/index.ts index 067add6..a5c4830 100644 --- a/src/index.ts +++ b/src/index.ts @@ -54,8 +54,10 @@ import { readJsonBody, requireString, SidebarError, writeError, writeJson, write export { Config } export type { SidebarConfig, ResolvedSidebarConfig } -// Re-export the Context augmentation (declare module 'cordis') so consumers -// `import type {} from 'dsh-better-sidebar'` and gain `ctx.betterSidebar`. +// Re-export the Context augmentation (`declare module '@deepseek-ai/cordis'`) +// so consumers `import type {} from 'dsh-better-sidebar'` and gain +// `ctx.betterSidebar`; the Context re-export below is the vendored cordis +// Context intersected with the structural service faces. // Also re-export the service descriptor types so consumers can type their // registerTab / registerFileViewer arguments without reaching into /client. export type { Context } from './context-types.ts' diff --git a/tests/market-manifest.spec.ts b/tests/market-manifest.spec.ts new file mode 100644 index 0000000..3638575 --- /dev/null +++ b/tests/market-manifest.spec.ts @@ -0,0 +1,108 @@ +/** + * DSH community-market compatibility guard: keeps the npm package inside the + * "Host 接受什么" rules of dsh-community-market's + * docs/install-and-uninstall.zh.md, so the catalog's verified npm install + * target (repository_backlink) keeps passing for this package: + * + * - no `cordis` entry in dependencies / peerDependencies / + * optionalDependencies (the market preview hard-rejects the legacy Cordis + * runtime name in any of the three fields, even when optional), + * - no install lifecycle scripts (preinstall / install / postinstall / + * prepare) in the **packed** manifest — the repo manifest legitimately + * keeps `prepare` for git-install builds; pnpm strips it from the published + * surface, and this guard verifies that stripped surface stays clean, + * - a safe, bund-visible `dsh.bundle.patch` path (`./cordis.patch.yml`), + * - a repository identity that normalizes to a credential-free HTTPS GitHub + * owner/repo URL (the catalog ↔ npm backlink comparison), + * - an exact stable SemVer version (no ranges, no tags, no prerelease). + * + * The packed-manifest checks run `pnpm pack` into a temp dir (the `prepare` + * script rebuilds the gitignored lib/, like the release flow does). + */ +import { execFileSync } from 'node:child_process' +import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' + +const ROOT = resolve(fileURLToPath(new URL('..', import.meta.url))) + +const pkg = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf8')) as { + name: string + version: string + repository?: { type?: string; url?: string } + engines?: { node?: string } + dsh?: { bundle?: { patch?: string } } + scripts?: Record + dependencies?: Record + peerDependencies?: Record + optionalDependencies?: Record +} + +/** The market's lifecycle-script reject list (install-and-uninstall.zh.md). */ +const LIFECYCLE_SCRIPTS = ['preinstall', 'install', 'postinstall', 'prepare'] as const + +/** Stable exact SemVer: exactly three numeric segments (no tag / prerelease). */ +const STABLE_SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u + +/** Read `package.json` from a fresh `pnpm pack` tarball (the publish surface). */ +function packedManifest(): Record { + const dir = mkdtempSync(join(tmpdir(), 'market-pack-')) + try { + execFileSync('pnpm', ['pack', '--pack-destination', dir], { cwd: ROOT, stdio: 'pipe' }) + execFileSync('tar', ['-xzf', join(dir, `${pkg.name}-${pkg.version}.tgz`), '-C', dir, 'package/package.json'], { stdio: 'pipe' }) + return JSON.parse(readFileSync(join(dir, 'package/package.json'), 'utf8')) as Record + } finally { + rmSync(dir, { recursive: true, force: true }) + } +} + +describe('DSH community-market manifest compatibility', () => { + it('declares no cordis entry in any dependency field (dependencies / peerDependencies / optionalDependencies)', () => { + for (const field of ['dependencies', 'peerDependencies', 'optionalDependencies'] as const) { + const deps = pkg[field] + if (deps === undefined) continue + expect(Object.keys(deps), field).not.toContain('cordis') + } + }) + + it('declares no install lifecycle scripts in the PACKED manifest (the published surface)', () => { + const packed = packedManifest() + const scripts = (packed.scripts as Record | undefined) ?? {} + for (const script of LIFECYCLE_SCRIPTS) { + expect(scripts, script).not.toHaveProperty(script) + } + }) + + it('declares a safe bundle patch that exists inside the package', () => { + const patch = pkg.dsh?.bundle?.patch + expect(patch, 'dsh.bundle.patch').toBeDefined() + const path = patch! + expect(path.startsWith('./'), 'must be a relative path').toBe(true) + const relative = path.slice(2) + expect(relative).not.toBe('') + expect(relative.startsWith('/')).toBe(false) + expect(relative.includes('\\')).toBe(false) + expect(relative.split('/').every(segment => segment.length > 0 && segment !== '.' && segment !== '..')) + .toBe(true) + expect(existsSync(resolve(ROOT, relative)), relative).toBe(true) + }) + + it('declares a repository identity the market can normalize to a credential-free HTTPS GitHub owner/repo URL', () => { + const repository = pkg.repository + expect(repository, 'repository').toBeDefined() + const url = new URL(repository!.url!) + expect(url.protocol).toBe('https:') + expect(url.username).toBe('') + expect(url.password).toBe('') + expect(url.search).toBe('') + expect(url.hash).toBe('') + expect(url.hostname.toLowerCase()).toBe('github.com') + expect(url.pathname.split('/').filter(Boolean)).toHaveLength(2) + }) + + it('declares an exact stable SemVer version (no range, tag, or prerelease)', () => { + expect(pkg.version).toMatch(STABLE_SEMVER) + }) +})