|
@@ -18,38 +18,29 @@ import { Log } from "@/util/log"
|
|
|
import { errorData, errorMessage } from "@/util/error"
|
|
import { errorData, errorMessage } from "@/util/error"
|
|
|
import { isRecord } from "@/util/record"
|
|
import { isRecord } from "@/util/record"
|
|
|
import { Instance } from "@/project/instance"
|
|
import { Instance } from "@/project/instance"
|
|
|
-import {
|
|
|
|
|
- checkPluginCompatibility,
|
|
|
|
|
- isDeprecatedPlugin,
|
|
|
|
|
- pluginSource,
|
|
|
|
|
- readPluginId,
|
|
|
|
|
- readV1Plugin,
|
|
|
|
|
- resolvePluginEntrypoint,
|
|
|
|
|
- resolvePluginId,
|
|
|
|
|
- resolvePluginTarget,
|
|
|
|
|
- type PluginSource,
|
|
|
|
|
-} from "@/plugin/shared"
|
|
|
|
|
|
|
+import { pluginSource, readPluginId, readV1Plugin, resolvePluginId, type PluginSource } from "@/plugin/shared"
|
|
|
|
|
+import { PluginLoader } from "@/plugin/loader"
|
|
|
import { PluginMeta } from "@/plugin/meta"
|
|
import { PluginMeta } from "@/plugin/meta"
|
|
|
import { installPlugin as installModulePlugin, patchPluginConfig, readPluginManifest } from "@/plugin/install"
|
|
import { installPlugin as installModulePlugin, patchPluginConfig, readPluginManifest } from "@/plugin/install"
|
|
|
-import { addTheme, hasTheme } from "../context/theme"
|
|
|
|
|
|
|
+import { hasTheme, upsertTheme } from "../context/theme"
|
|
|
import { Global } from "@/global"
|
|
import { Global } from "@/global"
|
|
|
import { Filesystem } from "@/util/filesystem"
|
|
import { Filesystem } from "@/util/filesystem"
|
|
|
import { Process } from "@/util/process"
|
|
import { Process } from "@/util/process"
|
|
|
import { Flag } from "@/flag/flag"
|
|
import { Flag } from "@/flag/flag"
|
|
|
-import { Installation } from "@/installation"
|
|
|
|
|
import { INTERNAL_TUI_PLUGINS, type InternalTuiPlugin } from "./internal"
|
|
import { INTERNAL_TUI_PLUGINS, type InternalTuiPlugin } from "./internal"
|
|
|
import { setupSlots, Slot as View } from "./slots"
|
|
import { setupSlots, Slot as View } from "./slots"
|
|
|
import type { HostPluginApi, HostSlots } from "./slots"
|
|
import type { HostPluginApi, HostSlots } from "./slots"
|
|
|
|
|
|
|
|
type PluginLoad = {
|
|
type PluginLoad = {
|
|
|
- item?: Config.PluginSpec
|
|
|
|
|
|
|
+ options: Config.PluginOptions | undefined
|
|
|
spec: string
|
|
spec: string
|
|
|
target: string
|
|
target: string
|
|
|
retry: boolean
|
|
retry: boolean
|
|
|
source: PluginSource | "internal"
|
|
source: PluginSource | "internal"
|
|
|
id: string
|
|
id: string
|
|
|
module: TuiPluginModule
|
|
module: TuiPluginModule
|
|
|
- install_theme: TuiTheme["install"]
|
|
|
|
|
|
|
+ theme_meta: TuiConfig.PluginMeta
|
|
|
|
|
+ theme_root: string
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
type Api = HostPluginApi
|
|
type Api = HostPluginApi
|
|
@@ -64,8 +55,8 @@ type PluginEntry = {
|
|
|
id: string
|
|
id: string
|
|
|
load: PluginLoad
|
|
load: PluginLoad
|
|
|
meta: TuiPluginMeta
|
|
meta: TuiPluginMeta
|
|
|
|
|
+ themes: Record<string, PluginMeta.Theme>
|
|
|
plugin: TuiPlugin
|
|
plugin: TuiPlugin
|
|
|
- options: Config.PluginOptions | undefined
|
|
|
|
|
enabled: boolean
|
|
enabled: boolean
|
|
|
scope?: PluginScope
|
|
scope?: PluginScope
|
|
|
}
|
|
}
|
|
@@ -76,13 +67,7 @@ type RuntimeState = {
|
|
|
slots: HostSlots
|
|
slots: HostSlots
|
|
|
plugins: PluginEntry[]
|
|
plugins: PluginEntry[]
|
|
|
plugins_by_id: Map<string, PluginEntry>
|
|
plugins_by_id: Map<string, PluginEntry>
|
|
|
- pending: Map<
|
|
|
|
|
- string,
|
|
|
|
|
- {
|
|
|
|
|
- item: Config.PluginSpec
|
|
|
|
|
- meta: TuiConfig.PluginMeta
|
|
|
|
|
- }
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ pending: Map<string, TuiConfig.PluginRecord>
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const log = Log.create({ service: "tui.plugin" })
|
|
const log = Log.create({ service: "tui.plugin" })
|
|
@@ -102,6 +87,11 @@ function fail(message: string, data: Record<string, unknown>) {
|
|
|
console.error(`[tui.plugin] ${text}`, next)
|
|
console.error(`[tui.plugin] ${text}`, next)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function warn(message: string, data: Record<string, unknown>) {
|
|
|
|
|
+ log.warn(message, data)
|
|
|
|
|
+ console.warn(`[tui.plugin] ${message}`, data)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
type CleanupResult = { type: "ok" } | { type: "error"; error: unknown } | { type: "timeout" }
|
|
type CleanupResult = { type: "ok" } | { type: "error"; error: unknown } | { type: "timeout" }
|
|
|
|
|
|
|
|
function runCleanup(fn: () => unknown, ms: number): Promise<CleanupResult> {
|
|
function runCleanup(fn: () => unknown, ms: number): Promise<CleanupResult> {
|
|
@@ -143,12 +133,54 @@ function resolveRoot(root: string) {
|
|
|
return path.resolve(process.cwd(), root)
|
|
return path.resolve(process.cwd(), root)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function createThemeInstaller(meta: TuiConfig.PluginMeta, root: string, spec: string): TuiTheme["install"] {
|
|
|
|
|
|
|
+function createThemeInstaller(
|
|
|
|
|
+ meta: TuiConfig.PluginMeta,
|
|
|
|
|
+ root: string,
|
|
|
|
|
+ spec: string,
|
|
|
|
|
+ plugin: PluginEntry,
|
|
|
|
|
+): TuiTheme["install"] {
|
|
|
return async (file) => {
|
|
return async (file) => {
|
|
|
const raw = file.startsWith("file://") ? fileURLToPath(file) : file
|
|
const raw = file.startsWith("file://") ? fileURLToPath(file) : file
|
|
|
const src = path.isAbsolute(raw) ? raw : path.resolve(root, raw)
|
|
const src = path.isAbsolute(raw) ? raw : path.resolve(root, raw)
|
|
|
- const theme = path.basename(src, path.extname(src))
|
|
|
|
|
- if (hasTheme(theme)) return
|
|
|
|
|
|
|
+ const name = path.basename(src, path.extname(src))
|
|
|
|
|
+ const source_dir = path.dirname(meta.source)
|
|
|
|
|
+ const local_dir =
|
|
|
|
|
+ path.basename(source_dir) === ".opencode"
|
|
|
|
|
+ ? path.join(source_dir, "themes")
|
|
|
|
|
+ : path.join(source_dir, ".opencode", "themes")
|
|
|
|
|
+ const dest_dir = meta.scope === "local" ? local_dir : path.join(Global.Path.config, "themes")
|
|
|
|
|
+ const dest = path.join(dest_dir, `${name}.json`)
|
|
|
|
|
+ const stat = await Filesystem.statAsync(src)
|
|
|
|
|
+ const mtime = stat ? Math.floor(typeof stat.mtimeMs === "bigint" ? Number(stat.mtimeMs) : stat.mtimeMs) : undefined
|
|
|
|
|
+ const size = stat ? (typeof stat.size === "bigint" ? Number(stat.size) : stat.size) : undefined
|
|
|
|
|
+ const exists = hasTheme(name)
|
|
|
|
|
+ const prev = plugin.themes[name]
|
|
|
|
|
+
|
|
|
|
|
+ if (exists) {
|
|
|
|
|
+ if (plugin.meta.state !== "updated") return
|
|
|
|
|
+ if (!prev) {
|
|
|
|
|
+ if (await Filesystem.exists(dest)) {
|
|
|
|
|
+ plugin.themes[name] = {
|
|
|
|
|
+ src,
|
|
|
|
|
+ dest,
|
|
|
|
|
+ mtime,
|
|
|
|
|
+ size,
|
|
|
|
|
+ }
|
|
|
|
|
+ await PluginMeta.setTheme(plugin.id, name, plugin.themes[name]!).catch((error) => {
|
|
|
|
|
+ log.warn("failed to track tui plugin theme", {
|
|
|
|
|
+ path: spec,
|
|
|
|
|
+ id: plugin.id,
|
|
|
|
|
+ theme: src,
|
|
|
|
|
+ dest,
|
|
|
|
|
+ error,
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (prev.dest !== dest) return
|
|
|
|
|
+ if (prev.mtime === mtime && prev.size === size) return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
const text = await Filesystem.readText(src).catch((error) => {
|
|
const text = await Filesystem.readText(src).catch((error) => {
|
|
|
log.warn("failed to read tui plugin theme", { path: spec, theme: src, error })
|
|
log.warn("failed to read tui plugin theme", { path: spec, theme: src, error })
|
|
@@ -170,90 +202,110 @@ function createThemeInstaller(meta: TuiConfig.PluginMeta, root: string, spec: st
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const source_dir = path.dirname(meta.source)
|
|
|
|
|
- const local_dir =
|
|
|
|
|
- path.basename(source_dir) === ".opencode"
|
|
|
|
|
- ? path.join(source_dir, "themes")
|
|
|
|
|
- : path.join(source_dir, ".opencode", "themes")
|
|
|
|
|
- const dest_dir = meta.scope === "local" ? local_dir : path.join(Global.Path.config, "themes")
|
|
|
|
|
- const dest = path.join(dest_dir, `${theme}.json`)
|
|
|
|
|
- if (!(await Filesystem.exists(dest))) {
|
|
|
|
|
|
|
+ if (exists || !(await Filesystem.exists(dest))) {
|
|
|
await Filesystem.write(dest, text).catch((error) => {
|
|
await Filesystem.write(dest, text).catch((error) => {
|
|
|
log.warn("failed to persist tui plugin theme", { path: spec, theme: src, dest, error })
|
|
log.warn("failed to persist tui plugin theme", { path: spec, theme: src, dest, error })
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- addTheme(theme, data)
|
|
|
|
|
|
|
+ upsertTheme(name, data)
|
|
|
|
|
+ plugin.themes[name] = {
|
|
|
|
|
+ src,
|
|
|
|
|
+ dest,
|
|
|
|
|
+ mtime,
|
|
|
|
|
+ size,
|
|
|
|
|
+ }
|
|
|
|
|
+ await PluginMeta.setTheme(plugin.id, name, plugin.themes[name]!).catch((error) => {
|
|
|
|
|
+ log.warn("failed to track tui plugin theme", {
|
|
|
|
|
+ path: spec,
|
|
|
|
|
+ id: plugin.id,
|
|
|
|
|
+ theme: src,
|
|
|
|
|
+ dest,
|
|
|
|
|
+ error,
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-async function loadExternalPlugin(
|
|
|
|
|
- item: Config.PluginSpec,
|
|
|
|
|
- meta: TuiConfig.PluginMeta | undefined,
|
|
|
|
|
- retry = false,
|
|
|
|
|
-): Promise<PluginLoad | undefined> {
|
|
|
|
|
- const spec = Config.pluginSpecifier(item)
|
|
|
|
|
- if (isDeprecatedPlugin(spec)) return
|
|
|
|
|
- log.info("loading tui plugin", { path: spec, retry })
|
|
|
|
|
- const resolved = await resolvePluginTarget(spec).catch((error) => {
|
|
|
|
|
- fail("failed to resolve tui plugin", { path: spec, retry, error })
|
|
|
|
|
- return
|
|
|
|
|
- })
|
|
|
|
|
- if (!resolved) return
|
|
|
|
|
|
|
+async function loadExternalPlugin(cfg: TuiConfig.PluginRecord, retry = false): Promise<PluginLoad | undefined> {
|
|
|
|
|
+ const plan = PluginLoader.plan(cfg.item)
|
|
|
|
|
+ if (plan.deprecated) 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
|
|
|
|
|
|
|
+ log.info("loading tui plugin", { path: plan.spec, retry })
|
|
|
|
|
+ const resolved = await PluginLoader.resolve(plan, "tui")
|
|
|
|
|
+ if (!resolved.ok) {
|
|
|
|
|
+ if (resolved.stage === "missing") {
|
|
|
|
|
+ warn("tui plugin has no entrypoint", {
|
|
|
|
|
+ path: plan.spec,
|
|
|
|
|
+ retry,
|
|
|
|
|
+ message: resolved.message,
|
|
|
})
|
|
})
|
|
|
- if (!ok) return
|
|
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (resolved.stage === "install") {
|
|
|
|
|
+ fail("failed to resolve tui plugin", { path: plan.spec, retry, error: resolved.error })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (resolved.stage === "compatibility") {
|
|
|
|
|
+ fail("tui plugin incompatible", { path: plan.spec, retry, error: resolved.error })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ fail("failed to resolve tui plugin entry", { path: plan.spec, retry, error: resolved.error })
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const target = resolved
|
|
|
|
|
- if (!meta) {
|
|
|
|
|
- fail("missing tui plugin metadata", {
|
|
|
|
|
- path: spec,
|
|
|
|
|
|
|
+ const loaded = await PluginLoader.load(resolved.value)
|
|
|
|
|
+ if (!loaded.ok) {
|
|
|
|
|
+ fail("failed to load tui plugin", {
|
|
|
|
|
+ path: plan.spec,
|
|
|
|
|
+ target: resolved.value.entry,
|
|
|
retry,
|
|
retry,
|
|
|
|
|
+ error: loaded.error,
|
|
|
})
|
|
})
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const root = resolveRoot(source === "file" ? spec : target)
|
|
|
|
|
- const install_theme = createThemeInstaller(meta, root, spec)
|
|
|
|
|
- const entry = await resolvePluginEntrypoint(spec, target, "tui").catch((error) => {
|
|
|
|
|
- fail("failed to resolve tui plugin entry", { path: spec, target, retry, error })
|
|
|
|
|
- return
|
|
|
|
|
- })
|
|
|
|
|
- if (!entry) return
|
|
|
|
|
-
|
|
|
|
|
- const mod = await import(entry)
|
|
|
|
|
- .then((raw) => {
|
|
|
|
|
- return readV1Plugin(raw as Record<string, unknown>, spec, "tui") as TuiPluginModule
|
|
|
|
|
|
|
+ const mod = await Promise.resolve()
|
|
|
|
|
+ .then(() => {
|
|
|
|
|
+ return readV1Plugin(loaded.value.mod as Record<string, unknown>, plan.spec, "tui") as TuiPluginModule
|
|
|
})
|
|
})
|
|
|
.catch((error) => {
|
|
.catch((error) => {
|
|
|
- fail("failed to load tui plugin", { path: spec, target: entry, retry, error })
|
|
|
|
|
|
|
+ fail("failed to load tui plugin", {
|
|
|
|
|
+ path: plan.spec,
|
|
|
|
|
+ target: loaded.value.entry,
|
|
|
|
|
+ retry,
|
|
|
|
|
+ error,
|
|
|
|
|
+ })
|
|
|
return
|
|
return
|
|
|
})
|
|
})
|
|
|
if (!mod) return
|
|
if (!mod) return
|
|
|
|
|
|
|
|
- const id = await resolvePluginId(source, spec, target, readPluginId(mod.id, spec)).catch((error) => {
|
|
|
|
|
- fail("failed to load tui plugin", { path: spec, target, retry, error })
|
|
|
|
|
|
|
+ const id = await resolvePluginId(
|
|
|
|
|
+ loaded.value.source,
|
|
|
|
|
+ plan.spec,
|
|
|
|
|
+ loaded.value.target,
|
|
|
|
|
+ readPluginId(mod.id, plan.spec),
|
|
|
|
|
+ loaded.value.pkg,
|
|
|
|
|
+ ).catch((error) => {
|
|
|
|
|
+ fail("failed to load tui plugin", { path: plan.spec, target: loaded.value.target, retry, error })
|
|
|
return
|
|
return
|
|
|
})
|
|
})
|
|
|
if (!id) return
|
|
if (!id) return
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
- item,
|
|
|
|
|
- spec,
|
|
|
|
|
- target,
|
|
|
|
|
|
|
+ options: plan.options,
|
|
|
|
|
+ spec: plan.spec,
|
|
|
|
|
+ target: loaded.value.target,
|
|
|
retry,
|
|
retry,
|
|
|
- source,
|
|
|
|
|
|
|
+ source: loaded.value.source,
|
|
|
id,
|
|
id,
|
|
|
module: mod,
|
|
module: mod,
|
|
|
- install_theme,
|
|
|
|
|
|
|
+ theme_meta: {
|
|
|
|
|
+ scope: cfg.scope,
|
|
|
|
|
+ source: cfg.source,
|
|
|
|
|
+ },
|
|
|
|
|
+ theme_root: loaded.value.pkg?.dir ?? resolveRoot(loaded.value.target),
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -291,20 +343,18 @@ function loadInternalPlugin(item: InternalTuiPlugin): PluginLoad {
|
|
|
const target = spec
|
|
const target = spec
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
|
|
+ options: undefined,
|
|
|
spec,
|
|
spec,
|
|
|
target,
|
|
target,
|
|
|
retry: false,
|
|
retry: false,
|
|
|
source: "internal",
|
|
source: "internal",
|
|
|
id: item.id,
|
|
id: item.id,
|
|
|
module: item,
|
|
module: item,
|
|
|
- install_theme: createThemeInstaller(
|
|
|
|
|
- {
|
|
|
|
|
- scope: "global",
|
|
|
|
|
- source: target,
|
|
|
|
|
- },
|
|
|
|
|
- process.cwd(),
|
|
|
|
|
- spec,
|
|
|
|
|
- ),
|
|
|
|
|
|
|
+ theme_meta: {
|
|
|
|
|
+ scope: "global",
|
|
|
|
|
+ source: target,
|
|
|
|
|
+ },
|
|
|
|
|
+ theme_root: process.cwd(),
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -436,10 +486,10 @@ async function activatePluginEntry(state: RuntimeState, plugin: PluginEntry, per
|
|
|
if (plugin.scope) return true
|
|
if (plugin.scope) return true
|
|
|
|
|
|
|
|
const scope = createPluginScope(plugin.load, plugin.id)
|
|
const scope = createPluginScope(plugin.load, plugin.id)
|
|
|
- const api = pluginApi(state, plugin.load, scope, plugin.id)
|
|
|
|
|
|
|
+ const api = pluginApi(state, plugin, scope, plugin.id)
|
|
|
const ok = await Promise.resolve()
|
|
const ok = await Promise.resolve()
|
|
|
.then(async () => {
|
|
.then(async () => {
|
|
|
- await plugin.plugin(api, plugin.options, plugin.meta)
|
|
|
|
|
|
|
+ await plugin.plugin(api, plugin.load.options, plugin.meta)
|
|
|
return true
|
|
return true
|
|
|
})
|
|
})
|
|
|
.catch((error) => {
|
|
.catch((error) => {
|
|
@@ -479,9 +529,10 @@ async function deactivatePluginById(state: RuntimeState | undefined, id: string,
|
|
|
return deactivatePluginEntry(state, plugin, persist)
|
|
return deactivatePluginEntry(state, plugin, persist)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function pluginApi(runtime: RuntimeState, load: PluginLoad, scope: PluginScope, base: string): TuiPluginApi {
|
|
|
|
|
|
|
+function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScope, base: string): TuiPluginApi {
|
|
|
const api = runtime.api
|
|
const api = runtime.api
|
|
|
const host = runtime.slots
|
|
const host = runtime.slots
|
|
|
|
|
+ const load = plugin.load
|
|
|
const command: TuiPluginApi["command"] = {
|
|
const command: TuiPluginApi["command"] = {
|
|
|
register(cb) {
|
|
register(cb) {
|
|
|
return scope.track(api.command.register(cb))
|
|
return scope.track(api.command.register(cb))
|
|
@@ -504,7 +555,7 @@ function pluginApi(runtime: RuntimeState, load: PluginLoad, scope: PluginScope,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const theme: TuiPluginApi["theme"] = Object.assign(Object.create(api.theme), {
|
|
const theme: TuiPluginApi["theme"] = Object.assign(Object.create(api.theme), {
|
|
|
- install: load.install_theme,
|
|
|
|
|
|
|
+ install: createThemeInstaller(load.theme_meta, load.theme_root, load.spec, plugin),
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const event: TuiPluginApi["event"] = {
|
|
const event: TuiPluginApi["event"] = {
|
|
@@ -563,20 +614,6 @@ function pluginApi(runtime: RuntimeState, load: PluginLoad, scope: PluginScope,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function collectPluginEntries(load: PluginLoad, meta: TuiPluginMeta) {
|
|
|
|
|
- const options = load.item ? Config.pluginOptions(load.item) : undefined
|
|
|
|
|
- return [
|
|
|
|
|
- {
|
|
|
|
|
- id: load.id,
|
|
|
|
|
- load,
|
|
|
|
|
- meta,
|
|
|
|
|
- plugin: load.module.tui,
|
|
|
|
|
- options,
|
|
|
|
|
- enabled: true,
|
|
|
|
|
- },
|
|
|
|
|
- ]
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function addPluginEntry(state: RuntimeState, plugin: PluginEntry) {
|
|
function addPluginEntry(state: RuntimeState, plugin: PluginEntry) {
|
|
|
if (state.plugins_by_id.has(plugin.id)) {
|
|
if (state.plugins_by_id.has(plugin.id)) {
|
|
|
fail("duplicate tui plugin id", {
|
|
fail("duplicate tui plugin id", {
|
|
@@ -600,12 +637,8 @@ function applyInitialPluginEnabledState(state: RuntimeState, config: TuiConfig.I
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-async function resolveExternalPlugins(
|
|
|
|
|
- list: Config.PluginSpec[],
|
|
|
|
|
- wait: () => Promise<void>,
|
|
|
|
|
- meta: (item: Config.PluginSpec) => TuiConfig.PluginMeta | undefined,
|
|
|
|
|
-) {
|
|
|
|
|
- const loaded = await Promise.all(list.map((item) => loadExternalPlugin(item, meta(item))))
|
|
|
|
|
|
|
+async function resolveExternalPlugins(list: TuiConfig.PluginRecord[], wait: () => Promise<void>) {
|
|
|
|
|
+ const loaded = await Promise.all(list.map((item) => loadExternalPlugin(item)))
|
|
|
const ready: PluginLoad[] = []
|
|
const ready: PluginLoad[] = []
|
|
|
let deps: Promise<void> | undefined
|
|
let deps: Promise<void> | undefined
|
|
|
|
|
|
|
@@ -614,13 +647,12 @@ async function resolveExternalPlugins(
|
|
|
if (!entry) {
|
|
if (!entry) {
|
|
|
const item = list[i]
|
|
const item = list[i]
|
|
|
if (!item) continue
|
|
if (!item) continue
|
|
|
- const spec = Config.pluginSpecifier(item)
|
|
|
|
|
- if (pluginSource(spec) !== "file") continue
|
|
|
|
|
|
|
+ if (pluginSource(Config.pluginSpecifier(item.item)) !== "file") continue
|
|
|
deps ??= wait().catch((error) => {
|
|
deps ??= wait().catch((error) => {
|
|
|
log.warn("failed waiting for tui plugin dependencies", { error })
|
|
log.warn("failed waiting for tui plugin dependencies", { error })
|
|
|
})
|
|
})
|
|
|
await deps
|
|
await deps
|
|
|
- entry = await loadExternalPlugin(item, meta(item), true)
|
|
|
|
|
|
|
+ entry = await loadExternalPlugin(item, true)
|
|
|
}
|
|
}
|
|
|
if (!entry) continue
|
|
if (!entry) continue
|
|
|
ready.push(entry)
|
|
ready.push(entry)
|
|
@@ -661,20 +693,28 @@ async function addExternalPluginEntries(state: RuntimeState, ready: PluginLoad[]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const row = createMeta(entry.source, entry.spec, entry.target, hit, entry.id)
|
|
const row = createMeta(entry.source, entry.spec, entry.target, hit, entry.id)
|
|
|
- for (const plugin of collectPluginEntries(entry, row)) {
|
|
|
|
|
- if (!addPluginEntry(state, plugin)) {
|
|
|
|
|
- ok = false
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
- plugins.push(plugin)
|
|
|
|
|
|
|
+ const themes = hit?.entry.themes ? { ...hit.entry.themes } : {}
|
|
|
|
|
+ const plugin: PluginEntry = {
|
|
|
|
|
+ id: entry.id,
|
|
|
|
|
+ load: entry,
|
|
|
|
|
+ meta: row,
|
|
|
|
|
+ themes,
|
|
|
|
|
+ plugin: entry.module.tui,
|
|
|
|
|
+ enabled: true,
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!addPluginEntry(state, plugin)) {
|
|
|
|
|
+ ok = false
|
|
|
|
|
+ continue
|
|
|
}
|
|
}
|
|
|
|
|
+ plugins.push(plugin)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return { plugins, ok }
|
|
return { plugins, ok }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function defaultPluginMeta(state: RuntimeState): TuiConfig.PluginMeta {
|
|
|
|
|
|
|
+function defaultPluginRecord(state: RuntimeState, spec: string): TuiConfig.PluginRecord {
|
|
|
return {
|
|
return {
|
|
|
|
|
+ item: spec,
|
|
|
scope: "local",
|
|
scope: "local",
|
|
|
source: state.api.state.path.config || path.join(state.directory, ".opencode", "tui.json"),
|
|
source: state.api.state.path.config || path.join(state.directory, ".opencode", "tui.json"),
|
|
|
}
|
|
}
|
|
@@ -712,36 +752,27 @@ async function addPluginBySpec(state: RuntimeState | undefined, raw: string) {
|
|
|
const spec = raw.trim()
|
|
const spec = raw.trim()
|
|
|
if (!spec) return false
|
|
if (!spec) return false
|
|
|
|
|
|
|
|
- const pending = state.pending.get(spec)
|
|
|
|
|
- const item = pending?.item ?? spec
|
|
|
|
|
- const nextSpec = Config.pluginSpecifier(item)
|
|
|
|
|
- if (state.plugins.some((plugin) => plugin.load.spec === nextSpec)) {
|
|
|
|
|
|
|
+ const cfg = state.pending.get(spec) ?? defaultPluginRecord(state, spec)
|
|
|
|
|
+ const next = Config.pluginSpecifier(cfg.item)
|
|
|
|
|
+ if (state.plugins.some((plugin) => plugin.load.spec === next)) {
|
|
|
state.pending.delete(spec)
|
|
state.pending.delete(spec)
|
|
|
return true
|
|
return true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const meta = pending?.meta ?? defaultPluginMeta(state)
|
|
|
|
|
-
|
|
|
|
|
const ready = await Instance.provide({
|
|
const ready = await Instance.provide({
|
|
|
directory: state.directory,
|
|
directory: state.directory,
|
|
|
- fn: () =>
|
|
|
|
|
- resolveExternalPlugins(
|
|
|
|
|
- [item],
|
|
|
|
|
- () => TuiConfig.waitForDependencies(),
|
|
|
|
|
- () => meta,
|
|
|
|
|
- ),
|
|
|
|
|
|
|
+ fn: () => resolveExternalPlugins([cfg], () => TuiConfig.waitForDependencies()),
|
|
|
}).catch((error) => {
|
|
}).catch((error) => {
|
|
|
- fail("failed to add tui plugin", { path: nextSpec, error })
|
|
|
|
|
|
|
+ fail("failed to add tui plugin", { path: next, error })
|
|
|
return [] as PluginLoad[]
|
|
return [] as PluginLoad[]
|
|
|
})
|
|
})
|
|
|
if (!ready.length) {
|
|
if (!ready.length) {
|
|
|
- fail("failed to add tui plugin", { path: nextSpec })
|
|
|
|
|
return false
|
|
return false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const first = ready[0]
|
|
const first = ready[0]
|
|
|
if (!first) {
|
|
if (!first) {
|
|
|
- fail("failed to add tui plugin", { path: nextSpec })
|
|
|
|
|
|
|
+ fail("failed to add tui plugin", { path: next })
|
|
|
return false
|
|
return false
|
|
|
}
|
|
}
|
|
|
if (state.plugins_by_id.has(first.id)) {
|
|
if (state.plugins_by_id.has(first.id)) {
|
|
@@ -758,7 +789,7 @@ async function addPluginBySpec(state: RuntimeState | undefined, raw: string) {
|
|
|
|
|
|
|
|
if (ok) state.pending.delete(spec)
|
|
if (ok) state.pending.delete(spec)
|
|
|
if (!ok) {
|
|
if (!ok) {
|
|
|
- fail("failed to add tui plugin", { path: nextSpec })
|
|
|
|
|
|
|
+ fail("failed to add tui plugin", { path: next })
|
|
|
}
|
|
}
|
|
|
return ok
|
|
return ok
|
|
|
}
|
|
}
|
|
@@ -806,7 +837,7 @@ async function installPluginBySpec(
|
|
|
if (manifest.code === "manifest_no_targets") {
|
|
if (manifest.code === "manifest_no_targets") {
|
|
|
return {
|
|
return {
|
|
|
ok: false,
|
|
ok: false,
|
|
|
- message: `"${spec}" does not declare supported targets in package.json`,
|
|
|
|
|
|
|
+ message: `"${spec}" does not expose plugin entrypoints in package.json`,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -841,12 +872,11 @@ async function installPluginBySpec(
|
|
|
const tui = manifest.targets.find((item) => item.kind === "tui")
|
|
const tui = manifest.targets.find((item) => item.kind === "tui")
|
|
|
if (tui) {
|
|
if (tui) {
|
|
|
const file = patch.items.find((item) => item.kind === "tui")?.file
|
|
const file = patch.items.find((item) => item.kind === "tui")?.file
|
|
|
|
|
+ const item = tui.opts ? ([spec, tui.opts] as Config.PluginSpec) : spec
|
|
|
state.pending.set(spec, {
|
|
state.pending.set(spec, {
|
|
|
- item: tui.opts ? [spec, tui.opts] : spec,
|
|
|
|
|
- meta: {
|
|
|
|
|
- scope: global ? "global" : "local",
|
|
|
|
|
- source: (file ?? dir.config) || path.join(patch.dir, "tui.json"),
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ item,
|
|
|
|
|
+ scope: global ? "global" : "local",
|
|
|
|
|
+ source: (file ?? dir.config) || path.join(patch.dir, "tui.json"),
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -929,25 +959,26 @@ export namespace TuiPluginRuntime {
|
|
|
directory: cwd,
|
|
directory: cwd,
|
|
|
fn: async () => {
|
|
fn: async () => {
|
|
|
const config = await TuiConfig.get()
|
|
const config = await TuiConfig.get()
|
|
|
- const plugins = Flag.OPENCODE_PURE ? [] : (config.plugin ?? [])
|
|
|
|
|
- if (Flag.OPENCODE_PURE && config.plugin?.length) {
|
|
|
|
|
- log.info("skipping external tui plugins in pure mode", { count: config.plugin.length })
|
|
|
|
|
|
|
+ const records = Flag.OPENCODE_PURE ? [] : (config.plugin_records ?? [])
|
|
|
|
|
+ if (Flag.OPENCODE_PURE && config.plugin_records?.length) {
|
|
|
|
|
+ log.info("skipping external tui plugins in pure mode", { count: config.plugin_records.length })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
for (const item of INTERNAL_TUI_PLUGINS) {
|
|
for (const item of INTERNAL_TUI_PLUGINS) {
|
|
|
log.info("loading internal tui plugin", { id: item.id })
|
|
log.info("loading internal tui plugin", { id: item.id })
|
|
|
const entry = loadInternalPlugin(item)
|
|
const entry = loadInternalPlugin(item)
|
|
|
const meta = createMeta(entry.source, entry.spec, entry.target, undefined, entry.id)
|
|
const meta = createMeta(entry.source, entry.spec, entry.target, undefined, entry.id)
|
|
|
- for (const plugin of collectPluginEntries(entry, meta)) {
|
|
|
|
|
- addPluginEntry(next, plugin)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ addPluginEntry(next, {
|
|
|
|
|
+ id: entry.id,
|
|
|
|
|
+ load: entry,
|
|
|
|
|
+ meta,
|
|
|
|
|
+ themes: {},
|
|
|
|
|
+ plugin: entry.module.tui,
|
|
|
|
|
+ enabled: true,
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const ready = await resolveExternalPlugins(
|
|
|
|
|
- plugins,
|
|
|
|
|
- () => TuiConfig.waitForDependencies(),
|
|
|
|
|
- (item) => config.plugin_meta?.[Config.pluginSpecifier(item)],
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ const ready = await resolveExternalPlugins(records, () => TuiConfig.waitForDependencies())
|
|
|
await addExternalPluginEntries(next, ready)
|
|
await addExternalPluginEntries(next, ready)
|
|
|
|
|
|
|
|
applyInitialPluginEnabledState(next, config)
|
|
applyInitialPluginEnabledState(next, config)
|