mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-24 06:45:22 +00:00
check plugin compat
This commit is contained in:
@@ -96,6 +96,9 @@ Example:
|
||||
"name": "@acme/opencode-plugin",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"engines": {
|
||||
"opencode": "^1.0.0"
|
||||
},
|
||||
"oc-plugin": [
|
||||
["server", { "custom": true }],
|
||||
["tui", { "compact": true }]
|
||||
@@ -103,6 +106,23 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
### Version compatibility
|
||||
|
||||
npm plugins can declare a version compatibility range in `package.json` using the standard `engines` field:
|
||||
|
||||
```json
|
||||
{
|
||||
"engines": {
|
||||
"opencode": "^1.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- The value is a semver range checked against the running OpenCode version.
|
||||
- If the range is not satisfied, the plugin is skipped with a warning and a session error.
|
||||
- If `engines.opencode` is absent, no check is performed (backward compatible).
|
||||
- File plugins are never checked; only npm package plugins are validated.
|
||||
|
||||
- `opencode plugin <module>` resolves and installs the package first, then reads `oc-plugin`, then patches config.
|
||||
- Alias: `opencode plug <module>`.
|
||||
- `-g` / `--global` writes into the global config dir.
|
||||
|
||||
@@ -18,6 +18,7 @@ import { errorData, errorMessage } from "@/util/error"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { Instance } from "@/project/instance"
|
||||
import {
|
||||
checkPluginCompatibility,
|
||||
getDefaultPlugin,
|
||||
isDeprecatedPlugin,
|
||||
pluginSource,
|
||||
@@ -32,6 +33,7 @@ import { addTheme, hasTheme } from "../context/theme"
|
||||
import { Global } from "@/global"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Installation } from "@/installation"
|
||||
import { INTERNAL_TUI_PLUGINS, type InternalTuiPlugin } from "./internal"
|
||||
import { setupSlots, Slot as View } from "./slots"
|
||||
import type { HostPluginApi, HostSlots } from "./slots"
|
||||
@@ -182,12 +184,24 @@ async function loadExternalPlugin(
|
||||
const spec = Config.pluginSpecifier(item)
|
||||
if (isDeprecatedPlugin(spec)) return
|
||||
log.info("loading tui plugin", { path: spec, retry })
|
||||
const target = await resolvePluginTarget(spec).catch((error) => {
|
||||
const resolved = await resolvePluginTarget(spec).catch((error) => {
|
||||
fail("failed to resolve tui plugin", { path: spec, retry, error })
|
||||
return
|
||||
})
|
||||
if (!target) return
|
||||
if (!resolved) return
|
||||
|
||||
const source = pluginSource(spec)
|
||||
if (source === "npm") {
|
||||
const ok = await checkPluginCompatibility(resolved, Installation.VERSION)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
fail("tui plugin incompatible", { path: spec, retry, error })
|
||||
return false
|
||||
})
|
||||
if (!ok) return
|
||||
}
|
||||
|
||||
const target = resolved
|
||||
const meta = config.plugin_meta?.[spec]
|
||||
if (!meta) {
|
||||
log.warn("missing tui plugin metadata", {
|
||||
@@ -197,7 +211,6 @@ async function loadExternalPlugin(
|
||||
return
|
||||
}
|
||||
|
||||
const source = pluginSource(spec)
|
||||
const root = resolveRoot(source === "file" ? spec : target)
|
||||
const install_theme = createThemeInstaller(meta, root, spec)
|
||||
const entry = await resolvePluginEntrypoint(spec, target, "tui").catch((error) => {
|
||||
|
||||
@@ -14,10 +14,13 @@ import { Effect, Layer, ServiceMap, Stream } from "effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { errorMessage } from "@/util/error"
|
||||
import { Installation } from "@/installation"
|
||||
import {
|
||||
checkPluginCompatibility,
|
||||
getDefaultPlugin,
|
||||
isDeprecatedPlugin,
|
||||
parsePluginSpecifier,
|
||||
pluginSource,
|
||||
resolvePluginEntrypoint,
|
||||
resolvePluginTarget,
|
||||
} from "./shared"
|
||||
@@ -106,9 +109,26 @@ export namespace Plugin {
|
||||
const spec = Config.pluginSpecifier(item)
|
||||
if (isDeprecatedPlugin(spec)) return
|
||||
log.info("loading plugin", { path: spec })
|
||||
const target = await resolvePlugin(spec)
|
||||
if (!target) return
|
||||
const resolved = await resolvePlugin(spec)
|
||||
if (!resolved) return
|
||||
|
||||
if (pluginSource(spec) === "npm") {
|
||||
const incompatible = await checkPluginCompatibility(resolved, Installation.VERSION)
|
||||
.then(() => false)
|
||||
.catch((err) => {
|
||||
const message = errorMessage(err)
|
||||
log.warn("plugin incompatible", { path: spec, error: message })
|
||||
Bus.publish(Session.Event.Error, {
|
||||
error: new NamedError.Unknown({
|
||||
message: `Plugin ${spec} skipped: ${message}`,
|
||||
}).toObject(),
|
||||
})
|
||||
return true
|
||||
})
|
||||
if (incompatible) return
|
||||
}
|
||||
|
||||
const target = resolved
|
||||
const entry = await resolvePluginEntrypoint(spec, target, "server").catch((err) => {
|
||||
const message = errorMessage(err)
|
||||
log.error("failed to resolve plugin server entry", { path: spec, target, error: message })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from "path"
|
||||
import { fileURLToPath, pathToFileURL } from "url"
|
||||
import semver from "semver"
|
||||
import { BunProc } from "@/bun"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { isRecord } from "@/util/record"
|
||||
@@ -87,6 +88,18 @@ export async function resolvePathPluginTarget(spec: string) {
|
||||
return pathToFileURL(path.resolve(file, pkg.main)).href
|
||||
}
|
||||
|
||||
export async function checkPluginCompatibility(target: string, opencodeVersion: string) {
|
||||
const pkg = await readPluginPackage(target).catch(() => undefined)
|
||||
if (!pkg) return
|
||||
const engines = pkg.json.engines
|
||||
if (!isRecord(engines)) return
|
||||
const range = engines.opencode
|
||||
if (typeof range !== "string") return
|
||||
if (!semver.satisfies(opencodeVersion, range)) {
|
||||
throw new Error(`Plugin requires opencode ${range} but running ${opencodeVersion}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolvePluginTarget(spec: string, parsed = parsePluginSpecifier(spec)) {
|
||||
if (isPathPluginSpec(spec)) return resolvePathPluginTarget(spec)
|
||||
return BunProc.install(parsed.pkg, parsed.version)
|
||||
|
||||
Reference in New Issue
Block a user