From 751fb954a29fb4cecbb94a8f1ac2edc9af55e07d Mon Sep 17 00:00:00 2001 From: HuanLinOTO Date: Sun, 23 Aug 2026 22:02:47 +0800 Subject: [PATCH] fix: address Copilot review (9 comments) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1 GitView: hide/disable 'open in editor' for external linked worktrees (path outside session workspace is rejected by the fence); add isWithinWorkspace client helper for the gate. #2 DiffTab: untracked diff read resolves against diff.repoRoot (child-repo paths are repo-root-relative, not cwd-relative); fs.read route threads selectedRepoOf into resolveGitPath so the host resolves the right base. #3 GitView: chooseRepo handler mirrors chooseWorktree — clears status/ history/log and bumps generation before async refresh, preventing stale rows from acting on the newly selected repository. #4 git.ts: repoRoots/repoRoot compare roots via pathIdentity() (platform- aware) instead of raw includes(), fixing win32 forward/backslash mismatch that silently fell back to roots[0]. #5 markdown-images: mask fenced code blocks and inline code spans before rewriting; only rewrite reference definitions whose label is actually referenced by image syntax (not plain links). +3 tests. #6 package.json: downgrade unrun ^0.3.1 → ^0.2.39 (0.3.x requires Node ^22.13, incompatible with declared engines >=20). #7 market-manifest.spec: shell:true on win32 so execFileSync resolves the pnpm .cmd shim. #8 smoke.spec: build html-route URL via encodeHtmlUrl() (production encoder) so the Windows drive separator is correct. #9 index.ts git.worktrees route: thread selectedRepoOf so a workspace container's selected child repo's linked worktrees appear. --- docs/plans/2026-08-23-bug-pr-merge-train.md | 408 ++++++++++++++++++++ package.json | 2 +- pnpm-lock.yaml | 247 +++++++++++- src/client/DiffTab.tsx | 5 +- src/client/GitView.tsx | 37 +- src/client/markdown-images.ts | 41 +- src/client/paths.ts | 16 + src/git.ts | 10 +- src/index.ts | 16 +- tests/markdown-images.spec.ts | 27 ++ tests/market-manifest.spec.ts | 5 +- tests/smoke.spec.ts | 7 +- 12 files changed, 793 insertions(+), 28 deletions(-) create mode 100644 docs/plans/2026-08-23-bug-pr-merge-train.md diff --git a/docs/plans/2026-08-23-bug-pr-merge-train.md b/docs/plans/2026-08-23-bug-pr-merge-train.md new file mode 100644 index 0000000..80eee28 --- /dev/null +++ b/docs/plans/2026-08-23-bug-pr-merge-train.md @@ -0,0 +1,408 @@ +# Bug PR 一次性合并方案(merge train + 并行 subagent) + +> 仓库:`omdsh-dev/DSH-better-sidebar`(本地 clone:`D:\Projects\deepseek-harness\DSH-better-sidebar`) +> 基准:main `d6d8db0` (= 0.15.2 + #339 locale ja) +> 目标:把 12 个 bug PR 一次性合入 main,**保留每个原作者的贡献归属** +> 输入:`bug-pr-review.md` 中 12 个 NOT_FIXED PR +> 日期:2026-08-23 + +--- + +## 0. 设计原则 + +| 原则 | 说明 | +|---|---| +| 保留作者归属 | 用 `git merge --no-ff`(**不 squash**),每个 PR 的原始 commits 全部进 main 历史;GitHub 通过 commit author email 匹配到 12 位作者 | +| 跨平台 commit trailer | #285 作者邮箱 `fix@local.invalid` 不匹配 GitHub → 在 merge commit message 加 `Co-authored-by: xxxg0001 ` | +| 零源码 patch | train 分支所有改动都来自 PR 本身或冲突解决;**不修改任何 PR 的 commits** | +| 并行加速 | 4 个 subagent 各自 worktree 独立合并簇内 PR;跨簇冲突由主 agent 串行解决 | +| 每步验证 | 每个 subagent 完成簇内合并后跑 `pnpm typecheck`;主 agent 跨簇合并后跑 `pnpm typecheck && pnpm test` | + +--- + +## 1. PR 清单与作者 + +| PR | 标题 | 作者 | author email | +|---|---|---|---| +| #345 | 限制文件 API 访问会话工作区 | wang-kaopu | 1970138194@qq.com | +| #342 | 搜索跳过 node_modules 等噪声目录 | sunrain520 | sunrain520@users.noreply.github.com | +| #341 | mermaid 抑制全局错误渲染 | duyiliu | 278950578@qq.com | +| #338 | 移除公开版 cordis peer,迁移类型基底 | Menghuan1918 | menghuan2003@outlook.com | +| #336 | tsdown 配置加载缺 unrun devDependency | MilchstraBe-Endless10 | milchstrasse.endless10@users.noreply.github.com | +| #330 | panel host 降到 DSH dynamic-plugin 层下 | ZgblKylin | 13114351+ZgblKylin@users.noreply.github.com | +| #326 | 支持选择子 git 仓库 | loeanxi | 2012065136@qq.com | +| #310 | layout push 下保 composer 在视口内 | Zzy-min | zzy19812007@gmail.com | +| #292 | markdown 预览本地/相对路径图片 | huzilin | huzilinha@gmail.com | +| #285 | linked worktrees 移植到 RC8 | xxxg0001 | fix@local.invalid ⚠ | +| #278 | 面板宿主裁剪视口、收起面板不再撑出双向滚动 | ch3n4y | ch3n4y@gmail.com | +| #254 | mobile 说明不可用侧边栏并防溢出 | SparkElf | laoyh@chinatelecom.cn | + +⚠ #285 author email 不匹配 GitHub 账号,需在 merge commit 加 `Co-authored-by` trailer 补 credit。 + +--- + +## 2. 文件冲突矩阵(跨簇) + +> 仅列出被 ≥2 个 PR 修改、且**跨簇**的文件。簇内冲突由 subagent 解决。 + +| 文件 | 涉及 PR(按簇) | +|---|---| +| `README.md` / `README_EN.md` | A: #338 #345;B: #292;C: #254 | +| `AGENTS.md` | A: #338;B: #342;C: #330 | +| `docs/external-plugin-guide.md` | A: #338 #345(簇内解决) | +| `package.json` | A: #338 #336;C: #254;D: #326 | +| `dsh.plugin.json` | A: #338;C: #254 | +| `src/index.ts` | A: #338 #345;D: #285 #326 | +| `src/client/sidebar.module.css` | C: #330 #278 #254(簇内解决);D: #285 | +| `src/client/Sidebar.tsx` | B: #310;C: #254 | +| `src/client/service.ts` | A: #338;C: #254 | +| `src/client/DiffTab.tsx` / `GitView.tsx` / `api.ts` / `state.ts` / `src/git.ts` | D: #285 #326(簇内解决) | + +--- + +## 3. 分簇(簇内串行,跨簇并行) + +### Cluster A — 基础设施(package.json / cordis / path security) + +**分支**:`train/base` +**worktree**:`D:\Projects\deepseek-harness\dsb-train-base` +**合并顺序**: +1. `#338` (cordis peer 迁移) — 改 package.json/dsh.plugin.json/src/index.ts/service.ts/AGENTS.md/README/docs +2. `#336` (unrun devDep) — 只改 package.json +3. `#345` (path security) — 改 README/docs/src/index.ts/fs-operations/html-route/path-security 新增 + +**簇内冲突点**: +- #336 vs #338:package.json(#338 移除 cordis peer,#336 加 unrun devDep,行不冲突但邻近) +- #345 vs #338:README.md / README_EN.md / docs/external-plugin-guide.md / src/index.ts(#338 迁移基底,#345 加 path-security 引用,可能 src/index.ts 注册新路由撞) + +### Cluster B — 独立修复 + +**分支**:`train/misc` +**worktree**:`D:\Projects\deepseek-harness\dsb-train-misc` +**合并顺序**: +1. `#341` (mermaid suppressErrorRendering) — 改 src/client/mermaid.tsx + 测试 +2. `#310` (layout-push composer clip) — 改 Sidebar.tsx + layout-push.ts + breakpoints.ts + layout.css + 测试 +3. `#342` (fs-search skip dirs) — 改 fs-search.ts + AGENTS.md + 测试 +4. `#292` (markdown images) — 改 TextEditor.tsx + markdown-images.ts + README + 测试 + +**簇内冲突点**:基本无(彼此文件不重叠)。#342 的 AGENTS.md 和 #292 的 README 是跨簇冲突,本 worktree 内基于 main,无冲突。 + +### Cluster C — CSS 簇(sidebar.module.css) + +**分支**:`train/css` +**worktree**:`D:\Projects\deepseek-harness\dsb-train-css` +**合并顺序**: +1. `#330` (z-index drop) — 改 sidebar.module.css:35 + AGENTS.md + docs/plans + e2e 测试 +2. `#278` (overflow hidden + panelHidden transform) — 改 sidebar.module.css + e2e 测试 +3. `#254` (mobile no-session feedback + overflow) — 改 Sidebar.tsx + sidebar.module.css + README + dsh.plugin.json + package.json + service.ts + +**簇内冲突点**: +- #278 vs #330:sidebar.module.css(#330 改 z-index 行,#278 改 overflow/transform 行,可能行邻近但不直接冲突) +- #254 vs #330/#278:sidebar.module.css(mobile media query 区域,可能撞 e2e 测试) + +### Cluster D — git 簇 + +**分支**:`train/git` +**worktree**:`D:\Projects\deepseek-harness\dsb-train-git` +**合并顺序**: +1. `#285` (worktree RC8 forward-port) — 改 DiffTab/GitView/api/state/git.ts/index.ts/builtins/tabs.tsx/locales.ts/sidebar.module.css + 测试 +2. `#326` (multi-repo selector) — 改 DiffTab/GitView/api/state/git.ts/index.ts/package.json + 截图 + 测试 + +**簇内冲突点**(严重): +- #326 vs #285:5 个核心文件全撞(DiffTab/GitView/api/state/git.ts)+ src/index.ts +- #326 应建立在 #285 的 worktree 支持基础上(多仓选择是 worktree list 的扩展) +- 合并 #326 时需要手工缝合:保留 #285 的 worktree list 逻辑 + #326 的子仓选择 UI + +--- + +## 4. 执行流程 + +### Phase 0 — 准备(主 agent,串行) + +```powershell +$repo = "D:\Projects\deepseek-harness\DSH-better-sidebar" + +# 0.1 拉所有 PR 分支到本地 ref(避免 subagent 重复 fetch) +$prs = 345,342,341,338,336,330,326,310,292,285,278,254 +foreach ($n in $prs) { + git -C $repo fetch origin "pull/$n/head:pr-$n" +} + +# 0.2 创建 4 个 worktree(每个独立目录,独立分支基于 origin/main) +git -C $repo worktree add "D:\Projects\deepseek-harness\dsb-train-base" -b train/base origin/main +git -C $repo worktree add "D:\Projects\deepseek-harness\dsb-train-misc" -b train/misc origin/main +git -C $repo worktree add "D:\Projects\deepseek-harness\dsb-train-css" -b train/css origin/main +git -C $repo worktree add "D:\Projects\deepseek-harness\dsb-train-git" -b train/git origin/main + +# 0.3 每个 worktree 都 pnpm install(独立 node_modules,避免锁文件竞争) +foreach ($wt in @("dsb-train-base","dsb-train-misc","dsb-train-css","dsb-train-git")) { + Push-Location "D:\Projects\deepseek-harness\$wt" + pnpm install --prefer-offline + Pop-Location +} +``` + +### Phase 1 — 簇内并行合并(4 个 subagent 同时跑) + +每个 subagent 接收: +- 自己的 worktree 路径 +- 簇内 PR 编号和合并顺序 +- PR 的预期改动文件清单(见 §3) +- 验证命令:`pnpm typecheck`(必跑)+ `pnpm test`(如簇内有相互依赖的测试) + +#### Subagent A(base) + +``` +worktree: D:\Projects\deepseek-harness\dsb-train-base +合并顺序: #338 → #336 → #345 +命令: + git merge --no-ff pr-338 -m "merge #338: 移除公开版 cordis peer,迁移类型基底到 @deepseek-ai/cordis" + # 解决冲突(README/docs/package.json/src/index.ts) + pnpm typecheck + + git merge --no-ff pr-336 -m "merge #336: add unrun devDependency for tsdown config loading" + # 解决 package.json 冲突 + pnpm typecheck + + git merge --no-ff pr-345 -m "merge #345: 限制文件 API 访问会话工作区" + # 解决 README/docs/src/index.ts 冲突 + pnpm typecheck + +完成后输出: + - train/base 的 HEAD SHA + - 解决的冲突文件清单 + - 跨簇预期冲突点(README.md / package.json / src/index.ts / docs/external-plugin-guide.md / AGENTS.md / dsh.plugin.json / src/client/service.ts) +``` + +#### Subagent B(misc) + +``` +worktree: D:\Projects\deepseek-harness\dsb-train-misc +合并顺序: #341 → #310 → #342 → #292 +命令: + git merge --no-ff pr-341 -m "merge #341: fix(mermaid): suppress global error rendering" + pnpm typecheck + + git merge --no-ff pr-310 -m "merge #310: keep conversation composer in viewport under layout push" + pnpm typecheck + + git merge --no-ff pr-342 -m "merge #342: fs-search skip node_modules and other noise dirs" + # AGENTS.md 可能无冲突(基于 main,A 簇的 AGENTS.md 改动在另一个 worktree) + pnpm typecheck + + git merge --no-ff pr-292 -m "merge #292: markdown preview local/relative images" + pnpm typecheck + +完成后输出: + - train/misc 的 HEAD SHA + - 跨簇预期冲突点(AGENTS.md 来自 #342;README.md 来自 #292;Sidebar.tsx 来自 #310) +``` + +#### Subagent C(css) + +``` +worktree: D:\Projects\deepseek-harness\dsb-train-css +合并顺序: #330 → #278 → #254 +命令: + git merge --no-ff pr-330 -m "merge #330: drop panel host below DSH cordis dynamic-plugin layer" + pnpm typecheck + + git merge --no-ff pr-278 -m "merge #278: panel host clip viewport, hidden panel no longer causes双向 overflow" + # sidebar.module.css 可能冲突 + pnpm typecheck + + git merge --no-ff pr-254 -m "merge #254: mobile explain unavailable sidebar and prevent overflow" + # sidebar.module.css 可能冲突;其他文件基于 main 无冲突 + pnpm typecheck + +完成后输出: + - train/css 的 HEAD SHA + - 跨簇预期冲突点(sidebar.module.css 来自 #285;README/package.json/dsh.plugin.json/Sidebar.tsx/service.ts 来自 #254) +``` + +#### Subagent D(git) + +``` +worktree: D:\Projects\deepseek-harness\dsb-train-git +合并顺序: #285 → #326 +命令: + git merge --no-ff pr-285 -m "merge #285: forward-port linked worktrees to RC8 + +Co-authored-by: xxxg0001 " + # 注意:#285 author email = fix@local.invalid,必须加 Co-authored-by trailer + pnpm typecheck + + git merge --no-ff pr-326 -m "merge #326: support selecting child git repositories" + # 严重冲突:DiffTab/GitView/api/state/git.ts/src/index.ts + # 缝合策略:保留 #285 的 worktree list 逻辑 + #326 的子仓选择 UI + pnpm typecheck + +完成后输出: + - train/git 的 HEAD SHA + - 跨簇预期冲突点(sidebar.module.css 来自 #285;src/index.ts 与 base 簇撞;package.json 与 base 簇撞) + - #326 缝合的关键决策(哪些函数来自 #285,哪些来自 #326) +``` + +### Phase 2 — 跨簇串行合并(主 agent) + +```powershell +$repo = "D:\Projects\deepseek-harness\DSH-better-sidebar" + +# 切回主 worktree +# 2.1 创建 mega-train 分支 +git -C $repo checkout -b merge/bug-pr-train origin/main + +# 2.2 merge train/base(应该 ff,因为 base 直接基于 main) +git -C $repo merge --no-ff train/base -m "integrate train/base: cordis peer migration + unrun devDep + path security" + +# 2.3 merge train/css +git -C $repo merge --no-ff train/css -m "integrate train/css: panel z-index + overflow clip + mobile sidebar" +# 解决冲突: +# - sidebar.module.css(train/base 没改,应该无冲突) +# - README.md(train/base 改了,train/css 的 #254 也改了 → 冲突) +# - package.json(train/base 加了 unrun devDep,train/css 的 #254 可能加 mobile deps → 冲突) +# - dsh.plugin.json(train/base 改了,train/css 的 #254 也改了 → 冲突) +# - src/client/service.ts(train/base 迁移了 cordis,train/css 的 #254 可能改了 mobile 检测 → 冲突) +pnpm install # 合并 package.json 后必须重装 +pnpm typecheck + +# 2.4 merge train/git +git -C $repo merge --no-ff train/git -m "integrate train/git: worktree RC8 forward-port + multi-repo selector" +# 解决冲突: +# - src/index.ts(train/base 改了 cordis 迁移,train/git 改了 worktree 注册 → 冲突) +# - src/client/sidebar.module.css(train/css 改了 z-index/overflow/mobile,train/git 的 #285 可能改了 git 面板样式 → 冲突) +# - package.json(train/git 的 #326 可能加了依赖 → 冲突) +pnpm install +pnpm typecheck + +# 2.5 merge train/misc +git -C $repo merge --no-ff train/misc -m "integrate train/misc: mermaid + layout-push + fs-search + markdown-images" +# 解决冲突: +# - AGENTS.md(train/base 改了,train/misc 的 #342 也改了 → 冲突) +# - README.md(train/base + train/css 都改了,train/misc 的 #292 又改 → 三方合并冲突) +# - src/client/Sidebar.tsx(train/misc 的 #310 改了 layout,train/css 的 #254 改了 mobile → 冲突) +pnpm typecheck + +# 2.6 全量验证 +pnpm typecheck +pnpm test +``` + +### Phase 3 — 推送与开 PR(主 agent) + +```powershell +# 3.1 推 mega-train 到 fork(不是 origin,因为 origin 是 omdsh-dev 上游) +git -C $repo push fork merge/bug-pr-train + +# 3.2 开 mega-PR(base = main,head = HuanLinOTO:merge/bug-pr-train) +gh pr create ` + --repo omdsh-dev/DSH-better-sidebar ` + --base main ` + --head HuanLinOTO:merge/bug-pr-train ` + --title "merge: bug PR train (12 PRs, --no-ff to preserve authors)" ` + --body "Closes #345, #342, #341, #338, #336, #330, #326, #310, #292, #285, #278, #254 + +一次性合并 12 个 bug PR,使用 --no-ff 保留每个 PR 的原始 commits 和作者归属。 + +合并顺序与分簇: +- Cluster A (base): #338 → #336 → #345 +- Cluster B (misc): #341 → #310 → #342 → #292 +- Cluster C (css): #330 → #278 → #254 +- Cluster D (git): #285 → #326 + +并行 subagent 在 4 个独立 worktree 中合并簇内 PR,主 agent 串行合并 4 个 train 子分支。 + +#285 author email 不匹配 GitHub,merge commit 加了 Co-authored-by: xxxg0001 trailer。 + +详细方案:docs/plans/2026-08-23-bug-pr-merge-train.md" + +# 3.3 PR 创建后,13 个 PR(12 个原始 + 1 个 mega)会显示在 PR 列表 +# 等 CI 通过后用 --merge 合并(不要 squash): +gh pr merge --repo omdsh-dev/DSH-better-sidebar --merge +# 合并后 12 个原始 PR 因 Closes 关键字自动 close +``` + +--- + +## 5. 验证清单 + +### Phase 1 完成后(每个 subagent 自查) + +- [ ] 簇内所有 PR 已 `--no-ff` 合并 +- [ ] `pnpm typecheck` 通过 +- [ ] git log 显示每个 PR 的原始 commits 保留 +- [ ] 输出 train 子分支的 HEAD SHA 给主 agent +- [ ] 输出解决的冲突文件清单 +- [ ] 输出跨簇预期冲突点 + +### Phase 2 完成后(主 agent 自查) + +- [ ] 4 个 train 子分支全部并入 `merge/bug-pr-train` +- [ ] `pnpm install` 成功 +- [ ] `pnpm typecheck` 通过 +- [ ] `pnpm test` 通过(win32 平台已知失败项可记录,不算回归) +- [ ] `git log --oneline d6d8db0..HEAD` 显示 12 个 merge commit + 各 PR 的原始 commits + +### Phase 3 完成后 + +- [ ] mega-PR 在 omdsh-dev/DSH-better-sidebar 创建成功 +- [ ] PR body 包含 12 个 `Closes #N` +- [ ] CI 通过(或失败项是已知非回归) +- [ ] 用 `--merge` 合并(**不是 squash**) +- [ ] 合并后 12 个原始 PR 全部 close +- [ ] main 上 `git log --author=<每位作者>` 都能查到 commits + +--- + +## 6. 风险点与应对 + +| 风险 | 概率 | 应对 | +|---|---|---| +| Phase 1 某个 subagent 簇内冲突解决失败 | 中 | 主 agent 介入协助;最坏退回该簇重做 | +| Phase 2 跨簇冲突比预期复杂(特别是 README 三方合并) | 高 | 手工缝合;以 #338 的 cordis 迁移为基底,叠加其他 PR 的章节 | +| Phase 2 #326 缝合 #285 后逻辑不一致(worktree list + 子仓选择) | 中 | 优先保 #285 的 RC8 forward-port 完整性;#326 的 UI 适配其 API | +| `pnpm test` 在 win32 平台已知失败 | 中 | 记录为已知非回归,不阻塞合并(参考 AGENTS.md "上游测试可能有 win32 平台假设失败") | +| GitHub SSL 抖动(fetch 失败) | 低 | 重试 / 用 `gh pr diff` 替代 fetch / 走 fork remote | +| `--merge` 合并 mega-PR 后 contributors 列表未立即更新 | 低 | GitHub 异步计算 contributors,等 24h;只要 commit author email 匹配即可 | +| #285 的 `fix@local.invalid` 邮箱即使加 Co-authored-by 也不被 GitHub 识别 | 中 | 已用 noreply 邮箱作为 Co-authored-by,GitHub 会识别;最坏在 PR body 注明 | + +--- + +## 7. 时间预估 + +| 阶段 | 串行耗时 | 并行耗时 | +|---|---|---| +| Phase 0 准备 | 5 min | 5 min | +| Phase 1 簇内合并 | 60 min | **20 min**(4 个 subagent 并行,取最慢的 git 簇) | +| Phase 2 跨簇合并 | 30 min | 30 min(必须串行) | +| Phase 3 推送 + PR | 5 min | 5 min | +| **总计** | 100 min | **60 min** | + +并行加速比 ≈ 1.7×(受 Phase 2 串行合并限制)。 + +--- + +## 8. 回滚 + +```powershell +# 任意阶段失败,回滚到 main +git -C $repo checkout main +git -C $repo branch -D merge/bug-pr-train train/base train/misc train/css train/git + +# 清理 worktree +git -C $repo worktree remove "D:\Projects\deepseek-harness\dsb-train-base" --force +git -C $repo worktree remove "D:\Projects\deepseek-harness\dsb-train-misc" --force +git -C $repo worktree remove "D:\Projects\deepseek-harness\dsb-train-css" --force +git -C $repo worktree remove "D:\Projects\deepseek-harness\dsb-train-git" --force + +# 删除本地 PR ref +$prs = 345,342,341,338,336,330,326,310,292,285,278,254 +foreach ($n in $prs) { git -C $repo branch -D "pr-$n" } +``` + +如果 mega-PR 已合并到 main 但发现严重回归: + +```powershell +# Revert mega-PR 的 merge commit(保留历史,不删除 PR 记录) +gh pr merge --repo omdsh-dev/DSH-better-sidebar --revert +``` diff --git a/package.json b/package.json index d10ed3a..e5b7b4a 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,7 @@ "react-dom": "18.2.0", "tsdown": "^0.22.2", "typescript": "^5.6.0", - "unrun": "^0.3.1", + "unrun": "^0.2.39", "vitest": "^4.1.8" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3738fa2..5ed2a9e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -182,13 +182,13 @@ importers: version: 18.2.0(react@18.2.0) tsdown: specifier: ^0.22.2 - version: 0.22.14(typescript@5.6.2)(unrun@0.3.1) + version: 0.22.14(typescript@5.6.2)(unrun@0.2.39) typescript: specifier: ^5.6.0 version: 5.6.2 unrun: - specifier: ^0.3.1 - version: 0.3.1 + specifier: ^0.2.39 + version: 0.2.39 vitest: specifier: ^4.1.8 version: 4.1.11(@types/node@24.13.3)(jsdom@29.1.1)(vite@8.2.2(@types/node@24.13.3)) @@ -1046,6 +1046,15 @@ packages: '@deepseek-ai/schemastery@3.18.1': resolution: {integrity: sha512-Qn0FCSwCQnpnj6SB31I6i2sIKgKWnkbJM8O0EU91Gv2UsYVvtZTl6IA0sCwk2e2MZf5S8w5hpq9QkeVvK9qwxg==} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@exodus/bytes@1.15.1': resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -1149,6 +1158,16 @@ packages: '@mermaid-js/parser@1.2.1': resolution: {integrity: sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw==} + '@napi-rs/wasm-runtime@1.2.3': + resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + '@oxc-project/types@0.146.0': resolution: {integrity: sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==} @@ -1166,36 +1185,73 @@ packages: cpu: [arm] os: [android] + '@rolldown/binding-android-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@rolldown/binding-android-arm64@1.2.5': resolution: {integrity: sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@rolldown/binding-darwin-arm64@1.2.5': resolution: {integrity: sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@rolldown/binding-darwin-x64@1.2.5': resolution: {integrity: sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@rolldown/binding-freebsd-x64@1.2.5': resolution: {integrity: sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@rolldown/binding-linux-arm-gnueabihf@1.2.5': resolution: {integrity: sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-arm64-gnu@1.2.5': resolution: {integrity: sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1203,6 +1259,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + '@rolldown/binding-linux-arm64-musl@1.2.5': resolution: {integrity: sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1210,6 +1273,13 @@ packages: os: [linux] libc: [musl] + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-ppc64-gnu@1.2.5': resolution: {integrity: sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1217,6 +1287,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-s390x-gnu@1.2.5': resolution: {integrity: sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1224,6 +1301,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-x64-gnu@1.2.5': resolution: {integrity: sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1231,6 +1315,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + '@rolldown/binding-linux-x64-musl@1.2.5': resolution: {integrity: sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1238,24 +1329,50 @@ packages: os: [linux] libc: [musl] + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@rolldown/binding-openharmony-arm64@1.2.5': resolution: {integrity: sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@rolldown/binding-win32-arm64-msvc@1.2.5': resolution: {integrity: sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/binding-win32-x64-msvc@1.2.5': resolution: {integrity: sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@rolldown/pluginutils@1.0.0-rc.17': + resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} + '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} @@ -1293,6 +1410,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -2389,6 +2509,11 @@ packages: vue-tsc: optional: true + rolldown@1.0.0-rc.17: + resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rolldown@1.2.5: resolution: {integrity: sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2562,9 +2687,9 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} - unrun@0.3.1: - resolution: {integrity: sha512-onIck/oNnCaytwths1ZVp1LK2Gq2hPoyFhiHebObuUXqR3S0uHuLLaBK8K6mRRgV7Ptip8AnNvaUsgzwWwBZuA==} - engines: {node: ^22.13.0 || >=24.0.0} + unrun@0.2.39: + resolution: {integrity: sha512-h9FxYVpztY/wwq+bauLOh6Y3CWu2IVeRLq5lxzneBiIU9Tn86OGp9xiQrGhnYspAmg5dzdY0Cc8+Y70kuTARCg==} + engines: {node: '>=20.19.0'} hasBin: true peerDependencies: synckit: ^0.11.11 @@ -3720,6 +3845,22 @@ snapshots: '@deepseek-ai/cosmokit': 1.8.2 '@standard-schema/spec': 1.1.0 + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@exodus/bytes@1.15.1': {} '@huanlin/dsh-plugin-better-locale@0.1.0(cb831dcffe1fb561624a57e14d05c62c)': @@ -3838,6 +3979,15 @@ snapshots: dependencies: '@chevrotain/types': 11.1.2 + '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-project/types@0.127.0': {} + '@oxc-project/types@0.146.0': {} '@playwright/test@1.62.1': @@ -3851,48 +4001,99 @@ snapshots: '@rolldown/binding-android-arm-eabi@1.2.5': optional: true + '@rolldown/binding-android-arm64@1.0.0-rc.17': + optional: true + '@rolldown/binding-android-arm64@1.2.5': optional: true + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + optional: true + '@rolldown/binding-darwin-arm64@1.2.5': optional: true + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + optional: true + '@rolldown/binding-darwin-x64@1.2.5': optional: true + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + optional: true + '@rolldown/binding-freebsd-x64@1.2.5': optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.2.5': optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + optional: true + '@rolldown/binding-linux-arm64-gnu@1.2.5': optional: true + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + optional: true + '@rolldown/binding-linux-arm64-musl@1.2.5': optional: true + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + optional: true + '@rolldown/binding-linux-ppc64-gnu@1.2.5': optional: true + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + optional: true + '@rolldown/binding-linux-s390x-gnu@1.2.5': optional: true + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + optional: true + '@rolldown/binding-linux-x64-gnu@1.2.5': optional: true + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + optional: true + '@rolldown/binding-linux-x64-musl@1.2.5': optional: true + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + optional: true + '@rolldown/binding-openharmony-arm64@1.2.5': optional: true + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + optional: true + '@rolldown/binding-win32-arm64-msvc@1.2.5': optional: true + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + optional: true + '@rolldown/binding-win32-x64-msvc@1.2.5': optional: true + '@rolldown/pluginutils@1.0.0-rc.17': {} + '@rolldown/pluginutils@1.0.1': {} '@shikijs/core@4.4.3': @@ -3937,6 +4138,11 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -5174,6 +5380,27 @@ snapshots: transitivePeerDependencies: - oxc-resolver + rolldown@1.0.0-rc.17: + dependencies: + '@oxc-project/types': 0.127.0 + '@rolldown/pluginutils': 1.0.0-rc.17 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-x64': 1.0.0-rc.17 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.17 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 + rolldown@1.2.5: dependencies: '@oxc-project/types': 0.146.0 @@ -5288,7 +5515,7 @@ snapshots: ts-dedent@2.3.0: {} - tsdown@0.22.14(typescript@5.6.2)(unrun@0.3.1): + tsdown@0.22.14(typescript@5.6.2)(unrun@0.2.39): dependencies: ansis: 4.3.1 cac: 7.0.0 @@ -5307,7 +5534,7 @@ snapshots: verkit: 0.3.2 optionalDependencies: typescript: 5.6.2 - unrun: 0.3.1 + unrun: 0.2.39 transitivePeerDependencies: - '@typescript/native-preview' - '@volar/typescript' @@ -5355,9 +5582,9 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - unrun@0.3.1: + unrun@0.2.39: dependencies: - rolldown: 1.2.5 + rolldown: 1.0.0-rc.17 use-sync-external-store@1.2.0(react@18.2.0): dependencies: diff --git a/src/client/DiffTab.tsx b/src/client/DiffTab.tsx index c96caf2..8aaef04 100644 --- a/src/client/DiffTab.tsx +++ b/src/client/DiffTab.tsx @@ -61,7 +61,10 @@ export function DiffTab(props: { sessionId: string; cwd: string | undefined; dif // Empty diff: an untracked file (git diff never lists it) falls back // to a full-file addition; anything else is a genuine no-text-change. if (diff.untracked === true && !diff.staged) { - const text = await api.fsRead(scope, resolveSidebarPath(diff.worktree ?? cwd, diff.path)) + // A child-repo path is relative to diff.repoRoot, not the session + // cwd or the linked-worktree root; resolve against whichever the + // diff ref carries so the untracked fallback reads the right file. + const text = await api.fsRead(scope, resolveSidebarPath(diff.repoRoot ?? diff.worktree ?? cwd, diff.path)) if (!cancelled) { setData(text.kind === 'text' ? { diff: '', untracked: text.content } : { diff: '' }) } diff --git a/src/client/GitView.tsx b/src/client/GitView.tsx index 168d121..6c5ab26 100644 --- a/src/client/GitView.tsx +++ b/src/client/GitView.tsx @@ -16,7 +16,7 @@ import { } from '@deepseek-ai/dsh-client-ui-primitives' import type { GitLogEntry, GitStatusEntry, GitStatusResult, GitWorktree, SessionScope } from './api.ts' import { api } from './api.ts' -import { relativeTo } from './paths.ts' +import { isWithinWorkspace, relativeTo } from './paths.ts' import { resolveSidebarPath } from './produced-files.ts' import { relativeTime, t } from './locales.ts' import type { SidebarTab } from './state.ts' @@ -231,6 +231,23 @@ export function GitView(props: { const generation = refreshGeneration.current += 1 void refreshTarget(target, { loading: true, generation }) } + /** Switching the selected child repository must invalidate every + * target-derived surface (status/history/log) before the asynchronous + * refresh resolves; otherwise stale rows remain actionable while their + * handlers already address the new repository. Mirrors chooseWorktree. */ + const chooseRepo = (target: string): void => { + setRepoRoot(target) + setStatus(null) + setBranchNames([]) + setLogEntries([]) + setLogEnded(false) + setLogLoadingMore(false) + // Re-list worktrees for the selected child (a workspace container's + // own worktree list is empty); keep the current linked-checkout choice + // unless it does not belong to the new repository. + const generation = refreshGeneration.current += 1 + void refreshTarget(selectedRef.current ?? '', { loading: true, generation }) + } useEffect(() => { if (!visible) return const timer = window.setInterval(() => { void refresh(true) }, 2_000) @@ -420,7 +437,7 @@ export function GitView(props: { className={css.gitBranchSelect} value={repoRoot ?? ''} title={repoRoot} - onChange={(event) => { setRepoRoot(event.target.value) }} + onChange={(event) => { chooseRepo(event.target.value) }} disabled={busy} > {status!.repositories!.map(root => )} @@ -551,7 +568,13 @@ export function GitView(props: { open={fileMenu !== null} onClose={() => { setFileMenu(null) }} items={[ - { id: 'open', label: t('openEditor'), icon: }, + // A linked worktree outside the session workspace cannot be + // opened in the editor: the host's workspace fence rejects + // every path under it. Hide the action for that checkout so + // the menu does not offer a no-op that confuses the user. + ...(fileMenu !== null && isWithinWorkspace(scope.cwd ?? '', resolveSidebarPath(repoRoot ?? selectedWorktree ?? scope.cwd, fileMenu.entry.path)) + ? [{ id: 'open', label: t('openEditor'), icon: }] + : []), fileMenu?.staged === true ? { id: 'stage', label: t('unstage'), icon: } : { id: 'stage', label: t('stage'), icon: }, @@ -567,7 +590,13 @@ export function GitView(props: { if (target === null) return setFileMenu(null) if (id === 'open') { - onOpenFile(resolveSidebarPath(repoRoot ?? selectedWorktree ?? scope.cwd, target.entry.path)) + const resolved = resolveSidebarPath(repoRoot ?? selectedWorktree ?? scope.cwd, target.entry.path) + // Defense-in-depth: the menu hides this action when the + // resolved path escapes the session workspace, but a + // racing repo switch could still reach here with a path + // the host would reject. No-op in that case. + if (!isWithinWorkspace(scope.cwd ?? '', resolved)) return + onOpenFile(resolved) return } if (id === 'stage') { diff --git a/src/client/markdown-images.ts b/src/client/markdown-images.ts index 115303c..b32e6ca 100644 --- a/src/client/markdown-images.ts +++ b/src/client/markdown-images.ts @@ -50,6 +50,14 @@ function normalizeLocalPath(path: string): string { * local paths pass through. Remote (http/https/data/mailto) and `#`-anchor * destinations are left untouched for `MarkdownText`. Reference-style images * (`![x][id]` + `[id]: url`) are covered by rewriting their definition lines. + * + * Code spans (`` `...` ``) and fenced code blocks (``` ```...``` ```) are + * masked before rewriting so documentation that demonstrates `![alt](./img.png)` + * is not mutated into a `/sidebar/file` URL. Reference definitions are only + * rewritten when their label is actually referenced by an image (collapsed + * `[![][id]]`, full `![alt][id]`, or shortcut `![]` referencing the next + * definition) — a plain link `[text][id]` must not have its destination + * redirected to the media route. * @param text - The raw markdown source (inline + reference images). * @param scope - The session scope (sessionId + cwd) for the media route. * @param filePath - The absolute path of the opened `.md` file. @@ -79,11 +87,38 @@ export function rewriteLocalImageUrls( const candidate = isAbsolutePath(trimmed) ? trimmed : directory + trimmed return mediaUrl(normalizeLocalPath(candidate)) } - const inline = text.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_match, alt, dest) => { + + // Mask fenced code blocks and inline code spans so image-looking text + // inside documentation examples is never rewritten. The sentinel uses a + // character unlikely to appear in prose; the original spans are restored + // after the image rewrite. + const masks: string[] = [] + const masked = text + .replace(/```[\s\S]*?```/g, (block) => { masks.push(block); return `\u0000${masks.length - 1}\u0000` }) + .replace(/`[^`\n]*`/g, (span) => { masks.push(span); return `\u0000${masks.length - 1}\u0000` }) + + const inline = masked.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_match, alt, dest) => { return `![${alt}](${resolve(dest)})` }) - // Reference-style images: rewrite their `[id]: ` definitions. - return inline.replace(/^(\s*\[[^\]]+\]:\s*)(<[^>]+>|[^\s]+)/gm, (_match, head, dest) => { + + // Collect labels referenced by image syntax (full `![alt][id]` and + // collapsed `![][id]`) so only those reference definitions are rewritten. + // A shortcut reference (`![alt]` with no `[id]`) resolves to the label + // text `alt` itself; include it too. Plain links `[text][id]` never match + // the leading `!` and are left untouched. + const imageLabels = new Set() + const labelRe = /!\[([^\]]*)\](?:\[((?:[^\][]|\[[^\]]*\])*)\])?/g + let labelMatch: RegExpExecArray | null + while ((labelMatch = labelRe.exec(inline)) !== null) { + const alt = labelMatch[1] ?? '' + const ref = labelMatch[2] + imageLabels.add(ref !== undefined && ref !== '' ? ref.toLowerCase() : alt.toLowerCase()) + } + + const refsRewritten = inline.replace(/^(\s*\[([^\]]+)\]:\s*)(<[^>]+>|[^\s]+)/gm, (match, head: string, label: string, dest: string) => { + if (!imageLabels.has(label.toLowerCase())) return match return `${head}${resolve(dest.replace(/^<|>$/g, ''))}` }) + + return refsRewritten.replace(/\u0000(\d+)\u0000/g, (_m, index: string) => masks[Number(index)] ?? '') } diff --git a/src/client/paths.ts b/src/client/paths.ts index d87569b..8fd5490 100644 --- a/src/client/paths.ts +++ b/src/client/paths.ts @@ -45,3 +45,19 @@ export function relativeTo(cwd: string, path: string): string { if (nPath.toLowerCase().startsWith(`${nBase.toLowerCase()}/`)) return nPath.slice(nBase.length + 1) return path } + +/** + * Whether `target` lies under `base` (or equals it), tolerant of separator + * style and — on Windows-style drive paths — of letter case. A client-side + * mirror of the host's `isWithin` (fs-tree.ts) used to decide whether a + * git-derived path can be opened in the editor (a linked worktree outside + * the session workspace cannot: the host's workspace fence would reject it). + */ +export function isWithinWorkspace(base: string, target: string): boolean { + const norm = (value: string): string => value.replace(/[\\/]+/g, '/').replace(/\/$/, '') + const b = norm(base) + const t = norm(target) + const lb = b.toLowerCase() + const lt = t.toLowerCase() + return lt === lb || lt.startsWith(`${lb}/`) +} diff --git a/src/git.ts b/src/git.ts index c52932a..dc0879f 100644 --- a/src/git.ts +++ b/src/git.ts @@ -212,7 +212,7 @@ export async function repoRoots(cwd: string): Promise { .sort((left, right) => left.name.localeCompare(right.name))) { try { const root = await directRepoRoot(join(cwd, entry.name)) - if (!roots.includes(root)) roots.push(root) + if (!roots.some(existing => pathIdentity(existing) === pathIdentity(root))) roots.push(root) } catch { // Ordinary child directory; keep discovering sibling repositories. } @@ -225,7 +225,13 @@ export async function repoRoots(cwd: string): Promise { export async function repoRoot(cwd: string, selected?: string): Promise { const roots = await repoRoots(cwd) if (roots.length === 0) throw new GitCommandError('not a git repository', 'not-repo', 'rev-parse') - if (selected !== undefined && roots.includes(selected)) return selected + // Git for Windows may return forward-slash roots while callers pass + // backslashes (or vice-versa); compare via the platform-aware identity. + if (selected !== undefined) { + const identity = pathIdentity(selected) + const match = roots.find(root => pathIdentity(root) === identity) + if (match !== undefined) return match + } return roots[0]! } diff --git a/src/index.ts b/src/index.ts index 924a33a..f0c490a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -286,8 +286,11 @@ function buildApi( 'fs.read': async (payload) => { const { cwd } = cwdOf(payload) // Relative paths are git-derived (status/diff report repo-root-relative - // names; the untracked diff view reads the file through this route). - const path = await ensureWorkspacePath(cwd, await resolveGitPath(cwd, requireString(payload, 'path'))) + // names; the untracked diff view reads the file through this route). A + // child-repo path is relative to the selected repoRoot, not the session + // cwd; thread it so the path resolves inside the authorized workspace. + const selected = selectedRepoOf(payload) + const path = await ensureWorkspacePath(cwd, await resolveGitPath(cwd, requireString(payload, 'path'), selected)) const { content, truncated, binary, size, head } = await readText(path, resolved.readLimit) if (binary) return { kind: 'binary', size, truncated, head } return { kind: 'text', content, truncated } @@ -308,8 +311,13 @@ function buildApi( return { ok: true } }, 'git.worktrees': async (payload) => { - const { cwd } = cwdOf(payload) - return git.worktrees(cwd) + const { cwd } = await gitCwdOf(payload) + const selected = selectedRepoOf(payload) + // A workspace container (no repo at cwd) has child repos; the worktree + // list belongs to the SELECTED child, not the container. Thread the + // validated repoRoot so linked checkouts of a chosen child appear. + const base = selected !== undefined ? await git.repoRoot(cwd, selected).catch(() => cwd) : cwd + return git.worktrees(base) }, 'git.status': async (payload) => { const { cwd } = await gitCwdOf(payload) diff --git a/tests/markdown-images.spec.ts b/tests/markdown-images.spec.ts index d867ff5..51fccf9 100644 --- a/tests/markdown-images.spec.ts +++ b/tests/markdown-images.spec.ts @@ -70,4 +70,31 @@ describe('rewriteLocalImageUrls', () => { expect(url).toContain('/sidebar/file?') expect(decodeURIComponent(url)).toContain('C:\\repo\\img.png') }) + + it('does not rewrite image-looking text inside fenced code blocks', () => { + const md = '```\n![alt](./img.png)\n```\n![real](./real.png)' + const out = rewriteLocalImageUrls(md, scope, '/repo/readme.md', ORIGIN) + // The fenced example must be preserved verbatim; only the real image is rewritten. + expect(out).toContain('```\n![alt](./img.png)\n```') + expect(out).toContain('/sidebar/file?') + }) + + it('does not rewrite image-looking text inside inline code spans', () => { + const md = 'Use `![alt](./img.png)` syntax.\n![real](./real.png)' + const out = rewriteLocalImageUrls(md, scope, '/repo/readme.md', ORIGIN) + expect(out).toContain('`![alt](./img.png)`') + expect(out).toContain('/sidebar/file?') + }) + + it('does not rewrite a reference definition used only by a plain link', () => { + // The `[manual][docs]` link references `[docs]: ./docs.md`, but that + // definition must NOT be redirected to /sidebar/file because it is not + // referenced by any image. Only image-referenced definitions are rewritten. + const md = '[manual][docs]\n\n![pic][pic-def]\n\n[docs]: ./docs.md\n[pic-def]: ./pic.png' + const out = rewriteLocalImageUrls(md, scope, '/repo/readme.md', ORIGIN) + expect(out).toContain('[docs]: ./docs.md') + expect(out).not.toMatch(/\[docs\]:.*\/sidebar\/file/) + // The image-referenced definition IS rewritten. + expect(out).toMatch(/\[pic-def\]:.*\/sidebar\/file/) + }) }) diff --git a/tests/market-manifest.spec.ts b/tests/market-manifest.spec.ts index def96c7..6ae4a7e 100644 --- a/tests/market-manifest.spec.ts +++ b/tests/market-manifest.spec.ts @@ -50,7 +50,10 @@ const STABLE_SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u function packedManifest(): Record { const dir = mkdtempSync(join(tmpdir(), 'market-pack-')) try { - execFileSync('pnpm', ['pack', '--pack-destination', dir], { cwd: ROOT, stdio: 'pipe' }) + // On Windows `pnpm` ships as a `.cmd` shim that `execFileSync` cannot + // spawn directly (ENOENT). Run it through the shell so the same call + // works on every platform; `tar` is available on all supported OSes. + execFileSync('pnpm', ['pack', '--pack-destination', dir], { cwd: ROOT, stdio: 'pipe', shell: process.platform === 'win32' }) 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 { diff --git a/tests/smoke.spec.ts b/tests/smoke.spec.ts index 4b57753..5cabab8 100644 --- a/tests/smoke.spec.ts +++ b/tests/smoke.spec.ts @@ -12,6 +12,7 @@ import { SettingsConflictError, settingsNamespace } from '@deepseek-ai/dsh-setti import { apply, mediaTypeForPath } from '../src/index.ts' import * as git from '../src/git.ts' import { listDirectory } from '../src/fs-tree.ts' +import { encodeHtmlUrl } from '../src/html-route.ts' import { defaultShell, PtyManager, type SidebarPty } from '../src/pty-manager.ts' import type { SidebarWebRoute, SidebarWebUpgradeRoute } from '../src/context-types.ts' @@ -636,8 +637,10 @@ describe('session cwd resolution over the API route', () => { const media = routes.find(route => route.path === '/sidebar/file')! const html = routes.find(route => route.path === '/sidebar/html')! const mediaResult = await invokeGet(media, `/sidebar/file?sessionId=security&path=${encodeURIComponent(join(workspace, 'link', 'secret.png'))}`) - const htmlPathname = join(workspace, 'link', 'secret.html').replace(/\\/g, '/').split('/').map(encodeURIComponent).join('/') - const htmlResult = await invokeGet(html, `/sidebar/html/security${htmlPathname}`) + // Use the production encoder so the URL is well-formed on every + // platform (a Windows drive path needs the leading slash separator + // that a naive join-without-separator drops). + const htmlResult = await invokeGet(html, encodeHtmlUrl('security', join(workspace, 'link', 'secret.html'))) expect(mediaResult).toMatchObject({ status: 403 }) expect(JSON.parse(mediaResult.body)).toMatchObject({ ok: false, error: { code: 'forbidden' } }) expect(htmlResult).toMatchObject({ status: 403 })