Files
deepseek-harness/apps/desktop/src/project-manager.ts
T
imccyu 02fec3209e fix(boot): remove link-backend profile projections on profile load
removeLinkProjections unlinks the symlinks under a profile's node_modules whose target lies inside <profile>/.dsh-module-fallback/node_modules and then removes that directory; pnpm-installed packages and every other symlink stay. loadProfile and the Desktop applyRelease call it once per launch. Regression: with bundle A installed but deselected and bundle B selected, B's same-named plugin resolves instead of A's projected copy.
2026-09-19 16:34:07 +08:00

177 lines
6.8 KiB
TypeScript

/** Desktop profile initialization and native recovery. */
import {
existsSync,
fsyncSync,
lstatSync,
mkdirSync,
openSync,
realpathSync,
closeSync,
readFileSync,
unlinkSync,
writeFileSync,
writeSync,
} from 'node:fs'
import { join } from 'node:path'
import {
DESKTOP_HOST_PACKAGE,
desktopCorePackageOverrides,
verifyDesktopCorePackageSet,
} from './core-package-set.ts'
import type { DesktopPaths } from './paths.ts'
import type { DesktopRelease } from './release.ts'
import { readDesktopRuntime } from './runtime-tree.ts'
import {
initProfile, PROFILE_TEMPLATES, removeLinkProjections, sanitizeProfile, type ProfileTemplate,
} from '@deepseek-ai/dsh-app-boot'
const PROJECT_NAME = '@deepseek-ai/dsh-desktop-runtime'
const DSH_PACKAGE = '@deepseek-ai/dsh'
const CORE_BUILD_PACKAGE = '@deepseek-ai/dsh-subprocess-local'
const WEB_PROFILE = PROFILE_TEMPLATES.web as ProfileTemplate
const WORKSPACE_SETTINGS = 'nodeLinker: hoisted\nautoInstallPeers: false\n'
function writeJson(path: string, value: unknown): void {
writeFileSync(path, `${JSON.stringify(value, undefined, 2)}\n`, { mode: 0o600 })
}
function workspaceFile(overrides: Readonly<Record<string, string>> = {}): string {
const entries = Object.entries(overrides).sort(([left], [right]) => left.localeCompare(right))
const overrideSection = entries.length === 0
? ''
: `overrides:\n${entries.map(([name, spec]) => ` ${JSON.stringify(name)}: ${JSON.stringify(spec)}`).join('\n')}\n`
if (entries.length === 0) return `packages:\n - .\n\n${WORKSPACE_SETTINGS}`
const coreBuildSpec = overrides[CORE_BUILD_PACKAGE]
const coreBuildKey = coreBuildSpec === undefined
? CORE_BUILD_PACKAGE
: `${CORE_BUILD_PACKAGE}@${coreBuildSpec.replace('file:./', 'file:')}`
return `packages:\n - .\n\n${overrideSection}${WORKSPACE_SETTINGS}allowBuilds:\n node-pty: true\n koffi: true\n fs-ext: true\n ${JSON.stringify(coreBuildKey)}: true\n '@google/genai': false\n protobufjs: false\n node-addon-require-builtin: false\n`
}
function migrateProfileSettings(projectDir: string): void {
const path = join(projectDir, 'pnpm-workspace.yaml')
if (!existsSync(path)) return
const legacy = `packages:\n - .\n\n${WORKSPACE_SETTINGS}strictDepBuilds: true\nallowBuilds:\n node-pty: true\n koffi: true\n fs-ext: true\n "${CORE_BUILD_PACKAGE}": true\n '@google/genai': false\n protobufjs: false\n node-addon-require-builtin: false\n`
if (readFileSync(path, 'utf8').replaceAll('\r\n', '\n') === legacy) {
writeFileSync(path, workspaceFile())
}
}
/** Initializes the Desktop profile and disables third-party bundles during recovery. */
export class DesktopProjectManager {
/**
* @param paths - Electron-owned package state and reserved desktop profile paths.
* @param runtime - location of the bundled application runtime.
*/
constructor(
readonly paths: DesktopPaths,
readonly runtime: { readonly dsh: string },
) {}
/**
* Back up the profile patch and disable third-party bundles without loading application resources.
* The caller must stop the Host first.
* @returns Backup path after the locked profile write, or undefined if the patch was absent.
*/
async disableAllPlugins(): Promise<string | undefined> {
return this.withLock(() => sanitizeProfile('dsh', this.paths.profile, WEB_PROFILE.bundles))
}
/**
* Load application metadata and prepare the external plugin profile without installing packages.
*/
async applyRelease(): Promise<void> {
await this.withLock(() => {
// Validation only: an unreadable or mismatched runtime descriptor stops preparation before the Host starts.
readDesktopRuntime(this.runtime.dsh)
migrateProfileSettings(this.paths.profile)
createPluginProfile(this.paths.profile)
removeLinkProjections(this.paths.profile)
})
}
private async withLock<T>(operation: () => T | Promise<T>): Promise<T> {
mkdirSync(this.paths.profile, { recursive: true, mode: 0o700 })
const lockPath = join(realpathSync(this.paths.profile), 'lock')
let descriptor: number
try {
descriptor = openSync(lockPath, 'wx', 0o600)
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
const lock = lstatSync(lockPath)
if (lock.isSymbolicLink() || !lock.isFile()) {
throw new Error('desktop project: profile lock is not a regular file')
}
const owner = Number.parseInt(readFileSync(lockPath, 'utf8').trim(), 10)
let active = !Number.isSafeInteger(owner) || owner <= 0
if (!active) {
try {
process.kill(owner, 0)
active = true
} catch (signalError) {
active = (signalError as NodeJS.ErrnoException).code !== 'ESRCH'
}
}
if (active) throw new Error('desktop project: another profile operation is active')
unlinkSync(lockPath)
descriptor = openSync(lockPath, 'wx', 0o600)
} else {
throw error
}
}
try {
writeSync(descriptor, `${String(process.pid)}\n`)
fsyncSync(descriptor)
return await operation()
} finally {
closeSync(descriptor)
unlinkSync(lockPath)
}
}
}
/** Create build-only project metadata for materializing the signed runtime. */
export function createRuntimeProjectMetadata(projectDir: string, release: DesktopRelease): void {
mkdirSync(projectDir, { recursive: true, mode: 0o700 })
const packageSet = verifyDesktopCorePackageSet(projectDir, release.version)
const manifest = {
name: PROJECT_NAME,
private: true,
version: '0.0.0',
dependencies: desktopCorePackageOverrides(packageSet),
dsh: { profile: { bundles: [...WEB_PROFILE.bundles] } },
}
writeJson(join(projectDir, 'package.json'), manifest)
writeFileSync(
join(projectDir, 'pnpm-workspace.yaml'),
workspaceFile(desktopCorePackageOverrides(packageSet)),
{ mode: 0o600 },
)
}
/**
* Create metadata for the unpackaged development project that links the current workspace.
* @param projectDir - Disposable development profile directory.
* @param release - Release identity shared by the linked CLI package and Electron shell.
*/
export function createDevelopmentProjectMetadata(projectDir: string, release: DesktopRelease): void {
mkdirSync(projectDir, { recursive: true, mode: 0o700 })
const manifest = {
name: PROJECT_NAME,
private: true,
version: '0.0.0',
dependencies: {
[DSH_PACKAGE]: release.version,
[DESKTOP_HOST_PACKAGE]: release.version,
},
dsh: { profile: { bundles: [...WEB_PROFILE.bundles] } },
}
writeJson(join(projectDir, 'package.json'), manifest)
writeFileSync(join(projectDir, 'pnpm-workspace.yaml'), workspaceFile(), { mode: 0o600 })
}
/** Create the first external plugin profile without running a package manager. */
export function createPluginProfile(projectDir: string): void {
initProfile(projectDir, WEB_PROFILE.bundles)
}