Files
deepseek-harness/apps/desktop/scripts/development-project.ts
T
2026-08-31 11:54:28 +08:00

110 lines
4.0 KiB
TypeScript

/** 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
/** 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<typeof lstatSync>
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 desktopHost = join(options.cliDir, 'lib', 'desktop-host.js')
if (!existsSync(desktopHost)) {
throw new Error('desktop development: apps/cli/lib/desktop-host.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)
return options.projectDir
}