✨ 亮点
本版本是继 0.8.8 加固与 0.8.9 插件加载器工作之后对 CLI 的一次开发体验打磨。用户最容易感知的变化是:workspace 配置错误现在会用一段可工作的示例直接指向实际问题,而不是只丢出一句干巴巴的话。多项长期存在的 CLI 缺口也一并补齐:deploy --force 标志位真正生效(不再静默覆盖)、migrate --dry-run 现在是真实可用的选项、migrate --upgrade 覆盖完整的旧键映射、dev 服务器在 docmd stop 时优雅退出、插件安装器在空操作场景下不再冒充成功。
没有公共 API 变更,没有破坏性配置变更。纯粹的开发体验版本。
🧪 migrate --dry-run 覆盖每一条迁移路径
docmd migrate --dry-run 现在是每一种受支持来源(Docusaurus、MkDocs、VitePress、Starlight)以及就地 --upgrade 路径的真正可用标志位。Dry-run 打印出将要发生的变化,然后以退出码 0 终止,不会写入任何东西:
┌─ Dry run: MkDocs migration
│ Would move 3 files → mkdocs-backup/
│ docs
│ index.md
│ mkdocs.yml
│ Would write docmd.config.js
│ Config {"title":"My Test Site","src":"docs","out":"dist","theme":{"appearance":"system"}}
└──────────────────────────────────────────────────────────
⬢ No changes made. Re-run without --dry-run to apply.
对于 --upgrade,dry-run 会完整打印出升级后的配置,便于在提交前与现有文件做 diff。
📦 migrate --upgrade 覆盖完整的旧键映射
过去的 --upgrade 路径只处理六个旧键(projects、siteTitle、siteUrl、baseUrl、srcDir、outputDir、defaultLocale)。现在它还会处理:
| 旧键 | 变为 |
|---|---|
source |
src |
outDir |
out |
nav |
navigation |
顶层 search(布尔) |
plugins.search |
顶层 sidebar |
layout.sidebar |
theme.defaultMode |
theme.appearance |
theme.enableModeToggle |
optionsMenu.components.themeSwitch |
theme.positionMode |
optionsMenu.position |
每一次升级都会发出一行 [ DONE ] Upgraded "X" to "Y",而一份不含任何旧键的配置仍会打印 Configuration is already up to date with the latest schema.
🩺 更可操作的 workspace 错误消息
当 workspace 配置缺少根项目(最常见的初次配置错误)时,原来的错误只是一句干巴巴的话:"Workspace configuration must have a root project with prefix "/""。这种报错既不给示例,也不告诉用户下一步该怎么办。
现在的错误消息如下:
Workspace configuration must include a root project with prefix "/".
Each workspace needs one project that owns the site root, with the others mounted under their own prefixes.
Example:
{
"workspace": {
"projects": [
{ "name": "main", "src": "./docs", "prefix": "/" },
{ "name": "api", "src": "./api-docs", "prefix": "/api/" }
]
}
}
See https://docs.docmd.io/guides/workspace for the full layout guide.
每一条 workspace 校验错误都采用同一种形态:都以一段能让验证器满意的"具体配置"结尾。
📦 deploy --force 名副其实
docmd deploy 上的 --force 标志位以前虽然被接受,但部署器从未读取它。在已经存在 Dockerfile 的项目上运行 docmd deploy --docker 时,会在没有警告的情况下默默覆盖它。
新的行为:
- 不加
--force时,会保留已有的配置文件,TUI 行打印[ SKIP ] Dockerfile (already exists, skipped — use --force to overwrite)。 - 加上
--force时,按原来的方式覆盖已有文件。
此规则适用于每一种部署目标:--docker、--nginx、--caddy、--github-pages、--vercel 和 --netlify。
🛰️ Netlify 模板不再对每个路由做 soft-404
过去生成的 netlify.toml 包含一个 [[redirects]] 块,把任何 URL 都以 HTTP 200 重定向到 /index.html:
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
这意味着任何缺失的路由都会以 200 状态返回首页 —— 一种 soft-404,会掩盖错误、损害 SEO,并带来 URL 枚举攻击面。该块已被移除:docmd 会为每条路由生成真正的 HTML 文件,因此 Netlify 在文件存在时返回该文件,在不存在时回落到打包好的 404.html(并返回 404 状态码)。
🔗 离线模式链接在各种托管形态下都能工作
docmd build --offline 标志位过去会让渲染出的 HTML 保留 Markdown 链接的绝对路径,例如 <a href="/destination/">。这种路径在 HTTP 服务器上有效,但当用户从文件系统打开 site/index.html 时会失效 —— file:// 无法解析没有主机的绝对路径。
修复方法是在离线模式下后处理渲染出的 Markdown HTML,把每一条内部 <a href> 和 <img src> 重写为相对的 .html 路径,所用逻辑与按钮容器(:::)相同。结果如下:
| 源 | build(HTTP) |
build --offline(file://) |
|---|---|---|
按钮容器 ::: "/destination" |
./destination/ |
./destination/index.html |
Markdown 链接 [text](/destination) |
/destination/ |
./destination/index.html |
Markdown 图片  |
/assets/x.png |
./assets/x.png |
外部 https://example.com |
不变 | 不变 |
锚点 #section |
不变 | 不变 |
非离线构建完全保持不变(为 SEO 保留干净 URL)。输出可在所有托管形态下工作:file://、HTTP 服务器、自定义域名、dev 服务器、live 编辑器。嵌套页面获得正确的 ../ 前缀,以便从子文件夹中向上导航。
🔌 docmd-search 同行依赖一并自动安装
当设置了 plugins.search.semantic: true,而 docmd-search 自身或其某个同行依赖(@huggingface/transformers、onnxruntime-node)缺失时,插件现在会:
- 将模块解析范围限制在用户项目内(仅
process.cwd()/node_modules)—— 此前它可以通过__dirname一路向上走到 monorepo 根目录并在那里发现一份副本,导致行为反直觉。 - 当仅有同行依赖缺失时,只安装这些同行依赖(过去是:发出警告然后放弃)。
- 在回退消息中透出真实的原因 —— 此前消息统一显示 “docmd-search not installed”,即便实际只是某个同行依赖缺失。
🛰️ Open Knowledge Format 默认开启
OKF 已在 0.8.8 发布,并现在默认完全开启:
- 每次构建都会自动加载,除非显式通过
plugins.okf: false关闭。 plugins.okf.typeField默认为type(frontmatter > 路径 > 回退)。- 缺失的
type字段会生成一行汇总TUI行,并在okf/_meta/lint-report.txt中按页面给出条目,确保细节不丢失。 - 孤立概念和损坏的内部链接罗列在同一份 lint 报告中。
- 图谱查看器通过
plugins.okf.graph: true选择性启用(会在okf/graph/下生成graph/index.html、graph.js和graph.css)。 plugins.okf.i18n: true会为每一种已配置 locale 生成 OKF bundle(否则只生成默认 locale)。
无公共 API 变更。
🛑 docmd stop 优雅退出
docmd stop 此前发送 SIGTERM,如果失败就立即发送 SIGKILL。dev 服务器的 SIGTERM 处理函数是一个一行的 process.exit(0),会绕过 watcher、HTTP 服务器、WebSocket 服务器和 worker pool 的清理。一次 stop 之后,子进程和处于活动状态的 socket 可能悬挂在那里。
新的流程如下:
docmd stop发送 SIGTERM。- dev 服务器会像按下 Ctrl+C 一样执行优雅退出流程(关闭 watcher → 关闭 wss → 关闭 http 服务器 → 终止 worker pool → exit 0)。
docmd stop在最多 5 秒内每 100 ms 检查一次进程是否自行退出。- 只有在宽限期结束后进程仍然存活时,
docmd stop才会升级到 SIGKILL。
TUI 行会在成功退出时显示 Stopped ... PID gracefully,如果需要升级则会显示 Killed ... PID (SIGKILL after grace)。
🐛 Bug 修复
- Workspace 校验器:在未配置根项目时给出可操作的错误消息。 单句错误被替换成多行消息,解释要求、给出有效 JSON 示例并链接到 workspace 布局指南。
- 插件安装器:在已安装场景下给出准确的结果消息。 对已存在于配置中的插件运行
docmd add <plugin>时,过去会打印 “Plugin successfully installed and activated”,尽管实际上并没有发生安装。TUI 末行现在反映真实结果:新安装打印成功消息;已配置则打印⬢ Plugin was already configured. Nothing changed. - 部署器:
--force不再静默覆盖已有配置。 每一种部署目标都尊重--force:默认行为是跳过已有文件;--force显式开启覆盖。 - 部署器(Netlify):移除
[[redirects]]块以修复 soft-404。 生成的netlify.toml不再把所有 URL 以状态 200 重写到/index.html。缺失的路由现在返回打包好的404.html,状态码为 404。 - 离线构建:Markdown 链接和图片现在被改写为相对的
.html路径。 此前只有按钮容器具备离线感知;Markdown 的[text](url)和会发出绝对路径,file://无法解析。它们现在与按钮容器走同一套fixHtmlLinks重写(#167)。 - dev 服务器:SIGTERM 与 SIGINT 走同一套优雅退出。 dev 服务器现在在 SIGTERM 退出前会先关闭 watcher、WebSocket 服务器、HTTP 服务器和 worker pool。
docmd stop轮询进程是否退出,只有在 5 秒宽限期后仍未退出时才升级到 SIGKILL。 - docmd-search:缺失的同行依赖会自动安装。 当设置了
plugins.search.semantic: true,且同行包(@huggingface/transformers、onnxruntime-node)未安装时,插件现在改为安装它们,而不是发出警告并退回到关键字搜索(#163 的进一步修复)。 - 版本:
config.versions.list作为all的别名被接受。 写成list(一种常见的"版本列表"字段命名)的用户过去会得到静默的 0 页构建,因为 schema 只认all。把list别名为all可以在不改动规范键名的情况下恢复他们的配置(M-6)。 - 版本:缺失当前版本目录是一个硬错误。 构建现在会中止,并以明确的指针指向缺失的路径与版本 id。之前会静默生成一个 0 页站点,没有任何可操作的提示。旧的(非当前)版本仍保持软警告(T-Z6)。
- llms.txt / llms.json 标题会被清洗。 包含 Markdown 注入字符(
`、[、]、换行)的 frontmatter 标题不再破坏链接形式或被渲染成原始 HTML;而以=、+、-、@开头的标题会被加上前导单引号,避免在电子表格中打开时被当成公式执行。CSV 公式中和(T-Z11)与 Markdown 注入防护(T-Z10)合并为一个 helper。 NO_COLOR与DOCMD_NO_BANNER抑制构建横幅。 设置NO_COLOR=1(或 docmd 特定的DOCMD_NO_BANNER=1)现在会在构建输出顶端隐藏 ASCII 美术与版本行。NO_COLOR是 CLI 业界的事实标准;DOCMD_NO_BANNER是一条 docmd 特定的逃生口,给"要彩色但不要横幅"的用户使用(N-13 + N-16)。- 迁移润色。
moveFilesToBackup现在保留 lockfile 和package.json,避免恢复时必须重新解析每一个依赖(N-10)。Docusaurus 与 MkDocs 迁移器保留原始的staticDir/site_dir(N-22),MkDocs 的nav:块被翻译成 docmd 的navigation格式,多级段落通过children保留(N-9)。
更新日志
- Workspace 校验器:
packages/core/src/engine/workspace.ts中的validateProjects现在在没有任何项目带prefix: "/"时抛出多行Error,附带 JSON 示例和文档链接(M-2)。 - 插件安装器:
packages/plugins/installer/src/index.ts中的addPlugin根据配置编辑器返回的injected标志分支,已配置场景发出TUI.info,替代原先误导性的TUI.success(M-14)。 - 部署器:
packages/deployer/src/index.ts中的write()现在接收force参数,并在目标文件已存在且未传入--force时输出一行[ SKIP ](N-2)。六个目标生成器(docker、nginx、caddy、github-pages、vercel、netlify)都透传opts.force。 - 部署器(Netlify):
packages/deployer/src/providers/netlify.ts中的generateNetlify不再发出[[redirects]] from = "/*" status = 200块。docmd 为每条路由生成真实的 HTML,因此该通配既多余也是 soft-404 的根源(T-Z9)。 - Stop:
packages/core/src/commands/stop.ts中的stopServer导出一个新的waitForExit(pid, timeoutMs)辅助函数,并在发送 SIGTERM 后用它轮询目标进程是否退出。只有在 5 秒宽限期过后才升级到 SIGKILL(M-11)。 - dev 服务器:
packages/core/src/commands/dev.ts中的startDevServer把 SIGINT 退出路径抽取为gracefulShutdown(),SIGINT和SIGTERM处理函数共用(M-11)。 - Migrate:
packages/core/src/commands/migrate.ts中的migrateProject接受一个新的dryRun选项(N-3)。每一条来源路径(Docusaurus、MkDocs、VitePress、Starlight)的 dry-run 都会在任何副作用发生之前打印文件移动清单与新的docmd.config.js;--upgrade则打印升级后的配置。packages/core/src/bin/docmd.ts把--dry-run加入 migrate CLI。 - Migrate:
packages/core/src/commands/migrate.ts中的--upgrade路径现在额外处理八个旧键:source、outDir、nav、顶层search、顶层sidebar、theme.defaultMode、theme.enableModeToggle、theme.positionMode(N-4)。 - 测试:
tests/cli-contracts/validate-workspace.test.js新增 5 条 M-2 断言,覆盖退出码与四条消息内容不变量。 - 测试:
tests/cli-contracts/plugin-add-remove.test.js新增 3 条 M-14 断言,覆盖"已安装"消息、虚假成功消息的不出现,以及全新安装的回归守卫。 - 测试:新增
tests/cli-contracts/deploy.test.js(5 条断言,在tests/runner.js中以deploy注册),覆盖 N-2 的跳过/覆盖/全新三种路径。 - 测试:新增
tests/cli-contracts/stop.test.js(3 条断言,注册名为stop),覆盖 M-11 的waitForExit辅助函数在真实子进程上的行为。 - 测试:新增
tests/cli-contracts/migrate.test.js(33 条断言,注册名为migrate),覆盖 N-3 dry-run 的源迁移与 upgrade 两种路径,以及 N-4 对全部 13 种旧键升级的覆盖。