From 75632666052d20a9a9eb256662068744b74e74f7 Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 24 Jun 2026 04:08:22 +0000 Subject: [PATCH] fix(opencode): restore v1 account config --- packages/opencode/src/config/config.ts | 48 +++++++++++++++++-- packages/opencode/test/config/config.test.ts | 49 +++++++++++++++++++- 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 57e2ceeebe49..7f568f492073 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -13,11 +13,12 @@ import { Env } from "../env" import { applyEdits, modify } from "jsonc-parser" import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version" import { existsSync } from "fs" +import { Account } from "@/account/account" import { isRecord } from "@/util/record" import type { ConsoleState } from "@opencode-ai/core/v1/config/console-state" import { FSUtil } from "@opencode-ai/core/fs-util" import { InstanceState } from "@/effect/instance-state" -import { Context, Duration, Effect, Exit, Fiber, Layer, Schema } from "effect" +import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { containsPath, type InstanceContext } from "../project/instance-context" @@ -176,6 +177,7 @@ export const layer = Layer.effect( Effect.gen(function* () { const fs = yield* FSUtil.Service const authSvc = yield* Auth.Service + const accountSvc = yield* Account.Service const env = yield* Env.Service const npmSvc = yield* Npm.Service const http = yield* HttpClient.HttpClient @@ -314,6 +316,7 @@ export const layer = Layer.effect( let result: Info = {} const authEnv: Record = {} + const consoleManagedProviders = new Set() let activeOrgName: string | undefined const pluginScopeForSource = Effect.fnUntraced(function* (source: string) { @@ -471,6 +474,44 @@ export const layer = Layer.effect( yield* Effect.logDebug("loaded custom config from OPENCODE_CONFIG_CONTENT") } + const activeAccount = Option.getOrUndefined( + yield* accountSvc.active().pipe(Effect.catch(() => Effect.succeed(Option.none()))), + ) + if (activeAccount?.active_org_id) { + const accountID = activeAccount.id + const orgID = activeAccount.active_org_id + const url = activeAccount.url + yield* Effect.gen(function* () { + const [configOpt, tokenOpt] = yield* Effect.all( + [accountSvc.config(accountID, orgID), accountSvc.token(accountID)], + { concurrency: 2 }, + ) + if (Option.isSome(tokenOpt)) { + process.env["OPENCODE_CONSOLE_TOKEN"] = tokenOpt.value + yield* env.set("OPENCODE_CONSOLE_TOKEN", tokenOpt.value) + } + + if (Option.isSome(configOpt)) { + const source = `${url}/api/config` + const next = yield* loadConfig(JSON.stringify(configOpt.value), { + dir: path.dirname(source), + source, + }) + for (const providerID of Object.keys(next.provider ?? {})) { + consoleManagedProviders.add(providerID) + } + yield* merge(source, next, "global") + } + }).pipe( + Effect.withSpan("Config.loadActiveOrgConfig"), + Effect.catch((err) => + Effect.logDebug("failed to fetch remote account config", { + error: err instanceof Error ? err.message : String(err), + }), + ), + ) + } + const managedDir = ConfigManaged.managedConfigDir() if (existsSync(managedDir)) { for (const file of ["opencode.json", "opencode.jsonc"]) { @@ -546,7 +587,7 @@ export const layer = Layer.effect( directories, deps, consoleState: { - consoleManagedProviders: [], + consoleManagedProviders: Array.from(consoleManagedProviders), activeOrgName, switchableOrgCount: 0, }, @@ -635,10 +676,11 @@ export const defaultLayer = layer.pipe( Layer.provide(FSUtil.defaultLayer), Layer.provide(Env.defaultLayer), Layer.provide(Auth.defaultLayer), + Layer.provide(Account.defaultLayer), Layer.provide(Npm.defaultLayer), Layer.provide(FetchHttpClient.layer), ) -export const node = LayerNode.make(layer, [FSUtil.node, Auth.node, Env.node, Npm.node, httpClient]) +export const node = LayerNode.make(layer, [FSUtil.node, Auth.node, Account.node, Env.node, Npm.node, httpClient]) export * as Config from "./config" diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index ed0377ed5729..02ace5366880 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1,6 +1,6 @@ import { test, expect, describe, afterEach, beforeEach, spyOn } from "bun:test" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" -import { Cause, Effect, Exit, Layer } from "effect" +import { Cause, Effect, Exit, Layer, Option } from "effect" import { NamedError } from "@opencode-ai/core/util/error" import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http" import { NodeFileSystem, NodePath } from "@effect/platform-node" @@ -13,6 +13,7 @@ import { InstanceRef } from "../../src/effect/instance-ref" import type { InstanceContext } from "../../src/project/instance-context" import { Auth } from "../../src/auth" import { Account } from "../../src/account/account" +import { AccessToken, AccountID, OrgID } from "../../src/account/schema" import { FSUtil } from "@opencode-ai/core/fs-util" import { Env } from "../../src/env" import { @@ -140,6 +141,7 @@ const clear = (wait = false) => Effect.runPromise(clearEffect(wait)) // Get managed config directory from environment (set in preload.ts) const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR! const originalTestToken = process.env.TEST_TOKEN +const originalConsoleToken = process.env.OPENCODE_CONSOLE_TOKEN beforeEach(async () => { await clear(true) @@ -149,6 +151,8 @@ afterEach(async () => { await fs.rm(managedConfigDir, { force: true, recursive: true }).catch(() => {}) if (originalTestToken === undefined) delete process.env.TEST_TOKEN else process.env.TEST_TOKEN = originalTestToken + if (originalConsoleToken === undefined) delete process.env.OPENCODE_CONSOLE_TOKEN + else process.env.OPENCODE_CONSOLE_TOKEN = originalConsoleToken await clear(true) }) @@ -560,6 +564,49 @@ it.instance("handles file inclusion with replacement tokens", () => }), ) +const accountTokenIt = configIt({ + account: Layer.mock(Account.Service)({ + active: () => + Effect.succeed( + Option.some({ + id: AccountID.make("account-1"), + email: "user@example.com", + url: "https://control.example.com", + active_org_id: OrgID.make("org-1"), + }), + ), + activeOrg: () => + Effect.succeed( + Option.some({ + account: { + id: AccountID.make("account-1"), + email: "user@example.com", + url: "https://control.example.com", + active_org_id: OrgID.make("org-1"), + }, + org: { + id: OrgID.make("org-1"), + name: "Example Org", + }, + }), + ), + config: () => + Effect.succeed( + Option.some({ + provider: { opencode: { options: { apiKey: "{env:OPENCODE_CONSOLE_TOKEN}" } } }, + }), + ), + token: () => Effect.succeed(Option.some(AccessToken.make("st_test_token"))), + }), +}) + +accountTokenIt.instance("resolves env templates in account config with account token", () => + Effect.gen(function* () { + const config = yield* Config.use.get() + expect(config.provider?.["opencode"]?.options?.apiKey).toBe("st_test_token") + }), +) + it.instance("validates config schema and throws on invalid fields", () => Effect.gen(function* () { const test = yield* TestInstance