Explorar o código

feat: unwrap PluginMeta, PluginLoader, CopilotModels namespaces to flat exports + barrel

Kit Langton hai 1 día
pai
achega
c74ea2166f

+ 2 - 2
packages/opencode/src/cli/cmd/tui/plugin/runtime.ts

@@ -24,8 +24,8 @@ import {
   type PluginPackage,
   type PluginSource,
 } from "@/plugin/shared"
-import { PluginLoader } from "@/plugin/loader"
-import { PluginMeta } from "@/plugin/meta"
+import { PluginLoader } from "@/plugin"
+import { PluginMeta } from "@/plugin"
 import { installPlugin as installModulePlugin, patchPluginConfig, readPluginManifest } from "@/plugin/install"
 import { hasTheme, upsertTheme } from "../context/theme"
 import { Global } from "@/global"

+ 1 - 1
packages/opencode/src/plugin/github-copilot/copilot.ts

@@ -5,7 +5,7 @@ import { InstallationVersion } from "@/installation/version"
 import { iife } from "@/util/iife"
 import { Log } from "../../util"
 import { setTimeout as sleep } from "node:timers/promises"
-import { CopilotModels } from "./models"
+import * as CopilotModels from "./models"
 import { MessageV2 } from "@/session/message-v2"
 
 const log = Log.create({ service: "plugin.copilot" })

+ 1 - 0
packages/opencode/src/plugin/github-copilot/index.ts

@@ -0,0 +1 @@
+export * as CopilotModels from "./models"

+ 125 - 127
packages/opencode/src/plugin/github-copilot/models.ts

@@ -1,146 +1,144 @@
 import { z } from "zod"
 import type { Model } from "@opencode-ai/sdk/v2"
 
