mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-19 23:03:33 +08:00
Harden public package installation and release validation
This commit is contained in:
parent
9271494b37
commit
9b2166e5b6
41 changed files with 994 additions and 363 deletions
|
|
@ -12,6 +12,7 @@
|
|||
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { recordBuild } from './build-integrity.mjs'
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const entry = join(root, 'src/client/index.ts')
|
||||
|
|
@ -35,6 +36,7 @@ const result = await Bun.build({
|
|||
format: 'cjs',
|
||||
sourcemap: 'none',
|
||||
minify: false,
|
||||
metafile: true,
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify('production'),
|
||||
},
|
||||
|
|
@ -80,4 +82,5 @@ ${body}
|
|||
|
||||
mkdirSync(dirname(outFile), { recursive: true })
|
||||
writeFileSync(outFile, artifact, 'utf8')
|
||||
recordBuild(outFile, result, 'scripts/build-client.mjs')
|
||||
console.log(`wrote ${outFile} (${artifact.length} bytes); requires: ${[...new Set(requires)].join(', ') || '(none)'}`)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
import { mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { recordBuild } from './build-integrity.mjs'
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const outDir = join(root, 'lib')
|
||||
|
|
@ -66,6 +67,7 @@ for (const { entry, out } of entries) {
|
|||
format: 'esm',
|
||||
sourcemap: 'none',
|
||||
minify: false,
|
||||
metafile: true,
|
||||
external,
|
||||
})
|
||||
|
||||
|
|
@ -76,5 +78,6 @@ for (const { entry, out } of entries) {
|
|||
|
||||
const text = await result.outputs[0].text()
|
||||
writeFileSync(out, text, 'utf8')
|
||||
recordBuild(out, result, 'scripts/build-host.mjs')
|
||||
console.log(`wrote ${out} (${text.length} bytes)`)
|
||||
}
|
||||
|
|
|
|||
44
scripts/build-integrity.mjs
Normal file
44
scripts/build-integrity.mjs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/** Track the actual bundle inputs so a release cannot silently ship stale JS. */
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, isAbsolute, relative, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const manifestPath = resolve(root, '.release-build.json')
|
||||
export const hashText = text => createHash('sha256').update(text.replace(/\r\n/g, '\n')).digest('hex')
|
||||
export const hashFile = path => hashText(readFileSync(path, 'utf8'))
|
||||
|
||||
export function recordBuild(output, result, buildScript) {
|
||||
if (!result.metafile) throw new Error('Build metadata is missing; use Bun >=1.3.13.')
|
||||
const inputs = [...new Set([
|
||||
'package.json', 'pnpm-lock.yaml', buildScript, 'scripts/build-integrity.mjs',
|
||||
...Object.keys(result.metafile.inputs).map(path => relative(root, resolve(path)).replaceAll('\\', '/'))
|
||||
.filter(path => path.startsWith('src/')),
|
||||
])].sort()
|
||||
if (!inputs.some(path => path.startsWith('src/'))) throw new Error('Bundle has no recorded source inputs.')
|
||||
for (const path of inputs) {
|
||||
if (isAbsolute(path) || path.startsWith('../')) throw new Error(`Build input escapes repository: ${path}`)
|
||||
}
|
||||
let builds = {}
|
||||
try { builds = JSON.parse(readFileSync(manifestPath, 'utf8')).builds ?? {} } catch { /* First build. */ }
|
||||
const key = relative(root, output).replaceAll('\\', '/')
|
||||
builds[key] = { output: hashFile(output), inputs: Object.fromEntries(inputs.map(path => [path, hashFile(resolve(root, path))])) }
|
||||
writeFileSync(manifestPath, `${JSON.stringify({ format: 1, builds: Object.fromEntries(Object.entries(builds).sort()) }, null, 2)}\n`)
|
||||
}
|
||||
|
||||
export function checkBuilds(directory = root) {
|
||||
const manifest = JSON.parse(readFileSync(resolve(directory, '.release-build.json'), 'utf8'))
|
||||
if (manifest.format !== 1) throw new Error('Unknown build manifest format.')
|
||||
const pkg = JSON.parse(readFileSync(resolve(directory, 'package.json'), 'utf8'))
|
||||
const outputs = [...new Set(Object.values(pkg.exports).filter(path => path.endsWith('.js')).map(path => path.replace(/^\.\//, '')))]
|
||||
for (const output of outputs) {
|
||||
const build = manifest.builds?.[output]
|
||||
if (!build || hashFile(resolve(directory, output)) !== build.output) throw new Error(`Missing or modified artifact: ${output}; run bun run bundle.`)
|
||||
for (const [path, hash] of Object.entries(build.inputs)) {
|
||||
if (isAbsolute(path) || path.startsWith('../')) throw new Error(`Invalid build input: ${path}`)
|
||||
if (hashFile(resolve(directory, path)) !== hash) throw new Error(`Stale artifact ${output}: ${path} changed; run bun run bundle.`)
|
||||
}
|
||||
}
|
||||
return outputs
|
||||
}
|
||||
17
scripts/install-preset.mjs
Normal file
17
scripts/install-preset.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/** Developer fallback: use the same composition and preservation rules as Host activation. */
|
||||
import { existsSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { composeNetxopsPresetArtifacts, installDirectoryPreset, syncDeclarativeIntoProfilePatch } from '../lib/index.js'
|
||||
|
||||
const index = process.argv.indexOf('--profile')
|
||||
const profile = index < 0 ? 'web' : process.argv[index + 1]
|
||||
if (!profile || !/^[\w-]+$/.test(profile)) throw new Error('Invalid profile name.')
|
||||
const dshHome = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh')
|
||||
if (!existsSync(join(dshHome, 'profiles', profile, 'package.json'))) {
|
||||
throw new Error(`Harness profile is not installed: ${profile}. Start this Host first.`)
|
||||
}
|
||||
const artifacts = composeNetxopsPresetArtifacts(profile)
|
||||
installDirectoryPreset(artifacts, console)
|
||||
syncDeclarativeIntoProfilePatch(profile, artifacts.declarativePatch, console)
|
||||
console.log(`Netx Ops preset prepared for ${profile}. Restart this Host to load its patch.`)
|
||||
|
|
@ -38,18 +38,13 @@ function profileArg() {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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', 'node_modules', '@deepseek-ai'),
|
||||
join(dshHome, 'profiles', profile, 'node_modules', '@deepseek-ai'),
|
||||
join(dshHome, 'profiles', 'web', 'node_modules', '@deepseek-ai'),
|
||||
join(dshHome, 'profiles', 'node_modules', '@deepseek-ai'),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +77,7 @@ function linkOne(shortName, 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 ')}`)
|
||||
|
|
|
|||
|
|
@ -1,82 +1,13 @@
|
|||
# Link DSH peer packages into this checkout for local `dsh plugin add <path>` / `link:`.
|
||||
#
|
||||
# Node resolves imports from the realpath of a linked package. A junction out of
|
||||
# ~/.dsh/profiles/<name>/node_modules therefore cannot see the profile's
|
||||
# hoisted peers or the healed profiles/node_modules fallback. Point the peers
|
||||
# this package imports at the shared installation copy instead.
|
||||
#
|
||||
# Usage (PowerShell):
|
||||
# .\scripts\link-dsh-peers.ps1
|
||||
# Optional:
|
||||
# .\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]$Profile = 'desktop'
|
||||
[string]$DshHome = $env:DSH_HOME,
|
||||
[ValidatePattern('^[\w-]+$')][string]$Profile = 'desktop'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
$DstRoot = Join-Path $Root 'node_modules\@deepseek-ai'
|
||||
# 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"),
|
||||
(Join-Path $DshHome 'profiles\web\node_modules\@deepseek-ai')
|
||||
)
|
||||
|
||||
# Runtime imports from src/index.ts (and peers they commonly need nearby).
|
||||
$Peers = @(
|
||||
'schemastery',
|
||||
'cordis',
|
||||
'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 }
|
||||
$PreviousDshHome = $env:DSH_HOME
|
||||
try {
|
||||
if ($DshHome) { $env:DSH_HOME = $DshHome }
|
||||
& node (Join-Path $PSScriptRoot 'link-dsh-peers.mjs') --profile $Profile
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Peer linking failed.' }
|
||||
} finally {
|
||||
$env:DSH_HOME = $PreviousDshHome
|
||||
}
|
||||
if (-not $CoherentRoot) { throw 'No complete Harness peer set found; refusing to mix package generations.' }
|
||||
|
||||
foreach ($name in $Peers) {
|
||||
$src = $null
|
||||
foreach ($root in @($CoherentRoot)) {
|
||||
$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
|
||||
$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) {
|
||||
Remove-Item -LiteralPath $absoluteDst -Force
|
||||
} else {
|
||||
throw "Refusing to replace a non-link peer directory: $absoluteDst"
|
||||
}
|
||||
}
|
||||
New-Item -ItemType Junction -Path $dst -Target $src | Out-Null
|
||||
$ver = (Get-Content (Join-Path $src 'package.json') -Raw | ConvertFrom-Json).version
|
||||
Write-Host "linked @deepseek-ai/$name@$ver <- $src"
|
||||
}
|
||||
|
||||
Write-Host "Done. Restart DeepSeek Harness Desktop (or disable/enable dsh-netxops)."
|
||||
|
|
|
|||
|
|
@ -1,29 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
# Link DSH peer packages into this checkout for local `dsh plugin add <path>` / `link:`.
|
||||
# See link-dsh-peers.ps1 for why.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
|
||||
SRC_ROOT="$DSH_HOME/profiles/node_modules/@deepseek-ai"
|
||||
DST_ROOT="$ROOT/node_modules/@deepseek-ai"
|
||||
PEERS=(schemastery cordis dsh-credentials dsh-settings dsh-mcp-client dsh-tools)
|
||||
|
||||
if [[ ! -d "$SRC_ROOT" ]]; then
|
||||
echo "DSH installation fallback missing: $SRC_ROOT" >&2
|
||||
echo "Run dsh web once so dsh heals profiles/node_modules." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DST_ROOT"
|
||||
for name in "${PEERS[@]}"; do
|
||||
src="$SRC_ROOT/$name"
|
||||
dst="$DST_ROOT/$name"
|
||||
if [[ ! -e "$src" ]]; then
|
||||
echo "Peer not found: $src" >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$dst"
|
||||
ln -s "$src" "$dst"
|
||||
echo "linked @deepseek-ai/$name -> $src"
|
||||
done
|
||||
echo "Done. Re-run: dsh web"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec node "$SCRIPT_DIR/link-dsh-peers.mjs" "$@"
|
||||
|
|
|
|||
|
|
@ -1,21 +1,4 @@
|
|||
# Link / install presets/netxops into the DSH user agent-presets root.
|
||||
# Prefer a real directory copy: DSH discovery skips Windows junctions
|
||||
# (Dirent.isDirectory() is false for reparse points).
|
||||
$ErrorActionPreference = "Stop"
|
||||
$RepoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$Source = Join-Path $RepoRoot "presets\netxops"
|
||||
$DshHome = if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE ".dsh" }
|
||||
$TargetParent = Join-Path $DshHome ".agent-presets"
|
||||
$Target = Join-Path $TargetParent "netxops"
|
||||
|
||||
if (-not (Test-Path (Join-Path $Source "agent.cordis.yml"))) {
|
||||
throw "Missing preset dir: $Source"
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path $TargetParent | Out-Null
|
||||
if (Test-Path $Target) {
|
||||
Remove-Item -Recurse -Force $Target
|
||||
}
|
||||
Copy-Item -Recurse -Force $Source $Target
|
||||
Set-Content -Path (Join-Path $Target ".dsh-netxops-managed") -Value ((Get-Date).ToString("o"))
|
||||
Write-Host "Installed $Target (copy from $Source)"
|
||||
Write-Host "Open Settings → Agent presets → Custom → Netx Ops (or new session)."
|
||||
param([ValidatePattern('^[\w-]+$')][string]$Profile = 'desktop')
|
||||
$ErrorActionPreference = 'Stop'
|
||||
& node (Join-Path $PSScriptRoot 'install-preset.mjs') --profile $Profile
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Preset installation failed.' }
|
||||
|
|
|
|||
|
|
@ -1,19 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
# Install presets/netxops into $DSH_HOME/.agent-presets (copy, not symlink).
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SRC="$ROOT/presets/netxops"
|
||||
DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
|
||||
DEST_PARENT="$DSH_HOME/.agent-presets"
|
||||
DEST="$DEST_PARENT/netxops"
|
||||
|
||||
if [[ ! -f "$SRC/agent.cordis.yml" ]]; then
|
||||
echo "Missing preset dir: $SRC" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$DEST_PARENT"
|
||||
rm -rf "$DEST"
|
||||
cp -R "$SRC" "$DEST"
|
||||
date -u +%Y-%m-%dT%H:%M:%SZ > "$DEST/.dsh-netxops-managed"
|
||||
echo "Installed $DEST"
|
||||
echo "Open Settings → Agent presets → Custom → Netx Ops"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec node "$SCRIPT_DIR/install-preset.mjs" "$@"
|
||||
|
|
|
|||
17
scripts/npm-cli.mjs
Normal file
17
scripts/npm-cli.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/** Resolve the npm bundled with Node instead of an unrelated global npm shim. */
|
||||
import assert from 'node:assert/strict'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
|
||||
export function npmRuntime() {
|
||||
const node = process.versions.bun
|
||||
? spawnSync('node', ['-p', 'process.execPath'], { encoding: 'utf8', windowsHide: true }).stdout.trim()
|
||||
: process.execPath
|
||||
const npm = [process.env.NETXOPS_NPM_CLI,
|
||||
join(dirname(node), 'node_modules/npm/bin/npm-cli.js'),
|
||||
resolve(dirname(node), '../lib/node_modules/npm/bin/npm-cli.js'),
|
||||
].filter(Boolean).find(existsSync)
|
||||
assert.ok(npm, 'Node.js must include npm to build and test the public installation package.')
|
||||
return { node, npm }
|
||||
}
|
||||
21
scripts/pack-release.mjs
Normal file
21
scripts/pack-release.mjs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { root } from './build-integrity.mjs'
|
||||
import { npmRuntime } from './npm-cli.mjs'
|
||||
import { verifyRelease } from './verify-release.mjs'
|
||||
|
||||
const verified = verifyRelease()
|
||||
const { node, npm } = npmRuntime()
|
||||
const packed = spawnSync(node, [npm, 'pack', '--json', '--ignore-scripts', '--cache', join(tmpdir(), 'netxops-npm-cache')], {
|
||||
cwd: root, encoding: 'utf8', windowsHide: true,
|
||||
})
|
||||
if (packed.status !== 0) throw new Error(packed.stderr || String(packed.error))
|
||||
const info = JSON.parse(packed.stdout)[0]
|
||||
const file = join(root, info.filename)
|
||||
const sha = createHash('sha256').update(readFileSync(file)).digest('hex')
|
||||
writeFileSync(`${file}.sha256`, `${sha} ${info.filename}\n`)
|
||||
console.log(`Release ${verified.version}: ${info.files.length} files, ${(info.size / 1024).toFixed(1)} KiB`)
|
||||
console.log(`Package: ${file}\nSHA256: ${sha}`)
|
||||
94
scripts/verify-release.mjs
Normal file
94
scripts/verify-release.mjs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/** Publisher check. No install lifecycle invokes this on a user's machine. */
|
||||
import assert from 'node:assert/strict'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { isBuiltin } from 'node:module'
|
||||
import { resolve } from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import vm from 'node:vm'
|
||||
import { load } from 'js-yaml'
|
||||
import ts from 'typescript'
|
||||
import { checkBuilds, root } from './build-integrity.mjs'
|
||||
|
||||
export function verifyRelease(directory = root) {
|
||||
const read = path => readFileSync(resolve(directory, path), 'utf8')
|
||||
const pkg = JSON.parse(read('package.json'))
|
||||
const outputs = checkBuilds(directory)
|
||||
assert.equal(pkg.name, 'dsh-netxops')
|
||||
assert.match(pkg.version, /^\d+\.\d+\.\d+$/)
|
||||
assert.equal(pkg.type, 'module')
|
||||
for (const script of ['preinstall', 'install', 'postinstall', 'prepare', 'prepack', 'postpack', 'prepublish']) {
|
||||
assert.ok(!pkg.scripts?.[script], `User installs must not run ${script}.`)
|
||||
}
|
||||
assert.deepEqual(pkg.dependencies, {}, 'Runtime third-party dependencies must be bundled.')
|
||||
for (const [name, range] of Object.entries(pkg.peerDependencies)) {
|
||||
assert.equal(pkg.peerDependenciesMeta[name]?.optional, true, `${name} must be supplied by Harness.`)
|
||||
assert.doesNotMatch(range, /workspace:|file:|link:|github:/)
|
||||
}
|
||||
for (const path of Object.values(pkg.exports)) {
|
||||
assert.ok(path.startsWith('./') && !path.includes('..'), `Unsafe export: ${path}`)
|
||||
if (!path.includes('*')) assert.ok(existsSync(resolve(directory, path)), `Missing export: ${path}`)
|
||||
}
|
||||
assert.ok(read(pkg.icon).includes('<svg'), 'Missing plugin icon.')
|
||||
for (const lang of ['en', 'zh']) {
|
||||
const locale = JSON.parse(read(`locale/${lang}.json`))
|
||||
assert.ok(locale.meta.title && locale.meta.description, `Missing ${lang} plugin metadata.`)
|
||||
}
|
||||
assert.equal(pkg.dsh.client.immediately, false)
|
||||
const patch = load(read(pkg.dsh.bundle.patch))
|
||||
assert.ok(Array.isArray(patch) && patch.length > 0)
|
||||
const hosts = patch.flatMap(row => row.insert ?? []).filter(row => row.name === pkg.name)
|
||||
assert.equal(hosts.length, 1, 'Bundle must insert exactly one Host bridge.')
|
||||
assert.equal(hosts[0].id, 'netxops')
|
||||
const declared = new Set(Object.keys(pkg.peerDependencies))
|
||||
for (const output of outputs) {
|
||||
const text = read(output)
|
||||
const parsed = spawnSync(process.execPath, ['--check', resolve(directory, output)], { encoding: 'utf8', windowsHide: true })
|
||||
assert.equal(parsed.status, 0, `Invalid JS in ${output}: ${parsed.stderr}`)
|
||||
assert.doesNotMatch(text, /\bBun\.|react\/jsx-dev-runtime|agent-tools-shared|(?:from|import\()\s*["']\.\//)
|
||||
if (output !== 'lib/client.js') {
|
||||
const source = ts.createSourceFile(output, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS)
|
||||
const visit = node => {
|
||||
const specifier = ts.isImportDeclaration(node) || ts.isExportDeclaration(node)
|
||||
? node.moduleSpecifier
|
||||
: ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword ? node.arguments[0] : undefined
|
||||
if (specifier && ts.isStringLiteral(specifier)) {
|
||||
assert.ok(isBuiltin(specifier.text) || declared.has(specifier.text), `${output} needs undeclared dependency ${specifier.text}`)
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(source)
|
||||
}
|
||||
}
|
||||
let client
|
||||
vm.runInNewContext(read('lib/client.js'), { window: { __ModuleLoader__: { load(value) { client = value } } } })
|
||||
assert.equal(client.id, pkg.name)
|
||||
assert.equal(typeof client.factory, 'function')
|
||||
const clientExports = client.factory(name => {
|
||||
assert.ok(['react', 'react/jsx-runtime'].includes(name), `Unseeded browser dependency: ${name}`)
|
||||
return {}
|
||||
})
|
||||
assert.equal(typeof clientExports.apply, 'function')
|
||||
assert.deepEqual(Array.from(clientExports.inject), ['slots', 'locale'])
|
||||
for (const group of ['ops/netx-ops', 'topology/netx-topology', 'biz-monitor/netx-biz-monitor']) {
|
||||
const skill = read(`presets/netxops/skills/${group}/SKILL.md`).replace(/\r\n/g, '\n')
|
||||
const frontmatter = /^---\n([\s\S]*?)\n---/.exec(skill)
|
||||
assert.ok(frontmatter, `Missing skill frontmatter: ${group}`)
|
||||
const parsed = load(frontmatter[1])
|
||||
assert.equal(typeof parsed.name, 'string')
|
||||
assert.equal(typeof parsed.description, 'string')
|
||||
assert.ok(skill.slice(frontmatter[0].length).trim())
|
||||
}
|
||||
for (const path of ['presets/netxops/PERSONA.md', 'presets/netxops/preset.yml']) assert.ok(read(path).trim())
|
||||
return { version: pkg.version, outputs }
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
||||
try {
|
||||
const result = verifyRelease()
|
||||
console.log(`Release ${result.version}: ${result.outputs.length} JS artifacts, bundle and skills verified.`)
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue