mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-24 22:14:37 +00:00
core: effectify Env service
This commit is contained in:
parent
e8471256f2
commit
e74c99320f
7 changed files with 713 additions and 669 deletions
|
|
@ -1161,13 +1161,15 @@ export namespace Config {
|
|||
}),
|
||||
)
|
||||
|
||||
export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Auth.Service | Account.Service> =
|
||||
type Reqs = Env.Service
|
||||
export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Auth.Service | Account.Service | Reqs> =
|
||||
Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const authSvc = yield* Auth.Service
|
||||
const accountSvc = yield* Account.Service
|
||||
const env = yield* Env.Service
|
||||
|
||||
const readConfigFile = Effect.fnUntraced(function* (filepath: string) {
|
||||
return yield* fs.readFileString(filepath).pipe(
|
||||
|
|
@ -1482,7 +1484,7 @@ export namespace Config {
|
|||
)
|
||||
if (Option.isSome(tokenOpt)) {
|
||||
process.env["OPENCODE_CONSOLE_TOKEN"] = tokenOpt.value
|
||||
Env.set("OPENCODE_CONSOLE_TOKEN", tokenOpt.value)
|
||||
yield* env.set("OPENCODE_CONSOLE_TOKEN", tokenOpt.value)
|
||||
}
|
||||
|
||||
activeOrgName = activeOrg.org.name
|
||||
|
|
@ -1659,5 +1661,6 @@ export namespace Config {
|
|||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Auth.defaultLayer),
|
||||
Layer.provide(Account.defaultLayer),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
78
packages/opencode/src/env/index.ts
vendored
78
packages/opencode/src/env/index.ts
vendored
|
|
@ -1,28 +1,64 @@
|
|||
import { Instance } from "../project/instance"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
|
||||
export namespace Env {
|
||||
const state = Instance.state(() => {
|
||||
// Create a shallow copy to isolate environment per instance
|
||||
// Prevents parallel tests from interfering with each other's env vars
|
||||
return { ...process.env } as Record<string, string | undefined>
|
||||
type State = Record<string, string | undefined>
|
||||
|
||||
export interface Interface {
|
||||
readonly get: (key: string) => Effect.Effect<string | undefined>
|
||||
readonly all: () => Effect.Effect<State>
|
||||
readonly set: (key: string, value: string) => Effect.Effect<void>
|
||||
readonly remove: (key: string) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Env") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* InstanceState.make<State>(
|
||||
Effect.fn("Env.state")(() =>
|
||||
Effect.succeed(
|
||||
// Create a shallow copy to isolate environment per instance
|
||||
// Prevents parallel tests from interfering with each other's env vars
|
||||
{ ...process.env } as State,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const get = Effect.fn("Env.get")((key: string) => InstanceState.use(state, (env) => env[key]))
|
||||
|
||||
const all = Effect.fn("Env.all")(() => InstanceState.get(state))
|
||||
|
||||
const set = Effect.fn("Env.set")(function* (key: string, value: string) {
|
||||
const env = yield* InstanceState.get(state)
|
||||
env[key] = value
|
||||
})
|
||||
|
||||
const remove = Effect.fn("Env.remove")(function* (key: string) {
|
||||
const env = yield* InstanceState.get(state)
|
||||
delete env[key]
|
||||
})
|
||||
|
||||
return Service.of({ get, all, set, remove })
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer
|
||||
|
||||
export const get = Effect.fn("Env.get")(function* (key: string) {
|
||||
return yield* (yield* Service).get(key)
|
||||
})
|
||||
|
||||
export function get(key: string) {
|
||||
const env = state()
|
||||
return env[key]
|
||||
}
|
||||
export const all = Effect.fn("Env.all")(function* () {
|
||||
return yield* (yield* Service).all()
|
||||
})
|
||||
|
||||
export function all() {
|
||||
return state()
|
||||
}
|
||||
export const set = Effect.fn("Env.set")(function* (key: string, value: string) {
|
||||
yield* (yield* Service).set(key, value)
|
||||
})
|
||||
|
||||
export function set(key: string, value: string) {
|
||||
const env = state()
|
||||
env[key] = value
|
||||
}
|
||||
|
||||
export function remove(key: string) {
|
||||
const env = state()
|
||||
delete env[key]
|
||||
}
|
||||
export const remove = Effect.fn("Env.remove")(function* (key: string) {
|
||||
yield* (yield* Service).remove(key)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -95,6 +95,7 @@ export namespace ToolRegistry {
|
|||
| Ripgrep.Service
|
||||
| Format.Service
|
||||
| Truncate.Service
|
||||
| Env.Service
|
||||
> = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
|
|
@ -103,6 +104,7 @@ export namespace ToolRegistry {
|
|||
const agents = yield* Agent.Service
|
||||
const skill = yield* Skill.Service
|
||||
const truncate = yield* Truncate.Service
|
||||
const env = yield* Env.Service
|
||||
|
||||
const invalid = yield* InvalidTool
|
||||
const task = yield* TaskTool
|
||||
|
|
@ -272,13 +274,14 @@ export namespace ToolRegistry {
|
|||
})
|
||||
|
||||
const tools: Interface["tools"] = Effect.fn("ToolRegistry.tools")(function* (input) {
|
||||
const e2e = !!(yield* env.get("OPENCODE_E2E_LLM_URL"))
|
||||
const filtered = (yield* all()).filter((tool) => {
|
||||
if (tool.id === CodeSearchTool.id || tool.id === WebSearchTool.id) {
|
||||
return input.providerID === ProviderID.opencode || Flag.OPENCODE_ENABLE_EXA
|
||||
}
|
||||
|
||||
const usePatch =
|
||||
!!Env.get("OPENCODE_E2E_LLM_URL") ||
|
||||
e2e ||
|
||||
(input.modelID.includes("gpt-") && !input.modelID.includes("oss") && !input.modelID.includes("gpt-4"))
|
||||
if (tool.id === ApplyPatchTool.id) return usePatch
|
||||
if (tool.id === EditTool.id || tool.id === WriteTool.id) return !usePatch
|
||||
|
|
@ -342,6 +345,7 @@ export namespace ToolRegistry {
|
|||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||
Layer.provide(Ripgrep.defaultLayer),
|
||||
Layer.provide(Truncate.defaultLayer),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { test, expect, describe, mock, afterEach, beforeEach, spyOn } from "bun:
|
|||
import { Deferred, Effect, Fiber, Layer, Option } from "effect"
|
||||
import { NodeFileSystem, NodePath } from "@effect/platform-node"
|
||||
import { Config } from "../../src/config/config"
|
||||
import { Env } from "../../src/env"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Auth } from "../../src/auth"
|
||||
import { AccessToken, Account, AccountID, OrgID } from "../../src/account"
|
||||
|
|
@ -37,6 +38,7 @@ const layer = Config.layer.pipe(
|
|||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(emptyAuth),
|
||||
Layer.provide(emptyAccount),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provideMerge(infra),
|
||||
)
|
||||
|
||||
|
|
@ -334,6 +336,7 @@ test("resolves env templates in account config with account token", async () =>
|
|||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(emptyAuth),
|
||||
Layer.provide(fakeAccount),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provideMerge(infra),
|
||||
)
|
||||
|
||||
|
|
@ -1826,6 +1829,7 @@ test("project config overrides remote well-known config", async () => {
|
|||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(fakeAuth),
|
||||
Layer.provide(emptyAccount),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provideMerge(infra),
|
||||
)
|
||||
|
||||
|
|
@ -1881,6 +1885,7 @@ test("wellknown URL with trailing slash is normalized", async () => {
|
|||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(fakeAuth),
|
||||
Layer.provide(emptyAccount),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provideMerge(infra),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Agent as AgentSvc } from "../../src/agent/agent"
|
|||
import { Bus } from "../../src/bus"
|
||||
import { Command } from "../../src/command"
|
||||
import { Config } from "../../src/config/config"
|
||||
import { Env } from "../../src/env"
|
||||
import { FileTime } from "../../src/file/time"
|
||||
import { LSP } from "../../src/lsp"
|
||||
import { MCP } from "../../src/mcp"
|
||||
|
|
@ -183,6 +184,7 @@ function makeHttp() {
|
|||
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
|
||||
const registry = ToolRegistry.layer.pipe(
|
||||
Layer.provide(Skill.defaultLayer),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||
Layer.provide(Ripgrep.defaultLayer),
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import { Agent as AgentSvc } from "../../src/agent/agent"
|
|||
import { Bus } from "../../src/bus"
|
||||
import { Command } from "../../src/command"
|
||||
import { Config } from "../../src/config/config"
|
||||
import { Env } from "../../src/env"
|
||||
import { FileTime } from "../../src/file/time"
|
||||
import { LSP } from "../../src/lsp"
|
||||
import { MCP } from "../../src/mcp"
|
||||
|
|
@ -137,6 +138,7 @@ function makeHttp() {
|
|||
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
|
||||
const registry = ToolRegistry.layer.pipe(
|
||||
Layer.provide(Skill.defaultLayer),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||
Layer.provide(Ripgrep.defaultLayer),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue