# Changelog 本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 规范,版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。 ## [Unreleased] ## [1.0.0] - 2026-10-05 **2026-10-05 · Fixed · CI 第四次修复:`isLinkEntry` 两侧统一 canonical,修 macOS/Windows 把真实目录当链接** - **背景**:前三次修复(`68da267` / `ef4b38a` / `063de7e`)都没让 CI 变绿:macOS 与 Windows 各 7 条固定失败,Ubuntu 三个 Node 版本全绿。7 条同源——`hasCollision` 判错(`add` 返回 202、`toggle` 返回 200,而不是 409 `collision`)与 `export` 的 `links` 记成空数组。 - **根因**:`isLinkEntry` 拿 `realpath` 的**规范化**结果去比 `resolve` 的**纯语法**结果:`resolve(target) !== resolve(path)`。macOS 上 `os.tmpdir()` 位于 `/var` 之下,而 `/var` 是指向 `/private/var` 的符号链接——链路两侧一个带 `/private` 一个不带,于是**普通目录也被判为 "resolves elsewhere"**,即「这是 nexus 管理的链接、可以安全替换」。Windows runner 的 temp 路径同理(8.3 短名 `RUNNER~1` 会被 `realpath` 展开)。Linux 的 `/tmp` 之上没有任何被重写的祖先,`resolve` 与 `realpath` 逐字节一致,所以只有两个平台挂——这正是「本地全绿、CI 全红」的来源。 - **变更**: - `src/link.ts` 的 `isLinkEntry` 改为**三个**判据、顺序固定:① `lstat` 认符号链接(捕获悬空 POSIX 符号链接;先判以免被路径格式比较吃掉);② 两侧都走 `safeRealPath` 后比较(修正点就是这处不对称);③ 两者都不可判定时才回退 `existsAsEntry`(只有「目标已删的 Windows junction」落这里,`realpath` 看不见它,但它是 nexus 可删的目录项)。 - `src/export.ts`:`links` 归属比较从 `resolve(l.target) === resolve(resourceBase)` 改为两侧 canonical(`safeRealPath` 预计算一次)。`EntryLink.target` 本就 canonical(由 `entryLinks` 产出),拿它比纯语法路径在同样的平台上匹配不到任何东西——与 `isLinkEntry` 同一类缺陷。 - **不做**:不动 `entryLinks` / `unlinkIfPointsInto` 已正确的 `safeRealPath` 用法(`ef4b38a` 已修);不动 `removeLinkEntry` 的 `rmdir` 优先删除。 - **测试**:新增 `test/link-canonical.test.ts`(已加入 `package.json` 的显式列表)。它用目录链接把整个 DSH_HOME 置于一个会被 `realpath` 重写的祖先之下,**并断言该前置条件成立**(`resolve(root) !== realpath(root)`),因此不可能空跑通过;随后校验真实目录仍是 collision、真实文件仍是 collision、受管链接既不是 collision 也仍可归属。用例用 `import('../src/paths.js?canonical=…')` 取新模块图(`paths.ts` 在导入时捕获 `DSH_HOME`),并在导入后立刻还原环境变量。 - **验证**:把 `isLinkEntry` 临时还原成修复前的实现,新用例**确实失败**(`false !== true`);装回修复后通过——即该用例真正卡住了这个缺陷,而不是同义反复。完整套件 578 条 · 575 通过 · 0 失败 · 3 跳过(原 577 · 574 · 3,新增 1 条)。 - **踩坑记录(写给以后在 Windows 上排查的人)**:`fs.realpathSync` **不解析** junction,而 `fs.promises.realpath` **解析**。用同步 API 做探针会把 junction 看成普通目录,从而得出「`realpath` 不解析 junction」的错误结论——本次排查在这上面绕了很久。生产代码全程用 promise API。 - **如何辨识改动**:`src/link.ts`(`isLinkEntry` 三个判据 + 注释)、`src/export.ts`(`canonicalRoot` 预计算 + 比较改为 canonical)、新增 `test/link-canonical.test.ts`、`package.json`(test 列表 +1)、`lib/` 重建产物。 **2026-10-05 · Fixed · `safeRealPath` 双边规范化 + `health.ts` 同步跟进,修 macOS/Windows 全矩阵路径格式错配** - **背景**:上一轮修复只给 `entryLinks` 的 `base` 加了 `safeRealPath`,但 `resolved` 仍用 `resolve(target)`——`resolve` 不解析父级符号链接也不展开 Windows 8.3 短路径,而 `resolveLinkTarget`(`realpath`)返回规范路径。两边格式不一致,`pointsInto` 仍然 false。同时 `health.ts` 的 `diagnoseEntry` 和 `findOrphanLinks` 也有同样的 `resolve` vs `realpath` 错配。 - **变更**: - `src/link.ts`:`safeRealPath` 升级为 `export`,路径不存在时逐级向上 `realpath` 再拼回剩余尾段;`entryLinks` 和 `unlinkIfPointsInto` 的 target/resolved 两边都走 `safeRealPath`。 - `src/health.ts`:`diagnoseEntry` 的 `base` 和 `resolved`、`findOrphanLinks` 的 `reposBase`/`bases`/`resolved` 全部改走 `safeRealPath`。 - `test/link.test.ts`:`assertLinkTarget` 改为 async,断言用 `realpath` 规范化两边;移除未使用的 `resolve` 导入。 - `test/adopt.test.ts`:subdir 断言改用 `realpath` 规范化两边。 - **验证**:本地六步门禁全通过(560 · 556 pass · 1 fail 预存 panel-render · 3 skip)。 - **如何辨识改动**:`src/link.ts`(`safeRealPath` 升级+双边调用)、`src/health.ts`(`diagnoseEntry`/`findOrphanLinks` 走 `safeRealPath`)、`test/link.test.ts`(`assertLinkTarget` async + `realpath`)、`test/adopt.test.ts`(`realpath` 断言)、`lib/` 重建产物。 **2026-10-05 · Fixed · `entryLinks` 路径解析走 `realpath` 对齐链接目标,修 macOS/Windows 全矩阵 link 归属失败** - **背景**:`fix(link)` 把链接读取从 `lstat`+`readlink` 改为 `realpath`(`resolveLinkTarget`),解决了 Windows junction 不可见的问题。但 `entryLinks` 和 `unlinkIfPointsInto` 中链接目标走 `realpath`(解析整条路径链上的所有符号链接),而 `base`/`dir` 只用 `resolve`(不解析父级符号链接)。macOS 上 `/var` → `/private/var` 是合成符号链接,temp 目录路径经 `realpath` 后多出一段 `/private`,导致 `pointsInto` 永远 false——所有 entry 报告 `enabled: false`、`links: []`。Windows CI runner 的 temp 路径也有类似 reparse point 解析差异。 - **变更**:`src/link.ts` 新增 `safeRealPath`(`realpath` + 路径不存在时回退 `resolve`);`entryLinks` 的 `base` 和 `unlinkIfPointsInto` 的 `dir` 改走 `safeRealPath`,与 `resolveLinkTarget` 同一把尺子。 - **验证**:本地六步门禁全通过(577 · 574 pass · 0 fail · 3 skip)。 - **如何辨识改动**:`src/link.ts`(新增 `safeRealPath` 函数、`entryLinks` 和 `unlinkIfPointsInto` 各一处调用)、`lib/link.js`(重建产物)。 **2026-10-05 · Fixed · completion 测试期望值补 `--json`,修 CI bash/zsh/fish 矩阵全红** - **背景**:`feat(cli): --json 机器接口` 给 `add` 命令新增 `--json` flag 时,三个 completion 模板源码(bash/zsh/fish)已同步更新,但 `test/completions.test.ts` 中三处 `add` 期望值漏跟。同时 zsh 测试中 `update --x` 的期望值也漏了 `--json`(`update` 命令接受 `--json`,zsh `compadd` 记录全量候选)。本地 Windows 无 bash/zsh/fish,测试 skip 不报错;CI ubuntu 镜像装有三 shell,测试真跑后全红。 - **变更**:`test/completions.test.ts` 四处期望值补 `--json`——bash add(L455)、zsh add(L548)、zsh update-flag(L555)、fish add(L607)。 - **验证**:本地 Git Bash 加入 PATH 后 bash 测试通过(22 pass · 0 fail · 2 skip);zsh/fish 模板源码与期望值逐字对照一致(zsh `compadd -- --name --ref --subdir --yes --json`、fish 五条 `-l` 规则含 `-l json`)。 - **如何辨识改动**:`test/completions.test.ts`(三处字符串字面量追加 `--json`)。 ## [0.6.0] - 2026-10-05 **2026-10-05 · Fixed · `test/panel-render` 改名 `.tsx` 后缺运行时 `React` 绑定** - **背景**:`test/panel-render.test.ts` 改名 `.tsx` 并改用 JSX 之后,13 条渲染断言全部以 `ReferenceError: React is not defined` 失败。原因:`tsconfig.json` 未设 `jsx`,`tsx`/esbuild 因此按**经典运行时**转译,把 JSX 编译成 `React.createElement`——文件里因此必须存在 `React` 绑定,哪怕没有一行代码按名字读它。 - **变更**:`test/panel-render.test.tsx` 补回 `import React from 'react'` 并写明它是运行时必需而非类型导入;`src/client/panel.tsx` 的同类导入本来就在(上一提交为「去掉无用导入」曾误删,已还原)。 - **已知告警(接受)**:两处 `React` 导入都被 typescript-eslint 判为未使用,故 `eslint-disable-next-line @typescript-eslint/no-unused-vars` 被报成 **unused directive**——规则没有建模经典转译的运行时绑定,而 `--fix` 会把这条载荷行删掉。两处保留并在注释中写明。`panel.tsx` 那处在本次工作开始前就存在于基线,非本轮的净新增。lint 退出码为 0(0 error · 2 warning);若要归零需给 `test`/`tsconfig.json` 打开 `jsx: react-jsx` 才能完全移除该导入,属独立的配置改造,不在本轮范围。 - **验证**:`npm test` 577 通过 · 0 失败 · 3 跳过(面板文件 18/18)。 - **如何辨识改动**:`test/panel-render.test.tsx`(顶部一处导入及其注释)、`src/client/panel.tsx`(同一处的还原)。 **2026-10-05 · Fixed · B0 收尾:CSS Modules 类名映射按键排序,修 `build:client` 产物不可重现** - **背景**:`lib/client.js` 每次 `npm run build:client` 都会变。原因是 lightningcss 返回的 `exports` 顺序在两次运行之间并不固定,未排序地灌进类名映射会让**同一份输入产出字节不同的产物**——实测 27 行删、27 行增,全部只是 `{"a": "…", "b": "…"}` 的键顺序。 - **后果**:提交重建产物时的 diff 无法审阅(每个键占一行,看起来像整块重写);更糟的是 `git diff --exit-code -- lib` 这类校验永远为真,会掩盖真正的产物漂移——而 B0 的验收标准恰恰依赖它。 - **变更**:`tsdown.config.ts` 的 `dsh-css-modules-inline` 在构造类名映射前对 `Object.entries(exports)` 按键排序。宿主的同名插件同样排序,此处对齐。 - **验证**:连续跑三次 `npm run build:client`,`lib/client.js` 的 SHA256 完全一致(`B372711E…`,60,040 B);随后 `git diff --exit-code -- lib` 返回 0,即提交的产物与新鲜构建逐字节相同。 - **如何辨识改动**:`tsdown.config.ts`(一处 `.sort()` 及其注释)、`lib/client.js`(按新顺序重建,与上一版逐字节差别仅在键顺序)。 **2026-10-05 · Changed · B2:面板原语化 + CSS Modules 布局 + `--dsw-*` 令牌主题(裁定与实现)** - **路线裁定(B0.4)**:宿主平台源码与运行中 shell 的 boot 表互证后确定——面板走「**宿主原语做原子 + CSS Modules 做布局 + `--dsw-*` 令牌做主题**」。这不是「原语 vs 自绘」的二选一:宿主自己的客户端面板就是这个组合(同一文件既 `import { StateDot, Tag }`,又 `import css from './X.module.css'`,CSS 内全是 `var(--dsw-alias-*)`)。设计稿 §7 的「固定三色强调色 + `rgba(128,128,128,…)` 半透明灰」是平台外的自造方案,**不予实现**:它需要按主题自行维护,且不会跟随宿主换肤。§7 的**紧凑行 / 信息密度**设计意图**保留**并落在 `panel.module.css`。 - **变更**: - `src/client/panel.tsx`:原子替换——状态点 → `StateDot`(含 `entryState()` 的三态推导:无源/禁用为 `idle`、有更新为 `warning`、正常为 `done`);动作按钮 → `Button`(`primary` / `outline` 区分语义,`icon` 传宿主图标);更新徽标与状态标签 → `Tag`(`warning` / `outline` / `quiet`);启停开关 → `Switch`(受控,`label` 提供无障碍名);文本输入与搜索框 → `Input`(搜索带 `IconSearchOutlineRegular` 前导图标);`pin` 内联展开 → `DisclosureRow`(受控展开,与宿主面板的折叠行为一致);job 输出 → `TerminalBlock`(`maxLines: 6` 取代原先手写的 `slice(-6)`);`window.confirm` → `RiskConfirmation` 受控遮罩(需勾选确认才能执行删除)。刷新按钮带 `IconRefreshOutlineRegular`,有更新的行其 update 按钮带 `IconWarningOutlineRegular`。 - 新增 `src/client/panel.module.css`:布局、密度与间距全部走 `--dsw-*` 令牌(13 个令牌,已逐个对照宿主自身客户端包的 CSS 核验存在)。不重绘任何原语的外观。 - `src/client/panel.tsx` 头注释重写:记录路线裁定、`PLATFORM_MODULES` 的关系,以及为什么设计稿的配色系统不落地。 - `src/client/index.tsx`:注释记录 `settings.section`(自有独立设置行)与宿主 plugin-inventory 用的 `settings.plugins.tab`(Plugins 区内的标签页)之别,并说明后者是**信息架构**变更而非呈现变更,故不在本轮范围,避免下一个人误"修"。 - **不做**:不实现 §7 的固定色值与自绘 `