refactor: convert config path constants to getter functions for dynamic OPENCODE_CONFIG_DIR support

🤖 Generated with assistance of OhMyOpenCode
This commit is contained in:
YeonGyu-Kim
2026-04-05 11:23:01 +09:00
parent b14665f174
commit 98c2f92251
10 changed files with 139 additions and 35 deletions
+2 -2
View File
@@ -9,7 +9,6 @@ import { loadAvailableModelsFromCache } from "./model-resolution-cache"
import { getModelResolutionInfoWithOverrides } from "./model-resolution"
import type { OmoConfig } from "./model-resolution-types"
const USER_CONFIG_DIR = getOpenCodeConfigDir({ binary: "opencode" })
const PROJECT_CONFIG_DIR = join(process.cwd(), ".opencode")
interface ConfigValidationResult {
@@ -24,7 +23,8 @@ function findConfigPath(): string | null {
const projectConfig = detectPluginConfigFile(PROJECT_CONFIG_DIR)
if (projectConfig.format !== "none") return projectConfig.path
const userConfig = detectPluginConfigFile(USER_CONFIG_DIR)
const userConfigDir = getOpenCodeConfigDir({ binary: "opencode" })
const userConfig = detectPluginConfigFile(userConfigDir)
if (userConfig.format !== "none") return userConfig.path
return null
@@ -1,9 +1,8 @@
import { readFileSync } from "node:fs"
import { join } from "node:path"
import { detectPluginConfigFile, getOpenCodeConfigPaths, parseJsonc } from "../../../shared"
import { detectPluginConfigFile, getOpenCodeConfigDir, parseJsonc } from "../../../shared"
import type { OmoConfig } from "./model-resolution-types"
const USER_CONFIG_DIR = getOpenCodeConfigPaths({ binary: "opencode", version: null }).configDir
const PROJECT_CONFIG_DIR = join(process.cwd(), ".opencode")
export function loadOmoConfig(): OmoConfig | null {
@@ -17,7 +16,8 @@ export function loadOmoConfig(): OmoConfig | null {
}
}
const userDetected = detectPluginConfigFile(USER_CONFIG_DIR)
const userConfigDir = getOpenCodeConfigDir({ binary: "opencode" })
const userDetected = detectPluginConfigFile(userConfigDir)
if (userDetected.format !== "none") {
try {
const content = readFileSync(userDetected.path, "utf-8")
+3 -2
View File
@@ -1,6 +1,6 @@
import * as fs from "node:fs"
import * as path from "node:path"
import { CACHE_DIR, PACKAGE_NAME, USER_CONFIG_DIR } from "./constants"
import { CACHE_DIR, PACKAGE_NAME, getUserConfigDir } from "./constants"
import { log } from "../../shared/logger"
interface BunLockfile {
@@ -61,8 +61,9 @@ function removeFromBunLock(packageName: string): boolean {
export function invalidatePackage(packageName: string = PACKAGE_NAME): boolean {
try {
const userConfigDir = getUserConfigDir()
const pkgDirs = [
path.join(USER_CONFIG_DIR, "node_modules", packageName),
path.join(userConfigDir, "node_modules", packageName),
path.join(CACHE_DIR, "node_modules", packageName),
]
@@ -1,18 +1,19 @@
import * as os from "node:os"
import * as path from "node:path"
import {
USER_CONFIG_DIR,
USER_OPENCODE_CONFIG,
USER_OPENCODE_CONFIG_JSONC,
getUserConfigDir,
getUserOpencodeConfig,
getUserOpencodeConfigJsonc,
getWindowsAppdataDir,
} from "../constants"
export function getConfigPaths(directory: string): string[] {
const userConfigDir = getUserConfigDir()
const paths = [
path.join(directory, ".opencode", "opencode.json"),
path.join(directory, ".opencode", "opencode.jsonc"),
USER_OPENCODE_CONFIG,
USER_OPENCODE_CONFIG_JSONC,
getUserOpencodeConfig(),
getUserOpencodeConfigJsonc(),
]
if (process.platform === "win32") {
@@ -20,7 +21,7 @@ export function getConfigPaths(directory: string): string[] {
const appdataDir = getWindowsAppdataDir()
if (appdataDir) {
const alternateDir = USER_CONFIG_DIR === crossPlatformDir ? appdataDir : crossPlatformDir
const alternateDir = userConfigDir === crossPlatformDir ? appdataDir : crossPlatformDir
const alternateConfig = path.join(alternateDir, "opencode", "opencode.json")
const alternateConfigJsonc = path.join(alternateDir, "opencode", "opencode.jsonc")
@@ -1,15 +1,51 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
import { spawnSync } from "node:child_process"
import * as fs from "node:fs"
import * as os from "node:os"
import * as path from "node:path"
import { PACKAGE_NAME } from "../constants"
import { findPluginEntry } from "./plugin-entry"
const PACKAGE_NAME = "oh-my-openagent"
type PluginEntryResult = {
entry: string
isPinned: boolean
pinnedVersion: string | null
configPath: string
} | null
function runFindPluginEntry(
directory: string,
envOverrides: Record<string, string | undefined> = {},
): { status: number | null; stdout: string; stderr: string } {
const command = [
`import { findPluginEntry } from ${JSON.stringify("./src/hooks/auto-update-checker/checker/plugin-entry")};`,
`const result = findPluginEntry(${JSON.stringify(directory)});`,
"console.log(JSON.stringify(result));",
].join("")
const execution = spawnSync(process.execPath, ["-e", command], {
cwd: process.cwd(),
env: {
...process.env,
...envOverrides,
},
encoding: "utf-8",
})
return {
status: execution.status,
stdout: execution.stdout,
stderr: execution.stderr,
}
}
describe("findPluginEntry", () => {
let temporaryDirectory: string
let configPath: string
let originalConfigDir: string | undefined
beforeEach(() => {
originalConfigDir = process.env.OPENCODE_CONFIG_DIR
temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "omo-plugin-entry-test-"))
const opencodeDirectory = path.join(temporaryDirectory, ".opencode")
fs.mkdirSync(opencodeDirectory, { recursive: true })
@@ -17,58 +53,93 @@ describe("findPluginEntry", () => {
})
afterEach(() => {
if (originalConfigDir === undefined) {
delete process.env.OPENCODE_CONFIG_DIR
} else {
process.env.OPENCODE_CONFIG_DIR = originalConfigDir
}
fs.rmSync(temporaryDirectory, { recursive: true, force: true })
})
test("returns unpinned for bare package name", () => {
test("returns unpinned for bare package name", async () => {
// #given plugin is configured without a tag
fs.writeFileSync(configPath, JSON.stringify({ plugin: [PACKAGE_NAME] }))
// #when plugin entry is detected
const pluginInfo = findPluginEntry(temporaryDirectory)
const execution = runFindPluginEntry(temporaryDirectory)
// #then entry is not pinned
expect(execution.status).toBe(0)
const pluginInfo = JSON.parse(execution.stdout.trim()) as PluginEntryResult
expect(pluginInfo).not.toBeNull()
expect(pluginInfo?.isPinned).toBe(false)
expect(pluginInfo?.pinnedVersion).toBeNull()
})
test("returns unpinned for latest dist-tag", () => {
test("returns unpinned for latest dist-tag", async () => {
// #given plugin is configured with latest dist-tag
fs.writeFileSync(configPath, JSON.stringify({ plugin: [`${PACKAGE_NAME}@latest`] }))
// #when plugin entry is detected
const pluginInfo = findPluginEntry(temporaryDirectory)
const execution = runFindPluginEntry(temporaryDirectory)
// #then latest is treated as channel, not pin
expect(execution.status).toBe(0)
const pluginInfo = JSON.parse(execution.stdout.trim()) as PluginEntryResult
expect(pluginInfo).not.toBeNull()
expect(pluginInfo?.isPinned).toBe(false)
expect(pluginInfo?.pinnedVersion).toBe("latest")
})
test("returns unpinned for beta dist-tag", () => {
test("returns unpinned for beta dist-tag", async () => {
// #given plugin is configured with beta dist-tag
fs.writeFileSync(configPath, JSON.stringify({ plugin: [`${PACKAGE_NAME}@beta`] }))
// #when plugin entry is detected
const pluginInfo = findPluginEntry(temporaryDirectory)
const execution = runFindPluginEntry(temporaryDirectory)
// #then beta is treated as channel, not pin
expect(execution.status).toBe(0)
const pluginInfo = JSON.parse(execution.stdout.trim()) as PluginEntryResult
expect(pluginInfo).not.toBeNull()
expect(pluginInfo?.isPinned).toBe(false)
expect(pluginInfo?.pinnedVersion).toBe("beta")
})
test("returns pinned for explicit semver", () => {
test("returns pinned for explicit semver", async () => {
// #given plugin is configured with explicit version
fs.writeFileSync(configPath, JSON.stringify({ plugin: [`${PACKAGE_NAME}@3.5.2`] }))
// #when plugin entry is detected
const pluginInfo = findPluginEntry(temporaryDirectory)
const execution = runFindPluginEntry(temporaryDirectory)
// #then explicit semver is treated as pin
expect(execution.status).toBe(0)
const pluginInfo = JSON.parse(execution.stdout.trim()) as PluginEntryResult
expect(pluginInfo).not.toBeNull()
expect(pluginInfo?.isPinned).toBe(true)
expect(pluginInfo?.pinnedVersion).toBe("3.5.2")
})
test("reads user config from profile dir even when OPENCODE_CONFIG_DIR changes after import", async () => {
// #given profile-specific user config after module import
const profileConfigDir = path.join(temporaryDirectory, "profiles", "today")
fs.mkdirSync(profileConfigDir, { recursive: true })
fs.writeFileSync(
path.join(profileConfigDir, "opencode.json"),
JSON.stringify({ plugin: [`${PACKAGE_NAME}@beta`] }),
)
// #when plugin entry is detected
const execution = runFindPluginEntry(path.join(temporaryDirectory, "workspace"), {
OPENCODE_CONFIG_DIR: profileConfigDir,
})
// #then profile dir is respected
expect(execution.status).toBe(0)
const pluginInfo = JSON.parse(execution.stdout.trim()) as PluginEntryResult
expect(pluginInfo).not.toBeNull()
expect(pluginInfo?.configPath).toEndWith("/profiles/today/opencode.json")
expect(pluginInfo?.pinnedVersion).toBe("beta")
})
})
+11 -3
View File
@@ -16,9 +16,17 @@ export function getWindowsAppdataDir(): string | null {
return process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming")
}
export const USER_CONFIG_DIR = getOpenCodeConfigDir({ binary: "opencode" })
export const USER_OPENCODE_CONFIG = path.join(USER_CONFIG_DIR, "opencode.json")
export const USER_OPENCODE_CONFIG_JSONC = path.join(USER_CONFIG_DIR, "opencode.jsonc")
export function getUserConfigDir(): string {
return getOpenCodeConfigDir({ binary: "opencode" })
}
export function getUserOpencodeConfig(): string {
return path.join(getUserConfigDir(), "opencode.json")
}
export function getUserOpencodeConfigJsonc(): string {
return path.join(getUserConfigDir(), "opencode.jsonc")
}
export const INSTALLED_PACKAGE_JSON = path.join(
CACHE_DIR,
@@ -94,6 +94,26 @@ describe("loadPluginExtendedConfig", () => {
},
})
})
test("#given OPENCODE_CONFIG_DIR points at a profile dir after module import #when loading extended config #then it reads the profile config file", async () => {
//#given
const profileConfigDir = join(tempDirectory, ".config", "opencode", "profiles", "today")
const profileConfigPath = join(profileConfigDir, "opencode-cc-plugin.json")
mkdirSync(profileConfigDir, { recursive: true })
process.env.OPENCODE_CONFIG_DIR = profileConfigDir
writeConfigFile(profileConfigPath, ["profile-stop"])
//#when
clearPluginExtendedConfigCache()
const result = await loadPluginExtendedConfig()
//#then
expect(result).toEqual({
disabledHooks: {
Stop: ["profile-stop"],
},
})
})
})
function writeConfigFile(filePath: string, stopPatterns: string[]): void {
+6 -3
View File
@@ -23,15 +23,18 @@ interface PluginExtendedConfigCacheEntry {
cachedAt: number
}
const USER_CONFIG_PATH = join(getOpenCodeConfigDir({ binary: "opencode" }), "opencode-cc-plugin.json")
const configCache = new Map<string, PluginExtendedConfigCacheEntry>()
function getUserConfigPath(): string {
return join(getOpenCodeConfigDir({ binary: "opencode" }), "opencode-cc-plugin.json")
}
function getProjectConfigPath(): string {
return join(process.cwd(), ".opencode", "opencode-cc-plugin.json")
}
function getCacheKey(): string {
return process.cwd()
return `${process.cwd()}::${getUserConfigPath()}`
}
function getCachedConfig(cacheKey: string): PluginExtendedConfig | undefined {
@@ -89,7 +92,7 @@ export async function loadPluginExtendedConfig(): Promise<PluginExtendedConfig>
return cachedConfig
}
const userConfig = await loadConfigFromPath(USER_CONFIG_PATH)
const userConfig = await loadConfigFromPath(getUserConfigPath())
const projectConfig = await loadConfigFromPath(getProjectConfigPath())
const merged: PluginExtendedConfig = {
+3 -3
View File
@@ -15,14 +15,14 @@ const _realLogger = require("../../shared/logger")
async function importFreshCacheModule(): Promise<typeof import("../auto-update-checker/cache")> {
mock.module("../auto-update-checker/constants", () => ({
CACHE_DIR: TEST_OPENCODE_CACHE_DIR,
USER_CONFIG_DIR: TEST_USER_CONFIG_DIR,
PACKAGE_NAME: "oh-my-opencode",
NPM_REGISTRY_URL: "https://registry.npmjs.org/-/package/oh-my-opencode/dist-tags",
NPM_FETCH_TIMEOUT: 5000,
VERSION_FILE: join(TEST_OPENCODE_CACHE_DIR, "version"),
USER_OPENCODE_CONFIG: join(TEST_USER_CONFIG_DIR, "opencode.json"),
USER_OPENCODE_CONFIG_JSONC: join(TEST_USER_CONFIG_DIR, "opencode.jsonc"),
INSTALLED_PACKAGE_JSON: join(TEST_OPENCODE_CACHE_DIR, "node_modules", "oh-my-opencode", "package.json"),
getUserConfigDir: () => TEST_USER_CONFIG_DIR,
getUserOpencodeConfig: () => join(TEST_USER_CONFIG_DIR, "opencode.json"),
getUserOpencodeConfigJsonc: () => join(TEST_USER_CONFIG_DIR, "opencode.jsonc"),
getWindowsAppdataDir: () => null,
}))
@@ -19,10 +19,10 @@ async function importFreshSyncPackageJsonModule(): Promise<typeof import("../aut
NPM_REGISTRY_URL: "https://registry.npmjs.org/-/package/oh-my-opencode/dist-tags",
NPM_FETCH_TIMEOUT: 5000,
VERSION_FILE: join(TEST_CACHE_DIR, "version"),
USER_CONFIG_DIR: "/tmp/opencode-config",
USER_OPENCODE_CONFIG: "/tmp/opencode-config/opencode.json",
USER_OPENCODE_CONFIG_JSONC: "/tmp/opencode-config/opencode.jsonc",
INSTALLED_PACKAGE_JSON: join(TEST_CACHE_DIR, "node_modules", "oh-my-opencode", "package.json"),
getUserConfigDir: () => "/tmp/opencode-config",
getUserOpencodeConfig: () => "/tmp/opencode-config/opencode.json",
getUserOpencodeConfigJsonc: () => "/tmp/opencode-config/opencode.jsonc",
getWindowsAppdataDir: () => null,
}))