Compare commits
2 commits
dev
...
kit/unwrap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50a52bf0f1 | ||
|
|
c74ea2166f |
10 changed files with 416 additions and 419 deletions
|
|
@ -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" })
|
||||
|
|
|
|||
0
packages/opencode/src/plugin/github-copilot/index.ts
Normal file
0
packages/opencode/src/plugin/github-copilot/index.ts
Normal file
|
|
@ -1,8 +1,7 @@
|
|||
import { z } from "zod"
|
||||
import type { Model } from "@opencode-ai/sdk/v2"
|
||||
|
||||
export namespace CopilotModels {
|
||||
export const schema = z.object({
|
||||
export const schema = z.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
model_picker_enabled: z.boolean(),
|
||||
|
|
@ -38,11 +37,11 @@ export namespace CopilotModels {
|
|||
}),
|
||||
}),
|
||||
),
|
||||
})
|
||||
})
|
||||
|
||||
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 {
|
||||
function build(key: string, remote: Item, url: string, prev?: Model): Model {
|
||||
const reasoning =
|
||||
!!remote.capabilities.supports.adaptive_thinking ||
|
||||
!!remote.capabilities.supports.reasoning_effort?.length ||
|
||||
|
|
@ -105,13 +104,13 @@ export namespace CopilotModels {
|
|||
(remote.version.startsWith(`${remote.id}-`) ? remote.version.slice(remote.id.length + 1) : remote.version),
|
||||
variants: prev?.variants ?? {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function get(
|
||||
export async function get(
|
||||
baseURL: string,
|
||||
headers: HeadersInit = {},
|
||||
existing: Record<string, Model> = {},
|
||||
): Promise<Record<string, Model>> {
|
||||
): Promise<Record<string, Model>> {
|
||||
const data = await fetch(`${baseURL}/models`, {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(5_000),
|
||||
|
|
@ -142,5 +141,5 @@ export namespace CopilotModels {
|
|||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
export * as CopilotModels from "./models"
|
||||
|
|
|
|||
|
|
@ -11,30 +11,29 @@ import {
|
|||
import { ConfigPlugin } from "@/config/plugin"
|
||||
import { InstallationVersion } from "@/installation/version"
|
||||
|
||||
export namespace PluginLoader {
|
||||
export type Plan = {
|
||||
export type Plan = {
|
||||
spec: string
|
||||
options: ConfigPlugin.Options | undefined
|
||||
deprecated: boolean
|
||||
}
|
||||
export type Resolved = Plan & {
|
||||
}
|
||||
export type Resolved = Plan & {
|
||||
source: PluginSource
|
||||
target: string
|
||||
entry: string
|
||||
pkg?: PluginPackage
|
||||
}
|
||||
export type Missing = Plan & {
|
||||
}
|
||||
export type Missing = Plan & {
|
||||
source: PluginSource
|
||||
target: string
|
||||
pkg?: PluginPackage
|
||||
message: string
|
||||
}
|
||||
export type Loaded = Resolved & {
|
||||
}
|
||||
export type Loaded = Resolved & {
|
||||
mod: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
type Candidate = { origin: ConfigPlugin.Origin; plan: Plan }
|
||||
type Report = {
|
||||
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?: (
|
||||
|
|
@ -44,21 +43,21 @@ export namespace PluginLoader {
|
|||
error: unknown,
|
||||
resolved?: Resolved,
|
||||
) => void
|
||||
}
|
||||
}
|
||||
|
||||
function plan(item: ConfigPlugin.Spec): Plan {
|
||||
function plan(item: ConfigPlugin.Spec): Plan {
|
||||
const spec = ConfigPlugin.pluginSpecifier(item)
|
||||
return { spec, options: ConfigPlugin.pluginOptions(item), deprecated: isDeprecatedPlugin(spec) }
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolve(
|
||||
export async function resolve(
|
||||
plan: Plan,
|
||||
kind: PluginKind,
|
||||
): Promise<
|
||||
): 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)
|
||||
|
|
@ -94,9 +93,9 @@ export namespace PluginLoader {
|
|||
}
|
||||
}
|
||||
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 }> {
|
||||
export async function load(row: Resolved): Promise<{ ok: true; value: Loaded } | { ok: false; error: unknown }> {
|
||||
let mod
|
||||
try {
|
||||
mod = await import(row.entry)
|
||||
|
|
@ -105,16 +104,16 @@ export namespace PluginLoader {
|
|||
}
|
||||
if (!mod) return { ok: false, error: new Error(`Plugin ${row.spec} module is empty`) }
|
||||
return { ok: true, value: { ...row, mod } }
|
||||
}
|
||||
}
|
||||
|
||||
async function attempt<R>(
|
||||
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> {
|
||||
): Promise<R | undefined> {
|
||||
const plan = candidate.plan
|
||||
if (plan.deprecated) return
|
||||
report?.start?.(candidate, retry)
|
||||
|
|
@ -138,18 +137,18 @@ export namespace PluginLoader {
|
|||
}
|
||||
if (!finish) return loaded.value as R
|
||||
return finish(loaded.value, candidate.origin, retry)
|
||||
}
|
||||
}
|
||||
|
||||
type Input<R> = {
|
||||
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[]> {
|
||||
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) {
|
||||
|
|
@ -170,5 +169,5 @@ export namespace PluginLoader {
|
|||
const ready: R[] = []
|
||||
for (const item of out) if (item !== undefined) ready.push(item)
|
||||
return ready
|
||||
}
|
||||
}
|
||||
export * as PluginLoader from "./loader"
|
||||
|
|
|
|||
|
|
@ -8,17 +8,16 @@ import { Flock } from "@opencode-ai/shared/util/flock"
|
|||
|
||||
import { parsePluginSpecifier, pluginSource } from "./shared"
|
||||
|
||||
export namespace PluginMeta {
|
||||
type Source = "file" | "npm"
|
||||
type Source = "file" | "npm"
|
||||
|
||||
export type Theme = {
|
||||
export type Theme = {
|
||||
src: string
|
||||
dest: string
|
||||
mtime?: number
|
||||
size?: number
|
||||
}
|
||||
}
|
||||
|
||||
export type Entry = {
|
||||
export type Entry = {
|
||||
id: string
|
||||
source: Source
|
||||
spec: string
|
||||
|
|
@ -32,56 +31,56 @@ export namespace PluginMeta {
|
|||
load_count: number
|
||||
fingerprint: string
|
||||
themes?: Record<string, Theme>
|
||||
}
|
||||
}
|
||||
|
||||
export type State = "first" | "updated" | "same"
|
||||
export type State = "first" | "updated" | "same"
|
||||
|
||||
export type Touch = {
|
||||
export type Touch = {
|
||||
spec: string
|
||||
target: string
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
type Store = Record<string, Entry>
|
||||
type Core = Omit<Entry, "first_time" | "last_time" | "time_changed" | "load_count" | "fingerprint" | "themes">
|
||||
type Row = Touch & { core: Core }
|
||||
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 storePath() {
|
||||
function storePath() {
|
||||
return Flag.OPENCODE_PLUGIN_META_FILE ?? path.join(Global.Path.state, "plugin-meta.json")
|
||||
}
|
||||
}
|
||||
|
||||
function lock(file: string) {
|
||||
function lock(file: string) {
|
||||
return `plugin-meta:${file}`
|
||||
}
|
||||
}
|
||||
|
||||
function fileTarget(spec: string, target: string) {
|
||||
function fileTarget(spec: string, target: string) {
|
||||
if (spec.startsWith("file://")) return fileURLToPath(spec)
|
||||
if (target.startsWith("file://")) return fileURLToPath(target)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
async function modifiedAt(file: string) {
|
||||
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 resolvedTarget(target: string) {
|
||||
function resolvedTarget(target: string) {
|
||||
if (target.startsWith("file://")) return fileURLToPath(target)
|
||||
return target
|
||||
}
|
||||
}
|
||||
|
||||
async function npmVersion(target: string) {
|
||||
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> {
|
||||
async function entryCore(item: Touch): Promise<Core> {
|
||||
const spec = item.spec
|
||||
const target = item.target
|
||||
const source = pluginSource(spec)
|
||||
|
|
@ -104,25 +103,25 @@ export namespace PluginMeta {
|
|||
requested: parsePluginSpecifier(spec).version,
|
||||
version: await npmVersion(target),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fingerprint(value: Core) {
|
||||
function fingerprint(value: Core) {
|
||||
if (value.source === "file") return [value.target, value.modified ?? ""].join("|")
|
||||
return [value.target, value.requested ?? "", value.version ?? ""].join("|")
|
||||
}
|
||||
}
|
||||
|
||||
async function read(file: string): Promise<Store> {
|
||||
async function read(file: string): Promise<Store> {
|
||||
return Filesystem.readJson<Store>(file).catch(() => ({}) as Store)
|
||||
}
|
||||
}
|
||||
|
||||
async function row(item: Touch): Promise<Row> {
|
||||
async function row(item: Touch): Promise<Row> {
|
||||
return {
|
||||
...item,
|
||||
core: await entryCore(item),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function next(prev: Entry | undefined, core: Core, now: number): { state: State; entry: Entry } {
|
||||
function next(prev: Entry | undefined, core: Core, now: number): { state: State; entry: Entry } {
|
||||
const entry: Entry = {
|
||||
...core,
|
||||
first_time: prev?.first_time ?? now,
|
||||
|
|
@ -138,9 +137,9 @@ export namespace PluginMeta {
|
|||
state,
|
||||
entry,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function touchMany(items: Touch[]): Promise<Array<{ state: State; entry: Entry }>> {
|
||||
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)))
|
||||
|
|
@ -157,17 +156,17 @@ export namespace PluginMeta {
|
|||
await Filesystem.writeJson(file, store)
|
||||
return out
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function touch(spec: string, target: string, id: string): Promise<{ state: State; entry: 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 setTheme(id: string, name: string, theme: Theme): Promise<void> {
|
||||
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)
|
||||
|
|
@ -179,10 +178,10 @@ export namespace PluginMeta {
|
|||
}
|
||||
await Filesystem.writeJson(file, store)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function list(): Promise<Store> {
|
||||
export async function list(): Promise<Store> {
|
||||
const file = storePath()
|
||||
return Flock.withLock(lock(file), async () => read(file))
|
||||
}
|
||||
}
|
||||
export * as PluginMeta from "./meta"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,5 +1,5 @@
|
|||
import { afterEach, expect, mock, test } from "bun:test"
|
||||
import { CopilotModels } from "@/plugin/github-copilot/models"
|
||||
import { CopilotModels } from "../../src/plugin/github-copilot/models"
|
||||
import { CopilotAuthPlugin } from "@/plugin/github-copilot/copilot"
|
||||
|
||||
const originalFetch = globalThis.fetch
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue