/** Prepare the disposable npm-project view used by an unpackaged Electron shell. */ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, symlinkSync, unlinkSync, } from 'node:fs' import { dirname, join } from 'node:path' import { createDevelopmentProjectMetadata } from '../src/project-manager.ts' import type { DesktopRelease } from '../src/release.ts' interface PackageManifest { readonly name?: string readonly version?: string } /** Inputs whose locations differ between the launcher and isolated tests. */ export interface DevelopmentProjectOptions { /** Directory replaced with the generated development project. */ readonly projectDir: string /** Current workspace's `apps/cli` package directory. */ readonly cliDir: string /** Current workspace's private Desktop Host application directory. */ readonly hostDir: string /** pnpm's workspace-wide virtual-hoist directory. */ readonly dependencyDir: string /** Release identity written into the disposable project metadata. */ readonly release: DesktopRelease } function readManifest(path: string): PackageManifest { return JSON.parse(readFileSync(path, 'utf8')) as PackageManifest } function removeOwnedPath(path: string): void { let stat: ReturnType try { stat = lstatSync(path) } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return throw error } if (stat.isSymbolicLink()) { unlinkSync(path) return } if (stat.isDirectory()) { rmSync(path, { recursive: true }) return } unlinkSync(path) } function linkDirectory(source: string, destination: string): void { mkdirSync(dirname(destination), { recursive: true }) symlinkSync(realpathSync(source), destination, process.platform === 'win32' ? 'junction' : 'dir') } function mirrorDependencyLinks(sourceRoot: string, destinationRoot: string): void { for (const entry of readdirSync(sourceRoot, { withFileTypes: true })) { if (entry.name === '.bin') continue const source = join(sourceRoot, entry.name) if (entry.name.startsWith('@') && (entry.isDirectory() || entry.isSymbolicLink())) { mkdirSync(join(destinationRoot, entry.name), { recursive: true }) for (const scoped of readdirSync(source, { withFileTypes: true })) { if (!scoped.isDirectory() && !scoped.isSymbolicLink()) continue linkDirectory(join(source, scoped.name), join(destinationRoot, entry.name, scoped.name)) } continue } if (entry.isDirectory() || entry.isSymbolicLink()) linkDirectory(source, join(destinationRoot, entry.name)) } } /** * Replace one disposable project with links to the current built workspace. * @param options - Project destination, CLI package, and release identity. * @returns the absolute project directory supplied by the caller. */ export function prepareDevelopmentProject(options: DevelopmentProjectOptions): string { const cliManifest = readManifest(join(options.cliDir, 'package.json')) if (cliManifest.name !== '@deepseek-ai/dsh' || cliManifest.version !== options.release.version) { throw new Error( `desktop development: apps/cli must be @deepseek-ai/dsh@${options.release.version}, found ` + `${String(cliManifest.name)}@${String(cliManifest.version)}`, ) } if (!existsSync(options.dependencyDir)) { throw new Error('desktop development: workspace dependency links are missing; run pnpm install') } const hostManifest = readManifest(join(options.hostDir, 'package.json')) if (hostManifest.name !== '@deepseek-ai/dsh-desktop-host' || hostManifest.version !== options.release.version) { throw new Error( `desktop development: apps/desktop-host must be @deepseek-ai/dsh-desktop-host@${options.release.version}, found ` + `${String(hostManifest.name)}@${String(hostManifest.version)}`, ) } if (!existsSync(join(options.hostDir, 'lib', 'index.js'))) { throw new Error('desktop development: apps/desktop-host/lib/index.js is missing; run pnpm run build') } removeOwnedPath(options.projectDir) createDevelopmentProjectMetadata(options.projectDir, options.release) const destinationModules = join(options.projectDir, 'node_modules') mkdirSync(destinationModules, { recursive: true }) mirrorDependencyLinks(options.dependencyDir, destinationModules) const dshLink = join(destinationModules, '@deepseek-ai', 'dsh') removeOwnedPath(dshLink) linkDirectory(options.cliDir, dshLink) const hostLink = join(destinationModules, '@deepseek-ai', 'dsh-desktop-host') removeOwnedPath(hostLink) linkDirectory(options.hostDir, hostLink) return options.projectDir }