fix(repository-cache): isolate Git package dependencies

This commit is contained in:
Tianyi Cui
2026-08-08 19:59:48 +08:00
parent 033fa4b0b1
commit a7832cbfbf
9 changed files with 98 additions and 37 deletions

View File

@@ -112,7 +112,7 @@ describe('RepositoryCache', () => {
await expect(cache.resolve(specifier)).rejects.toThrow('repository cache marker is invalid')
})
it('selects and prepares a root .dsh-plugin Git subpath through the bundled pnpm', { timeout: 60_000 }, async () => {
it('isolates and prepares a .dsh-plugin Git subpath from an enclosing pnpm workspace', { timeout: 60_000 }, async () => {
const root = await temporaryRoot('repository-pnpm')
const repository = join(root, 'source')
const executableDirectory = join(root, 'bin')
@@ -132,16 +132,40 @@ describe('RepositoryCache', () => {
'',
].join('\r\n'))
await mkdir(join(repository, '.dsh-plugin'), { recursive: true })
await mkdir(join(repository, 'build-helper'), { recursive: true })
await mkdir(join(repository, 'skills', 'fixture'), { recursive: true })
await writeFile(join(repository, 'package.json'), `${JSON.stringify({
name: 'repository-fixture',
private: true,
version: '1.0.0',
packageManager: `pnpm@${BUNDLED_PNPM_VERSION}`,
})}\n`)
await writeFile(join(repository, 'pnpm-workspace.yaml'), 'packages: []\n')
await writeFile(join(repository, 'pnpm-lock.yaml'), [
"lockfileVersion: '9.0'",
'settings:',
' autoInstallPeers: true',
' excludeLinksFromLockfile: false',
'importers:',
' .: {}',
'',
].join('\n'))
await writeFile(join(repository, 'build-helper', 'package.json'), `${JSON.stringify({
name: 'repository-build-helper',
version: '1.0.0',
bin: 'index.js',
})}\n`)
await writeFile(join(repository, 'build-helper', 'index.js'), [
'#!/usr/bin/env node',
"require('node:fs').writeFileSync('dependency-built.txt', 'dependency available\\n')",
'',
].join('\n'), { mode: 0o700 })
await writeFile(join(repository, 'skills', 'fixture', 'SKILL.md'), 'repository skill source\n')
await writeFile(join(repository, '.dsh-plugin', 'package.json'), `${JSON.stringify({
name: 'repository-plugin-fixture',
version: '1.0.0',
scripts: { prepack: 'dsh-plugin-prepare' },
scripts: { prepack: 'repository-build-helper && dsh-plugin-prepare' },
devDependencies: { 'repository-build-helper': 'file:../build-helper' },
dsh: { skills: ['../skills'] },
})}\n`)
await execFileAsync('git', ['init', '--quiet'], { cwd: repository })
@@ -159,6 +183,7 @@ describe('RepositoryCache', () => {
const installed = await new RepositoryCache(join(root, 'cache'), {
executableDirectories: [executableDirectory],
}).resolve(specifier)
await expect(readFile(join(installed, 'dependency-built.txt'), 'utf8')).resolves.toBe('dependency available\n')
await expect(readFile(join(installed, 'prepared.txt'), 'utf8')).resolves.toBe('visible|absent\n')
await expect(readFile(join(installed, 'dsh-plugin.mjs'), 'utf8')).resolves.toContain('export function apply')
await expect(readFile(join(installed, 'dsh-plugin-assets/skills/0/fixture/SKILL.md'), 'utf8'))

View File

@@ -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/self-modification/repository-plugin/README.md
README.md: 52d04f16684842749e57d1b47db0a95beb22558c
README.zh.md: 921bf70b8a2e78410510d1efce79ced5f52b4641
README.md: 1d858c5adcc208768dcbe99f129b47abb1722431
README.zh.md: c2d0e6d72baeb0efb25abd8d50fee2922d2e810b

View File

@@ -32,7 +32,7 @@ Place an ordinary package in the repository's `.dsh-plugin` directory:
}
```
`scripts.prepack` must be non-empty and invoke `dsh-plugin-prepare`; it may run arbitrary package-owned build steps first. DSH supplies only that helper command from its installed runtime: the package declares and runs its own compiler, runtime dependencies, and other npm lifecycle code. DSH does not transpile TypeScript or infer a package entry.
`scripts.prepack` must be non-empty and invoke `dsh-plugin-prepare`; it may run arbitrary package-owned build steps first. DSH supplies only that helper command from its installed runtime: the package declares and runs its own compiler, runtime dependencies, and other npm lifecycle code. The selected package is installed from its own manifest instead of inheriting an enclosing pnpm workspace, so declare every dependency it needs and do not depend on workspace-only hoisting. DSH does not transpile TypeScript or infer a package entry.
`dsh.entry` is an optional relative path to a compiled ESM Cordis Plugin inside `.dsh-plugin`. The module may use either namespace exports or a default export and owns its ordinary `name`, `inject`, `Config`, registrations, and effects. `dsh.skills` is an optional array of local skill roots, and `dsh.mcpServers` is an optional path to one `.mcp.json`; at least one of the three fields is required. Skill and MCP paths may reach adjacent repository assets but must remain beneath the directory containing `.dsh-plugin`; the compiled entry must remain inside the package selected and packed by the package manager. A repository containing several Plugins gives each one its own `.dsh-plugin` package under a different selectable subdirectory.
@@ -59,7 +59,7 @@ Long-lived surfaces watch both `cordis.patch.yml` layers through Cordis HMR. A v
## Preparation
During exact Git installation, DSH places a temporary host-owned `dsh-plugin-prepare` command on the isolated package lifecycle `PATH`; the command is not fetched from npm. The required `prepack` lifecycle runs after the Git package's dependency installation and before its selected subdirectory is packed, including when `.dsh-plugin` sits inside another package-manager workspace. Package-owned commands may build TypeScript or other source before invoking the helper. The helper validates `package.json#dsh`, verifies that the compiled entry is an in-package file, validates skill and MCP sources, copies static assets under `dsh-plugin-assets`, and writes `dsh-plugin.mjs`. Before importing that wrapper, DSH revalidates that the installed package retained a `prepack` declaration containing the helper command. Failure to build or prepare fails installation before a cache generation is published. Rationale: [host-owned Git source preparation Agent Note](../../../.agents/notes/implemented/bug-fix/2026-08-08-host-owned-git-repository-plugin-preparation.md).
During exact Git installation, DSH places temporary host-owned `pnpm` and `dsh-plugin-prepare` commands on the isolated package lifecycle `PATH`; neither command is fetched from the repository. The pnpm command reinvokes DSH's pinned pnpm with `--ignore-workspace`, so an enclosing workspace lockfile cannot suppress dependencies declared only by the selected `.dsh-plugin` package. The required `prepack` lifecycle runs after that dependency installation and before the selected subdirectory is packed. Package-owned commands may build TypeScript or other source before invoking the helper. The helper validates `package.json#dsh`, verifies that the compiled entry is an in-package file, validates skill and MCP sources, copies static assets under `dsh-plugin-assets`, and writes `dsh-plugin.mjs`. Before importing that wrapper, DSH revalidates that the installed package retained a `prepack` declaration containing the helper command. Failure to install, build, or prepare fails before a cache generation is published. Rationale: [host-owned Git source preparation Agent Note](../../../.agents/notes/implemented/bug-fix/2026-08-08-host-owned-git-repository-plugin-preparation.md).
## Runtime composition

View File

@@ -32,7 +32,7 @@
}
```
`scripts.prepack` 必须非空并调用 `dsh-plugin-prepare`可以先运行任意包自有的构建步骤。DSH 已安装的运行时只提供该辅助命令:包自行声明并运行编译器、运行时依赖和其他 NPM 生命周期代码。DSH 不转译 TypeScript也不推断包入口。
`scripts.prepack` 必须非空并调用 `dsh-plugin-prepare`可以先运行任意包自有的构建步骤。DSH 已安装的运行时只提供该辅助命令:包自行声明并运行编译器、运行时依赖和其他 NPM 生命周期代码。所选包按自身 manifest 独立安装,而不继承外层 pnpm workspace因此必须声明所需的每项依赖不能依赖仅由 workspace 提升而可见的包。DSH 不转译 TypeScript也不推断包入口。
`dsh.entry` 是指向 `.dsh-plugin` 内已编译 ESM Cordis 插件的可选相对路径。该模块可以使用 namespace 导出或 default export并自行拥有常规的 `name``inject``Config`、注册和 effect。`dsh.skills` 是可选的本地 skill 根数组,`dsh.mcpServers` 是指向一个 `.mcp.json` 的可选路径三个字段中至少声明一个。skill 和 MCP 路径可以引用相邻的 repository 资源,但必须留在包含 `.dsh-plugin` 的目录下;已编译入口必须留在由包管理器选中并打包的包内。一个仓库可以在不同的可选择子目录下放置多个各自独立的 `.dsh-plugin` 包。
@@ -59,7 +59,7 @@ Git 传输使用宿主的常规 Git 认证。公共仓库无需凭据;私有
## 准备阶段
安装精确指定的 Git 源时DSH 会把一个临时的宿主自有 `dsh-plugin-prepare` 命令放入隔离的包生命周期 `PATH`命令不从 NPM 获取。必需的 `prepack` 生命周期在 Git 包完成依赖安装后、选定子目录打包前运行,即使 `.dsh-plugin` 位于另一个包管理器工作区内也不例外。包自有命令可以在调用辅助程序前构建 TypeScript 或其他源码。辅助程序会校验 `package.json#dsh`,确认已编译入口是包内文件,校验 skill 与 MCP 源,把静态资源复制到 `dsh-plugin-assets`,并写入 `dsh-plugin.mjs`。导入该包装层前DSH 会重新校验已安装包是否仍保留包含该辅助命令的 `prepack` 声明。构建或准备失败时,安装会在发布缓存 generation 前失败。设计依据见[宿主自有 Git 源准备 Agent Note](../../../.agents/notes/implemented/bug-fix/2026-08-08-host-owned-git-repository-plugin-preparation.md)。
安装精确指定的 Git 源时DSH 会把临时的宿主自有 `pnpm` `dsh-plugin-prepare` 命令放入隔离的包生命周期 `PATH`两个命令不从 repository 获取。该 pnpm 命令会以 `--ignore-workspace` 重新调用 DSH 锁定的 pnpm因此外层 workspace lockfile 无法抑制仅由所选 `.dsh-plugin` 包声明的依赖。必需的 `prepack` 生命周期在依赖安装完成后、选定子目录打包前运行。包自有命令可以在调用辅助程序前构建 TypeScript 或其他源码。辅助程序会校验 `package.json#dsh`,确认已编译入口是包内文件,校验 skill 与 MCP 源,把静态资源复制到 `dsh-plugin-assets`,并写入 `dsh-plugin.mjs`。导入该包装层前DSH 会重新校验已安装包是否仍保留包含该辅助命令的 `prepack` 声明。安装、构建或准备失败时,流程会在发布缓存 generation 前失败。设计依据见[宿主自有 Git 源准备 Agent Note](../../../.agents/notes/implemented/bug-fix/2026-08-08-host-owned-git-repository-plugin-preparation.md)。
## 运行时组合