/** * Desktop / pnpm `link:` installs resolve `@deepseek-ai/*` from this package's * real path (the workspace), not from the profile `node_modules`. GitHub/file * installs work because the package is copied under the profile. * * Junction Host peer packages from the DSH installation into * `./node_modules/@deepseek-ai/*` so linked local installs can boot. * * Usage: * bun run link:peers * node scripts/link-dsh-peers.mjs --profile desktop * .\scripts\link-dsh-peers.ps1 */ import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, symlinkSync } from 'node:fs' import { homedir } from 'node:os' import { dirname, isAbsolute, join, relative, resolve } from 'node:path' import { fileURLToPath } from 'node:url' const root = join(dirname(fileURLToPath(import.meta.url)), '..') const dshHome = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh') /** Runtime imports from src/index.ts (+ commonly needed neighbors). */ const peers = [ 'schemastery', 'cordis', 'dsh-credentials', 'dsh-settings', 'dsh-tools', 'dsh-scope', 'dsh-llm', ] function profileArg() { const idx = process.argv.indexOf('--profile') if (idx >= 0 && process.argv[idx + 1]) return process.argv[idx + 1] return 'desktop' } /** * Select one complete set from the requested profile or shared Host fallback. * Never combine packages from different Host generations or other profiles. */ function candidateDirs(profile) { return [ join(dshHome, 'profiles', profile, 'node_modules', '@deepseek-ai'), join(dshHome, 'profiles', 'node_modules', '@deepseek-ai'), ] } function resolvePeer(shortName, dirs) { for (const dir of dirs) { const target = join(dir, shortName) if (existsSync(join(target, 'package.json'))) return target } return null } function linkOne(shortName, target) { const dest = join(root, 'node_modules', '@deepseek-ai', shortName) mkdirSync(dirname(dest), { recursive: true }) if (existsSync(dest)) { const within = relative(resolve(root, 'node_modules', '@deepseek-ai'), resolve(dest)) if (!within || within.startsWith('..') || isAbsolute(within) || within.includes('/') || within.includes('\\')) { throw new Error(`refusing to replace path outside the peer directory: ${dest}`) } const st = lstatSync(dest) if (st.isSymbolicLink()) { rmSync(dest, { force: true }) } else { throw new Error(`refusing to replace non-link path ${dest}`) } } symlinkSync(target, dest, 'junction') const ver = JSON.parse(readFileSync(join(target, 'package.json'), 'utf8')).version console.log(`linked @deepseek-ai/${shortName}@${ver} <- ${target}`) } const profile = profileArg() if (!/^[\w-]+$/.test(profile)) throw new Error('Invalid profile name.') const dirs = candidateDirs(profile) console.log(`DSH_HOME=${dshHome} profile=${profile}`) console.log(`search:\n ${dirs.join('\n ')}`) const coherent = dirs.find(dir => peers.every(name => existsSync(join(dir, name, 'package.json')))) if (!coherent) throw new Error('No complete Harness peer set found; refusing to mix package generations.') let missing = 0 for (const name of peers) { const target = resolvePeer(name, [coherent]) if (!target) { console.error(`missing peer @deepseek-ai/${name}`) missing++ continue } linkOne(name, target) } if (missing > 0) { console.error(`\n${missing} peer(s) missing — run Desktop/web once so profiles/node_modules is healed.`) process.exit(1) } console.log('\nDone. Restart DeepSeek Harness Desktop (or disable/enable dsh-netxops).')