refactor: unwrap PluginMeta namespace + self-reexport (#22945)

This commit is contained in:
Kit Langton
2026-04-16 20:02:05 -04:00
committed by GitHub
parent fb02744460
commit 974fa1b8b1

View File

@@ -8,181 +8,181 @@ import { Flock } from "@opencode-ai/shared/util/flock"
import { parsePluginSpecifier, pluginSource } from "./shared" import { parsePluginSpecifier, pluginSource } from "./shared"
export namespace PluginMeta { type Source = "file" | "npm"
type Source = "file" | "npm"
export type Theme = { export type Theme = {
src: string src: string
dest: string dest: string
mtime?: number mtime?: number
size?: number size?: number
} }
export type Entry = { export type Entry = {
id: string id: string
source: Source source: Source
spec: string spec: string
target: string target: string
requested?: string requested?: string
version?: string version?: string
modified?: number modified?: number
first_time: number first_time: number
last_time: number last_time: number
time_changed: number time_changed: number
load_count: number load_count: number
fingerprint: string fingerprint: string
themes?: Record<string, Theme> themes?: Record<string, Theme>
} }
export type State = "first" | "updated" | "same" export type State = "first" | "updated" | "same"
export type Touch = { export type Touch = {
spec: string spec: string
target: string target: string
id: string id: string
} }
type Store = Record<string, Entry> type Store = Record<string, Entry>
type Core = Omit<Entry, "first_time" | "last_time" | "time_changed" | "load_count" | "fingerprint" | "themes"> type Core = Omit<Entry, "first_time" | "last_time" | "time_changed" | "load_count" | "fingerprint" | "themes">
type Row = Touch & { core: Core } type Row = Touch & { core: Core }
function storePath() { function storePath() {
return Flag.OPENCODE_PLUGIN_META_FILE ?? path.join(Global.Path.state, "plugin-meta.json") 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}` return `plugin-meta:${file}`
} }
function fileTarget(spec: string, target: string) { function fileTarget(spec: string, target: string) {
if (spec.startsWith("file://")) return fileURLToPath(spec) if (spec.startsWith("file://")) return fileURLToPath(spec)
if (target.startsWith("file://")) return fileURLToPath(target) if (target.startsWith("file://")) return fileURLToPath(target)
return return
} }
async function modifiedAt(file: string) { async function modifiedAt(file: string) {
const stat = await Filesystem.statAsync(file) const stat = await Filesystem.statAsync(file)
if (!stat) return if (!stat) return
const mtime = stat.mtimeMs const mtime = stat.mtimeMs
return Math.floor(typeof mtime === "bigint" ? Number(mtime) : mtime) return Math.floor(typeof mtime === "bigint" ? Number(mtime) : mtime)
} }
function resolvedTarget(target: string) { function resolvedTarget(target: string) {
if (target.startsWith("file://")) return fileURLToPath(target) if (target.startsWith("file://")) return fileURLToPath(target)
return target return target
} }
async function npmVersion(target: string) { async function npmVersion(target: string) {
const resolved = resolvedTarget(target) const resolved = resolvedTarget(target)
const stat = await Filesystem.statAsync(resolved) const stat = await Filesystem.statAsync(resolved)
const dir = stat?.isDirectory() ? resolved : path.dirname(resolved) const dir = stat?.isDirectory() ? resolved : path.dirname(resolved)
return Filesystem.readJson<{ version?: string }>(path.join(dir, "package.json")) return Filesystem.readJson<{ version?: string }>(path.join(dir, "package.json"))
.then((item) => item.version) .then((item) => item.version)
.catch(() => undefined) .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,
modified: file ? await modifiedAt(file) : 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 { return {
id: item.id, id: item.id,
source, source,
spec, spec,
target, target,
requested: parsePluginSpecifier(spec).version, modified: file ? await modifiedAt(file) : undefined,
version: await npmVersion(target),
} }
} }
function fingerprint(value: Core) { return {
if (value.source === "file") return [value.target, value.modified ?? ""].join("|") id: item.id,
return [value.target, value.requested ?? "", value.version ?? ""].join("|") source,
} spec,
target,
async function read(file: string): Promise<Store> { requested: parsePluginSpecifier(spec).version,
return Filesystem.readJson<Store>(file).catch(() => ({}) as Store) version: await npmVersion(target),
}
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 } {
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 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 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> {
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))
} }
} }
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> {
return Filesystem.readJson<Store>(file).catch(() => ({}) as Store)
}
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 } {
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 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 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> {
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))
}
export * as PluginMeta from "./meta"