netxops/scripts/link-dsh-peers.mjs

107 lines
3.7 KiB
JavaScript

/**
* 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'
}
/**
* Prefer the shared healed fallback `profiles/node_modules` (one coherent peer
* set). Do NOT prefer the Desktop
* profile's schemastery first — Desktop 0.2 ships 3.18.4 which, when linked
* alone into this package, makes `Config({})` return cosmokit volatile proxies
* instead of defaults and Host apply fails ("web boot: dsh-netxops failed").
* Do NOT createRequire() — that can walk into an unrelated checkout via NODE_PATH.
*/
function candidateDirs(profile) {
return [
join(dshHome, 'profiles', 'node_modules', '@deepseek-ai'),
join(dshHome, 'profiles', profile, 'node_modules', '@deepseek-ai'),
join(dshHome, 'profiles', 'web', '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()
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).')