dedupe
parent
72c4deb90c
commit
71b960f2d1
|
|
@ -12,8 +12,9 @@ import "@opentui/solid/preload"
|
|||
import { Config } from "@/config/config"
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
import { Log } from "@/util/log"
|
||||
import { BunProc } from "@/bun"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { resolvePluginTarget, uniqueModuleEntries } from "@/plugin/shared"
|
||||
import { registerThemes } from "./context/theme"
|
||||
|
||||
type Slot = <K extends keyof TuiSlotMap>(props: { name: K } & TuiSlotMap[K]) => JSX.Element | null
|
||||
|
|
@ -22,12 +23,6 @@ function empty<K extends keyof TuiSlotMap>(_props: { name: K } & TuiSlotMap[K])
|
|||
return null
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
if (!value || typeof value !== "object") return false
|
||||
if (Array.isArray(value)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function isTuiSlotPlugin(value: unknown): value is SolidPlugin<TuiSlotMap, TuiSlotContext> {
|
||||
if (!isRecord(value)) return false
|
||||
if (typeof value.id !== "string") return false
|
||||
|
|
@ -99,14 +94,6 @@ export namespace TuiPlugin {
|
|||
return loaded
|
||||
}
|
||||
|
||||
async function resolve(spec: string) {
|
||||
if (spec.startsWith("file://")) return spec
|
||||
const lastAtIndex = spec.lastIndexOf("@")
|
||||
const pkg = lastAtIndex > 0 ? spec.substring(0, lastAtIndex) : spec
|
||||
const version = lastAtIndex > 0 ? spec.substring(lastAtIndex + 1) : "latest"
|
||||
return BunProc.install(pkg, version)
|
||||
}
|
||||
|
||||
async function load<Renderer>(input: TuiPluginInput<Renderer>) {
|
||||
const dir = process.cwd()
|
||||
|
||||
|
|
@ -120,7 +107,7 @@ export namespace TuiPlugin {
|
|||
for (const item of plugins) {
|
||||
const spec = Config.pluginSpecifier(item)
|
||||
log.info("loading tui plugin", { path: spec })
|
||||
const target = await resolve(spec).catch((error) => {
|
||||
const target = await resolvePluginTarget(spec).catch((error) => {
|
||||
log.error("failed to resolve tui plugin", { path: spec, error })
|
||||
return
|
||||
})
|
||||
|
|
@ -132,10 +119,7 @@ export namespace TuiPlugin {
|
|||
})
|
||||
if (!mod) continue
|
||||
|
||||
const seen = new Set<unknown>()
|
||||
for (const [name, entry] of Object.entries(mod)) {
|
||||
if (seen.has(entry)) continue
|
||||
seen.add(entry)
|
||||
for (const [name, entry] of uniqueModuleEntries(mod)) {
|
||||
if (!entry || typeof entry !== "object") {
|
||||
log.warn("ignoring non-object tui plugin export", {
|
||||
path: spec,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { Glob } from "../util/glob"
|
|||
import { PackageRegistry } from "@/bun/registry"
|
||||
import { proxied } from "@/util/proxied"
|
||||
import { iife } from "@/util/iife"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { Control } from "@/control"
|
||||
import { ConfigPaths } from "./paths"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
|
|
@ -1324,10 +1325,6 @@ export namespace Config {
|
|||
return candidates[0]
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function patchJsonc(input: string, patch: unknown, path: string[] = []): string {
|
||||
if (!isRecord(patch)) {
|
||||
const edits = modify(input, path, patch, {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { TuiInfo } from "./tui-schema"
|
|||
import { Instance } from "@/project/instance"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Log } from "@/util/log"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { Global } from "@/global"
|
||||
|
||||
export namespace TuiConfig {
|
||||
|
|
@ -100,12 +101,6 @@ export namespace TuiConfig {
|
|||
await Promise.all(deps)
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
if (!value || typeof value !== "object") return false
|
||||
if (Array.isArray(value)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
async function loadFile(filepath: string): Promise<Info> {
|
||||
const text = await ConfigPaths.readFile(filepath)
|
||||
if (!text) return {}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import { Bus } from "../bus"
|
|||
import { Log } from "../util/log"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk"
|
||||
import { Server } from "../server/server"
|
||||
import { BunProc } from "../bun"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { CodexAuthPlugin } from "./codex"
|
||||
import { Session } from "../session"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { CopilotAuthPlugin } from "./copilot"
|
||||
import { parsePluginSpecifier, resolvePluginTarget, uniqueModuleEntries } from "./shared"
|
||||
import { gitlabAuthPlugin as GitlabAuthPlugin } from "@gitlab/opencode-gitlab-auth"
|
||||
|
||||
export namespace Plugin {
|
||||
|
|
@ -55,25 +55,22 @@ export namespace Plugin {
|
|||
}
|
||||
|
||||
async function resolve(spec: string) {
|
||||
if (spec.startsWith("file://")) return spec
|
||||
const lastAtIndex = spec.lastIndexOf("@")
|
||||
const pkg = lastAtIndex > 0 ? spec.substring(0, lastAtIndex) : spec
|
||||
const version = lastAtIndex > 0 ? spec.substring(lastAtIndex + 1) : "latest"
|
||||
const builtIn = BUILTIN.some((x) => x.startsWith(pkg + "@"))
|
||||
const installed = await BunProc.install(pkg, version).catch((err) => {
|
||||
const parsed = parsePluginSpecifier(spec)
|
||||
const builtIn = BUILTIN.some((x) => x.startsWith(parsed.pkg + "@"))
|
||||
const target = await resolvePluginTarget(spec, parsed).catch((err) => {
|
||||
const cause = err instanceof Error ? err.cause : err
|
||||
const detail = cause instanceof Error ? cause.message : String(cause ?? err)
|
||||
log.error("failed to install plugin", { pkg, version, error: detail })
|
||||
log.error("failed to install plugin", { pkg: parsed.pkg, version: parsed.version, error: detail })
|
||||
const label = builtIn ? "built-in plugin" : "plugin"
|
||||
Bus.publish(Session.Event.Error, {
|
||||
error: new NamedError.Unknown({
|
||||
message: `Failed to install ${label} ${pkg}@${version}: ${detail}`,
|
||||
message: `Failed to install ${label} ${parsed.pkg}@${parsed.version}: ${detail}`,
|
||||
}).toObject(),
|
||||
})
|
||||
return ""
|
||||
})
|
||||
if (!installed) return
|
||||
return installed
|
||||
if (!target) return
|
||||
return target
|
||||
}
|
||||
|
||||
function isServerPlugin(value: unknown): value is PluginInstance {
|
||||
|
|
@ -108,11 +105,8 @@ export namespace Plugin {
|
|||
|
||||
// Prevent duplicate initialization when plugins export the same function
|
||||
// as both a named export and default export (e.g., `export const X` and `export default X`).
|
||||
// Object.entries(mod) would return both entries pointing to the same function reference.
|
||||
const seen = new Set<unknown>()
|
||||
for (const entry of Object.values(mod)) {
|
||||
if (seen.has(entry)) continue
|
||||
seen.add(entry)
|
||||
// uniqueModuleEntries keeps only the first export for each shared value reference.
|
||||
for (const [, entry] of uniqueModuleEntries(mod)) {
|
||||
const server = getServerPlugin(entry)
|
||||
if (!server) continue
|
||||
const init = await server(input, Config.pluginOptions(item)).catch((err) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import { BunProc } from "@/bun"
|
||||
|
||||
export function parsePluginSpecifier(spec: string) {
|
||||
const at = spec.lastIndexOf("@")
|
||||
const pkg = at > 0 ? spec.substring(0, at) : spec
|
||||
const version = at > 0 ? spec.substring(at + 1) : "latest"
|
||||
return { pkg, version }
|
||||
}
|
||||
|
||||
export async function resolvePluginTarget(spec: string, parsed = parsePluginSpecifier(spec)) {
|
||||
if (spec.startsWith("file://")) return spec
|
||||
return BunProc.install(parsed.pkg, parsed.version)
|
||||
}
|
||||
|
||||
export function uniqueModuleEntries(mod: Record<string, unknown>) {
|
||||
const seen = new Set<unknown>()
|
||||
const entries: [string, unknown][] = []
|
||||
|
||||
for (const [name, entry] of Object.entries(mod)) {
|
||||
if (seen.has(entry)) continue
|
||||
seen.add(entry)
|
||||
entries.push([name, entry])
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value)
|
||||
}
|
||||
Loading…
Reference in New Issue