mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-21 10:16:03 +00:00
refactor: isolate legacy flags
This commit is contained in:
@@ -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,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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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}$/)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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,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,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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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,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"
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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,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,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"
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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")}`)
|
||||
})
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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}>
|
||||
|
||||
Reference in New Issue
Block a user