Files
deepseek-harness/apps/desktop/scripts/development-project.ts
T
mektpoy 80e4da0e3d fix(desktop): take the development project target as an explicit input
`prepareDevelopmentProject()` resolved the build target from the host
platform and architecture, so a Linux host threw `unsupported target
linux-x64` while writing the runtime descriptor. Pass the target in
instead: the launcher supplies the target it prepared, and isolated tests
supply a fixed supported target.

Extract `desktopTargetPlatform()` into desktop-build-paths.mjs so the
target-to-payload mapping has one home, and use it for the qualification
runner, which previously derived both its target directory and its
descriptor from the host architecture.

Correct the README limitation to name the module that now performs the
architecture check.
2026-09-23 16:03:17 +08:00

170 lines
7.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,
writeFileSync,
} from 'node:fs'
import { dirname, join } from 'node:path'
import { createDevelopmentProjectMetadata } from '../src/project-manager.ts'
import type { DesktopRelease } from '../src/release.ts'
import { DESKTOP_RUNTIME_FILE, type DesktopRuntimeDescriptor } from '../src/runtime-tree.ts'
import type { DesktopAutoUpdateTarget } from './desktop-auto-update-environment.mjs'
import { desktopTargetPlatform } from './desktop-build-paths.mjs'
interface PackageManifest {
readonly name?: string
readonly version?: string
readonly dependencies?: Readonly<Record<string, 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
/** Build target whose prepared payload the disposable project runs against. */
readonly target: DesktopAutoUpdateTarget
}
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): string[] {
const names: string[] = []
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))
names.push(`${entry.name}/${scoped.name}`)
}
continue
}
if (entry.isDirectory() || entry.isSymbolicLink()) {
linkDirectory(source, join(destinationRoot, entry.name))
names.push(entry.name)
}
}
return names
}
/** Configured workspace plugins must resolve from the profile even when pnpm does not hoist them. */
function mirrorWorkspaceDependencies(roots: readonly string[], destinationRoot: string): string[] {
const names: string[] = []
const visited = new Set<string>()
const visit = (directory: string): void => {
const source = realpathSync(directory)
if (visited.has(source)) return
visited.add(source)
const manifest = readManifest(join(source, 'package.json'))
for (const [name, specifier] of Object.entries(manifest.dependencies ?? {})) {
if (!specifier.startsWith('workspace:')) continue
const dependency = join(source, 'node_modules', ...name.split('/'))
if (!existsSync(dependency)) {
throw new Error(`desktop development: ${name} is missing from ${source}; run pnpm install`)
}
const destination = join(destinationRoot, ...name.split('/'))
removeOwnedPath(destination)
linkDirectory(dependency, destination)
names.push(name)
visit(dependency)
}
}
for (const directory of roots) visit(directory)
return names
}
/**
* Replace one disposable project with links to the current built workspace.
* @param options - Project destination, CLI package, release identity, and build target.
* @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 })
const names = [
...mirrorDependencyLinks(options.dependencyDir, destinationModules),
...mirrorWorkspaceDependencies([options.cliDir, options.hostDir], 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)
const sharedPackages = [...new Set([...names, '@deepseek-ai/dsh', '@deepseek-ai/dsh-desktop-host'])].flatMap((name) => {
const manifest = readManifest(join(destinationModules, name, 'package.json'))
return typeof manifest.version === 'string' ? [{ name, version: manifest.version, path: `node_modules/${name}` }] : []
})
const runtime: DesktopRuntimeDescriptor = { schemaVersion: 1, release: options.release,
...desktopTargetPlatform(options.target), sharedPackages, files: [] }
writeFileSync(join(options.projectDir, DESKTOP_RUNTIME_FILE), `${JSON.stringify(runtime, undefined, 2)}\n`)
return options.projectDir
}