import { Config, Effect, Redacted } 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. // The opencode server password: sent by clients connecting to an explicit // --server, and adopted by a manually run or standalone server. The legacy // OPENCODE_SERVER_PASSWORD name is still honored. OPENCODE_PASSWORD_FILE // follows the *_PASSWORD_FILE convention: a path to a file holding the // secret, not a secret itself. A pointer whose file is missing, unreadable, // or empty fails rather than silently degrading to the generated password. export const password = Effect.gen(function* () { const direct = yield* Config.redacted("OPENCODE_PASSWORD").pipe( Config.orElse(() => Config.redacted("OPENCODE_SERVER_PASSWORD")), Config.withDefault(undefined), ) if (direct !== undefined) return direct const file = yield* Config.string("OPENCODE_PASSWORD_FILE").pipe(Config.withDefault(undefined)) if (file === undefined) return undefined return yield* filePassword(file, "OPENCODE_PASSWORD_FILE") }) // Reads the secret a password-file pointer names. The environment chain and // the managed service config share this trim-and-fail-loud contract. export function filePassword(file: string, source: string) { return Effect.gen(function* () { // Trim: a trailing newline in a secret file is not part of the password. const secret = (yield* Effect.tryPromise({ try: () => Bun.file(file).text(), catch: (cause) => new Error(`${source} is set but ${file} cannot be read`, { cause }), })).trim() if (!secret) return yield* Effect.fail(new Error(`${source} is set but ${file} is empty`)) return Redacted.make(secret) }) } export function session() { return Object.fromEntries( Object.entries(process.env).filter( (entry): entry is [string, string] => entry[1] !== undefined && entry[0] !== "OPENCODE_PASSWORD" && entry[0] !== "OPENCODE_SERVER_PASSWORD", ), ) } export * as Env from "./env"