144 lines
6.0 KiB
JavaScript
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,
|
|
})
|
|
}
|