-export namespace CopilotModels {
-  export const schema = z.object({
-    data: z.array(
-      z.object({
-        model_picker_enabled: z.boolean(),
-        id: z.string(),
-        name: z.string(),
-        // every version looks like: `{model.id}-YYYY-MM-DD`
-        version: z.string(),
-        supported_endpoints: z.array(z.string()).optional(),
-        capabilities: z.object({
-          family: z.string(),
-          limits: z.object({
-            max_context_window_tokens: z.number(),
-            max_output_tokens: z.number(),
-            max_prompt_tokens: z.number(),
-            vision: z
-              .object({
-                max_prompt_image_size: z.number(),
-                max_prompt_images: z.number(),
-                supported_media_types: z.array(z.string()),
-              })
-              .optional(),
-          }),
-          supports: z.object({
-            adaptive_thinking: z.boolean().optional(),
-            max_thinking_budget: z.number().optional(),
-            min_thinking_budget: z.number().optional(),
-            reasoning_effort: z.array(z.string()).optional(),
-            streaming: z.boolean(),
-            structured_outputs: z.boolean().optional(),
-            tool_calls: z.boolean(),
-            vision: z.boolean().optional(),
-          }),
+export const schema = z.object({
+  data: z.array(
+    z.object({
+      model_picker_enabled: z.boolean(),
+      id: z.string(),
+      name: z.string(),
+      // every version looks like: `{model.id}-YYYY-MM-DD`
+      version: z.string(),
+      supported_endpoints: z.array(z.string()).optional(),
+      capabilities: z.object({
+        family: z.string(),
+        limits: z.object({
+          max_context_window_tokens: z.number(),
+          max_output_tokens: z.number(),
+          max_prompt_tokens: z.number(),
+          vision: z
+            .object({
+              max_prompt_image_size: z.number(),
+              max_prompt_images: z.number(),
+              supported_media_types: z.array(z.string()),
+            })
+            .optional(),
+        }),
+        supports: z.object({
+          adaptive_thinking: z.boolean().optional(),
+          max_thinking_budget: z.number().optional(),
+          min_thinking_budget: z.number().optional(),
+          reasoning_effort: z.array(z.string()).optional(),
+          streaming: z.boolean(),
+          structured_outputs: z.boolean().optional(),
+          tool_calls: z.boolean(),
+          vision: z.boolean().optional(),
         }),
       }),
-    ),
-  })
+    }),
+  ),
+})
 
-  type Item = z.infer<typeof schema>["data"][number]
+type Item = z.infer<typeof schema>["data"][number]
 
-  function build(key: string, remote: Item, url: string, prev?: Model): Model {
-    const reasoning =
-      !!remote.capabilities.supports.adaptive_thinking ||
-      !!remote.capabilities.supports.reasoning_effort?.length ||
-      remote.capabilities.supports.max_thinking_budget !== undefined ||
-      remote.capabilities.supports.min_thinking_budget !== undefined
-    const image =
-      (remote.capabilities.supports.vision ?? false) ||
-      (remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/"))
+function build(key: string, remote: Item, url: string, prev?: Model): Model {
+  const reasoning =
+    !!remote.capabilities.supports.adaptive_thinking ||
+    !!remote.capabilities.supports.reasoning_effort?.length ||
+    remote.capabilities.supports.max_thinking_budget !== undefined ||
+    remote.capabilities.supports.min_thinking_budget !== undefined
+  const image =
+    (remote.capabilities.supports.vision ?? false) ||
+    (remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/"))
 
-    const isMsgApi = remote.supported_endpoints?.includes("/v1/messages")
+  const isMsgApi = remote.supported_endpoints?.includes("/v1/messages")
 
-    return {
-      id: key,
-      providerID: "github-copilot",
-      api: {
-        id: remote.id,
-        url: isMsgApi ? `${url}/v1` : url,
-        npm: isMsgApi ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot",
-      },
-      // API response wins
-      status: "active",
-      limit: {
-        context: remote.capabilities.limits.max_context_window_tokens,
-        input: remote.capabilities.limits.max_prompt_tokens,
-        output: remote.capabilities.limits.max_output_tokens,
+  return {
+    id: key,
+    providerID: "github-copilot",
+    api: {
+      id: remote.id,
+      url: isMsgApi ? `${url}/v1` : url,
+      npm: isMsgApi ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot",
+    },
+    // API response wins
+    status: "active",
+    limit: {
+      context: remote.capabilities.limits.max_context_window_tokens,
+      input: remote.capabilities.limits.max_prompt_tokens,
+      output: remote.capabilities.limits.max_output_tokens,
+    },
+    capabilities: {
+      temperature: prev?.capabilities.temperature ?? true,
+      reasoning: prev?.capabilities.reasoning ?? reasoning,
+      attachment: prev?.capabilities.attachment ?? true,
+      toolcall: remote.capabilities.supports.tool_calls,
+      input: {
+        text: true,
+        audio: false,
+        image,
+        video: false,
+        pdf: false,
       },
-      capabilities: {
-        temperature: prev?.capabilities.temperature ?? true,
-        reasoning: prev?.capabilities.reasoning ?? reasoning,
-        attachment: prev?.capabilities.attachment ?? true,
-        toolcall: remote.capabilities.supports.tool_calls,
-        input: {
-          text: true,
-          audio: false,
-          image,
-          video: false,
-          pdf: false,
-        },
-        output: {
-          text: true,
-          audio: false,
-          image: false,
-          video: false,
-          pdf: false,
-        },
-        interleaved: false,
+      output: {
+        text: true,
+        audio: false,
+        image: false,
+        video: false,
+        pdf: false,
       },
-      // existing wins
-      family: prev?.family ?? remote.capabilities.family,
-      name: prev?.name ?? remote.name,
-      cost: {
-        input: 0,
-        output: 0,
-        cache: { read: 0, write: 0 },
-      },
-      options: prev?.options ?? {},
-      headers: prev?.headers ?? {},
-      release_date:
-        prev?.release_date ??
-        (remote.version.startsWith(`${remote.id}-`) ? remote.version.slice(remote.id.length + 1) : remote.version),
-      variants: prev?.variants ?? {},
-    }
+      interleaved: false,
+    },
+    // existing wins
+    family: prev?.family ?? remote.capabilities.family,
+    name: prev?.name ?? remote.name,
+    cost: {
+      input: 0,
+      output: 0,
+      cache: { read: 0, write: 0 },
+    },
+    options: prev?.options ?? {},
+    headers: prev?.headers ?? {},
+    release_date:
+      prev?.release_date ??
+      (remote.version.startsWith(`${remote.id}-`) ? remote.version.slice(remote.id.length + 1) : remote.version),
+    variants: prev?.variants ?? {},
   }
+}
 
-  export async function get(
-    baseURL: string,
-    headers: HeadersInit = {},
-    existing: Record<string, Model> = {},
-  ): Promise<Record<string, Model>> {
-    const data = await fetch(`${baseURL}/models`, {
-      headers,
-      signal: AbortSignal.timeout(5_000),
-    }).then(async (res) => {
-      if (!res.ok) {
-        throw new Error(`Failed to fetch models: ${res.status}`)
-      }
-      return schema.parse(await res.json())
-    })
-
-    const result = { ...existing }
-    const remote = new Map(data.data.filter((m) => m.model_picker_enabled).map((m) => [m.id, m] as const))
-
-    // prune existing models whose api.id isn't in the endpoint response
-    for (const [key, model] of Object.entries(result)) {
-      const m = remote.get(model.api.id)
-      if (!m) {
-        delete result[key]
-        continue
-      }
-      result[key] = build(key, m, baseURL, model)
+export async function get(
+  baseURL: string,
+  headers: HeadersInit = {},
+  existing: Record<string, Model> = {},
+): Promise<Record<string, Model>> {
+  const data = await fetch(`${baseURL}/models`, {
+    headers,
+    signal: AbortSignal.timeout(5_000),
+  }).then(async (res) => {
+    if (!res.ok) {
+      throw new Error(`Failed to fetch models: ${res.status}`)
     }
+    return schema.parse(await res.json())
+  })
 
-    // add new endpoint models not already keyed in result
-    for (const [id, m] of remote) {
-      if (id in result) continue
-      result[id] = build(id, m, baseURL)
+  const result = { ...existing }
+  const remote = new Map(data.data.filter((m) => m.model_picker_enabled).map((m) => [m.id, m] as const))
+
+  // prune existing models whose api.id isn't in the endpoint response
+  for (const [key, model] of Object.entries(result)) {
+    const m = remote.get(model.api.id)
+    if (!m) {
+      delete result[key]
+      continue
     }
+    result[key] = build(key, m, baseURL, model)
+  }
 
-    return result
+  // add new endpoint models not already keyed in result
+  for (const [id, m] of remote) {
+    if (id in result) continue
+    result[id] = build(id, m, baseURL)
   }
+
+  return result
 }

+ 2 - 0
packages/opencode/src/plugin/index.ts

@@ -1 +1,3 @@
 export * as Plugin from "./plugin"
+export * as PluginMeta from "./meta"
+export * as PluginLoader from "./loader"

+ 137 - 139
packages/opencode/src/plugin/loader.ts

@@ -11,164 +11,162 @@ import {
 import { ConfigPlugin } from "@/config/plugin"
 import { InstallationVersion } from "@/installation/version"
 
-export namespace PluginLoader {
-  export type Plan = {
-    spec: string
-    options: ConfigPlugin.Options | undefined
-    deprecated: boolean
-  }
-  export type Resolved = Plan & {
-    source: PluginSource
-    target: string
-    entry: string
-    pkg?: PluginPackage
-  }
-  export type Missing = Plan & {
-    source: PluginSource
-    target: string
-    pkg?: PluginPackage
-    message: string
-  }
-  export type Loaded = Resolved & {
-    mod: Record<string, unknown>
-  }
+export type Plan = {
+  spec: string
+  options: ConfigPlugin.Options | undefined
+  deprecated: boolean
+}
+export type Resolved = Plan & {
+  source: PluginSource
+  target: string
+  entry: string
+  pkg?: PluginPackage
+}
+export type Missing = Plan & {
+  source: PluginSource
+  target: string
+  pkg?: PluginPackage
+  message: string
+}
+export type Loaded = Resolved & {
+  mod: Record<string, unknown>
+}
 
-  type Candidate = { origin: ConfigPlugin.Origin; plan: Plan }
-  type Report = {
-    start?: (candidate: Candidate, retry: boolean) => void
-    missing?: (candidate: Candidate, retry: boolean, message: string, resolved: Missing) => void
-    error?: (
-      candidate: Candidate,
-      retry: boolean,
-      stage: "install" | "entry" | "compatibility" | "load",
-      error: unknown,
-      resolved?: Resolved,
-    ) => void
-  }
+type Candidate = { origin: ConfigPlugin.Origin; plan: Plan }
+type Report = {
+  start?: (candidate: Candidate, retry: boolean) => void
+  missing?: (candidate: Candidate, retry: boolean, message: string, resolved: Missing) => void
+  error?: (
+    candidate: Candidate,
+    retry: boolean,
+    stage: "install" | "entry" | "compatibility" | "load",
+    error: unknown,
+    resolved?: Resolved,
+  ) => void
+}
 
-  function plan(item: ConfigPlugin.Spec): Plan {
-    const spec = ConfigPlugin.pluginSpecifier(item)
-    return { spec, options: ConfigPlugin.pluginOptions(item), deprecated: isDeprecatedPlugin(spec) }
+function plan(item: ConfigPlugin.Spec): Plan {
+  const spec = ConfigPlugin.pluginSpecifier(item)
+  return { spec, options: ConfigPlugin.pluginOptions(item), deprecated: isDeprecatedPlugin(spec) }
+}
+
+export async function resolve(
+  plan: Plan,
+  kind: PluginKind,
+): Promise<
+  | { ok: true; value: Resolved }
+  | { ok: false; stage: "missing"; value: Missing }
+  | { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown }
+> {
+  let target = ""
+  try {
+    target = await resolvePluginTarget(plan.spec)
+  } catch (error) {
+    return { ok: false, stage: "install", error }
   }
+  if (!target) return { ok: false, stage: "install", error: new Error(`Plugin ${plan.spec} target is empty`) }
 
-  export async function resolve(
-    plan: Plan,
-    kind: PluginKind,
-  ): Promise<
-    | { ok: true; value: Resolved }
-    | { ok: false; stage: "missing"; value: Missing }
-    | { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown }
-  > {
-    let target = ""
-    try {
-      target = await resolvePluginTarget(plan.spec)
-    } catch (error) {
-      return { ok: false, stage: "install", error }
+  let base
+  try {
+    base = await createPluginEntry(plan.spec, target, kind)
+  } catch (error) {
+    return { ok: false, stage: "entry", error }
+  }
+  if (!base.entry)
+    return {
+      ok: false,
+      stage: "missing",
+      value: {
+        ...plan,
+        source: base.source,
+        target: base.target,
+        pkg: base.pkg,
+        message: `Plugin ${plan.spec} does not expose a ${kind} entrypoint`,
+      },
     }
-    if (!target) return { ok: false, stage: "install", error: new Error(`Plugin ${plan.spec} target is empty`) }
 
-    let base
+  if (base.source === "npm") {
     try {
-      base = await createPluginEntry(plan.spec, target, kind)
+      await checkPluginCompatibility(base.target, InstallationVersion, base.pkg)
     } catch (error) {
-      return { ok: false, stage: "entry", error }
+      return { ok: false, stage: "compatibility", error }
     }
-    if (!base.entry)
-      return {
-        ok: false,
-        stage: "missing",
-        value: {
-          ...plan,
-          source: base.source,
-          target: base.target,
-          pkg: base.pkg,
-          message: `Plugin ${plan.spec} does not expose a ${kind} entrypoint`,
-        },
-      }
-
-    if (base.source === "npm") {
-      try {
-        await checkPluginCompatibility(base.target, InstallationVersion, base.pkg)
-      } catch (error) {
-        return { ok: false, stage: "compatibility", error }
-      }
-    }
-    return { ok: true, value: { ...plan, source: base.source, target: base.target, entry: base.entry, pkg: base.pkg } }
   }
+  return { ok: true, value: { ...plan, source: base.source, target: base.target, entry: base.entry, pkg: base.pkg } }
+}
 
-  export async function load(row: Resolved): Promise<{ ok: true; value: Loaded } | { ok: false; error: unknown }> {
-    let mod
-    try {
-      mod = await import(row.entry)
-    } catch (error) {
-      return { ok: false, error }
-    }
-    if (!mod) return { ok: false, error: new Error(`Plugin ${row.spec} module is empty`) }
-    return { ok: true, value: { ...row, mod } }
+export async function load(row: Resolved): Promise<{ ok: true; value: Loaded } | { ok: false; error: unknown }> {
+  let mod
+  try {
+    mod = await import(row.entry)
+  } catch (error) {
+    return { ok: false, error }
   }
+  if (!mod) return { ok: false, error: new Error(`Plugin ${row.spec} module is empty`) }
+  return { ok: true, value: { ...row, mod } }
+}
 
-  async function attempt<R>(
-    candidate: Candidate,
-    kind: PluginKind,
-    retry: boolean,
-    finish: ((load: Loaded, origin: ConfigPlugin.Origin, retry: boolean) => Promise<R | undefined>) | undefined,
-    missing: ((value: Missing, origin: ConfigPlugin.Origin, retry: boolean) => Promise<R | undefined>) | undefined,
-    report: Report | undefined,
-  ): Promise<R | undefined> {
-    const plan = candidate.plan
-    if (plan.deprecated) return
-    report?.start?.(candidate, retry)
-    const resolved = await resolve(plan, kind)
-    if (!resolved.ok) {
-      if (resolved.stage === "missing") {
-        if (missing) {
-          const value = await missing(resolved.value, candidate.origin, retry)
-          if (value !== undefined) return value
-        }
-        report?.missing?.(candidate, retry, resolved.value.message, resolved.value)
-        return
+async function attempt<R>(
+  candidate: Candidate,
+  kind: PluginKind,
+  retry: boolean,
+  finish: ((load: Loaded, origin: ConfigPlugin.Origin, retry: boolean) => Promise<R | undefined>) | undefined,
+  missing: ((value: Missing, origin: ConfigPlugin.Origin, retry: boolean) => Promise<R | undefined>) | undefined,
+  report: Report | undefined,
+): Promise<R | undefined> {
+  const plan = candidate.plan
+  if (plan.deprecated) return
+  report?.start?.(candidate, retry)
+  const resolved = await resolve(plan, kind)
+  if (!resolved.ok) {
+    if (resolved.stage === "missing") {
+      if (missing) {
+        const value = await missing(resolved.value, candidate.origin, retry)
+        if (value !== undefined) return value
       }
-      report?.error?.(candidate, retry, resolved.stage, resolved.error)
+      report?.missing?.(candidate, retry, resolved.value.message, resolved.value)
       return
     }
-    const loaded = await load(resolved.value)
-    if (!loaded.ok) {
-      report?.error?.(candidate, retry, "load", loaded.error, resolved.value)
-      return
-    }
-    if (!finish) return loaded.value as R
-    return finish(loaded.value, candidate.origin, retry)
+    report?.error?.(candidate, retry, resolved.stage, resolved.error)
+    return
   }
-
-  type Input<R> = {
-    items: ConfigPlugin.Origin[]
-    kind: PluginKind
-    wait?: () => Promise<void>
-    finish?: (load: Loaded, origin: ConfigPlugin.Origin, retry: boolean) => Promise<R | undefined>
-    missing?: (value: Missing, origin: ConfigPlugin.Origin, retry: boolean) => Promise<R | undefined>
-    report?: Report
+  const loaded = await load(resolved.value)
+  if (!loaded.ok) {
+    report?.error?.(candidate, retry, "load", loaded.error, resolved.value)
+    return
   }
+  if (!finish) return loaded.value as R
+  return finish(loaded.value, candidate.origin, retry)
+}
 
-  export async function loadExternal<R = Loaded>(input: Input<R>): Promise<R[]> {
-    const candidates = input.items.map((origin) => ({ origin, plan: plan(origin.spec) }))
-    const list: Array<Promise<R | undefined>> = []
-    for (const candidate of candidates) {
-      list.push(attempt(candidate, input.kind, false, input.finish, input.missing, input.report))
-    }
-    const out = await Promise.all(list)
-    if (input.wait) {
-      let deps: Promise<void> | undefined
-      for (let i = 0; i < candidates.length; i++) {
-        if (out[i] !== undefined) continue
-        const candidate = candidates[i]
-        if (!candidate || pluginSource(candidate.plan.spec) !== "file") continue
-        deps ??= input.wait()
-        await deps
-        out[i] = await attempt(candidate, input.kind, true, input.finish, input.missing, input.report)
-      }
+type Input<R> = {
+  items: ConfigPlugin.Origin[]
+  kind: PluginKind
+  wait?: () => Promise<void>
+  finish?: (load: Loaded, origin: ConfigPlugin.Origin, retry: boolean) => Promise<R | undefined>
+  missing?: (value: Missing, origin: ConfigPlugin.Origin, retry: boolean) => Promise<R | undefined>
+  report?: Report
+}
+
+export async function loadExternal<R = Loaded>(input: Input<R>): Promise<R[]> {
+  const candidates = input.items.map((origin) => ({ origin, plan: plan(origin.spec) }))
+  const list: Array<Promise<R | undefined>> = []
+  for (const candidate of candidates) {
+    list.push(attempt(candidate, input.kind, false, input.finish, input.missing, input.report))
+  }
+  const out = await Promise.all(list)
+  if (input.wait) {
+    let deps: Promise<void> | undefined
+    for (let i = 0; i < candidates.length; i++) {
+      if (out[i] !== undefined) continue
+      const candidate = candidates[i]
+      if (!candidate || pluginSource(candidate.plan.spec) !== "file") continue
+      deps ??= input.wait()
+      await deps
+      out[i] = await attempt(candidate, input.kind, true, input.finish, input.missing, input.report)
     }
-    const ready: R[] = []
-    for (const item of out) if (item !== undefined) ready.push(item)
-    return ready
   }
+  const ready: R[] = []
+  for (const item of out) if (item !== undefined) ready.push(item)
+  return ready
 }

+ 145 - 147
packages/opencode/src/plugin/meta.ts

@@ -8,181 +8,179 @@ import { Flock } from "@opencode-ai/shared/util/flock"
 
 import { parsePluginSpecifier, pluginSource } from "./shared"
 
-export namespace PluginMeta {
-  type Source = "file" | "npm"
-
-  export type Theme = {
-    src: string
-    dest: string
-    mtime?: number
-    size?: number
-  }
+type Source = "file" | "npm"
 
-  export type Entry = {
-    id: string
-    source: Source
-    spec: string
-    target: string
-    requested?: string
-    version?: string
-    modified?: number
-    first_time: number
-    last_time: number
-    time_changed: number
-    load_count: number
-    fingerprint: string
-    themes?: Record<string, Theme>
-  }
+export type Theme = {
+  src: string
+  dest: string
+  mtime?: number
+  size?: number
+}
 
-  export type State = "first" | "updated" | "same"
+export type Entry = {
+  id: string
+  source: Source
+  spec: string
+  target: string
+  requested?: string
+  version?: string
+  modified?: number
+  first_time: number
+  last_time: number
+  time_changed: number
+  load_count: number
+  fingerprint: string
+  themes?: Record<string, Theme>
+}
 
-  export type Touch = {
-    spec: string
-    target: string
-    id: string
-  }
+export type State = "first" | "updated" | "same"
 
-  type Store = Record<string, Entry>
-  type Core = Omit<Entry, "first_time" | "last_time" | "time_changed" | "load_count" | "fingerprint" | "themes">
-  type Row = Touch & { core: Core }
+export type Touch = {
+  spec: string
+  target: string
+  id: string
+}
 
-  function storePath() {
-    return Flag.OPENCODE_PLUGIN_META_FILE ?? path.join(Global.Path.state, "plugin-meta.json")
-  }
+type Store = Record<string, Entry>
+type Core = Omit<Entry, "first_time" | "last_time" | "time_changed" | "load_count" | "fingerprint" | "themes">
+type Row = Touch & { core: Core }
 
-  function lock(file: string) {
-    return `plugin-meta:${file}`
-  }
+function storePath() {
+  return Flag.OPENCODE_PLUGIN_META_FILE ?? path.join(Global.Path.state, "plugin-meta.json")
+}
 
-  function fileTarget(spec: string, target: string) {
-    if (spec.startsWith("file://")) return fileURLToPath(spec)
-    if (target.startsWith("file://")) return fileURLToPath(target)
-    return
-  }
+function lock(file: string) {
+  return `plugin-meta:${file}`
+}
 
-  async function modifiedAt(file: string) {
-    const stat = await Filesystem.statAsync(file)
-    if (!stat) return
-    const mtime = stat.mtimeMs
-    return Math.floor(typeof mtime === "bigint" ? Number(mtime) : mtime)
-  }
+function fileTarget(spec: string, target: string) {
+  if (spec.startsWith("file://")) return fileURLToPath(spec)
+  if (target.startsWith("file://")) return fileURLToPath(target)
+  return
+}
 
-  function resolvedTarget(target: string) {
-    if (target.startsWith("file://")) return fileURLToPath(target)
-    return target
-  }
+async function modifiedAt(file: string) {
+  const stat = await Filesystem.statAsync(file)
+  if (!stat) return
+  const mtime = stat.mtimeMs
+  return Math.floor(typeof mtime === "bigint" ? Number(mtime) : mtime)
+}
 
-  async function npmVersion(target: string) {
-    const resolved = resolvedTarget(target)
-    const stat = await Filesystem.statAsync(resolved)
-    const dir = stat?.isDirectory() ? resolved : path.dirname(resolved)
-    return Filesystem.readJson<{ version?: string }>(path.join(dir, "package.json"))
-      .then((item) => item.version)
-      .catch(() => undefined)
-  }
+function resolvedTarget(target: string) {
+  if (target.startsWith("file://")) return fileURLToPath(target)
+  return target
+}
 
-  async function entryCore(item: Touch): Promise<Core> {
-    const spec = item.spec
-    const target = item.target
-    const source = pluginSource(spec)
-    if (source === "file") {
-      const file = fileTarget(spec, target)
-      return {
-        id: item.id,
-        source,
-        spec,
-        target,
-        modified: file ? await modifiedAt(file) : undefined,
-      }
-    }
+async function npmVersion(target: string) {
+  const resolved = resolvedTarget(target)
+  const stat = await Filesystem.statAsync(resolved)
+  const dir = stat?.isDirectory() ? resolved : path.dirname(resolved)
+  return Filesystem.readJson<{ version?: string }>(path.join(dir, "package.json"))
+    .then((item) => item.version)
+    .catch(() => undefined)
+}
 
+async function entryCore(item: Touch): Promise<Core> {
+  const spec = item.spec
+  const target = item.target
+  const source = pluginSource(spec)
+  if (source === "file") {
+    const file = fileTarget(spec, target)
     return {
       id: item.id,
       source,
       spec,
       target,
-      requested: parsePluginSpecifier(spec).version,
-      version: await npmVersion(target),
+      modified: file ? await modifiedAt(file) : undefined,
     }
   }
 
-  function fingerprint(value: Core) {
-    if (value.source === "file") return [value.target, value.modified ?? ""].join("|")
-    return [value.target, value.requested ?? "", value.version ?? ""].join("|")
+  return {
+    id: item.id,
+    source,
+    spec,
+    target,
+    requested: parsePluginSpecifier(spec).version,
+    version: await npmVersion(target),
   }
+}
 
-  async function read(file: string): Promise<Store> {
-    return Filesystem.readJson<Store>(file).catch(() => ({}) as Store)
-  }
+function fingerprint(value: Core) {
+  if (value.source === "file") return [value.target, value.modified ?? ""].join("|")
+  return [value.target, value.requested ?? "", value.version ?? ""].join("|")
+}
 
-  async function row(item: Touch): Promise<Row> {
-    return {
-      ...item,
-      core: await entryCore(item),
-    }
-  }
+async function read(file: string): Promise<Store> {
+  return Filesystem.readJson<Store>(file).catch(() => ({}) as Store)
+}
 
-  function next(prev: Entry | undefined, core: Core, now: number): { state: State; entry: Entry } {
-    const entry: Entry = {
-      ...core,
-      first_time: prev?.first_time ?? now,
-      last_time: now,
-      time_changed: prev?.time_changed ?? now,
-      load_count: (prev?.load_count ?? 0) + 1,
-      fingerprint: fingerprint(core),
-      themes: prev?.themes,
-    }
-    const state: State = !prev ? "first" : prev.fingerprint === entry.fingerprint ? "same" : "updated"
-    if (state === "updated") entry.time_changed = now
-    return {
-      state,
-      entry,
-    }
+async function row(item: Touch): Promise<Row> {
+  return {
+    ...item,
+    core: await entryCore(item),
   }
+}
 
-  export async function touchMany(items: Touch[]): Promise<Array<{ state: State; entry: Entry }>> {
-    if (!items.length) return []
-    const file = storePath()
-    const rows = await Promise.all(items.map((item) => row(item)))
-
-    return Flock.withLock(lock(file), async () => {
-      const store = await read(file)
-      const now = Date.now()
-      const out: Array<{ state: State; entry: Entry }> = []
-      for (const item of rows) {
-        const hit = next(store[item.id], item.core, now)
-        store[item.id] = hit.entry
-        out.push(hit)
-      }
-      await Filesystem.writeJson(file, store)
-      return out
-    })
+function next(prev: Entry | undefined, core: Core, now: number): { state: State; entry: Entry } {
+  const entry: Entry = {
+    ...core,
+    first_time: prev?.first_time ?? now,
+    last_time: now,
+    time_changed: prev?.time_changed ?? now,
+    load_count: (prev?.load_count ?? 0) + 1,
+    fingerprint: fingerprint(core),
+    themes: prev?.themes,
+  }
+  const state: State = !prev ? "first" : prev.fingerprint === entry.fingerprint ? "same" : "updated"
+  if (state === "updated") entry.time_changed = now
+  return {
+    state,
+    entry,
   }
+}
 
-  export async function touch(spec: string, target: string, id: string): Promise<{ state: State; entry: Entry }> {
-    return touchMany([{ spec, target, id }]).then((item) => {
-      const hit = item[0]
-      if (hit) return hit
-      throw new Error("Failed to touch plugin metadata.")
-    })
-  }
+export async function touchMany(items: Touch[]): Promise<Array<{ state: State; entry: Entry }>> {
+  if (!items.length) return []
+  const file = storePath()
+  const rows = await Promise.all(items.map((item) => row(item)))
+
+  return Flock.withLock(lock(file), async () => {
+    const store = await read(file)
+    const now = Date.now()
+    const out: Array<{ state: State; entry: Entry }> = []
+    for (const item of rows) {
+      const hit = next(store[item.id], item.core, now)
+      store[item.id] = hit.entry
+      out.push(hit)
+    }
+    await Filesystem.writeJson(file, store)
+    return out
+  })
+}
 
-  export async function setTheme(id: string, name: string, theme: Theme): Promise<void> {
-    const file = storePath()
-    await Flock.withLock(lock(file), async () => {
-      const store = await read(file)
-      const entry = store[id]
-      if (!entry) return
-      entry.themes = {
-        ...entry.themes,
-        [name]: theme,
-      }
-      await Filesystem.writeJson(file, store)
-    })
-  }
+export async function touch(spec: string, target: string, id: string): Promise<{ state: State; entry: Entry }> {
+  return touchMany([{ spec, target, id }]).then((item) => {
+    const hit = item[0]
+    if (hit) return hit
+    throw new Error("Failed to touch plugin metadata.")
+  })
+}
 
-  export async function list(): Promise<Store> {
-    const file = storePath()
-    return Flock.withLock(lock(file), async () => read(file))
-  }
+export async function setTheme(id: string, name: string, theme: Theme): Promise<void> {
+  const file = storePath()
+  await Flock.withLock(lock(file), async () => {
+    const store = await read(file)
+    const entry = store[id]
+    if (!entry) return
+    entry.themes = {
+      ...entry.themes,
+      [name]: theme,
+    }
+    await Filesystem.writeJson(file, store)
+  })
+}
+
+export async function list(): Promise<Store> {
+  const file = storePath()
+  return Flock.withLock(lock(file), async () => read(file))
 }

+ 1 - 1
packages/opencode/src/plugin/plugin.ts

@@ -21,7 +21,7 @@ import { Effect, Layer, Context, Stream } from "effect"
 import { EffectBridge } from "@/effect"
 import { InstanceState } from "@/effect"
 import { errorMessage } from "@/util/error"
-import { PluginLoader } from "./loader"
+import * as PluginLoader from "./loader"
 import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared"
 import { registerAdaptor } from "@/control-plane/adaptors"
 import type { WorkspaceAdaptor } from "@/control-plane/types"

+ 1 - 1
packages/opencode/test/fixture/plugin-meta-worker.ts

@@ -14,6 +14,6 @@ if (typeof msg.id !== "string") throw new Error("Invalid worker payload")
 
 process.env.OPENCODE_PLUGIN_META_FILE = msg.file
 
-const { PluginMeta } = await import("../../src/plugin/meta")
+const PluginMeta = await import("../../src/plugin/meta")
 
 await PluginMeta.touch(msg.spec, msg.target, msg.id)

+ 1 - 1
packages/opencode/test/plugin/github-copilot-models.test.ts

@@ -1,5 +1,5 @@
 import { afterEach, expect, mock, test } from "bun:test"
-import { CopilotModels } from "@/plugin/github-copilot/models"
+import { CopilotModels } from "@/plugin/github-copilot"
 import { CopilotAuthPlugin } from "@/plugin/github-copilot/copilot"
 
 const originalFetch = globalThis.fetch

+ 1 - 1
packages/opencode/test/plugin/loader-shared.test.ts

@@ -10,7 +10,7 @@ const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS
 process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1"
 
 const { Plugin } = await import("../../src/plugin/index")
-const { PluginLoader } = await import("../../src/plugin/loader")
+const PluginLoader = await import("../../src/plugin/loader")
 const { readPackageThemes } = await import("../../src/plugin/shared")
 const { Instance } = await import("../../src/project/instance")
 const { Npm } = await import("../../src/npm")

+ 1 - 1
packages/opencode/test/plugin/meta.test.ts

@@ -7,7 +7,7 @@ import { tmpdir } from "../fixture/fixture"
 import { Process } from "../../src/util"
 import { Filesystem } from "../../src/util"
 
-const { PluginMeta } = await import("../../src/plugin/meta")
+const PluginMeta = await import("../../src/plugin/meta")
 const root = path.join(import.meta.dir, "../..")
 const worker = path.join(import.meta.dir, "../fixture/plugin-meta-worker.ts")