Files
deepseek-harness/packages/boot/app-boot/src/plugin-compatibility.ts
T
turtle1999 3de3a977e0 feat(plugin-market): add the Custom plugin markets bundle and Discover tab
Read configured plugin market indexes on the Host, search them with fuzzy
multilingual matching, and install a chosen version from the Plugins page.

- dsh-plugin-market-index validates format-1 indexes and builds install
  arguments; inline icons are capped at 32 KiB.
- The pluginMarket service fetches configured http(s) URLs, parses bodies in
  a Worker Thread, caches documents, and pages filtered, ordered results with
  installation status and engines.dsh compatibility.
- Discover is an opt-in experimental bundle; the Plugins page gains a
  plugins.tab slot and confirmed installs.
- The plugin_market tool lists indexes, plugins, and versions for Creator mode.
- Plugin compatibility reads engines.dsh before DSH peer dependencies.
2026-10-15 05:37:44 +08:00

118 lines
6.0 KiB
TypeScript

/** Evaluate plugin dsh requirements (`engines.dsh`, else dsh peers) without importing plugin code. */
import fs from 'node:fs'
import { fileURLToPath } from 'node:url'
import semver from 'semver'
/** Unmet dsh requirements and the exact plugin/runtime exemption decision. */
export interface PluginCompatibility {
name: string
version: string
runtimeVersion: string
/** Only requirements the running dsh version does not satisfy: `engines.dsh`, or dsh peer package names. */
requirements: Record<string, string>
exempted: boolean
}
function objectOf(value: unknown, field: string): Record<string, unknown> {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new Error(`${field} must be an object`)
}
return value as Record<string, unknown>
}
function runtimeVersionOf(value: unknown): string {
if (typeof value !== 'string' || semver.valid(value) === null) {
throw new Error(`Invalid dsh runtime version: ${JSON.stringify(value)}; expected a semantic version`)
}
return value
}
function identityField(manifest: Record<string, unknown>, field: 'name' | 'version'): string {
const value = Object.hasOwn(manifest, field) ? manifest[field] : undefined
if (typeof value !== 'string' || value.trim() === '') {
throw new Error(`Plugin manifest ${field} must be a non-empty string when dsh requirements are unmet`)
}
return value
}
/**
* Read this app-boot package's version in both source and bundled installations.
* @returns the validated runtime semantic version, preserving its exact spelling.
* @throws if package.json cannot be read or its version is missing or invalid.
*/
export function getDshRuntimeVersion(): string {
// The executable's virtual filesystem intercepts string paths, not URL arguments.
const filename = fileURLToPath(new URL('../package.json', import.meta.url))
const manifest = objectOf(JSON.parse(fs.readFileSync(filename, 'utf8')), 'app-boot package.json')
return runtimeVersionOf(Object.hasOwn(manifest, 'version') ? manifest.version : undefined)
}
function satisfies(runtimeVersion: string, range: string): boolean {
return range.trim() !== '' && semver.satisfies(runtimeVersion, range, { includePrerelease: true })
}
function unmetRequirements(fields: Record<string, unknown>, runtimeVersion: string): Record<string, string> {
const engines = Object.hasOwn(fields, 'engines') ? objectOf(fields.engines, 'Plugin manifest engines') : {}
if (Object.hasOwn(engines, 'dsh')) {
const range = engines.dsh
if (typeof range !== 'string') throw new Error('Plugin manifest engines.dsh must be a string')
return satisfies(runtimeVersion, range) ? {} : { 'engines.dsh': range }
}
if (!Object.hasOwn(fields, 'peerDependencies')) return {}
const dependencies = objectOf(fields.peerDependencies, 'Plugin manifest peerDependencies')
const unmet: Record<string, string> = {}
for (const [name, range] of Object.entries(dependencies)) {
if (typeof range !== 'string') {
throw new Error(`Plugin manifest peerDependencies[${JSON.stringify(name)}] must be a string`)
}
if (name !== '@deepseek-ai/dsh' && !name.startsWith('@deepseek-ai/dsh-')) continue
const requirement = ['workspace:^', 'workspace:~', 'workspace:*'].includes(range) ? runtimeVersion : range
if (!satisfies(runtimeVersion, requirement)) unmet[name] = range
}
return unmet
}
/**
* Check the plugin's dsh requirements against the runtime. An own `engines.dsh` range decides alone and
* peerDependencies are ignored; otherwise every @deepseek-ai/dsh or @deepseek-ai/dsh-* peer must match.
* Prereleases participate in ranges. For peers only, workspace:^, workspace:~, and workspace:* refer to
* the current runtime; other empty or invalid ranges, including any workspace: `engines.dsh`, are unmet.
* @param manifest - parsed plugin package.json; inherited fields are ignored.
* @param exemptions - exact plugin name@version keys mapped to exact runtime versions.
* @param runtimeVersion - running dsh version, defaulting to this app-boot package.
* @returns unmet requirements and exemption status, or undefined when every requirement is met.
* @throws for malformed manifest engines or peer fields, invalid runtime versions, or missing identity on a mismatch.
*/
export function evaluatePluginCompatibility(
manifest: object,
exemptions: Readonly<Record<string, readonly string[]>> = {},
runtimeVersion = getDshRuntimeVersion(),
): PluginCompatibility | undefined {
runtimeVersionOf(runtimeVersion)
const fields = objectOf(manifest, 'Plugin manifest')
const requirements = unmetRequirements(fields, runtimeVersion)
if (Object.keys(requirements).length === 0) return undefined
const name = identityField(fields, 'name')
const version = identityField(fields, 'version')
const key = `${name}@${version}`
const exemptedVersions = Object.hasOwn(exemptions, key) ? exemptions[key] : undefined
const exempted = exemptedVersions?.includes(runtimeVersion) === true
return { name, version, runtimeVersion, requirements, exempted }
}
/**
* Describe unmet requirements, their risk, and the exact-version remedy.
* Surfaces with their own grant mechanism or locale render the structured result themselves.
* @param issue - incompatible plugin/runtime result, including exempted mismatches.
* @returns an English diagnostic for logs and stderr.
*/
export function pluginCompatibilityWarning(issue: PluginCompatibility): string {
const key = `${issue.name}@${issue.version}`
return `Plugin ${key} is incompatible with dsh ${issue.runtimeVersion}: requirements ${JSON.stringify(issue.requirements)}. `
+ 'Running it may cause crashes or data loss. '
+ 'Update the plugin or install a plugin version compatible with this dsh runtime. '
+ `To accept this risk explicitly, grant the exact-version exemption for ${key} on dsh ${issue.runtimeVersion} with \`dsh plugin allow-version\` or the plugin manager, then retry the installation or restart dsh. `
+ `Exact-version exemption: ${issue.exempted ? 'active' : 'not active'}.`
}