refactor: isolate legacy flags

This commit is contained in:
Dax Raad
2026-07-20 21:42:02 -04:00
parent 407ac2d8aa
commit 43c08387f1
65 changed files with 282 additions and 147 deletions
+1
View File
@@ -72,6 +72,7 @@ Effect.logInfo("cli starting", {
Observability.layer({
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
headers: process.env.OTEL_EXPORTER_OTLP_HEADERS,
client: process.env.OPENCODE_CLIENT ?? "cli",
}),
),
Effect.provide(NodeServices.layer),
+1 -2
View File
@@ -1,6 +1,5 @@
import type { MiniFrontendInput } from "@opencode-ai/tui/mini"
import { createModelPreferenceRepository } from "@opencode-ai/tui/model-preference"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Global } from "@opencode-ai/core/global"
import fs from "node:fs"
import { readFile } from "node:fs/promises"
@@ -165,7 +164,7 @@ export function createMiniHost(input: {
sigusr2: signal("SIGUSR2"),
},
startup: {
showTiming: Flag.OPENCODE_SHOW_TTFD,
showTiming: ["1", "true"].includes(process.env.OPENCODE_SHOW_TTFD?.toLowerCase() ?? ""),
now: () => performance.now(),
},
diagnostics: {
+4
View File
@@ -69,9 +69,11 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
const instanceID = randomUUID()
const server = yield* start(
{
client: process.env.OPENCODE_CLIENT ?? "cli",
hostname,
port,
password,
simulation: truthy(process.env.OPENCODE_SIMULATE),
database: {
path: process.env.OPENCODE_DB,
},
@@ -89,6 +91,8 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
project: !truthy(
process.env.OPENCODE_CONFIG_PROJECT_DISABLE ?? process.env.OPENCODE_DISABLE_PROJECT_CONFIG,
),
file: process.env.OPENCODE_CONFIG,
content: process.env.OPENCODE_CONFIG_CONTENT,
},
windows: {
gitbash: process.env.OPENCODE_GIT_BASH_PATH,
+4 -2
View File
@@ -1,5 +1,4 @@
import { Global } from "@opencode-ai/core/global"
import { Flag } from "@opencode-ai/core/flag/flag"
import { AppProcess } from "@opencode-ai/core/process"
import {
InstallationChannel,
@@ -138,7 +137,10 @@ export const layer = Layer.effect(
})
const check = Effect.fn("cli.updater.check")(function* () {
if (InstallationLocal || Flag.OPENCODE_DISABLE_AUTOUPDATE)
if (
InstallationLocal ||
["1", "true"].includes(process.env.OPENCODE_DISABLE_AUTOUPDATE?.toLowerCase() ?? "")
)
return yield* Effect.logInfo("update check skipped", {
reason: InstallationLocal ? "local-install" : "disabled",
version: InstallationVersion,
+27
View File
@@ -155,6 +155,8 @@ export interface Interface {
export const Options = Schema.Struct({
project: Schema.optional(Schema.Boolean),
file: Schema.optional(Schema.String),
content: Schema.optional(Schema.String),
})
export type Options = typeof Options.Type
@@ -293,14 +295,39 @@ export const layer = (options?: Options) => Layer.effect(
Effect.map((entries) => entries.flat()),
)
const file = options?.file
const explicit = file
? yield* loadFile(path.resolve(file)).pipe(
Effect.map((config) => [
...(config ? [config] : []),
new File({ type: "file", path: AbsolutePath.make(path.resolve(file)) }),
]),
Effect.orDie,
)
: []
const content = options?.content
? yield* ConfigVariable.substitute({
type: "virtual",
source: "OPENCODE_CONFIG_CONTENT",
dir: location.directory,
text: options.content,
}).pipe(
Effect.map(parseInfo),
Effect.map((info) => (info ? [new Document({ type: "document", info })] : [])),
Effect.orDie,
)
: []
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
return [
...claude,
...agents,
...(supplementary[0] ?? []),
...explicit,
...direct,
...supplementary.slice(1).flat(),
...(yield* loadWellknown().pipe(Effect.orDie)),
...content,
]
})
+3 -4
View File
@@ -4,7 +4,6 @@ import { HttpClient, HttpClientRequest } from "effect/unstable/http"
import { ModelsDev } from "@opencode-ai/schema/models-dev"
import { Money } from "@opencode-ai/schema/money"
import { Global } from "./global"
import { Flag } from "./flag/flag"
import { Flock } from "./util/flock"
import { Hash } from "./util/hash"
import { FSUtil } from "./fs-util"
@@ -18,8 +17,6 @@ import { ProviderV2 } from "./provider"
export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"])
export type CatalogModelStatus = typeof CatalogModelStatus.Type
const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}`
type Cost = {
readonly input: Money.USDPerMillionTokens
readonly output: Money.USDPerMillionTokens
@@ -534,6 +531,7 @@ export const Options = Schema.Struct({
url: Schema.optional(Schema.String),
file: Schema.optional(Schema.String),
fetch: Schema.optional(Schema.Boolean),
client: Schema.optional(Schema.String),
})
export type Options = typeof Options.Type
@@ -556,6 +554,7 @@ export const layer = (options?: Options) => Layer.effect(
const source = options?.url ?? "https://models.dev"
const fetch = options?.fetch ?? true
const userAgent = `opencode/${InstallationChannel}/${InstallationVersion}/${options?.client ?? "cli"}`
const filepath = path.join(
Global.Path.cache,
source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`,
@@ -572,7 +571,7 @@ export const layer = (options?: Options) => Layer.effect(
const fetchApi = Effect.fn("ModelsDev.fetchApi")(function* () {
return yield* HttpClientRequest.get(`${source}/api.json`).pipe(
HttpClientRequest.setHeader("User-Agent", USER_AGENT),
HttpClientRequest.setHeader("User-Agent", userAgent),
http.execute,
Effect.flatMap((res) => res.text),
Effect.timeout("10 seconds"),
+2
View File
@@ -11,6 +11,7 @@ import { Otlp } from "./observability/otlp"
export const Options = Schema.Struct({
endpoint: Schema.optional(Schema.String),
headers: Schema.optional(Schema.String),
client: Schema.optional(Schema.String),
})
export type Options = typeof Options.Type
@@ -18,6 +19,7 @@ export function layer(
options: Options = {
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
headers: process.env.OTEL_EXPORTER_OTLP_HEADERS,
client: process.env.OPENCODE_CLIENT ?? "cli",
},
) {
const local = Logger.layer(Logging.loggers(), { mergeWithExisting: false }).pipe(
+9 -5
View File
@@ -1,12 +1,12 @@
import { Layer } from "effect"
import { OtlpLogger } from "effect/unstable/observability"
import { Flag } from "../flag/flag"
import { InstallationChannel, InstallationVersion } from "../installation/version"
import { runID } from "./shared"
export interface Options {
readonly endpoint?: string
readonly headers?: string
readonly client?: string
}
function parseHeaders(value?: string) {
@@ -38,14 +38,14 @@ function resourceAttributes() {
}
}
export function resource(): { serviceName: string; serviceVersion: string; attributes: Record<string, string> } {
export function resource(client = "cli"): { serviceName: string; serviceVersion: string; attributes: Record<string, string> } {
return {
serviceName: "opencode",
serviceVersion: InstallationVersion,
attributes: {
...resourceAttributes(),
"deployment.environment.name": InstallationChannel,
"opencode.client": Flag.OPENCODE_CLIENT,
"opencode.client": client,
"opencode.run": runID,
"service.instance.id": runID,
},
@@ -55,7 +55,11 @@ export function resource(): { serviceName: string; serviceVersion: string; attri
export function loggers(options?: Options) {
if (!options?.endpoint) return []
return [
OtlpLogger.make({ url: `${options.endpoint}/v1/logs`, resource: resource(), headers: parseHeaders(options.headers) }),
OtlpLogger.make({
url: `${options.endpoint}/v1/logs`,
resource: resource(options.client),
headers: parseHeaders(options.headers),
}),
]
}
@@ -73,7 +77,7 @@ export async function tracingLayer(options?: Options) {
context.setGlobalContextManager(manager)
return NodeSdk.layer(() => ({
resource: resource(),
resource: resource(options.client),
spanProcessor: new SdkBase.BatchSpanProcessor(
new OTLP.OTLPTraceExporter({
url: `${options.endpoint}/v1/traces`,
+13 -8
View File
@@ -60,6 +60,7 @@ type Settings = {
}
type Dependencies = {
readonly headers?: SessionModelHeaders.Options
readonly events: EventV2.Interface
readonly llm: {
readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError>
@@ -258,7 +259,7 @@ const make = (dependencies: Dependencies) => {
.stream(
LLM.request({
model: plan.model,
http: { headers: SessionModelHeaders.make(plan.session) },
http: { headers: SessionModelHeaders.make(plan.session, dependencies.headers) },
messages: [Message.user(plan.prompt)],
tools: [],
}),
@@ -390,19 +391,23 @@ const make = (dependencies: Dependencies) => {
})
}
export const layer = Layer.effect(
export const layer = (options?: SessionModelHeaders.Options) => Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const llm = yield* LLMClient.Service
const config = yield* Config.Service
const models = yield* SessionRunnerModel.Service
return make({ events, llm, models, config: settings(yield* config.entries()) })
return make({ events, llm, models, config: settings(yield* config.entries()), headers: options })
}),
)
export const node = makeLocationNode({
service: Service,
layer,
deps: [EventV2.node, llmClient, Config.node, SessionRunnerModel.node],
})
export function configured(options?: SessionModelHeaders.Options) {
return makeLocationNode({
service: Service,
layer: layer(options),
deps: [EventV2.node, llmClient, Config.node, SessionRunnerModel.node],
})
}
export const node = configured()
+11 -7
View File
@@ -14,7 +14,7 @@ import { SessionRunnerModel } from "./runner/model"
import PROMPT_DEFAULT from "./runner/prompt/base.txt"
import { toLLMMessages } from "./runner/to-llm-message"
const layer = Layer.effect(
export const layer = (options?: SessionModelHeaders.Options) => Layer.effect(
SessionGenerate.Service,
Effect.gen(function* () {
const context = yield* SessionContext.Service
@@ -49,7 +49,7 @@ const layer = Layer.effect(
return (yield* llm.generate(
LLM.request({
model: model.model,
http: { headers: SessionModelHeaders.make(selection.session) },
http: { headers: SessionModelHeaders.make(selection.session, options) },
providerOptions: { openai: { promptCacheKey } },
system: contextEvent.system,
messages: contextEvent.messages,
@@ -62,8 +62,12 @@ const layer = Layer.effect(
}),
)
export const node = makeLocationNode({
service: SessionGenerate.Service,
layer,
deps: [SessionContext.node, Database.node, PluginHooks.node, SessionRunnerModel.node, llmClient],
})
export function configured(options?: SessionModelHeaders.Options) {
return makeLocationNode({
service: SessionGenerate.Service,
layer: layer(options),
deps: [SessionContext.node, Database.node, PluginHooks.node, SessionRunnerModel.node, llmClient],
})
}
export const node = configured()
+11 -3
View File
@@ -1,15 +1,23 @@
export * as SessionModelHeaders from "./model-headers"
import { Flag } from "../flag/flag"
import { InstallationVersion } from "../installation/version"
import { SessionSchema } from "./schema"
import { Schema } from "effect"
export const make = (session: Pick<SessionSchema.Info, "id" | "parentID" | "projectID">) => ({
export const Options = Schema.Struct({
client: Schema.optional(Schema.String),
})
export type Options = typeof Options.Type
export const make = (
session: Pick<SessionSchema.Info, "id" | "parentID" | "projectID">,
options?: Options,
) => ({
"x-session-affinity": session.id,
"X-Session-Id": session.id,
...(session.parentID ? { "x-parent-session-id": session.parentID } : {}),
"User-Agent": `opencode/${InstallationVersion}`,
"x-opencode-project": session.projectID,
"x-opencode-session": session.id,
"x-opencode-client": Flag.OPENCODE_CLIENT,
"x-opencode-client": options?.client ?? "cli",
})
+7 -7
View File
@@ -39,7 +39,7 @@ export interface Interface {
/** Location-scoped outbound model-request preparation. */
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionModelRequest") {}
const layer = Layer.effect(
export const layer = (options?: SessionModelHeaders.Options) => Layer.effect(
Service,
Effect.gen(function* () {
const hooks = yield* PluginHooks.Service
@@ -81,7 +81,7 @@ const layer = Layer.effect(
const request = LLM.request({
model,
http: {
headers: SessionModelHeaders.make(session),
headers: SessionModelHeaders.make(session, options),
},
providerOptions: { openai: { promptCacheKey } },
system: contextEvent.system,
@@ -112,8 +112,8 @@ const layer = Layer.effect(
}),
)
export const node = makeLocationNode({
service: Service,
layer,
deps: [PluginHooks.node, ToolRegistry.node],
})
export function configured(options?: SessionModelHeaders.Options) {
return makeLocationNode({ service: Service, layer: layer(options), deps: [PluginHooks.node, ToolRegistry.node] })
}
export const node = configured()
+13 -8
View File
@@ -17,6 +17,7 @@ import { SessionUsage } from "./usage"
const MAX_LENGTH = 100
type Dependencies = {
readonly headers?: SessionModelHeaders.Options
readonly events: EventV2.Interface
readonly llm: {
readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError>
@@ -66,7 +67,7 @@ const make = (dependencies: Dependencies) => {
.stream(
LLM.request({
model: resolved.model,
http: { headers: SessionModelHeaders.make(session) },
http: { headers: SessionModelHeaders.make(session, dependencies.headers) },
system: agent.system,
messages: [Message.user(firstUser.text)],
tools: [],
@@ -102,7 +103,7 @@ const make = (dependencies: Dependencies) => {
return { generateForFirstPrompt }
}
export const layer = Layer.effect(
export const layer = (options?: SessionModelHeaders.Options) => Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
@@ -110,15 +111,19 @@ export const layer = Layer.effect(
const agents = yield* AgentV2.Service
const models = yield* SessionRunnerModel.Service
const database = yield* Database.Service
const title = make({ events, llm, agents, models })
const title = make({ events, llm, agents, models, headers: options })
return Service.of({
generateForFirstPrompt: (session) => title.generateForFirstPrompt(database.db, session),
})
}),
)
export const node = makeLocationNode({
service: Service,
layer,
deps: [EventV2.node, llmClient, AgentV2.node, SessionRunnerModel.node, Database.node],
})
export function configured(options?: SessionModelHeaders.Options) {
return makeLocationNode({
service: Service,
layer: layer(options),
deps: [EventV2.node, llmClient, AgentV2.node, SessionRunnerModel.node, Database.node],
})
}
export const node = configured()
+7 -3
View File
@@ -5,7 +5,6 @@ import { ToolFailure } from "@opencode-ai/ai"
import { Context, Duration, Effect, Layer, Schema } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
import { makeLocationNode } from "../effect/app-node"
import { truthy } from "../flag/flag"
import { InstallationVersion } from "../installation/version"
import { PositiveInt } from "../schema"
import { PermissionV2 } from "../permission"
@@ -74,8 +73,13 @@ export const defaultConfigLayer = Layer.sync(ConfigService, () =>
process.env.OPENCODE_WEBSEARCH_PROVIDER === "exa" || process.env.OPENCODE_WEBSEARCH_PROVIDER === "parallel"
? process.env.OPENCODE_WEBSEARCH_PROVIDER
: undefined,
enableExa: truthy("OPENCODE_EXPERIMENTAL") || truthy("OPENCODE_ENABLE_EXA") || truthy("OPENCODE_EXPERIMENTAL_EXA"),
enableParallel: truthy("OPENCODE_ENABLE_PARALLEL") || truthy("OPENCODE_EXPERIMENTAL_PARALLEL"),
enableExa:
["1", "true"].includes(process.env.OPENCODE_EXPERIMENTAL?.toLowerCase() ?? "") ||
["1", "true"].includes(process.env.OPENCODE_ENABLE_EXA?.toLowerCase() ?? "") ||
["1", "true"].includes(process.env.OPENCODE_EXPERIMENTAL_EXA?.toLowerCase() ?? ""),
enableParallel:
["1", "true"].includes(process.env.OPENCODE_ENABLE_PARALLEL?.toLowerCase() ?? "") ||
["1", "true"].includes(process.env.OPENCODE_EXPERIMENTAL_PARALLEL?.toLowerCase() ?? ""),
exaApiKey: process.env.EXA_API_KEY,
parallelApiKey: process.env.PARALLEL_API_KEY,
}),
+46
View File
@@ -100,6 +100,52 @@ const provider = {
}
describe("Config", () => {
it.live("loads explicit file and content overrides in priority order", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
).pipe(
Effect.flatMap((tmp) => {
const global = path.join(tmp.path, "global")
const project = path.join(tmp.path, "project")
const explicit = path.join(tmp.path, "custom.json")
return Effect.promise(async () => {
await fs.mkdir(global, { recursive: true })
await fs.mkdir(project, { recursive: true })
await fs.writeFile(path.join(global, "opencode.json"), JSON.stringify({ shell: "global" }))
await fs.writeFile(explicit, JSON.stringify({ shell: "explicit" }))
await fs.writeFile(path.join(project, "opencode.json"), JSON.stringify({ shell: "project" }))
}).pipe(
Effect.andThen(
Effect.gen(function* () {
const config = yield* Config.Service
const entries = yield* config.entries()
expect(
entries.flatMap((entry) =>
entry.type === "document" && entry.info.shell ? [entry.info.shell] : [],
),
).toEqual(["global", "explicit", "project", "content"])
expect(Config.latest(entries, "shell")).toBe("content")
}).pipe(
Effect.provide(
testLayer(
project,
global,
project,
undefined,
undefined,
emptyCredentialNode,
emptyWellknownNode,
{ file: explicit, content: JSON.stringify({ shell: "content" }) },
),
),
),
),
)
}),
),
)
it.live("skips project configuration when project discovery is disabled", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
@@ -8,14 +8,11 @@ import { fileLogger } from "../../src/observability/logging"
import { resource } from "../../src/observability/otlp"
const otelResourceAttributes = process.env.OTEL_RESOURCE_ATTRIBUTES
const opencodeClient = process.env.OPENCODE_CLIENT
afterEach(() => {
if (otelResourceAttributes === undefined) delete process.env.OTEL_RESOURCE_ATTRIBUTES
else process.env.OTEL_RESOURCE_ATTRIBUTES = otelResourceAttributes
if (opencodeClient === undefined) delete process.env.OPENCODE_CLIENT
else process.env.OPENCODE_CLIENT = opencodeClient
})
describe("resource", () => {
@@ -39,16 +36,15 @@ describe("resource", () => {
})
test("keeps built-in attributes when env values conflict", () => {
process.env.OPENCODE_CLIENT = "cli"
process.env.OTEL_RESOURCE_ATTRIBUTES =
"opencode.client=web,service.instance.id=override,service.namespace=anomalyco"
expect(resource().attributes).toMatchObject({
expect(resource("cli").attributes).toMatchObject({
"opencode.client": "cli",
"service.namespace": "anomalyco",
})
expect(resource().attributes["service.instance.id"]).not.toBe("override")
expect(resource().attributes["opencode.run"]).toMatch(/^[0-9a-f]{8}$/)
expect(resource("cli").attributes["service.instance.id"]).not.toBe("override")
expect(resource("cli").attributes["opencode.run"]).toMatch(/^[0-9a-f]{8}$/)
})
})
+1 -1
View File
@@ -56,7 +56,7 @@ const locations = Layer.effect(
() =>
// The test only needs the compaction location service used by SessionV2.compact.
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion
SessionCompaction.layer.pipe(
SessionCompaction.layer().pipe(
Layer.provide(client),
Layer.provide(config),
Layer.provide(models),
@@ -18,7 +18,6 @@ import { SessionStore } from "@opencode-ai/core/session/store"
import { SessionV2 } from "@opencode-ai/core/session"
import { Project } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { Flag } from "@opencode-ai/core/flag/flag"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Money } from "@opencode-ai/schema/money"
@@ -190,7 +189,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
"User-Agent": `opencode/${InstallationVersion}`,
"x-opencode-project": Project.ID.global,
"x-opencode-session": sessionID,
"x-opencode-client": Flag.OPENCODE_CLIENT,
"x-opencode-client": "cli",
})
expect(requests[0]?.generation).toBeUndefined()
expect(JSON.stringify(requests[0]?.messages)).toContain("Manual compaction should include this short conversation.")
+1 -2
View File
@@ -22,7 +22,6 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { Flag } from "@opencode-ai/core/flag/flag"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { EventTable } from "@opencode-ai/core/event/sql"
@@ -3183,7 +3182,7 @@ describe("SessionRunnerLLM", () => {
"User-Agent": `opencode/${InstallationVersion}`,
"x-opencode-project": Project.ID.global,
"x-opencode-session": sessionID,
"x-opencode-client": Flag.OPENCODE_CLIENT,
"x-opencode-client": "cli",
})
}),
)
+1 -2
View File
@@ -17,7 +17,6 @@ import { SessionTitle } from "@opencode-ai/core/session/title"
import { SessionV2 } from "@opencode-ai/core/session"
import { Project } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { Flag } from "@opencode-ai/core/flag/flag"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Money } from "@opencode-ai/schema/money"
@@ -153,7 +152,7 @@ it.effect("generates a title from the sole user message and renames the session"
"User-Agent": `opencode/${InstallationVersion}`,
"x-opencode-project": Project.ID.global,
"x-opencode-session": sessionID,
"x-opencode-client": Flag.OPENCODE_CLIENT,
"x-opencode-client": "cli",
})
expect(JSON.stringify(requests[0]?.messages)).toContain("Help me debug the failing build")
const renamed = yield* store.get(sessionID)
+1 -1
View File
@@ -1,6 +1,6 @@
import { Global } from "@opencode-ai/core/global"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import os from "os"
import { Duration, Effect } from "effect"
import { effectCmd } from "../../effect-cmd"
+1 -1
View File
@@ -1,7 +1,7 @@
import { Effect } from "effect"
import { effectCmd } from "../effect-cmd"
import { withNetworkOptions, resolveNetworkOptions } from "../network"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
export const ServeCommand = effectCmd({
command: "serve",
+1 -1
View File
@@ -6,7 +6,7 @@ import { Session } from "@/session/session"
import { SessionID } from "../../session/schema"
import { UI } from "../ui"
import { Locale } from "@/util/locale"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { Filesystem } from "@/util/filesystem"
import { Process } from "@/util/process"
import { NotFoundError } from "@/storage/storage"
+1 -1
View File
@@ -2,7 +2,7 @@ import { Effect } from "effect"
import { UI } from "../ui"
import { effectCmd } from "../effect-cmd"
import { withNetworkOptions, resolveNetworkOptions } from "../network"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import open from "open"
import { networkInterfaces } from "os"
+1 -1
View File
@@ -1,6 +1,6 @@
import path from "path"
import { writeHeapSnapshot } from "node:v8"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { Global } from "@opencode-ai/core/global"
const MINUTE = 60_000
const LIMIT = 2 * 1024 * 1024 * 1024
+1 -1
View File
@@ -1,6 +1,6 @@
import { Config } from "@/config/config"
import { AppRuntime } from "@/effect/app-runtime"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { Installation } from "@/installation"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { GlobalBus } from "@/bus/global"
+1 -1
View File
@@ -7,7 +7,7 @@ import os from "os"
import { mergeDeep } from "remeda"
import { Global } from "@opencode-ai/core/global"
import fsNode from "fs/promises"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { Auth } from "../auth"
import { Env } from "../env"
import { applyEdits, modify } from "jsonc-parser"
+1 -1
View File
@@ -1,7 +1,7 @@
export * as ConfigPaths from "./paths"
import path from "path"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { Global } from "@opencode-ai/core/global"
import { unique } from "remeda"
import * as Effect from "effect/Effect"
+1 -1
View File
@@ -3,7 +3,7 @@ import { type ParseError as JsoncParseError, applyEdits, modify, parse as parseJ
import { unique } from "remeda"
import { Option, Schema } from "effect"
import { TuiConfig } from "@opencode-ai/tui/config/v1"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util/filesystem"
import * as ConfigPaths from "@/config/paths"
+1 -1
View File
@@ -9,7 +9,7 @@ import { ConfigParse } from "@/config/parse"
import * as ConfigPaths from "@/config/paths"
import { migrateTuiConfig } from "./tui-migrate"
import { resolveHostAttentionSoundPaths } from "./tui-host-attention"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { isRecord } from "@opencode-ai/tui/util/record"
import { Global } from "@opencode-ai/core/global"
import { FSUtil } from "@opencode-ai/core/fs-util"
@@ -1,5 +1,3 @@
import { Config } from "effect"
export function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
return value === "true" || value === "1"
@@ -17,30 +15,33 @@ export const Flag = {
OTEL_EXPORTER_OTLP_ENDPOINT: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"],
// V2: ServerOptions.observability.headers
OTEL_EXPORTER_OTLP_HEADERS: process.env["OTEL_EXPORTER_OTLP_HEADERS"],
OPENCODE_AUTO_HEAP_SNAPSHOT: truthy("OPENCODE_AUTO_HEAP_SNAPSHOT"),
// V2: ServerOptions.windows.gitbash
OPENCODE_GIT_BASH_PATH: process.env["OPENCODE_GIT_BASH_PATH"],
OPENCODE_CONFIG: process.env["OPENCODE_CONFIG"],
OPENCODE_CONFIG_CONTENT: process.env["OPENCODE_CONFIG_CONTENT"],
// V2: CLI updater environment adapter
OPENCODE_DISABLE_AUTOUPDATE: truthy("OPENCODE_DISABLE_AUTOUPDATE"),
OPENCODE_ALWAYS_NOTIFY_UPDATE: truthy("OPENCODE_ALWAYS_NOTIFY_UPDATE"),
OPENCODE_DISABLE_PRUNE: truthy("OPENCODE_DISABLE_PRUNE"),
// V2: TUI config terminal.title
OPENCODE_DISABLE_TERMINAL_TITLE: truthy("OPENCODE_DISABLE_TERMINAL_TITLE"),
// V2: CLI Mini environment adapter and TUI config debug.timing
OPENCODE_SHOW_TTFD: truthy("OPENCODE_SHOW_TTFD"),
OPENCODE_DISABLE_AUTOCOMPACT: truthy("OPENCODE_DISABLE_AUTOCOMPACT"),
// V2: ServerOptions.models.fetch
OPENCODE_DISABLE_MODELS_FETCH: truthy("OPENCODE_DISABLE_MODELS_FETCH"),
// V2: TUI config mouse
OPENCODE_DISABLE_MOUSE: truthy("OPENCODE_DISABLE_MOUSE"),
OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"],
// V2: CLI password environment adapter
OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"],
OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"],
// V2: ServerOptions.fs.fff
OPENCODE_DISABLE_FFF: fff === undefined ? process.platform === "win32" : truthy("OPENCODE_DISABLE_FFF"),
// V2: ServerOptions.fs.filewatcher
OPENCODE_DISABLE_FILEWATCHER: truthy("OPENCODE_DISABLE_FILEWATCHER"),
// Experimental
// V2: TUI config terminal.copy_on_select
OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT:
copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"),
// V2: ServerOptions.models.url
@@ -49,12 +50,8 @@ export const Flag = {
OPENCODE_MODELS_PATH: process.env["OPENCODE_MODELS_PATH"],
// V2: ServerOptions.database.path
OPENCODE_DB: process.env["OPENCODE_DB"],
OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"],
OPENCODE_EXPERIMENTAL_WORKSPACES: enabledByExperimental("OPENCODE_EXPERIMENTAL_WORKSPACES"),
// Evaluated at access time (not module load) because tests, the CLI, and
// external tooling set these env vars at runtime.
// V2: ServerOptions.config.project
get OPENCODE_DISABLE_PROJECT_CONFIG() {
return truthy("OPENCODE_DISABLE_PROJECT_CONFIG")
@@ -78,6 +75,7 @@ export const Flag = {
get OPENCODE_PLUGIN_META_FILE() {
return process.env["OPENCODE_PLUGIN_META_FILE"]
},
// V2: ServerOptions.client
get OPENCODE_CLIENT() {
return process.env["OPENCODE_CLIENT"] ?? "cli"
},
@@ -1,6 +1,6 @@
export * as InstallationDatabase from "./database"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { Global } from "@opencode-ai/core/global"
import { InstallationChannel } from "@opencode-ai/core/installation/version"
import { isAbsolute, join } from "node:path"
+1 -1
View File
@@ -1,7 +1,7 @@
import path from "path"
import { fileURLToPath } from "url"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util/filesystem"
import { Flock } from "@opencode-ai/core/util/flock"
+1 -1
View File
@@ -34,7 +34,7 @@ import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util/filesystem"
import { Process } from "@/util/process"
import { Flock } from "@opencode-ai/core/util/flock"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { internalTuiPlugins, type InternalTuiPlugin } from "./internal"
import type { HostPluginApi, HostSlots } from "@opencode-ai/tui/plugin/slots"
import { ConfigPlugin } from "@/config/plugin"
+1 -1
View File
@@ -5,7 +5,7 @@ import { ProjectDirectoryTable, ProjectTable } from "@opencode-ai/core/project/s
import { ProjectDirectories } from "@opencode-ai/core/project/directories"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { GlobalBus } from "@/bus/global"
import { which } from "@opencode-ai/core/util/which"
import { Command } from "@/command"
+1 -1
View File
@@ -1,7 +1,7 @@
export * as ServerAuth from "./auth"
import { ConfigService } from "@/effect/config-service"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { Config as EffectConfig, Context, Option, Redacted } from "effect"
export type Credentials = {
@@ -1,4 +1,4 @@
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { Database } from "@opencode-ai/core/database/database"
import { Effect } from "effect"
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
@@ -7,7 +7,7 @@ import { HttpApiProxy } from "./proxy"
import * as Fence from "@/server/shared/fence"
import { getWorkspaceRouteSessionID, isLocalWorkspaceRoute, workspaceProxyURL } from "@/server/shared/workspace-routing"
import { NotFoundError } from "@/storage/storage"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { Context, Data, Effect, Layer, Option, Schema } from "effect"
import { HttpClient, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpApiMiddleware } from "effect/unstable/httpapi"
+1 -1
View File
@@ -7,7 +7,7 @@ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/
import { Config } from "@/config/config"
import { InstanceState } from "@/effect/instance-state"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { withTransientReadRetry } from "@/util/effect-http-client"
import { Global } from "@opencode-ai/core/global"
+1 -1
View File
@@ -1,5 +1,5 @@
import type { WorkspaceV2 } from "@opencode-ai/core/workspace"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { Effect, Scope } from "effect"
/**
+1 -1
View File
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, test } from "bun:test"
import { Option, Redacted } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { ServerAuth } from "../../src/server/auth"
const original = {
@@ -1,5 +1,5 @@
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { describe, expect } from "bun:test"
import { Config, ConfigProvider, Effect, Layer } from "effect"
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
@@ -1,4 +1,4 @@
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { Effect } from "effect"
import path from "path"
@@ -1,4 +1,4 @@
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Cause, Duration, Effect, Layer, Scope } from "effect"
@@ -1,6 +1,6 @@
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { describe, expect } from "bun:test"
import { Config, Context, Effect, FileSystem, Layer, Path } from "effect"
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test"
import net from "node:net"
import path from "node:path"
import { pathToFileURL } from "node:url"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { Server } from "../../src/server/server"
import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
import { withTimeout } from "../../src/util/timeout"
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, mock, test } from "bun:test"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { withTimeout } from "../../src/util/timeout"
import { resetDatabase } from "../fixture/db"
import { disposeAllInstances } from "../fixture/fixture"
@@ -1,7 +1,7 @@
import { afterEach, describe, expect } from "bun:test"
import { Effect, Schema } from "effect"
import { OpenApi } from "effect/unstable/httpapi"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { Server } from "../../src/server/server"
import { SessionID } from "../../src/session/schema"
import { PublicApi } from "../../src/server/routes/instance/httpapi/public"
@@ -9,7 +9,7 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import { validateSession } from "../../src/cli/tui/validate-session"
import { InstanceBootstrap } from "../../src/project/bootstrap"
@@ -10,7 +10,7 @@ import { layerWebSocketConstructorGlobal } from "effect/unstable/socket/Socket"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { registerAdapter } from "../../src/control-plane/adapters"
import type { WorkspaceAdapter } from "../../src/control-plane/types"
@@ -1,7 +1,7 @@
import { afterEach, describe, expect, mock } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Context, Effect, Layer } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { SyncPaths } from "../../src/server/routes/instance/httpapi/groups/sync"
import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server"
import { Session } from "@/session/session"
@@ -1,6 +1,6 @@
import { createHash } from "node:crypto"
import { describe, expect } from "bun:test"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { ConfigProvider, Effect, Layer, Option } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import {
@@ -4,7 +4,7 @@ import path from "node:path"
import { Effect, Layer, Stream } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { registerAdapter } from "../../src/control-plane/adapters"
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
import type { WorkspaceAdapter } from "../../src/control-plane/types"
@@ -1,6 +1,6 @@
import { describe, expect } from "bun:test"
import { Effect, Layer, Queue } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Flag } from "@/flag/flag"
import { GlobalBus, type GlobalEvent } from "@/bus/global"
import { Worktree } from "@/worktree"
import { Server } from "../../src/server/server"
+5 -18
View File
@@ -1,11 +1,9 @@
export * as ServerAuth from "./auth"
import { Context, Effect, Layer, Option, Redacted } from "effect"
import { all, option, string, withDefault } from "effect/Config"
import { Context, Layer, Option, Redacted } from "effect"
export type Credentials = {
password?: string
username?: string
}
export type DecodedCredentials = {
@@ -15,7 +13,6 @@ export type DecodedCredentials = {
export type Info = {
readonly password: Option.Option<string>
readonly username: string
}
export class Config extends Context.Service<Config, Info>()("@opencode/ServerAuthConfig") {
@@ -24,17 +21,7 @@ export class Config extends Context.Service<Config, Info>()("@opencode/ServerAut
}
static get layer() {
return Layer.effect(
this,
Effect.gen(function* () {
return Config.of(
yield* all({
password: string("OPENCODE_SERVER_PASSWORD").pipe(option),
username: string("OPENCODE_SERVER_USERNAME").pipe(withDefault("opencode")),
}),
)
}),
)
return this.configLayer({ password: Option.none() })
}
}
@@ -45,16 +32,16 @@ export function required(config: Info) {
export function authorized(credentials: DecodedCredentials, config: Info) {
return (
Option.isSome(config.password) &&
credentials.username === config.username &&
credentials.username === "opencode" &&
Redacted.value(credentials.password) === config.password.value
)
}
export function header(credentials?: Credentials) {
const password = credentials?.password ?? process.env.OPENCODE_SERVER_PASSWORD
const password = credentials?.password
if (!password) return undefined
return `Basic ${Buffer.from(`${credentials?.username ?? process.env.OPENCODE_SERVER_USERNAME ?? "opencode"}:${password}`).toString("base64")}`
return `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
}
export function headers(credentials?: Credentials) {
+4
View File
@@ -4,11 +4,13 @@ import { Observability } from "@opencode-ai/core/observability"
import { Schema } from "effect"
export const ServerOptions = Schema.Struct({
client: Schema.optional(Schema.String),
hostname: Schema.optional(Schema.String),
port: Schema.optional(
Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(65_535)),
),
password: Schema.optional(Schema.String),
simulation: Schema.optional(Schema.Boolean),
database: Schema.optional(Database.Options),
models: Schema.optional(ModelsDev.Options),
observability: Schema.optional(Observability.Options),
@@ -16,6 +18,8 @@ export const ServerOptions = Schema.Struct({
Schema.Struct({
directory: Schema.optional(Schema.String),
project: Schema.optional(Schema.Boolean),
file: Schema.optional(Schema.String),
content: Schema.optional(Schema.String),
}),
),
windows: Schema.optional(
+1 -1
View File
@@ -148,7 +148,7 @@ function dispatch(
application: Ref.Ref<Option.Option<App>>,
shutdown: Deferred.Deferred<void>,
): App {
const auth = ServerAuth.Config.of({ username: "opencode", password: Option.some(password) })
const auth = ServerAuth.Config.of({ password: Option.some(password) })
return Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest
const url = new URL(request.url, "http://localhost")
+21 -10
View File
@@ -14,6 +14,10 @@ import { PtyTicket } from "@opencode-ai/core/pty/ticket"
import { Pty } from "@opencode-ai/core/pty"
import { Project } from "@opencode-ai/core/project"
import { SessionV2 } from "@opencode-ai/core/session"
import { SessionCompaction } from "@opencode-ai/core/session/compaction"
import { SessionGenerateNode } from "@opencode-ai/core/session/generate-node"
import { SessionModelRequest } from "@opencode-ai/core/session/model-request"
import { SessionTitle } from "@opencode-ai/core/session/title"
import { Shell } from "@opencode-ai/core/shell"
import { Job } from "@opencode-ai/core/job"
import { Global } from "@opencode-ai/core/global"
@@ -64,7 +68,7 @@ const applicationServices = LayerNode.group([
export function createRoutes(options: ServerOptions = {}, serviceURLs: () => ReadonlyArray<string> = () => []) {
return makeRoutes(
options.password
? ServerAuth.Config.configLayer({ username: "opencode", password: Option.some(options.password) })
? ServerAuth.Config.configLayer({ password: Option.some(options.password) })
: ServerAuth.Config.layer,
options,
serviceURLs,
@@ -72,7 +76,7 @@ export function createRoutes(options: ServerOptions = {}, serviceURLs: () => Rea
}
export function createEmbeddedRoutes(options: ServerOptions = {}) {
return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }), options, () => [])
return makeRoutes(ServerAuth.Config.configLayer({ password: Option.none() }), options, () => [])
}
function makeRoutes<AuthError, AuthServices>(
@@ -83,19 +87,30 @@ function makeRoutes<AuthError, AuthServices>(
const pluginRuntimeCell = PluginRuntime.makeCell()
const replacements: LayerNode.Replacements = [
[Database.node, Database.configured(options.database)],
[ModelsDev.node, ModelsDev.configured(options.models)],
[ModelsDev.node, ModelsDev.configured({ ...options.models, client: options.client })],
[Watcher.node, Watcher.configured({ enabled: options.fs?.filewatcher })],
[FileSystemSearch.node, FileSystemSearch.configured({ fff: options.fs?.fff })],
[Global.node, Global.layerWith(options.config?.directory ? { config: options.config.directory } : {})],
[Config.node, Config.configured({ project: options.config?.project })],
[
Config.node,
Config.configured({
project: options.config?.project,
file: options.config?.file,
content: options.config?.content,
}),
],
[InstructionDiscovery.node, InstructionDiscovery.configured({ project: options.config?.project })],
[CommandV2.node, CommandV2.configured({ gitbash: options.windows?.gitbash })],
[Pty.node, Pty.configured({ gitbash: options.windows?.gitbash })],
[Shell.node, Shell.configured({ gitbash: options.windows?.gitbash })],
[SessionCompaction.node, SessionCompaction.configured({ client: options.client })],
[SessionGenerateNode.node, SessionGenerateNode.configured({ client: options.client })],
[SessionModelRequest.node, SessionModelRequest.configured({ client: options.client })],
[SessionTitle.node, SessionTitle.configured({ client: options.client })],
[PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)],
[PluginRuntime.providerNode, PluginRuntime.providerNodeWithCell(pluginRuntimeCell)],
]
const serviceLayer = simulateEnabled()
const serviceLayer = options.simulation
? Layer.unwrap(
Effect.gen(function* () {
const { simulationReplacements } = yield* Effect.promise(() => import("@opencode-ai/simulation/backend"))
@@ -120,7 +135,7 @@ function makeRoutes<AuthError, AuthServices>(
Layer.provide(authorizationLayer),
Layer.provide(schemaErrorLayer),
Layer.provide(auth),
Layer.provide(Observability.layer(options.observability)),
Layer.provide(Observability.layer({ ...options.observability, client: options.client })),
HttpRouter.provideRequest(requestServices),
Layer.provideMerge(services),
Layer.provideMerge(HttpRouter.layer),
@@ -129,8 +144,4 @@ function makeRoutes<AuthError, AuthServices>(
)
}
function simulateEnabled() {
return !!process.env.OPENCODE_SIMULATE
}
export const webHandler = () => HttpRouter.toWebHandler(createRoutes().pipe(Layer.provide(HttpServer.layerServices)))
+13
View File
@@ -0,0 +1,13 @@
import { expect, test } from "bun:test"
import { ServerAuth } from "@opencode-ai/server/auth"
import { Option, Redacted } from "effect"
test("accepts only the fixed opencode username", () => {
const config = { password: Option.some("secret") }
expect(ServerAuth.authorized({ username: "opencode", password: Redacted.make("secret") }, config)).toBe(true)
expect(ServerAuth.authorized({ username: "custom", password: Redacted.make("secret") }, config)).toBe(false)
})
test("encodes the fixed opencode username", () => {
expect(ServerAuth.header({ password: "secret" })).toBe(`Basic ${Buffer.from("opencode:secret").toString("base64")}`)
})
+1 -1
View File
@@ -12,7 +12,7 @@ import { SimulatedProvider } from "./simulated-provider"
/**
* Layer replacements applied when the server is built in simulation mode.
*
* The server merges these into the app node build when `OPENCODE_SIMULATE`
* The server merges these into the app node build when simulation is enabled
* is enabled, via a dynamic import so this module is never loaded eagerly.
*
* - Network: all outbound HTTP resolves against the simulated route table;
+8 -8
View File
@@ -4,7 +4,6 @@ import { Deferred, Effect } from "effect"
import { Service, type Endpoint } from "@opencode-ai/client/effect/service"
import { OpenCode } from "@opencode-ai/client"
import { Global } from "@opencode-ai/core/global"
import { Flag } from "@opencode-ai/core/flag/flag"
import { ClipboardProvider, useClipboard } from "./context/clipboard"
import { LogProvider, useLog, type LogSink } from "./context/log"
import { ExitProvider, useExit } from "./context/exit"
@@ -211,7 +210,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
useKittyKeyboard: {},
autoFocus: false,
openConsoleOnError: false,
useMouse: !Flag.OPENCODE_DISABLE_MOUSE && config.mouse,
useMouse: config.mouse,
consoleOptions: {
keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
},
@@ -466,7 +465,7 @@ function App(props: { pair?: DialogPairCredentials; started: number }) {
const offSelectionKeys = keymap.intercept(
"key",
({ event }) => {
if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return
if (config.data.terminal?.copy_on_select ?? process.platform !== "win32") return
Selection.handleSelectionKey(renderer, toast, event, clipboard)
},
{ priority: 1 },
@@ -487,15 +486,16 @@ function App(props: { pair?: DialogPairCredentials; started: number }) {
renderer.clearSelection()
}
const terminalTitleEnabled = () => config.data.terminal?.title ?? true
const copyOnSelectEnabled = () => config.data.terminal?.copy_on_select ?? process.platform !== "win32"
const pasteSummaryEnabled = () => config.data.prompt?.paste !== "full"
createEffect(() => {
renderer.useMouse = !Flag.OPENCODE_DISABLE_MOUSE && config.data.mouse
renderer.useMouse = config.data.mouse
})
// Update terminal window title based on current route and session
createEffect(() => {
if (!terminalTitleEnabled() || Flag.OPENCODE_DISABLE_TERMINAL_TITLE) return
if (!terminalTitleEnabled()) return
if (route.data.type === "home") {
renderer.setTerminalTitle("OpenCode")
@@ -1090,7 +1090,7 @@ function App(props: { pair?: DialogPairCredentials; started: number }) {
flexDirection="column"
backgroundColor={themeV2.background.default}
onMouseDown={(evt) => {
if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return
if (copyOnSelectEnabled()) return
if (evt.button !== MouseButton.RIGHT) return
if (!Selection.copy(renderer, toast, clipboard)) return
@@ -1098,12 +1098,12 @@ function App(props: { pair?: DialogPairCredentials; started: number }) {
evt.stopPropagation()
}}
onMouseUp={
!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT
copyOnSelectEnabled()
? () => Selection.copy(renderer, toast, clipboard)
: undefined
}
>
<Show when={Flag.OPENCODE_SHOW_TTFD}>
<Show when={config.data.debug?.timing}>
<TimeToFirstDraw />
</Show>
<box flexGrow={1} minHeight={0} flexDirection="row">
@@ -206,6 +206,14 @@ const settings: Setting[] = [
values: [false, true],
labels: ["off", "on"],
},
{
title: "Copy on select",
category: "Terminal",
path: ["terminal", "copy_on_select"],
default: process.platform !== "win32",
values: [false, true],
labels: ["off", "on"],
},
{
title: "DevTools",
category: "Debug",
@@ -214,6 +222,14 @@ const settings: Setting[] = [
values: [false, true],
labels: ["off", "on"],
},
{
title: "Timing",
category: "Debug",
path: ["debug", "timing"],
default: false,
values: [false, true],
labels: ["off", "on"],
},
]
export function DialogConfig() {
@@ -12,7 +12,6 @@ import { registerOpencodeSpinner } from "../register-spinner"
import path from "path"
import { fileURLToPath } from "url"
import { useLocal } from "../../context/local"
import { Flag } from "@opencode-ai/core/flag/flag"
import { useTheme } from "../../context/theme"
import { tint } from "../../theme/color"
import { EmptyBorder, SplitBorder } from "../../ui/border"
+2
View File
@@ -92,6 +92,7 @@ export const Info = Schema.Struct({
terminal: Schema.optional(
Schema.Struct({
title: Schema.optional(Schema.Boolean).annotate({ description: "Update the terminal window title" }),
copy_on_select: Schema.optional(Schema.Boolean).annotate({ description: "Copy selected terminal text" }),
}),
).annotate({ description: "Terminal integration settings" }),
prompt: Schema.optional(
@@ -129,6 +130,7 @@ export const Info = Schema.Struct({
debug: Schema.optional(
Schema.Struct({
devtools: Schema.optional(Schema.Boolean).annotate({ description: "Show the DevTools sidebar" }),
timing: Schema.optional(Schema.Boolean).annotate({ description: "Show time-to-first-draw diagnostics" }),
}),
).annotate({ description: "Debugging settings" }),
animations: Schema.optional(Schema.Boolean).annotate({ description: "Enable interface animations" }),
+5 -3
View File
@@ -5,8 +5,8 @@ import { useTheme } from "../context/theme"
import { MouseButton, Renderable, RGBA } from "@opentui/core"
import { createStore } from "solid-js/store"
import { useToast } from "./toast"
import { Flag } from "@opencode-ai/core/flag/flag"
import { useClipboard } from "../context/clipboard"
import { useConfig } from "../config"
export function Dialog(
props: ParentProps<{
@@ -197,6 +197,8 @@ export function DialogProvider(props: ParentProps) {
const renderer = useRenderer()
const toast = useToast()
const clipboard = useClipboard()
const config = useConfig()
const copyOnSelectEnabled = () => config.data.terminal?.copy_on_select ?? process.platform !== "win32"
function copySelection() {
const text = renderer.getSelection()?.getSelectedText()
@@ -216,14 +218,14 @@ export function DialogProvider(props: ParentProps) {
position="absolute"
zIndex={3000}
onMouseDown={(evt: { button: number; preventDefault(): void; stopPropagation(): void }) => {
if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return
if (copyOnSelectEnabled()) return
if (evt.button !== MouseButton.RIGHT) return
if (!copySelection()) return
evt.preventDefault()
evt.stopPropagation()
}}
onMouseUp={!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT ? copySelection : undefined}
onMouseUp={copyOnSelectEnabled() ? copySelection : undefined}
>
<Show when={value.stack.length}>
<Dialog onClose={() => value.clear()} size={value.size} centered={value.centered}>