/** * Repository package discovery and NPM dependency-closure rewriting for live links. * * @module @deepseek-ai/dsh-helper/package-managers/link-workspace */ import { readFile, readdir } from 'node:fs/promises' import { existsSync, realpathSync } from 'node:fs' import { basename, dirname, join, relative, resolve, sep } from 'node:path' import type { PackageJsonFile, PackageManifest } from '../documents/package-json-file.ts' import { PnpmWorkspaceFile } from '../documents/pnpm-workspace-file.ts' import type { ProjectFile } from '../documents/project-file.ts' import type { PackageManager } from './package-manager.ts' interface WorkspacePackage { directory: string manifest: PackageManifest } function posixPath(path: string): string { return path.split(sep).join('/') } function canonicalPath(path: string): string { let existing = resolve(path) const suffix: string[] = [] while (!existsSync(existing)) { const parent = dirname(existing) /* v8 ignore next -- every absolute path reaches the existing filesystem root */ if (parent === existing) throw new Error(`cannot resolve an existing ancestor for ${path}`) suffix.unshift(basename(existing)) existing = parent } return resolve(realpathSync(existing), ...suffix) } async function packageDirectories(root: string): Promise { const result: string[] = [] for (const vendor of await readdir(join(root, 'vendor'), { withFileTypes: true })) { if (vendor.isDirectory()) result.push(join(root, 'vendor', vendor.name)) } for (const group of await readdir(join(root, 'packages'), { withFileTypes: true })) { if (!group.isDirectory()) continue for (const pkg of await readdir(join(root, 'packages', group.name), { withFileTypes: true })) { if (pkg.isDirectory()) result.push(join(root, 'packages', group.name, pkg.name)) } } return result } /** Index of repository packages used by `--link-workspace`. */ export class LinkWorkspace { readonly root: string private readonly packages: Map private constructor(root: string, packages: Map) { this.root = root this.packages = packages } /** Scan vendor and package workspaces from a repository root. */ static async open(root: string): Promise { const absolute = resolve(root) const packages = new Map() for (const directory of await packageDirectories(absolute)) { let manifest: PackageManifest try { manifest = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8')) as PackageManifest } catch (error) { throw new Error(`cannot read linked package at ${directory}: ${String(error)}`) } if (!manifest.name || typeof manifest.name !== 'string') continue if (packages.has(manifest.name)) throw new Error(`duplicate linked package name: ${manifest.name}`) packages.set(manifest.name, { directory, manifest }) } if (!packages.has('@deepseek-ai/cordis') || !packages.has('@deepseek-ai/dsh-scripts')) { throw new Error(`not a DeepSeek Harness repository root: ${absolute}`) } return new LinkWorkspace(absolute, packages) } /** Expand direct NPM dependencies through all repository-local NPM dependency edges. */ closure(names: Iterable): string[] { const pending = [...names] const result = new Set() while (pending.length > 0) { const name = pending.pop() if (!name || result.has(name)) continue const pkg = this.packages.get(name) /* v8 ignore next -- closure() only returns names present in this package map */ if (!pkg) continue result.add(name) const edges = { ...pkg.manifest.dependencies, ...pkg.manifest.peerDependencies as Record | undefined, } for (const dependencyName of Object.keys(edges)) { if (this.packages.has(dependencyName) && !result.has(dependencyName)) pending.push(dependencyName) } } return [...result].sort() } /** Rewrite the full local closure to manager-specific live-link specs. */ apply( projectRoot: string, manifest: PackageJsonFile, manager: PackageManager, documents: readonly ProjectFile[], ): void { const canonicalProjectRoot = canonicalPath(projectRoot) const names = this.closure(manifest.npmDependencyNames()) for (const name of names) { const pkg = this.packages.get(name) /* v8 ignore next -- closure() only returns names present in this package map */ if (!pkg) continue const relativePath = posixPath(relative(canonicalProjectRoot, realpathSync(pkg.directory))) const spec = manager.linkSpec(relativePath) const current = manifest.npmDependency(name) manifest.setNpmDependency(current?.section ?? 'dependencies', name, spec) if (manager.name === 'yarn') manifest.setResolution(name, spec) } if (manager.name === 'pnpm') { const workspace = documents.find((item): item is PnpmWorkspaceFile => item instanceof PnpmWorkspaceFile) if (!workspace) throw new Error('pnpm link mode requires pnpm-workspace.yaml') workspace.disableAutoInstallPeers() } } /** Resolve a package directory for diagnostics and tests. */ packageDirectory(name: string): string | undefined { const directory = this.packages.get(name)?.directory return directory ? resolve(dirname(directory), directory.split(sep).at(-1) as string) : undefined } /** * Rewrite one nested generated manifest's local dependencies to live-link specs. * * A generated workspace member resolves its own dependencies, so every local * name it declares must point into this repository as well: none of them — * the harness packages or the rescoped framework — exists on a public * registry, so a semver spec there fails the install outright. * `peerDependencies` keeps its range because a peer states what the consumer * must supply, and package managers reject a link spec in that section. * @param projectRoot - Absolute root of the generated project. * @param manifestPath - The nested manifest's project-relative POSIX path. * @param text - The nested manifest's complete current text. * @param manager - Package manager whose link-spec form applies. * @returns The manifest text with every resolved local dependency relinked. */ relinkNestedManifest(projectRoot: string, manifestPath: string, text: string, manager: PackageManager): string { const manifest = JSON.parse(text) as Record const manifestDirectory = resolve(canonicalPath(projectRoot), dirname(manifestPath)) let changed = false for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) { const dependencies = manifest[section] if (typeof dependencies !== 'object' || dependencies === null) continue for (const [name] of Object.entries(dependencies as Record)) { const pkg = this.packages.get(name) if (!pkg) continue const relativePath = posixPath(relative(manifestDirectory, realpathSync(pkg.directory))) ;(dependencies as Record)[name] = manager.linkSpec(relativePath) changed = true } } return changed ? `${JSON.stringify(manifest, null, 2)}\n` : text } }