fix(repository-cache): isolate Git package dependencies
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-08-host-owned-git-repository-plugin-preparation.md
|
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-08-host-owned-git-repository-plugin-preparation.md
|
||||||
2026-08-08-host-owned-git-repository-plugin-preparation.md: b459184f802981f91f611b0b781651cede0c9efd
|
2026-08-08-host-owned-git-repository-plugin-preparation.md: 3f92c0b2d782735bfb548711bb13a7a8d03a3824
|
||||||
2026-08-08-host-owned-git-repository-plugin-preparation.zh.md: 5e24efa59ce44ffa8214f9c5ef9ff722ac0ca73a
|
2026-08-08-host-owned-git-repository-plugin-preparation.zh.md: ef34c99b5d9f31ea13c5fddfee77c30e640d25c9
|
||||||
+7
-2
@@ -10,13 +10,15 @@ The repository Plugin authoring contract depended on `scripts.prepare: "dsh-plug
|
|||||||
|
|
||||||
The lifecycle choice also failed for a selectable `.dsh-plugin` inside a pnpm workspace. pnpm prepares a Git-hosted package by running the repository's preferred package manager before packing the selected subdirectory. A nested `pnpm install` joins the containing workspace and need not execute the unlisted `.dsh-plugin` package's `prepare` script. The install could therefore succeed and publish a cache generation containing only the source package metadata; real DSH startup failed later because `dsh-plugin.mjs` did not exist.
|
The lifecycle choice also failed for a selectable `.dsh-plugin` inside a pnpm workspace. pnpm prepares a Git-hosted package by running the repository's preferred package manager before packing the selected subdirectory. A nested `pnpm install` joins the containing workspace and need not execute the unlisted `.dsh-plugin` package's `prepare` script. The install could therefore succeed and publish a cache generation containing only the source package metadata; real DSH startup failed later because `dsh-plugin.mjs` did not exist.
|
||||||
|
|
||||||
|
The same workspace discovery could suppress package-owned dependencies after the move to `prepack`. When the source repository carried a root pnpm lockfile but did not list the selected `.dsh-plugin` as a workspace importer, pnpm reported a successful workspace install without installing dependencies declared only by that package. Its TypeScript build then failed because Cordis and the MCP SDK were absent.
|
||||||
|
|
||||||
The checked-in headless fixture did not catch either defect because it mounted an already prepared wrapper. It proved runtime composition, not GitHub acquisition or package preparation.
|
The checked-in headless fixture did not catch either defect because it mounted an already prepared wrapper. It proved runtime composition, not GitHub acquisition or package preparation.
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
The authoring format requires a non-empty `scripts.prepack` that invokes `dsh-plugin-prepare` and needs no DSH dependency for that helper. The package may declare its own build and runtime dependencies and run compilation before the helper. pnpm's Git-hosted package preparation invokes `prepack` explicitly after its dependency-install step and before packlist selects the `.dsh-plugin` subtree, so the helper can validate built entries and still copy sibling repository assets such as `../skills` into the package.
|
The authoring format requires a non-empty `scripts.prepack` that invokes `dsh-plugin-prepare` and needs no DSH dependency for that helper. The package may declare its own build and runtime dependencies and run compilation before the helper. pnpm's Git-hosted package preparation invokes `prepack` explicitly after its dependency-install step and before packlist selects the `.dsh-plugin` subtree, so the helper can validate built entries and still copy sibling repository assets such as `../skills` into the package.
|
||||||
|
|
||||||
`@deepseek-ai/dsh-repository-plugin` materializes short-lived POSIX and Windows command wrappers that invoke its own built `dsh-plugin-prepare` entry. `RepositoryCache` accepts caller-owned executable directories, resolves them absolutely, and prepends them to the credential-scrubbed lifecycle `PATH` passed to bundled pnpm. The command directory exists only for the installation transaction and is removed on success or failure. The repository remains trusted package-manager input: DSH supplies one command, but other lifecycle scripts and dependencies still execute under the existing trust contract.
|
`@deepseek-ai/dsh-repository-plugin` materializes short-lived POSIX and Windows command wrappers that invoke its own built `dsh-plugin-prepare` entry. `RepositoryCache` accepts caller-owned executable directories, resolves them absolutely, and prepends them to the credential-scrubbed lifecycle `PATH` passed to bundled pnpm. It also prepends a transaction-owned `pnpm` wrapper: the outer install still runs the pinned pnpm entry directly, while pnpm's hard-coded Git-package `pnpm install` reinvokes that same entry with `--ignore-workspace`. The selected package therefore owns dependency resolution even beneath another pnpm lockfile. Both command directories are removed after the child settles. The repository remains trusted package-manager input: DSH supplies the two host commands, but all package lifecycle scripts and dependencies still execute under the existing trust contract.
|
||||||
|
|
||||||
The Node 24 consumer lane passes an exact source derived from the pull request head repository and SHA. Because that repository is private, the workflow writes a job-scoped Git configuration that uses the read-only job token for GitHub HTTPS and rewrites pnpm's SSH fallback to that authenticated transport. Its built-entry acceptance launches the real `apps/cli/lib/bin.js run` command with a one-run patch selecting a `private: true` GitHub fixture. That fixture installs pinned npm dependencies, type-checks and bundles a TypeScript Cordis entry and MCP server in `prepack`, invokes the host helper, and proves the skill, MCP call, and code entry through real model requests and immutable-cache artifacts. The test fails if CI omits the exact source instead of silently skipping.
|
The Node 24 consumer lane passes an exact source derived from the pull request head repository and SHA. Because that repository is private, the workflow writes a job-scoped Git configuration that uses the read-only job token for GitHub HTTPS and rewrites pnpm's SSH fallback to that authenticated transport. Its built-entry acceptance launches the real `apps/cli/lib/bin.js run` command with a one-run patch selecting a `private: true` GitHub fixture. That fixture installs pinned npm dependencies, type-checks and bundles a TypeScript Cordis entry and MCP server in `prepack`, invokes the host helper, and proves the skill, MCP call, and code entry through real model requests and immutable-cache artifacts. The test fails if CI omits the exact source instead of silently skipping.
|
||||||
|
|
||||||
@@ -30,14 +32,17 @@ The Node 24 consumer lane passes an exact source derived from the pull request h
|
|||||||
|
|
||||||
**Clone GitHub repositories in DSH and bypass pnpm's Git fetcher.** Rejected because it would duplicate ref resolution, subdirectory selection, dependency installation, packlist behavior, and cache integrity already owned by the pinned package manager.
|
**Clone GitHub repositories in DSH and bypass pnpm's Git fetcher.** Rejected because it would duplicate ref resolution, subdirectory selection, dependency installation, packlist behavior, and cache integrity already owned by the pinned package manager.
|
||||||
|
|
||||||
|
**Add the in-repository CI fixture to this repository's pnpm workspace.** Rejected because that would repair only the proof fixture and leave an arbitrary selected package vulnerable to its containing repository's workspace membership and lockfile.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- A repository author can commit the fixed `.dsh-plugin/package.json` and source assets to GitHub without publishing either the Plugin or its preparation helper to npm.
|
- A repository author can commit the fixed `.dsh-plugin/package.json` and source assets to GitHub without publishing either the Plugin or its preparation helper to npm.
|
||||||
- Private GitHub sources use the host's standard Git authentication. CI proves that path with a temporary read-only configuration rather than persistent runner credentials.
|
- Private GitHub sources use the host's standard Git authentication. CI proves that path with a temporary read-only configuration rather than persistent runner credentials.
|
||||||
- `prepack`, not `prepare`, is part of the pre-release authoring format. It may contain package-owned build steps but must invoke the host helper; missing or empty lifecycle metadata fails installed-package validation instead of producing an ambiguous partial format.
|
- `prepack`, not `prepare`, is part of the pre-release authoring format. It may contain package-owned build steps but must invoke the host helper; missing or empty lifecycle metadata fails installed-package validation instead of producing an ambiguous partial format.
|
||||||
|
- A selected package in a pnpm repository installs from its own manifest rather than an enclosing workspace. It must declare its dependencies and cannot rely on workspace-only hoisting; ordinary registry and relative `file:` dependencies remain package-owned inputs.
|
||||||
- Exact source strings still identify immutable cache generations; a changed ref or source configuration selects another generation.
|
- Exact source strings still identify immutable cache generations; a changed ref or source configuration selects another generation.
|
||||||
- The host supplies only the preparation executable. Package dependencies, compilation, and the trusted `dsh.entry` contribution remain owned by the repository package and the [trusted-code decision](../architecture/2026-08-08-trusted-repository-package-code.md).
|
- The host supplies only the preparation executable. Package dependencies, compilation, and the trusted `dsh.entry` contribution remain owned by the repository package and the [trusted-code decision](../architecture/2026-08-08-trusted-repository-package-code.md).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
`packages/ui/app-boot/tests/repository-cache.spec.ts` runs a local Git subpath through bundled pnpm with an injected command directory and proves that visible environment survives while credential-shaped variables are scrubbed. `packages/cordis/repository-plugin/tests/repository-plugin.spec.ts` pins helper-bearing `prepack` metadata and temporary command cleanup. `examples/headless-agent/tests/keyless-smoke.e2e.ts` keeps the checked-in prepared fixture on that source contract. `apps/cli/tests/github-repository-plugin.built.e2e.ts` is the product acceptance: fresh DSH home, exact authenticated private GitHub source, actual built `dsh run`, package-owned TypeScript build, real MCP execution, code-entry transformation, mock LLM request observation, and prepared cache inspection.
|
`packages/ui/app-boot/tests/repository-cache.spec.ts` runs a package excluded from its source repository's root pnpm lockfile through a local Git subpath and requires a relative `file:` build dependency during `prepack`; it also proves that visible environment survives while credential-shaped variables are scrubbed. `packages/cordis/repository-plugin/tests/repository-plugin.spec.ts` pins helper-bearing `prepack` metadata and temporary command cleanup. `examples/headless-agent/tests/keyless-smoke.e2e.ts` keeps the checked-in prepared fixture on that source contract. `apps/cli/tests/github-repository-plugin.built.e2e.ts` is the product acceptance: fresh DSH home, exact authenticated private GitHub source, actual built `dsh run`, package-owned TypeScript build, real MCP execution, code-entry transformation, mock LLM request observation, and prepared cache inspection.
|
||||||
+7
-2
@@ -10,13 +10,15 @@ repository 插件的创作契约依赖 `scripts.prepare: "dsh-plugin-prepare"`
|
|||||||
|
|
||||||
这种生命周期选择也无法支持 pnpm 工作区内可选的 `.dsh-plugin`。pnpm 会先运行 Git 托管仓库首选的包管理器,再打包选定的子目录,从而准备 Git 托管包。嵌套执行的 `pnpm install` 会加入外层工作区,而不一定执行未列入其中的 `.dsh-plugin` 包的 `prepare` 脚本。因此,安装可能成功并发布一个仅包含源包元数据的缓存 generation;随后真实 DSH 启动因 `dsh-plugin.mjs` 不存在而失败。
|
这种生命周期选择也无法支持 pnpm 工作区内可选的 `.dsh-plugin`。pnpm 会先运行 Git 托管仓库首选的包管理器,再打包选定的子目录,从而准备 Git 托管包。嵌套执行的 `pnpm install` 会加入外层工作区,而不一定执行未列入其中的 `.dsh-plugin` 包的 `prepare` 脚本。因此,安装可能成功并发布一个仅包含源包元数据的缓存 generation;随后真实 DSH 启动因 `dsh-plugin.mjs` 不存在而失败。
|
||||||
|
|
||||||
|
迁移到 `prepack` 后,同一项 workspace 发现行为还可能抑制包自有依赖。如果源仓库带有根 pnpm lockfile,却未把所选 `.dsh-plugin` 列为 workspace importer,pnpm 会报告 workspace 安装成功,但不会安装仅由该包声明的依赖。随后其 TypeScript 构建会因缺少 Cordis 和 MCP SDK 而失败。
|
||||||
|
|
||||||
签入仓库的 headless fixture(测试前置数据)没有捕获任一缺陷,因为它挂载的是已准备好的包装层。它证明的是运行时组合,而不是 GitHub 获取或包准备。
|
签入仓库的 headless fixture(测试前置数据)没有捕获任一缺陷,因为它挂载的是已准备好的包装层。它证明的是运行时组合,而不是 GitHub 获取或包准备。
|
||||||
|
|
||||||
## 决策
|
## 决策
|
||||||
|
|
||||||
创作格式要求 `scripts.prepack` 非空且调用 `dsh-plugin-prepare`,使用该辅助程序无需 DSH 依赖。包可以声明自己的构建依赖与运行时依赖,并在调用辅助程序前完成编译。pnpm 针对 Git 托管包的准备流程会在依赖安装步骤之后、打包清单选择 `.dsh-plugin` 子树之前显式调用 `prepack`,因此辅助程序可以校验构建入口,并继续把 `../skills` 等同仓库的相邻资源复制进包内。
|
创作格式要求 `scripts.prepack` 非空且调用 `dsh-plugin-prepare`,使用该辅助程序无需 DSH 依赖。包可以声明自己的构建依赖与运行时依赖,并在调用辅助程序前完成编译。pnpm 针对 Git 托管包的准备流程会在依赖安装步骤之后、打包清单选择 `.dsh-plugin` 子树之前显式调用 `prepack`,因此辅助程序可以校验构建入口,并继续把 `../skills` 等同仓库的相邻资源复制进包内。
|
||||||
|
|
||||||
`@deepseek-ai/dsh-repository-plugin` 会生成临时的 POSIX 和 Windows 命令包装脚本,用于调用其自有的已构建 `dsh-plugin-prepare` 入口。`RepositoryCache` 接受由调用方持有的可执行文件目录,将它们解析为绝对路径,再前置到传给随附 pnpm、已清除凭据的包生命周期 `PATH`。该命令目录仅存在于安装事务期间,无论成功还是失败都会被移除。仓库仍是受信任的包管理器输入:DSH 仅提供这一条命令,其他生命周期脚本和依赖仍会按既有信任契约执行。
|
`@deepseek-ai/dsh-repository-plugin` 会生成临时的 POSIX 和 Windows 命令包装脚本,用于调用其自有的已构建 `dsh-plugin-prepare` 入口。`RepositoryCache` 接受由调用方持有的可执行文件目录,将它们解析为绝对路径,再前置到传给随附 pnpm、已清除凭据的包生命周期 `PATH`。它还会前置一个由安装事务持有的 `pnpm` 包装命令:外层安装仍直接运行锁定的 pnpm 入口,而 pnpm 为 Git 包硬编码的 `pnpm install` 会通过 `--ignore-workspace` 重新调用同一入口。因此,即使位于另一个 pnpm lockfile 之下,所选包仍自行拥有依赖解析。两个命令目录都会在子进程结算后移除。仓库仍是受信任的包管理器输入:DSH 提供这两条宿主命令,但所有包生命周期脚本和依赖仍按既有信任契约执行。
|
||||||
|
|
||||||
Node 24 消费方 CI 任务会传入从 PR(Pull Request)head 仓库和 SHA 派生的精确源。由于该仓库为私有仓库,工作流会写入一份作业作用域的 Git 配置,使用该作业的只读 token 对 GitHub HTTPS 连接进行认证,并将 pnpm 的 SSH 回退路径重写为这一已认证的传输方式。其构建入口验收会启动真实的 `apps/cli/lib/bin.js run` 命令,并通过一个仅作用于当次运行的 patch 选择 `private: true` 的 GitHub fixture。该 fixture 安装固定版本的 NPM 依赖,在 `prepack` 中对 TypeScript Cordis 入口和 MCP server 进行类型检查与打包,调用宿主辅助程序,并通过真实模型请求和不可变缓存产物验证 skill(技能)、MCP 调用和代码入口。如果 CI 遗漏精确源,测试会失败,而不是静默跳过。
|
Node 24 消费方 CI 任务会传入从 PR(Pull Request)head 仓库和 SHA 派生的精确源。由于该仓库为私有仓库,工作流会写入一份作业作用域的 Git 配置,使用该作业的只读 token 对 GitHub HTTPS 连接进行认证,并将 pnpm 的 SSH 回退路径重写为这一已认证的传输方式。其构建入口验收会启动真实的 `apps/cli/lib/bin.js run` 命令,并通过一个仅作用于当次运行的 patch 选择 `private: true` 的 GitHub fixture。该 fixture 安装固定版本的 NPM 依赖,在 `prepack` 中对 TypeScript Cordis 入口和 MCP server 进行类型检查与打包,调用宿主辅助程序,并通过真实模型请求和不可变缓存产物验证 skill(技能)、MCP 调用和代码入口。如果 CI 遗漏精确源,测试会失败,而不是静默跳过。
|
||||||
|
|
||||||
@@ -30,14 +32,17 @@ Node 24 消费方 CI 任务会传入从 PR(Pull Request)head 仓库和 SHA
|
|||||||
|
|
||||||
**在 DSH 中克隆 GitHub 仓库,并绕过 pnpm 的 Git 获取器。** 拒绝,因为这会重复实现已由锁定版本的包管理器负责的 ref 解析、子目录选择、依赖安装、打包清单行为和缓存完整性。
|
**在 DSH 中克隆 GitHub 仓库,并绕过 pnpm 的 Git 获取器。** 拒绝,因为这会重复实现已由锁定版本的包管理器负责的 ref 解析、子目录选择、依赖安装、打包清单行为和缓存完整性。
|
||||||
|
|
||||||
|
**把仓库内的 CI fixture 加入本仓库的 pnpm workspace。** 拒绝,因为这只能修复证明用的 fixture,任意所选包仍会受其所在仓库的 workspace membership 与 lockfile 影响。
|
||||||
|
|
||||||
## 后果
|
## 后果
|
||||||
|
|
||||||
- 仓库作者可以把修复后的 `.dsh-plugin/package.json` 和源资源提交到 GitHub,而无需把插件或其准备辅助程序发布到 NPM。
|
- 仓库作者可以把修复后的 `.dsh-plugin/package.json` 和源资源提交到 GitHub,而无需把插件或其准备辅助程序发布到 NPM。
|
||||||
- 私有 GitHub 源使用宿主的标准 Git 认证。CI 使用临时的只读配置而非运行器上的持久凭据来验证该路径。
|
- 私有 GitHub 源使用宿主的标准 Git 认证。CI 使用临时的只读配置而非运行器上的持久凭据来验证该路径。
|
||||||
- 预发布创作格式使用 `prepack` 而不是 `prepare`。其中可以包含包自有构建步骤,但必须调用宿主辅助程序;生命周期元数据缺失或为空会在已安装包校验时失败,而不会留下状态不明的半成品格式。
|
- 预发布创作格式使用 `prepack` 而不是 `prepare`。其中可以包含包自有构建步骤,但必须调用宿主辅助程序;生命周期元数据缺失或为空会在已安装包校验时失败,而不会留下状态不明的半成品格式。
|
||||||
|
- pnpm 仓库中的所选包按自身 manifest 安装,而不是按外层 workspace 安装。它必须声明自己的依赖,不能依赖仅由 workspace 提升而可见的包;常规 registry 依赖与相对 `file:` 依赖仍是包自有输入。
|
||||||
- 精确源字符串仍标识不可变缓存 generation;改变 ref 或源配置会选择另一个 generation。
|
- 精确源字符串仍标识不可变缓存 generation;改变 ref 或源配置会选择另一个 generation。
|
||||||
- 宿主只提供准备阶段可执行文件。包依赖、编译和受信任的 `dsh.entry` 贡献仍由 repository 包和[受信任代码决策](../architecture/2026-08-08-trusted-repository-package-code.md)负责。
|
- 宿主只提供准备阶段可执行文件。包依赖、编译和受信任的 `dsh.entry` 贡献仍由 repository 包和[受信任代码决策](../architecture/2026-08-08-trusted-repository-package-code.md)负责。
|
||||||
|
|
||||||
## 测试
|
## 测试
|
||||||
|
|
||||||
`packages/ui/app-boot/tests/repository-cache.spec.ts` 会用注入的命令目录通过随附 pnpm 运行本地 Git 子路径,并证明可见环境变量得以保留,而名称符合凭据模式的变量会被清除。`packages/cordis/repository-plugin/tests/repository-plugin.spec.ts` 锁定包含辅助命令的 `prepack` 元数据和临时命令清理行为。`examples/headless-agent/tests/keyless-smoke.e2e.ts` 使签入仓库的已准备 fixture 继续符合该源格式契约。`apps/cli/tests/github-repository-plugin.built.e2e.ts` 是产品验收测试:全新的 DSH 主目录、精确且经过认证的私有 GitHub 源、实际构建产物的 `dsh run`、包自有 TypeScript 构建、真实 MCP 执行、代码入口转换、mock LLM(大语言模型)请求观测,以及对已准备缓存的检查。
|
`packages/ui/app-boot/tests/repository-cache.spec.ts` 会让一个被源仓库根 pnpm lockfile 排除的包通过本地 Git 子路径运行,并要求 `prepack` 使用相对 `file:` 构建依赖;该测试还证明可见环境变量得以保留,而名称符合凭据模式的变量会被清除。`packages/cordis/repository-plugin/tests/repository-plugin.spec.ts` 锁定包含辅助命令的 `prepack` 元数据和临时命令清理行为。`examples/headless-agent/tests/keyless-smoke.e2e.ts` 使签入仓库的已准备 fixture 继续符合该源格式契约。`apps/cli/tests/github-repository-plugin.built.e2e.ts` 是产品验收测试:全新的 DSH 主目录、精确且经过认证的私有 GitHub 源、实际构建产物的 `dsh run`、包自有 TypeScript 构建、真实 MCP 执行、代码入口转换、mock LLM(大语言模型)请求观测,以及对已准备缓存的检查。
|
||||||
@@ -112,7 +112,7 @@ describe('RepositoryCache', () => {
|
|||||||
await expect(cache.resolve(specifier)).rejects.toThrow('repository cache marker is invalid')
|
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 root = await temporaryRoot('repository-pnpm')
|
||||||
const repository = join(root, 'source')
|
const repository = join(root, 'source')
|
||||||
const executableDirectory = join(root, 'bin')
|
const executableDirectory = join(root, 'bin')
|
||||||
@@ -132,16 +132,40 @@ describe('RepositoryCache', () => {
|
|||||||
'',
|
'',
|
||||||
].join('\r\n'))
|
].join('\r\n'))
|
||||||
await mkdir(join(repository, '.dsh-plugin'), { recursive: true })
|
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 mkdir(join(repository, 'skills', 'fixture'), { recursive: true })
|
||||||
await writeFile(join(repository, 'package.json'), `${JSON.stringify({
|
await writeFile(join(repository, 'package.json'), `${JSON.stringify({
|
||||||
name: 'repository-fixture',
|
name: 'repository-fixture',
|
||||||
|
private: true,
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
|
packageManager: `pnpm@${BUNDLED_PNPM_VERSION}`,
|
||||||
})}\n`)
|
})}\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, 'skills', 'fixture', 'SKILL.md'), 'repository skill source\n')
|
||||||
await writeFile(join(repository, '.dsh-plugin', 'package.json'), `${JSON.stringify({
|
await writeFile(join(repository, '.dsh-plugin', 'package.json'), `${JSON.stringify({
|
||||||
name: 'repository-plugin-fixture',
|
name: 'repository-plugin-fixture',
|
||||||
version: '1.0.0',
|
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'] },
|
dsh: { skills: ['../skills'] },
|
||||||
})}\n`)
|
})}\n`)
|
||||||
await execFileAsync('git', ['init', '--quiet'], { cwd: repository })
|
await execFileAsync('git', ['init', '--quiet'], { cwd: repository })
|
||||||
@@ -159,6 +183,7 @@ describe('RepositoryCache', () => {
|
|||||||
const installed = await new RepositoryCache(join(root, 'cache'), {
|
const installed = await new RepositoryCache(join(root, 'cache'), {
|
||||||
executableDirectories: [executableDirectory],
|
executableDirectories: [executableDirectory],
|
||||||
}).resolve(specifier)
|
}).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, '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.mjs'), 'utf8')).resolves.toContain('export function apply')
|
||||||
await expect(readFile(join(installed, 'dsh-plugin-assets/skills/0/fixture/SKILL.md'), 'utf8'))
|
await expect(readFile(join(installed, 'dsh-plugin-assets/skills/0/fixture/SKILL.md'), 'utf8'))
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# 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
|
# pnpm run verify-translation-pairing --write packages/self-modification/repository-plugin/README.md
|
||||||
README.md: 52d04f16684842749e57d1b47db0a95beb22558c
|
README.md: 1d858c5adcc208768dcbe99f129b47abb1722431
|
||||||
README.zh.md: 921bf70b8a2e78410510d1efce79ced5f52b4641
|
README.zh.md: c2d0e6d72baeb0efb25abd8d50fee2922d2e810b
|
||||||
@@ -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.
|
`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
|
## 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
|
## Runtime composition
|
||||||
|
|
||||||
|
|||||||
@@ -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` 包。
|
`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)。
|
||||||
|
|
||||||
## 运行时组合
|
## 运行时组合
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
-1
@@ -39,7 +39,7 @@ Keep this log exhaustive — every divergence from upstream must be listed.
|
|||||||
7. **`cordis/src/*.ts` JSDoc enrichment**: added `@param`/`@returns` tags and contract documentation (disposal semantics, waterfall veto, bail conditions, error cases) across the public plugin-author surface — `Context` (class, statics, and the `Context` interface properties incl. `root`), `EventsService`, `Fiber`, `RegistryService`, `ReflectService`, `Service`, `LoggerService` and their `declare module './context.ts'` overloads. Comment-only; no code changes. Motivation: the website API-reference generator renders these docs and hard-errors on undocumented members. Retire this entry when the enrichment is upstreamed to the fork.
|
7. **`cordis/src/*.ts` JSDoc enrichment**: added `@param`/`@returns` tags and contract documentation (disposal semantics, waterfall veto, bail conditions, error cases) across the public plugin-author surface — `Context` (class, statics, and the `Context` interface properties incl. `root`), `EventsService`, `Fiber`, `RegistryService`, `ReflectService`, `Service`, `LoggerService` and their `declare module './context.ts'` overloads. Comment-only; no code changes. Motivation: the website API-reference generator renders these docs and hard-errors on undocumented members. Retire this entry when the enrichment is upstreamed to the fork.
|
||||||
8. **Transactional Loader/Include config reconciliation**: Loader imports a changed entry name before disposal, awaits lifecycle settlement, and restores the previous plugin or config when candidate application fails. Loader settlement rechecks service-gated fibers after current tasks drain, rejects failures, and leaves fibers with absent dependencies pending. Group updates start candidates concurrently, await every outcome, undo changes and additions on failure, await removal, preserve programmatic option identity, and persist direct or tree-level mutations only after success. Include reads and validates detached candidate content, applies patches to a clone, reconciles the tree, and only then commits its cached content/data; direct refresh failures propagate for the caller to contain. A non-array parse is invalid, patches re-apply on every file or Include-config update, an omitted patch list clears the overlay, and initial content falls back to `initial` only on `ENOENT`. Covered by `packages/boot/app-boot/tests/config-reload.spec.ts` and `packages/host/webserver/tests/webserver.spec.ts`.
|
8. **Transactional Loader/Include config reconciliation**: Loader imports a changed entry name before disposal, awaits lifecycle settlement, and restores the previous plugin or config when candidate application fails. Loader settlement rechecks service-gated fibers after current tasks drain, rejects failures, and leaves fibers with absent dependencies pending. Group updates start candidates concurrently, await every outcome, undo changes and additions on failure, await removal, preserve programmatic option identity, and persist direct or tree-level mutations only after success. Include reads and validates detached candidate content, applies patches to a clone, reconciles the tree, and only then commits its cached content/data; direct refresh failures propagate for the caller to contain. A non-array parse is invalid, patches re-apply on every file or Include-config update, an omitted patch list clears the overlay, and initial content falls back to `initial` only on `ENOENT`. Covered by `packages/boot/app-boot/tests/config-reload.spec.ts` and `packages/host/webserver/tests/webserver.spec.ts`.
|
||||||
9. **`hmr/src/index.ts` exact config watching**: `registerConfig()` watches one absolute config path outside module roots, including a path under missing parents, serializes and coalesces refreshes, and returns an async disposer that closes the watcher and drains active work. Refresh failures are normalized to `Error`, logged, and broadcast through the parallel `hmr/config-update-failed` event; observer failures are contained. Config-file changes discovered by the ordinary HMR watcher use the same serialized path. Covered by `packages/boot/app-boot/tests/hmr-config.spec.ts`.
|
9. **`hmr/src/index.ts` exact config watching**: `registerConfig()` watches one absolute config path outside module roots, including a path under missing parents, serializes and coalesces refreshes, and returns an async disposer that closes the watcher and drains active work. Refresh failures are normalized to `Error`, logged, and broadcast through the parallel `hmr/config-update-failed` event; observer failures are contained. Config-file changes discovered by the ordinary HMR watcher use the same serialized path. Covered by `packages/boot/app-boot/tests/hmr-config.spec.ts`.
|
||||||
10. **`loader/src/repository.ts`, `loader/tsdown.config.ts`, and the `@cordisjs/plugin-loader/repository` export**: the Node-only `RepositoryCache` installs one exact dependency specifier through the bundled `pnpm@11.7.0`, single-flights callers, and atomically publishes only a prepared package plus marker under the specifier hash. The subpath stays out of the browser-reachable Loader entry. Identical specifiers permanently reuse that entry; callers change the ref/specifier for another generation. Callers may prepend host-owned executable directories to the isolated package lifecycle `PATH`; all paths are resolved before the child starts. The isolated workspace permits dependency build scripts because a configured repository is executable code, while the child drops ambient credential-shaped variables. Covered by `packages/boot/app-boot/tests/repository-cache.spec.ts`, including a keyless local-Git `prepack` run through the bundled pnpm and an injected command directory.
|
10. **`loader/src/repository.ts`, `loader/tsdown.config.ts`, and the `@cordisjs/plugin-loader/repository` export**: the Node-only `RepositoryCache` installs one exact dependency specifier through the bundled `pnpm@11.7.0`, single-flights callers, and atomically publishes only a prepared package plus marker under the specifier hash. The subpath stays out of the browser-reachable Loader entry. Identical specifiers permanently reuse that entry; callers change the ref/specifier for another generation. Callers may prepend host-owned executable directories to the isolated package lifecycle `PATH`; all paths are resolved before the child starts. A transaction-owned `pnpm` wrapper makes pnpm's nested Git-package install reinvoke the same bundled entry with `--ignore-workspace`, so the selected package installs its own manifest dependencies instead of joining an enclosing source workspace. Temporary command directories are removed after the child settles. The isolated workspace permits dependency build scripts because a configured repository is executable code, while the child drops ambient credential-shaped variables. Covered by `packages/boot/app-boot/tests/repository-cache.spec.ts`, including a keyless local-Git `prepack` whose package is excluded from an enclosing pnpm lockfile and requires its own build dependency.
|
||||||
11. **Vendored Node-compatible TypeScript**: marked erased imports explicitly across `cordis`, `loader`, `include`, `hmr`, and `schemastery` so Node's native TypeScript transform does not request types as runtime exports. Schemastery's source uses an ESM default export and its package declares `type: module`; its built ESM/CJS entries retain explicit `.mjs`/`.cjs` extensions.
|
11. **Vendored Node-compatible TypeScript**: marked erased imports explicitly across `cordis`, `loader`, `include`, `hmr`, and `schemastery` so Node's native TypeScript transform does not request types as runtime exports. Schemastery's source uses an ESM default export and its package declares `type: module`; its built ESM/CJS entries retain explicit `.mjs`/`.cjs` extensions.
|
||||||
12. **`include/src/index.ts` patch-semantics export**: extracted the private `applyPatches` body into the exported pure function `applyEntryPatches(data, patches, warn)` (the method delegates to it) and exported the `!!js` YAML dialect as `entryListSchema`, so `dsh --dump-config` composes and prints exactly what the include would mount without booting a tree. Behavior-preserving for mounting; the extraction exists because config tooling must never reimplement (and drift from) the patch algorithm. `applyEntryPatches` also indexes each `insert`ed entry as it is added, so a later patch in the same list can configure or disable a row an earlier patch inserted; upstream built the id index once before the patch loop, leaving inserted rows silently unpatchable. That matters because `dsh` composes an empty profile root with each bundle's patch layer, the profile's and the home-level `cordis.patch.yml`, and any `--patch` overlays as sibling patch lists at one include level — patches never cross an include boundary, so surface-only rows would otherwise be unreachable from user config. Covered by `packages/boot/app-boot/tests/config-reload.spec.ts`.
|
12. **`include/src/index.ts` patch-semantics export**: extracted the private `applyPatches` body into the exported pure function `applyEntryPatches(data, patches, warn)` (the method delegates to it) and exported the `!!js` YAML dialect as `entryListSchema`, so `dsh --dump-config` composes and prints exactly what the include would mount without booting a tree. Behavior-preserving for mounting; the extraction exists because config tooling must never reimplement (and drift from) the patch algorithm. `applyEntryPatches` also indexes each `insert`ed entry as it is added, so a later patch in the same list can configure or disable a row an earlier patch inserted; upstream built the id index once before the patch loop, leaving inserted rows silently unpatchable. That matters because `dsh` composes an empty profile root with each bundle's patch layer, the profile's and the home-level `cordis.patch.yml`, and any `--patch` overlays as sibling patch lists at one include level — patches never cross an include boundary, so surface-only rows would otherwise be unreachable from user config. Covered by `packages/boot/app-boot/tests/config-reload.spec.ts`.
|
||||||
13. **`include/src/index.ts` serialized child-tree mutation and `hmr/src/index.ts` main-watcher initial-scan suppression**: every Include child-tree mutation (initial apply, refresh, `internal/update` patch re-application) runs through one per-Include queue, because the group's transactional `update` is not reentrant — two concurrent applies interleave create and rollback on the same entries and strand the Include fiber without ever settling. The HMR main watcher passes `ignoreInitial: true`: the initial scan re-announced files boot had just consumed, and its `add` for a config file refreshed an Include mid-initial-apply; once serialized, a failing initial apply's rollback disposed HMR, whose teardown drain waited on the queued refresh sitting behind that same apply — a deadlock that exited 13 with no diagnostic. `registerConfig()` keeps its own `ignoreInitial: false` watcher because a user patch layer present at registration must apply once. Covered by the patch-overlay boot-failure built-bin case in `apps/cli/tests/built-bin.e2e.ts`.
|
13. **`include/src/index.ts` serialized child-tree mutation and `hmr/src/index.ts` main-watcher initial-scan suppression**: every Include child-tree mutation (initial apply, refresh, `internal/update` patch re-application) runs through one per-Include queue, because the group's transactional `update` is not reentrant — two concurrent applies interleave create and rollback on the same entries and strand the Include fiber without ever settling. The HMR main watcher passes `ignoreInitial: true`: the initial scan re-announced files boot had just consumed, and its `add` for a config file refreshed an Include mid-initial-apply; once serialized, a failing initial apply's rollback disposed HMR, whose teardown drain waited on the queued refresh sitting behind that same apply — a deadlock that exited 13 with no diagnostic. `registerConfig()` keeps its own `ignoreInitial: false` watcher because a user patch layer present at registration must apply once. Covered by the patch-overlay boot-failure built-bin case in `apps/cli/tests/built-bin.e2e.ts`.
|
||||||
|
|||||||
Vendored
+48
-22
@@ -8,6 +8,7 @@ import { spawn } from 'node:child_process'
|
|||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
import { mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
|
import { mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
|
||||||
import { createRequire } from 'node:module'
|
import { createRequire } from 'node:module'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
import { delimiter, dirname, join, resolve } from 'node:path'
|
import { delimiter, dirname, join, resolve } from 'node:path'
|
||||||
|
|
||||||
/** Exact pnpm release shipped with the Loader for repository installation. */
|
/** Exact pnpm release shipped with the Loader for repository installation. */
|
||||||
@@ -48,6 +49,14 @@ function installEnvironment(executableDirectories: readonly string[]): NodeJS.Pr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shellQuote(value: string): string {
|
||||||
|
return `'${value.replaceAll("'", "'\\''")}'`
|
||||||
|
}
|
||||||
|
|
||||||
|
function batchQuote(value: string): string {
|
||||||
|
return `"${value.replaceAll('%', '%%')}"`
|
||||||
|
}
|
||||||
|
|
||||||
function appendOutput(current: string, chunk: Uint8Array): string {
|
function appendOutput(current: string, chunk: Uint8Array): string {
|
||||||
const combined = current + Buffer.from(chunk).toString('utf8')
|
const combined = current + Buffer.from(chunk).toString('utf8')
|
||||||
return combined.length <= MAX_ERROR_OUTPUT ? combined : combined.slice(-MAX_ERROR_OUTPUT)
|
return combined.length <= MAX_ERROR_OUTPUT ? combined : combined.slice(-MAX_ERROR_OUTPUT)
|
||||||
@@ -60,29 +69,46 @@ async function installWithBundledPnpm(
|
|||||||
const require = createRequire(import.meta.url)
|
const require = createRequire(import.meta.url)
|
||||||
const pnpmManifest = require.resolve('pnpm')
|
const pnpmManifest = require.resolve('pnpm')
|
||||||
const pnpmBin = join(dirname(pnpmManifest), 'bin', 'pnpm.mjs')
|
const pnpmBin = join(dirname(pnpmManifest), 'bin', 'pnpm.mjs')
|
||||||
let output = ''
|
const commandDirectory = await mkdtemp(join(tmpdir(), 'cordis-repository-pnpm-'))
|
||||||
const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {
|
try {
|
||||||
const child = spawn(process.execPath, [
|
await Promise.all([
|
||||||
pnpmBin,
|
writeFile(join(commandDirectory, 'pnpm'), [
|
||||||
'install',
|
'#!/bin/sh',
|
||||||
'--no-frozen-lockfile',
|
`exec ${shellQuote(process.execPath)} ${shellQuote(pnpmBin)} --ignore-workspace "$@"`,
|
||||||
'--reporter=append-only',
|
'',
|
||||||
], {
|
].join('\n'), { mode: 0o700 }),
|
||||||
cwd: directory,
|
writeFile(join(commandDirectory, 'pnpm.cmd'), [
|
||||||
env: installEnvironment(executableDirectories),
|
'@echo off',
|
||||||
shell: false,
|
`${batchQuote(process.execPath)} ${batchQuote(pnpmBin)} --ignore-workspace %*`,
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
'',
|
||||||
|
].join('\r\n'), { mode: 0o700 }),
|
||||||
|
])
|
||||||
|
let output = ''
|
||||||
|
const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {
|
||||||
|
const child = spawn(process.execPath, [
|
||||||
|
pnpmBin,
|
||||||
|
'install',
|
||||||
|
'--no-frozen-lockfile',
|
||||||
|
'--reporter=append-only',
|
||||||
|
], {
|
||||||
|
cwd: directory,
|
||||||
|
env: installEnvironment([commandDirectory, ...executableDirectories]),
|
||||||
|
shell: false,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
})
|
||||||
|
child.stdout.on('data', (chunk: Uint8Array) => { output = appendOutput(output, chunk) })
|
||||||
|
child.stderr.on('data', (chunk: Uint8Array) => { output = appendOutput(output, chunk) })
|
||||||
|
child.once('error', reject)
|
||||||
|
child.once('close', (code, signal) => { resolve({ code, signal }) })
|
||||||
})
|
})
|
||||||
child.stdout.on('data', (chunk: Uint8Array) => { output = appendOutput(output, chunk) })
|
if (result.signal !== null) {
|
||||||
child.stderr.on('data', (chunk: Uint8Array) => { output = appendOutput(output, chunk) })
|
throw new Error(`bundled pnpm install was killed by ${result.signal}${output ? `\n${output.trimEnd()}` : ''}`)
|
||||||
child.once('error', reject)
|
}
|
||||||
child.once('close', (code, signal) => { resolve({ code, signal }) })
|
if (result.code !== 0) {
|
||||||
})
|
throw new Error(`bundled pnpm install exited with code ${String(result.code)}${output ? `\n${output.trimEnd()}` : ''}`)
|
||||||
if (result.signal !== null) {
|
}
|
||||||
throw new Error(`bundled pnpm install was killed by ${result.signal}${output ? `\n${output.trimEnd()}` : ''}`)
|
} finally {
|
||||||
}
|
await rm(commandDirectory, { recursive: true, force: true })
|
||||||
if (result.code !== 0) {
|
|
||||||
throw new Error(`bundled pnpm install exited with code ${String(result.code)}${output ? `\n${output.trimEnd()}` : ''}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user