Fix Desktop link-install boot and show Netx Ops settings.

Prefer shared schemastery 3.18.2 peers (Desktop 3.18.4 alone breaks Config), and register settings.section eagerly with a deferred form scope so the Settings nav appears without waiting on configForms.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-10-09 19:28:32 +08:00
parent 3861006c13
commit 838ff2fed4
6 changed files with 487 additions and 149 deletions

100
scripts/link-dsh-peers.mjs Normal file
View file

@ -0,0 +1,100 @@
/**
* 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, join } 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-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: schemastery 3.18.2 + dsh-* 0.1.5-rc.2). 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 st = lstatSync(dest)
if (st.isSymbolicLink() || st.isDirectory()) {
rmSync(dest, { recursive: true, 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 ')}`)
let missing = 0
for (const name of peers) {
const target = resolvePeer(name, dirs)
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).')

View file

@ -11,13 +11,20 @@
# .\scripts\link-dsh-peers.ps1 -DshHome $env:DSH_HOME
param(
[string]$DshHome = $(if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE '.dsh' })
[string]$DshHome = $(if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE '.dsh' }),
[string]$Profile = 'desktop'
)
$ErrorActionPreference = 'Stop'
$Root = Split-Path -Parent $PSScriptRoot
$SrcRoot = Join-Path $DshHome 'profiles\node_modules\@deepseek-ai'
$DstRoot = Join-Path $Root 'node_modules\@deepseek-ai'
# Shared heal first — profile-local schemastery (Desktop 0.2 = 3.18.4) breaks
# Config({}) when mixed with shared dsh-* 0.1.5-rc.2 (volatile proxies, Host fail).
$SearchRoots = @(
(Join-Path $DshHome 'profiles\node_modules\@deepseek-ai'),
(Join-Path $DshHome "profiles\$Profile\node_modules\@deepseek-ai"),
(Join-Path $DshHome 'profiles\web\node_modules\@deepseek-ai')
)
# Runtime imports from src/index.ts (and peers they commonly need nearby).
$Peers = @(
@ -25,22 +32,26 @@ $Peers = @(
'cordis',
'dsh-credentials',
'dsh-settings',
'dsh-mcp-client',
'dsh-tools'
'dsh-tools',
'dsh-llm'
)
if (-not (Test-Path $SrcRoot)) {
throw "DSH installation fallback missing: $SrcRoot`nRun ``dsh web`` once (or any profile) so dsh heals profiles/node_modules."
}
New-Item -ItemType Directory -Force -Path $DstRoot | Out-Null
Write-Host "DSH_HOME=$DshHome profile=$Profile"
foreach ($name in $Peers) {
$src = Join-Path $SrcRoot $name
$dst = Join-Path $DstRoot $name
if (-not (Test-Path $src)) {
throw "Peer not found under installation fallback: $src"
$src = $null
foreach ($root in $SearchRoots) {
$candidate = Join-Path $root $name
if (Test-Path (Join-Path $candidate 'package.json')) {
$src = $candidate
break
}
}
if (-not $src) {
throw "Peer @deepseek-ai/$name not found under:`n $($SearchRoots -join "`n ")"
}
$dst = Join-Path $DstRoot $name
if (Test-Path $dst) {
$item = Get-Item $dst -Force
if ($item.LinkType -eq 'Junction' -or $item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
@ -50,7 +61,8 @@ foreach ($name in $Peers) {
}
}
New-Item -ItemType Junction -Path $dst -Target $src | Out-Null
Write-Host "linked @deepseek-ai/$name -> $src"
$ver = (Get-Content (Join-Path $src 'package.json') -Raw | ConvertFrom-Json).version
Write-Host "linked @deepseek-ai/$name@$ver <- $src"
}
Write-Host "Done. Re-run: dsh web"
Write-Host "Done. Restart DeepSeek Harness Desktop (or disable/enable dsh-netxops)."