diff --git a/src/shared/load-opencode-plugins.test.ts b/src/shared/load-opencode-plugins.test.ts
new file mode 100644
index 000000000..9723c1cd3
--- /dev/null
+++ b/src/shared/load-opencode-plugins.test.ts
@@ -0,0 +1,89 @@
+///
+
+import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"
+import * as fs from "node:fs"
+
+type LoadOpencodePluginsModule = {
+ loadOpencodePlugins: (directory: string) => string[]
+ clearOpencodePluginsCache?: () => void
+}
+
+const existsSyncMock = mock((_path: string) => true)
+const readFileSyncMock = mock((_path: string, _encoding?: string) => `{
+ "plugin": ["plugin-a", "plugin-b"]
+}`)
+
+async function importFreshLoadOpencodePluginsModule(): Promise {
+ const modulePath = `${new URL("./load-opencode-plugins.ts", import.meta.url).pathname}?test=${Date.now()}-${Math.random()}`
+ return import(modulePath)
+}
+
+describe("loadOpencodePlugins", () => {
+ beforeEach(() => {
+ existsSyncMock.mockReset()
+ existsSyncMock.mockImplementation((_path: string) => true)
+ readFileSyncMock.mockReset()
+ readFileSyncMock.mockImplementation((_path: string, _encoding?: string) => `{
+ "plugin": ["plugin-a", "plugin-b"]
+}`)
+
+ mock.module("node:fs", () => ({
+ ...fs,
+ existsSync: existsSyncMock,
+ readFileSync: readFileSyncMock,
+ }))
+ })
+
+ afterEach(() => {
+ mock.restore()
+ })
+
+ describe("#given the same directory is loaded twice", () => {
+ describe("#when loading plugins repeatedly", () => {
+ it("#then does not call readFileSync on the second load", async () => {
+ // given
+ const { loadOpencodePlugins } = await importFreshLoadOpencodePluginsModule()
+
+ // when
+ const firstResult = loadOpencodePlugins("/some/fake/dir")
+ const readCountAfterFirstLoad = readFileSyncMock.mock.calls.length
+ const secondResult = loadOpencodePlugins("/some/fake/dir")
+ const readCountAfterSecondLoad = readFileSyncMock.mock.calls.length
+
+ // then
+ expect(firstResult).toEqual(["plugin-a", "plugin-b"])
+ expect(secondResult).toEqual(["plugin-a", "plugin-b"])
+ expect(readCountAfterFirstLoad).toBeGreaterThan(0)
+ expect(readCountAfterSecondLoad - readCountAfterFirstLoad).toBe(0)
+ })
+ })
+ })
+
+ describe("#given the plugin cache was cleared", () => {
+ describe("#when loading the same directory again", () => {
+ it("#then re-reads plugin config files from disk", async () => {
+ // given
+ const { loadOpencodePlugins, clearOpencodePluginsCache } = await importFreshLoadOpencodePluginsModule()
+
+ if (typeof clearOpencodePluginsCache !== "function") {
+ throw new Error("clearOpencodePluginsCache export is missing")
+ }
+
+ // when
+ const firstResult = loadOpencodePlugins("/some/fake/dir")
+ const readCountAfterFirstLoad = readFileSyncMock.mock.calls.length
+ loadOpencodePlugins("/some/fake/dir")
+ const readCountAfterSecondLoad = readFileSyncMock.mock.calls.length
+ clearOpencodePluginsCache()
+ const thirdResult = loadOpencodePlugins("/some/fake/dir")
+ const readCountAfterThirdLoad = readFileSyncMock.mock.calls.length
+
+ // then
+ expect(firstResult).toEqual(["plugin-a", "plugin-b"])
+ expect(thirdResult).toEqual(["plugin-a", "plugin-b"])
+ expect(readCountAfterSecondLoad - readCountAfterFirstLoad).toBe(0)
+ expect(readCountAfterThirdLoad - readCountAfterSecondLoad).toBeGreaterThan(0)
+ })
+ })
+ })
+})
diff --git a/src/shared/load-opencode-plugins.ts b/src/shared/load-opencode-plugins.ts
index 5517c74b1..a6beffdcf 100644
--- a/src/shared/load-opencode-plugins.ts
+++ b/src/shared/load-opencode-plugins.ts
@@ -8,6 +8,8 @@ interface OpencodeConfig {
plugin?: (string | [string, ...unknown[]])[]
}
+const opencodePluginsCache = new Map()
+
function getWindowsAppdataDir(): string | null {
return process.env.APPDATA || null
}
@@ -33,6 +35,11 @@ function getConfigPaths(directory: string): string[] {
}
export function loadOpencodePlugins(directory: string): string[] {
+ const cachedPluginEntries = opencodePluginsCache.get(directory)
+ if (cachedPluginEntries) {
+ return cachedPluginEntries
+ }
+
const pluginEntries: string[] = []
const seenPluginEntries = new Set()
@@ -56,5 +63,10 @@ export function loadOpencodePlugins(directory: string): string[] {
}
}
+ opencodePluginsCache.set(directory, pluginEntries)
return pluginEntries
}
+
+export function clearOpencodePluginsCache(): void {
+ opencodePluginsCache.clear()
+}