refactor(cli)!: namespace the profile and bundle manifests under dsh.profile and dsh.bundle
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).
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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、关闭行为、部署默认值和源码启动器。
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,7 @@ This reference defines the profile, web-alias, plugin-management, and config-dum
|
||||
|
||||
## Profile boot
|
||||
|
||||
`dsh --profile <name>` boots the profile at `$DSH_HOME/profiles/<name>`. 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 <path>` 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 <name>` boots the profile at `$DSH_HOME/profiles/<name>`. 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 <path>` 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 <name> <args...>` initializes the profile when missing (shipped template, or `@deepseek-ai/dsh-base` alone for other names), then forwards `<args...>` 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 <name> <args...>` initializes the profile when missing (shipped template, or `@deepseek-ai/dsh-base` alone for other names), then forwards `<args...>` 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
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## Profile 启动
|
||||
|
||||
`dsh --profile <name>` 启动位于 `$DSH_HOME/profiles/<name>` 的 profile。生效配置树在空根节点之上按以下顺序逐层组合:profile manifest(元数据清单)的 `dsh.plugins` 列表所列的各个组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(各 profile 共享的机器本地偏好,因此优先级高于逐 profile 的层)、按 argv 顺序的各个 `--patch <path>` overlay,以及启动器 flag patch。后应用的层按行胜出;patch 替换目标行完整的 `config` 值,而不是深度合并各键,并且可以插入新行。配置解析、schema 校验、模块解析或插件启动失败会得到报告并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。
|
||||
`dsh --profile <name>` 启动位于 `$DSH_HOME/profiles/<name>` 的 profile。生效配置树在空根节点之上按以下顺序逐层组合:profile manifest(元数据清单)的 `dsh.profile.bundles` 列表所列的各个组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(各 profile 共享的机器本地偏好,因此优先级高于逐 profile 的层)、按 argv 顺序的各个 `--patch <path>` 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 <name> <args...>` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `<args...>` 转发给 `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 <name> <args...>` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `<args...>` 转发给 `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
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* `dsh plugin --profile <name> <args...>` — profile plugin management as a
|
||||
* thin pnpm forwarder: initialize the profile on first use, run
|
||||
* `pnpm <args...>` 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 <args...>` 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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, string>
|
||||
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 })
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ async function runHeadlessPtySmoke(): Promise<string> {
|
||||
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:',
|
||||
|
||||
Reference in New Issue
Block a user