From 1a734adb4d1ce6071432bd68ac45fa4457f0dc2e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 25 Apr 2026 13:29:52 -0400 Subject: core: consolidate shared infrastructure into core package Moves effect logging, observability, runtime utilities, flags, installation version info, and process utilities from opencode to core package. This enables better code sharing across packages and establishes core as the single source of truth for foundational utilities. All internal imports updated to use @opencode-ai/core paths for consistency. --- bun.lock | 5 + packages/core/package.json | 5 + packages/core/src/effect/logger.ts | 73 ++++++++ packages/core/src/effect/memo-map.ts | 3 + packages/core/src/effect/observability.ts | 107 ++++++++++++ packages/core/src/effect/runtime.ts | 21 +++ packages/core/src/flag/flag.ts | 107 ++++++++++++ packages/core/src/global.ts | 41 +++-- packages/core/src/installation/version.ts | 8 + packages/core/src/util/log.ts | 185 +++++++++++++++++++++ packages/core/src/util/opencode-process.ts | 24 +++ packages/core/test/effect/observability.test.ts | 46 +++++ packages/opencode/src/acp/agent.ts | 2 +- packages/opencode/src/cli/cmd/mcp.ts | 2 +- packages/opencode/src/cli/cmd/run.ts | 2 +- packages/opencode/src/cli/cmd/serve.ts | 2 +- packages/opencode/src/cli/cmd/session.ts | 2 +- packages/opencode/src/cli/cmd/tui/app.tsx | 2 +- .../cli/cmd/tui/component/dialog-session-list.tsx | 2 +- .../cmd/tui/component/dialog-workspace-create.tsx | 2 +- .../src/cli/cmd/tui/component/error-component.tsx | 2 +- .../opencode/src/cli/cmd/tui/config/tui-migrate.ts | 2 +- packages/opencode/src/cli/cmd/tui/config/tui.ts | 6 +- packages/opencode/src/cli/cmd/tui/context/sdk.tsx | 2 +- packages/opencode/src/cli/cmd/tui/layer.ts | 2 +- packages/opencode/src/cli/cmd/tui/plugin/api.tsx | 2 +- .../opencode/src/cli/cmd/tui/plugin/runtime.ts | 2 +- .../src/cli/cmd/tui/routes/session/index.tsx | 2 +- .../src/cli/cmd/tui/routes/session/sidebar.tsx | 2 +- packages/opencode/src/cli/cmd/tui/thread.ts | 2 +- packages/opencode/src/cli/cmd/tui/ui/dialog.tsx | 2 +- packages/opencode/src/cli/cmd/tui/worker.ts | 4 +- packages/opencode/src/cli/cmd/upgrade.ts | 2 +- packages/opencode/src/cli/cmd/web.ts | 2 +- packages/opencode/src/cli/heap.ts | 2 +- packages/opencode/src/cli/upgrade.ts | 4 +- packages/opencode/src/config/config.ts | 4 +- packages/opencode/src/config/paths.ts | 2 +- packages/opencode/src/control-plane/workspace.ts | 2 +- packages/opencode/src/effect/app-runtime.ts | 4 +- packages/opencode/src/effect/bootstrap-runtime.ts | 4 +- packages/opencode/src/effect/index.ts | 4 +- packages/opencode/src/effect/instance-state.ts | 2 +- packages/opencode/src/effect/logger.ts | 73 -------- packages/opencode/src/effect/memo-map.ts | 3 - packages/opencode/src/effect/observability.ts | 107 ------------ packages/opencode/src/effect/run-service.ts | 4 +- packages/opencode/src/effect/runtime.ts | 19 --- packages/opencode/src/file/ripgrep.ts | 2 +- packages/opencode/src/file/watcher.ts | 2 +- packages/opencode/src/flag/flag.ts | 107 ------------ packages/opencode/src/format/formatter.ts | 2 +- packages/opencode/src/index.ts | 4 +- packages/opencode/src/installation/index.ts | 4 +- packages/opencode/src/installation/version.ts | 8 - packages/opencode/src/lsp/lsp.ts | 2 +- packages/opencode/src/lsp/server.ts | 2 +- packages/opencode/src/mcp/index.ts | 2 +- packages/opencode/src/npm/index.ts | 2 +- packages/opencode/src/plugin/codex.ts | 2 +- .../opencode/src/plugin/github-copilot/copilot.ts | 2 +- packages/opencode/src/plugin/index.ts | 2 +- packages/opencode/src/plugin/loader.ts | 2 +- packages/opencode/src/plugin/meta.ts | 2 +- packages/opencode/src/project/project.ts | 2 +- packages/opencode/src/provider/models.ts | 2 +- packages/opencode/src/provider/provider.ts | 4 +- packages/opencode/src/provider/transform.ts | 2 +- packages/opencode/src/server/middleware.ts | 2 +- packages/opencode/src/server/routes/global.ts | 2 +- .../src/server/routes/instance/httpapi/auth.ts | 2 +- .../src/server/routes/instance/httpapi/server.ts | 2 +- .../opencode/src/server/routes/instance/index.ts | 2 +- packages/opencode/src/server/routes/ui.ts | 2 +- packages/opencode/src/server/server.ts | 2 +- packages/opencode/src/server/workspace.ts | 2 +- packages/opencode/src/session/instruction.ts | 2 +- packages/opencode/src/session/llm.ts | 4 +- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/src/session/session.ts | 4 +- packages/opencode/src/share/session.ts | 2 +- packages/opencode/src/shell/shell.ts | 2 +- packages/opencode/src/skill/index.ts | 2 +- packages/opencode/src/storage/db.ts | 4 +- packages/opencode/src/sync/index.ts | 2 +- packages/opencode/src/temporary.ts | 2 +- packages/opencode/src/tool/bash.ts | 2 +- packages/opencode/src/tool/registry.ts | 2 +- packages/opencode/src/util/index.ts | 2 +- packages/opencode/src/util/log.ts | 185 --------------------- packages/opencode/src/util/opencode-process.ts | 24 --- .../opencode/test/effect/observability.test.ts | 46 ----- .../test/installation/installation.test.ts | 2 +- .../opencode/test/plugin/workspace-adaptor.test.ts | 2 +- .../opencode/test/server/httpapi-bridge.test.ts | 2 +- .../opencode/test/server/httpapi-instance.test.ts | 2 +- packages/opencode/test/storage/db.test.ts | 2 +- packages/opencode/test/sync/index.test.ts | 2 +- .../test/workspace/workspace-restore.test.ts | 2 +- 99 files changed, 702 insertions(+), 681 deletions(-) create mode 100644 packages/core/src/effect/logger.ts create mode 100644 packages/core/src/effect/memo-map.ts create mode 100644 packages/core/src/effect/observability.ts create mode 100644 packages/core/src/effect/runtime.ts create mode 100644 packages/core/src/flag/flag.ts create mode 100644 packages/core/src/installation/version.ts create mode 100644 packages/core/src/util/log.ts create mode 100644 packages/core/src/util/opencode-process.ts create mode 100644 packages/core/test/effect/observability.test.ts delete mode 100644 packages/opencode/src/effect/logger.ts delete mode 100644 packages/opencode/src/effect/memo-map.ts delete mode 100644 packages/opencode/src/effect/observability.ts delete mode 100644 packages/opencode/src/effect/runtime.ts delete mode 100644 packages/opencode/src/flag/flag.ts delete mode 100644 packages/opencode/src/installation/version.ts delete mode 100644 packages/opencode/src/util/log.ts delete mode 100644 packages/opencode/src/util/opencode-process.ts delete mode 100644 packages/opencode/test/effect/observability.test.ts diff --git a/bun.lock b/bun.lock index e28376682..2420ab6df 100644 --- a/bun.lock +++ b/bun.lock @@ -197,8 +197,13 @@ "opencode": "./bin/opencode", }, "dependencies": { + "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", "@npmcli/arborist": "catalog:", + "@opentelemetry/api": "1.9.0", + "@opentelemetry/context-async-hooks": "2.6.1", + "@opentelemetry/exporter-trace-otlp-http": "0.214.0", + "@opentelemetry/sdk-trace-base": "2.6.1", "effect": "catalog:", "glob": "13.0.5", "mime-types": "3.0.2", diff --git a/packages/core/package.json b/packages/core/package.json index 48d44ccf3..a244ea8b4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,8 +23,13 @@ "@types/npmcli__arborist": "6.3.3" }, "dependencies": { + "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", "@npmcli/arborist": "catalog:", + "@opentelemetry/api": "1.9.0", + "@opentelemetry/context-async-hooks": "2.6.1", + "@opentelemetry/exporter-trace-otlp-http": "0.214.0", + "@opentelemetry/sdk-trace-base": "2.6.1", "effect": "catalog:", "glob": "13.0.5", "mime-types": "3.0.2", diff --git a/packages/core/src/effect/logger.ts b/packages/core/src/effect/logger.ts new file mode 100644 index 000000000..69f9631e0 --- /dev/null +++ b/packages/core/src/effect/logger.ts @@ -0,0 +1,73 @@ +import { Cause, Effect, Logger, References } from "effect" +import * as Log from "../util/log" + +type Fields = Record + +const normalizeKey = (key: string) => (key === "sessionID" ? "session.id" : key) + +export interface Handle { + readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect + readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect + readonly warn: (msg?: unknown, extra?: Fields) => Effect.Effect + readonly error: (msg?: unknown, extra?: Fields) => Effect.Effect + readonly with: (extra: Fields) => Handle +} + +const clean = (input?: Fields): Fields => + Object.fromEntries( + Object.entries(input ?? {}) + .filter((entry) => entry[1] !== undefined && entry[1] !== null) + .map(([key, value]) => [normalizeKey(key), value]), + ) + +const text = (input: unknown): string => { + // oxlint-disable-next-line no-base-to-string + if (Array.isArray(input)) return input.map((item) => String(item)).join(" ") + // oxlint-disable-next-line no-base-to-string + return input === undefined ? "" : String(input) +} + +const call = (run: (msg?: unknown) => Effect.Effect, base: Fields, msg?: unknown, extra?: Fields) => { + const ann = clean({ ...base, ...extra }) + const fx = run(msg) + return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx +} + +export const logger = Logger.make((opts) => { + const extra = clean(opts.fiber.getRef(References.CurrentLogAnnotations)) + const now = opts.date.getTime() + for (const [key, start] of opts.fiber.getRef(References.CurrentLogSpans)) { + extra[`logSpan.${key}`] = `${now - start}ms` + } + if (opts.cause.reasons.length > 0) { + extra.cause = Cause.pretty(opts.cause) + } + + const svc = typeof extra.service === "string" ? extra.service : undefined + if (svc) delete extra.service + const log = svc ? Log.create({ service: svc }) : Log.Default + const msg = text(opts.message) + + switch (opts.logLevel) { + case "Trace": + case "Debug": + return log.debug(msg, extra) + case "Warn": + return log.warn(msg, extra) + case "Error": + case "Fatal": + return log.error(msg, extra) + default: + return log.info(msg, extra) + } +}) + +export const layer = Logger.layer([logger], { mergeWithExisting: false }) + +export const create = (base: Fields = {}): Handle => ({ + debug: (msg, extra) => call((item) => Effect.logDebug(item), base, msg, extra), + info: (msg, extra) => call((item) => Effect.logInfo(item), base, msg, extra), + warn: (msg, extra) => call((item) => Effect.logWarning(item), base, msg, extra), + error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra), + with: (extra) => create({ ...base, ...extra }), +}) diff --git a/packages/core/src/effect/memo-map.ts b/packages/core/src/effect/memo-map.ts new file mode 100644 index 000000000..c797dbf42 --- /dev/null +++ b/packages/core/src/effect/memo-map.ts @@ -0,0 +1,3 @@ +import { Layer } from "effect" + +export const memoMap = Layer.makeMemoMapUnsafe() diff --git a/packages/core/src/effect/observability.ts b/packages/core/src/effect/observability.ts new file mode 100644 index 000000000..0203079ab --- /dev/null +++ b/packages/core/src/effect/observability.ts @@ -0,0 +1,107 @@ +import { Effect, Layer, Logger } from "effect" +import { FetchHttpClient } from "effect/unstable/http" +import { OtlpLogger, OtlpSerialization } from "effect/unstable/observability" +import * as EffectLogger from "./logger" +import { Flag } from "../flag/flag" +import { InstallationChannel, InstallationVersion } from "../installation/version" +import { ensureProcessMetadata } from "../util/opencode-process" + +const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT +export const enabled = !!base +const processID = crypto.randomUUID() + +const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS + ? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce( + (acc, x) => { + const [key, ...value] = x.split("=") + acc[key] = value.join("=") + return acc + }, + {} as Record, + ) + : undefined + +export function resource(): { serviceName: string; serviceVersion: string; attributes: Record } { + const processMetadata = ensureProcessMetadata("main") + const attributes: Record = (() => { + const value = process.env.OTEL_RESOURCE_ATTRIBUTES + if (!value) return {} + try { + return Object.fromEntries( + value.split(",").map((entry) => { + const index = entry.indexOf("=") + if (index < 1) throw new Error("Invalid OTEL_RESOURCE_ATTRIBUTES entry") + return [decodeURIComponent(entry.slice(0, index)), decodeURIComponent(entry.slice(index + 1))] + }), + ) + } catch { + return {} + } + })() + + return { + serviceName: "opencode", + serviceVersion: InstallationVersion, + attributes: { + ...attributes, + "deployment.environment.name": InstallationChannel, + "opencode.client": Flag.OPENCODE_CLIENT, + "opencode.process_role": processMetadata.processRole, + "opencode.run_id": processMetadata.runID, + "service.instance.id": processID, + }, + } +} + +function logs() { + return Logger.layer( + [ + EffectLogger.logger, + OtlpLogger.make({ + url: `${base}/v1/logs`, + resource: resource(), + headers, + }), + ], + { mergeWithExisting: false }, + ).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer)) +} + +const traces = async () => { + const NodeSdk = await import("@effect/opentelemetry/NodeSdk") + const OTLP = await import("@opentelemetry/exporter-trace-otlp-http") + const SdkBase = await import("@opentelemetry/sdk-trace-base") + + // @effect/opentelemetry creates a NodeTracerProvider but never calls + // register(), so the global @opentelemetry/api context manager stays + // as the no-op default. Non-Effect code (like the AI SDK) that calls + // tracer.startActiveSpan() relies on context.active() to find the + // parent span - without a real context manager every span starts a + // new trace. Registering AsyncLocalStorageContextManager fixes this. + const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks") + const { context } = await import("@opentelemetry/api") + const mgr = new AsyncLocalStorageContextManager() + mgr.enable() + context.setGlobalContextManager(mgr) + + return NodeSdk.layer(() => ({ + resource: resource(), + spanProcessor: new SdkBase.BatchSpanProcessor( + new OTLP.OTLPTraceExporter({ + url: `${base}/v1/traces`, + headers, + }), + ), + })) +} + +export const layer = !base + ? EffectLogger.layer + : Layer.unwrap( + Effect.gen(function* () { + const trace = yield* Effect.promise(traces) + return Layer.mergeAll(trace, logs()) + }), + ) + +export const Observability = { enabled, layer } diff --git a/packages/core/src/effect/runtime.ts b/packages/core/src/effect/runtime.ts new file mode 100644 index 000000000..e4f682709 --- /dev/null +++ b/packages/core/src/effect/runtime.ts @@ -0,0 +1,21 @@ +import { Layer, type Context, ManagedRuntime, type Effect } from "effect" +import { memoMap } from "./memo-map" +import { Observability } from "./observability" + +export function makeRuntime(service: Context.Service, layer: Layer.Layer) { + let rt: ManagedRuntime.ManagedRuntime | undefined + const getRuntime = () => + (rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer) as Layer.Layer, { + memoMap, + })) + + return { + runSync: (fn: (svc: S) => Effect.Effect) => getRuntime().runSync(service.use(fn)), + runPromiseExit: (fn: (svc: S) => Effect.Effect, options?: Effect.RunOptions) => + getRuntime().runPromiseExit(service.use(fn), options), + runPromise: (fn: (svc: S) => Effect.Effect, options?: Effect.RunOptions) => + getRuntime().runPromise(service.use(fn), options), + runFork: (fn: (svc: S) => Effect.Effect) => getRuntime().runFork(service.use(fn)), + runCallback: (fn: (svc: S) => Effect.Effect) => getRuntime().runCallback(service.use(fn)), + } +} diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts new file mode 100644 index 000000000..72c8931f5 --- /dev/null +++ b/packages/core/src/flag/flag.ts @@ -0,0 +1,107 @@ +import { Config } from "effect" + +function truthy(key: string) { + const value = process.env[key]?.toLowerCase() + return value === "true" || value === "1" +} + +function falsy(key: string) { + const value = process.env[key]?.toLowerCase() + return value === "false" || value === "0" +} + +function number(key: string) { + const value = process.env[key] + if (!value) return undefined + const parsed = Number(value) + return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined +} + +const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL") +const OPENCODE_DISABLE_CLAUDE_CODE = truthy("OPENCODE_DISABLE_CLAUDE_CODE") +const OPENCODE_DISABLE_CLAUDE_CODE_SKILLS = + OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS") +const copy = process.env["OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"] + +export const Flag = { + OTEL_EXPORTER_OTLP_ENDPOINT: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"], + OTEL_EXPORTER_OTLP_HEADERS: process.env["OTEL_EXPORTER_OTLP_HEADERS"], + + OPENCODE_AUTO_SHARE: truthy("OPENCODE_AUTO_SHARE"), + OPENCODE_AUTO_HEAP_SNAPSHOT: truthy("OPENCODE_AUTO_HEAP_SNAPSHOT"), + OPENCODE_GIT_BASH_PATH: process.env["OPENCODE_GIT_BASH_PATH"], + OPENCODE_CONFIG: process.env["OPENCODE_CONFIG"], + OPENCODE_CONFIG_CONTENT: process.env["OPENCODE_CONFIG_CONTENT"], + OPENCODE_DISABLE_AUTOUPDATE: truthy("OPENCODE_DISABLE_AUTOUPDATE"), + OPENCODE_ALWAYS_NOTIFY_UPDATE: truthy("OPENCODE_ALWAYS_NOTIFY_UPDATE"), + OPENCODE_DISABLE_PRUNE: truthy("OPENCODE_DISABLE_PRUNE"), + OPENCODE_DISABLE_TERMINAL_TITLE: truthy("OPENCODE_DISABLE_TERMINAL_TITLE"), + OPENCODE_SHOW_TTFD: truthy("OPENCODE_SHOW_TTFD"), + OPENCODE_PERMISSION: process.env["OPENCODE_PERMISSION"], + OPENCODE_DISABLE_DEFAULT_PLUGINS: truthy("OPENCODE_DISABLE_DEFAULT_PLUGINS"), + OPENCODE_DISABLE_LSP_DOWNLOAD: truthy("OPENCODE_DISABLE_LSP_DOWNLOAD"), + OPENCODE_ENABLE_EXPERIMENTAL_MODELS: truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS"), + OPENCODE_DISABLE_AUTOCOMPACT: truthy("OPENCODE_DISABLE_AUTOCOMPACT"), + OPENCODE_DISABLE_MODELS_FETCH: truthy("OPENCODE_DISABLE_MODELS_FETCH"), + OPENCODE_DISABLE_MOUSE: truthy("OPENCODE_DISABLE_MOUSE"), + OPENCODE_DISABLE_CLAUDE_CODE, + OPENCODE_DISABLE_CLAUDE_CODE_PROMPT: OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT"), + OPENCODE_DISABLE_CLAUDE_CODE_SKILLS, + OPENCODE_DISABLE_EXTERNAL_SKILLS: OPENCODE_DISABLE_CLAUDE_CODE_SKILLS || truthy("OPENCODE_DISABLE_EXTERNAL_SKILLS"), + OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"], + OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"], + OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"], + OPENCODE_ENABLE_QUESTION_TOOL: truthy("OPENCODE_ENABLE_QUESTION_TOOL"), + + // Experimental + OPENCODE_EXPERIMENTAL, + OPENCODE_EXPERIMENTAL_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_FILEWATCHER").pipe( + Config.withDefault(false), + ), + OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER").pipe( + Config.withDefault(false), + ), + OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY"), + OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT: + copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"), + OPENCODE_ENABLE_EXA: truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA"), + OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: number("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS"), + OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: number("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX"), + OPENCODE_EXPERIMENTAL_OXFMT: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_OXFMT"), + OPENCODE_EXPERIMENTAL_LSP_TY: truthy("OPENCODE_EXPERIMENTAL_LSP_TY"), + OPENCODE_EXPERIMENTAL_LSP_TOOL: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL"), + OPENCODE_EXPERIMENTAL_PLAN_MODE: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE"), + OPENCODE_EXPERIMENTAL_MARKDOWN: !falsy("OPENCODE_EXPERIMENTAL_MARKDOWN"), + OPENCODE_MODELS_URL: process.env["OPENCODE_MODELS_URL"], + OPENCODE_MODELS_PATH: process.env["OPENCODE_MODELS_PATH"], + OPENCODE_DISABLE_EMBEDDED_WEB_UI: truthy("OPENCODE_DISABLE_EMBEDDED_WEB_UI"), + OPENCODE_DB: process.env["OPENCODE_DB"], + OPENCODE_DISABLE_CHANNEL_DB: truthy("OPENCODE_DISABLE_CHANNEL_DB"), + OPENCODE_SKIP_MIGRATIONS: truthy("OPENCODE_SKIP_MIGRATIONS"), + OPENCODE_STRICT_CONFIG_DEPS: truthy("OPENCODE_STRICT_CONFIG_DEPS"), + + OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"], + OPENCODE_EXPERIMENTAL_HTTPAPI: truthy("OPENCODE_EXPERIMENTAL_HTTPAPI"), + OPENCODE_EXPERIMENTAL_WORKSPACES: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WORKSPACES"), + + // Evaluated at access time (not module load) because tests, the CLI, and + // external tooling set these env vars at runtime. + get OPENCODE_DISABLE_PROJECT_CONFIG() { + return truthy("OPENCODE_DISABLE_PROJECT_CONFIG") + }, + get OPENCODE_TUI_CONFIG() { + return process.env["OPENCODE_TUI_CONFIG"] + }, + get OPENCODE_CONFIG_DIR() { + return process.env["OPENCODE_CONFIG_DIR"] + }, + get OPENCODE_PURE() { + return truthy("OPENCODE_PURE") + }, + get OPENCODE_PLUGIN_META_FILE() { + return process.env["OPENCODE_PLUGIN_META_FILE"] + }, + get OPENCODE_CLIENT() { + return process.env["OPENCODE_CLIENT"] ?? "cli" + }, +} diff --git a/packages/core/src/global.ts b/packages/core/src/global.ts index 538cc091b..bf605618f 100644 --- a/packages/core/src/global.ts +++ b/packages/core/src/global.ts @@ -3,6 +3,24 @@ import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir" import os from "os" import { Context, Effect, Layer } from "effect" +const app = "opencode" +const data = path.join(xdgData!, app) +const cache = path.join(xdgCache!, app) +const config = path.join(xdgConfig!, app) +const state = path.join(xdgState!, app) + +export const Path = { + get home() { + return process.env.OPENCODE_TEST_HOME ?? os.homedir() + }, + data, + bin: path.join(cache, "bin"), + log: path.join(data, "log"), + cache, + config, + state, +} + export namespace Global { export class Service extends Context.Service()("@opencode/Global") {} @@ -19,23 +37,14 @@ export namespace Global { export const layer = Layer.effect( Service, Effect.gen(function* () { - const app = "opencode" - const home = process.env.OPENCODE_TEST_HOME ?? os.homedir() - const data = path.join(xdgData!, app) - const cache = path.join(xdgCache!, app) - const cfg = path.join(xdgConfig!, app) - const state = path.join(xdgState!, app) - const bin = path.join(cache, "bin") - const log = path.join(data, "log") - return Service.of({ - home, - data, - cache, - config: cfg, - state, - bin, - log, + home: Path.home, + data: Path.data, + cache: Path.cache, + config: Path.config, + state: Path.state, + bin: Path.bin, + log: Path.log, }) }), ) diff --git a/packages/core/src/installation/version.ts b/packages/core/src/installation/version.ts new file mode 100644 index 000000000..25d9cd99a --- /dev/null +++ b/packages/core/src/installation/version.ts @@ -0,0 +1,8 @@ +declare global { + const OPENCODE_VERSION: string + const OPENCODE_CHANNEL: string +} + +export const InstallationVersion = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local" +export const InstallationChannel = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local" +export const InstallationLocal = InstallationChannel === "local" diff --git a/packages/core/src/util/log.ts b/packages/core/src/util/log.ts new file mode 100644 index 000000000..a61c15f7a --- /dev/null +++ b/packages/core/src/util/log.ts @@ -0,0 +1,185 @@ +import path from "path" +import fs from "fs/promises" +import { createWriteStream } from "fs" +import * as Global from "../global" +import z from "zod" +import { Glob } from "./glob" + +export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" }) +export type Level = z.infer + +const levelPriority: Record = { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, +} +const keep = 10 + +let level: Level = "INFO" + +function shouldLog(input: Level): boolean { + return levelPriority[input] >= levelPriority[level] +} + +export type Logger = { + debug(message?: any, extra?: Record): void + info(message?: any, extra?: Record): void + error(message?: any, extra?: Record): void + warn(message?: any, extra?: Record): void + tag(key: string, value: string): Logger + clone(): Logger + time( + message: string, + extra?: Record, + ): { + stop(): void + [Symbol.dispose](): void + } +} + +const loggers = new Map() + +export const Default = create({ service: "default" }) + +export interface Options { + print: boolean + dev?: boolean + level?: Level +} + +let logpath = "" +export function file() { + return logpath +} +let write = (msg: any) => { + process.stderr.write(msg) + return msg.length +} + +export async function init(options: Options) { + if (options.level) level = options.level + void cleanup(Global.Path.log) + if (options.print) return + logpath = path.join( + Global.Path.log, + options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log", + ) + await fs.truncate(logpath).catch(() => {}) + const stream = createWriteStream(logpath, { flags: "a" }) + write = async (msg: any) => { + return new Promise((resolve, reject) => { + stream.write(msg, (err) => { + if (err) reject(err) + else resolve(msg.length) + }) + }) + } +} + +async function cleanup(dir: string) { + const files = ( + await Glob.scan("????-??-??T??????.log", { + cwd: dir, + absolute: false, + include: "file", + }).catch(() => []) + ) + .filter((file) => path.basename(file) === file) + .sort() + if (files.length <= keep) return + + const doomed = files.slice(0, -keep) + await Promise.all(doomed.map((file) => fs.unlink(path.join(dir, file)).catch(() => {}))) +} + +function formatError(error: Error, depth = 0): string { + const result = error.message + return error.cause instanceof Error && depth < 10 + ? result + " Caused by: " + formatError(error.cause, depth + 1) + : result +} + +let last = Date.now() +export function create(tags?: Record) { + tags = tags || {} + + const service = tags["service"] + if (service && typeof service === "string") { + const cached = loggers.get(service) + if (cached) { + return cached + } + } + + function build(message: any, extra?: Record) { + const prefix = Object.entries({ + ...tags, + ...extra, + }) + .filter(([_, value]) => value !== undefined && value !== null) + .map(([key, value]) => { + const prefix = `${key}=` + if (value instanceof Error) return prefix + formatError(value) + if (typeof value === "object") return prefix + JSON.stringify(value) + return prefix + value + }) + .join(" ") + const next = new Date() + const diff = next.getTime() - last + last = next.getTime() + return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n" + } + const result: Logger = { + debug(message?: any, extra?: Record) { + if (shouldLog("DEBUG")) { + write("DEBUG " + build(message, extra)) + } + }, + info(message?: any, extra?: Record) { + if (shouldLog("INFO")) { + write("INFO " + build(message, extra)) + } + }, + error(message?: any, extra?: Record) { + if (shouldLog("ERROR")) { + write("ERROR " + build(message, extra)) + } + }, + warn(message?: any, extra?: Record) { + if (shouldLog("WARN")) { + write("WARN " + build(message, extra)) + } + }, + tag(key: string, value: string) { + if (tags) tags[key] = value + return result + }, + clone() { + return create({ ...tags }) + }, + time(message: string, extra?: Record) { + const now = Date.now() + result.info(message, { status: "started", ...extra }) + function stop() { + result.info(message, { + status: "completed", + duration: Date.now() - now, + ...extra, + }) + } + return { + stop, + [Symbol.dispose]() { + stop() + }, + } + }, + } + + if (service && typeof service === "string") { + loggers.set(service, result) + } + + return result +} diff --git a/packages/core/src/util/opencode-process.ts b/packages/core/src/util/opencode-process.ts new file mode 100644 index 000000000..f59270ad2 --- /dev/null +++ b/packages/core/src/util/opencode-process.ts @@ -0,0 +1,24 @@ +export const OPENCODE_RUN_ID = "OPENCODE_RUN_ID" +export const OPENCODE_PROCESS_ROLE = "OPENCODE_PROCESS_ROLE" + +export function ensureRunID() { + return (process.env[OPENCODE_RUN_ID] ??= crypto.randomUUID()) +} + +export function ensureProcessRole(fallback: "main" | "worker") { + return (process.env[OPENCODE_PROCESS_ROLE] ??= fallback) +} + +export function ensureProcessMetadata(fallback: "main" | "worker") { + return { + runID: ensureRunID(), + processRole: ensureProcessRole(fallback), + } +} + +export function sanitizedProcessEnv(overrides?: Record) { + const env = Object.fromEntries( + Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined), + ) + return overrides ? Object.assign(env, overrides) : env +} diff --git a/packages/core/test/effect/observability.test.ts b/packages/core/test/effect/observability.test.ts new file mode 100644 index 000000000..50ea23f89 --- /dev/null +++ b/packages/core/test/effect/observability.test.ts @@ -0,0 +1,46 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { resource } from "@opencode-ai/core/effect/observability" + +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", () => { + test("parses and decodes OTEL resource attributes", () => { + process.env.OTEL_RESOURCE_ATTRIBUTES = + "service.namespace=anomalyco,team=platform%2Cobservability,label=hello%3Dworld,key%2Fname=value%20here" + + expect(resource().attributes).toMatchObject({ + "service.namespace": "anomalyco", + team: "platform,observability", + label: "hello=world", + "key/name": "value here", + }) + }) + + test("drops OTEL resource attributes when any entry is invalid", () => { + process.env.OTEL_RESOURCE_ATTRIBUTES = "service.namespace=anomalyco,broken" + + expect(resource().attributes["service.namespace"]).toBeUndefined() + expect(resource().attributes["opencode.client"]).toBeDefined() + }) + + 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({ + "opencode.client": "cli", + "service.namespace": "anomalyco", + }) + expect(resource().attributes["service.instance.id"]).not.toBe("override") + }) +}) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 6ab24e26b..aff523a7e 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -50,7 +50,7 @@ import { Result, Schema } from "effect" import { LoadAPIKeyError } from "ai" import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2" import { applyPatch } from "diff" -import { InstallationVersion } from "@/installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" type ModeOption = { id: string; name: string; description?: string } type ModelOption = { modelId: string; name: string } diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index a5751ce83..3269b4a3d 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -11,7 +11,7 @@ import { Config } from "../../config" import { ConfigMCP } from "../../config/mcp" import { Instance } from "../../project/instance" import { Installation } from "../../installation" -import { InstallationVersion } from "../../installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import path from "path" import { Global } from "../../global" import { modify, applyEdits } from "jsonc-parser" diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 0874beee1..a9e044f18 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -3,7 +3,7 @@ import path from "path" import { pathToFileURL } from "url" import { UI } from "../ui" import { cmd } from "./cmd" -import { Flag } from "../../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { bootstrap } from "../bootstrap" import { EOL } from "os" import { Filesystem } from "../../util" diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index d5eee75dd..5f3211aa1 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -1,7 +1,7 @@ import { Server } from "../../server/server" import { cmd } from "./cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" -import { Flag } from "../../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" export const ServeCommand = cmd({ command: "serve", diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts index 8537a74d4..0d4bd96b0 100644 --- a/packages/opencode/src/cli/cmd/session.ts +++ b/packages/opencode/src/cli/cmd/session.ts @@ -5,7 +5,7 @@ import { SessionID } from "../../session/schema" import { bootstrap } from "../bootstrap" import { UI } from "../ui" import { Locale } from "../../util" -import { Flag } from "../../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Filesystem } from "../../util" import { Process } from "../../util" import { EOL } from "os" diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 30a597b91..015b0ed8f 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -16,7 +16,7 @@ import { on, } from "solid-js" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import semver from "semver" import { DialogProvider, useDialog } from "@tui/ui/dialog" import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider" diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 32342e772..7260a14f9 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -8,7 +8,7 @@ import { useProject } from "@tui/context/project" import { useKeybind } from "../context/keybind" import { useTheme } from "../context/theme" import { useSDK } from "../context/sdk" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { DialogSessionRename } from "./dialog-session-rename" import { Keybind } from "@/util" import { createDebouncedSignal } from "../util/signal" diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index a16c98a9f..899ab42ee 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -7,7 +7,7 @@ import { useProject } from "@tui/context/project" import { createMemo, createSignal, onMount } from "solid-js" import { setTimeout as sleep } from "node:timers/promises" import { errorData, errorMessage } from "@/util/error" -import * as Log from "@/util/log" +import * as Log from "@opencode-ai/core/util/log" import { useSDK } from "../context/sdk" import { useToast } from "../ui/toast" diff --git a/packages/opencode/src/cli/cmd/tui/component/error-component.tsx b/packages/opencode/src/cli/cmd/tui/component/error-component.tsx index c74d3bbc6..fcbd27ca9 100644 --- a/packages/opencode/src/cli/cmd/tui/component/error-component.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/error-component.tsx @@ -2,7 +2,7 @@ import { TextAttributes } from "@opentui/core" import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import * as Clipboard from "@tui/util/clipboard" import { createSignal } from "solid-js" -import { InstallationVersion } from "@/installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import { win32FlushInputBuffer } from "../win32" import { getScrollAcceleration } from "../util/scroll" diff --git a/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts b/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts index a7f50ddf9..d5599c170 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts @@ -3,7 +3,7 @@ import { type ParseError as JsoncParseError, applyEdits, modify, parse as parseJ import { unique } from "remeda" import z from "zod" import { TuiInfo, TuiOptions } from "./tui-schema" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Global } from "@/global" import { Filesystem, Log } from "@/util" import * as ConfigPaths from "@/config/paths" diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index 8dc6ab07e..64ec5f1c5 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -7,15 +7,15 @@ import { ConfigParse } from "@/config/parse" import * as ConfigPaths from "@/config/paths" import { migrateTuiConfig } from "./tui-migrate" import { TuiInfo } from "./tui-schema" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { isRecord } from "@/util/record" import { Global } from "@/global" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { CurrentWorkingDirectory } from "./cwd" import { ConfigPlugin } from "@/config/plugin" import { ConfigKeybinds } from "@/config/keybinds" -import { InstallationLocal, InstallationVersion } from "@/installation/version" -import { makeRuntime } from "@/effect/runtime" +import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version" +import { makeRuntime } from "@opencode-ai/core/effect/runtime" import { Filesystem, Log } from "@/util" import { ConfigVariable } from "@/config/variable" import { Npm } from "@/npm" diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index 6a240ceef..96fa54487 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -2,7 +2,7 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2" import type { GlobalEvent } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "./helper" import { createGlobalEmitter } from "@solid-primitives/event-bus" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { batch, onCleanup, onMount } from "solid-js" export type EventSource = { diff --git a/packages/opencode/src/cli/cmd/tui/layer.ts b/packages/opencode/src/cli/cmd/tui/layer.ts index 64cba08e8..785455334 100644 --- a/packages/opencode/src/cli/cmd/tui/layer.ts +++ b/packages/opencode/src/cli/cmd/tui/layer.ts @@ -1,6 +1,6 @@ import { Layer } from "effect" import { TuiConfig } from "./config/tui" import { Npm } from "@/npm" -import { Observability } from "@/effect/observability" +import { Observability } from "@opencode-ai/core/effect/observability" export const CliLayer = Observability.layer.pipe(Layer.merge(TuiConfig.layer), Layer.provide(Npm.defaultLayer)) diff --git a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx index 5bea48380..25ea3ac9e 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx +++ b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx @@ -18,7 +18,7 @@ import { DialogSelect, type DialogSelectOption as SelectOption } from "../ui/dia import { Prompt } from "../component/prompt" import { Slot as HostSlot } from "./slots" import type { useToast } from "../ui/toast" -import { InstallationVersion } from "@/installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" type RouteEntry = { key: symbol diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index 8eda7e022..95d050d7f 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -33,7 +33,7 @@ import { Global } from "@/global" import { Filesystem } from "@/util" import { Process } from "@/util" import { Flock } from "@opencode-ai/core/util/flock" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { INTERNAL_TUI_PLUGINS, type InternalTuiPlugin } from "./internal" import { setupSlots, Slot as View } from "./slots" import type { HostPluginApi, HostSlots } from "./slots" diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index c04e58ace..6ba43deb9 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -64,7 +64,7 @@ import { DialogForkFromTimeline } from "./dialog-fork-from-timeline" import { DialogSessionRename } from "../../component/dialog-session-rename" import { Sidebar } from "./sidebar" import { SubagentFooter } from "./subagent-footer.tsx" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import parsers from "../../../../../../parsers-config.ts" import * as Clipboard from "../../util/clipboard" diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index 6d92752ef..c49946df7 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -3,7 +3,7 @@ import { useSync } from "@tui/context/sync" import { createMemo, Show } from "solid-js" import { useTheme } from "../../context/theme" import { useTuiConfig } from "../../context/tui-config" -import { InstallationChannel, InstallationVersion } from "@/installation/version" +import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version" import { TuiPluginRuntime } from "../../plugin" import { getScrollAcceleration } from "../../util/scroll" diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index a2a53ecaf..60c5d5ece 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -15,7 +15,7 @@ import type { EventSource } from "./context/sdk" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" import { writeHeapSnapshot } from "v8" import { TuiConfig } from "./config/tui" -import { OPENCODE_PROCESS_ROLE, OPENCODE_RUN_ID, ensureRunID, sanitizedProcessEnv } from "@/util/opencode-process" +import { OPENCODE_PROCESS_ROLE, OPENCODE_RUN_ID, ensureRunID, sanitizedProcessEnv } from "@opencode-ai/core/util/opencode-process" import { validateSession } from "./validate-session" declare global { diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx index 29eb6fd4c..a5da735f6 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx @@ -4,7 +4,7 @@ import { useTheme } from "@tui/context/theme" import { MouseButton, Renderable, RGBA } from "@opentui/core" import { createStore } from "solid-js/store" import { useToast } from "./toast" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import * as Selection from "@tui/util/selection" export function Dialog( diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index 8cec99c61..df09d5cc9 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -7,11 +7,11 @@ import { Rpc } from "@/util" import { upgrade } from "@/cli/upgrade" import { Config } from "@/config" import { GlobalBus } from "@/bus/global" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { writeHeapSnapshot } from "node:v8" import { Heap } from "@/cli/heap" import { AppRuntime } from "@/effect/app-runtime" -import { ensureProcessMetadata } from "@/util/opencode-process" +import { ensureProcessMetadata } from "@opencode-ai/core/util/opencode-process" ensureProcessMetadata("worker") diff --git a/packages/opencode/src/cli/cmd/upgrade.ts b/packages/opencode/src/cli/cmd/upgrade.ts index b80648c24..a60b1fb0b 100644 --- a/packages/opencode/src/cli/cmd/upgrade.ts +++ b/packages/opencode/src/cli/cmd/upgrade.ts @@ -3,7 +3,7 @@ import { UI } from "../ui" import * as prompts from "@clack/prompts" import { AppRuntime } from "@/effect/app-runtime" import { Installation } from "../../installation" -import { InstallationVersion } from "../../installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" export const UpgradeCommand = { command: "upgrade [target]", diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index 9dd8796d6..19ee38ff5 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -2,7 +2,7 @@ import { Server } from "../../server/server" import { UI } from "../ui" import { cmd } from "./cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" -import { Flag } from "../../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import open from "open" import { networkInterfaces } from "os" diff --git a/packages/opencode/src/cli/heap.ts b/packages/opencode/src/cli/heap.ts index 87b7b2ebf..0cb4299c5 100644 --- a/packages/opencode/src/cli/heap.ts +++ b/packages/opencode/src/cli/heap.ts @@ -1,6 +1,6 @@ import path from "path" import { writeHeapSnapshot } from "node:v8" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Global } from "@/global" import { Log } from "@/util" diff --git a/packages/opencode/src/cli/upgrade.ts b/packages/opencode/src/cli/upgrade.ts index a3e3f3013..da0451c55 100644 --- a/packages/opencode/src/cli/upgrade.ts +++ b/packages/opencode/src/cli/upgrade.ts @@ -1,9 +1,9 @@ import { Bus } from "@/bus" import { Config } from "@/config" import { AppRuntime } from "@/effect/app-runtime" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Installation } from "@/installation" -import { InstallationVersion } from "@/installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" export async function upgrade() { const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal())) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 70ba14464..3958e1436 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -7,12 +7,12 @@ import { mergeDeep, pipe } from "remeda" import { Global } from "../global" import fsNode from "fs/promises" import { NamedError } from "@opencode-ai/core/util/error" -import { Flag } from "../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Auth } from "../auth" import { Env } from "../env" import { applyEdits, modify } from "jsonc-parser" import { Instance, type InstanceContext } from "../project/instance" -import { InstallationLocal, InstallationVersion } from "@/installation/version" +import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version" import { existsSync } from "fs" import { GlobalBus } from "@/bus/global" import { Event } from "../server/event" diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts index 572676fcc..df98bebb2 100644 --- a/packages/opencode/src/config/paths.ts +++ b/packages/opencode/src/config/paths.ts @@ -2,7 +2,7 @@ export * as ConfigPaths from "./paths" import path from "path" import { Filesystem } from "@/util" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Global } from "@/global" import { unique } from "remeda" import { JsonError } from "./error" diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index e1ebb613e..fbc4336fe 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -8,7 +8,7 @@ import { GlobalBus } from "@/bus/global" import { Auth } from "@/auth" import { SyncEvent } from "@/sync" import { EventSequenceTable, EventTable } from "@/sync/event.sql" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Log } from "@/util" import { Filesystem } from "@/util" import { ProjectID } from "@/project/schema" diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index 6c9d949b8..b4bdbfca4 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -1,6 +1,6 @@ import { Layer, ManagedRuntime } from "effect" import { attach } from "./run-service" -import * as Observability from "./observability" +import * as Observability from "@opencode-ai/core/effect/observability" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Bus } from "@/bus" @@ -47,7 +47,7 @@ import { Installation } from "@/installation" import { ShareNext } from "@/share" import { SessionShare } from "@/share" import { Npm } from "@/npm" -import { memoMap } from "./memo-map" +import { memoMap } from "@opencode-ai/core/effect/memo-map" export const AppLayer = Layer.mergeAll( Npm.defaultLayer, diff --git a/packages/opencode/src/effect/bootstrap-runtime.ts b/packages/opencode/src/effect/bootstrap-runtime.ts index 37698c43a..2d542bf24 100644 --- a/packages/opencode/src/effect/bootstrap-runtime.ts +++ b/packages/opencode/src/effect/bootstrap-runtime.ts @@ -10,8 +10,8 @@ import { Vcs } from "@/project" import { Snapshot } from "@/snapshot" import { Bus } from "@/bus" import { Config } from "@/config" -import * as Observability from "./observability" -import { memoMap } from "./memo-map" +import * as Observability from "@opencode-ai/core/effect/observability" +import { memoMap } from "@opencode-ai/core/effect/memo-map" export const BootstrapLayer = Layer.mergeAll( Config.defaultLayer, diff --git a/packages/opencode/src/effect/index.ts b/packages/opencode/src/effect/index.ts index 410ce00c2..623bd5f0b 100644 --- a/packages/opencode/src/effect/index.ts +++ b/packages/opencode/src/effect/index.ts @@ -1,5 +1,5 @@ export * as InstanceState from "./instance-state" export * as EffectBridge from "./bridge" export * as Runner from "./runner" -export * as Observability from "./observability" -export * as EffectLogger from "./logger" +export * as Observability from "@opencode-ai/core/effect/observability" +export * as EffectLogger from "@opencode-ai/core/effect/logger" diff --git a/packages/opencode/src/effect/instance-state.ts b/packages/opencode/src/effect/instance-state.ts index 7095657f5..dc9214494 100644 --- a/packages/opencode/src/effect/instance-state.ts +++ b/packages/opencode/src/effect/instance-state.ts @@ -1,5 +1,5 @@ import { Effect, Fiber, ScopedCache, Scope, Context } from "effect" -import * as EffectLogger from "./logger" +import * as EffectLogger from "@opencode-ai/core/effect/logger" import { Instance, type InstanceContext } from "@/project/instance" import { LocalContext } from "@/util" import { InstanceRef, WorkspaceRef } from "./instance-ref" diff --git a/packages/opencode/src/effect/logger.ts b/packages/opencode/src/effect/logger.ts deleted file mode 100644 index 0e58b8acb..000000000 --- a/packages/opencode/src/effect/logger.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Cause, Effect, Logger, References } from "effect" -import { Log } from "@/util" - -type Fields = Record - -const normalizeKey = (key: string) => (key === "sessionID" ? "session.id" : key) - -export interface Handle { - readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect - readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect - readonly warn: (msg?: unknown, extra?: Fields) => Effect.Effect - readonly error: (msg?: unknown, extra?: Fields) => Effect.Effect - readonly with: (extra: Fields) => Handle -} - -const clean = (input?: Fields): Fields => - Object.fromEntries( - Object.entries(input ?? {}) - .filter((entry) => entry[1] !== undefined && entry[1] !== null) - .map(([key, value]) => [normalizeKey(key), value]), - ) - -const text = (input: unknown): string => { - // oxlint-disable-next-line no-base-to-string - if (Array.isArray(input)) return input.map((item) => String(item)).join(" ") - // oxlint-disable-next-line no-base-to-string - return input === undefined ? "" : String(input) -} - -const call = (run: (msg?: unknown) => Effect.Effect, base: Fields, msg?: unknown, extra?: Fields) => { - const ann = clean({ ...base, ...extra }) - const fx = run(msg) - return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx -} - -export const logger = Logger.make((opts) => { - const extra = clean(opts.fiber.getRef(References.CurrentLogAnnotations)) - const now = opts.date.getTime() - for (const [key, start] of opts.fiber.getRef(References.CurrentLogSpans)) { - extra[`logSpan.${key}`] = `${now - start}ms` - } - if (opts.cause.reasons.length > 0) { - extra.cause = Cause.pretty(opts.cause) - } - - const svc = typeof extra.service === "string" ? extra.service : undefined - if (svc) delete extra.service - const log = svc ? Log.create({ service: svc }) : Log.Default - const msg = text(opts.message) - - switch (opts.logLevel) { - case "Trace": - case "Debug": - return log.debug(msg, extra) - case "Warn": - return log.warn(msg, extra) - case "Error": - case "Fatal": - return log.error(msg, extra) - default: - return log.info(msg, extra) - } -}) - -export const layer = Logger.layer([logger], { mergeWithExisting: false }) - -export const create = (base: Fields = {}): Handle => ({ - debug: (msg, extra) => call((item) => Effect.logDebug(item), base, msg, extra), - info: (msg, extra) => call((item) => Effect.logInfo(item), base, msg, extra), - warn: (msg, extra) => call((item) => Effect.logWarning(item), base, msg, extra), - error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra), - with: (extra) => create({ ...base, ...extra }), -}) diff --git a/packages/opencode/src/effect/memo-map.ts b/packages/opencode/src/effect/memo-map.ts deleted file mode 100644 index c797dbf42..000000000 --- a/packages/opencode/src/effect/memo-map.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Layer } from "effect" - -export const memoMap = Layer.makeMemoMapUnsafe() diff --git a/packages/opencode/src/effect/observability.ts b/packages/opencode/src/effect/observability.ts deleted file mode 100644 index fb81d5f5b..000000000 --- a/packages/opencode/src/effect/observability.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Effect, Layer, Logger } from "effect" -import { FetchHttpClient } from "effect/unstable/http" -import { OtlpLogger, OtlpSerialization } from "effect/unstable/observability" -import * as EffectLogger from "./logger" -import { Flag } from "@/flag/flag" -import { InstallationChannel, InstallationVersion } from "@/installation/version" -import { ensureProcessMetadata } from "@/util/opencode-process" - -const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT -export const enabled = !!base -const processID = crypto.randomUUID() - -const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS - ? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce( - (acc, x) => { - const [key, ...value] = x.split("=") - acc[key] = value.join("=") - return acc - }, - {} as Record, - ) - : undefined - -export function resource(): { serviceName: string; serviceVersion: string; attributes: Record } { - const processMetadata = ensureProcessMetadata("main") - const attributes: Record = (() => { - const value = process.env.OTEL_RESOURCE_ATTRIBUTES - if (!value) return {} - try { - return Object.fromEntries( - value.split(",").map((entry) => { - const index = entry.indexOf("=") - if (index < 1) throw new Error("Invalid OTEL_RESOURCE_ATTRIBUTES entry") - return [decodeURIComponent(entry.slice(0, index)), decodeURIComponent(entry.slice(index + 1))] - }), - ) - } catch { - return {} - } - })() - - return { - serviceName: "opencode", - serviceVersion: InstallationVersion, - attributes: { - ...attributes, - "deployment.environment.name": InstallationChannel, - "opencode.client": Flag.OPENCODE_CLIENT, - "opencode.process_role": processMetadata.processRole, - "opencode.run_id": processMetadata.runID, - "service.instance.id": processID, - }, - } -} - -function logs() { - return Logger.layer( - [ - EffectLogger.logger, - OtlpLogger.make({ - url: `${base}/v1/logs`, - resource: resource(), - headers, - }), - ], - { mergeWithExisting: false }, - ).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer)) -} - -const traces = async () => { - const NodeSdk = await import("@effect/opentelemetry/NodeSdk") - const OTLP = await import("@opentelemetry/exporter-trace-otlp-http") - const SdkBase = await import("@opentelemetry/sdk-trace-base") - - // @effect/opentelemetry creates a NodeTracerProvider but never calls - // register(), so the global @opentelemetry/api context manager stays - // as the no-op default. Non-Effect code (like the AI SDK) that calls - // tracer.startActiveSpan() relies on context.active() to find the - // parent span — without a real context manager every span starts a - // new trace. Registering AsyncLocalStorageContextManager fixes this. - const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks") - const { context } = await import("@opentelemetry/api") - const mgr = new AsyncLocalStorageContextManager() - mgr.enable() - context.setGlobalContextManager(mgr) - - return NodeSdk.layer(() => ({ - resource: resource(), - spanProcessor: new SdkBase.BatchSpanProcessor( - new OTLP.OTLPTraceExporter({ - url: `${base}/v1/traces`, - headers, - }), - ), - })) -} - -export const layer = !base - ? EffectLogger.layer - : Layer.unwrap( - Effect.gen(function* () { - const trace = yield* Effect.promise(traces) - return Layer.mergeAll(trace, logs()) - }), - ) - -export const Observability = { enabled, layer } diff --git a/packages/opencode/src/effect/run-service.ts b/packages/opencode/src/effect/run-service.ts index 98ff83ea5..2a54979af 100644 --- a/packages/opencode/src/effect/run-service.ts +++ b/packages/opencode/src/effect/run-service.ts @@ -3,10 +3,10 @@ import * as Context from "effect/Context" import { Instance } from "@/project/instance" import { LocalContext } from "@/util" import { InstanceRef, WorkspaceRef } from "./instance-ref" -import * as Observability from "./observability" +import * as Observability from "@opencode-ai/core/effect/observability" import { WorkspaceContext } from "@/control-plane/workspace-context" import type { InstanceContext } from "@/project/instance" -import { memoMap } from "./memo-map" +import { memoMap } from "@opencode-ai/core/effect/memo-map" type Refs = { instance?: InstanceContext diff --git a/packages/opencode/src/effect/runtime.ts b/packages/opencode/src/effect/runtime.ts deleted file mode 100644 index ad7872f0b..000000000 --- a/packages/opencode/src/effect/runtime.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Observability } from "./observability" -import { Layer, type Context, ManagedRuntime, type Effect } from "effect" -import { memoMap } from "./memo-map" - -export function makeRuntime(service: Context.Service, layer: Layer.Layer) { - let rt: ManagedRuntime.ManagedRuntime | undefined - const getRuntime = () => - (rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer) as Layer.Layer, { memoMap })) - - return { - runSync: (fn: (svc: S) => Effect.Effect) => getRuntime().runSync(service.use(fn)), - runPromiseExit: (fn: (svc: S) => Effect.Effect, options?: Effect.RunOptions) => - getRuntime().runPromiseExit(service.use(fn), options), - runPromise: (fn: (svc: S) => Effect.Effect, options?: Effect.RunOptions) => - getRuntime().runPromise(service.use(fn), options), - runFork: (fn: (svc: S) => Effect.Effect) => getRuntime().runFork(service.use(fn)), - runCallback: (fn: (svc: S) => Effect.Effect) => getRuntime().runCallback(service.use(fn)), - } -} diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index e31f53733..5602a4c41 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -9,7 +9,7 @@ import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import { Global } from "@/global" import { Log } from "@/util" -import { sanitizedProcessEnv } from "@/util/opencode-process" +import { sanitizedProcessEnv } from "@opencode-ai/core/util/opencode-process" import { which } from "@/util/which" import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 0ac98b9c2..57f3dda9f 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -8,7 +8,7 @@ import z from "zod" import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" import { InstanceState } from "@/effect" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Git } from "@/git" import { Instance } from "@/project/instance" import { lazy } from "@/util/lazy" diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts deleted file mode 100644 index 72c8931f5..000000000 --- a/packages/opencode/src/flag/flag.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Config } from "effect" - -function truthy(key: string) { - const value = process.env[key]?.toLowerCase() - return value === "true" || value === "1" -} - -function falsy(key: string) { - const value = process.env[key]?.toLowerCase() - return value === "false" || value === "0" -} - -function number(key: string) { - const value = process.env[key] - if (!value) return undefined - const parsed = Number(value) - return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined -} - -const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL") -const OPENCODE_DISABLE_CLAUDE_CODE = truthy("OPENCODE_DISABLE_CLAUDE_CODE") -const OPENCODE_DISABLE_CLAUDE_CODE_SKILLS = - OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS") -const copy = process.env["OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"] - -export const Flag = { - OTEL_EXPORTER_OTLP_ENDPOINT: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"], - OTEL_EXPORTER_OTLP_HEADERS: process.env["OTEL_EXPORTER_OTLP_HEADERS"], - - OPENCODE_AUTO_SHARE: truthy("OPENCODE_AUTO_SHARE"), - OPENCODE_AUTO_HEAP_SNAPSHOT: truthy("OPENCODE_AUTO_HEAP_SNAPSHOT"), - OPENCODE_GIT_BASH_PATH: process.env["OPENCODE_GIT_BASH_PATH"], - OPENCODE_CONFIG: process.env["OPENCODE_CONFIG"], - OPENCODE_CONFIG_CONTENT: process.env["OPENCODE_CONFIG_CONTENT"], - OPENCODE_DISABLE_AUTOUPDATE: truthy("OPENCODE_DISABLE_AUTOUPDATE"), - OPENCODE_ALWAYS_NOTIFY_UPDATE: truthy("OPENCODE_ALWAYS_NOTIFY_UPDATE"), - OPENCODE_DISABLE_PRUNE: truthy("OPENCODE_DISABLE_PRUNE"), - OPENCODE_DISABLE_TERMINAL_TITLE: truthy("OPENCODE_DISABLE_TERMINAL_TITLE"), - OPENCODE_SHOW_TTFD: truthy("OPENCODE_SHOW_TTFD"), - OPENCODE_PERMISSION: process.env["OPENCODE_PERMISSION"], - OPENCODE_DISABLE_DEFAULT_PLUGINS: truthy("OPENCODE_DISABLE_DEFAULT_PLUGINS"), - OPENCODE_DISABLE_LSP_DOWNLOAD: truthy("OPENCODE_DISABLE_LSP_DOWNLOAD"), - OPENCODE_ENABLE_EXPERIMENTAL_MODELS: truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS"), - OPENCODE_DISABLE_AUTOCOMPACT: truthy("OPENCODE_DISABLE_AUTOCOMPACT"), - OPENCODE_DISABLE_MODELS_FETCH: truthy("OPENCODE_DISABLE_MODELS_FETCH"), - OPENCODE_DISABLE_MOUSE: truthy("OPENCODE_DISABLE_MOUSE"), - OPENCODE_DISABLE_CLAUDE_CODE, - OPENCODE_DISABLE_CLAUDE_CODE_PROMPT: OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT"), - OPENCODE_DISABLE_CLAUDE_CODE_SKILLS, - OPENCODE_DISABLE_EXTERNAL_SKILLS: OPENCODE_DISABLE_CLAUDE_CODE_SKILLS || truthy("OPENCODE_DISABLE_EXTERNAL_SKILLS"), - OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"], - OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"], - OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"], - OPENCODE_ENABLE_QUESTION_TOOL: truthy("OPENCODE_ENABLE_QUESTION_TOOL"), - - // Experimental - OPENCODE_EXPERIMENTAL, - OPENCODE_EXPERIMENTAL_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_FILEWATCHER").pipe( - Config.withDefault(false), - ), - OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER").pipe( - Config.withDefault(false), - ), - OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY"), - OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT: - copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"), - OPENCODE_ENABLE_EXA: truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA"), - OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: number("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS"), - OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: number("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX"), - OPENCODE_EXPERIMENTAL_OXFMT: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_OXFMT"), - OPENCODE_EXPERIMENTAL_LSP_TY: truthy("OPENCODE_EXPERIMENTAL_LSP_TY"), - OPENCODE_EXPERIMENTAL_LSP_TOOL: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL"), - OPENCODE_EXPERIMENTAL_PLAN_MODE: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE"), - OPENCODE_EXPERIMENTAL_MARKDOWN: !falsy("OPENCODE_EXPERIMENTAL_MARKDOWN"), - OPENCODE_MODELS_URL: process.env["OPENCODE_MODELS_URL"], - OPENCODE_MODELS_PATH: process.env["OPENCODE_MODELS_PATH"], - OPENCODE_DISABLE_EMBEDDED_WEB_UI: truthy("OPENCODE_DISABLE_EMBEDDED_WEB_UI"), - OPENCODE_DB: process.env["OPENCODE_DB"], - OPENCODE_DISABLE_CHANNEL_DB: truthy("OPENCODE_DISABLE_CHANNEL_DB"), - OPENCODE_SKIP_MIGRATIONS: truthy("OPENCODE_SKIP_MIGRATIONS"), - OPENCODE_STRICT_CONFIG_DEPS: truthy("OPENCODE_STRICT_CONFIG_DEPS"), - - OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"], - OPENCODE_EXPERIMENTAL_HTTPAPI: truthy("OPENCODE_EXPERIMENTAL_HTTPAPI"), - OPENCODE_EXPERIMENTAL_WORKSPACES: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WORKSPACES"), - - // Evaluated at access time (not module load) because tests, the CLI, and - // external tooling set these env vars at runtime. - get OPENCODE_DISABLE_PROJECT_CONFIG() { - return truthy("OPENCODE_DISABLE_PROJECT_CONFIG") - }, - get OPENCODE_TUI_CONFIG() { - return process.env["OPENCODE_TUI_CONFIG"] - }, - get OPENCODE_CONFIG_DIR() { - return process.env["OPENCODE_CONFIG_DIR"] - }, - get OPENCODE_PURE() { - return truthy("OPENCODE_PURE") - }, - get OPENCODE_PLUGIN_META_FILE() { - return process.env["OPENCODE_PLUGIN_META_FILE"] - }, - get OPENCODE_CLIENT() { - return process.env["OPENCODE_CLIENT"] ?? "cli" - }, -} diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index 03f836527..eefafd575 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -3,7 +3,7 @@ import type { InstanceContext } from "../project/instance" import { Filesystem } from "../util" import { Process } from "../util" import { which } from "../util/which" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" export interface Context extends Pick {} diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index c27f6b740..3764e1b1c 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -11,7 +11,7 @@ import { UninstallCommand } from "./cli/cmd/uninstall" import { ModelsCommand } from "./cli/cmd/models" import { UI } from "./cli/ui" import { Installation } from "./installation" -import { InstallationVersion } from "./installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import { NamedError } from "@opencode-ai/core/util/error" import { FormatError } from "./cli/error" import { ServeCommand } from "./cli/cmd/serve" @@ -38,7 +38,7 @@ import { errorMessage } from "./util/error" import { PluginCommand } from "./cli/cmd/plug" import { Heap } from "./cli/heap" import { drizzle } from "drizzle-orm/bun-sqlite" -import { ensureProcessMetadata } from "./util/opencode-process" +import { ensureProcessMetadata } from "@opencode-ai/core/util/opencode-process" const processMetadata = ensureProcessMetadata("main") diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index bb3de3f3b..1a39d3c61 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -6,11 +6,11 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import path from "path" import z from "zod" import { BusEvent } from "@/bus/bus-event" -import { Flag } from "../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Log } from "../util" import semver from "semver" -import { InstallationChannel, InstallationVersion } from "./version" +import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version" const log = Log.create({ service: "installation" }) diff --git a/packages/opencode/src/installation/version.ts b/packages/opencode/src/installation/version.ts deleted file mode 100644 index 25d9cd99a..000000000 --- a/packages/opencode/src/installation/version.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare global { - const OPENCODE_VERSION: string - const OPENCODE_CHANNEL: string -} - -export const InstallationVersion = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local" -export const InstallationChannel = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local" -export const InstallationLocal = InstallationChannel === "local" diff --git a/packages/opencode/src/lsp/lsp.ts b/packages/opencode/src/lsp/lsp.ts index 5078cbadb..96741b687 100644 --- a/packages/opencode/src/lsp/lsp.ts +++ b/packages/opencode/src/lsp/lsp.ts @@ -7,7 +7,7 @@ import { pathToFileURL, fileURLToPath } from "url" import * as LSPServer from "./server" import z from "zod" import { Config } from "../config" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Process } from "../util" import { spawn as lspspawn } from "./launch" import { Effect, Layer, Context, Schema } from "effect" diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index ef001888e..9b585c9fb 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -7,7 +7,7 @@ import { text } from "node:stream/consumers" import fs from "fs/promises" import { Filesystem } from "../util" import type { InstanceContext } from "../project/instance" -import { Flag } from "../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Archive } from "../util" import { Process } from "../util" import { which } from "../util/which" diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 8b2562dc4..23862db63 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -15,7 +15,7 @@ import { Log } from "../util" import { NamedError } from "@opencode-ai/core/util/error" import z from "zod/v4" import { Installation } from "../installation" -import { InstallationVersion } from "../installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import { withTimeout } from "@/util/timeout" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { McpOAuthProvider } from "./oauth-provider" diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts index d876b0e52..ca67491d0 100644 --- a/packages/opencode/src/npm/index.ts +++ b/packages/opencode/src/npm/index.ts @@ -11,10 +11,10 @@ import { NodeFileSystem } from "@effect/platform-node" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Global } from "@opencode-ai/core/global" import { EffectFlock } from "@opencode-ai/core/util/effect-flock" +import { makeRuntime } from "@opencode-ai/core/effect/runtime" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import * as CrossSpawnSpawner from "../effect/cross-spawn-spawner" -import { makeRuntime } from "../effect/runtime" export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", { add: Schema.Array(Schema.String).pipe(Schema.optional), diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index 60d2d5b47..337a4e91f 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -1,7 +1,7 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin" import { Log } from "../util" import { Installation } from "../installation" -import { InstallationVersion } from "../installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import { OAUTH_DUMMY_KEY } from "../auth" import os from "os" import { setTimeout as sleep } from "node:timers/promises" diff --git a/packages/opencode/src/plugin/github-copilot/copilot.ts b/packages/opencode/src/plugin/github-copilot/copilot.ts index 9b6f54459..6f0e46402 100644 --- a/packages/opencode/src/plugin/github-copilot/copilot.ts +++ b/packages/opencode/src/plugin/github-copilot/copilot.ts @@ -1,6 +1,6 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin" import type { Model } from "@opencode-ai/sdk/v2" -import { InstallationVersion } from "@/installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import { iife } from "@/util/iife" import { Log } from "../../util" import { setTimeout as sleep } from "node:timers/promises" diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 4587d8fb1..762d38be3 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -9,7 +9,7 @@ import { Config } from "../config" import { Bus } from "../bus" import { Log } from "../util" import { createOpencodeClient } from "@opencode-ai/sdk" -import { Flag } from "../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { CodexAuthPlugin } from "./codex" import { Session } from "../session" import { NamedError } from "@opencode-ai/core/util/error" diff --git a/packages/opencode/src/plugin/loader.ts b/packages/opencode/src/plugin/loader.ts index e61612561..f8da9d6a9 100644 --- a/packages/opencode/src/plugin/loader.ts +++ b/packages/opencode/src/plugin/loader.ts @@ -9,7 +9,7 @@ import { type PluginSource, } from "./shared" import { ConfigPlugin } from "@/config/plugin" -import { InstallationVersion } from "@/installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" export namespace PluginLoader { // A normalized plugin declaration derived from config before any filesystem or npm work happens. diff --git a/packages/opencode/src/plugin/meta.ts b/packages/opencode/src/plugin/meta.ts index 4c14a0dec..ab067c592 100644 --- a/packages/opencode/src/plugin/meta.ts +++ b/packages/opencode/src/plugin/meta.ts @@ -1,7 +1,7 @@ import path from "path" import { fileURLToPath } from "url" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Global } from "@/global" import { Filesystem } from "@/util" import { Flock } from "@opencode-ai/core/util/flock" diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 88d033921..e622464b4 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -3,7 +3,7 @@ import { and, Database, eq } from "../storage" import { ProjectTable } from "./project.sql" import { SessionTable } from "../session/session.sql" import { Log } from "../util" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" import { which } from "../util/which" diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index e52464d6d..c3df06abc 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -3,7 +3,7 @@ import { Log } from "../util" import path from "path" import { Schema } from "effect" import { Installation } from "../installation" -import { Flag } from "../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { lazy } from "@/util/lazy" import { Filesystem } from "../util" import { Flock } from "@opencode-ai/core/util/flock" diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index d6ccbacfc..96039af9b 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -11,8 +11,8 @@ import { type LanguageModelV3 } from "@ai-sdk/provider" import * as ModelsDev from "./models" import { Auth } from "../auth" import { Env } from "../env" -import { InstallationVersion } from "../installation/version" -import { Flag } from "../flag/flag" +import { InstallationVersion } from "@opencode-ai/core/installation/version" +import { Flag } from "@opencode-ai/core/flag/flag" import { zod } from "@/util/effect-zod" import { namedSchemaError } from "@/util/named-schema-error" import { iife } from "@/util/iife" diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 50529c4dd..67b02c089 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -5,7 +5,7 @@ import type { JSONSchema } from "zod/v4/core" import type * as Provider from "./provider" import type * as ModelsDev from "./models" import { iife } from "@/util/iife" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" type Modality = NonNullable["input"][number] diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts index 55d9dee79..aceba8821 100644 --- a/packages/opencode/src/server/middleware.ts +++ b/packages/opencode/src/server/middleware.ts @@ -6,7 +6,7 @@ import type { ContentfulStatusCode } from "hono/utils/http-status" import type { ErrorHandler, MiddlewareHandler } from "hono" import { HTTPException } from "hono/http-exception" import { Log } from "../util" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { basicAuth } from "hono/basic-auth" import { cors } from "hono/cors" import { compress } from "hono/compress" diff --git a/packages/opencode/src/server/routes/global.ts b/packages/opencode/src/server/routes/global.ts index a1199a469..c2f8b695d 100644 --- a/packages/opencode/src/server/routes/global.ts +++ b/packages/opencode/src/server/routes/global.ts @@ -10,7 +10,7 @@ import { AppRuntime } from "@/effect/app-runtime" import { AsyncQueue } from "@/util/queue" import { Instance } from "../../project/instance" import { Installation } from "@/installation" -import { InstallationVersion } from "@/installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import { Log } from "../../util" import { lazy } from "../../util/lazy" import { Config } from "../../config" diff --git a/packages/opencode/src/server/routes/instance/httpapi/auth.ts b/packages/opencode/src/server/routes/instance/httpapi/auth.ts index fe72b7822..2fe196b56 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/auth.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/auth.ts @@ -1,6 +1,6 @@ import { Effect, Encoding, Layer, Redacted, Schema } from "effect" import { HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" class Unauthorized extends Schema.TaggedErrorClass()( "Unauthorized", diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index 903cd103b..17c3ba4b4 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -18,7 +18,7 @@ import { ProjectApi, projectHandlers } from "./project" import { ProviderApi, providerHandlers } from "./provider" import { QuestionApi, questionHandlers } from "./question" import { WorkspaceApi, workspaceHandlers } from "./workspace" -import { memoMap } from "@/effect/memo-map" +import { memoMap } from "@opencode-ai/core/effect/memo-map" const Query = Schema.Struct({ directory: Schema.optional(Schema.String), diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index bc9d2b2ad..df50be406 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -14,7 +14,7 @@ import { LSP } from "@/lsp" import { Command } from "@/command" import { QuestionRoutes } from "./question" import { PermissionRoutes } from "./permission" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { ExperimentalHttpApiServer } from "./httpapi/server" import { FilePaths } from "./httpapi/file" import { InstancePaths } from "./httpapi/instance" diff --git a/packages/opencode/src/server/routes/ui.ts b/packages/opencode/src/server/routes/ui.ts index d449cd1c4..5e47e6bf7 100644 --- a/packages/opencode/src/server/routes/ui.ts +++ b/packages/opencode/src/server/routes/ui.ts @@ -1,4 +1,4 @@ -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Hono } from "hono" import { proxy } from "hono/proxy" import { getMimeType } from "hono/utils/mime" diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index d74de559d..fb278f268 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -3,7 +3,7 @@ import { Hono } from "hono" import { adapter } from "#hono" import { lazy } from "@/util/lazy" import { Log } from "@/util" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { WorkspaceID } from "@/control-plane/schema" import { MDNS } from "./mdns" import { AuthMiddleware, CompressionMiddleware, CorsMiddleware, ErrorMiddleware, LoggerMiddleware } from "./middleware" diff --git a/packages/opencode/src/server/workspace.ts b/packages/opencode/src/server/workspace.ts index d30a117d6..3f71bf2f7 100644 --- a/packages/opencode/src/server/workspace.ts +++ b/packages/opencode/src/server/workspace.ts @@ -4,7 +4,7 @@ import { getAdaptor } from "@/control-plane/adaptors" import { WorkspaceID } from "@/control-plane/schema" import { WorkspaceContext } from "@/control-plane/workspace-context" import { Workspace } from "@/control-plane/workspace" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { InstanceBootstrap } from "@/project/bootstrap" import { Instance } from "@/project/instance" import { Session } from "@/session" diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index a18a55584..56fca5359 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -4,7 +4,7 @@ import { Effect, Layer, Context } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" import { Config } from "@/config" import { InstanceState } from "@/effect" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { withTransientReadRetry } from "@/util/effect-http-client" import { Global } from "../global" diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index b72f873de..d5ff4e61c 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -12,7 +12,7 @@ import type { Agent } from "@/agent/agent" import type { MessageV2 } from "./message-v2" import { Plugin } from "@/plugin" import { SystemPrompt } from "./system" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Permission } from "@/permission" import { PermissionID } from "@/permission/schema" import { Bus } from "@/bus" @@ -20,7 +20,7 @@ import { Wildcard } from "@/util" import { SessionID } from "@/session/schema" import { Auth } from "@/auth" import { Installation } from "@/installation" -import { InstallationVersion } from "@/installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import { EffectBridge } from "@/effect" import * as Option from "effect/Option" import * as OtelTracer from "@effect/opentelemetry/Tracer" diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 8e227e602..708961168 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -24,7 +24,7 @@ import MAX_STEPS from "../session/prompt/max-steps.txt" import { ToolRegistry } from "../tool" import { MCP } from "../mcp" import { LSP } from "../lsp" -import { Flag } from "../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { ulid } from "ulid" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 472339b05..db77c0e21 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -5,8 +5,8 @@ import { Bus } from "@/bus" import { Decimal } from "decimal.js" import z from "zod" import { type ProviderMetadata, type LanguageModelUsage } from "ai" -import { Flag } from "../flag/flag" -import { InstallationVersion } from "../installation/version" +import { Flag } from "@opencode-ai/core/flag/flag" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import { Database, NotFoundError, eq, and, gte, isNull, desc, like, inArray, lt } from "../storage" import { SyncEvent } from "../sync" diff --git a/packages/opencode/src/share/session.ts b/packages/opencode/src/share/session.ts index 63b767078..c5394716b 100644 --- a/packages/opencode/src/share/session.ts +++ b/packages/opencode/src/share/session.ts @@ -3,7 +3,7 @@ import { SessionID } from "@/session/schema" import { SyncEvent } from "@/sync" import { Effect, Layer, Scope, Context } from "effect" import { Config } from "../config" -import { Flag } from "../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import * as ShareNext from "./share-next" export interface Interface { diff --git a/packages/opencode/src/shell/shell.ts b/packages/opencode/src/shell/shell.ts index 60643c10b..1c8996194 100644 --- a/packages/opencode/src/shell/shell.ts +++ b/packages/opencode/src/shell/shell.ts @@ -1,4 +1,4 @@ -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { lazy } from "@/util/lazy" import { Filesystem } from "@/util" import { which } from "@/util/which" diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index e5282e250..a425e13d5 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -7,7 +7,7 @@ import { NamedError } from "@opencode-ai/core/util/error" import type { Agent } from "@/agent/agent" import { Bus } from "@/bus" import { InstanceState } from "@/effect" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Global } from "@/global" import { Permission } from "@/permission" import { AppFileSystem } from "@opencode-ai/core/filesystem" diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 67f5f1289..898810581 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -10,8 +10,8 @@ import { NamedError } from "@opencode-ai/core/util/error" import z from "zod" import path from "path" import { readFileSync, readdirSync, existsSync } from "fs" -import { Flag } from "../flag/flag" -import { InstallationChannel } from "../installation/version" +import { Flag } from "@opencode-ai/core/flag/flag" +import { InstallationChannel } from "@opencode-ai/core/installation/version" import { InstanceState } from "@/effect" import { iife } from "@/util/iife" import { init } from "#db" diff --git a/packages/opencode/src/sync/index.ts b/packages/opencode/src/sync/index.ts index 35a5abd0b..da33b7aa9 100644 --- a/packages/opencode/src/sync/index.ts +++ b/packages/opencode/src/sync/index.ts @@ -7,7 +7,7 @@ import { Instance } from "@/project/instance" import { EventSequenceTable, EventTable } from "./event.sql" import { WorkspaceContext } from "@/control-plane/workspace-context" import { EventID } from "./schema" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Schema as EffectSchema } from "effect" import { zodObject } from "@/util/effect-zod" import type { DeepMutable } from "@/util/schema" diff --git a/packages/opencode/src/temporary.ts b/packages/opencode/src/temporary.ts index bbb97e0f0..7747eb1e9 100644 --- a/packages/opencode/src/temporary.ts +++ b/packages/opencode/src/temporary.ts @@ -1,6 +1,6 @@ import yargs from "yargs" import { TuiThreadCommand } from "./cli/cmd/tui/thread" -import { InstallationVersion } from "./installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import { hideBin } from "yargs/helpers" import { Log } from "./node" diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 1b8875326..eeba5ebd6 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -11,7 +11,7 @@ import { Language, type Node } from "web-tree-sitter" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { fileURLToPath } from "url" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Shell } from "@/shell/shell" import { BashArity } from "@/permission/arity" diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 629c57965..b7fa696c8 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -23,7 +23,7 @@ import { Provider } from "../provider" import { ProviderID, type ModelID } from "../provider/schema" import { WebSearchTool } from "./websearch" import { CodeSearchTool } from "./codesearch" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Log } from "@/util" import { LspTool } from "./lsp" import * as Truncate from "./truncate" diff --git a/packages/opencode/src/util/index.ts b/packages/opencode/src/util/index.ts index f051ad964..c67a3d140 100644 --- a/packages/opencode/src/util/index.ts +++ b/packages/opencode/src/util/index.ts @@ -5,7 +5,7 @@ export * as Keybind from "./keybind" export * as LocalContext from "./local-context" export * as Locale from "./locale" export * as Lock from "./lock" -export * as Log from "./log" +export * as Log from "@opencode-ai/core/util/log" export * as Process from "./process" export * as Rpc from "./rpc" export * as Token from "./token" diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts deleted file mode 100644 index e335a8b43..000000000 --- a/packages/opencode/src/util/log.ts +++ /dev/null @@ -1,185 +0,0 @@ -import path from "path" -import fs from "fs/promises" -import { createWriteStream } from "fs" -import { Global } from "../global" -import z from "zod" -import { Glob } from "@opencode-ai/core/util/glob" - -export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" }) -export type Level = z.infer - -const levelPriority: Record = { - DEBUG: 0, - INFO: 1, - WARN: 2, - ERROR: 3, -} -const keep = 10 - -let level: Level = "INFO" - -function shouldLog(input: Level): boolean { - return levelPriority[input] >= levelPriority[level] -} - -export type Logger = { - debug(message?: any, extra?: Record): void - info(message?: any, extra?: Record): void - error(message?: any, extra?: Record): void - warn(message?: any, extra?: Record): void - tag(key: string, value: string): Logger - clone(): Logger - time( - message: string, - extra?: Record, - ): { - stop(): void - [Symbol.dispose](): void - } -} - -const loggers = new Map() - -export const Default = create({ service: "default" }) - -export interface Options { - print: boolean - dev?: boolean - level?: Level -} - -let logpath = "" -export function file() { - return logpath -} -let write = (msg: any) => { - process.stderr.write(msg) - return msg.length -} - -export async function init(options: Options) { - if (options.level) level = options.level - void cleanup(Global.Path.log) - if (options.print) return - logpath = path.join( - Global.Path.log, - options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log", - ) - await fs.truncate(logpath).catch(() => {}) - const stream = createWriteStream(logpath, { flags: "a" }) - write = async (msg: any) => { - return new Promise((resolve, reject) => { - stream.write(msg, (err) => { - if (err) reject(err) - else resolve(msg.length) - }) - }) - } -} - -async function cleanup(dir: string) { - const files = ( - await Glob.scan("????-??-??T??????.log", { - cwd: dir, - absolute: false, - include: "file", - }).catch(() => []) - ) - .filter((file) => path.basename(file) === file) - .sort() - if (files.length <= keep) return - - const doomed = files.slice(0, -keep) - await Promise.all(doomed.map((file) => fs.unlink(path.join(dir, file)).catch(() => {}))) -} - -function formatError(error: Error, depth = 0): string { - const result = error.message - return error.cause instanceof Error && depth < 10 - ? result + " Caused by: " + formatError(error.cause, depth + 1) - : result -} - -let last = Date.now() -export function create(tags?: Record) { - tags = tags || {} - - const service = tags["service"] - if (service && typeof service === "string") { - const cached = loggers.get(service) - if (cached) { - return cached - } - } - - function build(message: any, extra?: Record) { - const prefix = Object.entries({ - ...tags, - ...extra, - }) - .filter(([_, value]) => value !== undefined && value !== null) - .map(([key, value]) => { - const prefix = `${key}=` - if (value instanceof Error) return prefix + formatError(value) - if (typeof value === "object") return prefix + JSON.stringify(value) - return prefix + value - }) - .join(" ") - const next = new Date() - const diff = next.getTime() - last - last = next.getTime() - return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n" - } - const result: Logger = { - debug(message?: any, extra?: Record) { - if (shouldLog("DEBUG")) { - write("DEBUG " + build(message, extra)) - } - }, - info(message?: any, extra?: Record) { - if (shouldLog("INFO")) { - write("INFO " + build(message, extra)) - } - }, - error(message?: any, extra?: Record) { - if (shouldLog("ERROR")) { - write("ERROR " + build(message, extra)) - } - }, - warn(message?: any, extra?: Record) { - if (shouldLog("WARN")) { - write("WARN " + build(message, extra)) - } - }, - tag(key: string, value: string) { - if (tags) tags[key] = value - return result - }, - clone() { - return create({ ...tags }) - }, - time(message: string, extra?: Record) { - const now = Date.now() - result.info(message, { status: "started", ...extra }) - function stop() { - result.info(message, { - status: "completed", - duration: Date.now() - now, - ...extra, - }) - } - return { - stop, - [Symbol.dispose]() { - stop() - }, - } - }, - } - - if (service && typeof service === "string") { - loggers.set(service, result) - } - - return result -} diff --git a/packages/opencode/src/util/opencode-process.ts b/packages/opencode/src/util/opencode-process.ts deleted file mode 100644 index f59270ad2..000000000 --- a/packages/opencode/src/util/opencode-process.ts +++ /dev/null @@ -1,24 +0,0 @@ -export const OPENCODE_RUN_ID = "OPENCODE_RUN_ID" -export const OPENCODE_PROCESS_ROLE = "OPENCODE_PROCESS_ROLE" - -export function ensureRunID() { - return (process.env[OPENCODE_RUN_ID] ??= crypto.randomUUID()) -} - -export function ensureProcessRole(fallback: "main" | "worker") { - return (process.env[OPENCODE_PROCESS_ROLE] ??= fallback) -} - -export function ensureProcessMetadata(fallback: "main" | "worker") { - return { - runID: ensureRunID(), - processRole: ensureProcessRole(fallback), - } -} - -export function sanitizedProcessEnv(overrides?: Record) { - const env = Object.fromEntries( - Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined), - ) - return overrides ? Object.assign(env, overrides) : env -} diff --git a/packages/opencode/test/effect/observability.test.ts b/packages/opencode/test/effect/observability.test.ts deleted file mode 100644 index d06220282..000000000 --- a/packages/opencode/test/effect/observability.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { afterEach, describe, expect, test } from "bun:test" -import { resource } from "../../src/effect/observability" - -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", () => { - test("parses and decodes OTEL resource attributes", () => { - process.env.OTEL_RESOURCE_ATTRIBUTES = - "service.namespace=anomalyco,team=platform%2Cobservability,label=hello%3Dworld,key%2Fname=value%20here" - - expect(resource().attributes).toMatchObject({ - "service.namespace": "anomalyco", - team: "platform,observability", - label: "hello=world", - "key/name": "value here", - }) - }) - - test("drops OTEL resource attributes when any entry is invalid", () => { - process.env.OTEL_RESOURCE_ATTRIBUTES = "service.namespace=anomalyco,broken" - - expect(resource().attributes["service.namespace"]).toBeUndefined() - expect(resource().attributes["opencode.client"]).toBeDefined() - }) - - 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({ - "opencode.client": "cli", - "service.namespace": "anomalyco", - }) - expect(resource().attributes["service.instance.id"]).not.toBe("override") - }) -}) diff --git a/packages/opencode/test/installation/installation.test.ts b/packages/opencode/test/installation/installation.test.ts index 0d3e92989..469ebb714 100644 --- a/packages/opencode/test/installation/installation.test.ts +++ b/packages/opencode/test/installation/installation.test.ts @@ -3,7 +3,7 @@ import { Effect, Layer, Stream } from "effect" import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { Installation } from "../../src/installation" -import { InstallationChannel } from "../../src/installation/version" +import { InstallationChannel } from "@opencode-ai/core/installation/version" const encoder = new TextEncoder() diff --git a/packages/opencode/test/plugin/workspace-adaptor.test.ts b/packages/opencode/test/plugin/workspace-adaptor.test.ts index e74522c8b..2695e9b28 100644 --- a/packages/opencode/test/plugin/workspace-adaptor.test.ts +++ b/packages/opencode/test/plugin/workspace-adaptor.test.ts @@ -7,7 +7,7 @@ import { tmpdir } from "../fixture/fixture" const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1" -const { Flag } = await import("../../src/flag/flag") +const { Flag } = await import("@opencode-ai/core/flag/flag") const { Plugin } = await import("../../src/plugin/index") const { Workspace } = await import("../../src/control-plane/workspace") const { Instance } = await import("../../src/project/instance") diff --git a/packages/opencode/test/server/httpapi-bridge.test.ts b/packages/opencode/test/server/httpapi-bridge.test.ts index 3f0de26a1..35f173371 100644 --- a/packages/opencode/test/server/httpapi-bridge.test.ts +++ b/packages/opencode/test/server/httpapi-bridge.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, test } from "bun:test" import type { UpgradeWebSocket } from "hono/ws" -import { Flag } from "../../src/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Instance } from "../../src/project/instance" import { InstanceRoutes } from "../../src/server/routes/instance" import { FilePaths } from "../../src/server/routes/instance/httpapi/file" diff --git a/packages/opencode/test/server/httpapi-instance.test.ts b/packages/opencode/test/server/httpapi-instance.test.ts index f25d29518..6bd30d2ca 100644 --- a/packages/opencode/test/server/httpapi-instance.test.ts +++ b/packages/opencode/test/server/httpapi-instance.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, test } from "bun:test" import type { UpgradeWebSocket } from "hono/ws" import path from "path" -import { Flag } from "../../src/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Instance } from "../../src/project/instance" import { InstanceRoutes } from "../../src/server/routes/instance" import { InstancePaths } from "../../src/server/routes/instance/httpapi/instance" diff --git a/packages/opencode/test/storage/db.test.ts b/packages/opencode/test/storage/db.test.ts index 6beb95ac5..2bfaae1da 100644 --- a/packages/opencode/test/storage/db.test.ts +++ b/packages/opencode/test/storage/db.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import path from "path" import { Global } from "../../src/global" -import { InstallationChannel } from "../../src/installation/version" +import { InstallationChannel } from "@opencode-ai/core/installation/version" import { Database } from "../../src/storage" describe("Database.Path", () => { diff --git a/packages/opencode/test/sync/index.test.ts b/packages/opencode/test/sync/index.test.ts index d50f0d7c9..c9f6812ca 100644 --- a/packages/opencode/test/sync/index.test.ts +++ b/packages/opencode/test/sync/index.test.ts @@ -7,7 +7,7 @@ import { SyncEvent } from "../../src/sync" import { Database } from "../../src/storage" import { EventTable } from "../../src/sync/event.sql" import { Identifier } from "../../src/id/id" -import { Flag } from "../../src/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { initProjectors } from "../../src/server/projectors" const original = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES diff --git a/packages/opencode/test/workspace/workspace-restore.test.ts b/packages/opencode/test/workspace/workspace-restore.test.ts index ad6ac2c5f..2f8b236b5 100644 --- a/packages/opencode/test/workspace/workspace-restore.test.ts +++ b/packages/opencode/test/workspace/workspace-restore.test.ts @@ -6,7 +6,7 @@ import { registerAdaptor } from "../../src/control-plane/adaptors" import type { WorkspaceAdaptor } from "../../src/control-plane/types" import { Workspace } from "../../src/control-plane/workspace" import { AppRuntime } from "../../src/effect/app-runtime" -import { Flag } from "../../src/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { ModelID, ProviderID } from "../../src/provider/schema" import { Instance } from "../../src/project/instance" import { Session as SessionNs } from "../../src/session" -- cgit v1.2.3