refactor(cli): manage environment variables with effect config

This commit is contained in:
Dax Raad 2026-07-03 13:37:12 -04:00
parent c22793d5f6
commit 24d26365e6
3 changed files with 32 additions and 6 deletions

View file

@ -1,8 +1,9 @@
import { NodeFileSystem } from "@effect/platform-node"
import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime"
import { Effect, Option } from "effect"
import { Effect, Option, Redacted } from "effect"
import { Service } from "@opencode-ai/client/effect"
import { Env } from "../../env"
import { ServiceConfig } from "../../services/service-config"
import { Standalone } from "../../services/standalone"
import { Updater } from "../../services/updater"
@ -18,10 +19,12 @@ export default Runtime.handler(Commands, (input) =>
return yield* Effect.fail(new Error("--server and --standalone cannot be combined"))
const transport = yield* Effect.gen(function* () {
if (server !== undefined) {
const password = process.env["OPENCODE_PASSWORD"]
const password = Option.getOrUndefined(yield* Env.password)
const explicit = {
url: server,
headers: password ? { authorization: "Basic " + btoa("opencode:" + password) } : undefined,
headers: password
? { authorization: "Basic " + btoa("opencode:" + Redacted.value(password)) }
: undefined,
} satisfies Service.Transport
// Fail loudly before entering the TUI: an explicit server that is
// unreachable or rejects auth should not present as reconnect churn.

View file

@ -4,7 +4,7 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
import { Global } from "@opencode-ai/core/global"
import { Context, FileSystem, Layer, Option, Schedule, Schema } from "effect"
import { Context, FileSystem, Layer, Option, Redacted, Schedule, Schema } from "effect"
import * as Effect from "effect/Effect"
import { HttpRouter, HttpServer } from "effect/unstable/http"
import { createServer } from "node:http"
@ -15,6 +15,7 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime"
import { Service } from "@opencode-ai/client/effect"
import { Env } from "../../env"
import { ServiceConfig } from "../../services/service-config"
import { Updater } from "../../services/updater"
import { randomBytes, randomUUID } from "crypto"
@ -26,12 +27,16 @@ export default Runtime.handler(
if (input.service) yield* Effect.sync(() => process.chdir(Global.Path.home))
return yield* Effect.scoped(
Effect.gen(function* () {
const standalonePassword = process.env.OPENCODE_SERVER_PASSWORD
const standalonePassword = Option.getOrUndefined(yield* Env.serverPassword)
// Keep the lease credential out of the environment inherited by any
// process this server spawns.
if (input.stdio) delete process.env.OPENCODE_SERVER_PASSWORD
const config = input.service ? yield* ServiceConfig.read() : {}
const password = input.service
? yield* ServiceConfig.password()
: standalonePassword || randomBytes(32).toString("base64url")
: standalonePassword
? Redacted.value(standalonePassword)
: randomBytes(32).toString("base64url")
if (!password) return yield* Effect.fail(new Error("Missing server password"))
const hostname = Option.getOrUndefined(input.hostname) ?? config.hostname ?? "127.0.0.1"
const port = Option.isSome(input.port)

18
packages/cli/src/env.ts Normal file
View file

@ -0,0 +1,18 @@
import { Config } from "effect"
// Every environment variable the CLI reads, in one place. Consumers yield
// these instead of touching process.env so the full surface stays visible,
// typed, and redacted where secret.
// Client-side password for an explicit --server target. The legacy name is
// still honored; it also remains the variable a standalone child inherits.
export const password = Config.redacted("OPENCODE_PASSWORD").pipe(
Config.orElse(() => Config.redacted("OPENCODE_SERVER_PASSWORD")),
Config.option,
)
// Server-side lease password: set by the standalone spawner for its child,
// or preset for a manually managed `opencode serve`.
export const serverPassword = Config.redacted("OPENCODE_SERVER_PASSWORD").pipe(Config.option)
export * as Env from "./env"