Skip to content

Commit 65e1186

Browse files
committed
wip(app): global config
1 parent 8faa2ff commit 65e1186

8 files changed

Lines changed: 146 additions & 106 deletions

File tree

packages/app/src/components/dialog-custom-provider.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { showToast } from "@opencode-ai/ui/toast"
88
import { For } from "solid-js"
99
import { createStore, produce } from "solid-js/store"
1010
import { Link } from "@/components/link"
11+
import { useGlobalSDK } from "@/context/global-sdk"
1112
import { useGlobalSync } from "@/context/global-sync"
1213
import { useLanguage } from "@/context/language"
1314
import { DialogSelectProvider } from "./dialog-select-provider"
@@ -22,6 +23,7 @@ type Props = {
2223
export function DialogCustomProvider(props: Props) {
2324
const dialog = useDialog()
2425
const globalSync = useGlobalSync()
26+
const globalSDK = useGlobalSDK()
2527
const language = useLanguage()
2628

2729
const [form, setForm] = createStore({
@@ -118,6 +120,9 @@ export function DialogCustomProvider(props: Props) {
118120
const baseURL = form.baseURL.trim()
119121
const apiKey = form.apiKey.trim()
120122

123+
const env = apiKey.match(/^\{env:([^}]+)\}$/)?.[1]?.trim()
124+
const key = apiKey && !env ? apiKey : undefined
125+
121126
const idError = !providerID
122127
? "Provider ID is required"
123128
: !PROVIDER_ID.test(providerID)
@@ -196,16 +201,17 @@ export function DialogCustomProvider(props: Props) {
196201

197202
const options = {
198203
baseURL,
199-
...(apiKey ? { apiKey } : {}),
200204
...(Object.keys(headers).length ? { headers } : {}),
201205
}
202206

203207
return {
204208
providerID,
205209
name,
210+
key,
206211
config: {
207212
npm: OPENAI_COMPATIBLE,
208213
name,
214+
...(env ? { env: [env] } : {}),
209215
options,
210216
models,
211217
},
@@ -224,8 +230,20 @@ export function DialogCustomProvider(props: Props) {
224230
const disabledProviders = globalSync.data.config.disabled_providers ?? []
225231
const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
226232

227-
globalSync
228-
.updateConfig({ provider: { [result.providerID]: result.config }, disabled_providers: nextDisabled })
233+
const auth = result.key
234+
? globalSDK.client.auth.set({
235+
providerID: result.providerID,
236+
auth: {
237+
type: "api",
238+
key: result.key,
239+
},
240+
})
241+
: Promise.resolve()
242+
243+
auth
244+
.then(() =>
245+
globalSync.updateConfig({ provider: { [result.providerID]: result.config }, disabled_providers: nextDisabled }),
246+
)
229247
.then(() => {
230248
dialog.close()
231249
showToast({
@@ -301,7 +319,7 @@ export function DialogCustomProvider(props: Props) {
301319
/>
302320
<TextField
303321
label="API key"
304-
placeholder="{env:MYPROVIDER_API_KEY}"
322+
placeholder="API key"
305323
description="Optional. Leave empty if you manage auth via headers."
306324
value={form.apiKey}
307325
onChange={setForm.bind(null, "apiKey")}

packages/opencode/src/auth/index.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import path from "path"
22
import { Global } from "../global"
3-
import fs from "fs/promises"
43
import z from "zod"
54

65
export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
@@ -59,15 +58,13 @@ export namespace Auth {
5958
export async function set(key: string, info: Info) {
6059
const file = Bun.file(filepath)
6160
const data = await all()
62-
await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2))
63-
await fs.chmod(file.name!, 0o600)
61+
await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2), { mode: 0o600 })
6462
}
6563

6664
export async function remove(key: string) {
6765
const file = Bun.file(filepath)
6866
const data = await all()
6967
delete data[key]
70-
await Bun.write(file, JSON.stringify(data, null, 2))
71-
await fs.chmod(file.name!, 0o600)
68+
await Bun.write(file, JSON.stringify(data, null, 2), { mode: 0o600 })
7269
}
7370
}

packages/opencode/src/config/config.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1341,24 +1341,35 @@ export namespace Config {
13411341
throw new JsonError({ path: filepath }, { cause: err })
13421342
})
13431343

1344-
if (!filepath.endsWith(".jsonc")) {
1345-
const existing = parseConfig(before, filepath)
1346-
await Bun.write(filepath, JSON.stringify(mergeDeep(existing, config), null, 2))
1347-
} else {
1348-
const next = patchJsonc(before, config)
1349-
parseConfig(next, filepath)
1350-
await Bun.write(filepath, next)
1351-
}
1344+
const next = await (async () => {
1345+
if (!filepath.endsWith(".jsonc")) {
1346+
const existing = parseConfig(before, filepath)
1347+
const merged = mergeDeep(existing, config)
1348+
await Bun.write(filepath, JSON.stringify(merged, null, 2))
1349+
return merged
1350+
}
1351+
1352+
const updated = patchJsonc(before, config)
1353+
const merged = parseConfig(updated, filepath)
1354+
await Bun.write(filepath, updated)
1355+
return merged
1356+
})()
13521357

13531358
global.reset()
1354-
await Instance.disposeAll()
1355-
GlobalBus.emit("event", {
1356-
directory: "global",
1357-
payload: {
1358-
type: Event.Disposed.type,
1359-
properties: {},
1360-
},
1361-
})
1359+
1360+
void Instance.disposeAll()
1361+
.catch(() => undefined)
1362+
.finally(() => {
1363+
GlobalBus.emit("event", {
1364+
directory: "global",
1365+
payload: {
1366+
type: Event.Disposed.type,
1367+
properties: {},
1368+
},
1369+
})
1370+
})
1371+
1372+
return next
13621373
}
13631374

13641375
export async function directories() {

packages/opencode/src/mcp/auth.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import path from "path"
2-
import fs from "fs/promises"
32
import z from "zod"
43
import { Global } from "../global"
54

@@ -65,16 +64,14 @@ export namespace McpAuth {
6564
if (serverUrl) {
6665
entry.serverUrl = serverUrl
6766
}
68-
await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2))
69-
await fs.chmod(file.name!, 0o600)
67+
await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2), { mode: 0o600 })
7068
}
7169

7270
export async function remove(mcpName: string): Promise<void> {
7371
const file = Bun.file(filepath)
7472
const data = await all()
7573
delete data[mcpName]
76-
await Bun.write(file, JSON.stringify(data, null, 2))
77-
await fs.chmod(file.name!, 0o600)
74+
await Bun.write(file, JSON.stringify(data, null, 2), { mode: 0o600 })
7875
}
7976

8077
export async function updateTokens(mcpName: string, tokens: Tokens, serverUrl?: string): Promise<void> {

packages/opencode/src/project/instance.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { State } from "./state"
55
import { iife } from "@/util/iife"
66
import { GlobalBus } from "@/bus/global"
77
import { Filesystem } from "@/util/filesystem"
8+
import { withTimeout } from "@/util/timeout"
89

910
interface Context {
1011
directory: string
@@ -14,6 +15,8 @@ interface Context {
1415
const context = Context.create<Context>("instance")
1516
const cache = new Map<string, Promise<Context>>()
1617

18+
const DISPOSE_TIMEOUT_MS = 10_000
19+
1720
export const Instance = {
1821
async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
1922
let existing = cache.get(input.directory)
@@ -78,13 +81,18 @@ export const Instance = {
7881
},
7982
async disposeAll() {
8083
Log.Default.info("disposing all instances")
81-
for (const [_key, value] of cache) {
82-
const awaited = await value.catch(() => {})
83-
if (awaited) {
84-
await context.provide(await value, async () => {
85-
await Instance.dispose()
86-
})
84+
for (const [key, value] of cache) {
85+
const ctx = await withTimeout(value, DISPOSE_TIMEOUT_MS).catch((error) => {
86+
Log.Default.warn("instance dispose timed out", { key, error })
87+
return undefined
88+
})
89+
if (!ctx) {
90+
cache.delete(key)
91+
continue
8792
}
93+
await context.provide(ctx, async () => {
94+
await Instance.dispose()
95+
})
8896
}
8997
cache.clear()
9098
},

packages/opencode/src/project/state.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Log } from "@/util/log"
2+
import { withTimeout } from "@/util/timeout"
23

34
export namespace State {
45
interface Entry {
@@ -7,6 +8,7 @@ export namespace State {
78
}
89

910
const log = Log.create({ service: "state" })
11+
const DISPOSE_TIMEOUT_MS = 10_000
1012
const recordsByKey = new Map<string, Map<any, Entry>>()
1113

1214
export function create<S>(root: () => string, init: () => S, dispose?: (state: Awaited<S>) => Promise<void>) {
@@ -46,14 +48,21 @@ export namespace State {
4648
}, 10000).unref()
4749

4850
const tasks: Promise<void>[] = []
49-
for (const entry of entries.values()) {
51+
for (const [init, entry] of entries) {
5052
if (!entry.dispose) continue
5153

52-
const task = Promise.resolve(entry.state)
53-
.then((state) => entry.dispose!(state))
54-
.catch((error) => {
55-
log.error("Error while disposing state:", { error, key })
56-
})
54+
const label = typeof init === "function" ? init.name : String(init)
55+
56+
const task = withTimeout(
57+
Promise.resolve(entry.state).then((state) => entry.dispose!(state)),
58+
DISPOSE_TIMEOUT_MS,
59+
).catch((error) => {
60+
if (error instanceof Error && error.message.includes("Operation timed out")) {
61+
log.warn("state disposal timed out", { key, init: label })
62+
return
63+
}
64+
log.error("Error while disposing state:", { error, key, init: label })
65+
})
5766

5867
tasks.push(task)
5968
}

packages/opencode/src/server/routes/global.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@ export const GlobalRoutes = lazy(() =>
147147
validator("json", Config.Info),
148148
async (c) => {
149149
const config = c.req.valid("json")
150-
await Config.updateGlobal(config)
151-
return c.json(await Config.getGlobal())
150+
const next = await Config.updateGlobal(config)
151+
return c.json(next)
152152
},
153153
)
154154
.post(

0 commit comments

Comments
 (0)