From 07eebbbad0281a3ffb7db8dc590d7f9432e4940a Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Sun, 30 Aug 2026 23:42:16 +0800 Subject: [PATCH] =?UTF-8?q?feat!:=20move=20to=20the=20alpha=20track=20?= =?UTF-8?q?=E2=80=94=20DSH=200.1.2-alpha.x=20only=20(v0.18.0-alpha.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DSH itself is still on its 0.1.2-alpha line (npm `alpha` dist-tag), so the plugin mirrors it: v0.18.0-alpha.0 publishes under the npm `alpha` dist-tag and supports DSH 0.1.2-alpha.x only. Stable-DSH (0.1.0-rc.8 ~ 0.1.1-rc.2) users stay on v0.17.1 (npm `latest`). Verified end-to-end against a real 0.1.2-alpha.2 host: full vitest suite, typecheck, build/pack, consumer type surface, and the headless mount smoke 14/14 (the prerelease version also passes `dsh plugin add file:`). - peer floor raised `^0.1.0-rc.8` -> `^0.1.2-alpha.2` (the mount install no longer warns: the old floor could not semver-match a prerelease host); dsh.plugin.json engines.dsh `>=0.1.2-alpha.2`. - Dropped the pre-alpha compatibility layer: - e2e host RPC collapsed from the dot/slash dual dialect to slash-only (rpcAttempt; the 404-probe negotiation and its cache are gone), and parseLaunchUrl now requires the ?token= launch URL; - MarkdownText labels collapsed to the nested single shape (the cast became unnecessary against the alpha.2 declarations); - the defunct `@deepseek-ai/dsh-client-runtime` row is gone from the chunk externals allowlist (chunk-loader / tsdown / two specs) and from dsh.client.inject. - market-manifest guard now allows an exact prerelease version (alpha cuts are not the market's verified `latest` target); release.yml picks the npm dist-tag from the version (prerelease -> `alpha`, else `latest`) so an alpha can never hijack `latest`. - Docs retargeted: AGENTS.md §2/§3, README/README_EN badges + v0.18.0 -alpha.0 section + install note (`dsh-better-sidebar@alpha`), guide. --- .github/workflows/release.yml | 10 ++- AGENTS.md | 28 ++++--- README.md | 19 +++-- README_EN.md | 19 +++-- docs/external-plugin-guide.md | 2 +- dsh.plugin.json | 4 +- package.json | 29 ++++--- src/client/chunk-loader.ts | 18 ++--- src/client/markdown-labels.tsx | 57 +++++--------- src/client/service.ts | 2 +- tests/chunk-loader.spec.ts | 1 - tests/e2e-host-protocol.spec.ts | 118 +++++++++-------------------- tests/e2e/host-protocol.ts | 106 ++++++++++++-------------- tests/e2e/host.ts | 104 ++++++++++--------------- tests/manifest-consistency.spec.ts | 1 - tests/market-manifest.spec.ts | 13 ++-- tsdown.config.ts | 3 +- 17 files changed, 222 insertions(+), 312 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d5c2f36..de47be6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,6 +6,11 @@ name: Release # Trigger: a GitHub Release is published (tag vX.Y.Z). The tag MUST match # package.json `version`; the workflow verifies this before publishing. # +# Dist-tag policy: a PRERELEASE version (e.g. v0.18.0-alpha.0 — the DSH +# 0.1.2-alpha adapter track) publishes under the npm `alpha` dist-tag so the +# `latest` install target keeps pointing at the last stable cut (which +# supports the stable DSH line); stable versions publish as `latest`. +# # One-time npm-side setup (manual, on npmjs.com): # 1. package `dsh-better-sidebar` -> Settings -> Publishing access # -> Trusted Publishers -> "Add Trusted Publisher". @@ -94,4 +99,7 @@ jobs: - name: Publish to npm if: github.event_name == 'release' || github.event.inputs.dry_run != 'true' - run: pnpm publish --provenance --access public --no-git-checks + run: | + TAG=$(node -p "require('./package.json').version.includes('-') ? 'alpha' : 'latest'") + echo "publishing to npm dist-tag: ${TAG}" + pnpm publish --provenance --access public --no-git-checks --tag "$TAG" diff --git a/AGENTS.md b/AGENTS.md index 275267e..8a95f46 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,26 +19,24 @@ 「npm 打包 → 真实挂载 → 无头渲染」门禁(证明打包产物在真实 DSH 挂载后不 crash):`pnpm build && pnpm pack` 产 tarball → `scripts/e2e-mount.sh` 装进全新 scratch profile(`dsh plugin --profile web add file:`)并启动真实 `dsh web`(keyless,`--port 0`)→ `tests/e2e/mount.e2e.ts`(Playwright)断言 `[data-dsh-better-sidebar]` 挂载、无错误条/pageerror/console 错误,经「+ 菜单」逐个打开内置 tab(含终端懒加载 chunk),再经文件树打开 seed 文件强制加载 editor chunk(`client-editor.js`)。 -本地:`pnpm build && pnpm pack && pnpm exec playwright install chromium && pnpm test:mount`。CI 钉 `@deepseek-ai/dsh@0.1.2-alpha.2`(`alpha` dist-tag;peer 下限 `^0.1.0-rc.8`,双向兼容)。e2e spec 命名 `*.e2e.ts` + vitest `exclude` 双保险;**改 `exclude` 必须保留默认排除项**(exclude 整体替换默认值)。 +本地:`pnpm build && pnpm pack && pnpm exec playwright install chromium && pnpm test:mount`。CI 钉 `@deepseek-ai/dsh@0.1.2-alpha.2`(`alpha` dist-tag;peer 下限 `^0.1.2-alpha.2`)。e2e spec 命名 `*.e2e.ts` + vitest `exclude` 双保险;**改 `exclude` 必须保留默认排除项**(exclude 整体替换默认值)。 --- -## 3. DSH 0.1.2-alpha.x 适配要点(双协议 e2e) +## 3. DSH 0.1.2-alpha 适配要点(alpha 通道) -tag `dsh-v0.1.2-alpha.1`,尚未发布 npm;**0.1.2-alpha.2 已发布 npm(`alpha` dist-tag),CI 钉版与 devDependencies 基线随之升级**(devDeps 里的死包 `@deepseek-ai/dsh-client-runtime` 已移除)。alpha.1 的破坏性变更(alpha.2 上经真机挂载冒烟 14/14 复验仍成立): +**v0.18.0-alpha.0 起插件进入 alpha 通道,仅支持 DSH 0.1.2-alpha.x**(peer 下限 `^0.1.2-alpha.2`,CI 钉 `@deepseek-ai/dsh@0.1.2-alpha.2`,npm 走 `alpha` dist-tag)。对 0.1.0-rc.8 ~ 0.1.1-rc.2 的兼容层已删除——双方言 RPC(点分回退)、MarkdownText 双形状 labels、`dsh-client-runtime` externals/inject 残留行;stable DSH 用户请装 v0.17.1(npm `latest`)。发版:release.yml 按版本号是否含 `-` 自动选 `alpha`/`latest` dist-tag。 -1. **一次性 token 鉴权**:就绪行变为 `dsh web: http://127.0.0.1:/?token=<43字符>`(导航换签名 cookie,干净 URL 401)。`e2e-mount.sh` 的 URL grep 必须延伸到空白(`[^ ]*`,在 `/` 截断丢 token);e2e 统一走 `tests/e2e/host.ts`:`PAGE_URL`/`pageUrl({...})` goto(合并 query 与 token),`createHostApi()` 先 token 换 `Set-Cookie`(`redirect: 'manual'`)。token 换 cookie 的 303 会**丢弃同 URL 其它 query 参数**——带 stamps 导航走 `host.ts` 的 `gotoPage()`(先 addCookies 再直达)。插件 `/sidebar/*` 路由不受影响,同源 fetch 照旧。 -2. **Remote gateway 取代 ApiProxy**:点分 `POST /api/workspace.create`(裸参数)→ 斜杠 `POST /api/workspace/create`,payload 恰为 `{args: {...}}`,**args 按控制器 TS 参数名包装**(`workspace/create`、`session/create` → `{args:{request:{...}}}`;`session/list` 参数名 `_request` **不可省略**——`{}` 也被拒 `args fields do not match the descriptor`);envelope `method` 与路径一致,点分 404。`hostRpc(api, 'workspace.create', args)` 点分优先、404 回退斜杠并缓存;`SLASH_ARGS_KEY` 由 `tests/e2e-host-protocol.spec.ts` 锁定。 -3. **`@deepseek-ai/dsh-client-runtime` 已移除**(继任 seed 是裸名 `@deepseek-ai/dsh-client-store`,无 `/client` 子路径)。插件源码零引用(externals 白名单解析失败被 `buildExternalsRequire` 宽容跳过,见 `src/client/chunk-loader.ts` 头注释),已从 peerDependencies 移除;`dsh.client.inject` 保留该条目无害。 -4. **`MarkdownText` labels 契约(真机验证的破坏)**:labels prop 从扁平可选 `codeLabels` 改为**必填嵌套** `labels: { code: { copyLabel, copiedLabel }, footnotes }`——只传旧 prop 时 fence 渲染抛 `Cannot read properties of undefined (reading 'code')`。四个渲染点(mermaid.tsx / MarkdownHtml.tsx / TextEditor.tsx / SideChatView.tsx)统一走 `src/client/markdown-labels.tsx` 的 `markdownTextProps()`(双形状 + 双 prop 名两版通吃;漏传回退硬编码中文)。 -5. **侧边对话转录走自有路由,不碰客户端宿主 RPC**:`ctx.connection.api`(含 `sessions.history`)在 alpha.1 整体移除,继任 `session/follow|page` 又对 `origin:'subagent'` 会话强制 subagent 地址(普通 `{kind:'session'}` 被 `agent-busy` 拒)且分页 `throughSeq` 不得超当前游标(纯 page 拿不到尾页)。转录因此由 **`sidechat.events`** 插件路由供给(`src/sidechat-routes.ts`:live 读 `agent.session.events`、冷读 `sessionPersistence.inspect`(两版同形,alpha.1 的 inspect 返回展开后逻辑日志),服务端 `session/end-seed` 切割 + `afterSeq` 增量);`ctx.connection` 镜像与 inject 已删。设计见 [docs/plans/2026-08-20-sidechat-tab-design.md](docs/plans/2026-08-20-sidechat-tab-design.md) §10。 +宿主契约(均经真机挂载冒烟 14/14 验证): -alpha.2 相对 alpha.1 的增量(均已适配): - -6. **`dsh-settings` 删除运行时导出 `settingsNamespace`**:命名空间合法性校验转为编译期模板字面量 `SettingsNamespaceInput`(小写字母开头 + `[a-z0-9-]` 尾部),`'dsh-better-sidebar'` 字面量直接过——宿主侧直接传常量(`src/index.ts` 的 settings inject),不再调运行时助手。 -7. **`dsh-subagent` 的 `SUBAGENT_DESCRIPTOR_VERSION` 2 → 3**:sidechat 种子的 `subagent/descriptor` 版本由宿主包盖章,插件不硬编码;测试断言跟随常量(`tests/sidechat-routes.spec.ts`),勿钉字面量。 -8. **其余核实为无破坏**:`SessionEvent.ignorable` 恢复(插件无引用);Remote 网关统一 `RemoteError` 封装不改变 e2e 消费的 envelope 形状;token URL / 斜杠 RPC / `MarkdownText` labels 契约与 alpha.1 一致。 -9. **e2e scratch profile 的 `minimumReleaseAgeExclude` 增加 `'@deepseek-ai/*'`**(`scripts/e2e-mount.sh` / `e2e-aggregate-mount.sh`,与仓库根 `pnpm-workspace.yaml` 同策):alpha 版本常在发布后 24h 内跑 lane,pnpm 11 的 `minimumReleaseAge` 默认会拒装新鲜包。 +1. **一次性 token 鉴权**:就绪行 `dsh web: http://127.0.0.1:/?token=<43字符>`(导航换签名 cookie,干净 URL 401)。`e2e-mount.sh` 的 URL grep 必须延伸到空白(`[^ ]*`,在 `/` 截断丢 token);e2e 统一走 `tests/e2e/host.ts`(token 必选:`parseLaunchUrl` 对裸 origin 直接抛错),带 stamps 导航走 `gotoPage()`(先 addCookies 再直达——token 换 cookie 的 303 会丢弃同 URL 其它 query 参数)。插件 `/sidebar/*` 路由不受影响,同源 fetch 照旧。 +2. **Remote gateway 斜杠 RPC(唯一方言)**:`POST /api/workspace/create`,payload 恰为 `{args: {...}}`,**args 按控制器 TS 参数名包装**(`workspace/create`、`session/create` → `{args:{request:{...}}}`;`session/list` 参数名 `_request` **不可省略**——`{}` 也被拒 `args fields do not match the descriptor`);envelope `method` 与路径一致,点分路径 404。请求由 `tests/e2e/host-protocol.ts` 的 `rpcAttempt` 构造、`tests/e2e-host-protocol.spec.ts` 锁定;要调新方法先在真机验参数名再进 `RPC_ARGS_KEY`。 +3. **`MarkdownText` labels 嵌套契约**:必填 `labels: { code: { copyLabel, copiedLabel }, footnotes }`(漏传回退硬编码中文)。四个渲染点(mermaid.tsx / MarkdownHtml.tsx / TextEditor.tsx / SideChatView.tsx)统一走 `src/client/markdown-labels.tsx` 的 `markdownTextProps()`。 +4. **侧边对话转录走自有路由,不碰客户端宿主 RPC**:`ctx.connection.api`(含 `sessions.history`)在 alpha.1 整体移除,继任 `session/follow|page` 又对 `origin:'subagent'` 会话强制 subagent 地址(普通 `{kind:'session'}` 被 `agent-busy` 拒)且分页 `throughSeq` 不得超当前游标。转录因此由 **`sidechat.events`** 插件路由供给(`src/sidechat-routes.ts`:live 读 `agent.session.events`、冷读 `sessionPersistence.inspect`,服务端 `session/end-seed` 切割 + `afterSeq` 增量);`ctx.connection` 镜像与 inject 已删。设计见 [docs/plans/2026-08-20-sidechat-tab-design.md](docs/plans/2026-08-20-sidechat-tab-design.md) §10。 +5. **`dsh-settings` 无运行时 `settingsNamespace`**:命名空间合法性校验转为编译期模板字面量 `SettingsNamespaceInput`(小写字母开头 + `[a-z0-9-]` 尾部),`'dsh-better-sidebar'` 字面量直接过——宿主侧直接传常量(`src/index.ts` 的 settings inject)。 +6. **`dsh-subagent` 的 `SUBAGENT_DESCRIPTOR_VERSION` 2 → 3**:sidechat 种子的 `subagent/descriptor` 版本由宿主包盖章,插件不硬编码;测试断言跟随常量(`tests/sidechat-routes.spec.ts`),勿钉字面量。 +7. **`@deepseek-ai/dsh-client-runtime` 包已消亡**(继任 seed 是裸名 `dsh-client-store`,无 `/client` 子路径):peerDependencies、devDependencies、`dsh.client.inject`、chunk externals 白名单(`src/client/chunk-loader.ts` / `tsdown.config.ts` / `tests/chunk-loader.spec.ts` / `tests/manifest-consistency.spec.ts` 四处同步)均已无该条目。 +8. **e2e scratch profile 的 `minimumReleaseAgeExclude` 含 `'@deepseek-ai/*'`**(`scripts/e2e-mount.sh` / `e2e-aggregate-mount.sh`,与仓库根 `pnpm-workspace.yaml` 同策):alpha 版本常在发布后 24h 内跑 lane,pnpm 11 的 `minimumReleaseAge` 默认会拒装新鲜包。 --- @@ -57,7 +55,7 @@ alpha.2 相对 alpha.1 的增量(均已适配): - **构建纯度门**:client bundle 禁止 value-import `@dsh-external/*` 或非白名单 `@deepseek-ai/*`(`tsdown.config.ts` 拦截);`import type {}` 被擦除不触发——类型可共享,运行时符号不行;跨插件交互走 `ctx.betterSidebar` 方法调用。 - **懒加载 chunk**:重依赖(xterm/CodeMirror/mermaid)在独立 bundle(`lib/client-.js`),经 `/sidebar/bundle` 按需下发、`globalThis.__dshChunks__` 物化(`src/client/chunk-loader.ts`),**核心 bundle 禁止静态 import `src/client/chunks/*`**。 -- **i18n**:词典在 `betterSidebar` 命名空间,跟随 DSH `ctx.locale`;**新增 zh key 必须同步 `src/client/locales-ja.ts` 的 ja 翻译**(否则 ja 下回退 en)。渲染 `MarkdownText` 必须经 `markdownTextProps()`(§3 第 4 条)。 +- **i18n**:词典在 `betterSidebar` 命名空间,跟随 DSH `ctx.locale`;**新增 zh key 必须同步 `src/client/locales-ja.ts` 的 ja 翻译**(否则 ja 下回退 en)。渲染 `MarkdownText` 必须经 `markdownTextProps()`(§3 第 3 条)。 - **皮肤契约**:视觉值只消费 `--dsw-alias-*` / `--dsw-font-*` / `--ds-*` 令牌,无硬编码颜色;契约全文与 titleBar 四方案模型见[指南 §12](docs/external-plugin-guide.md),改动必须同步该节与 `tests/theme.spec.ts`。 - **契约反向引用**:皮肤契约被 `src/client/shell-presets.ts`、`tests/e2e/mount.e2e.ts` 的注释以「指南 §12」引用——调整指南章节结构时同步检查这两处。 - **接入 API 即文档**:`src/client/service.ts` 与 `src/client/builtins/` 的任何行为变更,必须同步 [docs/external-plugin-guide.md](docs/external-plugin-guide.md)(唯一权威接入文档,不再双份维护)。 diff --git a/README.md b/README.md index 8226cde..fa59121 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ GitHub stars License: MIT dshfind

- 支持的 DSH 版本:0.1.0-rc.8 · 0.1.1-rc.1 · 0.1.1-rc.2 · 0.1.2-alpha.1 · 0.1.2-alpha.2 + 支持的 DSH 版本(v0.18.0-alpha.0 alpha 通道):0.1.2-alpha.2 插件生态:GitHub topic dsh-better-sidebar

文件管理 编辑预览 内嵌浏览器 真实终端 Git 面板 后台任务 侧边对话 插件接入

右侧栏 + 底部面板双工作台,并把 ctx.betterSidebar 服务开放给所有插件——
@@ -62,7 +62,9 @@ **前置**:已装好 DSH(`dsh web` 能正常运行),Node.js ≥ 20、pnpm ≥ 10。 **支持的 DSH 版本**: -支持的 DSH 版本:0.1.0-rc.8 · 0.1.1-rc.1 · 0.1.1-rc.2 · 0.1.2-alpha.1 · 0.1.2-alpha.2 +支持的 DSH 版本(v0.18.0-alpha.0 alpha 通道):0.1.2-alpha.2 + +> 🧪 **alpha 通道**:`v0.18.0-alpha.0` 起适配 DSH **0.1.2-alpha.x**(npm dist-tag `alpha`),不再支持 0.1.0-rc.8 ~ 0.1.1-rc.2——DSH stable 用户请继续用 `@latest`(v0.17.1);DSH 0.1.2-alpha.2 用户安装 `dsh-better-sidebar@alpha`。 ```sh dsh plugin --profile web add dsh-better-sidebar@latest # 首次会因 pnpm 11 拦截 node-pty 构建脚本而失败(依赖已写入) @@ -256,16 +258,19 @@ 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 · 0.1.2-alpha.1 · 0.1.2-alpha.2 · 完整发布历史见 [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases) +**支持的 DSH 版本**:支持的 DSH 版本(v0.18.0-alpha.0 alpha 通道):0.1.2-alpha.2 · 完整发布历史见 [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases) -### v0.17.2 +### v0.18.0-alpha.0 -- 🔗 **DSH 0.1.2-alpha.2 适配**:DSH 0.1.2-alpha.2 已发布 npm(`alpha` dist-tag),CI 挂载门禁钉版与 `@deepseek-ai/*` devDependencies 基线随之升级(真机挂载冒烟 14/14 验证;token URL / 斜杠 RPC / `MarkdownText` labels 契约与 alpha.1 一致,未再破坏)。适配点:`dsh-settings` 移除运行时导出 `settingsNamespace`(命名空间改为编译期校验,宿主侧直接传常量);`dsh-subagent` 描述符版本 2→3(由宿主包盖章,测试断言跟随 `SUBAGENT_DESCRIPTOR_VERSION` 常量);`SessionEvent.ignorable` 恢复与 Remote 网关 `RemoteError` 封装经核实对本插件无破坏。 -- 🐛 **修复 DSH 0.1.2-alpha.1+ 上侧边对话转录空白**:转录轮询此前仍走 alpha.1 已移除的 `ctx.connection.api`(错误被静默吞掉,tab 永远渲染空转录),现改由插件自有 `sidechat.events` 路由供给(活线程读内存事件日志、冷线程读会话持久化,`afterSeq` 增量拉取,[sidechat-routes.ts](./src/sidechat-routes.ts))——合并自 `fix/sidechat-transcript-host-route` 分支。 +> 🧪 **alpha 通道**:本版仅支持 **DSH 0.1.2-alpha.x**(peer 下限 `^0.1.2-alpha.2`,npm dist-tag `alpha`,安装 `dsh-better-sidebar@alpha`);不再支持 0.1.0-rc.8 ~ 0.1.1-rc.2——stable DSH 用户请用 v0.17.1(npm `latest`)。 + +- 🔗 **适配 DSH 0.1.2-alpha.2(npm 已发布,`alpha` dist-tag)**:CI 挂载门禁钉版与 `@deepseek-ai/*` devDependencies 基线升至 0.1.2-alpha.2(真机挂载冒烟 14/14 验证)。适配点:`dsh-settings` 移除运行时导出 `settingsNamespace`(命名空间改为编译期校验,宿主侧直接传常量);`dsh-subagent` 描述符版本 2→3(由宿主包盖章,测试断言跟随 `SUBAGENT_DESCRIPTOR_VERSION` 常量);`SessionEvent.ignorable` 恢复与 Remote 网关 `RemoteError` 封装经核实对本插件无破坏。 +- 🐛 **修复 DSH 0.1.2-alpha.1+ 上侧边对话转录空白**:转录轮询此前仍走 alpha.1 已移除的 `ctx.connection.api`(错误被静默吞掉,tab 永远渲染空转录),现改由插件自有 `sidechat.events` 路由供给(活线程读内存事件日志、冷线程读会话持久化,`afterSeq` 增量拉取,[sidechat-routes.ts](./src/sidechat-routes.ts))。 +- 🧹 **删除对 0.1.1-rc.x 及更早的兼容层**:e2e 宿主 RPC 从点分/斜杠双方言收敛为斜杠单方言(token URL 必选,[host-protocol.ts](./tests/e2e/host-protocol.ts));`MarkdownText` labels 收敛为嵌套单形状([markdown-labels.tsx](./src/client/markdown-labels.tsx),不再双 prop 名);peerDependencies / devDependencies / `dsh.client.inject` / chunk externals 白名单四处同步清除已消亡的 `@deepseek-ai/dsh-client-runtime`。 ### v0.17.1 -- 🔗 **DSH 0.1.2-alpha.1 适配(双版本兼容)**:DSH 0.1.2-alpha.1 的 Remote gateway、一次性 token 浏览器鉴权与 `MarkdownText` labels 契约变更已全量适配,插件在 0.1.0-rc.8 ~ 0.1.1-rc.2 与 0.1.2-alpha.1 上一致工作(后者经 GitHub tag 源码构建的真机挂载冒烟 14/14 验证;alpha.1 至今未发布 npm,CI 钉版已在 v0.17.2 升至 npm 发布的 0.1.2-alpha.2)。要点:`MarkdownText` 四个渲染点统一改走双形状 labels helper([markdown-labels.tsx](./src/client/markdown-labels.tsx)),修复 alpha.1 上 markdown/mermaid 预览的 `reading 'code'` 崩溃;e2e 冒烟双协议化(token URL 换 cookie、`/api` 斜杠端点 + 按参数名包装 args,[tests/e2e/host-protocol.ts](./tests/e2e/host-protocol.ts));移除已在 0.1.2-alpha.1 消亡的 `@deepseek-ai/dsh-client-runtime` peer +- 🔗 **DSH 0.1.2-alpha.1 适配(双版本兼容)**:DSH 0.1.2-alpha.1 的 Remote gateway、一次性 token 浏览器鉴权与 `MarkdownText` labels 契约变更已全量适配,插件在 0.1.0-rc.8 ~ 0.1.1-rc.2 与 0.1.2-alpha.1 上一致工作(后者经 GitHub tag 源码构建的真机挂载冒烟 14/14 验证;alpha.1 至今未发布 npm,CI 钉版已在 v0.18.0-alpha.0 升至 npm 发布的 0.1.2-alpha.2)。要点:`MarkdownText` 四个渲染点统一改走双形状 labels helper([markdown-labels.tsx](./src/client/markdown-labels.tsx)),修复 alpha.1 上 markdown/mermaid 预览的 `reading 'code'` 崩溃;e2e 冒烟双协议化(token URL 换 cookie、`/api` 斜杠端点 + 按参数名包装 args,[tests/e2e/host-protocol.ts](./tests/e2e/host-protocol.ts));移除已在 0.1.2-alpha.1 消亡的 `@deepseek-ai/dsh-client-runtime` peer ### v0.16.1 diff --git a/README_EN.md b/README_EN.md index 65b5ea6..64af9e5 100644 --- a/README_EN.md +++ b/README_EN.md @@ -9,7 +9,7 @@ GitHub stars License: MIT dshfind

- Supported DSH versions: 0.1.0-rc.8 · 0.1.1-rc.1 · 0.1.1-rc.2 · 0.1.2-alpha.1 · 0.1.2-alpha.2 + Supported DSH versions (v0.18.0-alpha.0 alpha track): 0.1.2-alpha.2 Plugin ecosystem: GitHub topic dsh-better-sidebar

File management Edit & preview Embedded browser Real terminal Git panel Background tasks Side Chat Plugin integration

A dual workbench (right sidebar + bottom panel) that opens its ctx.betterSidebar service to every plugin —
@@ -62,7 +62,9 @@ **Prerequisites**: DSH installed (`dsh web` boots), Node.js ≥ 20, pnpm ≥ 10. **Supported DSH versions**: -Supported DSH versions: 0.1.0-rc.8 · 0.1.1-rc.1 · 0.1.1-rc.2 · 0.1.2-alpha.1 · 0.1.2-alpha.2 +Supported DSH versions (v0.18.0-alpha.0 alpha track): 0.1.2-alpha.2 + +> 🧪 **Alpha track**: starting with `v0.18.0-alpha.0` the plugin targets DSH **0.1.2-alpha.x** (npm dist-tag `alpha`) and drops 0.1.0-rc.8 ~ 0.1.1-rc.2 — stable-DSH users should stay on `@latest` (v0.17.1); DSH 0.1.2-alpha.2 users install `dsh-better-sidebar@alpha`. ```sh dsh plugin --profile web add dsh-better-sidebar@latest # first run fails: pnpm 11 blocks node-pty build scripts (the dependency is still written) @@ -261,16 +263,19 @@ The GitHub topic [`dsh-better-sidebar`](https://github.com/topics/dsh-better-sid Service API base screenshot -**Supported DSH versions**: Supported DSH versions: 0.1.0-rc.8 · 0.1.1-rc.1 · 0.1.1-rc.2 · 0.1.2-alpha.1 · 0.1.2-alpha.2 · full release history on the [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases) page +**Supported DSH versions**: Supported DSH versions (v0.18.0-alpha.0 alpha track): 0.1.2-alpha.2 · full release history on the [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases) page -### v0.17.2 +### v0.18.0-alpha.0 -- 🔗 **DSH 0.1.2-alpha.2 adaptation**: DSH 0.1.2-alpha.2 is published to npm (under the `alpha` dist-tag), so the CI mount gate's pin and the `@deepseek-ai/*` devDependencies baseline moved up to it (verified by a real-host mount smoke 14/14; the token URL / slash RPC / `MarkdownText` labels contracts are unchanged from alpha.1). Adaptation points: `dsh-settings` dropped the runtime `settingsNamespace` export (namespaces are now validated at compile time — the host passes the constant directly); the `dsh-subagent` descriptor version went 2→3 (stamped by the host package; the test assertion follows the `SUBAGENT_DESCRIPTOR_VERSION` constant); the restored `SessionEvent.ignorable` and the Remote gateway's unified `RemoteError` wrapping were verified to not affect this plugin. -- 🐛 **Fixed blank Side Chat transcripts on DSH 0.1.2-alpha.1+**: transcript polling still called the `ctx.connection.api` face removed in alpha.1 (the error was silently swallowed, so the tab rendered an empty transcript forever). Transcripts now come from the plugin's own `sidechat.events` route (live threads read the in-memory event log, cold threads read session persistence, with `afterSeq` delta pulls, [sidechat-routes.ts](./src/sidechat-routes.ts)) — merged from the `fix/sidechat-transcript-host-route` branch. +> 🧪 **Alpha track**: this release targets **DSH 0.1.2-alpha.x only** (peer floor `^0.1.2-alpha.2`, npm dist-tag `alpha`, install `dsh-better-sidebar@alpha`); 0.1.0-rc.8 ~ 0.1.1-rc.2 are no longer supported — stable-DSH users should stay on v0.17.1 (npm `latest`). + +- 🔗 **Adapted to DSH 0.1.2-alpha.2 (published to npm, `alpha` dist-tag)**: the CI mount gate's pin and the `@deepseek-ai/*` devDependencies baseline moved up to it (verified by a real-host mount smoke 14/14). Adaptation points: `dsh-settings` dropped the runtime `settingsNamespace` export (namespaces are now validated at compile time — the host passes the constant directly); the `dsh-subagent` descriptor version went 2→3 (stamped by the host package; the test assertion follows the `SUBAGENT_DESCRIPTOR_VERSION` constant); the restored `SessionEvent.ignorable` and the Remote gateway's unified `RemoteError` wrapping were verified to not affect this plugin. +- 🐛 **Fixed blank Side Chat transcripts on DSH 0.1.2-alpha.1+**: transcript polling still called the `ctx.connection.api` face removed in alpha.1 (the error was silently swallowed, so the tab rendered an empty transcript forever). Transcripts now come from the plugin's own `sidechat.events` route (live threads read the in-memory event log, cold threads read session persistence, with `afterSeq` delta pulls, [sidechat-routes.ts](./src/sidechat-routes.ts)). +- 🧹 **Dropped the pre-alpha (0.1.1-rc.x and older) compatibility layer**: the e2e host RPC collapsed from the dot/slash dual dialect to slash-only (token URL required, [host-protocol.ts](./tests/e2e/host-protocol.ts)); `MarkdownText` labels collapsed to the nested single shape ([markdown-labels.tsx](./src/client/markdown-labels.tsx), no more dual prop names); peerDependencies / devDependencies / `dsh.client.inject` / the chunk externals allowlist all dropped the defunct `@deepseek-ai/dsh-client-runtime` (four synchronized spots). ### v0.17.1 -- 🔗 **DSH 0.1.2-alpha.1 adaptation (dual-version compatible)**: fully adapted to DSH 0.1.2-alpha.1's Remote gateway, one-time-token browser authentication, and the `MarkdownText` labels contract change — the plugin works identically on 0.1.0-rc.8 ~ 0.1.1-rc.2 and 0.1.2-alpha.1 (the latter verified by a real-host mount smoke 14/14 against a source build of the GitHub tag; alpha.1 was never published to npm, and the CI pin moved to the npm-published 0.1.2-alpha.2 in v0.17.2). Highlights: all four `MarkdownText` render sites now go through the dual-shape labels helper ([markdown-labels.tsx](./src/client/markdown-labels.tsx)), fixing the `reading 'code'` crash in markdown/mermaid previews on alpha.1; the e2e mount smoke speaks both wire dialects (token-URL cookie exchange, slash `/api` endpoints with parameter-named args, [tests/e2e/host-protocol.ts](./tests/e2e/host-protocol.ts)); dropped the `@deepseek-ai/dsh-client-runtime` peer removed upstream in 0.1.2-alpha.1 +- 🔗 **DSH 0.1.2-alpha.1 adaptation (dual-version compatible)**: fully adapted to DSH 0.1.2-alpha.1's Remote gateway, one-time-token browser authentication, and the `MarkdownText` labels contract change — the plugin works identically on 0.1.0-rc.8 ~ 0.1.1-rc.2 and 0.1.2-alpha.1 (the latter verified by a real-host mount smoke 14/14 against a source build of the GitHub tag; alpha.1 was never published to npm, and the CI pin moved to the npm-published 0.1.2-alpha.2 in v0.18.0-alpha.0). Highlights: all four `MarkdownText` render sites now go through the dual-shape labels helper ([markdown-labels.tsx](./src/client/markdown-labels.tsx)), fixing the `reading 'code'` crash in markdown/mermaid previews on alpha.1; the e2e mount smoke speaks both wire dialects (token-URL cookie exchange, slash `/api` endpoints with parameter-named args, [tests/e2e/host-protocol.ts](./tests/e2e/host-protocol.ts)); dropped the `@deepseek-ai/dsh-client-runtime` peer removed upstream in 0.1.2-alpha.1 ### v0.16.1 diff --git a/docs/external-plugin-guide.md b/docs/external-plugin-guide.md index c887d73..5870554 100644 --- a/docs/external-plugin-guide.md +++ b/docs/external-plugin-guide.md @@ -2,7 +2,7 @@ > 面向 **消费插件开发者**:如何让你的插件向 better-sidebar 注册新的侧边栏页面(tab)和文件类型预览器。 > -> 适用版本:**v0.4.0+**(`ctx.betterSidebar` 服务);声明式设置 **v0.4.1+**;text/number 设置行 **v0.11.0+**;badge/生命周期/定向打开/插件设置/版本探测 **v0.12.0+**;select 设置行(`settingSelect`)与外链认领(`urlTarget`)**v0.13.0+**;统一 `@deepseek-ai/cordis` 类型基底 **v0.15.2+**;自由窗口(`floatWindows`)**v0.16.0+**;终端固定(pin)**v0.17.0+**。当前版本 v0.17.2。 +> 适用版本:**v0.4.0+**(`ctx.betterSidebar` 服务);声明式设置 **v0.4.1+**;text/number 设置行 **v0.11.0+**;badge/生命周期/定向打开/插件设置/版本探测 **v0.12.0+**;select 设置行(`settingSelect`)与外链认领(`urlTarget`)**v0.13.0+**;统一 `@deepseek-ai/cordis` 类型基底 **v0.15.2+**;自由窗口(`floatWindows`)**v0.16.0+**;终端固定(pin)**v0.17.0+**。当前版本 **v0.18.0-alpha.0**(alpha 通道,仅支持 DSH 0.1.2-alpha.x;stable 线为 v0.17.1)。 > 权威代码:`src/client/service.ts`(服务实现)、`src/client/builtins/`(内置 7 tab + 6 viewer 参考实现)、`lib/types/client/service.d.ts`(类型声明)。 > 仓库开发规则(硬约束 / CI / 发版)见 [AGENTS.md](../AGENTS.md)。 diff --git a/dsh.plugin.json b/dsh.plugin.json index 5d1f01b..92e0dec 100644 --- a/dsh.plugin.json +++ b/dsh.plugin.json @@ -1,10 +1,10 @@ { "id": "dsh-external/dsh-better-sidebar", - "version": "0.17.2", + "version": "0.18.0-alpha.0", "main": "./lib/index.js", "description": "VSCode 风格右侧侧边栏:文件资源管理器 / 编辑器 / 终端 / Git / 浏览器,按会话隔离。暴露服务供其他插件注册侧边栏页面和文件预览器", "engines": { - "dsh": ">=0.0.1" + "dsh": ">=0.1.2-alpha.2" }, "contributes": { "tools": [], diff --git a/package.json b/package.json index 1ec9fb7..036a3ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsh-better-sidebar", - "version": "0.17.2", + "version": "0.18.0-alpha.0", "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": { @@ -45,7 +45,6 @@ }, "client": { "inject": [ - "@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-ui-slots", "@deepseek-ai/dsh-client-ui-conversation", @@ -85,19 +84,19 @@ "license": "MIT", "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", - "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", - "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", - "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", - "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.8", - "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8", - "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.8", - "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8", - "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", - "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", - "@deepseek-ai/dsh-session": "^0.1.0-rc.8", - "@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", + "@deepseek-ai/dsh-agent": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-session": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-subagent": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-tools": "^0.1.2-alpha.2", "@huanlin/dsh-plugin-better-locale": "^0.1.0", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/src/client/chunk-loader.ts b/src/client/chunk-loader.ts index 5f7f64f..76c90e1 100644 --- a/src/client/chunk-loader.ts +++ b/src/client/chunk-loader.ts @@ -62,18 +62,11 @@ type ChunkFactory = (require: (spec: string) => unknown) => ChunkExports * CLIENT_EXTERNALS in tsdown.config.ts — the chunk builds keep these * external and the loader resolves them here). A superset is safe: the * require only answers what the chunk actually asks for. The shell's static - * module table seeds React, Cordis, and the UI libraries (primitives/slots); - * `dsh-client-runtime/client` normalizes onto the runtime package row - * (stripClientSuffix). dsh-client-web-react / dsh-client-schema-form were - * dropped in DSH 0.1.0-rc.8 (no rc.8 publish, nothing requires them) — the - * chunks never asked for them, so they no longer belong here. - * - * DSH 0.1.2-alpha.1 removed the `dsh-client-runtime` package outright (the - * seed table gained bare-name `@deepseek-ai/dsh-client-store` instead); the - * runtime/client row below stays for 0.1.1-rc.x hosts — no chunk requires - * it, and {@link buildExternalsRequire} keeps an unresolvable spec - * undefined until a chunk actually asks (only then is it a loud error), so - * the entry is inert on 0.1.2-alpha.1+. + * module table seeds React, Cordis, and the UI libraries (primitives/slots). + * `@deepseek-ai/dsh-client-runtime` was removed upstream in DSH 0.1.2-alpha + * (its seed row became bare-name `@deepseek-ai/dsh-client-store`) and no + * chunk ever required it, so its row is gone; so are dsh-client-web-react / + * dsh-client-schema-form, dropped back in DSH 0.1.0-rc.8. */ export const CHUNK_EXTERNALS: readonly string[] = [ 'react', @@ -83,7 +76,6 @@ export const CHUNK_EXTERNALS: readonly string[] = [ 'cordis', '@deepseek-ai/dsh-client-ui-slots', '@deepseek-ai/dsh-client-ui-primitives', - '@deepseek-ai/dsh-client-runtime/client', ] /** Chunk script endpoint served by the plugin host half (src/bundle-route.ts). */ diff --git a/src/client/markdown-labels.tsx b/src/client/markdown-labels.tsx index e29b0d6..1058d56 100644 --- a/src/client/markdown-labels.tsx +++ b/src/client/markdown-labels.tsx @@ -1,52 +1,31 @@ /** - * Copy-button / chrome labels for DSH's shared `MarkdownText`, shaped for - * BOTH prop generations the plugin supports: - * - * - 0.1.1-rc.x: optional flat prop `codeLabels` — the renderer reads - * `labels.copyLabel` / `labels.copiedLabel` directly. - * - 0.1.2-alpha.1+: renamed to a REQUIRED `labels` prop with a nested shape - * (`labels.code.copyLabel` + a screen-reader-only `labels.footnotes` - * heading). Passing only the old prop crashes the fence render with - * "Cannot read properties of undefined (reading 'code')" — the exact - * regression the mount lane caught on a real 0.1.2-alpha.1 host. - * - * The union object satisfies both readers, and {@link markdownTextProps} - * passes it under BOTH prop names — each host ignores the one it does not - * know. `footnotes` is left empty (the heading is sr-only; give it a real - * string only if a locale key ever earns its place in all 19 dictionaries). + * Copy-button / chrome labels for DSH's shared `MarkdownText` (DSH + * 0.1.2-alpha contract): the renderer takes a REQUIRED nested `labels` prop + * — `labels.code.copyLabel` / `labels.code.copiedLabel` for the fence copy + * buttons plus a screen-reader-only `labels.footnotes` heading — and the + * MarkdownText/CodeBlock are cordis-free, falling back to HARDCODED Chinese + * when the labels are omitted, so every render site threads the plugin + * dictionary's localized pair through here (re-evaluated per render). + * `footnotes` is left empty (the heading is sr-only; give it a real string + * only if a locale key ever earns its place in all 19 dictionaries). */ import type { ComponentProps } from 'react' import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives' -/** The flat copy-button pair the plugin threads through its own props - * (e.g. MermaidMarkdownProps.codeLabels — the chunk contract stays put). */ +/** The flat copy-button pair the plugin threads through its own props (e.g. + * MermaidMarkdownProps.codeLabels — the chunk contract stays put). */ export interface MarkdownCopyLabels { copyLabel: string copiedLabel: string } -/** The dual-generation chrome labels object (see module doc). */ -export interface MarkdownChromeLabels extends MarkdownCopyLabels { - /** 0.1.2-alpha.1+ nested reads. */ - code: MarkdownCopyLabels - /** 0.1.2-alpha.1+ sr-only footnotes heading. */ - footnotes: string -} - -/** Build the dual-shape chrome labels from a flat copy-button pair. */ -export function markdownChromeLabels(labels: MarkdownCopyLabels): MarkdownChromeLabels { +/** MarkdownText props carrying the nested chrome labels. */ +export function markdownTextProps(text: string, labels: MarkdownCopyLabels): ComponentProps { return { - copyLabel: labels.copyLabel, - copiedLabel: labels.copiedLabel, - code: { copyLabel: labels.copyLabel, copiedLabel: labels.copiedLabel }, - footnotes: '', + text, + labels: { + code: { copyLabel: labels.copyLabel, copiedLabel: labels.copiedLabel }, + footnotes: '', + }, } } - -/** MarkdownText props carrying the labels under BOTH prop names. The cast is - * load-bearing: the plugin builds against the 0.1.1-rc.x declaration, where - * `labels` does not exist yet (and vice versa on a 0.1.2-alpha.1+ host). */ -export function markdownTextProps(text: string, labels: MarkdownCopyLabels): ComponentProps { - const chrome = markdownChromeLabels(labels) - return { text, codeLabels: chrome, labels: chrome } as unknown as ComponentProps -} diff --git a/src/client/service.ts b/src/client/service.ts index 39ff47b..79aaa17 100644 --- a/src/client/service.ts +++ b/src/client/service.ts @@ -472,7 +472,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.17.2' +export const SIDEBAR_SERVICE_VERSION = '0.18.0-alpha.0' /** * Monotonic capability list consumers use to gate new API usage (features diff --git a/tests/chunk-loader.spec.ts b/tests/chunk-loader.spec.ts index 154100a..5fb94d3 100644 --- a/tests/chunk-loader.spec.ts +++ b/tests/chunk-loader.spec.ts @@ -417,7 +417,6 @@ describe('externals contract', () => { 'cordis', '@deepseek-ai/dsh-client-ui-slots', '@deepseek-ai/dsh-client-ui-primitives', - '@deepseek-ai/dsh-client-runtime/client', ]) }) }) diff --git a/tests/e2e-host-protocol.spec.ts b/tests/e2e-host-protocol.spec.ts index 4bf6262..0769ec5 100644 --- a/tests/e2e-host-protocol.spec.ts +++ b/tests/e2e-host-protocol.spec.ts @@ -1,23 +1,22 @@ /** * Unit tests for the e2e lanes' host-transport contracts (tests/e2e/ - * host-protocol.ts + host.ts), locking in the two DSH web dialects the lanes - * must speak: + * host-protocol.ts + host.ts), locking in the DSH 0.1.2-alpha web dialect + * the lanes speak: * - * - 0.1.1-rc.x: bare-origin launch URL, no browser auth, ApiProxy dot - * endpoints (`POST /api/workspace.create`, payload = the bare args). - * - 0.1.2-alpha.1+ (Remote gateway + one-time-token browser auth): - * `/?token=<43 chars>` launch URL (303 → signed cookie), slash endpoints - * (`POST /api/workspace/create`, payload must be exactly `{ args }`, the - * envelope method equal to the path endpoint; a dot path is no longer - * claimed → 404). + * - one-time-token browser auth: `/?token=<43 chars>` launch URL + * (303 → signed cookie; a token-less URL is a pre-alpha host and must + * fail loudly), + * - Remote-gateway slash endpoints (`POST /api/workspace/create`, payload + * must be exactly `{ args }` keyed by the controller's parameter name, + * the envelope method equal to the path endpoint). * - * The shapes were transcribed from the deepseek-harness sources at tags - * dsh-v0.1.1-rc.2 and dsh-v0.1.2-alpha.1 (packages/client/connection, - * packages/api/gateway, packages/bundle/web-app). + * The shapes were transcribed from the deepseek-harness sources at tag + * dsh-v0.1.2-alpha.1 and re-verified on the npm-published 0.1.2-alpha.2 + * (packages/client/connection, packages/api/gateway, packages/bundle/web-app). */ -import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { beforeAll, describe, expect, it, vi } from 'vitest' import type { APIRequestContext } from '@playwright/test' -import { parseLaunchUrl, pageUrlWith, rpcAttempts } from './e2e/host-protocol' +import { parseLaunchUrl, pageUrlWith, rpcAttempt } from './e2e/host-protocol' const BARE_URL = 'http://127.0.0.1:4199' const TOKEN_URL = 'http://127.0.0.1:4199/?token=AbCdEf0123456789_-AbCdEf0123456789_-AbCd' @@ -27,32 +26,24 @@ const TOKEN_URL = 'http://127.0.0.1:4199/?token=AbCdEf0123456789_-AbCdEf01234567 let host: typeof import('./e2e/host') beforeAll(async () => { - vi.stubEnv('DSH_E2E_URL', BARE_URL) + vi.stubEnv('DSH_E2E_URL', TOKEN_URL) host = await import('./e2e/host') }) describe('host-protocol: launch URL parsing', () => { - it('splits a bare-origin launch URL (0.1.1-rc.x hosts)', () => { - const launch = parseLaunchUrl(BARE_URL) - expect(launch.origin).toBe(BARE_URL) - expect(launch.pageUrl).toBe(BARE_URL) - expect(launch.token).toBeUndefined() - }) - - it('extracts the one-time token of an authenticated launch URL (0.1.2-alpha.1+)', () => { + it('extracts the one-time token of an authenticated launch URL', () => { const launch = parseLaunchUrl(TOKEN_URL) expect(launch.origin).toBe(BARE_URL) expect(launch.pageUrl).toBe(TOKEN_URL) expect(launch.token).toBe('AbCdEf0123456789_-AbCdEf0123456789_-AbCd') }) + + it('rejects a token-less launch URL (a pre-0.1.2-alpha host the plugin no longer supports)', () => { + expect(() => parseLaunchUrl(BARE_URL)).toThrow(/no \?token=/) + }) }) describe('host-protocol: page URL query merge', () => { - it('appends stamps to a bare-origin URL', () => { - expect(pageUrlWith(BARE_URL, { 'dsh-desktop-mode': 'advanced' })) - .toBe(`${BARE_URL}/?dsh-desktop-mode=advanced`) - }) - it('merges stamps into a token URL without breaking the token (never append ?…?…)', () => { const merged = pageUrlWith(TOKEN_URL, { 'dsh-desktop-mode': 'advanced', 'dsh-desktop-platform': 'win32' }) const parsed = new URL(merged) @@ -63,45 +54,33 @@ describe('host-protocol: page URL query merge', () => { }) }) -describe('host-protocol: dual-dialect RPC attempts', () => { - it('orders the 0.1.1-rc.x dot dialect first (published hosts never pay a probe)', () => { +describe('host-protocol: slash RPC attempts', () => { + it('targets the slash endpoint with args wrapped by the parameter name', () => { const args = { path: '/tmp/w' } - const [dot, slash] = rpcAttempts('workspace.create', args) - expect(dot).toEqual({ - protocol: 'dot', - path: '/api/workspace.create', - method: 'workspace.create', - payload: args, - }) - // The dot payload is the BARE args object (same reference — no wrapper). - expect(dot!.payload).toBe(args) - expect(slash).toEqual({ - protocol: 'slash', + expect(rpcAttempt('workspace.create', args)).toEqual({ path: '/api/workspace/create', method: 'workspace/create', payload: { args: { request: args } }, }) }) - it('keys slash args by the parameter name — session/list is literally `_request` (verified on a live 0.1.2-alpha.1 host)', () => { + it('keys args by the parameter name — session/list is literally `_request` (verified on a live 0.1.2-alpha host)', () => { // typert gateway: session/list declares `_request` and rejects `{}` / // `{request:{}}` — the wrapper table must reproduce the exact shape. - expect(rpcAttempts('session.list', {})[1]).toEqual({ - protocol: 'slash', + expect(rpcAttempt('session.list', {})).toEqual({ path: '/api/session/list', method: 'session/list', payload: { args: { _request: {} } }, }) - expect(rpcAttempts('session.create', { workspaceId: 'w' })[1]).toEqual({ - protocol: 'slash', + expect(rpcAttempt('session.create', { workspaceId: 'w' })).toEqual({ path: '/api/session/create', method: 'session/create', payload: { args: { request: { workspaceId: 'w' } } }, }) }) - it('fails loudly for methods without a verified slash args key', () => { - expect(() => rpcAttempts('settings.describe', {})).toThrow(/no slash-dialect args key/) + it('fails loudly for methods without a verified args key', () => { + expect(() => rpcAttempt('settings.describe', {})).toThrow(/no args key/) }) }) @@ -136,43 +115,16 @@ function stubApi(responses: StubResponse[]): APIRequestContext & { paths: string return api as unknown as APIRequestContext & { paths: string[]; bodies: unknown[] } } -describe('host glue: hostRpc dialect negotiation', () => { - beforeEach(() => { - host.resetHostRpcForTests() - }) - - it('uses the dot dialect directly when the host answers it (0.1.1-rc.x)', async () => { - const api = stubApi([{ status: 200, body: { type: 'server-response', rpcId: 'x', result: { ok: true, value: { items: [] } } } }]) - const first = await host.hostRpc<{ items: unknown[] }>(api, 'session.list', {}) - expect(first.value.items).toEqual([]) - // Second call must NOT probe again: the resolved dialect is cached. - await host.hostRpc(api, 'session.list', {}) - expect(api.paths).toEqual(['/api/session.list', '/api/session.list']) - }) - - it('falls back to the slash dialect on 404 and caches it (0.1.2-alpha.1+)', async () => { - const api = stubApi([ - { status: 404, body: { error: 'not found' } }, - { status: 200, body: { type: 'server-response', rpcId: 'x', result: { ok: true, value: { sessionId: 'session-1' } } } }, - ]) +describe('host glue: hostRpc', () => { + it('posts the Remote-gateway envelope to the slash endpoint and unwraps the result', async () => { + const api = stubApi([{ status: 200, body: { type: 'server-response', rpcId: 'x', result: { ok: true, value: { sessionId: 'session-1' } } } }]) const seeded = await host.hostRpc<{ sessionId: string }>(api, 'session.create', { workspaceId: 'w' }) expect(seeded.value.sessionId).toBe('session-1') - expect(api.paths).toEqual(['/api/session.create', '/api/session/create']) - // The slash attempt carries the Remote-gateway payload shape: args keyed - // by the controller's parameter name (`request` for session/create), - // method equal to the path endpoint. - const slashEnvelope = api.bodies[1] as { type: string; method: string; payload: Record } - expect(slashEnvelope.type).toBe('client-request') - expect(slashEnvelope.method).toBe('session/create') - expect(slashEnvelope.payload).toEqual({ args: { request: { workspaceId: 'w' } } }) - // Cached: the next call goes straight to the slash endpoint. - await host.hostRpc(api, 'session.list', {}) - expect(api.paths[2]).toBe('/api/session/list') - }) - - it('fails loudly when neither dialect is claimed', async () => { - const api = stubApi([{ status: 404, body: {} }]) - await expect(host.hostRpc(api, 'workspace.create', { path: '/tmp/w' })).rejects.toThrow(/no dialect answered/) + expect(api.paths).toEqual(['/api/session/create']) + const envelope = api.bodies[0] as { type: string; method: string; payload: Record } + expect(envelope.type).toBe('client-request') + expect(envelope.method).toBe('session/create') + expect(envelope.payload).toEqual({ args: { request: { workspaceId: 'w' } } }) }) it('surfaces an envelope error instead of returning it', async () => { diff --git a/tests/e2e/host-protocol.ts b/tests/e2e/host-protocol.ts index 99b303d..fc43a16 100644 --- a/tests/e2e/host-protocol.ts +++ b/tests/e2e/host-protocol.ts @@ -1,28 +1,28 @@ /** - * Pure host-transport contracts for the e2e lanes, spanning the DSH - * 0.1.1-rc.x → 0.1.2-alpha.1 breaking split (Remote gateway + one-time-token - * browser auth). No Playwright imports here — these shapes are unit-testable + * Pure host-transport contracts for the e2e lanes against the DSH + * 0.1.2-alpha.x web host (Remote gateway + one-time-token browser auth). No + * Playwright imports here — these shapes are unit-testable * (tests/e2e-host-protocol.spec.ts); ./host.ts wires them to a request * context. * - * What changed in 0.1.2-alpha.1 (deepseek-harness tag dsh-v0.1.2-alpha.1): + * Host contract (deepseek-harness tags dsh-v0.1.2-alpha.1/2, verified on + * live hosts; the pre-alpha dual-dialect layer was dropped in the plugin's + * v0.18.0-alpha.0 alpha track): * * - `dsh web` prints an AUTHENTICATED launch URL — `dsh web: * http://127.0.0.1:/?token=<43 base64url chars>`. Navigating it * exchanges the token for a signed cookie (HttpOnly, SameSite=Strict) that - * every `/api` request must then carry; the clean URL answers 401. Older - * hosts print a bare origin and have no browser auth. - * - The legacy ApiProxy dot-method endpoints (`POST /api/workspace.create` - * with `payload: `) were replaced by the Remote gateway's slash - * endpoints — `POST /api/workspace/create` with `payload: { args }` whose - * args object is keyed by the controller's TypeScript PARAMETER name - * (`workspace/create` → `{ request: {...} }`; `session/list`'s unused - * parameter is literally `_request` and is not omissible); a dot path is - * no longer claimed (404). The `{type:'client-request', rpcId, method, - * payload}` / `{type:'server-response', rpcId, result}` envelopes are - * unchanged, and the plugin's own `/sidebar/*` routes stay public (the - * webserver carrier owns no authentication — only `/api`, the index HTML, - * and the remote mux upgrade sit behind the browser auth). + * every `/api` request must then carry; the clean URL answers 401. + * - The Remote gateway serves SLASH endpoints — `POST /api/workspace/create` + * with `payload: { args }` whose args object is keyed by the controller's + * TypeScript PARAMETER name (`workspace/create` → `{ request: {...} }`; + * `session/list`'s unused parameter is literally `_request` and is NOT + * omissible); the legacy dot path is no longer claimed (404). The + * `{type:'client-request', rpcId, method, payload}` / + * `{type:'server-response', rpcId, result}` envelopes are unchanged, and + * the plugin's own `/sidebar/*` routes stay public (the webserver carrier + * owns no authentication — only `/api`, the index HTML, and the remote mux + * upgrade sit behind the browser auth). */ /** The pieces of a `dsh web` launch URL the lanes need. */ @@ -30,22 +30,23 @@ export interface LaunchUrl { /** Scheme + host + port — the base for URL construction (a token URL would * corrupt path concatenation). */ origin: string - /** The URL as printed (token query included on 0.1.2-alpha.1+); the correct - * page.goto target — navigating it performs the token→cookie exchange. */ + /** The URL as printed (token query included) — the correct page.goto + * target; navigating it performs the token→cookie exchange. */ pageUrl: string - /** The one-time launch token, when the host printed one. */ - token: string | undefined + /** The one-time launch token. */ + token: string } -/** Split a `dsh web` launch URL (bare origin on 0.1.1-rc.x, `/?token=` on - * 0.1.2-alpha.1+) into the pieces the lanes address. */ +/** Split a `dsh web` launch URL (`/?token=` — every supported host prints + * one) into the pieces the lanes address. Throws without a token: a bare + * origin is a pre-0.1.2-alpha host this plugin no longer supports. */ export function parseLaunchUrl(raw: string): LaunchUrl { const url = new URL(raw) - return { - origin: url.origin, - pageUrl: raw, - token: url.searchParams.get('token') ?? undefined, + const token = url.searchParams.get('token') + if (token === null) { + throw new Error(`launch URL carries no ?token= — not a DSH 0.1.2-alpha+ authenticated launch URL: ${raw}`) } + return { origin: url.origin, pageUrl: raw, token } } /** Page navigation URL with extra query stamps (e.g. desktop-shell URL @@ -57,48 +58,39 @@ export function pageUrlWith(raw: string, extra: Record): string return url.toString() } -/** One host-RPC protocol attempt. */ +/** One host-RPC request. */ export interface RpcAttempt { - /** Which host dialect this attempt speaks. */ - protocol: 'dot' | 'slash' /** Request path, relative to the origin, starting with `/api/`. */ path: string /** Envelope `method` string — must equal the endpoint the path selects. */ method: string - /** Envelope `payload` — the bare args object on dot hosts, `{ args }` on - * slash hosts (the gateway rejects any other payload shape). */ + /** Envelope `payload` — `{ args }` keyed by the controller parameter name + * (the gateway rejects any other payload shape). */ payload: Record } -/** The slash-dialect args key per host method: the 0.1.2-alpha.1 Remote - * controllers key the args object by the TypeScript PARAMETER name, not by - * the endpoint's own naming — workspace/create and session/create declare a - * single `request` parameter, while session/list's unused parameter is - * literally named `_request` (and is NOT omissible: the typert gateway - * rejects `{}` and `{request:{}}` alike). Verified against a live - * dsh-v0.1.2-alpha.1 host: every other shape fails with `args fields do not - * match the descriptor`. */ -const SLASH_ARGS_KEY: Record = { +/** The args key per host method: the 0.1.2-alpha Remote controllers key the + * args object by the TypeScript PARAMETER name, not by the endpoint's own + * naming — workspace/create and session/create declare a single `request` + * parameter, while session/list's unused parameter is literally named + * `_request` (and is NOT omissible: the typert gateway rejects `{}` and + * `{request:{}}` alike). Verified against live dsh-v0.1.2-alpha hosts: + * every other shape fails with `args fields do not match the descriptor`. */ +const RPC_ARGS_KEY: Record = { 'workspace.create': 'request', 'session.create': 'request', 'session.list': '_request', } -/** Build the two dialects of one host RPC (method in dot form, e.g. - * `'workspace.create'`): the 0.1.1-rc.x dot endpoint first — the dialect - * every currently-published host speaks, so the deployed lanes never pay a - * probe round-trip — then the 0.1.2-alpha.1+ slash endpoint as the 404 - * fallback. Throws for methods without a known slash args key: the wrapper - * is per-parameter, so an unverified shape must fail loudly at build time - * instead of as a confusing gateway error mid-lane. */ -export function rpcAttempts(method: string, args: Record): RpcAttempt[] { - const slashKey = SLASH_ARGS_KEY[method] - if (slashKey === undefined) { - throw new Error(`host-protocol: no slash-dialect args key for ${JSON.stringify(method)} — verify the parameter name on the host and extend SLASH_ARGS_KEY`) +/** Build one host RPC request (method in dot form, e.g. + * `'workspace.create'`). Throws for methods without a known args key: the + * wrapper is per-parameter, so an unverified shape must fail loudly at + * build time instead of as a confusing gateway error mid-lane. */ +export function rpcAttempt(method: string, args: Record): RpcAttempt { + const key = RPC_ARGS_KEY[method] + if (key === undefined) { + throw new Error(`host-protocol: no args key for ${JSON.stringify(method)} — verify the parameter name on the host and extend RPC_ARGS_KEY`) } - const slash = method.split('.').join('/') - return [ - { protocol: 'dot', path: `/api/${method}`, method, payload: args }, - { protocol: 'slash', path: `/api/${slash}`, method: slash, payload: { args: { [slashKey]: args } } }, - ] + const endpoint = method.split('.').join('/') + return { path: `/api/${endpoint}`, method: endpoint, payload: { args: { [key]: args } } } } diff --git a/tests/e2e/host.ts b/tests/e2e/host.ts index ad9fcc7..1b07849 100644 --- a/tests/e2e/host.ts +++ b/tests/e2e/host.ts @@ -1,21 +1,18 @@ /** * Host glue for the e2e lanes: page/asset URL construction, the authenticated - * request context, and the dual-protocol host RPC. The cross-version shapes - * (bare origin vs one-time-token URL, dot vs slash `/api` endpoints) live in - * ./host-protocol.ts; this module only wires them to Playwright. + * request context, and the host RPC. The transport contracts (one-time-token + * launch URL, slash `/api` endpoints) live in ./host-protocol.ts; this + * module only wires them to Playwright. * - * Every lane consumes the same contract regardless of host version: + * Every lane consumes the same contract: * - `PAGE_URL` / `pageUrl({...})` for page.goto (the token URL performs the - * browser's token→cookie exchange on 0.1.2-alpha.1+; a bare origin is a - * no-op on older hosts), + * browser's token→cookie exchange), * - `sidebarApi(path)` for the plugin's own public routes, - * - `createHostApi()` for an APIRequestContext that carries the auth cookie - * when the host printed a token, - * - `hostRpc(api, 'workspace.create', {...})` for the host's unary RPC — - * dot endpoint first, slash endpoint on 404 fallback (see host-protocol). + * - `createHostApi()` for an APIRequestContext carrying the auth cookie, + * - `hostRpc(api, 'workspace.create', {...})` for the host's unary RPC. */ import { request, type APIRequestContext, type Page } from '@playwright/test' -import { parseLaunchUrl, pageUrlWith, rpcAttempts } from './host-protocol' +import { parseLaunchUrl, pageUrlWith, rpcAttempt } from './host-protocol' const envUrl = process.env.DSH_E2E_URL if (!envUrl) { @@ -30,8 +27,8 @@ const LAUNCH = parseLaunchUrl(RAW_URL) /** Origin for URL construction (a token URL would corrupt path joins). */ export const ORIGIN = LAUNCH.origin -/** page.goto target: the launch URL as printed (token included on - * 0.1.2-alpha.1+ — navigating it exchanges the token for the cookie). */ +/** page.goto target: the launch URL as printed (token included — + * navigating it exchanges the token for the cookie). */ export const PAGE_URL = LAUNCH.pageUrl /** page.goto target with extra query stamps merged (token preserved). */ @@ -64,14 +61,14 @@ export async function exchangeLaunchCookie( let cookieHeader: string | undefined /** page.goto through the host auth. A plain navigation goes to PAGE_URL (the - * token URL performs the browser's exchange on 0.1.2-alpha.1+), but a - * STAMPED navigation cannot: the exchange answers `303 → /` and silently - * DROPS every sibling query param (the mount lane caught the desktop-shell - * stamps vanishing this way), so pre-seed the browser context with the - * exchanged cookie and navigate the stamped URL directly. Playwright gives - * every test a fresh context, so the seeding is per-call and idempotent. */ + * token URL performs the browser's exchange), but a STAMPED navigation + * cannot: the exchange answers `303 → /` and silently DROPS every sibling + * query param (the mount lane caught the desktop-shell stamps vanishing + * this way), so pre-seed the browser context with the exchanged cookie and + * navigate the stamped URL directly. Playwright gives every test a fresh + * context, so the seeding is per-call and idempotent. */ export async function gotoPage(page: Page, extra: Record = {}): Promise { - if (LAUNCH.token !== undefined && Object.keys(extra).length > 0) { + if (Object.keys(extra).length > 0) { if (cookieHeader === undefined) cookieHeader = await exchangeLaunchCookie(RAW_URL) const eq = cookieHeader.indexOf('=') await page.context().addCookies([{ @@ -85,15 +82,14 @@ export async function gotoPage(page: Page, extra: Record = {}): await page.goto(pageUrl(extra), { waitUntil: 'domcontentloaded' }) } -/** Authenticated request context for host RPC seeding (cookie attached when - * the launch URL carried a one-time token; plain on older hosts). */ +/** Authenticated request context for host RPC seeding (cookie attached). */ export async function createHostApi(): Promise { - if (cookieHeader === undefined && LAUNCH.token !== undefined) { + if (cookieHeader === undefined) { cookieHeader = await exchangeLaunchCookie(RAW_URL) } return request.newContext({ baseURL: ORIGIN, - extraHTTPHeaders: cookieHeader === undefined ? {} : { cookie: cookieHeader }, + extraHTTPHeaders: { cookie: cookieHeader }, }) } @@ -103,52 +99,36 @@ export interface HostRpcOk { value: T } -/** Resolved dialect for hostRpc — cached after the first call so only the - * first RPC ever pays the 404 probe (dot-first: published hosts answer it - * directly; 0.1.2-alpha.1+ 404s the dot path and answers the slash one). */ -let protocol: 'dot' | 'slash' | undefined - -/** Test hook: forget the resolved dialect between unit-test cases. */ -export function resetHostRpcForTests(): void { - protocol = undefined -} - let rpcCounter = 0 -/** Call a host unary RPC (`'workspace.create'`, `'session.create'`, …) with - * the args object both dialects accept. Throws with the response body when - * the HTTP call or the envelope result fails — the lanes' seeding treats any - * failure as fatal, so the error text carries everything a report needs. */ +/** Call a host unary RPC (`'workspace.create'`, `'session.create'`, …). + * Throws with the response body when the HTTP call or the envelope result + * fails — the lanes' seeding treats any failure as fatal, so the error text + * carries everything a report needs. */ export async function hostRpc( api: APIRequestContext, method: string, args: Record = {}, ): Promise> { - const attempts = rpcAttempts(method, args) - const ordered = protocol === 'slash' ? [attempts[1]!, attempts[0]!] : attempts + const attempt = rpcAttempt(method, args) rpcCounter += 1 const rpcId = `e2e-${method}-${rpcCounter}` - for (const attempt of ordered) { - const res = await api.post(attempt.path, { - data: { type: 'client-request', rpcId, method: attempt.method, payload: attempt.payload }, - }) - const bodyText = await res.text() - if (res.status() === 404 && protocol === undefined) continue // endpoint not claimed on this host → other dialect - if (!res.ok()) { - throw new Error(`hostRpc ${method} [${attempt.protocol}] HTTP ${res.status()}: ${bodyText.slice(0, 400)}`) - } - let envelope: { type?: string; result?: { ok: true; value: T } | { ok: false; error: unknown } } - try { - envelope = JSON.parse(bodyText) as typeof envelope - } catch { - throw new Error(`hostRpc ${method} [${attempt.protocol}]: non-JSON response: ${bodyText.slice(0, 400)}`) - } - const result = envelope.result - if (result === undefined || result.ok !== true) { - throw new Error(`hostRpc ${method} [${attempt.protocol}] envelope error: ${bodyText.slice(0, 400)}`) - } - protocol = attempt.protocol - return result + const res = await api.post(attempt.path, { + data: { type: 'client-request', rpcId, method: attempt.method, payload: attempt.payload }, + }) + const bodyText = await res.text() + if (!res.ok()) { + throw new Error(`hostRpc ${method} [${attempt.path}] HTTP ${res.status()}: ${bodyText.slice(0, 400)}`) } - throw new Error(`hostRpc ${method}: no dialect answered (dot and slash both 404) — is this a DSH web host?`) + let envelope: { type?: string; result?: { ok: true; value: T } | { ok: false; error: unknown } } + try { + envelope = JSON.parse(bodyText) as typeof envelope + } catch { + throw new Error(`hostRpc ${method} [${attempt.path}]: non-JSON response: ${bodyText.slice(0, 400)}`) + } + const result = envelope.result + if (result === undefined || result.ok !== true) { + throw new Error(`hostRpc ${method} [${attempt.path}] envelope error: ${bodyText.slice(0, 400)}`) + } + return result } diff --git a/tests/manifest-consistency.spec.ts b/tests/manifest-consistency.spec.ts index 66168d1..00539e9 100644 --- a/tests/manifest-consistency.spec.ts +++ b/tests/manifest-consistency.spec.ts @@ -51,7 +51,6 @@ const CLIENT_REQUIRE_ALLOWED = new Set([ 'cordis', '@deepseek-ai/dsh-client-ui-slots', '@deepseek-ai/dsh-client-ui-primitives', - '@deepseek-ai/dsh-client-runtime/client', ]) /** The registered `__ModuleLoader__.load({ id })` value of a built client bundle. */ diff --git a/tests/market-manifest.spec.ts b/tests/market-manifest.spec.ts index 6ae4a7e..945e459 100644 --- a/tests/market-manifest.spec.ts +++ b/tests/market-manifest.spec.ts @@ -14,7 +14,9 @@ * - 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). + * - an exact SemVer version (no ranges, no tags; a prerelease suffix is + * allowed — prerelease releases ride the npm `alpha` dist-tag and are not + * the market's verified `latest` install target until a stable cut). * * The packed-manifest checks run `pnpm pack` into a temp dir (the `prepare` * script rebuilds the gitignored lib/, like the release flow does). @@ -43,8 +45,9 @@ const pkg = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf8')) as { /** 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 +/** Exact SemVer: exactly three numeric segments, optional prerelease suffix + * (no range / tag — prerelease cuts publish under the npm `alpha` dist-tag). */ +const EXACT_SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u /** Read `package.json` from a fresh `pnpm pack` tarball (the publish surface). */ function packedManifest(): Record { @@ -105,7 +108,7 @@ describe('DSH community-market manifest compatibility', () => { 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) + it('declares an exact SemVer version (no range or tag; prerelease allowed for the alpha track)', () => { + expect(pkg.version).toMatch(EXACT_SEMVER) }) }) diff --git a/tsdown.config.ts b/tsdown.config.ts index 99d8261..2183fd9 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -55,7 +55,7 @@ const NODE_BUILTINS = new Set([ ...builtinModules.map(id => `node:${id}`), ]) -/** Module specifiers the web shell shares into the frozen module table (the official PLATFORM_MODULES list, plus the runtime/client exemption — removed in DSH 0.1.2-alpha.1 along with the runtime package, but no chunk requires it, so the entry stays inert there and live on 0.1.1-rc.x hosts). */ +/** Module specifiers the web shell shares into the frozen module table (the official PLATFORM_MODULES list; `dsh-client-runtime` was removed upstream in DSH 0.1.2-alpha and no chunk requires it). */ const CLIENT_EXTERNALS = [ 'react', 'react/jsx-runtime', @@ -64,7 +64,6 @@ const CLIENT_EXTERNALS = [ 'cordis', '@deepseek-ai/dsh-client-ui-slots', '@deepseek-ai/dsh-client-ui-primitives', - '@deepseek-ai/dsh-client-runtime/client', ] /**