diff --git a/src/core.ts b/src/core.ts index 9367af7..4605cc2 100644 --- a/src/core.ts +++ b/src/core.ts @@ -126,6 +126,9 @@ export const userHomeDir = () => process.env.HOME || process.env.USERPROFILE || export const sharedGlobalSettingsPath = () => path.join(userHomeDir(), SHARED_SETTINGS_DIR_NAME, SHARED_SETTINGS_FILE_NAME) +export const sharedConfigPath = (configPathOverride?: string) => + configPathOverride ? path.resolve(configPathOverride) : sharedGlobalSettingsPath() + export const normalizeId = (value: string) => value.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "default" diff --git a/src/index.ts b/src/index.ts index 75ce13c..21d7231 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ import { observationUpgradeNotice, resolveSessionPeerIds, SETTING_ENUMS, + sharedConfigPath, sharedGlobalSettingsPath, stampedHostObservationMode, timestampToIso, @@ -773,9 +774,6 @@ const deriveProjectRoot = (host: HostAdapter) => { return path.resolve(host.worktree || host.projectWorktree || host.directory || process.cwd()) } -const sharedConfigPath = (configPathOverride?: string) => - configPathOverride ? path.resolve(configPathOverride) : sharedGlobalSettingsPath() - const readJsonFile = async (configPath: string) => { try { return JSON.parse(await readFile(configPath, "utf-8")) as Record diff --git a/src/server.ts b/src/server.ts index 4c0d4b9..f07b617 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,7 +2,9 @@ import { createHonchoRuntimePlugin } from "./index.js" import { definition } from "./v2/runtime.js" /** OpenCode 1.x server plugin (hook map). */ -export const server = createHonchoRuntimePlugin() +export const server = createHonchoRuntimePlugin({ + configPath: process.env.OPENCODE_HONCHO_CONFIG_PATH, +}) /** * One default export for both OpenCode generations. Both resolve this package's `./server` diff --git a/src/tui.ts b/src/tui.ts index c1bdd5e..a12a1d4 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -1,9 +1,9 @@ import type { TuiPluginModule } from "@opencode-ai/plugin/tui" -import { sharedGlobalSettingsPath } from "./core.js" import { modeEditableFieldPaths, normalizeSettings, readSharedConfig, + resolveConfigPath, resolveSharedConfigField, saveSettings, settingsMessage, @@ -33,7 +33,7 @@ export const __testing = { resolveSharedConfigField, saveSettings, settingsMessage, - sharedConfigPath: sharedGlobalSettingsPath, + sharedConfigPath: resolveConfigPath, sharedConfigPresetOptions, statusMessage, validateCloudApiKey, diff --git a/src/tui/commands.ts b/src/tui/commands.ts index a475524..54bf4d2 100644 --- a/src/tui/commands.ts +++ b/src/tui/commands.ts @@ -13,12 +13,14 @@ import { needsObservationUpgradePrompt, observationUpgradeNotice, resolveSessionPeerIds, - sharedGlobalSettingsPath, + sharedConfigPath, unifiedImportFollowUp, type ObservationMode, } from "../core.js" import type { DialogOption, GlobalSettings, TuiCommandSpec, TuiSession } from "./dialogs.js" +export const resolveConfigPath = () => sharedConfigPath(process.env.OPENCODE_HONCHO_CONFIG_PATH) + const SHARED_CONFIG_PRESETS: Record = Object.fromEntries( Object.entries(SETTING_ENUMS).map(([key, values]) => [key.toLowerCase(), values]), ) @@ -36,7 +38,7 @@ const MODE_EDITABLE_FIELD_PATHS = [ ] as const export const readGlobalSettings = async (): Promise => { - const configPath = sharedGlobalSettingsPath() + const configPath = resolveConfigPath() try { const raw = await readFile(configPath, "utf-8") const parsed = JSON.parse(raw) @@ -50,7 +52,7 @@ export const readGlobalSettings = async (): Promise => { } export const readSharedConfig = async (): Promise | null> => { - const configPath = sharedGlobalSettingsPath() + const configPath = resolveConfigPath() try { const raw = await readFile(configPath, "utf-8") const parsed = JSON.parse(raw) @@ -67,7 +69,7 @@ export const readSharedConfig = async (): Promise | null } export const writeSharedConfig = async (settings: Record) => { - const configPath = sharedGlobalSettingsPath() + const configPath = resolveConfigPath() await mkdir(path.dirname(configPath), { recursive: true }) await writeFile(configPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8") return configPath @@ -100,7 +102,7 @@ export const resolveSharedConfigField = (config: Record, field: (candidate) => candidate.toLowerCase() === field.trim().toLowerCase(), ) if (!canonical) { - throw new Error(`Field '${field}' does not exist in ${sharedGlobalSettingsPath()}.`) + throw new Error(`Field '${field}' does not exist in ${resolveConfigPath()}.`) } return canonical } @@ -134,7 +136,7 @@ const parseSharedConfigValue = (currentValue: unknown, rawValue: string) => { } const writeGlobalSettings = async (settings: GlobalSettings) => { - const configPath = sharedGlobalSettingsPath() + const configPath = resolveConfigPath() await mkdir(path.dirname(configPath), { recursive: true }) await writeFile(configPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8") return configPath @@ -171,7 +173,7 @@ export const statusMessage = ( `Peer name: ${normalized.peerName || "user"}`, ...(liveStatus?.workspaceName ? [`Workspace: ${liveStatus.workspaceName}`] : []), ...(liveStatus?.openCodeSessionId ? [`OpenCode session: ${liveStatus.openCodeSessionId}`] : []), - `Config path: ${sharedGlobalSettingsPath()}`, + `Config path: ${resolveConfigPath()}`, "", configured ? "Honcho is ready for OpenCode." : "Run /honcho:setup to finish configuration.", ...(configured && needsObservationUpgradePrompt(settings as Record) @@ -183,7 +185,7 @@ export const statusMessage = ( export const settingsMessage = (settings: GlobalSettings) => { const host = settings.hosts?.opencode || {} return [ - `Config path: ${sharedGlobalSettingsPath()}`, + `Config path: ${resolveConfigPath()}`, `API key: ${settings.apiKey?.trim() ? "set" : "not set"}`, `Peer name: ${settings.peerName?.trim() || "user"}`, `Base URL: ${settings.baseUrl?.trim() || DEFAULT_SETTINGS.baseUrl}`, @@ -357,7 +359,7 @@ export const runConfig = async (session: TuiSession) => { if (!config) { await session.dialogs.alert({ title: "Honcho config missing", - message: `The config does not exist at ${sharedGlobalSettingsPath()}.`, + message: `The config does not exist at ${resolveConfigPath()}.`, }) return } diff --git a/src/v2/runtime.ts b/src/v2/runtime.ts index 8e7bdba..80c6324 100644 --- a/src/v2/runtime.ts +++ b/src/v2/runtime.ts @@ -55,7 +55,8 @@ const isCurrentGeneration = (location: string, generation: number) => generation export const setup = async (ctx: PluginContext) => { const location = ctx.location.directory const generation = nextGeneration(location) - const configPath = typeof ctx.options?.configPath === "string" ? ctx.options.configPath : undefined + const configPath = + typeof ctx.options?.configPath === "string" ? ctx.options.configPath : process.env.OPENCODE_HONCHO_CONFIG_PATH || undefined const core = createHonchoCore(consoleHost(ctx.location.directory, ctx.location.project?.directory), configPath) core.setHostVersion(ctx.app.version) diff --git a/tests/tui-behavior.test.js b/tests/tui-behavior.test.js index fabf42f..a2e9904 100644 --- a/tests/tui-behavior.test.js +++ b/tests/tui-behavior.test.js @@ -64,3 +64,45 @@ test("tui saveSettings persists only supported root and host fields", async () = else process.env.USERPROFILE = previousUserProfile } }) + +test("tui honors OPENCODE_HONCHO_CONFIG_PATH override for reads, writes, and display", async () => { + const homeDir = await mkdtemp(path.join(os.tmpdir(), "honcho-tui-override-")) + const globalConfigPath = path.join(homeDir, ".honcho", "config.json") + const overrideDir = path.join(homeDir, "override") + const overrideConfigPath = path.join(overrideDir, "custom.json") + const previousHome = process.env.HOME + const previousUserProfile = process.env.USERPROFILE + const previousOverride = process.env.OPENCODE_HONCHO_CONFIG_PATH + + await mkdir(overrideDir, { recursive: true }) + await writeFile(overrideConfigPath, JSON.stringify({ peerName: "override-peer" }, null, 2)) + process.env.HOME = homeDir + process.env.USERPROFILE = homeDir + process.env.OPENCODE_HONCHO_CONFIG_PATH = overrideConfigPath + + try { + assert.equal(__testing.sharedConfigPath(), overrideConfigPath) + + const readBack = await __testing.readSharedConfig() + assert.equal(readBack.peerName, "override-peer") + + await __testing.saveSettings({ + apiKey: "key", + baseUrl: "https://api.honcho.dev", + hosts: { opencode: { workspace: "opencode" } }, + }) + const persisted = JSON.parse(await readFile(overrideConfigPath, "utf-8")) + assert.equal(persisted.apiKey, "key") + assert.equal(persisted.peerName, "override-peer") + assert.rejects(readFile(globalConfigPath, "utf-8")) + + assert.match(__testing.settingsMessage({}), new RegExp(`Config path: ${overrideConfigPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`)) + } finally { + if (previousHome === undefined) delete process.env.HOME + else process.env.HOME = previousHome + if (previousUserProfile === undefined) delete process.env.USERPROFILE + else process.env.USERPROFILE = previousUserProfile + if (previousOverride === undefined) delete process.env.OPENCODE_HONCHO_CONFIG_PATH + else process.env.OPENCODE_HONCHO_CONFIG_PATH = previousOverride + } +})