mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
174 lines
7.8 KiB
TypeScript
174 lines
7.8 KiB
TypeScript
/**
|
|
* Repository knowledge for the packer: where this tree's workspaces, profile
|
|
* composition, and config trees are, and how to report a pack.
|
|
*
|
|
* The library half takes all of this as parameters. Keeping the lookup here is what
|
|
* lets the same library pack a different tree, and what keeps `pack.ts` free of
|
|
* assumptions about pnpm workspaces or the `dsh` CLI.
|
|
* @module @deepseek-ai/dsh-experimental-webworker-packer/src/repository
|
|
*/
|
|
import { execFileSync } from 'node:child_process'
|
|
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join, relative } from 'node:path'
|
|
import { DSH_HOME_ENV } from '@deepseek-ai/dsh-home-paths'
|
|
import type { ConfigTree, PackResult } from './pack.ts'
|
|
|
|
/**
|
|
* Repository directories scanned for workspace and vendored packages. The
|
|
* image only ever materializes runtime packages, which live here. The Landlock
|
|
* package family contributes its unchanged JavaScript entry from `native/`;
|
|
* examples and python never occur on a roster's dependency chain.
|
|
*/
|
|
const WORKSPACE_SCAN_ROOTS = ['vendor', 'packages', 'native/landlock-run/packages', 'apps']
|
|
|
|
/** Composition entry point package: the `dsh` CLI, run from source. */
|
|
const CLI_PACKAGE = 'apps/cli'
|
|
|
|
/** Composition entry point: the `dsh` CLI, run from source. */
|
|
const CLI_ENTRY = `${CLI_PACKAGE}/src/bin.ts`
|
|
|
|
/**
|
|
* Index every workspace and vendored package by name.
|
|
* @param repoRoot - Absolute repository root.
|
|
* @returns Package name to absolute directory.
|
|
*/
|
|
export function indexWorkspacePackages(repoRoot: string): Map<string, string> {
|
|
const index = new Map<string, string>()
|
|
const visit = (directory: string): void => {
|
|
const manifest = join(directory, 'package.json')
|
|
if (existsSync(manifest)) {
|
|
const name = (JSON.parse(readFileSync(manifest, 'utf8')) as { name?: unknown }).name
|
|
if (typeof name === 'string') index.set(name, directory)
|
|
// A package root owns its subtree; anything below (test fixtures,
|
|
// nested manifests) is not a separate workspace package.
|
|
return
|
|
}
|
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
if (!entry.isDirectory()) continue
|
|
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
|
|
visit(join(directory, entry.name))
|
|
}
|
|
}
|
|
for (const scanRoot of WORKSPACE_SCAN_ROOTS) {
|
|
const absolute = join(repoRoot, scanRoot)
|
|
if (existsSync(absolute)) visit(absolute)
|
|
}
|
|
return index
|
|
}
|
|
|
|
/**
|
|
* Compose one profile through the real CLI dump path, leaving `!!js`
|
|
* unevaluated. The dump runs against a throwaway Harness home and default
|
|
* layers only, so the image is the shipped profile: the machine's `$DSH_HOME`
|
|
* — its profile manifest with locally installed bundles, and its patch files —
|
|
* would otherwise leak this machine's plugins into the image and break the
|
|
* same-tree-same-bytes guarantee.
|
|
* @param repoRoot - Absolute repository root.
|
|
* @param profile - Profile name to compose.
|
|
* @returns The composed YAML.
|
|
*/
|
|
export function composeProfile(repoRoot: string, profile: string): string {
|
|
const home = mkdtempSync(join(tmpdir(), 'dsh-pack-home-'))
|
|
try {
|
|
return execFileSync(
|
|
process.execPath,
|
|
['--import', 'tsx/esm', join(repoRoot, CLI_ENTRY), '--profile', profile, '--dump-default-config'],
|
|
{ cwd: repoRoot, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, env: { ...process.env, [DSH_HOME_ENV]: home } },
|
|
)
|
|
} finally {
|
|
rmSync(home, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
/** One `dsh.configTrees` declaration entry, validated field by field. */
|
|
interface ConfigTreeDeclaration {
|
|
mount: string
|
|
path: string
|
|
scanRoster?: boolean
|
|
}
|
|
|
|
/**
|
|
* Config trees the CLI package declares for deployment images
|
|
* (`dsh.configTrees` in its package.json): `path` is relative to the CLI
|
|
* package root, `mount` is the image path, `scanRoster` feeds the tree's yml
|
|
* plugin rows into the pack roster. The CLI owns its config layout; this
|
|
* reader follows the declaration instead of naming directories. A malformed
|
|
* declaration refuses the pack.
|
|
* @param repoRoot - Absolute repository root.
|
|
* @returns Trees with absolute source directories.
|
|
*/
|
|
export function configTrees(repoRoot: string): ConfigTree[] {
|
|
const packageDir = join(repoRoot, CLI_PACKAGE)
|
|
const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')) as {
|
|
dsh?: { configTrees?: unknown }
|
|
}
|
|
const declared = manifest.dsh?.configTrees
|
|
if (declared === undefined) return []
|
|
if (!Array.isArray(declared)) {
|
|
throw new Error(`vfs image: ${CLI_PACKAGE} dsh.configTrees must be an array`)
|
|
}
|
|
const mounts = new Set<string>()
|
|
return declared.map((entry, index) => {
|
|
const tree = entry as Partial<ConfigTreeDeclaration> | null
|
|
const at = `${CLI_PACKAGE} dsh.configTrees[${String(index)}]`
|
|
if (tree === null || typeof tree !== 'object'
|
|
|| typeof tree.mount !== 'string' || tree.mount === ''
|
|
|| typeof tree.path !== 'string' || tree.path === ''
|
|
|| (tree.scanRoster !== undefined && typeof tree.scanRoster !== 'boolean')) {
|
|
throw new Error(`vfs image: ${at} must declare a string mount, a string path, and an optional boolean scanRoster`)
|
|
}
|
|
if (mounts.has(tree.mount)) {
|
|
throw new Error(`vfs image: ${at} repeats mount ${JSON.stringify(tree.mount)}`)
|
|
}
|
|
mounts.add(tree.mount)
|
|
return {
|
|
mount: tree.mount,
|
|
directory: join(packageDir, tree.path),
|
|
...tree.scanRoster === undefined ? {} : { scanRoster: tree.scanRoster },
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Render one pack as the lines a build log should carry.
|
|
*
|
|
* Refusals and unresolved dependencies are the two states a reader must not miss, so
|
|
* they are spelled out rather than counted.
|
|
* @param result - What the pack produced.
|
|
* @param repoRoot - Absolute repository root, for relative paths.
|
|
* @param outputFile - Where the image was written.
|
|
* @returns Lines to print.
|
|
*/
|
|
export function describePack(result: PackResult, repoRoot: string, outputFile: string): string[] {
|
|
const sizeOf = (prefix: string): number => Object.entries(result.files)
|
|
.filter(([name]) => name.startsWith(prefix))
|
|
.reduce((sum, [, bytes]) => sum + bytes.byteLength, 0)
|
|
const megabytes = (bytes: number): string => `${(bytes / 1024 / 1024).toFixed(2)} MB`
|
|
const workspaceCount = result.workspacePackages
|
|
const heaviest = [...result.packages.entries()]
|
|
.map(([name, count]) => ({ name, count, bytes: sizeOf(`node_modules/${name}/`) }))
|
|
.sort((left, right) => right.bytes - left.bytes)
|
|
.slice(0, 12)
|
|
|
|
return [
|
|
`vfs image: ${relative(repoRoot, outputFile)}`,
|
|
` roster entries ${String(result.roster.length)}`,
|
|
` packages ${String(result.packages.size)} (${String(workspaceCount)} workspace)`,
|
|
` files ${String(Object.keys(result.files).length)}`,
|
|
` raw ${megabytes(Object.values(result.files).reduce((sum, bytes) => sum + bytes.byteLength, 0))}`,
|
|
` compressed ${megabytes(result.image.byteLength)}`,
|
|
` config + presets ${megabytes(sizeOf('config/'))}`,
|
|
` javascript entries ${String(result.javascriptEntries)} (dropped ${String(result.executables.length)} executable scripts, ${String(result.pageBundles.length)} page bundles verbatim)`,
|
|
` wrapper contract ${result.contract}`,
|
|
` transform ${String(result.transform.rewritten)} of ${String(result.transform.visited)} reached entries rewritten, ${String(result.droppedJavascriptEntries)} unreachable dropped`,
|
|
` unresolved ${String(result.unresolvedExternalRequests.length)} third-party request(s) left to fail loud at require time`,
|
|
' heaviest packages:',
|
|
...heaviest.map(entry => ` ${entry.bytes.toString().padStart(9)} B ${entry.name} (${String(entry.count)} files)`),
|
|
...result.missing.length === 0
|
|
? []
|
|
: [' unresolved dependencies:', ...result.missing.map(entry => ` ${entry}`)],
|
|
'',
|
|
]
|
|
}
|