mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(repository-cache): isolate Git package dependencies
This commit is contained in:
2
vendor/README.md
vendored
2
vendor/README.md
vendored
@@ -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.
|
||||
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`.
|
||||
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.
|
||||
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`.
|
||||
|
||||
70
vendor/loader/src/repository.ts
vendored
70
vendor/loader/src/repository.ts
vendored
@@ -8,6 +8,7 @@ import { spawn } from 'node:child_process'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, dirname, join, resolve } from 'node:path'
|
||||
|
||||
/** 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 {
|
||||
const combined = current + Buffer.from(chunk).toString('utf8')
|
||||
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 pnpmManifest = require.resolve('pnpm')
|
||||
const pnpmBin = join(dirname(pnpmManifest), 'bin', 'pnpm.mjs')
|
||||
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(executableDirectories),
|
||||
shell: false,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
const commandDirectory = await mkdtemp(join(tmpdir(), 'cordis-repository-pnpm-'))
|
||||
try {
|
||||
await Promise.all([
|
||||
writeFile(join(commandDirectory, 'pnpm'), [
|
||||
'#!/bin/sh',
|
||||
`exec ${shellQuote(process.execPath)} ${shellQuote(pnpmBin)} --ignore-workspace "$@"`,
|
||||
'',
|
||||
].join('\n'), { mode: 0o700 }),
|
||||
writeFile(join(commandDirectory, 'pnpm.cmd'), [
|
||||
'@echo off',
|
||||
`${batchQuote(process.execPath)} ${batchQuote(pnpmBin)} --ignore-workspace %*`,
|
||||
'',
|
||||
].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) })
|
||||
child.stderr.on('data', (chunk: Uint8Array) => { output = appendOutput(output, chunk) })
|
||||
child.once('error', reject)
|
||||
child.once('close', (code, signal) => { resolve({ code, signal }) })
|
||||
})
|
||||
if (result.signal !== null) {
|
||||
throw new Error(`bundled pnpm install was killed by ${result.signal}${output ? `\n${output.trimEnd()}` : ''}`)
|
||||
}
|
||||
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()}` : ''}`)
|
||||
}
|
||||
if (result.code !== 0) {
|
||||
throw new Error(`bundled pnpm install exited with code ${String(result.code)}${output ? `\n${output.trimEnd()}` : ''}`)
|
||||
}
|
||||
} finally {
|
||||
await rm(commandDirectory, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user