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.
118 lines
6.0 KiB
TypeScript
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'}.`
|
|
}
|