/** Select and copy the local npm tarball closures that supply Desktop dsh and its private Host. */ import { createHash } from 'node:crypto' import { constants, copyFileSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs' import { basename, join, resolve } from 'node:path' import { parseArgs } from 'node:util' import { DESKTOP_HOST_PACKAGE, DESKTOP_HOST_RUNTIME_FILES, DESKTOP_PACKAGES_DIR, DESKTOP_PACKAGE_SET_FILE, parseDesktopCorePackageSet, type DesktopCorePackageRecord, } from '../src/core-package-set.ts' import { capture } from '../../../scripts/release/process.ts' import { tarballFiles } from '../../../scripts/release/tarball.ts' import { resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs' const DSH_PACKAGE = '@deepseek-ai/dsh' const ROOT_PACKAGES = [DSH_PACKAGE, DESKTOP_HOST_PACKAGE] as const const APP_ROOT = resolve(import.meta.dirname, '..') const REPOSITORY_ROOT = resolve(APP_ROOT, '..', '..') const REQUIRED_DEPENDENCY_SECTIONS = ['dependencies', 'peerDependencies'] as const const OPTIONAL_DEPENDENCY_SECTION = 'optionalDependencies' /** Packed package information needed to form the local Desktop closure. */ export interface PackedDesktopPackage { readonly tarball: string readonly manifest: Readonly> } function dependencyNames(manifest: Readonly>, section: string): string[] { const value = manifest[section] if (value === undefined) return [] if (value === null || typeof value !== 'object' || Array.isArray(value)) { throw new Error(`desktop package set: ${String(manifest.name)} has invalid ${section}`) } return Object.keys(value).sort() } /** * Select the complete available first-party dependency closures rooted at dsh and its private Host. * @param available - Packed packages indexed by package name. * @returns Selected packages sorted by name. */ export function selectDesktopPackageClosure( available: ReadonlyMap, ): PackedDesktopPackage[] { const selected = new Map() const visit = (name: string): void => { if (selected.has(name)) return const packed = available.get(name) if (packed === undefined) throw new Error(`desktop package set: packed inputs omit required package ${name}`) selected.set(name, packed) for (const section of REQUIRED_DEPENDENCY_SECTIONS) { for (const dependency of dependencyNames(packed.manifest, section)) { if (available.has(dependency)) visit(dependency) else if (dependency.startsWith('@deepseek-ai/')) { throw new Error(`desktop package set: ${name} requires unpacked internal package ${dependency}`) } } } for (const dependency of dependencyNames(packed.manifest, OPTIONAL_DEPENDENCY_SECTION)) { if (available.has(dependency)) visit(dependency) } } for (const name of ROOT_PACKAGES) { if (!available.has(name)) throw new Error(`desktop package set: packed inputs omit ${name}`) visit(name) } return [...selected.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([, packed]) => packed) } function packedManifest(tarball: string): Record { const value: unknown = JSON.parse(capture('tar', ['-xOzf', tarball, 'package/package.json'])) if (value === null || typeof value !== 'object' || Array.isArray(value)) { throw new Error(`desktop package set: ${tarball} has no package manifest`) } return value as Record } function packedPackages(inputs: readonly string[]): Map { const available = new Map() for (const input of inputs) { const tarballs = readdirSync(input).filter(file => file.endsWith('.tgz')).sort() if (tarballs.length === 0) throw new Error(`desktop package set: ${input} contains no tarballs`) for (const file of tarballs) { const tarball = join(input, file) const manifest = packedManifest(tarball) const name = manifest.name if (typeof name !== 'string' || name === '') throw new Error(`desktop package set: ${tarball} has no package name`) if (available.has(name)) throw new Error(`desktop package set: duplicate packed package ${name}`) available.set(name, { tarball, manifest }) } } return available } /** * Require every private Host file used before the Desktop profile can pass its health check. * @param files - Tarball paths rooted at `package/`. * @returns Nothing. */ export function assertDesktopHostPackageFiles(files: readonly string[]): void { const available = new Set(files) const missing = DESKTOP_HOST_RUNTIME_FILES .map(file => `package/${file}`) .filter(file => !available.has(file)) if (missing.length > 0) { throw new Error(`desktop package set: ${DESKTOP_HOST_PACKAGE} tarball omits required file(s): ${missing.join(', ')}`) } } /** Prepare a package set from release tarball directories. */ export function prepareDesktopPackageSet(inputs: readonly string[], output: string): void { const selected = selectDesktopPackageClosure(packedPackages(inputs)) const host = selected.find(packed => packed.manifest.name === DESKTOP_HOST_PACKAGE) if (host === undefined) throw new Error(`desktop package set: selected closure omits ${DESKTOP_HOST_PACKAGE}`) assertDesktopHostPackageFiles(tarballFiles(host.tarball)) rmSync(output, { recursive: true, force: true }) const packageDir = join(output, DESKTOP_PACKAGES_DIR) mkdirSync(packageDir, { recursive: true }) const records: DesktopCorePackageRecord[] = selected.map((packed) => { const name = packed.manifest.name const version = packed.manifest.version if (typeof name !== 'string' || typeof version !== 'string') { throw new Error(`desktop package set: ${packed.tarball} has no package identity`) } const file = basename(packed.tarball) const destination = join(packageDir, file) copyFileSync(packed.tarball, destination, constants.COPYFILE_EXCL) const body = readFileSync(destination) return { name, version, file, bytes: statSync(destination).size, integrity: `sha512-${createHash('sha512').update(body).digest('base64')}`, } }) const packageSet = parseDesktopCorePackageSet({ schemaVersion: 1, packages: records }) writeFileSync(join(output, DESKTOP_PACKAGE_SET_FILE), `${JSON.stringify(packageSet, undefined, 2)}\n`, { mode: 0o600 }) } function main(): void { const buildPaths = resolveDesktopTargetBuildPaths() const defaultInputs = [ buildPaths.packedDsh, buildPaths.packedVendor, buildPaths.packedLandlock, ] const { values } = parseArgs({ options: { from: { type: 'string', multiple: true }, out: { type: 'string' } }, allowPositionals: false, }) const inputs = (values.from ?? defaultInputs).map(path => resolve(REPOSITORY_ROOT, path)) const output = values.out === undefined ? buildPaths.packageSet : resolve(REPOSITORY_ROOT, values.out) prepareDesktopPackageSet(inputs, output) console.log(`desktop package set: prepared ${output}`) } if (import.meta.main) main()