From 62d0f26fd68a0b49307f78affd140dad971432f5 Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 6 Aug 2026 17:28:30 +0800 Subject: [PATCH] refactor(cli)!: namespace the profile and bundle manifests under dsh.profile and dsh.bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A profile manifest and a bundle manifest are different kinds and shared one flat `dsh` section: `dsh.plugins` listed bundles (not plugins) and `dsh.patch` declared a bundle's layer. Each kind now names its role — a bundle declares `dsh.bundle.patch`, a profile declares `dsh.profile.bundles` — so a package.json states which role it plays and the list name matches its contents. `DEFAULT_PROFILE_PLUGINS` becomes `DEFAULT_PROFILE_BUNDLES`, and `DshManifestSection` splits into `DshBundleManifest`/`DshProfileManifest`. Pre-release: no compatibility shim; turtle-ui moved with it (bd5ff10). --- ...026-08-05-profile-plugin-bundles.i18n.yaml | 4 +- .../2026-08-05-profile-plugin-bundles.md | 12 ++-- .../2026-08-05-profile-plugin-bundles.zh.md | 12 ++-- apps/cli/README.i18n.yaml | 4 +- apps/cli/README.md | 2 +- apps/cli/README.zh.md | 2 +- apps/cli/reference/README.i18n.yaml | 4 +- apps/cli/reference/README.md | 4 +- apps/cli/reference/README.zh.md | 4 +- apps/cli/src/plugin.ts | 45 +++++++------ apps/cli/src/profile-boot.ts | 12 ++-- apps/cli/tests/built-bin.e2e.ts | 51 +++++++++----- apps/cli/tests/headless-shutdown.e2e.ts | 2 +- apps/web/tests/scaffold.ts | 4 +- docs/user/guide/config.i18n.yaml | 4 +- docs/user/guide/config.md | 2 +- docs/user/guide/config.zh.md | 2 +- packages/bundle/README.i18n.yaml | 4 +- packages/bundle/README.md | 2 +- packages/bundle/README.zh.md | 2 +- packages/bundle/base/README.i18n.yaml | 4 +- packages/bundle/base/README.md | 2 +- packages/bundle/base/README.zh.md | 2 +- packages/bundle/base/package.json | 4 +- packages/bundle/base/src/index.ts | 2 +- packages/bundle/base/tests/base.spec.ts | 12 ++-- packages/bundle/headless/package.json | 4 +- packages/bundle/web-app/package.json | 4 +- packages/bundle/web-app/src/index.ts | 2 +- packages/ui/app-boot/README.i18n.yaml | 4 +- packages/ui/app-boot/README.md | 8 +-- packages/ui/app-boot/README.zh.md | 8 +-- packages/ui/app-boot/src/index.ts | 36 +++++----- packages/ui/app-boot/src/profile.ts | 67 ++++++++++++------- packages/ui/app-boot/tests/profile.spec.ts | 20 +++--- scripts/check-workspace-constraints.ts | 2 +- 36 files changed, 203 insertions(+), 156 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml index a95e7d578f..eed6bee5f0 100644 --- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md -2026-08-05-profile-plugin-bundles.md: d35a8d7e3976e3dfc40a3574f216bc0344d1283b -2026-08-05-profile-plugin-bundles.zh.md: 5bfe28c19d3d14921ef76a84aacfbc31fa8d8b0e +2026-08-05-profile-plugin-bundles.md: 11a8ac3d4005371ca9596ba237aaf42a8e770dee +2026-08-05-profile-plugin-bundles.zh.md: 0e9ebf657ccb9d05967d90a935b356acf287a24c diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md index d35a8d7e39..11a8ac3d40 100644 --- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md +++ b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md @@ -10,20 +10,20 @@ The `dsh` launcher hardcoded its compositions: `base.cordis.yml` + `web.cordis.y ## Decision -Everything becomes a **profile**: a directory `$DSH_HOME/profiles/` with a `package.json` (pnpm-managed out-of-tree plugin `dependencies` plus the ordered `dsh.plugins` bundle-layer list) and a user `cordis.patch.yml`. A **bundle** is an npm package declaring `"dsh": { "patch": "./cordis.patch.yml" }`; the tree composes over an empty root by applying each bundle's patch in `dsh.plugins` order, then the user layer, then `--patch` overlays, then flag patches — one `applyEntryPatches` call, identical for boot, flag derivation, and `--dump-config`. +Everything becomes a **profile**: a directory `$DSH_HOME/profiles/` with a `package.json` (pnpm-managed out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list) and a user `cordis.patch.yml`. A **bundle** is an npm package declaring `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; the two manifest kinds live under distinct `dsh.profile` / `dsh.bundle` keys so a package.json states which role it plays. The tree composes over an empty root by applying each bundle's patch in `dsh.profile.bundles` order, then the user layer, then `--patch` overlays, then flag patches — one `applyEntryPatches` call, identical for boot, flag derivation, and `--dump-config`. -The shipped compositions became bundles: `@deepseek-ai/dsh-base` (the former base rows as one insert), `@deepseek-ai/dsh-web-app` (the former web overlay plus a runtime glue plugin that owns what used to be launcher code — frontend-dist resolution, the web-surface prompt section, bash runtime variables, the URL line), and `@deepseek-ai/dsh-headless` (a one-shot runner plugin over base + web-app). `dsh web` stays as an alias for `--profile web` carrying the Web flag family; `dsh --profile headless "task"` replaces `-p`; `dsh --config` is removed (its uses migrate to `--patch`). `dsh plugin --profile ` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.plugins` after `add`/`remove` (a patch-less package warns and stays a plain dependency). +The shipped compositions became bundles: `@deepseek-ai/dsh-base` (the former base rows as one insert), `@deepseek-ai/dsh-web-app` (the former web overlay plus a runtime glue plugin that owns what used to be launcher code — frontend-dist resolution, the web-surface prompt section, bash runtime variables, the URL line), and `@deepseek-ai/dsh-headless` (a one-shot runner plugin over base + web-app). `dsh web` stays as an alias for `--profile web` carrying the Web flag family; `dsh --profile headless "task"` replaces `-p`; `dsh --config` is removed (its uses migrate to `--patch`). `dsh plugin --profile ` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` after `add`/`remove` (a bundle-less package warns and stays a plain dependency). -Resolution is two-anchored by construction: `dsh.plugins` names resolve from the dsh installation first, then the profile directory — so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them — while bare plugin names in patch rows resolve through the profile directory's Node parent-walk into the maintained flat fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch). +Resolution is two-anchored by construction: `dsh.profile.bundles` names resolve from the dsh installation first, then the profile directory — so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them — while bare plugin names in patch rows resolve through the profile directory's Node parent-walk into the maintained flat fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch). -Two supporting refactors: the webserver's built-in static dist serving became the single-owner **fallback seat** (`registerFallback`/`applyIndexTaps`), with the SPA server extracted to `@deepseek-ai/dsh-frontend-static` so the web bundle owns its dist as composition, not launcher code; and the personal-overlay machinery (`loadPersonalPatches`, `$DSH_HOME/config.yaml`) was retargeted to per-profile `cordis.patch.yml` files (`loadOptionalPatches`, `watchPersonalPatches` taking a filename). +Two supporting refactors: the webserver's built-in static dist serving became the single-owner **fallback seat** (`registerFallback`/`applyIndexTaps`), with the SPA server extracted to `@deepseek-ai/dsh-frontend-static` so the web bundle owns its dist as composition, not launcher code; and the personal-overlay machinery of the [dsh CLI personal-config decision](../feature/2026-07-20-dsh-cli-personal-config.md) (`loadPersonalPatches`, `$DSH_HOME/config.yaml`) was retargeted to the per-profile and home-level `cordis.patch.yml` layers (`loadOptionalPatches`, `watchUserPatches` taking a filename), superseding that note's entry modes and file location while keeping its Harness-home root, patch semantics, and fail-loud parsing. ## Alternatives considered -- **Dependency-scan plus partial `patchOrder`** (the original sketch): scanning `dependencies` for bundles and ordering unlisted ones alphabetically has two sources of truth and an implicit tie-break; one explicit ordered `dsh.plugins` list is smaller and fully deterministic. A raw `pnpm add` inside the profile installs a library without activating any patch — explicit, no spooky scan. +- **Dependency-scan plus partial `patchOrder`** (the original sketch): scanning `dependencies` for bundles and ordering unlisted ones alphabetically has two sources of truth and an implicit tie-break; one explicit ordered `dsh.profile.bundles` list is smaller and fully deterministic. A raw `pnpm add` inside the profile installs a library without activating any patch — explicit, no spooky scan. - **`link:` entries for in-box bundles**: pnpm cannot version, install, or update a `link:` into the installation, it embeds a machine path in a user file, and it breaks when the installation moves. The two-anchor resolution plus healed symlink fallback gives the same guarantee ("bundles come from the installation") without ceremony. - **A pre-boot `context` module in the bundle manifest** for boot-time values (dist path, flag facts): rejected in favor of pure plugins — the glue is ordinary rows the launcher patches, so the composition stays fully dumpable and the manifest stays data-only. The launcher-owned `ctx.headlessIo` seam is the one host-provided slot, and it is provided in `boot()`'s `prepare` hook, before any config-tree entry mounts. -- **Transitive bundle auto-application**: only direct `dsh.plugins` entries contribute layers; a meta-bundle wanting to re-export another bundle's patch must do so explicitly in its own patch file. +- **Transitive bundle auto-application**: only direct `dsh.profile.bundles` entries contribute layers; a meta-bundle wanting to re-export another bundle's patch must do so explicitly in its own patch file. ## Consequences diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md index 5bfe28c19d..0e9ebf657c 100644 --- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md @@ -10,20 +10,20 @@ Status: implemented ## Decision -一切都变成 **profile**:即目录 `$DSH_HOME/profiles/`,其中包含一个 `package.json`(pnpm 管理的树外插件 `dependencies`,加上有序的 `dsh.plugins` 组合包层列表)和一份用户 `cordis.patch.yml`。**组合包**(bundle)是声明了 `"dsh": { "patch": "./cordis.patch.yml" }` 的 npm 包;配置树在空的根之上组合:按 `dsh.plugins` 顺序应用每个组合包的 patch,然后是用户层,然后是 `--patch` overlay,最后是 flag patch——全部收敛为一次 `applyEntryPatches` 调用,启动、flag 派生与 `--dump-config` 使用完全相同的路径。 +一切都变成 **profile**:即目录 `$DSH_HOME/profiles/`,其中包含一个 `package.json`(pnpm 管理的树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表)和一份用户 `cordis.patch.yml`。**组合包**(bundle)是声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;两种 manifest 分别位于互不相同的 `dsh.profile` / `dsh.bundle` 键下,因此一份 package.json 能说明自己扮演哪种角色。配置树在空的根之上组合:按 `dsh.profile.bundles` 顺序应用每个组合包的 patch,然后是用户层,然后是 `--patch` overlay,最后是 flag patch——全部收敛为一次 `applyEntryPatches` 调用,启动、flag 派生与 `--dump-config` 使用完全相同的路径。 -已交付的组合改造成了组合包:`@deepseek-ai/dsh-base`(原有基础行合并为一次插入)、`@deepseek-ai/dsh-web-app`(原 web overlay,外加一个接管原启动器代码的运行时粘合插件——前端 dist 解析、web 表层提示词段落、bash 运行时变量、URL 行)、`@deepseek-ai/dsh-headless`(叠加在 base + web-app 之上的一次性 runner 插件)。`dsh web` 保留为携带 Web flag 家族的 `--profile web` 别名;`dsh --profile headless "task"` 取代 `-p`;`dsh --config` 被移除(其用途迁移到 `--patch`)。`dsh plugin --profile ` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并在 `add`/`remove` 后调和 `dsh.plugins`(没有 patch 声明的包会给出警告,保持为普通依赖)。 +已交付的组合改造成了组合包:`@deepseek-ai/dsh-base`(原有基础行合并为一次插入)、`@deepseek-ai/dsh-web-app`(原 web overlay,外加一个接管原启动器代码的运行时粘合插件——前端 dist 解析、web 表层提示词段落、bash 运行时变量、URL 行)、`@deepseek-ai/dsh-headless`(叠加在 base + web-app 之上的一次性 runner 插件)。`dsh web` 保留为携带 Web flag 家族的 `--profile web` 别名;`dsh --profile headless "task"` 取代 `-p`;`dsh --config` 被移除(其用途迁移到 `--patch`)。`dsh plugin --profile ` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并在 `add`/`remove` 后调和 `dsh.profile.bundles`(没有组合包声明的包会给出警告,保持为普通依赖)。 -解析在构造上就是双锚点的:`dsh.plugins` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析——因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们——而 patch 行中的裸插件名称经 profile 目录的 Node 父目录逐级查找,落到受维护的扁平回退目录 `$DSH_HOME/profiles/node_modules`(安装目录的应用与各组合包所依赖的每个包各一个符号链接,每次启动时修复)。 +解析在构造上就是双锚点的:`dsh.profile.bundles` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析——因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们——而 patch 行中的裸插件名称经 profile 目录的 Node 父目录逐级查找,落到受维护的扁平回退目录 `$DSH_HOME/profiles/node_modules`(安装目录的应用与各组合包所依赖的每个包各一个符号链接,每次启动时修复)。 -两项配套重构:webserver 内置的静态 dist 服务改为单一所有者的**回退席位**(`registerFallback`/`applyIndexTaps`),SPA 服务器提取到 `@deepseek-ai/dsh-frontend-static`,使 web 组合包以组合的方式持有自己的 dist,而不是靠启动器代码;个人 overlay 机制(`loadPersonalPatches`、`$DSH_HOME/config.yaml`)改为面向每个 profile 的 `cordis.patch.yml` 文件(`loadOptionalPatches`、接受文件名的 `watchPersonalPatches`)。 +两项配套重构:webserver 内置的静态 dist 服务改为单一所有者的**回退席位**(`registerFallback`/`applyIndexTaps`),SPA 服务器提取到 `@deepseek-ai/dsh-frontend-static`,使 web 组合包以组合的方式持有自己的 dist,而不是靠启动器代码;[dsh CLI 个人配置决策](../feature/2026-07-20-dsh-cli-personal-config.md)的个人 overlay 机制(`loadPersonalPatches`、`$DSH_HOME/config.yaml`)改为面向逐 profile 与 home 级的 `cordis.patch.yml` 层(`loadOptionalPatches`、接受文件名的 `watchUserPatches`),取代该笔记的各入口模式与文件位置,同时保留其 Harness home 根目录、patch 语义与大声失败的解析。 ## Alternatives considered -- **依赖扫描加部分 `patchOrder`**(最初的草案):扫描 `dependencies` 找出组合包、未列出者按字母序排列,会产生两个真源和一条隐式决胜规则;一份显式有序的 `dsh.plugins` 列表更小、完全确定。在 profile 内直接 `pnpm add` 只会安装一个库,不激活任何 patch——行为显式,没有暗中扫描。 +- **依赖扫描加部分 `patchOrder`**(最初的草案):扫描 `dependencies` 找出组合包、未列出者按字母序排列,会产生两个真源和一条隐式决胜规则;一份显式有序的 `dsh.profile.bundles` 列表更小、完全确定。在 profile 内直接 `pnpm add` 只会安装一个库,不激活任何 patch——行为显式,没有暗中扫描。 - **内置组合包使用 `link:` 条目**:pnpm 无法对指向安装目录的 `link:` 做版本管理、安装或更新,它会把机器路径嵌进用户文件,并且在安装目录移动后失效。双锚点解析加上每次启动修复的符号链接回退提供了同样的保证(「组合包来自安装目录」),且没有这些繁文缛节。 - **在组合包 manifest(元数据清单)中放一个启动前 `context` 模块**承载启动期取值(dist 路径、flag 事实):否决,改用纯插件——粘合逻辑就是启动器 patch 的普通配置行,因此组合始终可完整 dump,manifest 保持纯数据。启动器持有的 `ctx.headlessIo` seam 是唯一由宿主提供的 slot,且在任何配置树条目挂载之前,于 `boot()` 的 `prepare` 钩子中提供。 -- **组合包的传递式自动应用**:只有直接列在 `dsh.plugins` 中的条目才贡献层;想重新导出另一个组合包 patch 的元组合包,必须在自己的 patch 文件中显式完成。 +- **组合包的传递式自动应用**:只有直接列在 `dsh.profile.bundles` 中的条目才贡献层;想重新导出另一个组合包 patch 的元组合包,必须在自己的 patch 文件中显式完成。 ## Consequences diff --git a/apps/cli/README.i18n.yaml b/apps/cli/README.i18n.yaml index cdeaa77139..cbc74b6d0a 100644 --- a/apps/cli/README.i18n.yaml +++ b/apps/cli/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/cli/README.md -README.md: bfff1408f001dd10e665d1c56944f778e87aea56 -README.zh.md: 2d585f7e0654cbe58fbdd2f33e3d7b77f154a487 +README.md: f50f26ec5de54e094e17221f7cd355483483754e +README.zh.md: 242e64a0c42064b9f0b7621e665e85fe44523fe5 diff --git a/apps/cli/README.md b/apps/cli/README.md index bfff1408f0..f50f26ec5d 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -17,7 +17,7 @@ The invoking directory is the default workspace root. The `web` and `headless` p ## Profiles -A profile directory holds a `package.json` (out-of-tree plugin dependencies plus the ordered `dsh.plugins` bundle list) and a `cordis.patch.yml` (the user's own patch layer, hot-reloaded on long-lived surfaces). The tree composes over an empty root: each bundle's patch in `dsh.plugins` order, then the profile's `cordis.patch.yml`, then the home-level `$DSH_HOME/cordis.patch.yml`, then `--patch` overlays, then flag patches. Bundles named in `dsh.plugins` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins. Use `--dump-default-config` and `--dump-config` to inspect the composed tree without booting it. +A profile directory holds a `package.json` (out-of-tree plugin dependencies plus the profile manifest `dsh.profile` with its ordered `bundles` list) and a `cordis.patch.yml` (the user's own patch layer, hot-reloaded on long-lived surfaces). The tree composes over an empty root: each bundle's patch in `dsh.profile.bundles` order, then the profile's `cordis.patch.yml`, then the home-level `$DSH_HOME/cordis.patch.yml`, then `--patch` overlays, then flag patches. Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins. Use `--dump-default-config` and `--dump-config` to inspect the composed tree without booting it. The [CLI behavior reference](reference/README.md) owns exact layer precedence, flags, shutdown behavior, deployment defaults, and the source launcher. diff --git a/apps/cli/README.zh.md b/apps/cli/README.zh.md index 2d585f7e06..242e64a0c4 100644 --- a/apps/cli/README.zh.md +++ b/apps/cli/README.zh.md @@ -17,7 +17,7 @@ ## Profile -profile 目录包含一个 `package.json`(树外插件依赖,加上有序的 `dsh.plugins` 组合包列表)和一个 `cordis.patch.yml`(用户自己的 patch 层,在长期运行的 surface 上热重载)。配置树在空根之上组合:先按 `dsh.plugins` 顺序应用各组合包的 patch,然后是 profile 的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml`,然后是 `--patch` overlay,最后是 flag patch。`dsh.plugins` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`),再从 profile 自己的 `node_modules` 解析;pnpm 把树外插件安装在后者。使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。 +profile 目录包含一个 `package.json`(树外插件依赖,加上 profile manifest(元数据清单)`dsh.profile` 及其有序的 `bundles` 列表)和一个 `cordis.patch.yml`(用户自己的 patch 层,在长期运行的 surface 上热重载)。配置树在空根之上组合:先按 `dsh.profile.bundles` 顺序应用各组合包的 patch,然后是 profile 的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml`,然后是 `--patch` overlay,最后是 flag patch。`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`),再从 profile 自己的 `node_modules` 解析;pnpm 把树外插件安装在后者。使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。 [CLI(命令行界面)行为参考](reference/README.md)负责确切的层优先级、flag、关闭行为、部署默认值和源码启动器。 diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index f962567ca0..6be68170b9 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/cli/reference/README.md -README.md: 25c74bc6020aec409381796e129873bbdc937436 -README.zh.md: fd29f8f6a29d5858e6c9b6b66e21d5b92733480c +README.md: 62275139809364d80296433e804ade3d447f37e6 +README.zh.md: e508caaf05622c0c177eaacd070978368ff84af3 diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md index 25c74bc602..6227513980 100644 --- a/apps/cli/reference/README.md +++ b/apps/cli/reference/README.md @@ -6,7 +6,7 @@ This reference defines the profile, web-alias, plugin-management, and config-dum ## Profile boot -`dsh --profile ` boots the profile at `$DSH_HOME/profiles/`. The effective tree is composed over an empty root by applying, in order: each bundle patch named in the profile manifest's `dsh.plugins` list, the profile's own `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml` (machine-local preferences shared by every profile, so it outranks the per-profile layer), each `--patch ` overlay in argv order, and launcher flag patches. Later layers win per row; a patch replaces the targeted row's complete `config` value rather than deep-merging keys, and may insert new rows. A parse, schema, resolution, or plugin boot failure is reported and exits nonzero. SIGINT and SIGTERM dispose the mounted root before exit. +`dsh --profile ` boots the profile at `$DSH_HOME/profiles/`. The effective tree is composed over an empty root by applying, in order: each bundle patch named in the profile manifest's `dsh.profile.bundles` list, the profile's own `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml` (machine-local preferences shared by every profile, so it outranks the per-profile layer), each `--patch ` overlay in argv order, and launcher flag patches. Later layers win per row; a patch replaces the targeted row's complete `config` value rather than deep-merging keys, and may insert new rows. A parse, schema, resolution, or plugin boot failure is reported and exits nonzero. SIGINT and SIGTERM dispose the mounted root before exit. Bundle names resolve from the dsh installation first, then from the profile directory. In-box bundles (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`) therefore always come from the same installation as the running `dsh`; out-of-tree bundles come from the profile's pnpm-managed `node_modules`. A bare plugin `name` in any patch row resolves through the profile directory's Node parent-walk, which reaches the maintained installation fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch). @@ -25,7 +25,7 @@ dsh --profile web --patch ./extra.yml --dump-config ## Plugin management -`dsh plugin --profile ` initializes the profile when missing (shipped template, or `@deepseek-ai/dsh-base` alone for other names), then forwards `` to `pnpm` with the profile directory as working directory — `add`, `remove`, `why`, `update`, and every other pnpm verb work unchanged; pnpm must be on PATH. Relative path specs (`.`, `../plugin`, and their `file:`/`link:` forms) are anchored to the invoking directory first, so `add .` from a plugin checkout installs that checkout, not the profile. After every successful run, `dsh.plugins` is reconciled against the installed state: each dependency resolving to a package whose manifest declares `"dsh": { "patch": "./cordis.patch.yml" }` joins the layer stack (so an `update` that gains the declaration activates it), a patch-less dependency stays plain with a one-time warning, and a removed dependency leaves the stack. +`dsh plugin --profile ` initializes the profile when missing (shipped template, or `@deepseek-ai/dsh-base` alone for other names), then forwards `` to `pnpm` with the profile directory as working directory — `add`, `remove`, `why`, `update`, and every other pnpm verb work unchanged; pnpm must be on PATH. Relative path specs (`.`, `../plugin`, and their `file:`/`link:` forms) are anchored to the invoking directory first, so `add .` from a plugin checkout installs that checkout, not the profile. After every successful run, `dsh.profile.bundles` is reconciled against the installed state: each dependency resolving to a package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` joins the layer stack (so an `update` that gains the declaration activates it), a bundle-less dependency stays plain with a one-time warning, and a removed dependency leaves the stack. ```sh dsh plugin --profile tui add github:deepseek-harness/turtle-ui diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md index fd29f8f6a2..e508caaf05 100644 --- a/apps/cli/reference/README.zh.md +++ b/apps/cli/reference/README.zh.md @@ -6,7 +6,7 @@ ## Profile 启动 -`dsh --profile ` 启动位于 `$DSH_HOME/profiles/` 的 profile。生效配置树在空根节点之上按以下顺序逐层组合:profile manifest(元数据清单)的 `dsh.plugins` 列表所列的各个组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(各 profile 共享的机器本地偏好,因此优先级高于逐 profile 的层)、按 argv 顺序的各个 `--patch ` overlay,以及启动器 flag patch。后应用的层按行胜出;patch 替换目标行完整的 `config` 值,而不是深度合并各键,并且可以插入新行。配置解析、schema 校验、模块解析或插件启动失败会得到报告并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。 +`dsh --profile ` 启动位于 `$DSH_HOME/profiles/` 的 profile。生效配置树在空根节点之上按以下顺序逐层组合:profile manifest(元数据清单)的 `dsh.profile.bundles` 列表所列的各个组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(各 profile 共享的机器本地偏好,因此优先级高于逐 profile 的层)、按 argv 顺序的各个 `--patch ` overlay,以及启动器 flag patch。后应用的层按行胜出;patch 替换目标行完整的 `config` 值,而不是深度合并各键,并且可以插入新行。配置解析、schema 校验、模块解析或插件启动失败会得到报告并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。 组合包名称先从 dsh 安装解析,再从 profile 目录解析。因此内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`)总是来自与正在运行的 `dsh` 相同的安装;树外组合包来自 profile 由 pnpm 管理的 `node_modules`。任何 patch 行中的裸插件 `name` 通过 profile 目录的 Node 父目录逐级查找解析,该查找可达到持续维护的安装后备目录 `$DSH_HOME/profiles/node_modules`(安装的应用和组合包所依赖的每个包对应一个符号链接,每次启动时修复)。 @@ -25,7 +25,7 @@ dsh --profile web --patch ./extra.yml --dump-config ## 插件管理 -`dsh plugin --profile ` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `` 转发给 `pnpm`:`add`、`remove`、`why`、`update` 及其他所有 pnpm 子命令都照常可用;pnpm 必须在 PATH 上。相对路径 spec(`.`、`../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout,而不是 profile。每次成功运行后,`dsh.plugins` 都会与已安装状态对齐:每个解析到 manifest 中声明了 `"dsh": { "patch": "./cordis.patch.yml" }` 的包的依赖加入层栈(因此让包获得该声明的 `update` 会将其激活),没有 patch 的依赖保持为普通依赖并给出一次性警告,已移除的依赖则退出层栈。 +`dsh plugin --profile ` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `` 转发给 `pnpm`:`add`、`remove`、`why`、`update` 及其他所有 pnpm 子命令都照常可用;pnpm 必须在 PATH 上。相对路径 spec(`.`、`../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout,而不是 profile。每次成功运行后,`dsh.profile.bundles` 都会与已安装状态对齐:每个解析到 manifest 中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的包的依赖加入层栈(因此让包获得该声明的 `update` 会将其激活),没有组合包声明的依赖保持为普通依赖并给出一次性警告,已移除的依赖则退出层栈。 ```sh dsh plugin --profile tui add github:deepseek-harness/turtle-ui diff --git a/apps/cli/src/plugin.ts b/apps/cli/src/plugin.ts index 4370f86557..4a366a9a5d 100644 --- a/apps/cli/src/plugin.ts +++ b/apps/cli/src/plugin.ts @@ -1,12 +1,12 @@ /** * `dsh plugin --profile ` — profile plugin management as a * thin pnpm forwarder: initialize the profile on first use, run - * `pnpm ` in the profile directory, then reconcile the `dsh.plugins` - * bundle-layer list against the installed state (a dependency resolving to a - * package that declares `dsh.patch` joins the layer stack; a removed or - * patch-less dependency leaves it). Reconciling by installed state, not by - * dependency diff, means `update` activates a package that gained its - * `dsh.patch` in a newer version. + * `pnpm ` in the profile directory, then reconcile the + * `dsh.profile.bundles` layer list against the installed state (a dependency + * resolving to a package that declares `dsh.bundle` joins the layer stack; a + * removed or bundle-less dependency leaves it). Reconciling by installed + * state, not by dependency diff, means `update` activates a package that + * gained its `dsh.bundle` declaration in a newer version. * @module @deepseek-ai/dsh/plugin */ @@ -14,7 +14,7 @@ import { spawnSync } from 'node:child_process' import { existsSync } from 'node:fs' import { join, resolve } from 'node:path' import { - DEFAULT_PROFILE_PLUGINS, + DEFAULT_PROFILE_BUNDLES, initProfile, PROFILE_TEMPLATES, readProfileManifest, @@ -31,7 +31,7 @@ const NAME = 'dsh' * Whether a resolved dependency exports a profile patch, i.e. is a bundle. * @param packageName - the dependency's package name. * @param profileDir - the profile directory (resolution anchor). - * @returns true when the package manifest declares `dsh.patch`. + * @returns true when the package manifest declares `dsh.bundle`. */ function exportsPatch(packageName: string, profileDir: string): boolean { let dir: string @@ -41,25 +41,26 @@ function exportsPatch(packageName: string, profileDir: string): boolean { return false // pnpm reported success yet the package is unresolvable — treat as plain } const manifest = readProfileManifest(NAME, dir) - return manifest.dsh?.patch !== undefined + return manifest.dsh?.bundle?.patch !== undefined } /** - * Reconcile `dsh.plugins` against the installed state: pnpm has already - * written the real installed names (so a git/path/tarball/alias spec on the - * command line reconciles by its true package name) and materialized the - * packages. A dependency that resolves to a `dsh.patch`-declaring package - * joins the layer stack (appended in dependency order); a dependency-listed - * name that no longer does — removed, or the installed version dropped the - * declaration — leaves it. In-box bundles from the profile template are not - * dependencies and are never touched. Warns once per newly-added patch-less - * dependency (a plain library is fine; the warning is orientation). + * Reconcile `dsh.profile.bundles` against the installed state: pnpm has + * already written the real installed names (so a git/path/tarball/alias spec + * on the command line reconciles by its true package name) and materialized + * the packages. A dependency that resolves to a `dsh.bundle`-declaring + * package joins the layer stack (appended in dependency order); a + * dependency-listed name that no longer does — removed, or the installed + * version dropped the declaration — leaves it. In-box bundles from the + * profile template are not dependencies and are never touched. Warns once + * per newly-added bundle-less dependency (a plain library is fine; the + * warning is orientation). */ function reconcilePlugins(before: ProfileManifest, profileDir: string): void { const after = readProfileManifest(NAME, profileDir) const beforeDeps = new Set(Object.keys(before.dependencies ?? {})) const dependencies = Object.keys(after.dependencies ?? {}) - const plugins = after.dsh?.plugins ?? [] + const plugins = after.dsh?.profile?.bundles ?? [] let changed = false for (const packageName of dependencies) { const isBundle = exportsPatch(packageName, profileDir) @@ -68,7 +69,7 @@ function reconcilePlugins(before: ProfileManifest, profileDir: string): void { changed = true } else if (!isBundle && !beforeDeps.has(packageName)) { process.stderr.write( - `${NAME}: warning: ${packageName} declares no dsh.patch — installed as a plain dependency, not a profile layer ` + `${NAME}: warning: ${packageName} declares no dsh.bundle — installed as a plain dependency, not a profile layer ` + '(a later update that gains one activates it automatically)\n', ) } @@ -85,7 +86,7 @@ function reconcilePlugins(before: ProfileManifest, profileDir: string): void { } } if (!changed) return - after.dsh = { ...after.dsh, plugins } + after.dsh = { ...after.dsh, profile: { ...after.dsh?.profile, bundles: plugins } } writeProfileManifest(profileDir, after) } @@ -119,7 +120,7 @@ function anchorPathSpec(argument: string, cwd: string): string { export function runPlugin(profile: string, args: readonly string[]): number { const dir = resolveProfileDir(profile) if (!existsSync(join(dir, 'package.json'))) { - initProfile(dir, PROFILE_TEMPLATES[profile] ?? DEFAULT_PROFILE_PLUGINS) + initProfile(dir, PROFILE_TEMPLATES[profile] ?? DEFAULT_PROFILE_BUNDLES) process.stderr.write(`${NAME}: initialized profile ${profile} at ${dir}\n`) } const before = readProfileManifest(NAME, dir) diff --git a/apps/cli/src/profile-boot.ts b/apps/cli/src/profile-boot.ts index a316cec48a..44c943a7aa 100644 --- a/apps/cli/src/profile-boot.ts +++ b/apps/cli/src/profile-boot.ts @@ -1,6 +1,6 @@ /** * Shared profile boot for every `dsh` surface: resolve the profile, stack its - * patch layers (bundle layers in `dsh.plugins` order, the profile's own + * patch layers (bundle layers in `dsh.profile.bundles` order, the profile's own * `cordis.patch.yml`, `--patch` overlays, flag-derived patches, the telemetry * switch), mount the tree over the profile's empty root config, keep the * profile patch layer live, and wire fail-loud plus bounded shutdown. @@ -21,7 +21,7 @@ import { loadOverlayPatches, loadProfile, PROFILE_PATCH_FILENAME, - watchPersonalPatches, + watchUserPatches, type Profile, } from '@deepseek-ai/dsh-app-boot' import { resolveDshHome } from '@deepseek-ai/dsh-paths' @@ -51,7 +51,7 @@ const HEADLESS_ROW_ID = 'headless-runner' /** The empty root entry list every profile tree patches over. */ const PROFILE_ROOT_CONFIG = `# dsh profile root — an empty entry list. The tree is composed as patches: -# each bundle in package.json's dsh.plugins, then cordis.patch.yml, then any +# each bundle in package.json's dsh.profile.bundles, then cordis.patch.yml, then any # --patch overlays. Edit cordis.patch.yml, not this file. [] ` @@ -119,7 +119,7 @@ function allPatches(composed: ComposedProfile): PatchOptions[] { /** * Load `name` and compose its effective patch stack: bundle layers in - * `dsh.plugins` order, the profile's user layer, the home-level user layer + * `dsh.profile.bundles` order, the profile's user layer, the home-level user layer * (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply to * every profile, so it outranks the per-profile layer), `--patch` overlays, * then flag patches derived from the composed rows, then the telemetry @@ -251,12 +251,12 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con } await ctx.loader.create({ name: '@cordisjs/plugin-hmr', config: { root: [] } }) } - await watchPersonalPatches(ctx, { + await watchUserPatches(ctx, { binName: NAME, filename: composed.profile.patchPath, compose: composeLive, }) - await watchPersonalPatches(ctx, { + await watchUserPatches(ctx, { binName: NAME, filename: homePatchPath(), compose: composeLive, diff --git a/apps/cli/tests/built-bin.e2e.ts b/apps/cli/tests/built-bin.e2e.ts index 8e49206119..e8c7d18b18 100644 --- a/apps/cli/tests/built-bin.e2e.ts +++ b/apps/cli/tests/built-bin.e2e.ts @@ -8,6 +8,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' /** Published-entry acceptance for argument errors, profile lifecycle, and boot-free config dumps. */ const repoRoot = fileURLToPath(new URL('../../../', import.meta.url)) const dshBin = join(repoRoot, 'apps/cli/lib/bin.js') +const invalidProvider = fileURLToPath(new URL('./fixtures/invalid-provider.cordis.yml', import.meta.url)) async function runBuiltBin( args: readonly string[] = [], @@ -43,7 +44,7 @@ interface ProfileLifecycleFixture { /** * A minimal custom profile: one lifecycle-marker plugin bundle listed in - * dsh.plugins, no dsh-base — proving out-of-box composition machinery without + * dsh.profile.bundles, no dsh-base — proving out-of-box composition machinery without * booting the entire product tree. */ function createProfileLifecycleFixture(): ProfileLifecycleFixture { @@ -86,7 +87,7 @@ function createProfileLifecycleFixture(): ProfileLifecycleFixture { name: 'dsh-lifecycle-bundle', version: '0.0.0', type: 'module', - dsh: { patch: './cordis.patch.yml' }, + dsh: { bundle: { patch: './cordis.patch.yml' } }, }, undefined, 2)) const profileDir = join(home, 'profiles', 'lifecycle') mkdirSync(join(profileDir, 'node_modules'), { recursive: true }) @@ -94,7 +95,7 @@ function createProfileLifecycleFixture(): ProfileLifecycleFixture { name: 'dsh-profile-lifecycle', private: true, dependencies: {}, - dsh: { plugins: ['dsh-lifecycle-bundle'] }, + dsh: { profile: { bundles: ['dsh-lifecycle-bundle'] } }, }, undefined, 2)) // Hand-place the "installed" bundle where profile resolution finds it. writeFileSync(join(profileDir, 'cordis.patch.yml'), '[]\n') @@ -154,6 +155,26 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', } }, 30_000) + it('reports a patch-overlay boot failure without hanging', async () => { + // The HMR main watcher's initial scan once refreshed the include + // mid-initial-apply, deadlocking the failing apply's rollback against the + // refresh drain: dsh exited 13 with no diagnostic instead of settling + // ([Agent Note](../../../.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.md)). + const home = mkdtempSync(join(tmpdir(), 'dsh-invalid-patch-')) + try { + const result = await runBuiltBin(['--profile', 'web', '--patch', invalidProvider], { + DSH_HOME: home, + DEEPSEEK_API_KEY: 'keyless-invalid-config', + DSH_TELEMETRY_DISABLED: '1', + }) + expect(result.code).toBe(1) + expect(result.stdout).toBe('') + expect(result.stderr).toContain('llm-pi-ai') + } finally { + rmSync(home, { recursive: true, force: true }) + } + }, 30_000) + it('applies a custom profile bundle and disposes it on a startup-time signal', async () => { const fixture = createProfileLifecycleFixture() const child = startProfileLifecycle(fixture) @@ -232,7 +253,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', writeFileSync(join(checkout, 'package.json'), JSON.stringify({ name: 'anchored-bundle', version: '1.0.0', - dsh: { patch: './cordis.patch.yml' }, + dsh: { bundle: { patch: './cordis.patch.yml' } }, })) writeFileSync(join(checkout, 'cordis.patch.yml'), '[]\n') const result = await execa(process.execPath, [dshBin, 'plugin', '--profile', 'anchor', 'add', '.'], { @@ -246,20 +267,20 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', expect(result.exitCode).toBe(0) const manifest = JSON.parse(readFileSync(join(home, 'profiles', 'anchor', 'package.json'), 'utf8')) as { dependencies: Record - dsh: { plugins: string[] } + dsh: { profile: { bundles: string[] } } } expect(Object.keys(manifest.dependencies)).toEqual(['anchored-bundle']) - expect(manifest.dsh.plugins).toContain('anchored-bundle') + expect(manifest.dsh.profile.bundles).toContain('anchored-bundle') } finally { rmSync(home, { recursive: true, force: true }) rmSync(checkout, { recursive: true, force: true }) } }, 90_000) - it('activates a dependency that gained dsh.patch in a later update', async () => { + it('activates a dependency that gained dsh.bundle in a later update', async () => { // Reconcile runs against the INSTALLED state on every successful pnpm // run, so `update` (not only `add`) activates a package whose newer - // version declares dsh.patch. Simulated without a registry: hand-place + // version declares dsh.bundle. Simulated without a registry: hand-place // the installed package, flip its manifest, and run a benign pnpm verb. const home = mkdtempSync(join(tmpdir(), 'dsh-plugin-update-')) try { @@ -270,24 +291,24 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', name: 'dsh-profile-up', private: true, dependencies: { 'late-bundle': 'file:./late-bundle' }, - dsh: { plugins: ['@deepseek-ai/dsh-base'] }, + dsh: { profile: { bundles: ['@deepseek-ai/dsh-base'] } }, })) writeFileSync(join(profileDir, 'cordis.patch.yml'), '[]\n') // v1: no dsh manifest — a plain dependency. writeFileSync(join(installed, 'package.json'), JSON.stringify({ name: 'late-bundle', version: '1.0.0' })) const first = await runBuiltBin(['plugin', '--profile', 'up', 'root'], { DSH_HOME: home }) expect(first.code).toBe(0) - let manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8')) as { dsh: { plugins: string[] } } - expect(manifest.dsh.plugins).toEqual(['@deepseek-ai/dsh-base']) - // v2: the installed package now declares dsh.patch (an update landed). + let manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8')) as { dsh: { profile: { bundles: string[] } } } + expect(manifest.dsh.profile.bundles).toEqual(['@deepseek-ai/dsh-base']) + // v2: the installed package now declares dsh.bundle (an update landed). writeFileSync(join(installed, 'package.json'), JSON.stringify({ - name: 'late-bundle', version: '2.0.0', dsh: { patch: './cordis.patch.yml' }, + name: 'late-bundle', version: '2.0.0', dsh: { bundle: { patch: './cordis.patch.yml' } }, })) writeFileSync(join(installed, 'cordis.patch.yml'), '[]\n') const second = await runBuiltBin(['plugin', '--profile', 'up', 'root'], { DSH_HOME: home }) expect(second.code).toBe(0) - manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8')) as { dsh: { plugins: string[] } } - expect(manifest.dsh.plugins).toEqual(['@deepseek-ai/dsh-base', 'late-bundle']) + manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8')) as { dsh: { profile: { bundles: string[] } } } + expect(manifest.dsh.profile.bundles).toEqual(['@deepseek-ai/dsh-base', 'late-bundle']) } finally { rmSync(home, { recursive: true, force: true }) } diff --git a/apps/cli/tests/headless-shutdown.e2e.ts b/apps/cli/tests/headless-shutdown.e2e.ts index 55730ec3d2..cfa87b03de 100644 --- a/apps/cli/tests/headless-shutdown.e2e.ts +++ b/apps/cli/tests/headless-shutdown.e2e.ts @@ -73,7 +73,7 @@ async function runHeadlessPtySmoke(): Promise { name: 'dsh-profile-headless', private: true, dependencies: {}, - dsh: { plugins: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@deepseek-ai/dsh-headless'] }, + dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@deepseek-ai/dsh-headless'] } }, }, undefined, 2)) await writeFile(join(profileDir, 'cordis.patch.yml'), [ '- insert:', diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index 71a6f77219..2239ade66f 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -12,7 +12,7 @@ // masking its credential, without making a model call. // // Composition divergences from `dsh web`, all deliberate, all via include -// patches after the shipped surface overlay, over the SAME tree (never a +// patches after the shipped bundle layers, over the SAME tree (never a // second yml): temp persistenceRoot; host-level skill roots confined to the // temp workspace while project skill discovery remains real; workspace-context // disabled (recorded fixtures must not embed this repo's AGENTS.md); @@ -244,7 +244,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise` composes the profile's bundle patch layers (its manifest's `dsh.plugins` list, in order) over an empty root, then the profile's own `~/.dsh/profiles//cordis.patch.yml`, then each `--patch ` overlay, then CLI-flag patches. Later layers win per row. +`dsh --profile ` composes the profile's bundle patch layers (its manifest's `dsh.profile.bundles` list, in order) over an empty root, then the profile's own `~/.dsh/profiles//cordis.patch.yml`, then each `--patch ` overlay, then CLI-flag patches. Later layers win per row. A patch replaces a row's entire `config` value; it does not deep-merge keys. For example, patching `llm-deepseek` with only `config: { thinking: disabled }` also removes that row's configured `apiKey` and `baseURL`, so restate every key the row must retain. diff --git a/docs/user/guide/config.zh.md b/docs/user/guide/config.zh.md index 4bfab3c4a8..74e14e4e6e 100644 --- a/docs/user/guide/config.zh.md +++ b/docs/user/guide/config.zh.md @@ -51,7 +51,7 @@ Cordis 会并发启动同级配置项。插件通过 `inject` 声明必需服务 ## CLI 补丁层 -`dsh --profile ` 按该 profile 的 manifest(元数据清单)中 `dsh.plugins` 列表的顺序,在空根之上组合各组合包补丁层,随后依次应用该 profile 自己的 `~/.dsh/profiles//cordis.patch.yml`、每个 `--patch ` overlay,最后是 CLI(命令行界面)标志补丁。同一行以较后的层为准。 +`dsh --profile ` 按该 profile 的 manifest(元数据清单)中 `dsh.profile.bundles` 列表的顺序,在空根之上组合各组合包补丁层,随后依次应用该 profile 自己的 `~/.dsh/profiles//cordis.patch.yml`、每个 `--patch ` overlay,最后是 CLI(命令行界面)标志补丁。同一行以较后的层为准。 补丁会替换目标行的整个 `config` 值,而不是深度合并各个键。例如,只用 `config: { thinking: disabled }` 修补 `llm-deepseek`,也会移除该行原有的 `apiKey` 与 `baseURL`;因此必须重新写出该行需要保留的全部键。 diff --git a/packages/bundle/README.i18n.yaml b/packages/bundle/README.i18n.yaml index c8d9d871f4..27e50409cd 100644 --- a/packages/bundle/README.i18n.yaml +++ b/packages/bundle/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/bundle/README.md -README.md: 505750322d59eb524b1544ae439c54aea6376ec0 -README.zh.md: 4e6410d181e4810e98108453c4b91bce122e83e9 +README.md: 4759170435a80e85731446cef21d24fff2abed66 +README.zh.md: 1ef610a1b7b3c591c9a900e04f2d8096b0b086b9 diff --git a/packages/bundle/README.md b/packages/bundle/README.md index 505750322d..4759170435 100644 --- a/packages/bundle/README.md +++ b/packages/bundle/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Profile bundles: npm packages whose manifest declares `"dsh": { "patch": "./cordis.patch.yml" }`, making them installable patch layers for `dsh --profile` compositions ([profile contract](../ui/app-boot/README.md#profiles)). A bundle's substance is its patch list; some also ship runtime glue plugins their patch mounts. +Profile bundles: npm packages whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`, making them installable patch layers for `dsh --profile` compositions ([profile contract](../ui/app-boot/README.md#profiles)). A bundle's substance is its patch list; some also ship runtime glue plugins their patch mounts. | Package | Role | ctx key | |---|---|---| diff --git a/packages/bundle/README.zh.md b/packages/bundle/README.zh.md index 4e6410d181..1ef610a1b7 100644 --- a/packages/bundle/README.zh.md +++ b/packages/bundle/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -Profile 组合包:在 manifest(元数据清单)中声明 `"dsh": { "patch": "./cordis.patch.yml" }` 的 npm 包,因此可作为 patch 层安装进 `dsh --profile` 组合([profile 契约](../ui/app-boot/README.md#profiles))。组合包的实体是它的 patch 列表;有些组合包还附带由其 patch 挂载的运行时粘合插件。 +Profile 组合包:在 manifest(元数据清单)中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包,因此可作为 patch 层安装进 `dsh --profile` 组合([profile 契约](../ui/app-boot/README.md#profiles))。组合包的实体是它的 patch 列表;有些组合包还附带由其 patch 挂载的运行时粘合插件。 | 包 | 职责 | ctx key | |---|---|---| diff --git a/packages/bundle/base/README.i18n.yaml b/packages/bundle/base/README.i18n.yaml index bbc2e0f681..2ae7df0bdc 100644 --- a/packages/bundle/base/README.i18n.yaml +++ b/packages/bundle/base/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/bundle/base/README.md -README.md: 627dddc3808f67a2624e6e5b4d7f71c1617f227a -README.zh.md: 84f48357d7b66df334d9f78eff64b0c7de3080e1 +README.md: 301d397d4c87687b382665cf63af47ab5e3f85be +README.zh.md: f007bc817b6cbad84725fe8abe72549cf67d8cd7 diff --git a/packages/bundle/base/README.md b/packages/bundle/base/README.md index 627dddc380..301d397d4c 100644 --- a/packages/bundle/base/README.md +++ b/packages/bundle/base/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, tools, persistence, policy, settings/credentials, repository Plugins, telemetry — over the empty profile root, as the first layer of every profile's `dsh.plugins` list. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.patch` manifest field, never through code. +The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, tools, persistence, policy, settings/credentials, repository Plugins, telemetry — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code. The row set and its rationale are documented inline in the patch file; the [generated composition graph](../../../apps/cli/composition.md) renders it. diff --git a/packages/bundle/base/README.zh.md b/packages/bundle/base/README.zh.md index 84f48357d7..f007bc817b 100644 --- a/packages/bundle/base/README.zh.md +++ b/packages/bundle/base/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、工具、持久化、策略、settings/credentials、repository 插件、遥测——作为每个 profile 的 `dsh.plugins` 列表中的第一层。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.patch` 字段解析 patch,绝不通过代码。 +以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、工具、持久化、策略、settings/credentials、repository 插件、遥测——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析 patch,绝不通过代码。 行集合及其设计依据以行内注释写在 patch 文件里;[生成的组合图](../../../apps/cli/composition.md)负责渲染它。 diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index e28fa41163..95e169cabb 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -27,7 +27,9 @@ ], "license": "BSD-3-Clause", "dsh": { - "patch": "./cordis.patch.yml" + "bundle": { + "patch": "./cordis.patch.yml" + } }, "dependencies": { "@cordisjs/plugin-hmr": "workspace:*", diff --git a/packages/bundle/base/src/index.ts b/packages/bundle/base/src/index.ts index 88c1a2140d..ca7d8f9526 100644 --- a/packages/bundle/base/src/index.ts +++ b/packages/bundle/base/src/index.ts @@ -1,6 +1,6 @@ /** * @deepseek-ai/dsh-base — the shared dsh core as a profile bundle. The - * package's substance is `cordis.patch.yml`, declared by the `dsh.patch` + * package's substance is `cordis.patch.yml`, declared by the `dsh.bundle.patch` * manifest field and resolved by the profile composer through that field; * this module carries no runtime API. * @module @deepseek-ai/dsh-base diff --git a/packages/bundle/base/tests/base.spec.ts b/packages/bundle/base/tests/base.spec.ts index 7784530bd9..24ee2a1ba3 100644 --- a/packages/bundle/base/tests/base.spec.ts +++ b/packages/bundle/base/tests/base.spec.ts @@ -1,6 +1,6 @@ /** - * The bundle's substance is its patch file: the `dsh.patch` manifest field - * must name a real, parseable patch list. + * The bundle's substance is its patch file: the `dsh.bundle.patch` manifest + * field must name a real, parseable patch list. */ import { readFileSync } from 'node:fs' @@ -11,11 +11,11 @@ import * as yaml from 'js-yaml' import { entryListSchema } from '@cordisjs/plugin-include' describe('dsh-base bundle', () => { - it('declares a parseable patch list through the dsh.patch manifest field', () => { + it('declares a parseable patch list through the dsh.bundle.patch manifest field', () => { const root = fileURLToPath(new URL('..', import.meta.url)) - const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { dsh?: { patch?: string } } - expect(manifest.dsh?.patch).toBe('./cordis.patch.yml') - const parsed = yaml.load(readFileSync(resolve(root, manifest.dsh!.patch!), 'utf8'), { schema: entryListSchema }) + const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { dsh?: { bundle?: { patch?: string } } } + expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml') + const parsed = yaml.load(readFileSync(resolve(root, manifest.dsh!.bundle!.patch!), 'utf8'), { schema: entryListSchema }) expect(Array.isArray(parsed)).toBe(true) // The base layer is one insert list over the empty profile root. const rows = (parsed as { insert?: { id?: string }[] }[]).flatMap(patch => patch.insert ?? []) diff --git a/packages/bundle/headless/package.json b/packages/bundle/headless/package.json index f5a3892468..2b28423168 100644 --- a/packages/bundle/headless/package.json +++ b/packages/bundle/headless/package.json @@ -27,7 +27,9 @@ ], "license": "BSD-3-Clause", "dsh": { - "patch": "./cordis.patch.yml" + "bundle": { + "patch": "./cordis.patch.yml" + } }, "dependencies": { "schemastery": "^3.18.0" diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index d642ba9be1..1b812b9943 100644 --- a/packages/bundle/web-app/package.json +++ b/packages/bundle/web-app/package.json @@ -27,7 +27,9 @@ ], "license": "BSD-3-Clause", "dsh": { - "patch": "./cordis.patch.yml" + "bundle": { + "patch": "./cordis.patch.yml" + } }, "dependencies": { "@deepseek-ai/dsh-client-connection": "workspace:^", diff --git a/packages/bundle/web-app/src/index.ts b/packages/bundle/web-app/src/index.ts index ccfa375b73..b817642be8 100644 --- a/packages/bundle/web-app/src/index.ts +++ b/packages/bundle/web-app/src/index.ts @@ -1,6 +1,6 @@ /** * @deepseek-ai/dsh-web-app — the browser-surface bundle's runtime glue plugin - * plus the bundle patch (`cordis.patch.yml`, declared by the `dsh.patch` + * plus the bundle patch (`cordis.patch.yml`, declared by the `dsh.bundle.patch` * manifest field). The plugin owns what used to be launcher code: it resolves * the built frontend dist (workspace knowledge of this bundle, never user * config), mounts the `frontend-static` fallback owner over it, registers the diff --git a/packages/ui/app-boot/README.i18n.yaml b/packages/ui/app-boot/README.i18n.yaml index 4fd8a12e8b..398ec6e923 100644 --- a/packages/ui/app-boot/README.i18n.yaml +++ b/packages/ui/app-boot/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/ui/app-boot/README.md -README.md: 49ad8270ffc62974023cdeba17f3f1356aaf27ae -README.zh.md: 8d01d850d467eb6e21789201fbdef6d79fcc68f0 +README.md: cdd78047b6ad71148c6ebeba598b63b4ae4cfa7b +README.zh.md: ee2b07884e68510e2b59b9f2c27053c263d15f1a diff --git a/packages/ui/app-boot/README.md b/packages/ui/app-boot/README.md index 49ad8270ff..cdd78047b6 100644 --- a/packages/ui/app-boot/README.md +++ b/packages/ui/app-boot/README.md @@ -15,8 +15,8 @@ Shared boot glue for the app bins ([`dsh`](../../../apps/cli/README.md), [`dsh-c | `loadOptionalPatches(binName, file)` | Parse an optional patch-list file (a profile's `cordis.patch.yml`) — a top-level YAML array of include `PatchOptions` (id-targeted config overrides, `insert` lists, `!!js` allowed); absent file → `undefined`, an unreadable/unparsable/non-array file throws | | `loadOverlayPatches(binName, file)` | Parse a required patch-list file with the same shape; a missing file also throws, because the caller named it | | `mountRootInclude(ctx, absoluteConfigPath, patches?)` | Mount the statically imported Include builtin and retain the exact root entry used by user patch-layer HMR | -| `watchPersonalPatches(ctx, options)` | Register the named patch file with the existing Cordis HMR service; each add/change/removal transactionally recomposes the full patch list through the caller's `compose` closure (app-owned layers around the current user layer) and returns an async disposer | -| `resolveProfileDir` / `initProfile` / `loadProfile` / `readProfileManifest` / `writeProfileManifest` / `resolveBundleDir` / `composeEntries` / `healProfilesModuleFallback` / `PROFILE_TEMPLATES` / `DEFAULT_PROFILE_PLUGINS` / `PROFILES_DIR` / `PROFILE_PATCH_FILENAME` | Profile machinery (see [Profiles](#profiles)) | +| `watchUserPatches(ctx, options)` | Register the named patch file with the existing Cordis HMR service; each add/change/removal transactionally recomposes the full patch list through the caller's `compose` closure (app-owned layers around the current user layer) and returns an async disposer | +| `resolveProfileDir` / `initProfile` / `loadProfile` / `readProfileManifest` / `writeProfileManifest` / `resolveBundleDir` / `composeEntries` / `healProfilesModuleFallback` / `PROFILE_TEMPLATES` / `DEFAULT_PROFILE_BUNDLES` / `PROFILES_DIR` / `PROFILE_PATCH_FILENAME` | Profile machinery (see [Profiles](#profiles)) | | `boot(binName, absoluteConfigPath, patches?, prepare?)` | Create the root context, expose `dshHomePath(...segments)` to Loader `!!js` config expressions, install Loader, run optional host preparation before config-tree entries mount (`prepare` may use Loader and provide launcher-owned context slots), then mount and await the include tree, assert entries loaded and activated, and return the root context — or dispose the partial context and reject a labelled error | | `renderConfigDump(binName, absoluteConfigPath, layers, warn?)` | Compose the base config and labeled overlay layers offline — the include's own parser and patch algorithm (`entryListSchema`/`applyEntryPatches`), so the result equals what `boot()` mounts — and render YAML with `!!js` expressions verbatim; each run of same-provenance rows is preceded by a `# ==` comment naming the contributing file and the layers that patched it, keeping the output one loadable document; a patch matching no row goes to `warn` with its layer label (default: one stderr line), read/parse/shape failures throw | | `addHarnessSourceSection(ctx, sourceRoot)` | Add a global `harness:source` prompt section (ordered just after the harness identity, before the persona) telling the agent the on-disk path to the DSH implementation checkout while warning it not to infer the current working directory from that path and to use `pwd` instead; a no-op returning `undefined` when the booted tree has no `systemPrompt` service. The section is registered against that service's fiber, so a dev HMR reload of the system prompt drops it until the next boot | @@ -32,14 +32,14 @@ This package carries no loader hooks and no dev-mode surface. The [`dsh` app](.. ## Profiles -A profile is a directory under `$DSH_HOME/profiles/` (the Harness home resolves through [`resolveDshHome`](../../util/paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the ordered `dsh.plugins` bundle-layer list — and the user's own `cordis.patch.yml`. A bundle is an npm package whose manifest declares `"dsh": { "patch": "./cordis.patch.yml" }`; `loadProfile` resolves each `dsh.plugins` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a patch declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps can never drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory — one symlink per package the installation's app and bundles depend on — so bare plugin names in any profile resolve through Node's ordinary parent-walk without pnpm ever managing in-box packages. `PROFILE_TEMPLATES` (`web`, `headless`) auto-initialize on first use; other names fail loud until `initProfile` creates them (the `dsh plugin` path). +A profile is a directory under `$DSH_HOME/profiles/` (the Harness home resolves through [`resolveDshHome`](../../util/paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list — and the user's own `cordis.patch.yml`. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps can never drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory — one symlink per package the installation's app and bundles depend on — so bare plugin names in any profile resolve through Node's ordinary parent-walk without pnpm ever managing in-box packages. `PROFILE_TEMPLATES` (`web`, `headless`) auto-initialize on first use; other names fail loud until `initProfile` creates them (the `dsh plugin` path). User-level machine-local preferences also live in the Harness home: - **`.env`** — the credential store of [`dsh-credentials-local`](../../credentials/credentials-local/README.md), read by that provider alone. No surface hoists it into `process.env`: doing so would make every stored key look like a read-only launch override on the next run, blocking rotation from the Web settings page. The environment layers are the ambient one and the invoking directory's `.env` (loaded by the bin; `process.loadEnvFile` never overrides), and a composition without the credential provider keeps resolving keys from those alone. - **`cordis.patch.yml`** (home level) and **`profiles//cordis.patch.yml`** — the user patch layers, applied after every bundle layer (per-profile first, then the home-level file, which therefore outranks it): an id-targeted patch replaces the named entry's whole `config` (restate unchanged fields), `insert` adds entries, and `!!js` expressions interpolate at mount. A patch naming an entry id absent from the composed tree is a stderr warning. An empty or comments-only file throws (it parses to nothing, not to a list); disable the layer with `[]`. -Long-lived surfaces keep `cordis.patch.yml` live through `watchPersonalPatches`; one-shot runs read only the startup value. The watcher targets the exact path even when the file or immediate parent does not exist, serializes bursts, and recomposes the user patches inside the caller's layer order (bundle layers below, overlay/flag patches above). A rejected read, parse, or Loader candidate leaves the last good tree running and the HMR service broadcasts `hmr/config-update-failed(filename, Error)` after logging it; observer failures are contained. Disposing the context closes the watcher and drains an active refresh. +Long-lived surfaces keep `cordis.patch.yml` live through `watchUserPatches`; one-shot runs read only the startup value. The watcher targets the exact path even when the file or immediate parent does not exist, serializes bursts, and recomposes the user patches inside the caller's layer order (bundle layers below, overlay/flag patches above). A rejected read, parse, or Loader candidate leaves the last good tree running and the HMR service broadcasts `hmr/config-update-failed(filename, Error)` after logging it; observer failures are contained. Disposing the context closes the watcher and drains an active refresh. ## Model Experience diff --git a/packages/ui/app-boot/README.zh.md b/packages/ui/app-boot/README.zh.md index 8d01d850d4..ee2b07884e 100644 --- a/packages/ui/app-boot/README.zh.md +++ b/packages/ui/app-boot/README.zh.md @@ -15,8 +15,8 @@ | `loadOptionalPatches(binName, file)` | 解析一份可选的 patch 列表文件(即 profile 的 `cordis.patch.yml`):其顶层是一个 YAML 数组,内容为 include 的 `PatchOptions`(按 id 定位的配置覆盖、`insert` 列表,允许 `!!js`);文件不存在时返回 `undefined`,文件不可读、不可解析或内容不是数组时抛出异常 | | `loadOverlayPatches(binName, file)` | 解析一份形状相同的必需 patch 列表文件;文件缺失同样抛出异常,因为该文件是调用方指名的 | | `mountRootInclude(ctx, absoluteConfigPath, patches?)` | 挂载静态导入的 Include builtin,并保留用户 patch 层 HMR(热模块替换)使用的确切根配置项 | -| `watchPersonalPatches(ctx, options)` | 向现有 Cordis HMR 服务注册指名的 patch 文件;每次新增、变更或移除都会通过调用方的 `compose` 闭包(应用自有层围绕当前用户层)以事务方式重新组合完整 patch 列表,并返回异步 disposer | -| `resolveProfileDir` / `initProfile` / `loadProfile` / `readProfileManifest` / `writeProfileManifest` / `resolveBundleDir` / `composeEntries` / `healProfilesModuleFallback` / `PROFILE_TEMPLATES` / `DEFAULT_PROFILE_PLUGINS` / `PROFILES_DIR` / `PROFILE_PATCH_FILENAME` | Profile 机制(见 [Profile](#profiles)) | +| `watchUserPatches(ctx, options)` | 向现有 Cordis HMR 服务注册指名的 patch 文件;每次新增、变更或移除都会通过调用方的 `compose` 闭包(应用自有层围绕当前用户层)以事务方式重新组合完整 patch 列表,并返回异步 disposer | +| `resolveProfileDir` / `initProfile` / `loadProfile` / `readProfileManifest` / `writeProfileManifest` / `resolveBundleDir` / `composeEntries` / `healProfilesModuleFallback` / `PROFILE_TEMPLATES` / `DEFAULT_PROFILE_BUNDLES` / `PROFILES_DIR` / `PROFILE_PATCH_FILENAME` | Profile 机制(见 [Profile](#profiles)) | | `boot(binName, absoluteConfigPath, patches?, prepare?)` | 创建根上下文,向 Loader `!!js` 配置表达式暴露 `dshHomePath(...segments)` 并安装 Loader,在配置树条目挂载前执行可选的宿主准备操作(`prepare` 可以使用 Loader,也可以提供由启动器拥有的上下文插槽),再挂载并等待 include 树结算,断言所有条目均已加载并激活,最后返回根上下文——失败时 dispose(资源释放)部分构造的上下文,并以带标签的错误 reject | | `renderConfigDump(binName, absoluteConfigPath, layers, warn?)` | 离线合成基础配置与带标签的覆盖层——使用 include 自己的解析器和补丁算法(`entryListSchema`/`applyEntryPatches`),因此结果与 `boot()` 挂载的内容一致——并渲染为 YAML,`!!js` 表达式原样保留;每段来源相同的连续行之前都有一条 `# ==` 注释,标明贡献该段的文件以及修补过它的层,输出仍是一份可加载的文档;未匹配到行的补丁连同其层标签交给 `warn`(默认:一行 stderr),读取/解析/形状失败则抛出 | | `addHarnessSourceSection(ctx, sourceRoot)` | 添加全局 `harness:source` 提示词段落(顺序紧随 harness 身份、位于 persona 之前),告知 agent(智能体)DSH 实现代码 checkout 的磁盘路径,同时提醒它不得据此推断当前工作目录,而应使用 `pwd`;如果已启动树没有此项服务,则不执行操作并返回 `undefined`。这里的服务是 `systemPrompt`;该段落注册到它的 fiber,因此开发环境 HMR(热模块替换)重新加载系统提示词后,它会消失直至下次启动 | @@ -32,14 +32,14 @@ Loader 并发挂载各个条目,因此当其他环节失败时,某个界面 ## Profile -profile 是位于 `$DSH_HOME/profiles/` 下的目录(Harness home 由 [`resolveDshHome`](../../util/paths/README.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上有序的 `dsh.plugins` 组合包层列表)和用户自己的 `cordis.patch.yml`。组合包是在 manifest 中声明 `"dsh": { "patch": "./cordis.patch.yml" }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.plugins` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有 patch 声明则大声失败。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录(安装目录的应用与各组合包依赖的每个包对应一个符号链接),使任意 profile 中的裸插件名都能经 Node 常规的逐级向上查找解析,而 pnpm 从不管理随安装内置的包。`PROFILE_TEMPLATES`(`web`、`headless`)在首次使用时自动初始化;其他名称在 `initProfile` 创建之前都会大声失败(即 `dsh plugin` 路径)。 +profile 是位于 `$DSH_HOME/profiles/` 下的目录(Harness home 由 [`resolveDshHome`](../../util/paths/README.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表)和用户自己的 `cordis.patch.yml`。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则大声失败。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录(安装目录的应用与各组合包依赖的每个包对应一个符号链接),使任意 profile 中的裸插件名都能经 Node 常规的逐级向上查找解析,而 pnpm 从不管理随安装内置的包。`PROFILE_TEMPLATES`(`web`、`headless`)在首次使用时自动初始化;其他名称在 `initProfile` 创建之前都会大声失败(即 `dsh plugin` 路径)。 用户级的机器本地偏好同样位于 Harness home 中: - **`.env`**:[`dsh-credentials-local`](../../credentials/credentials-local/README.md) 的凭据存储,只由该 provider 读取。没有任何表层会把它提升进 `process.env`:那样做会让每个已存密钥在下次运行时看起来都像只读的启动时覆盖,从而阻断从 Web 设置页面轮换密钥。环境层次由环境中的值与调用目录的 `.env` 构成(由 bin 加载;`process.loadEnvFile` 从不覆盖已有值),没有凭据 provider 的组合仍然只从这两者解析密钥。 - **`cordis.patch.yml`**(home 级)与 **`profiles//cordis.patch.yml`**:用户 patch 层,应用在所有组合包层之后(先应用逐 profile 的文件,再应用 home 级文件,因此后者优先级更高):按 id 定位的 patch 会替换对应条目的整个 `config`(未改字段也要重述),`insert` 会添加条目,`!!js` 表达式则在挂载时插值。如果 patch 指定的条目 id 不在组合后的树中,则输出一条 stderr 警告。空文件或仅含注释的文件会抛出异常(其解析结果为空,而不是列表);如需禁用该层,请使用 `[]`。 -长期运行的 surface 会持续应用 `cordis.patch.yml` 的变更,具体由 `watchPersonalPatches` 负责;一次性运行只读取启动时的值。即使该文件或其直接父目录不存在,watcher 仍会监视确切路径;它会串行处理突发变更,并按调用方的层次顺序重新组合用户 patch(组合包层在下、overlay/标志 patch 在上)。读取失败、解析失败或 Loader 候选被拒时,最后一个可用树会继续运行;HMR 服务记录错误后广播 `hmr/config-update-failed(filename, Error)`,并隔离 observer 失败。上下文 dispose 时会关闭 watcher,并等待进行中的刷新结束。 +长期运行的 surface 会持续应用 `cordis.patch.yml` 的变更,具体由 `watchUserPatches` 负责;一次性运行只读取启动时的值。即使该文件或其直接父目录不存在,watcher 仍会监视确切路径;它会串行处理突发变更,并按调用方的层次顺序重新组合用户 patch(组合包层在下、overlay/标志 patch 在上)。读取失败、解析失败或 Loader 候选被拒时,最后一个可用树会继续运行;HMR 服务记录错误后广播 `hmr/config-update-failed(filename, Error)`,并隔离 observer 失败。上下文 dispose 时会关闭 watcher,并等待进行中的刷新结束。 ## 模型体验 diff --git a/packages/ui/app-boot/src/index.ts b/packages/ui/app-boot/src/index.ts index 1e52b92954..e14b249f5c 100644 --- a/packages/ui/app-boot/src/index.ts +++ b/packages/ui/app-boot/src/index.ts @@ -1,7 +1,7 @@ /** * Shared boot glue for the app bins (`dsh`, `dsh-cli-demo`, `dsh-acp-demo`): load the gitignored * `.env`, install the fail-loud Loader guards, resolve the config path (snapshot-aware), load the - * optional personal overlay patches from the Harness home (`~/.dsh`), expose its path resolver to + * optional user patch layers from the Harness home (`~/.dsh`), expose its path resolver to * config expressions, and drive the Cordis Loader against a leaf `cordis.yml` until the tree settles. * @module @deepseek-ai/dsh-app-boot */ @@ -27,7 +27,7 @@ declare module 'cordis' { export { composeEntries, - DEFAULT_PROFILE_PLUGINS, + DEFAULT_PROFILE_BUNDLES, healProfilesModuleFallback, initProfile, loadProfile, @@ -38,7 +38,9 @@ export { resolveBundleDir, resolveProfileDir, writeProfileManifest, + type DshBundleManifest, type DshManifestSection, + type DshProfileManifest, type Profile, type ProfileLayer, type ProfileManifest, @@ -89,12 +91,12 @@ const bootstrapIncludes = new WeakMap() // The include's YAML dialect (`!!js` scalars become expression nodes the // Loader interpolates against each entry's context at mount time), imported // from the include itself so patch parsing and config dumping can never drift -// from what the include mounts. Personal patches share it so they may +// from what the include mounts. User patch layers share it so they may // reference `process.env`. -const personalPatchesSchema = entryListSchema +const userPatchesSchema = entryListSchema /** Options for live user patch-layer reconciliation. */ -export interface PersonalPatchWatchOptions { +export interface UserPatchWatchOptions { /** Diagnostic prefix used by {@link loadOptionalPatches}. */ binName: string /** Absolute path of the watched patch file (a profile's `cordis.patch.yml`). */ @@ -106,7 +108,7 @@ export interface PersonalPatchWatchOptions { * overlay/flag patches above). Identity when omitted: the user layer * is the whole patch list. */ - compose?: (personalPatches: PatchOptions[]) => PatchOptions[] + compose?: (userPatches: PatchOptions[]) => PatchOptions[] } /** @@ -116,22 +118,22 @@ export interface PersonalPatchWatchOptions { * @returns an asynchronous disposer after the exact-path watcher is ready. * @throws when HMR or the root Include is absent, watcher setup fails, or initial path resolution fails. */ -export async function watchPersonalPatches( +export async function watchUserPatches( ctx: Context, - options: PersonalPatchWatchOptions, + options: UserPatchWatchOptions, ): Promise<() => Promise> { const { binName, filename, compose = (patches: PatchOptions[]) => patches } = options const hmr = ctx.get('hmr') - if (hmr === undefined) throw new Error(`${binName}: personal config watching requires the Cordis HMR service`) + if (hmr === undefined) throw new Error(`${binName}: user patch-layer watching requires the Cordis HMR service`) const entry = bootstrapIncludes.get(ctx) - if (entry === undefined) throw new Error(`${binName}: personal config watching requires the root Include entry`) + if (entry === undefined) throw new Error(`${binName}: user patch-layer watching requires the root Include entry`) const register = hmr.registerConfig(filename, async () => { // Re-read the include's non-patch options per refresh: a writer that // updates the root Include's other options between refreshes (none exists - // today) must not have them silently reverted by a personal reload. + // today) must not have them silently reverted by a user-layer reload. const { patches: _previousPatches, ...includeConfig } = entry.options.config as Include.Config - const personalPatches = loadOptionalPatches(binName, filename) ?? [] - const patches = compose(personalPatches) + const userPatches = loadOptionalPatches(binName, filename) ?? [] + const patches = compose(userPatches) await entry.update({ config: { ...includeConfig, @@ -201,7 +203,7 @@ export function loadOverlayPatches(binName: string, file: string): PatchOptions[ * @param binName - the diagnostic prefix on the thrown error. * @param file - the source path, quoted in errors. * @param content - the file's text. - * @param label - what to call this list in errors (`personal patches`, `overlay`). + * @param label - what to call this list in errors (`patches`, `overlay`). * @returns the parsed patch list. */ function parsePatchList( @@ -209,7 +211,7 @@ function parsePatchList( ): PatchOptions[] { let parsed: unknown try { - parsed = yaml.load(content, { schema: personalPatchesSchema }) + parsed = yaml.load(content, { schema: userPatchesSchema }) } catch (error) { throw new Error(`${binName}: failed to parse ${label} ${file}: ${String(error)}`) } @@ -360,10 +362,10 @@ function groupedDump( } /** - * Mount and remember the exact root Include entry used by app boot and personal-config HMR. + * Mount and remember the exact root Include entry used by app boot and user patch-layer HMR. * @param ctx - context carrying an initialized Loader service. * @param absoluteConfigPath - absolute YAML or JSON configuration path. - * @param patches - initial app and personal patches, applied in order. + * @param patches - initial app and user patches, applied in order. * @returns the created root Include entry, or `undefined` when a surface * disposed the whole tree (taking the Loader service with it) while the * transactional create was still settling entry lifecycle. diff --git a/packages/ui/app-boot/src/profile.ts b/packages/ui/app-boot/src/profile.ts index 47840871bc..c353f18bef 100644 --- a/packages/ui/app-boot/src/profile.ts +++ b/packages/ui/app-boot/src/profile.ts @@ -3,11 +3,12 @@ * `dsh --profile` launcher family. * * A profile is a directory under `$DSH_HOME/profiles/` holding a - * `package.json` (out-of-tree plugin dependencies plus the ordered - * `dsh.plugins` bundle list) and a `cordis.patch.yml` (the user's own patch - * layer, applied after every bundle layer). Bundles are npm packages whose - * manifest declares `"dsh": { "patch": "./cordis.patch.yml" }`; the tree is - * composed by applying each bundle's patch list in `dsh.plugins` order over + * `package.json` (out-of-tree plugin dependencies plus the profile manifest + * `dsh.profile` with its ordered `bundles` list) and a `cordis.patch.yml` + * (the user's own patch layer, applied after every bundle layer). Bundles are + * npm packages whose manifest declares + * `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; the tree is + * composed by applying each bundle's patch list in `dsh.profile.bundles` order over * an empty entry list, then the profile's own patches, then any launcher * layers (`--patch` files and flag-derived patches). * @@ -37,12 +38,28 @@ export const PROFILES_DIR = 'profiles' /** The user patch layer inside a profile directory (hot-reloaded on long-lived surfaces). */ export const PROFILE_PATCH_FILENAME = 'cordis.patch.yml' -/** The `dsh`-owned manifest section of a profile's or bundle's package.json. */ +/** The bundle half of the `dsh` manifest section: what a bundle package exports. */ +export interface DshBundleManifest { + /** The patch layer this bundle exports, relative to its package root. */ + patch: string +} + +/** The profile half of the `dsh` manifest section: what a profile directory composes. */ +export interface DshProfileManifest { + /** Ordered bundle layer list (package names). */ + bundles?: string[] +} + +/** + * The `dsh`-owned manifest section of a package.json. The nested key names + * the manifest kind: a bundle package declares `bundle`, a profile directory + * declares `profile`; nothing declares both. + */ export interface DshManifestSection { - /** Bundle manifest: profile patch this package exports, relative to its root. */ - patch?: string - /** Profile manifest: ordered bundle layer list (package names). */ - plugins?: string[] + /** Present on bundle packages only. */ + bundle?: DshBundleManifest + /** Present on profile manifests only. */ + profile?: DshProfileManifest } /** The slice of package.json both profiles and bundles use. */ @@ -55,7 +72,7 @@ export interface ProfileManifest { /** One resolved bundle layer of a profile. */ export interface ProfileLayer { - /** The bundle's package name, as listed in `dsh.plugins`. */ + /** The bundle's package name, as listed in `dsh.profile.bundles`. */ packageName: string /** Absolute directory of the resolved bundle package. */ packageDir: string @@ -71,7 +88,7 @@ export interface Profile { name: string /** Absolute profile directory. */ dir: string - /** Bundle layers in `dsh.plugins` order. */ + /** Bundle layers in `dsh.profile.bundles` order. */ layers: ProfileLayer[] /** Absolute path of the profile's own patch file. */ patchPath: string @@ -101,7 +118,7 @@ export const PROFILE_TEMPLATES: Record = { } /** The bundle list a `dsh plugin` init uses for a name with no shipped template. */ -export const DEFAULT_PROFILE_PLUGINS: readonly string[] = ['@deepseek-ai/dsh-base'] +export const DEFAULT_PROFILE_BUNDLES: readonly string[] = ['@deepseek-ai/dsh-base'] const PROFILE_PATCH_TEMPLATE = `# Your patch layer for this dsh profile, applied after every bundle layer: # a top-level YAML array of loader patch entries (id-targeted config @@ -126,9 +143,9 @@ autoInstallPeers: false * pnpm settings out-of-tree plugins need. Existing files are never touched, * so re-running is a no-op on an initialized profile. * @param dir - the profile directory from {@link resolveProfileDir}. - * @param plugins - the initial `dsh.plugins` bundle list. + * @param bundles - the initial `dsh.profile.bundles` layer list. */ -export function initProfile(dir: string, plugins: readonly string[]): void { +export function initProfile(dir: string, bundles: readonly string[]): void { mkdirSync(dir, { recursive: true }) const manifestPath = join(dir, 'package.json') if (!existsSync(manifestPath)) { @@ -136,7 +153,7 @@ export function initProfile(dir: string, plugins: readonly string[]): void { name: `dsh-profile-${basename(dir)}`, private: true, dependencies: {}, - dsh: { plugins: [...plugins] }, + dsh: { profile: { bundles: [...bundles] } }, } writeFileSync(manifestPath, JSON.stringify(manifest, undefined, 2) + '\n') } @@ -287,7 +304,7 @@ function packageDirFromAnchor(anchor: string, packageName: string): string | und * the same installation as the running dsh, never from a profile-local copy. * Resolution does not require the package to export `./package.json`. * @param binName - the diagnostic prefix on the thrown error. - * @param packageName - the bundle's package name from `dsh.plugins`. + * @param packageName - the bundle's package name from `dsh.profile.bundles`. * @param installAnchor - absolute path of a file inside the dsh app package (its package.json). * @param profileDir - the profile directory (second anchor). * @returns the bundle package's absolute directory. @@ -306,10 +323,10 @@ export function resolveBundleDir( } /** - * Load a profile: resolve every `dsh.plugins` bundle to its patch layer and - * parse the profile's own patch file. A listed bundle without a `dsh.patch` - * manifest field fails loud — naming a patch-less package as a layer is a - * misconfiguration, not "no patches". + * Load a profile: resolve every `dsh.profile.bundles` entry to its patch + * layer and parse the profile's own patch file. A listed bundle without a + * `dsh.bundle` manifest fails loud — naming a bundle-less package as a layer + * is a misconfiguration, not "no patches". * @param binName - the diagnostic prefix on thrown errors. * @param name - the profile name. * @param installAnchor - absolute path of the dsh app's package.json (first resolution anchor). @@ -335,13 +352,13 @@ export function loadProfile( } const manifest = readProfileManifest(binName, dir) // A hand-written profile manifest may omit the dsh section entirely. - const plugins = manifest.dsh?.plugins ?? [] - const layers = plugins.map((packageName): ProfileLayer => { + const bundles = manifest.dsh?.profile?.bundles ?? [] + const layers = bundles.map((packageName): ProfileLayer => { const packageDir = resolveBundleDir(binName, packageName, installAnchor, dir) const bundleManifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')) as ProfileManifest - const declared = bundleManifest.dsh?.patch + const declared = bundleManifest.dsh?.bundle?.patch if (declared === undefined) { - throw new Error(`${binName}: profile bundle ${JSON.stringify(packageName)} declares no dsh.patch in its package.json`) + throw new Error(`${binName}: profile bundle ${JSON.stringify(packageName)} declares no dsh.bundle in its package.json`) } const patchPath = join(packageDir, declared) return { packageName, packageDir, patchPath, patches: loadOverlayPatches(binName, patchPath) } diff --git a/packages/ui/app-boot/tests/profile.spec.ts b/packages/ui/app-boot/tests/profile.spec.ts index 0419721034..f0bd6f5da7 100644 --- a/packages/ui/app-boot/tests/profile.spec.ts +++ b/packages/ui/app-boot/tests/profile.spec.ts @@ -37,7 +37,7 @@ function stageInstallation(bundles: Record { const dir = resolveProfileDir('tui', home) initProfile(dir, ['@deepseek-ai/dsh-base']) const manifest = readProfileManifest('t', dir) - expect(manifest.dsh?.plugins).toEqual(['@deepseek-ai/dsh-base']) + expect(manifest.dsh?.profile?.bundles).toEqual(['@deepseek-ai/dsh-base']) expect(readFileSync(join(dir, PROFILE_PATCH_FILENAME), 'utf8')).toContain('[]') expect(readFileSync(join(dir, 'pnpm-workspace.yaml'), 'utf8')).toContain('nodeLinker: hoisted') // Re-init keeps user edits. writeFileSync(join(dir, PROFILE_PATCH_FILENAME), '- id: x\n config: {}\n') initProfile(dir, ['other']) - expect(readProfileManifest('t', dir).dsh?.plugins).toEqual(['@deepseek-ai/dsh-base']) + expect(readProfileManifest('t', dir).dsh?.profile?.bundles).toEqual(['@deepseek-ai/dsh-base']) expect(readFileSync(join(dir, PROFILE_PATCH_FILENAME), 'utf8')).toContain('- id: x') }) }) @@ -75,8 +75,8 @@ describe('initProfile', () => { describe('manifest round-trip', () => { it('writes and reads back, and fails loud on a broken manifest', () => { const dir = tmp() - writeProfileManifest(dir, { name: 'p', dsh: { plugins: ['a'] } }) - expect(readProfileManifest('t', dir).dsh?.plugins).toEqual(['a']) + writeProfileManifest(dir, { name: 'p', dsh: { profile: { bundles: ['a'] } } }) + expect(readProfileManifest('t', dir).dsh?.profile?.bundles).toEqual(['a']) writeFileSync(join(dir, 'package.json'), '[]') expect(() => readProfileManifest('t', dir)).toThrow('must hold a JSON object') expect(() => readProfileManifest('t', join(dir, 'nope'))).toThrow('failed to read profile manifest') @@ -109,7 +109,7 @@ describe('resolveBundleDir', () => { name: 'sealed-bundle', version: '0.0.0', exports: { '.': './index.js' }, - dsh: { patch: './cordis.patch.yml' }, + dsh: { bundle: { patch: './cordis.patch.yml' } }, })) writeFileSync(join(dir, 'index.js'), '') writeFileSync(join(dir, 'cordis.patch.yml'), '[]\n') @@ -118,7 +118,7 @@ describe('resolveBundleDir', () => { }) describe('loadProfile', () => { - it('resolves each dsh.plugins bundle to its patch layer in order, plus the user layer', () => { + it('resolves each dsh.profile.bundles entry to its patch layer in order, plus the user layer', () => { const anchor = stageInstallation({ 'bundle-a': { patch: '- insert:\n - id: a\n name: pkg-a\n' }, 'bundle-b': { patch: '- id: a\n config:\n v: 2\n' }, @@ -157,16 +157,16 @@ describe('loadProfile', () => { } catch { // Resolution failure is the plain-Node outcome for this empty anchor. } - expect(readProfileManifest('t', resolveProfileDir('web', home)).dsh?.plugins) + expect(readProfileManifest('t', resolveProfileDir('web', home)).dsh?.profile?.bundles) .toEqual([...PROFILE_TEMPLATES.web ?? []]) }) - it('fails loud when a listed bundle declares no dsh.patch', () => { + it('fails loud when a listed bundle declares no dsh.bundle', () => { const anchor = stageInstallation({ 'not-a-bundle': {} }) const home = tmp() const dir = resolveProfileDir('demo', home) initProfile(dir, ['not-a-bundle']) - expect(() => loadProfile('t', 'demo', anchor, home)).toThrow('declares no dsh.patch') + expect(() => loadProfile('t', 'demo', anchor, home)).toThrow('declares no dsh.bundle') }) }) diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts index 99dede8ba0..e0b9344cdf 100644 --- a/scripts/check-workspace-constraints.ts +++ b/scripts/check-workspace-constraints.ts @@ -102,7 +102,7 @@ function workspaceManifests(): WorkspaceManifest[] { } const packageFileExtras: Readonly> = { - // Profile bundles publish their dsh.patch layer beside the lib. + // Profile bundles publish their dsh.bundle.patch layer beside the lib. '@deepseek-ai/dsh-base': ['cordis.patch.yml'], '@deepseek-ai/dsh-web-app': ['cordis.patch.yml'], '@deepseek-ai/dsh-headless': ['cordis.patch.yml'],