mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-20 01:23:17 +08:00
Fix preset publication, volatile settings and client form binding
This commit is contained in:
parent
fe0fe70d33
commit
0b9a3a82bc
66 changed files with 8546 additions and 3690 deletions
|
|
@ -1,10 +1,14 @@
|
|||
/**
|
||||
* Build lib/index.js (host) and lib/agent-tools.js (Ops preset tool mount).
|
||||
* Build lib/index.js (host) and lib/agent-tools*.js (preset tool mounts).
|
||||
* Node refuses to strip TypeScript inside node_modules, so GitHub/npm installs
|
||||
* must ship prebuilt JS. Usage: bun run scripts/build-host.mjs
|
||||
*
|
||||
* Agent-tools entries are built as self-contained bundles (no shared chunk).
|
||||
* Cordis/Desktop load each export path independently; a split chunk once missed
|
||||
* a connection-publish race and is harder to reason about under custom loaders.
|
||||
*/
|
||||
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { basename, dirname, join } from 'node:path'
|
||||
import { mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
|
|
@ -16,6 +20,7 @@ const external = [
|
|||
'@deepseek-ai/dsh-credentials',
|
||||
'@deepseek-ai/dsh-settings',
|
||||
'@deepseek-ai/dsh-tools',
|
||||
'@deepseek-ai/dsh-scope',
|
||||
'@deepseek-ai/dsh-llm',
|
||||
'@deepseek-ai/dsh-brand',
|
||||
'@deepseek-ai/dsh-agent',
|
||||
|
|
@ -39,6 +44,21 @@ const entries = [
|
|||
|
||||
mkdirSync(outDir, { recursive: true })
|
||||
|
||||
/** Drop split-build leftovers from 0.1.56–0.1.57. */
|
||||
for (const name of readdirSync(outDir)) {
|
||||
if (name === 'agent-tools-shared.js' || /^chunk-.*\.js$/.test(name)) {
|
||||
rmSync(join(outDir, name), { force: true })
|
||||
}
|
||||
}
|
||||
for (const name of ['_split-test', '_split-test2']) {
|
||||
const target = resolve(outDir, name)
|
||||
const within = relative(resolve(root), target)
|
||||
if (within.startsWith('..') || isAbsolute(within) || dirname(target) !== resolve(outDir)) {
|
||||
throw new Error(`Refusing cleanup outside the build directory: ${target}`)
|
||||
}
|
||||
rmSync(target, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
for (const { entry, out } of entries) {
|
||||
const result = await Bun.build({
|
||||
entrypoints: [entry],
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ import { composeNetxopsPresetArtifacts } from '../src/agent-preset-install.ts'
|
|||
import { readFileSync } from 'node:fs'
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const artifacts = composeNetxopsPresetArtifacts()
|
||||
// This distributable declaration targets Desktop; runtime installation composes
|
||||
// separately against the running Host instead of reusing this static template.
|
||||
const artifacts = composeNetxopsPresetArtifacts('desktop')
|
||||
const declarativePath = join(root, 'presets', 'netxops.preset.patch.yml')
|
||||
writeFileSync(declarativePath, artifacts.declarativePatch)
|
||||
console.log(
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, symlinkSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
|
|
@ -27,6 +27,7 @@ const peers = [
|
|||
'dsh-credentials',
|
||||
'dsh-settings',
|
||||
'dsh-tools',
|
||||
'dsh-scope',
|
||||
'dsh-llm',
|
||||
]
|
||||
|
||||
|
|
@ -38,7 +39,7 @@ function profileArg() {
|
|||
|
||||
/**
|
||||
* 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
|
||||
* 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").
|
||||
|
|
@ -64,9 +65,13 @@ 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() || st.isDirectory()) {
|
||||
rmSync(dest, { recursive: true, force: true })
|
||||
if (st.isSymbolicLink()) {
|
||||
rmSync(dest, { force: true })
|
||||
} else {
|
||||
throw new Error(`refusing to replace non-link path ${dest}`)
|
||||
}
|
||||
|
|
@ -81,9 +86,11 @@ 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, dirs)
|
||||
const target = resolvePeer(name, [coherent])
|
||||
if (!target) {
|
||||
console.error(`missing peer @deepseek-ai/${name}`)
|
||||
missing++
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ param(
|
|||
$ErrorActionPreference = 'Stop'
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
$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).
|
||||
# Select one complete peer set, avoiding packages from different Host generations.
|
||||
$SearchRoots = @(
|
||||
(Join-Path $DshHome 'profiles\node_modules\@deepseek-ai'),
|
||||
(Join-Path $DshHome "profiles\$Profile\node_modules\@deepseek-ai"),
|
||||
|
|
@ -33,15 +32,25 @@ $Peers = @(
|
|||
'dsh-credentials',
|
||||
'dsh-settings',
|
||||
'dsh-tools',
|
||||
'dsh-scope',
|
||||
'dsh-llm'
|
||||
)
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $DstRoot | Out-Null
|
||||
Write-Host "DSH_HOME=$DshHome profile=$Profile"
|
||||
$CoherentRoot = $null
|
||||
foreach ($searchRoot in $SearchRoots) {
|
||||
$complete = $true
|
||||
foreach ($peer in $Peers) {
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $searchRoot "$peer\package.json"))) { $complete = $false; break }
|
||||
}
|
||||
if ($complete) { $CoherentRoot = $searchRoot; break }
|
||||
}
|
||||
if (-not $CoherentRoot) { throw 'No complete Harness peer set found; refusing to mix package generations.' }
|
||||
|
||||
foreach ($name in $Peers) {
|
||||
$src = $null
|
||||
foreach ($root in $SearchRoots) {
|
||||
foreach ($root in @($CoherentRoot)) {
|
||||
$candidate = Join-Path $root $name
|
||||
if (Test-Path (Join-Path $candidate 'package.json')) {
|
||||
$src = $candidate
|
||||
|
|
@ -52,12 +61,17 @@ foreach ($name in $Peers) {
|
|||
throw "Peer @deepseek-ai/$name not found under:`n $($SearchRoots -join "`n ")"
|
||||
}
|
||||
$dst = Join-Path $DstRoot $name
|
||||
$absoluteDst = [IO.Path]::GetFullPath($dst)
|
||||
$absolutePeerRoot = [IO.Path]::GetFullPath($DstRoot).TrimEnd('\')
|
||||
if ([IO.Path]::GetDirectoryName($absoluteDst) -ne $absolutePeerRoot) {
|
||||
throw "Refusing to replace a path outside the peer directory: $absoluteDst"
|
||||
}
|
||||
if (Test-Path $dst) {
|
||||
$item = Get-Item $dst -Force
|
||||
if ($item.LinkType -eq 'Junction' -or $item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
||||
cmd /c "rmdir `"$dst`""
|
||||
Remove-Item -LiteralPath $absoluteDst -Force
|
||||
} else {
|
||||
Remove-Item -Recurse -Force $dst
|
||||
throw "Refusing to replace a non-link peer directory: $absoluteDst"
|
||||
}
|
||||
}
|
||||
New-Item -ItemType Junction -Path $dst -Target $src | Out-Null
|
||||
|
|
|
|||
33
scripts/preview-client.mjs
Normal file
33
scripts/preview-client.mjs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/** Local UI QA using the real controller; no requests to Netx or the installed Host. */
|
||||
import { createRequire } from 'node:module'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url)))
|
||||
const home = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh')
|
||||
const anchors = ['desktop', 'web', ''].map(profile =>
|
||||
join(home, 'profiles', profile, 'node_modules', '@deepseek-ai', 'dsh-client-ui-renderer', 'package.json'))
|
||||
let require
|
||||
for (const anchor of anchors.filter(existsSync)) {
|
||||
const candidate = createRequire(anchor)
|
||||
try { candidate.resolve('react-dom/client'); require = candidate; break } catch { /* Try another Host. */ }
|
||||
}
|
||||
if (!require) throw new Error('UI preview requires the installed Harness React runtime.')
|
||||
const result = await Bun.build({ entrypoints: [join(root, 'examples/local-debug/ui-preview.tsx')],
|
||||
target: 'browser', format: 'esm', define: { 'process.env.NODE_ENV': JSON.stringify('development') },
|
||||
plugins: [{ name: 'host-react', setup(build) {
|
||||
build.onResolve({ filter: /^(react(?:\/.*)?|react-dom\/client)$/ }, args => ({ path: require.resolve(args.path) }))
|
||||
} }],
|
||||
})
|
||||
if (!result.success) throw new Error(result.logs.map(String).join('\n'))
|
||||
const bundle = await result.outputs[0].text()
|
||||
const server = Bun.serve({ hostname: '127.0.0.1', port: 0, fetch(request) {
|
||||
if (new URL(request.url).pathname === '/app.js') return new Response(bundle, { headers: { 'content-type': 'text/javascript' } })
|
||||
return new Response(`<!doctype html><html lang="zh"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Netx Ops · UI QA</title><style>body{margin:0;background:#f5f6f8;font-family:system-ui,sans-serif}main{max-width:820px;margin:24px auto;padding:0 20px}.preview-note{font-size:12px;color:#646a73;margin-bottom:16px}</style>
|
||||
<main><p class="preview-note">本地 UI 验证 · 模拟预设与状态,仅在内存中保存。Local UI preview; simulated Host data.</p><div id="root"></div></main><script type="module" src="/app.js"></script></html>`,
|
||||
{ headers: { 'content-type': 'text/html; charset=utf-8' } })
|
||||
} })
|
||||
console.log(`Netx Ops UI preview: ${server.url}`)
|
||||
54
scripts/typecheck.mjs
Normal file
54
scripts/typecheck.mjs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/** Check both plugin faces against the locally installed Harness declarations. */
|
||||
import { createRequire } from 'node:module'
|
||||
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { homedir, tmpdir } from 'node:os'
|
||||
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url)))
|
||||
const home = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh')
|
||||
const anchors = [join(root, 'package.json'), ...['desktop', 'web', ''].map(profile =>
|
||||
join(home, 'profiles', profile, 'node_modules', '@deepseek-ai', 'dsh-client-ui-renderer', 'package.json'))]
|
||||
const requires = anchors.filter(existsSync).flatMap(path => [createRequire(path), createRequire(realpathSync(path))])
|
||||
function findPackage(name) {
|
||||
for (const req of requires) {
|
||||
try { return req.resolve(`${name}/package.json`) } catch { /* Try another installed profile. */ }
|
||||
}
|
||||
throw new Error(`Missing typecheck peer ${name}; install the Harness and run link:peers.`)
|
||||
}
|
||||
const paths = {}
|
||||
for (const spec of [
|
||||
'@deepseek-ai/dsh-client-ui-slots', '@deepseek-ai/dsh-client-locale/client',
|
||||
'@deepseek-ai/dsh-client-ui-renderer/client', '@deepseek-ai/dsh-api-remotes/client',
|
||||
'@deepseek-ai/dsh-client-ui-settings/client',
|
||||
'@deepseek-ai/dsh-client-ui-settings-plugins/client',
|
||||
]) {
|
||||
const parts = spec.split('/'), pkgName = parts.slice(0, 2).join('/'), subpath = parts.slice(2).join('/')
|
||||
const pkgPath = findPackage(pkgName), pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
||||
const entry = pkg.exports?.[subpath ? `./${subpath}` : '.']
|
||||
const types = typeof entry === 'object' ? entry.types : pkg.types
|
||||
if (!types) throw new Error(`No declarations for ${spec}`)
|
||||
paths[spec] = [join(dirname(pkgPath), types)]
|
||||
}
|
||||
const nodeTypes = dirname(findPackage('@types/node'))
|
||||
const folder = mkdtempSync(join(tmpdir(), 'netxops-typecheck-'))
|
||||
try {
|
||||
const configPath = join(folder, 'tsconfig.json')
|
||||
writeFileSync(configPath, JSON.stringify({ compilerOptions: {
|
||||
target: 'ES2022', module: 'ESNext', moduleResolution: 'Bundler', jsx: 'react-jsx',
|
||||
noEmit: true, strict: true, skipLibCheck: true, allowImportingTsExtensions: true,
|
||||
types: ['node', 'react'], typeRoots: [dirname(nodeTypes), join(root, 'node_modules', '@types')], paths,
|
||||
}, include: [join(root, 'src', '**', '*.ts'), join(root, 'src', '**', '*.tsx')].map(path => path.replaceAll('\\', '/')) }))
|
||||
const result = spawnSync(process.execPath, [join(root, 'node_modules', 'typescript', 'bin', 'tsc'), '-p', configPath],
|
||||
{ cwd: root, stdio: 'inherit' })
|
||||
process.exitCode = result.status ?? 1
|
||||
} finally {
|
||||
// The only cleanup target is the exact unique temporary directory returned above.
|
||||
const target = realpathSync(folder)
|
||||
const within = relative(realpathSync(tmpdir()), target)
|
||||
if (!within || within.startsWith('..') || isAbsolute(within) || !target.startsWith(resolve(folder))) {
|
||||
throw new Error(`Refusing cleanup outside the temporary workspace: ${target}`)
|
||||
}
|
||||
rmSync(target, { recursive: true, force: true })
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue