Files
dsh/upstream/container/platform/lib/dsh-compatibility.mjs
T

144 lines
6.0 KiB
JavaScript

import { execFile } from 'node:child_process'
import { readFile, readdir } from 'node:fs/promises'
import { promisify } from 'node:util'
import { join } from 'node:path'
const execFileAsync = promisify(execFile)
export const DSH_COMPATIBILITY_PACKAGES = Object.freeze({
'@deepseek-ai/dsh-api-remotes': Object.freeze(['.', './client']),
'@deepseek-ai/dsh-app-boot': Object.freeze(['.']),
'@deepseek-ai/dsh-client-connection': Object.freeze(['.', './client']),
'@deepseek-ai/dsh-client-locale': Object.freeze(['.', './client']),
'@deepseek-ai/dsh-client-ui-settings': Object.freeze(['.', './client']),
'@deepseek-ai/dsh-client-ui-theme': Object.freeze(['.', './client']),
'@deepseek-ai/dsh-host-directory-picker-browse': Object.freeze(['.']),
'@deepseek-ai/dsh-llm': Object.freeze(['.', './remote']),
'@deepseek-ai/dsh-settings': Object.freeze(['.']),
'@deepseek-ai/dsh-web-app': Object.freeze(['.', './startup', './cordis.patch.yml']),
})
const DSH_DIRECT_COMPATIBILITY_PACKAGES = Object.freeze([
'@deepseek-ai/dsh-app-boot',
'@deepseek-ai/dsh-web-app',
])
export const DSH_WEB_ROWS = Object.freeze({
connection: '@deepseek-ai/dsh-client-connection',
'plugin-inventory': '@deepseek-ai/dsh-host-plugin-inventory',
'settings-controller': '@deepseek-ai/dsh-api-settings-controller',
webserver: '@deepseek-ai/dsh-host-webserver',
'web-runtime': '@deepseek-ai/dsh-web-app',
})
export const DSH_REMOTE_METHODS = Object.freeze([
'@deepseek-ai/dsh-api-settings-controller#settings/describe',
'@deepseek-ai/dsh-llm#llm/listConfigurableProviders',
'@deepseek-ai/dsh-llm#llm/listProviders',
])
export const DSH_STATIC_CLIENT_MODULES = Object.freeze([
'@deepseek-ai/dsh-client-ui-primitives',
'react',
'react-dom',
])
function packagePath(root, packageName, file = 'package.json') {
return join(root, 'node_modules', ...packageName.split('/'), file)
}
async function json(path) {
return JSON.parse(await readFile(path, 'utf8'))
}
function requireObject(value, description) {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`${description} is invalid`)
}
return value
}
function requireText(source, expected, description) {
if (!source.includes(expected)) throw new Error(`${description} is missing ${JSON.stringify(expected)}`)
}
function requireStaticModule(source, name) {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const pattern = name === 'react'
? /(?:^|[{,])["']?react["']?:/
: new RegExp(`["']${escaped}["']:`)
if (!pattern.test(source)) throw new Error(`DSH Web static module table is missing ${JSON.stringify(name)}`)
}
function exported(manifest, subpath) {
const exports = requireObject(manifest.exports, `${manifest.name} exports`)
return Object.hasOwn(exports, subpath)
}
function webRowPattern(id, packageName) {
const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const escapedPackage = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
return new RegExp(`(?:^|\\n)- id: ${escapedId}\\n(?: {2}[^\\n]*\\n)*? {2}name: ['\"]?${escapedPackage}['\"]?(?:\\n|$)`)
}
export async function verifyDshCompatibility({ packageRoot, expectedVersion, home }) {
if (typeof packageRoot !== 'string' || packageRoot === '') throw new TypeError('DSH package root is required')
if (typeof expectedVersion !== 'string' || expectedVersion === '') throw new TypeError('Expected DSH version is required')
if (typeof home !== 'string' || home === '') throw new TypeError('Temporary DSH home is required')
const manifest = await json(join(packageRoot, 'package.json'))
if (manifest.name !== '@deepseek-ai/dsh') throw new Error('DSH package name is invalid')
if (manifest.version !== expectedVersion) {
throw new Error(`DSH package version ${JSON.stringify(manifest.version)} does not match ${JSON.stringify(expectedVersion)}`)
}
if (manifest.bin?.dsh !== 'lib/bin.js') throw new Error('DSH command entry is not lib/bin.js')
const dependencies = requireObject(manifest.dependencies, 'DSH dependencies')
for (const packageName of DSH_DIRECT_COMPATIBILITY_PACKAGES) {
if (typeof dependencies[packageName] !== 'string') throw new Error(`DSH does not declare ${packageName}`)
}
for (const [packageName, requiredExports] of Object.entries(DSH_COMPATIBILITY_PACKAGES)) {
const dependency = await json(packagePath(packageRoot, packageName))
if (dependency.name !== packageName || dependency.version !== expectedVersion) {
throw new Error(`${packageName} is not installed at DSH version ${expectedVersion}`)
}
for (const subpath of requiredExports) {
if (!exported(dependency, subpath)) throw new Error(`${packageName} does not export ${subpath}`)
}
}
const remotes = await readFile(packagePath(packageRoot, '@deepseek-ai/dsh-api-remotes', 'lib/client.js'), 'utf8')
for (const method of DSH_REMOTE_METHODS) requireText(remotes, method, 'DSH Remote registry')
const frontendRoot = packagePath(packageRoot, '@deepseek-ai/dsh-web-frontend', 'dist/assets')
const frontend = (await Promise.all((await readdir(frontendRoot))
.filter(name => name.endsWith('.js'))
.map(name => readFile(join(frontendRoot, name), 'utf8')))).join('\n')
for (const moduleName of DSH_STATIC_CLIENT_MODULES) {
requireStaticModule(frontend, moduleName)
}
const { stdout, stderr } = await execFileAsync(
process.execPath,
[join(packageRoot, 'lib/bin.js'), '--profile', 'web', '--dump-default-config'],
{
env: { ...process.env, DSH_HOME: home },
maxBuffer: 8 * 1024 * 1024,
timeout: 30_000,
},
)
if (stderr.trim() !== '') throw new Error(`DSH config dump wrote to stderr: ${stderr.trim()}`)
for (const [id, packageName] of Object.entries(DSH_WEB_ROWS)) {
if (!webRowPattern(id, packageName).test(stdout)) {
throw new Error(`DSH Web profile does not contain ${id} (${packageName})`)
}
}
return Object.freeze({
name: manifest.name,
version: manifest.version,
packages: Object.keys(DSH_COMPATIBILITY_PACKAGES).length,
remoteMethods: DSH_REMOTE_METHODS.length,
webRows: Object.keys(DSH_WEB_ROWS).length,
})
}