Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export const normalizeId = (value: string) =>
value.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "default"

Expand Down
4 changes: 1 addition & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
observationUpgradeNotice,
resolveSessionPeerIds,
SETTING_ENUMS,
sharedConfigPath,
sharedGlobalSettingsPath,
stampedHostObservationMode,
timestampToIso,
Expand Down Expand Up @@ -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<string, unknown>
Expand Down
4 changes: 3 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
4 changes: 2 additions & 2 deletions src/tui.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -33,7 +33,7 @@ export const __testing = {
resolveSharedConfigField,
saveSettings,
settingsMessage,
sharedConfigPath: sharedGlobalSettingsPath,
sharedConfigPath: resolveConfigPath,
sharedConfigPresetOptions,
statusMessage,
validateCloudApiKey,
Expand Down
20 changes: 11 additions & 9 deletions src/tui/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, readonly string[]> = Object.fromEntries(
Object.entries(SETTING_ENUMS).map(([key, values]) => [key.toLowerCase(), values]),
)
Expand All @@ -36,7 +38,7 @@ const MODE_EDITABLE_FIELD_PATHS = [
] as const

export const readGlobalSettings = async (): Promise<GlobalSettings> => {
const configPath = sharedGlobalSettingsPath()
const configPath = resolveConfigPath()
try {
const raw = await readFile(configPath, "utf-8")
const parsed = JSON.parse(raw)
Expand All @@ -50,7 +52,7 @@ export const readGlobalSettings = async (): Promise<GlobalSettings> => {
}

export const readSharedConfig = async (): Promise<Record<string, unknown> | null> => {
const configPath = sharedGlobalSettingsPath()
const configPath = resolveConfigPath()
try {
const raw = await readFile(configPath, "utf-8")
const parsed = JSON.parse(raw)
Expand All @@ -67,7 +69,7 @@ export const readSharedConfig = async (): Promise<Record<string, unknown> | null
}

export const writeSharedConfig = async (settings: Record<string, unknown>) => {
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
Expand Down Expand Up @@ -100,7 +102,7 @@ export const resolveSharedConfigField = (config: Record<string, unknown>, 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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<string, unknown>)
Expand All @@ -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}`,
Expand Down Expand Up @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion src/v2/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
42 changes: 42 additions & 0 deletions tests/tui-behavior.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
})