refactor(cli): manage environment variables with effect config
This commit is contained in:
parent
c22793d5f6
commit
24d26365e6
3 changed files with 32 additions and 6 deletions
|
|
@ -1,8 +1,9 @@
|
||||||
import { NodeFileSystem } from "@effect/platform-node"
|
import { NodeFileSystem } from "@effect/platform-node"
|
||||||
import { Commands } from "../commands"
|
import { Commands } from "../commands"
|
||||||
import { Runtime } from "../../framework/runtime"
|
import { Runtime } from "../../framework/runtime"
|
||||||
import { Effect, Option } from "effect"
|
import { Effect, Option, Redacted } from "effect"
|
||||||
import { Service } from "@opencode-ai/client/effect"
|
import { Service } from "@opencode-ai/client/effect"
|
||||||
|
import { Env } from "../../env"
|
||||||
import { ServiceConfig } from "../../services/service-config"
|
import { ServiceConfig } from "../../services/service-config"
|
||||||
import { Standalone } from "../../services/standalone"
|
import { Standalone } from "../../services/standalone"
|
||||||
import { Updater } from "../../services/updater"
|
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"))
|
return yield* Effect.fail(new Error("--server and --standalone cannot be combined"))
|
||||||
const transport = yield* Effect.gen(function* () {
|
const transport = yield* Effect.gen(function* () {
|
||||||
if (server !== undefined) {
|
if (server !== undefined) {
|
||||||
const password = process.env["OPENCODE_PASSWORD"]
|
const password = Option.getOrUndefined(yield* Env.password)
|
||||||
const explicit = {
|
const explicit = {
|
||||||
url: server,
|
url: server,
|
||||||
headers: password ? { authorization: "Basic " + btoa("opencode:" + password) } : undefined,
|
headers: password
|
||||||
|
? { authorization: "Basic " + btoa("opencode:" + Redacted.value(password)) }
|
||||||
|
: undefined,
|
||||||
} satisfies Service.Transport
|
} satisfies Service.Transport
|
||||||
// Fail loudly before entering the TUI: an explicit server that is
|
// Fail loudly before entering the TUI: an explicit server that is
|
||||||
// unreachable or rejects auth should not present as reconnect churn.
|
// unreachable or rejects auth should not present as reconnect churn.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
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 * as Effect from "effect/Effect"
|
||||||
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
||||||
import { createServer } from "node:http"
|
import { createServer } from "node:http"
|
||||||
|
|
@ -15,6 +15,7 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||||
import { Commands } from "../commands"
|
import { Commands } from "../commands"
|
||||||
import { Runtime } from "../../framework/runtime"
|
import { Runtime } from "../../framework/runtime"
|
||||||
import { Service } from "@opencode-ai/client/effect"
|
import { Service } from "@opencode-ai/client/effect"
|
||||||
|
import { Env } from "../../env"
|
||||||
import { ServiceConfig } from "../../services/service-config"
|
import { ServiceConfig } from "../../services/service-config"
|
||||||
import { Updater } from "../../services/updater"
|
import { Updater } from "../../services/updater"
|
||||||
import { randomBytes, randomUUID } from "crypto"
|
import { randomBytes, randomUUID } from "crypto"
|
||||||
|
|
@ -26,12 +27,16 @@ export default Runtime.handler(
|
||||||
if (input.service) yield* Effect.sync(() => process.chdir(Global.Path.home))
|
if (input.service) yield* Effect.sync(() => process.chdir(Global.Path.home))
|
||||||
return yield* Effect.scoped(
|
return yield* Effect.scoped(
|
||||||
Effect.gen(function* () {
|
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
|
if (input.stdio) delete process.env.OPENCODE_SERVER_PASSWORD
|
||||||
const config = input.service ? yield* ServiceConfig.read() : {}
|
const config = input.service ? yield* ServiceConfig.read() : {}
|
||||||
const password = input.service
|
const password = input.service
|
||||||
? yield* ServiceConfig.password()
|
? 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"))
|
if (!password) return yield* Effect.fail(new Error("Missing server password"))
|
||||||
const hostname = Option.getOrUndefined(input.hostname) ?? config.hostname ?? "127.0.0.1"
|
const hostname = Option.getOrUndefined(input.hostname) ?? config.hostname ?? "127.0.0.1"
|
||||||
const port = Option.isSome(input.port)
|
const port = Option.isSome(input.port)
|
||||||
|
|
|
||||||
18
packages/cli/src/env.ts
Normal file
18
packages/cli/src/env.ts
Normal 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"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue