diff --git a/src/features/hook-message-injector/constants.ts b/src/features/hook-message-injector/constants.ts index dc90e661a..0424b96cf 100644 --- a/src/features/hook-message-injector/constants.ts +++ b/src/features/hook-message-injector/constants.ts @@ -1,6 +1 @@ -import { join } from "node:path" -import { getOpenCodeStorageDir } from "../../shared/data-path" - -export const OPENCODE_STORAGE = getOpenCodeStorageDir() -export const MESSAGE_STORAGE = join(OPENCODE_STORAGE, "message") -export const PART_STORAGE = join(OPENCODE_STORAGE, "part") +export { OPENCODE_STORAGE, MESSAGE_STORAGE, PART_STORAGE } from "../../shared" diff --git a/src/shared/index.ts b/src/shared/index.ts index 54bcf6795..6a0ef5bf0 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -22,6 +22,7 @@ export type { OpenCodeConfigPaths, } from "./opencode-config-dir-types" export * from "./opencode-version" +export * from "./opencode-storage-detection" export * from "./permission-compat" export * from "./external-plugin-detector" export * from "./zip-extractor" @@ -49,4 +50,5 @@ export * from "./port-utils" export * from "./git-worktree" export * from "./safe-create-hook" export * from "./truncate-description" +export * from "./opencode-storage-paths" export * from "./opencode-message-dir" diff --git a/src/shared/opencode-storage-detection.test.ts b/src/shared/opencode-storage-detection.test.ts new file mode 100644 index 000000000..a87b7bf3c --- /dev/null +++ b/src/shared/opencode-storage-detection.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "bun:test" +import { existsSync } from "node:fs" +import { join } from "node:path" +import { isSqliteBackend, resetSqliteBackendCache } from "./opencode-storage-detection" +import { getDataDir } from "./data-path" +import { isOpenCodeVersionAtLeast, OPENCODE_SQLITE_VERSION } from "./opencode-version" + +// Mock the dependencies +const mockExistsSync = vi.fn() +const mockGetDataDir = vi.fn() +const mockIsOpenCodeVersionAtLeast = vi.fn() + +vi.mock("node:fs", () => ({ + existsSync: mockExistsSync, +})) + +vi.mock("./data-path", () => ({ + getDataDir: mockGetDataDir, +})) + +vi.mock("./opencode-version", () => ({ + isOpenCodeVersionAtLeast: mockIsOpenCodeVersionAtLeast, + OPENCODE_SQLITE_VERSION: "1.1.53", +})) + +describe("isSqliteBackend", () => { + beforeEach(() => { + // Reset the cached result + resetSqliteBackendCache() + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + it("returns false when version is below threshold", () => { + // given + mockIsOpenCodeVersionAtLeast.mockReturnValue(false) + mockGetDataDir.mockReturnValue("/home/user/.local/share") + mockExistsSync.mockReturnValue(true) + + // when + const result = isSqliteBackend() + + // then + expect(result).toBe(false) + expect(mockIsOpenCodeVersionAtLeast).toHaveBeenCalledWith(OPENCODE_SQLITE_VERSION) + }) + + it("returns false when DB file does not exist", () => { + // given + mockIsOpenCodeVersionAtLeast.mockReturnValue(true) + mockGetDataDir.mockReturnValue("/home/user/.local/share") + mockExistsSync.mockReturnValue(false) + + // when + const result = isSqliteBackend() + + // then + expect(result).toBe(false) + expect(mockExistsSync).toHaveBeenCalledWith(join("/home/user/.local/share", "opencode", "opencode.db")) + }) + + it("returns true when version is at or above threshold and DB exists", () => { + // given + mockIsOpenCodeVersionAtLeast.mockReturnValue(true) + mockGetDataDir.mockReturnValue("/home/user/.local/share") + mockExistsSync.mockReturnValue(true) + + // when + const result = isSqliteBackend() + + // then + expect(result).toBe(true) + expect(mockIsOpenCodeVersionAtLeast).toHaveBeenCalledWith(OPENCODE_SQLITE_VERSION) + expect(mockExistsSync).toHaveBeenCalledWith(join("/home/user/.local/share", "opencode", "opencode.db")) + }) + + it("caches the result and does not re-check on subsequent calls", () => { + // given + mockIsOpenCodeVersionAtLeast.mockReturnValue(true) + mockGetDataDir.mockReturnValue("/home/user/.local/share") + mockExistsSync.mockReturnValue(true) + + // when + isSqliteBackend() + isSqliteBackend() + isSqliteBackend() + + // then + expect(mockIsOpenCodeVersionAtLeast).toHaveBeenCalledTimes(1) + expect(mockExistsSync).toHaveBeenCalledTimes(1) + }) +}) \ No newline at end of file diff --git a/src/shared/opencode-storage-detection.ts b/src/shared/opencode-storage-detection.ts new file mode 100644 index 000000000..7fdb5a5cd --- /dev/null +++ b/src/shared/opencode-storage-detection.ts @@ -0,0 +1,23 @@ +import { existsSync } from "node:fs" +import { join } from "node:path" +import { getDataDir } from "./data-path" +import { isOpenCodeVersionAtLeast, OPENCODE_SQLITE_VERSION } from "./opencode-version" + +let cachedResult: boolean | null = null + +export function isSqliteBackend(): boolean { + if (cachedResult !== null) { + return cachedResult + } + + const versionOk = isOpenCodeVersionAtLeast(OPENCODE_SQLITE_VERSION) + const dbPath = join(getDataDir(), "opencode", "opencode.db") + const dbExists = existsSync(dbPath) + + cachedResult = versionOk && dbExists + return cachedResult +} + +export function resetSqliteBackendCache(): void { + cachedResult = null +} \ No newline at end of file diff --git a/src/shared/opencode-storage-paths.ts b/src/shared/opencode-storage-paths.ts new file mode 100644 index 000000000..baf1a4dc6 --- /dev/null +++ b/src/shared/opencode-storage-paths.ts @@ -0,0 +1,7 @@ +import { join } from "node:path" +import { getOpenCodeStorageDir } from "./data-path" + +export const OPENCODE_STORAGE = getOpenCodeStorageDir() +export const MESSAGE_STORAGE = join(OPENCODE_STORAGE, "message") +export const PART_STORAGE = join(OPENCODE_STORAGE, "part") +export const SESSION_STORAGE = join(OPENCODE_STORAGE, "session") \ No newline at end of file diff --git a/src/shared/opencode-version.ts b/src/shared/opencode-version.ts index f02161ac0..e4eecd766 100644 --- a/src/shared/opencode-version.ts +++ b/src/shared/opencode-version.ts @@ -15,6 +15,12 @@ export const MINIMUM_OPENCODE_VERSION = "1.1.1" */ export const OPENCODE_NATIVE_AGENTS_INJECTION_VERSION = "1.1.37" +/** + * OpenCode version that introduced SQLite backend for storage. + * When this version is detected AND opencode.db exists, SQLite backend is used. + */ +export const OPENCODE_SQLITE_VERSION = "1.1.53" + const NOT_CACHED = Symbol("NOT_CACHED") let cachedVersion: string | null | typeof NOT_CACHED = NOT_CACHED diff --git a/src/tools/session-manager/constants.ts b/src/tools/session-manager/constants.ts index 5f079a1a8..cdcb914c1 100644 --- a/src/tools/session-manager/constants.ts +++ b/src/tools/session-manager/constants.ts @@ -1,11 +1,7 @@ import { join } from "node:path" -import { getOpenCodeStorageDir } from "../../shared/data-path" import { getClaudeConfigDir } from "../../shared" -export const OPENCODE_STORAGE = getOpenCodeStorageDir() -export const MESSAGE_STORAGE = join(OPENCODE_STORAGE, "message") -export const PART_STORAGE = join(OPENCODE_STORAGE, "part") -export const SESSION_STORAGE = join(OPENCODE_STORAGE, "session") +export { OPENCODE_STORAGE, MESSAGE_STORAGE, PART_STORAGE, SESSION_STORAGE } from "../../shared" export const TODO_DIR = join(getClaudeConfigDir(), "todos") export const TRANSCRIPT_DIR = join(getClaudeConfigDir(), "transcripts") export const SESSION_LIST_DESCRIPTION = `List all OpenCode sessions with optional filtering.