From 9ff999cc2b468ac7cee5747a95cd2195f4328aea Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Sat, 25 Apr 2026 14:21:35 +0200 Subject: [PATCH 01/45] tool/lsp: include request details in permission metadata (#24139) --- packages/opencode/src/tool/lsp.ts | 24 +++- packages/opencode/test/tool/lsp.test.ts | 162 ++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 packages/opencode/test/tool/lsp.test.ts diff --git a/packages/opencode/src/tool/lsp.ts b/packages/opencode/src/tool/lsp.ts index 29c6a8d84..3bcae426a 100644 --- a/packages/opencode/src/tool/lsp.ts +++ b/packages/opencode/src/tool/lsp.ts @@ -36,7 +36,6 @@ export const LspTool = Tool.define( Effect.gen(function* () { const lsp = yield* LSP.Service const fs = yield* AppFileSystem.Service - return { description: DESCRIPTION, parameters: Parameters, @@ -47,12 +46,29 @@ export const LspTool = Tool.define( Effect.gen(function* () { const file = path.isAbsolute(args.filePath) ? args.filePath : path.join(Instance.directory, args.filePath) yield* assertExternalDirectoryEffect(ctx, file) - yield* ctx.ask({ permission: "lsp", patterns: ["*"], always: ["*"], metadata: {} }) + const meta = + args.operation === "workspaceSymbol" + ? { operation: args.operation } + : args.operation === "documentSymbol" + ? { operation: args.operation, filePath: file } + : { operation: args.operation, filePath: file, line: args.line, character: args.character } + yield* ctx.ask({ + permission: "lsp", + patterns: ["*"], + always: ["*"], + metadata: meta, + }) const uri = pathToFileURL(file).href const position = { file, line: args.line - 1, character: args.character - 1 } const relPath = path.relative(Instance.worktree, file) - const title = `${args.operation} ${relPath}:${args.line}:${args.character}` + const detail = + args.operation === "workspaceSymbol" + ? "" + : args.operation === "documentSymbol" + ? relPath + : `${relPath}:${args.line}:${args.character}` + const title = detail ? `${args.operation} ${detail}` : args.operation const exists = yield* fs.existsSafe(file) if (!exists) throw new Error(`File not found: ${file}`) @@ -90,7 +106,7 @@ export const LspTool = Tool.define( metadata: { result }, output: result.length === 0 ? `No results found for ${args.operation}` : JSON.stringify(result, null, 2), } - }), + }).pipe(Effect.orDie), } }), ) diff --git a/packages/opencode/test/tool/lsp.test.ts b/packages/opencode/test/tool/lsp.test.ts new file mode 100644 index 000000000..57b8fc6e8 --- /dev/null +++ b/packages/opencode/test/tool/lsp.test.ts @@ -0,0 +1,162 @@ +import { afterEach, describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import path from "path" +import { Agent } from "../../src/agent/agent" +import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { LSP } from "../../src/lsp" +import { Permission } from "../../src/permission" +import { Instance } from "../../src/project/instance" +import { MessageID, SessionID } from "../../src/session/schema" +import { Tool, Truncate } from "../../src/tool" +import { LspTool } from "../../src/tool/lsp" +import { provideTmpdirInstance } from "../fixture/fixture" +import { testEffect } from "../lib/effect" + +afterEach(async () => { + await Instance.disposeAll() +}) + +const ctx = { + sessionID: SessionID.make("ses_test"), + messageID: MessageID.make(""), + callID: "", + agent: "build", + abort: AbortSignal.any([]), + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, +} + +const lsp = Layer.succeed( + LSP.Service, + LSP.Service.of({ + init: () => Effect.void, + status: () => Effect.succeed([]), + hasClients: () => Effect.succeed(true), + touchFile: () => Effect.void, + diagnostics: () => Effect.succeed({}), + hover: () => Effect.succeed([]), + definition: () => Effect.succeed([]), + references: () => Effect.succeed([]), + implementation: () => Effect.succeed([]), + documentSymbol: () => Effect.succeed([]), + workspaceSymbol: () => Effect.succeed([]), + prepareCallHierarchy: () => Effect.succeed([]), + incomingCalls: () => Effect.succeed([]), + outgoingCalls: () => Effect.succeed([]), + }), +) + +const it = testEffect( + Layer.mergeAll( + Agent.defaultLayer, + AppFileSystem.defaultLayer, + CrossSpawnSpawner.defaultLayer, + Truncate.defaultLayer, + lsp, + ), +) + +const init = Effect.fn("LspToolTest.init")(function* () { + const info = yield* LspTool + return yield* info.init() +}) + +const run = Effect.fn("LspToolTest.run")(function* ( + args: Tool.InferParameters, + next: Tool.Context = ctx, +) { + const tool = yield* init() + return yield* tool.execute(args, next) +}) + +const put = Effect.fn("LspToolTest.put")(function* (file: string) { + const fs = yield* AppFileSystem.Service + yield* fs.writeWithDirs(file, "export const x = 1\n") +}) + +const asks = () => { + const items: Array> = [] + return { + items, + next: { + ...ctx, + ask: (req: Omit) => + Effect.sync(() => { + items.push(req) + }), + }, + } +} + +describe("tool.lsp", () => { + describe("permission metadata", () => { + it.live("keeps cursor details for position-based operations", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const file = path.join(dir, "test.ts") + yield* put(file) + + const { items, next } = asks() + const result = yield* run({ operation: "goToDefinition", filePath: file, line: 3, character: 7 }, next) + const req = items.find((item) => item.permission === "lsp") + + expect(req).toBeDefined() + expect(req!.metadata).toEqual({ + operation: "goToDefinition", + filePath: file, + line: 3, + character: 7, + }) + expect(result.title).toBe("goToDefinition test.ts:3:7") + }), + { git: true }, + ), + ) + + it.live("omits cursor details for documentSymbol", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const file = path.join(dir, "test.ts") + yield* put(file) + + const { items, next } = asks() + const result = yield* run({ operation: "documentSymbol", filePath: file, line: 3, character: 7 }, next) + const req = items.find((item) => item.permission === "lsp") + + expect(req).toBeDefined() + expect(req!.metadata).toEqual({ + operation: "documentSymbol", + filePath: file, + }) + expect(result.title).toBe("documentSymbol test.ts") + }), + { git: true }, + ), + ) + + it.live("omits file and cursor details for workspaceSymbol", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const file = path.join(dir, "test.ts") + yield* put(file) + + const { items, next } = asks() + const result = yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7 }, next) + const req = items.find((item) => item.permission === "lsp") + + expect(req).toBeDefined() + expect(req!.metadata).toEqual({ + operation: "workspaceSymbol", + }) + expect(result.title).toBe("workspaceSymbol") + }), + { git: true }, + ), + ) + }) +}) From 66f93035b03a2779a374c608a57dec5d30f35df1 Mon Sep 17 00:00:00 2001 From: Dax Date: Sat, 25 Apr 2026 09:18:42 -0400 Subject: [PATCH 02/45] fix permission config order (#24222) --- packages/opencode/src/config/permission.ts | 30 ++++++++----- packages/opencode/src/permission/index.ts | 12 +---- packages/opencode/test/config/config.test.ts | 24 +++------- .../opencode/test/permission/next.test.ts | 44 ++++++------------- 4 files changed, 40 insertions(+), 70 deletions(-) diff --git a/packages/opencode/src/config/permission.ts b/packages/opencode/src/config/permission.ts index fdd574683..909112c7c 100644 --- a/packages/opencode/src/config/permission.ts +++ b/packages/opencode/src/config/permission.ts @@ -1,6 +1,7 @@ export * as ConfigPermission from "./permission" import { Schema, SchemaGetter } from "effect" -import { zod } from "@/util/effect-zod" +import z from "zod" +import { ZodOverride, zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" export const Action = Schema.Literals(["ask", "allow", "deny"]) @@ -18,17 +19,9 @@ export const Rule = Schema.Union([Action, Object]) .pipe(withStatics((s) => ({ zod: zod(s) }))) export type Rule = Schema.Schema.Type -// Known permission keys get explicit types — most are full Rule (either a -// single Action or a per-pattern object), but a handful of tools take no -// sub-target patterns and are Action-only. Unknown keys fall through the -// Record rest signature as Rule. -// -// StructWithRest canonicalises key order on decode (known first, then rest), -// which used to require the `__originalKeys` preprocess hack because -// `Permission.fromConfig` depended on the user's insertion order. That -// dependency is gone — `fromConfig` now sorts top-level keys so wildcard -// permissions come before specifics, making the final precedence -// order-independent. +// Known permission keys get explicit types in the Effect schema for generated +// docs/types. Runtime config parsing uses `InfoZod` below so user key order is +// preserved for permission precedence. const InputObject = Schema.StructWithRest( Schema.Struct({ read: Schema.optional(Rule), @@ -60,6 +53,18 @@ const InputSchema = Schema.Union([Action, InputObject]) const normalizeInput = (input: Schema.Schema.Type): Schema.Schema.Type => typeof input === "string" ? { "*": input } : input +const ACTION_ONLY = new Set(["todowrite", "question", "webfetch", "websearch", "codesearch", "doom_loop"]) + +const InfoZod = z + .union([zod(Action), z.record(z.string(), z.union([zod(Action), z.record(z.string(), zod(Action))]))]) + .transform(normalizeInput) + .superRefine((input, ctx) => { + for (const [key, value] of globalThis.Object.entries(input)) { + if (!ACTION_ONLY.has(key) || typeof value === "string") continue + ctx.addIssue({ code: "custom", message: `${key} must be a permission action`, path: [key] }) + } + }) + export const Info = InputSchema.pipe( Schema.decodeTo(InputObject, { decode: SchemaGetter.transform(normalizeInput), @@ -70,6 +75,7 @@ export const Info = InputSchema.pipe( }), ) .annotate({ identifier: "PermissionConfig" }) + .annotate({ [ZodOverride]: InfoZod }) .pipe( // Walker already emits the decodeTo transform into the derived zod (see // `encoded()` in effect-zod.ts), so just expose that directly. diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 05c832016..428514ecd 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -288,18 +288,8 @@ function expand(pattern: string): string { } export function fromConfig(permission: ConfigPermission.Info) { - // Sort top-level keys so wildcard permissions (`*`, `mcp_*`) come before - // specific ones. Combined with `findLast` in evaluate(), this gives the - // intuitive semantic "specific tool rules override the `*` fallback" - // regardless of the user's JSON key order. Sub-pattern order inside a - // single permission key is preserved — only top-level keys are sorted. - const entries = Object.entries(permission).sort(([a], [b]) => { - const aWild = a.includes("*") - const bWild = b.includes("*") - return aWild === bWild ? 0 : aWild ? -1 : 1 - }) const ruleset: Ruleset = [] - for (const [key, value] of entries) { + for (const [key, value] of Object.entries(permission)) { if (typeof value === "string") { ruleset.push({ permission: key, action: value, pattern: "*" }) continue diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 73dd46e31..361ac0b5d 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1495,16 +1495,9 @@ test("merges legacy tools with existing permission config", async () => { }) }) -test("permission config canonicalises known keys first, preserves rest-key insertion order", async () => { - // ConfigPermission.Info is a StructWithRest schema — the decoder reorders - // keys into declaration-order for known permission names (edit, read, - // todowrite, external_directory are declared in `config/permission.ts`), - // followed by rest keys in the user's insertion order. - // - // Rule precedence is NOT affected by this reordering: `Permission.fromConfig` - // sorts wildcards before specifics before iterating. See the - // "fromConfig - specific key beats wildcard regardless of JSON key order" - // test in test/permission/next.test.ts for the behavioural guarantee. +test("permission config preserves user key order", async () => { + // Permission precedence follows the order users write in config, so parsing + // must not canonicalise known keys ahead of wildcard or custom keys. await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write( @@ -1532,15 +1525,12 @@ test("permission config canonicalises known keys first, preserves rest-key inser fn: async () => { const config = await load() expect(Object.keys(config.permission!)).toEqual([ - // known fields that the user provided, in declaration order from - // config/permission.ts (read, edit, ..., external_directory, todowrite) - "read", - "edit", - "external_directory", - "todowrite", - // rest keys (not in the known list), in user's insertion order "*", + "edit", "write", + "external_directory", + "read", + "todowrite", "thoughts_*", "reasoning_model_*", "tools_*", diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts index 372e1be7e..b58c716d8 100644 --- a/packages/opencode/test/permission/next.test.ts +++ b/packages/opencode/test/permission/next.test.ts @@ -128,61 +128,45 @@ test("fromConfig - does not expand tilde in middle of path", () => { expect(result).toEqual([{ permission: "external_directory", pattern: "/some/~/path", action: "allow" }]) }) -// Top-level wildcard-vs-specific precedence semantics. -// -// fromConfig sorts top-level keys so wildcard permissions (containing "*") -// come before specific permissions. Combined with `findLast` in evaluate(), -// this gives the intuitive semantic "specific tool rules override the `*` -// fallback", regardless of the order the user wrote the keys in their JSON. -// -// Sub-pattern order inside a single permission key (e.g. `bash: { "*": "allow", "rm": "deny" }`) -// still depends on insertion order — only top-level keys are sorted. +// Permission precedence follows config insertion order. `evaluate()` uses the +// last matching rule, so later config entries intentionally override earlier +// entries even when a wildcard appears after a specific permission. -test("fromConfig - specific key beats wildcard regardless of JSON key order", () => { +test("fromConfig - preserves top-level config key order", () => { const wildcardFirst = Permission.fromConfig({ "*": "deny", bash: "allow" }) const specificFirst = Permission.fromConfig({ bash: "allow", "*": "deny" }) - // Both orderings produce the same ruleset - expect(wildcardFirst).toEqual(specificFirst) + expect(wildcardFirst.map((r) => r.permission)).toEqual(["*", "bash"]) + expect(specificFirst.map((r) => r.permission)).toEqual(["bash", "*"]) - // And both evaluate bash → allow (bash rule wins over * fallback) expect(Permission.evaluate("bash", "ls", wildcardFirst).action).toBe("allow") - expect(Permission.evaluate("bash", "ls", specificFirst).action).toBe("allow") + expect(Permission.evaluate("bash", "ls", specificFirst).action).toBe("deny") }) -test("fromConfig - wildcard acts as fallback for permissions with no specific rule", () => { - const ruleset = Permission.fromConfig({ bash: "allow", "*": "ask" }) +test("fromConfig - wildcard acts as fallback when it appears before specifics", () => { + const ruleset = Permission.fromConfig({ "*": "ask", bash: "allow" }) expect(Permission.evaluate("edit", "foo.ts", ruleset).action).toBe("ask") expect(Permission.evaluate("bash", "ls", ruleset).action).toBe("allow") }) -test("fromConfig - top-level ordering: wildcards first, specifics after", () => { +test("fromConfig - top-level ordering is not sorted by wildcard specificity", () => { const ruleset = Permission.fromConfig({ bash: "allow", "*": "ask", edit: "deny", "mcp_*": "allow", }) - // wildcards (* and mcp_*) come before specifics (bash, edit) - const permissions = ruleset.map((r) => r.permission) - expect(permissions.slice(0, 2).sort()).toEqual(["*", "mcp_*"]) - expect(permissions.slice(2)).toEqual(["bash", "edit"]) + expect(ruleset.map((r) => r.permission)).toEqual(["bash", "*", "edit", "mcp_*"]) }) -test("fromConfig - sub-pattern insertion order inside a tool key is preserved (only top-level sorts)", () => { - // Sub-patterns within a single tool key use the documented "`*` first, - // specific patterns after" convention (findLast picks specifics). The - // top-level sort must not touch sub-pattern ordering. +test("fromConfig - sub-pattern insertion order inside a tool key is preserved", () => { const ruleset = Permission.fromConfig({ bash: { "*": "deny", "git *": "allow" } }) expect(ruleset.map((r) => r.pattern)).toEqual(["*", "git *"]) - // * fallback for unknown commands expect(Permission.evaluate("bash", "rm foo", ruleset).action).toBe("deny") - // specific pattern wins for git commands (it's last, findLast picks it) expect(Permission.evaluate("bash", "git status", ruleset).action).toBe("allow") }) -test("fromConfig - canonical documented example unchanged", () => { - // Regression guard for the example in docs/permissions.mdx +test("fromConfig - documented fallback-first example", () => { const ruleset = Permission.fromConfig({ "*": "ask", bash: "allow", edit: "deny" }) expect(Permission.evaluate("bash", "ls", ruleset).action).toBe("allow") expect(Permission.evaluate("edit", "foo.ts", ruleset).action).toBe("deny") @@ -448,7 +432,7 @@ test("evaluate - wildcard permission fallback for unknown tool", () => { expect(result.action).toBe("ask") }) -test("evaluate - permission patterns sorted by length regardless of object order", () => { +test("evaluate - later wildcard permission can override earlier specific permission", () => { const result = Permission.evaluate("bash", "rm", [ { permission: "bash", pattern: "*", action: "allow" }, { permission: "*", pattern: "*", action: "deny" }, From fc88ed1262c78e7ad75ca564a5f9e970c7dae00f Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Apr 2026 13:19:42 +0000 Subject: [PATCH 03/45] chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 28 ++------ packages/sdk/openapi.json | 85 +++++-------------------- 2 files changed, 21 insertions(+), 92 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 40e661b46..51a79d99d 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1198,32 +1198,14 @@ export type ServerConfig = { export type PermissionActionConfig = "ask" | "allow" | "deny" -export type PermissionObjectConfig = { - [key: string]: PermissionActionConfig -} - -export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig - export type PermissionConfig = | PermissionActionConfig | { - read?: PermissionRuleConfig - edit?: PermissionRuleConfig - glob?: PermissionRuleConfig - grep?: PermissionRuleConfig - list?: PermissionRuleConfig - bash?: PermissionRuleConfig - task?: PermissionRuleConfig - external_directory?: PermissionRuleConfig - todowrite?: PermissionActionConfig - question?: PermissionActionConfig - webfetch?: PermissionActionConfig - websearch?: PermissionActionConfig - codesearch?: PermissionActionConfig - lsp?: PermissionRuleConfig - doom_loop?: PermissionActionConfig - skill?: PermissionRuleConfig - [key: string]: PermissionRuleConfig | PermissionActionConfig | undefined + [key: string]: + | PermissionActionConfig + | { + [key: string]: PermissionActionConfig + } } export type AgentConfig = { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index cd7b381d8..cbb9aaecc 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -10926,25 +10926,6 @@ "type": "string", "enum": ["ask", "allow", "deny"] }, - "PermissionObjectConfig": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "$ref": "#/components/schemas/PermissionActionConfig" - } - }, - "PermissionRuleConfig": { - "anyOf": [ - { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - { - "$ref": "#/components/schemas/PermissionObjectConfig" - } - ] - }, "PermissionConfig": { "anyOf": [ { @@ -10952,58 +10933,24 @@ }, { "type": "object", - "properties": { - "read": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "edit": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "glob": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "grep": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "list": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "bash": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "task": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "external_directory": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "todowrite": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "question": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "webfetch": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "websearch": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "codesearch": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "lsp": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "doom_loop": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "skill": { - "$ref": "#/components/schemas/PermissionRuleConfig" - } + "propertyNames": { + "type": "string" }, "additionalProperties": { - "$ref": "#/components/schemas/PermissionRuleConfig" + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/components/schemas/PermissionActionConfig" + } + } + ] } } ] From d748c718457f64f6eeef02f406ebca67f78864fa Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 25 Apr 2026 09:41:30 -0400 Subject: [PATCH 04/45] ci: centralize opentui dependencies in workspace catalog Use catalog references for @opentui/core, @opentui/solid, and opentui-spinner across packages to ensure consistent versions and simplify updates. --- bun.lock | 15 ++++++++------- package.json | 5 +++-- packages/opencode/package.json | 6 +++--- packages/plugin/package.json | 4 ++-- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/bun.lock b/bun.lock index 47d178706..d0640276b 100644 --- a/bun.lock +++ b/bun.lock @@ -367,8 +367,8 @@ "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", - "@opentui/core": "0.1.103", - "@opentui/solid": "0.1.103", + "@opentui/core": "catalog:", + "@opentui/solid": "catalog:", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -403,7 +403,7 @@ "open": "10.1.2", "opencode-gitlab-auth": "2.0.1", "opencode-poe-auth": "0.0.1", - "opentui-spinner": "0.0.6", + "opentui-spinner": "catalog:", "partial-json": "0.1.7", "remeda": "catalog:", "semver": "^7.6.3", @@ -467,8 +467,8 @@ "zod": "catalog:", }, "devDependencies": { - "@opentui/core": "0.1.103", - "@opentui/solid": "0.1.103", + "@opentui/core": "catalog:", + "@opentui/solid": "catalog:", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", @@ -678,8 +678,8 @@ "@npmcli/arborist": "9.4.0", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "0.1.103", + "@opentui/solid": "0.1.103", "@pierre/diffs": "1.1.0-beta.18", "@playwright/test": "1.59.1", "@solid-primitives/storage": "4.3.3", @@ -708,6 +708,7 @@ "luxon": "3.6.1", "marked": "17.0.1", "marked-shiki": "1.2.1", + "opentui-spinner": "0.0.6", "remeda": "2.26.0", "remend": "1.3.0", "semver": "7.7.4", diff --git a/package.json b/package.json index f918bcd02..b2c8a2d7a 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "@types/cross-spawn": "6.0.6", "@octokit/rest": "22.0.0", "@hono/zod-validator": "0.4.2", - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "0.1.103", + "@opentui/solid": "0.1.103", "ulid": "3.0.1", "@kobalte/core": "0.13.11", "@types/luxon": "3.7.1", @@ -46,6 +46,7 @@ "@cloudflare/workers-types": "4.20251008.0", "@openauthjs/openauth": "0.0.0-20250322224806", "@pierre/diffs": "1.1.0-beta.18", + "opentui-spinner": "0.0.6", "@solid-primitives/storage": "4.3.3", "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 880459e96..8d54ad2e7 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -124,8 +124,8 @@ "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", - "@opentui/core": "0.1.103", - "@opentui/solid": "0.1.103", + "@opentui/core": "catalog:", + "@opentui/solid": "catalog:", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -160,7 +160,7 @@ "open": "10.1.2", "opencode-gitlab-auth": "2.0.1", "opencode-poe-auth": "0.0.1", - "opentui-spinner": "0.0.6", + "opentui-spinner": "catalog:", "partial-json": "0.1.7", "remeda": "catalog:", "semver": "^7.6.3", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index e7d9b87ed..3678ccd25 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -34,8 +34,8 @@ } }, "devDependencies": { - "@opentui/core": "0.1.103", - "@opentui/solid": "0.1.103", + "@opentui/core": "catalog:", + "@opentui/solid": "catalog:", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "typescript": "catalog:", From 1b92c95425e5725b9908e13c92b98201e8debe8d Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 25 Apr 2026 09:48:09 -0400 Subject: [PATCH 05/45] core: permission config schema now provides full IntelliSense for all tool permission keys The permission configuration previously used a generic record type that didn't offer editor completions. Updated the schema to explicitly list all tool permission keys (read, edit, glob, grep, list, bash, task, external_directory, lsp, skill, todowrite, question, webfetch, websearch, codesearch, doom_loop) with proper types, enabling autocomplete when editing permission files. --- packages/opencode/src/config/permission.ts | 35 +++++-- packages/sdk/js/src/v2/gen/types.gen.ts | 34 +++++-- packages/sdk/openapi.json | 102 +++++++++++++++++---- 3 files changed, 137 insertions(+), 34 deletions(-) diff --git a/packages/opencode/src/config/permission.ts b/packages/opencode/src/config/permission.ts index 909112c7c..a7390e953 100644 --- a/packages/opencode/src/config/permission.ts +++ b/packages/opencode/src/config/permission.ts @@ -53,17 +53,34 @@ const InputSchema = Schema.Union([Action, InputObject]) const normalizeInput = (input: Schema.Schema.Type): Schema.Schema.Type => typeof input === "string" ? { "*": input } : input -const ACTION_ONLY = new Set(["todowrite", "question", "webfetch", "websearch", "codesearch", "doom_loop"]) - const InfoZod = z - .union([zod(Action), z.record(z.string(), z.union([zod(Action), z.record(z.string(), zod(Action))]))]) + .union([ + zod(Action), + z.intersection( + z.record(z.string(), zod(Rule)), + z + .object({ + read: zod(Rule).optional(), + edit: zod(Rule).optional(), + glob: zod(Rule).optional(), + grep: zod(Rule).optional(), + list: zod(Rule).optional(), + bash: zod(Rule).optional(), + task: zod(Rule).optional(), + external_directory: zod(Rule).optional(), + todowrite: zod(Action).optional(), + question: zod(Action).optional(), + webfetch: zod(Action).optional(), + websearch: zod(Action).optional(), + codesearch: zod(Action).optional(), + lsp: zod(Rule).optional(), + doom_loop: zod(Action).optional(), + skill: zod(Rule).optional(), + }) + .catchall(zod(Rule)), + ), + ]) .transform(normalizeInput) - .superRefine((input, ctx) => { - for (const [key, value] of globalThis.Object.entries(input)) { - if (!ACTION_ONLY.has(key) || typeof value === "string") continue - ctx.addIssue({ code: "custom", message: `${key} must be a permission action`, path: [key] }) - } - }) export const Info = InputSchema.pipe( Schema.decodeTo(InputObject, { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 51a79d99d..0ad88bb50 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1198,15 +1198,35 @@ export type ServerConfig = { export type PermissionActionConfig = "ask" | "allow" | "deny" +export type PermissionObjectConfig = { + [key: string]: PermissionActionConfig +} + +export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig + export type PermissionConfig = | PermissionActionConfig - | { - [key: string]: - | PermissionActionConfig - | { - [key: string]: PermissionActionConfig - } - } + | ({ + [key: string]: PermissionRuleConfig + } & { + read?: PermissionRuleConfig + edit?: PermissionRuleConfig + glob?: PermissionRuleConfig + grep?: PermissionRuleConfig + list?: PermissionRuleConfig + bash?: PermissionRuleConfig + task?: PermissionRuleConfig + external_directory?: PermissionRuleConfig + todowrite?: PermissionActionConfig + question?: PermissionActionConfig + webfetch?: PermissionActionConfig + websearch?: PermissionActionConfig + codesearch?: PermissionActionConfig + lsp?: PermissionRuleConfig + doom_loop?: PermissionActionConfig + skill?: PermissionRuleConfig + [key: string]: PermissionRuleConfig | PermissionActionConfig | undefined + }) export type AgentConfig = { model?: string diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index cbb9aaecc..7be58195b 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -10926,32 +10926,98 @@ "type": "string", "enum": ["ask", "allow", "deny"] }, + "PermissionObjectConfig": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/components/schemas/PermissionActionConfig" + } + }, + "PermissionRuleConfig": { + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + { + "$ref": "#/components/schemas/PermissionObjectConfig" + } + ] + }, "PermissionConfig": { "anyOf": [ { "$ref": "#/components/schemas/PermissionActionConfig" }, { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "anyOf": [ - { - "$ref": "#/components/schemas/PermissionActionConfig" + "allOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" }, - { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "$ref": "#/components/schemas/PermissionActionConfig" - } + "additionalProperties": { + "$ref": "#/components/schemas/PermissionRuleConfig" } - ] - } + }, + { + "type": "object", + "properties": { + "read": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "edit": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "glob": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "grep": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "list": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "bash": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "task": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "external_directory": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "todowrite": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "question": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "webfetch": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "websearch": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "codesearch": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "lsp": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "doom_loop": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "skill": { + "$ref": "#/components/schemas/PermissionRuleConfig" + } + }, + "additionalProperties": { + "$ref": "#/components/schemas/PermissionRuleConfig" + } + } + ] } ] }, From bad732c26a8c093ce7a3d724432f05470e953ee2 Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 25 Apr 2026 14:37:01 +0000 Subject: [PATCH 06/45] sync release versions for v1.14.25 --- bun.lock | 32 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop-electron/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++----- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/shared/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 19 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index d0640276b..ff5f6bb7d 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.14.24", + "version": "1.14.25", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -83,7 +83,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.14.24", + "version": "1.14.25", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -117,7 +117,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.14.24", + "version": "1.14.25", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -144,7 +144,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.14.24", + "version": "1.14.25", "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/openai": "3.0.48", @@ -168,7 +168,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.14.24", + "version": "1.14.25", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -192,7 +192,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.14.24", + "version": "1.14.25", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -225,7 +225,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "1.14.24", + "version": "1.14.25", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -269,7 +269,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.14.24", + "version": "1.14.25", "dependencies": { "@opencode-ai/shared": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -298,7 +298,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.14.24", + "version": "1.14.25", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -314,7 +314,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.14.24", + "version": "1.14.25", "bin": { "opencode": "./bin/opencode", }, @@ -460,7 +460,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.14.24", + "version": "1.14.25", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", @@ -495,7 +495,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.14.24", + "version": "1.14.25", "dependencies": { "cross-spawn": "catalog:", }, @@ -510,7 +510,7 @@ }, "packages/shared": { "name": "@opencode-ai/shared", - "version": "1.14.24", + "version": "1.14.25", "bin": { "opencode": "./bin/opencode", }, @@ -534,7 +534,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.14.24", + "version": "1.14.25", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -569,7 +569,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.14.24", + "version": "1.14.25", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -618,7 +618,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.14.24", + "version": "1.14.25", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 324d8058c..7f65da4d9 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.14.24", + "version": "1.14.25", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index e553d1d26..5abd19256 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.14.24", + "version": "1.14.25", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 09dad6014..31f4f9a0a 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.14.24", + "version": "1.14.25", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 2005a941b..4f30ea99b 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.14.24", + "version": "1.14.25", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 16d99c7c4..39556c7d3 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.14.24", + "version": "1.14.25", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index 0c9d8b253..f382ad16d 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "1.14.24", + "version": "1.14.25", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 34a1375f6..242bd1971 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.14.24", + "version": "1.14.25", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 13be3723d..5f6b14ed7 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.14.24", + "version": "1.14.25", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index d8c9ecc8c..6deaee220 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.14.24" +version = "1.14.25" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.24/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.25/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.24/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.25/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.24/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.25/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.24/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.25/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.24/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.25/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 5a1e2beb4..6c1428ad5 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.14.24", + "version": "1.14.25", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 8d54ad2e7..a0b6ddaff 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.24", + "version": "1.14.25", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 3678ccd25..88f7a65f6 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.14.24", + "version": "1.14.25", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index d924f1e68..3f6ffa7a5 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.14.24", + "version": "1.14.25", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/shared/package.json b/packages/shared/package.json index 151751bda..beb0d50ed 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.24", + "version": "1.14.25", "name": "@opencode-ai/shared", "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index 89f54026a..9ab39fad7 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.14.24", + "version": "1.14.25", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 6339b1f5b..9feb8c035 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.14.24", + "version": "1.14.25", "type": "module", "license": "MIT", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index a8e9d0e0e..ce4a80436 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.14.24", + "version": "1.14.25", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 217cb037a..73c6cc9fb 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.14.24", + "version": "1.14.25", "publisher": "sst-dev", "repository": { "type": "git", From d5bfaef53d36b9b3236600a92c21a5e226de9151 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Apr 2026 10:42:31 -0400 Subject: [PATCH 07/45] feat(httpapi): bridge instance read endpoints (#24258) --- packages/opencode/specs/effect/http-api.md | 16 ++- packages/opencode/src/project/vcs.ts | 45 ++++---- .../routes/instance/httpapi/instance.ts | 103 ++++++++++++++++++ .../server/routes/instance/httpapi/server.ts | 2 + .../src/server/routes/instance/index.ts | 10 +- .../test/server/httpapi-instance.test.ts | 53 +++++++++ 6 files changed, 193 insertions(+), 36 deletions(-) create mode 100644 packages/opencode/src/server/routes/instance/httpapi/instance.ts create mode 100644 packages/opencode/test/server/httpapi-instance.test.ts diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index ad9fcb2ba..7f19a612b 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -139,8 +139,8 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `project` | `bridged` partial | reads only; git-init remains Hono | | `file` | `bridged` partial | list/content/status only | | `mcp` | `bridged` partial | status only | -| `workspace` | `implemented` | `HttpApi` group exists, but bridge mounting needs verification | -| top-level instance reads | `next` | path, vcs, command, agent, skill, lsp, formatter | +| `workspace` | `bridged` | list, get, enter | +| top-level instance reads | `bridged` partial | path and vcs reads; command, agent, skill, lsp, formatter next | | experimental JSON routes | `next/later` | console, tool, worktree, resource, global session list | | `session` | `later/special` | large stateful surface plus streaming | | `sync` | `later` | process/control side effects | @@ -150,11 +150,9 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho ## Next PRs -1. Add bridge-level auth and instance-context tests for the current `HttpApi` bridge. -2. Produce a generated route inventory from Hono registrations and update `Current Route Status` with exact paths. -3. Fix the `workspace` status: mount it if it should be reachable, or remove it from the composed `HttpApi` layer. -4. Port the top-level JSON reads. -5. Start the Effect OpenAPI/SDK generation path for already-bridged routes. +1. Produce a generated route inventory from Hono registrations and update `Current Route Status` with exact paths. +2. Continue porting top-level JSON reads. +3. Start the Effect OpenAPI/SDK generation path for already-bridged routes. ## Checklist @@ -164,9 +162,9 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho - [x] Provide auth, instance lookup, and observability in the Effect route layer. - [x] Attach auth middleware in route modules. - [x] Support `auth_token` as a query security scheme. -- [ ] Add bridge-level auth and instance tests. +- [x] Add bridge-level auth and instance tests. - [ ] Complete exact Hono route inventory. -- [ ] Resolve implemented-but-unmounted route groups. +- [x] Resolve implemented-but-unmounted route groups. - [ ] Port remaining JSON routes. - [ ] Generate SDK/OpenAPI from Effect routes. - [ ] Flip ported JSON routes to default-on with fallback. diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts index e8c6ff2ac..1c1da97bf 100644 --- a/packages/opencode/src/project/vcs.ts +++ b/packages/opencode/src/project/vcs.ts @@ -8,7 +8,8 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { FileWatcher } from "@/file/watcher" import { Git } from "@/git" import { Log } from "@/util" -import z from "zod" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" const log = Log.create({ service: "vcs" }) @@ -101,8 +102,8 @@ const compare = Effect.fnUntraced(function* ( ) }) -export const Mode = z.enum(["git", "branch"]) -export type Mode = z.infer +export const Mode = Schema.Literals(["git", "branch"]).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Mode = Schema.Schema.Type export const Event = { BranchUpdated: BusEvent.define( @@ -113,28 +114,24 @@ export const Event = { ), } -export const Info = z - .object({ - branch: z.string().optional(), - default_branch: z.string().optional(), - }) - .meta({ - ref: "VcsInfo", - }) -export type Info = z.infer +export const Info = Schema.Struct({ + branch: Schema.optional(Schema.String), + default_branch: Schema.optional(Schema.String), +}) + .annotate({ identifier: "VcsInfo" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Info = Schema.Schema.Type -export const FileDiff = z - .object({ - file: z.string(), - patch: z.string(), - additions: z.number(), - deletions: z.number(), - status: z.enum(["added", "deleted", "modified"]).optional(), - }) - .meta({ - ref: "VcsFileDiff", - }) -export type FileDiff = z.infer +export const FileDiff = Schema.Struct({ + file: Schema.String, + patch: Schema.String, + additions: Schema.Number, + deletions: Schema.Number, + status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])), +}) + .annotate({ identifier: "VcsFileDiff" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type FileDiff = Schema.Schema.Type export interface Interface { readonly init: () => Effect.Effect diff --git a/packages/opencode/src/server/routes/instance/httpapi/instance.ts b/packages/opencode/src/server/routes/instance/httpapi/instance.ts new file mode 100644 index 000000000..f7c3a02ad --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/instance.ts @@ -0,0 +1,103 @@ +import { Global } from "@/global" +import { Vcs } from "@/project" +import * as InstanceState from "@/effect/instance-state" +import { Effect, Layer, Schema } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" + +const PathInfo = Schema.Struct({ + home: Schema.String, + state: Schema.String, + config: Schema.String, + worktree: Schema.String, + directory: Schema.String, +}).annotate({ identifier: "Path" }) + +const VcsDiffQuery = Schema.Struct({ + mode: Vcs.Mode, +}) + +export const InstancePaths = { + path: "/path", + vcs: "/vcs", + vcsDiff: "/vcs/diff", +} as const + +export const InstanceApi = HttpApi.make("instance") + .add( + HttpApiGroup.make("instance") + .add( + HttpApiEndpoint.get("path", InstancePaths.path, { + success: PathInfo, + }).annotateMerge( + OpenApi.annotations({ + identifier: "path.get", + summary: "Get paths", + description: "Retrieve the current working directory and related path information for the OpenCode instance.", + }), + ), + HttpApiEndpoint.get("vcs", InstancePaths.vcs, { + success: Vcs.Info, + }).annotateMerge( + OpenApi.annotations({ + identifier: "vcs.get", + summary: "Get VCS info", + description: "Retrieve version control system (VCS) information for the current project, such as git branch.", + }), + ), + HttpApiEndpoint.get("vcsDiff", InstancePaths.vcsDiff, { + query: VcsDiffQuery, + success: Schema.Array(Vcs.FileDiff), + }).annotateMerge( + OpenApi.annotations({ + identifier: "vcs.diff", + summary: "Get VCS diff", + description: "Retrieve the current git diff for the working tree or against the default branch.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "instance", + description: "Experimental HttpApi instance read routes.", + }), + ) + .middleware(Authorization), + ) + .annotateMerge( + OpenApi.annotations({ + title: "opencode experimental HttpApi", + version: "0.0.1", + description: "Experimental HttpApi surface for selected instance routes.", + }), + ) + +export const instanceHandlers = Layer.unwrap( + Effect.gen(function* () { + const vcs = yield* Vcs.Service + + const getPath = Effect.fn("InstanceHttpApi.path")(function* () { + const ctx = yield* InstanceState.context + return { + home: Global.Path.home, + state: Global.Path.state, + config: Global.Path.config, + worktree: ctx.worktree, + directory: ctx.directory, + } + }) + + const getVcs = Effect.fn("InstanceHttpApi.vcs")(function* () { + const [branch, default_branch] = yield* Effect.all([vcs.branch(), vcs.defaultBranch()], { concurrency: 2 }) + return { branch, default_branch } + }) + + const getVcsDiff = Effect.fn("InstanceHttpApi.vcsDiff")(function* (ctx: { query: { mode: Vcs.Mode } }) { + return yield* vcs.diff(ctx.query.mode) + }) + + return HttpApiBuilder.group(InstanceApi, "instance", (handlers) => + handlers.handle("path", getPath).handle("vcs", getVcs).handle("vcsDiff", getVcsDiff), + ) + }), +).pipe(Layer.provide(Vcs.defaultLayer)) diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index 14c2550ed..903cd103b 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -11,6 +11,7 @@ import { Filesystem } from "@/util" import { authorizationLayer } from "./auth" import { ConfigApi, configHandlers } from "./config" import { FileApi, fileHandlers } from "./file" +import { InstanceApi, instanceHandlers } from "./instance" import { McpApi, mcpHandlers } from "./mcp" import { PermissionApi, permissionHandlers } from "./permission" import { ProjectApi, projectHandlers } from "./project" @@ -63,6 +64,7 @@ const instance = HttpRouter.middleware()( export const routes = Layer.mergeAll( HttpApiBuilder.layer(ConfigApi).pipe(Layer.provide(configHandlers)), HttpApiBuilder.layer(FileApi).pipe(Layer.provide(fileHandlers)), + HttpApiBuilder.layer(InstanceApi).pipe(Layer.provide(instanceHandlers)), HttpApiBuilder.layer(McpApi).pipe(Layer.provide(mcpHandlers)), HttpApiBuilder.layer(ProjectApi).pipe(Layer.provide(projectHandlers)), HttpApiBuilder.layer(QuestionApi).pipe(Layer.provide(questionHandlers)), diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index b899eb108..bc9d2b2ad 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -17,6 +17,7 @@ import { PermissionRoutes } from "./permission" import { Flag } from "@/flag/flag" import { ExperimentalHttpApiServer } from "./httpapi/server" import { FilePaths } from "./httpapi/file" +import { InstancePaths } from "./httpapi/instance" import { McpPaths } from "./httpapi/mcp" import { ProjectRoutes } from "./project" import { SessionRoutes } from "./session" @@ -53,6 +54,9 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.get(FilePaths.list, (c) => handler(c.req.raw, context)) app.get(FilePaths.content, (c) => handler(c.req.raw, context)) app.get(FilePaths.status, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.path, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.vcs, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.vcsDiff, (c) => handler(c.req.raw, context)) app.get(McpPaths.status, (c) => handler(c.req.raw, context)) } @@ -142,7 +146,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { description: "VCS info", content: { "application/json": { - schema: resolver(Vcs.Info), + schema: resolver(Vcs.Info.zod), }, }, }, @@ -168,7 +172,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { description: "VCS diff", content: { "application/json": { - schema: resolver(Vcs.FileDiff.array()), + schema: resolver(Vcs.FileDiff.zod.array()), }, }, }, @@ -177,7 +181,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { validator( "query", z.object({ - mode: Vcs.Mode, + mode: Vcs.Mode.zod, }), ), async (c) => diff --git a/packages/opencode/test/server/httpapi-instance.test.ts b/packages/opencode/test/server/httpapi-instance.test.ts new file mode 100644 index 000000000..f25d29518 --- /dev/null +++ b/packages/opencode/test/server/httpapi-instance.test.ts @@ -0,0 +1,53 @@ +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 { Instance } from "../../src/project/instance" +import { InstanceRoutes } from "../../src/server/routes/instance" +import { InstancePaths } from "../../src/server/routes/instance/httpapi/instance" +import { Log } from "../../src/util" +import { resetDatabase } from "../fixture/db" +import { tmpdir } from "../fixture/fixture" + +void Log.init({ print: false }) + +const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI +const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket + +function app() { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true + return InstanceRoutes(websocket) +} + +afterEach(async () => { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original + await Instance.disposeAll() + await resetDatabase() +}) + +describe("instance HttpApi", () => { + test("serves path and VCS read endpoints through Hono bridge", async () => { + await using tmp = await tmpdir({ git: true }) + await Bun.write(path.join(tmp.path, "changed.txt"), "hello") + + const vcsDiff = new URL(`http://localhost${InstancePaths.vcsDiff}`) + vcsDiff.searchParams.set("mode", "git") + + const [paths, vcs, diff] = await Promise.all([ + app().request(InstancePaths.path, { headers: { "x-opencode-directory": tmp.path } }), + app().request(InstancePaths.vcs, { headers: { "x-opencode-directory": tmp.path } }), + app().request(vcsDiff, { headers: { "x-opencode-directory": tmp.path } }), + ]) + + expect(paths.status).toBe(200) + expect(await paths.json()).toMatchObject({ directory: tmp.path, worktree: tmp.path }) + + expect(vcs.status).toBe(200) + expect(await vcs.json()).toMatchObject({ branch: expect.any(String) }) + + expect(diff.status).toBe(200) + expect(await diff.json()).toContainEqual( + expect.objectContaining({ file: "changed.txt", additions: 1, status: "added" }), + ) + }) +}) From 5b0e828c10a2d33cb5284566a705a9d61f98b8f1 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Apr 2026 14:43:27 +0000 Subject: [PATCH 08/45] chore: generate --- packages/opencode/specs/effect/http-api.md | 4 ++-- .../opencode/src/server/routes/instance/httpapi/instance.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 7f19a612b..1b9da7a2c 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -139,8 +139,8 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `project` | `bridged` partial | reads only; git-init remains Hono | | `file` | `bridged` partial | list/content/status only | | `mcp` | `bridged` partial | status only | -| `workspace` | `bridged` | list, get, enter | -| top-level instance reads | `bridged` partial | path and vcs reads; command, agent, skill, lsp, formatter next | +| `workspace` | `bridged` | list, get, enter | +| top-level instance reads | `bridged` partial | path and vcs reads; command, agent, skill, lsp, formatter next | | experimental JSON routes | `next/later` | console, tool, worktree, resource, global session list | | `session` | `later/special` | large stateful surface plus streaming | | `sync` | `later` | process/control side effects | diff --git a/packages/opencode/src/server/routes/instance/httpapi/instance.ts b/packages/opencode/src/server/routes/instance/httpapi/instance.ts index f7c3a02ad..97b53c1e9 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/instance.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/instance.ts @@ -33,7 +33,8 @@ export const InstanceApi = HttpApi.make("instance") OpenApi.annotations({ identifier: "path.get", summary: "Get paths", - description: "Retrieve the current working directory and related path information for the OpenCode instance.", + description: + "Retrieve the current working directory and related path information for the OpenCode instance.", }), ), HttpApiEndpoint.get("vcs", InstancePaths.vcs, { @@ -42,7 +43,8 @@ export const InstanceApi = HttpApi.make("instance") OpenApi.annotations({ identifier: "vcs.get", summary: "Get VCS info", - description: "Retrieve version control system (VCS) information for the current project, such as git branch.", + description: + "Retrieve version control system (VCS) information for the current project, such as git branch.", }), ), HttpApiEndpoint.get("vcsDiff", InstancePaths.vcsDiff, { From 37aa8442dc023fad250f2573c8235a544789900c Mon Sep 17 00:00:00 2001 From: Dax Date: Sat, 25 Apr 2026 10:46:16 -0400 Subject: [PATCH 09/45] refactor: remove lazy cross-spawn runtime (#24305) --- packages/opencode/src/effect/cross-spawn-spawner.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/opencode/src/effect/cross-spawn-spawner.ts b/packages/opencode/src/effect/cross-spawn-spawner.ts index 5e25263a0..ad8d4126d 100644 --- a/packages/opencode/src/effect/cross-spawn-spawner.ts +++ b/packages/opencode/src/effect/cross-spawn-spawner.ts @@ -502,13 +502,4 @@ export const layer: Layer.Layer { - // Dynamic import to avoid circular dep: cross-spawn-spawner → run-service → Instance → project → cross-spawn-spawner - const { makeRuntime } = await import("@/effect/run-service") - return makeRuntime(ChildProcessSpawner, defaultLayer) -}) - -type RT = Awaited> -export const runPromiseExit: RT["runPromiseExit"] = async (...args) => (await rt()).runPromiseExit(...(args as [any])) +export * as CrossSpawnSpawner from "./cross-spawn-spawner" From 62ef2a220723a6d6cb050e523fcdfaa974dafdda Mon Sep 17 00:00:00 2001 From: Dax Date: Sat, 25 Apr 2026 10:59:17 -0400 Subject: [PATCH 10/45] refactor: rename shared package to core (#24309) --- bun.lock | 60 +++++++++---------- packages/app/package.json | 2 +- .../src/components/dialog-edit-project.tsx | 2 +- packages/app/src/components/dialog-fork.tsx | 2 +- .../components/dialog-select-directory.tsx | 2 +- .../app/src/components/dialog-select-file.tsx | 4 +- .../prompt-input/build-request-parts.ts | 2 +- .../components/prompt-input/context-items.tsx | 2 +- .../components/prompt-input/slash-popover.tsx | 2 +- .../components/prompt-input/submit.test.ts | 2 +- .../app/src/components/prompt-input/submit.ts | 4 +- .../session/session-context-tab.tsx | 4 +- .../src/components/session/session-header.tsx | 2 +- .../components/session/session-new-view.tsx | 2 +- .../session/session-sortable-tab.tsx | 2 +- packages/app/src/context/file.tsx | 2 +- packages/app/src/context/global-sync.tsx | 2 +- .../app/src/context/global-sync/bootstrap.ts | 4 +- .../src/context/global-sync/event-reducer.ts | 2 +- packages/app/src/context/local.tsx | 2 +- packages/app/src/context/notification.tsx | 4 +- .../context/permission-auto-respond.test.ts | 2 +- .../src/context/permission-auto-respond.ts | 2 +- packages/app/src/context/prompt.tsx | 2 +- packages/app/src/context/sync.tsx | 4 +- packages/app/src/pages/directory-layout.tsx | 2 +- packages/app/src/pages/home.tsx | 2 +- packages/app/src/pages/layout.tsx | 8 +-- packages/app/src/pages/layout/helpers.ts | 2 +- .../app/src/pages/layout/sidebar-items.tsx | 2 +- .../app/src/pages/layout/sidebar-project.tsx | 2 +- .../src/pages/layout/sidebar-workspace.tsx | 4 +- packages/app/src/pages/session.tsx | 2 +- packages/app/src/pages/session/file-tabs.tsx | 2 +- .../src/pages/session/message-timeline.tsx | 4 +- .../pages/session/use-session-commands.tsx | 2 +- packages/app/src/utils/base64.ts | 2 +- packages/app/src/utils/persist.ts | 2 +- packages/{shared => core}/package.json | 2 +- packages/{shared => core}/src/filesystem.ts | 0 packages/{shared => core}/src/global.ts | 0 packages/{shared => core}/src/types.d.ts | 0 packages/{shared => core}/src/util/array.ts | 0 packages/{shared => core}/src/util/binary.ts | 0 .../{shared => core}/src/util/effect-flock.ts | 0 packages/{shared => core}/src/util/encode.ts | 0 packages/{shared => core}/src/util/error.ts | 0 packages/{shared => core}/src/util/flock.ts | 0 packages/{shared => core}/src/util/fn.ts | 0 packages/{shared => core}/src/util/glob.ts | 0 packages/{shared => core}/src/util/hash.ts | 0 .../{shared => core}/src/util/identifier.ts | 0 packages/{shared => core}/src/util/iife.ts | 0 packages/{shared => core}/src/util/lazy.ts | 0 packages/{shared => core}/src/util/module.ts | 0 packages/{shared => core}/src/util/path.ts | 0 packages/{shared => core}/src/util/retry.ts | 0 packages/{shared => core}/src/util/slug.ts | 0 packages/{shared => core}/sst-env.d.ts | 0 .../test/filesystem/filesystem.test.ts | 2 +- .../test/fixture/effect-flock-worker.ts | 6 +- .../test/fixture/flock-worker.ts | 2 +- packages/{shared => core}/test/lib/effect.ts | 0 .../test/util/effect-flock.test.ts | 8 +-- .../{shared => core}/test/util/flock.test.ts | 4 +- packages/{shared => core}/tsconfig.json | 0 packages/enterprise/package.json | 2 +- packages/enterprise/src/core/share.ts | 4 +- packages/enterprise/src/core/storage.ts | 2 +- .../enterprise/src/routes/share/[shareID].tsx | 6 +- packages/enterprise/test/core/share.test.ts | 2 +- packages/opencode/package.json | 2 +- packages/opencode/src/acp/agent.ts | 2 +- packages/opencode/src/auth/index.ts | 2 +- .../opencode/src/cli/cmd/tui/config/tui.ts | 2 +- .../opencode/src/cli/cmd/tui/context/kv.tsx | 2 +- .../opencode/src/cli/cmd/tui/context/sync.tsx | 2 +- .../src/cli/cmd/tui/context/theme.tsx | 2 +- .../src/cli/cmd/tui/plugin/runtime.ts | 2 +- packages/opencode/src/cli/error.ts | 2 +- packages/opencode/src/cli/ui.ts | 2 +- packages/opencode/src/config/agent.ts | 4 +- packages/opencode/src/config/command.ts | 4 +- packages/opencode/src/config/config.ts | 6 +- packages/opencode/src/config/error.ts | 2 +- packages/opencode/src/config/markdown.ts | 2 +- packages/opencode/src/config/paths.ts | 2 +- packages/opencode/src/config/plugin.ts | 2 +- .../opencode/src/control-plane/workspace.ts | 2 +- packages/opencode/src/effect/app-runtime.ts | 2 +- packages/opencode/src/file/ignore.ts | 2 +- packages/opencode/src/file/index.ts | 2 +- packages/opencode/src/file/ripgrep.ts | 2 +- packages/opencode/src/global/index.ts | 2 +- packages/opencode/src/ide/index.ts | 2 +- packages/opencode/src/index.ts | 2 +- packages/opencode/src/lsp/client.ts | 2 +- packages/opencode/src/lsp/lsp.ts | 2 +- packages/opencode/src/lsp/server.ts | 2 +- packages/opencode/src/mcp/auth.ts | 2 +- packages/opencode/src/mcp/index.ts | 4 +- packages/opencode/src/npm/index.ts | 6 +- packages/opencode/src/plugin/index.ts | 2 +- packages/opencode/src/plugin/install.ts | 2 +- packages/opencode/src/plugin/meta.ts | 2 +- packages/opencode/src/project/instance.ts | 2 +- packages/opencode/src/project/project.ts | 2 +- packages/opencode/src/project/vcs.ts | 2 +- packages/opencode/src/provider/models.ts | 4 +- packages/opencode/src/provider/provider.ts | 4 +- packages/opencode/src/pty/index.ts | 2 +- packages/opencode/src/server/middleware.ts | 2 +- .../src/server/routes/instance/middleware.ts | 2 +- .../src/server/routes/instance/session.ts | 2 +- packages/opencode/src/session/instruction.ts | 2 +- packages/opencode/src/session/message-v2.ts | 2 +- packages/opencode/src/session/prompt.ts | 4 +- packages/opencode/src/session/retry.ts | 2 +- packages/opencode/src/session/session.ts | 2 +- packages/opencode/src/skill/discovery.ts | 2 +- packages/opencode/src/skill/index.ts | 6 +- packages/opencode/src/snapshot/index.ts | 4 +- packages/opencode/src/storage/db.ts | 2 +- .../opencode/src/storage/json-migration.ts | 2 +- packages/opencode/src/storage/storage.ts | 4 +- packages/opencode/src/tool/apply_patch.ts | 2 +- packages/opencode/src/tool/bash.ts | 2 +- packages/opencode/src/tool/edit.ts | 2 +- .../opencode/src/tool/external-directory.ts | 2 +- packages/opencode/src/tool/glob.ts | 2 +- packages/opencode/src/tool/grep.ts | 2 +- packages/opencode/src/tool/lsp.ts | 2 +- packages/opencode/src/tool/read.ts | 2 +- packages/opencode/src/tool/registry.ts | 4 +- packages/opencode/src/tool/truncate.ts | 2 +- packages/opencode/src/tool/write.ts | 2 +- packages/opencode/src/util/bom.ts | 2 +- packages/opencode/src/util/filesystem.ts | 2 +- packages/opencode/src/util/log.ts | 2 +- packages/opencode/src/worktree/index.ts | 6 +- packages/opencode/test/config/config.test.ts | 6 +- .../test/filesystem/filesystem.test.ts | 2 +- .../opencode/test/fixture/flock-worker.ts | 2 +- packages/opencode/test/npm.test.ts | 6 +- .../opencode/test/project/project.test.ts | 2 +- packages/opencode/test/session/prompt.test.ts | 4 +- packages/opencode/test/session/retry.test.ts | 2 +- .../test/session/snapshot-tool-race.test.ts | 2 +- .../opencode/test/storage/storage.test.ts | 2 +- .../opencode/test/tool/apply_patch.test.ts | 2 +- packages/opencode/test/tool/bash.test.ts | 2 +- packages/opencode/test/tool/edit.test.ts | 2 +- packages/opencode/test/tool/glob.test.ts | 2 +- packages/opencode/test/tool/grep.test.ts | 2 +- packages/opencode/test/tool/lsp.test.ts | 2 +- packages/opencode/test/tool/read.test.ts | 2 +- packages/opencode/test/tool/write.test.ts | 2 +- packages/opencode/test/util/glob.test.ts | 2 +- packages/opencode/test/util/module.test.ts | 2 +- packages/ui/package.json | 2 +- packages/ui/src/components/file.tsx | 2 +- packages/ui/src/components/line-comment.tsx | 2 +- packages/ui/src/components/markdown.tsx | 2 +- packages/ui/src/components/message-part.tsx | 4 +- packages/ui/src/components/session-review.tsx | 4 +- packages/ui/src/components/session-turn.tsx | 4 +- 166 files changed, 218 insertions(+), 218 deletions(-) rename packages/{shared => core}/package.json (96%) rename packages/{shared => core}/src/filesystem.ts (100%) rename packages/{shared => core}/src/global.ts (100%) rename packages/{shared => core}/src/types.d.ts (100%) rename packages/{shared => core}/src/util/array.ts (100%) rename packages/{shared => core}/src/util/binary.ts (100%) rename packages/{shared => core}/src/util/effect-flock.ts (100%) rename packages/{shared => core}/src/util/encode.ts (100%) rename packages/{shared => core}/src/util/error.ts (100%) rename packages/{shared => core}/src/util/flock.ts (100%) rename packages/{shared => core}/src/util/fn.ts (100%) rename packages/{shared => core}/src/util/glob.ts (100%) rename packages/{shared => core}/src/util/hash.ts (100%) rename packages/{shared => core}/src/util/identifier.ts (100%) rename packages/{shared => core}/src/util/iife.ts (100%) rename packages/{shared => core}/src/util/lazy.ts (100%) rename packages/{shared => core}/src/util/module.ts (100%) rename packages/{shared => core}/src/util/path.ts (100%) rename packages/{shared => core}/src/util/retry.ts (100%) rename packages/{shared => core}/src/util/slug.ts (100%) rename packages/{shared => core}/sst-env.d.ts (100%) rename packages/{shared => core}/test/filesystem/filesystem.test.ts (99%) rename packages/{shared => core}/test/fixture/effect-flock-worker.ts (88%) rename packages/{shared => core}/test/fixture/flock-worker.ts (96%) rename packages/{shared => core}/test/lib/effect.ts (100%) rename packages/{shared => core}/test/util/effect-flock.test.ts (98%) rename packages/{shared => core}/test/util/flock.test.ts (98%) rename packages/{shared => core}/tsconfig.json (100%) diff --git a/bun.lock b/bun.lock index ff5f6bb7d..e28376682 100644 --- a/bun.lock +++ b/bun.lock @@ -32,8 +32,8 @@ "version": "1.14.25", "dependencies": { "@kobalte/core": "catalog:", + "@opencode-ai/core": "workspace:*", "@opencode-ai/sdk": "workspace:*", - "@opencode-ai/shared": "workspace:*", "@opencode-ai/ui": "workspace:*", "@shikijs/transformers": "3.9.2", "@solid-primitives/active-element": "2.1.3", @@ -190,6 +190,30 @@ "cloudflare": "5.2.0", }, }, + "packages/core": { + "name": "@opencode-ai/core", + "version": "1.14.25", + "bin": { + "opencode": "./bin/opencode", + }, + "dependencies": { + "@effect/platform-node": "catalog:", + "@npmcli/arborist": "catalog:", + "effect": "catalog:", + "glob": "13.0.5", + "mime-types": "3.0.2", + "minimatch": "10.2.5", + "semver": "catalog:", + "xdg-basedir": "5.1.0", + "zod": "catalog:", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/npmcli__arborist": "6.3.3", + "@types/semver": "catalog:", + }, + }, "packages/desktop": { "name": "@opencode-ai/desktop", "version": "1.14.25", @@ -271,7 +295,7 @@ "name": "@opencode-ai/enterprise", "version": "1.14.25", "dependencies": { - "@opencode-ai/shared": "workspace:*", + "@opencode-ai/core": "workspace:*", "@opencode-ai/ui": "workspace:*", "@pierre/diffs": "catalog:", "@solidjs/meta": "catalog:", @@ -426,8 +450,8 @@ "@babel/core": "7.28.4", "@effect/language-service": "0.84.2", "@octokit/webhooks-types": "7.6.1", + "@opencode-ai/core": "workspace:*", "@opencode-ai/script": "workspace:*", - "@opencode-ai/shared": "workspace:*", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", @@ -508,30 +532,6 @@ "typescript": "catalog:", }, }, - "packages/shared": { - "name": "@opencode-ai/shared", - "version": "1.14.25", - "bin": { - "opencode": "./bin/opencode", - }, - "dependencies": { - "@effect/platform-node": "catalog:", - "@npmcli/arborist": "catalog:", - "effect": "catalog:", - "glob": "13.0.5", - "mime-types": "3.0.2", - "minimatch": "10.2.5", - "semver": "catalog:", - "xdg-basedir": "5.1.0", - "zod": "catalog:", - }, - "devDependencies": { - "@tsconfig/bun": "catalog:", - "@types/bun": "catalog:", - "@types/npmcli__arborist": "6.3.3", - "@types/semver": "catalog:", - }, - }, "packages/slack": { "name": "@opencode-ai/slack", "version": "1.14.25", @@ -572,8 +572,8 @@ "version": "1.14.25", "dependencies": { "@kobalte/core": "catalog:", + "@opencode-ai/core": "workspace:*", "@opencode-ai/sdk": "workspace:*", - "@opencode-ai/shared": "workspace:*", "@pierre/diffs": "catalog:", "@shikijs/transformers": "3.9.2", "@solid-primitives/bounds": "0.1.3", @@ -1554,6 +1554,8 @@ "@opencode-ai/console-resource": ["@opencode-ai/console-resource@workspace:packages/console/resource"], + "@opencode-ai/core": ["@opencode-ai/core@workspace:packages/core"], + "@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"], "@opencode-ai/desktop-electron": ["@opencode-ai/desktop-electron@workspace:packages/desktop-electron"], @@ -1568,8 +1570,6 @@ "@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"], - "@opencode-ai/shared": ["@opencode-ai/shared@workspace:packages/shared"], - "@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"], "@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"], diff --git a/packages/app/package.json b/packages/app/package.json index 7f65da4d9..f9d8150ba 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -42,7 +42,7 @@ "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", - "@opencode-ai/shared": "workspace:*", + "@opencode-ai/core": "workspace:*", "@shikijs/transformers": "3.9.2", "@solid-primitives/active-element": "2.1.3", "@solid-primitives/audio": "1.4.2", diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 8eb12daf5..b4b69246c 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -9,7 +9,7 @@ import { createStore } from "solid-js/store" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" import { type LocalProject, getAvatarColors } from "@/context/layout" -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { Avatar } from "@opencode-ai/ui/avatar" import { useLanguage } from "@/context/language" import { getProjectAvatarSource } from "@/pages/layout/sidebar-items" diff --git a/packages/app/src/components/dialog-fork.tsx b/packages/app/src/components/dialog-fork.tsx index 710618c30..3618a0581 100644 --- a/packages/app/src/components/dialog-fork.tsx +++ b/packages/app/src/components/dialog-fork.tsx @@ -9,7 +9,7 @@ import { List } from "@opencode-ai/ui/list" import { showToast } from "@opencode-ai/ui/toast" import { extractPromptFromParts } from "@/utils/prompt" import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client" -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { base64Encode } from "@opencode-ai/core/util/encode" import { useLanguage } from "@/context/language" interface ForkableMessage { diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx index 903cb1915..005d28709 100644 --- a/packages/app/src/components/dialog-select-directory.tsx +++ b/packages/app/src/components/dialog-select-directory.tsx @@ -3,7 +3,7 @@ import { Dialog } from "@opencode-ai/ui/dialog" import { FileIcon } from "@opencode-ai/ui/file-icon" import { List } from "@opencode-ai/ui/list" import type { ListRef } from "@opencode-ai/ui/list" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import fuzzysort from "fuzzysort" import { createMemo, createResource, createSignal } from "solid-js" import { useGlobalSDK } from "@/context/global-sdk" diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 186906f92..63a321e46 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -4,8 +4,8 @@ import { FileIcon } from "@opencode-ai/ui/file-icon" import { Icon } from "@opencode-ai/ui/icon" import { Keybind } from "@opencode-ai/ui/keybind" import { List } from "@opencode-ai/ui/list" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { useNavigate } from "@solidjs/router" import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js" import { formatKeybind, useCommand, type CommandOption } from "@/context/command" diff --git a/packages/app/src/components/prompt-input/build-request-parts.ts b/packages/app/src/components/prompt-input/build-request-parts.ts index c268af35e..98771aedd 100644 --- a/packages/app/src/components/prompt-input/build-request-parts.ts +++ b/packages/app/src/components/prompt-input/build-request-parts.ts @@ -1,4 +1,4 @@ -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/sdk/v2/client" import type { FileSelection } from "@/context/file" import { encodeFilePath } from "@/context/file/path" diff --git a/packages/app/src/components/prompt-input/context-items.tsx b/packages/app/src/components/prompt-input/context-items.tsx index 9f20f1c04..95289f989 100644 --- a/packages/app/src/components/prompt-input/context-items.tsx +++ b/packages/app/src/components/prompt-input/context-items.tsx @@ -2,7 +2,7 @@ import { Component, For, Show } from "solid-js" import { FileIcon } from "@opencode-ai/ui/file-icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip } from "@opencode-ai/ui/tooltip" -import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/shared/util/path" +import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/core/util/path" import type { ContextItem } from "@/context/prompt" type PromptContextItem = ContextItem & { key: string } diff --git a/packages/app/src/components/prompt-input/slash-popover.tsx b/packages/app/src/components/prompt-input/slash-popover.tsx index 0c8c95923..d8c4bd035 100644 --- a/packages/app/src/components/prompt-input/slash-popover.tsx +++ b/packages/app/src/components/prompt-input/slash-popover.tsx @@ -1,7 +1,7 @@ import { Component, For, Match, Show, Switch } from "solid-js" import { FileIcon } from "@opencode-ai/ui/file-icon" import { Icon } from "@opencode-ai/ui/icon" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" export type AtOption = | { type: "agent"; name: string; display: string } diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts index cf9949723..83b6212dc 100644 --- a/packages/app/src/components/prompt-input/submit.test.ts +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -74,7 +74,7 @@ beforeAll(async () => { showToast: () => 0, })) - mock.module("@opencode-ai/shared/util/encode", () => ({ + mock.module("@opencode-ai/core/util/encode", () => ({ base64Encode: (value: string) => value, })) diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 6805f619c..05f0a3ed2 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -1,7 +1,7 @@ import type { Message, Session } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { Binary } from "@opencode-ai/shared/util/binary" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { Binary } from "@opencode-ai/core/util/binary" import { useNavigate, useParams } from "@solidjs/router" import { batch, type Accessor } from "solid-js" import type { FileSelection } from "@/context/file" diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index abf4c9334..43741bd3f 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -1,8 +1,8 @@ import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js" import type { JSX } from "solid-js" import { useSync } from "@/context/sync" -import { checksum } from "@opencode-ai/shared/util/encode" -import { findLast } from "@opencode-ai/shared/util/array" +import { checksum } from "@opencode-ai/core/util/encode" +import { findLast } from "@opencode-ai/core/util/array" import { same } from "@/utils/same" import { Icon } from "@opencode-ai/ui/icon" import { Accordion } from "@opencode-ai/ui/accordion" diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 021e5be67..3d4f58dee 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -7,7 +7,7 @@ import { Keybind } from "@opencode-ai/ui/keybind" import { Spinner } from "@opencode-ai/ui/spinner" import { showToast } from "@opencode-ai/ui/toast" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js" import { createStore } from "solid-js/store" import { Portal } from "solid-js/web" diff --git a/packages/app/src/components/session/session-new-view.tsx b/packages/app/src/components/session/session-new-view.tsx index d2cac28fc..36c1eb42c 100644 --- a/packages/app/src/components/session/session-new-view.tsx +++ b/packages/app/src/components/session/session-new-view.tsx @@ -5,7 +5,7 @@ import { useSDK } from "@/context/sdk" import { useLanguage } from "@/context/language" import { Icon } from "@opencode-ai/ui/icon" import { Mark } from "@opencode-ai/ui/logo" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" const MAIN_WORKTREE = "main" const CREATE_WORKTREE = "create" diff --git a/packages/app/src/components/session/session-sortable-tab.tsx b/packages/app/src/components/session/session-sortable-tab.tsx index fb2275c44..f04228ca6 100644 --- a/packages/app/src/components/session/session-sortable-tab.tsx +++ b/packages/app/src/components/session/session-sortable-tab.tsx @@ -5,7 +5,7 @@ import { FileIcon } from "@opencode-ai/ui/file-icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { TooltipKeybind } from "@opencode-ai/ui/tooltip" import { Tabs } from "@opencode-ai/ui/tabs" -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { useFile } from "@/context/file" import { useLanguage } from "@/context/language" import { useCommand } from "@/context/command" diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx index 8998731a6..0298e3416 100644 --- a/packages/app/src/context/file.tsx +++ b/packages/app/src/context/file.tsx @@ -3,7 +3,7 @@ import { createStore, produce, reconcile } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import { showToast } from "@opencode-ai/ui/toast" import { useParams } from "@solidjs/router" -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { useSDK } from "./sdk" import { useSync } from "./sync" import { useLanguage } from "@/context/language" diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index b742667d7..86496bad5 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -8,7 +8,7 @@ import type { Todo, } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js" import { createStore, produce, reconcile } from "solid-js/store" import { useLanguage } from "@/context/language" diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index be789a5e5..66f4a3b15 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -11,8 +11,8 @@ import type { Todo, } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" -import { getFilename } from "@opencode-ai/shared/util/path" -import { retry } from "@opencode-ai/shared/util/retry" +import { getFilename } from "@opencode-ai/core/util/path" +import { retry } from "@opencode-ai/core/util/retry" import { batch } from "solid-js" import { reconcile, type SetStoreFunction, type Store } from "solid-js/store" import type { State, VcsCache } from "./types" diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index 82408fdfe..5f43c341b 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -1,4 +1,4 @@ -import { Binary } from "@opencode-ai/shared/util/binary" +import { Binary } from "@opencode-ai/core/util/binary" import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store" import type { Message, diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index 0b0972ee6..2db0f9b04 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -1,5 +1,5 @@ import { createSimpleContext } from "@opencode-ai/ui/context" -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { base64Encode } from "@opencode-ai/core/util/encode" import { useParams } from "@solidjs/router" import { batch, createEffect, createMemo } from "solid-js" import { createStore } from "solid-js/store" diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx index 251b67b06..c926dc1d9 100644 --- a/packages/app/src/context/notification.tsx +++ b/packages/app/src/context/notification.tsx @@ -7,8 +7,8 @@ import { useGlobalSync } from "./global-sync" import { usePlatform } from "@/context/platform" import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" -import { Binary } from "@opencode-ai/shared/util/binary" -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { Binary } from "@opencode-ai/core/util/binary" +import { base64Encode } from "@opencode-ai/core/util/encode" import { decode64 } from "@/utils/base64" import { EventSessionError } from "@opencode-ai/sdk/v2" import { Persist, persisted } from "@/utils/persist" diff --git a/packages/app/src/context/permission-auto-respond.test.ts b/packages/app/src/context/permission-auto-respond.test.ts index 2f8ca6265..002ae94e5 100644 --- a/packages/app/src/context/permission-auto-respond.test.ts +++ b/packages/app/src/context/permission-auto-respond.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client" -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { base64Encode } from "@opencode-ai/core/util/encode" import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond" const session = (input: { id: string; parentID?: string }) => diff --git a/packages/app/src/context/permission-auto-respond.ts b/packages/app/src/context/permission-auto-respond.ts index 2ebca3434..58ab75c57 100644 --- a/packages/app/src/context/permission-auto-respond.ts +++ b/packages/app/src/context/permission-auto-respond.ts @@ -1,4 +1,4 @@ -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { base64Encode } from "@opencode-ai/core/util/encode" export function acceptKey(sessionID: string, directory?: string) { if (!directory) return sessionID diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx index 15af57b35..dffb79831 100644 --- a/packages/app/src/context/prompt.tsx +++ b/packages/app/src/context/prompt.tsx @@ -1,5 +1,5 @@ import { createSimpleContext } from "@opencode-ai/ui/context" -import { checksum } from "@opencode-ai/shared/util/encode" +import { checksum } from "@opencode-ai/core/util/encode" import { useParams } from "@solidjs/router" import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js" import { createStore, type SetStoreFunction } from "solid-js/store" diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx index 29b7fe68c..34b597b6b 100644 --- a/packages/app/src/context/sync.tsx +++ b/packages/app/src/context/sync.tsx @@ -1,7 +1,7 @@ import { batch, createMemo } from "solid-js" import { createStore, produce, reconcile } from "solid-js/store" -import { Binary } from "@opencode-ai/shared/util/binary" -import { retry } from "@opencode-ai/shared/util/retry" +import { Binary } from "@opencode-ai/core/util/binary" +import { retry } from "@opencode-ai/core/util/retry" import { createSimpleContext } from "@opencode-ai/ui/context" import { clearSessionPrefetch, diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index 36514f56c..90ce3c1a5 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -1,6 +1,6 @@ import { DataProvider } from "@opencode-ai/ui/context" import { showToast } from "@opencode-ai/ui/toast" -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { base64Encode } from "@opencode-ai/core/util/encode" import { useLocation, useNavigate, useParams } from "@solidjs/router" import { createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js" import { useLanguage } from "@/context/language" diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 46cacdf62..2df69ee92 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -3,7 +3,7 @@ import { Button } from "@opencode-ai/ui/button" import { Logo } from "@opencode-ai/ui/logo" import { useLayout } from "@/context/layout" import { useNavigate } from "@solidjs/router" -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { base64Encode } from "@opencode-ai/core/util/encode" import { Icon } from "@opencode-ai/ui/icon" import { usePlatform } from "@/context/platform" import { DateTime } from "luxon" diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index ac5cf104a..d9ce87a02 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -17,7 +17,7 @@ import { useLocation, useNavigate, useParams } from "@solidjs/router" import { useLayout, LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" import { Persist, persisted } from "@/utils/persist" -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { base64Encode } from "@opencode-ai/core/util/encode" import { decode64 } from "@/utils/base64" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Button } from "@opencode-ai/ui/button" @@ -25,7 +25,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip } from "@opencode-ai/ui/tooltip" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Dialog } from "@opencode-ai/ui/dialog" -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { Session, type Message } from "@opencode-ai/sdk/v2/client" import { usePlatform } from "@/context/platform" import { useSettings } from "@/context/settings" @@ -48,8 +48,8 @@ import { } from "@/context/global-sync/session-prefetch" import { useNotification } from "@/context/notification" import { usePermission } from "@/context/permission" -import { Binary } from "@opencode-ai/shared/util/binary" -import { retry } from "@opencode-ai/shared/util/retry" +import { Binary } from "@opencode-ai/core/util/binary" +import { retry } from "@opencode-ai/core/util/retry" import { playSoundById } from "@/utils/sound" import { createAim } from "@/utils/aim" import { setNavigate } from "@/utils/notification-click" diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts index 32b94c9cb..4bc5254d9 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -1,4 +1,4 @@ -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { type Session } from "@opencode-ai/sdk/v2/client" type SessionStore = { diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index 9a9a1d7fc..d9fd4d6a8 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -4,7 +4,7 @@ import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Spinner } from "@opencode-ai/ui/spinner" import { Tooltip } from "@opencode-ai/ui/tooltip" -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { A, useParams } from "@solidjs/router" import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js" import { useGlobalSync } from "@/context/global-sync" diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx index 076e1ef88..2ba20092c 100644 --- a/packages/app/src/pages/layout/sidebar-project.tsx +++ b/packages/app/src/pages/layout/sidebar-project.tsx @@ -1,6 +1,6 @@ import { createMemo, For, Show, type Accessor, type JSX } from "solid-js" import { createStore } from "solid-js/store" -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { base64Encode } from "@opencode-ai/core/util/encode" import { Button } from "@opencode-ai/ui/button" import { ContextMenu } from "@opencode-ai/ui/context-menu" import { HoverCard } from "@opencode-ai/ui/hover-card" diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index cbb570106..0a3fc7f41 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -3,8 +3,8 @@ import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "so import { createStore } from "solid-js/store" import { createSortable } from "@thisbeyond/solid-dnd" import { createMediaQuery } from "@solid-primitives/media" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { getFilename } from "@opencode-ai/shared/util/path" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { getFilename } from "@opencode-ai/core/util/path" import { Button } from "@opencode-ai/ui/button" import { Collapsible } from "@opencode-ai/ui/collapsible" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 4ae973b85..1345e355e 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -28,7 +28,7 @@ import { createAutoScroll } from "@opencode-ai/ui/hooks" import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" import { Button } from "@opencode-ai/ui/button" import { showToast } from "@opencode-ai/ui/toast" -import { checksum } from "@opencode-ai/shared/util/encode" +import { checksum } from "@opencode-ai/core/util/encode" import { useSearchParams } from "@solidjs/router" import { NewSessionView, SessionHeader } from "@/components/session" import { useComments } from "@/context/comments" diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index 37bffcd2f..65b076d7c 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/packages/app/src/pages/session/file-tabs.tsx @@ -6,7 +6,7 @@ import type { FileSearchHandle } from "@opencode-ai/ui/file" import { useFileComponent } from "@opencode-ai/ui/context/file" import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" import { createLineCommentController } from "@opencode-ai/ui/line-comment-annotations" -import { sampledChecksum } from "@opencode-ai/shared/util/encode" +import { sampledChecksum } from "@opencode-ai/core/util/encode" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tabs } from "@opencode-ai/ui/tabs" diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 592ca774e..8bbaafb4e 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -15,8 +15,8 @@ import { ScrollView } from "@opencode-ai/ui/scroll-view" import { TextField } from "@opencode-ai/ui/text-field" import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2" import { showToast } from "@opencode-ai/ui/toast" -import { Binary } from "@opencode-ai/shared/util/binary" -import { getFilename } from "@opencode-ai/shared/util/path" +import { Binary } from "@opencode-ai/core/util/binary" +import { getFilename } from "@opencode-ai/core/util/path" import { Popover as KobaltePopover } from "@kobalte/core/popover" import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture" import { SessionContextUsage } from "@/components/session-context-usage" diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index d649aeb0c..922299bec 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -14,7 +14,7 @@ import { useSettings } from "@/context/settings" import { useSync } from "@/context/sync" import { useTerminal } from "@/context/terminal" import { showToast } from "@opencode-ai/ui/toast" -import { findLast } from "@opencode-ai/shared/util/array" +import { findLast } from "@opencode-ai/core/util/array" import { createSessionTabs } from "@/pages/session/helpers" import { extractPromptFromParts } from "@/utils/prompt" import { UserMessage } from "@opencode-ai/sdk/v2" diff --git a/packages/app/src/utils/base64.ts b/packages/app/src/utils/base64.ts index f60dff2b6..34b904051 100644 --- a/packages/app/src/utils/base64.ts +++ b/packages/app/src/utils/base64.ts @@ -1,4 +1,4 @@ -import { base64Decode } from "@opencode-ai/shared/util/encode" +import { base64Decode } from "@opencode-ai/core/util/encode" export function decode64(value: string | undefined) { if (value === undefined) return diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts index 0cac30cb1..024552727 100644 --- a/packages/app/src/utils/persist.ts +++ b/packages/app/src/utils/persist.ts @@ -1,6 +1,6 @@ import { Platform, usePlatform } from "@/context/platform" import { makePersisted, type AsyncStorage, type SyncStorage } from "@solid-primitives/storage" -import { checksum } from "@opencode-ai/shared/util/encode" +import { checksum } from "@opencode-ai/core/util/encode" import { createResource, type Accessor } from "solid-js" import type { SetStoreFunction, Store } from "solid-js/store" diff --git a/packages/shared/package.json b/packages/core/package.json similarity index 96% rename from packages/shared/package.json rename to packages/core/package.json index beb0d50ed..48d44ccf3 100644 --- a/packages/shared/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "version": "1.14.25", - "name": "@opencode-ai/shared", + "name": "@opencode-ai/core", "type": "module", "license": "MIT", "private": true, diff --git a/packages/shared/src/filesystem.ts b/packages/core/src/filesystem.ts similarity index 100% rename from packages/shared/src/filesystem.ts rename to packages/core/src/filesystem.ts diff --git a/packages/shared/src/global.ts b/packages/core/src/global.ts similarity index 100% rename from packages/shared/src/global.ts rename to packages/core/src/global.ts diff --git a/packages/shared/src/types.d.ts b/packages/core/src/types.d.ts similarity index 100% rename from packages/shared/src/types.d.ts rename to packages/core/src/types.d.ts diff --git a/packages/shared/src/util/array.ts b/packages/core/src/util/array.ts similarity index 100% rename from packages/shared/src/util/array.ts rename to packages/core/src/util/array.ts diff --git a/packages/shared/src/util/binary.ts b/packages/core/src/util/binary.ts similarity index 100% rename from packages/shared/src/util/binary.ts rename to packages/core/src/util/binary.ts diff --git a/packages/shared/src/util/effect-flock.ts b/packages/core/src/util/effect-flock.ts similarity index 100% rename from packages/shared/src/util/effect-flock.ts rename to packages/core/src/util/effect-flock.ts diff --git a/packages/shared/src/util/encode.ts b/packages/core/src/util/encode.ts similarity index 100% rename from packages/shared/src/util/encode.ts rename to packages/core/src/util/encode.ts diff --git a/packages/shared/src/util/error.ts b/packages/core/src/util/error.ts similarity index 100% rename from packages/shared/src/util/error.ts rename to packages/core/src/util/error.ts diff --git a/packages/shared/src/util/flock.ts b/packages/core/src/util/flock.ts similarity index 100% rename from packages/shared/src/util/flock.ts rename to packages/core/src/util/flock.ts diff --git a/packages/shared/src/util/fn.ts b/packages/core/src/util/fn.ts similarity index 100% rename from packages/shared/src/util/fn.ts rename to packages/core/src/util/fn.ts diff --git a/packages/shared/src/util/glob.ts b/packages/core/src/util/glob.ts similarity index 100% rename from packages/shared/src/util/glob.ts rename to packages/core/src/util/glob.ts diff --git a/packages/shared/src/util/hash.ts b/packages/core/src/util/hash.ts similarity index 100% rename from packages/shared/src/util/hash.ts rename to packages/core/src/util/hash.ts diff --git a/packages/shared/src/util/identifier.ts b/packages/core/src/util/identifier.ts similarity index 100% rename from packages/shared/src/util/identifier.ts rename to packages/core/src/util/identifier.ts diff --git a/packages/shared/src/util/iife.ts b/packages/core/src/util/iife.ts similarity index 100% rename from packages/shared/src/util/iife.ts rename to packages/core/src/util/iife.ts diff --git a/packages/shared/src/util/lazy.ts b/packages/core/src/util/lazy.ts similarity index 100% rename from packages/shared/src/util/lazy.ts rename to packages/core/src/util/lazy.ts diff --git a/packages/shared/src/util/module.ts b/packages/core/src/util/module.ts similarity index 100% rename from packages/shared/src/util/module.ts rename to packages/core/src/util/module.ts diff --git a/packages/shared/src/util/path.ts b/packages/core/src/util/path.ts similarity index 100% rename from packages/shared/src/util/path.ts rename to packages/core/src/util/path.ts diff --git a/packages/shared/src/util/retry.ts b/packages/core/src/util/retry.ts similarity index 100% rename from packages/shared/src/util/retry.ts rename to packages/core/src/util/retry.ts diff --git a/packages/shared/src/util/slug.ts b/packages/core/src/util/slug.ts similarity index 100% rename from packages/shared/src/util/slug.ts rename to packages/core/src/util/slug.ts diff --git a/packages/shared/sst-env.d.ts b/packages/core/sst-env.d.ts similarity index 100% rename from packages/shared/sst-env.d.ts rename to packages/core/sst-env.d.ts diff --git a/packages/shared/test/filesystem/filesystem.test.ts b/packages/core/test/filesystem/filesystem.test.ts similarity index 99% rename from packages/shared/test/filesystem/filesystem.test.ts rename to packages/core/test/filesystem/filesystem.test.ts index b49026bcb..b77f4e356 100644 --- a/packages/shared/test/filesystem/filesystem.test.ts +++ b/packages/core/test/filesystem/filesystem.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "bun:test" import { Effect, Layer, FileSystem } from "effect" import { NodeFileSystem } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { testEffect } from "../lib/effect" import path from "path" diff --git a/packages/shared/test/fixture/effect-flock-worker.ts b/packages/core/test/fixture/effect-flock-worker.ts similarity index 88% rename from packages/shared/test/fixture/effect-flock-worker.ts rename to packages/core/test/fixture/effect-flock-worker.ts index c9116c2d5..3dc3ee2c8 100644 --- a/packages/shared/test/fixture/effect-flock-worker.ts +++ b/packages/core/test/fixture/effect-flock-worker.ts @@ -1,9 +1,9 @@ import fs from "fs/promises" import os from "os" import { Effect, Layer } from "effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" -import { Global } from "@opencode-ai/shared/global" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { EffectFlock } from "@opencode-ai/core/util/effect-flock" +import { Global } from "@opencode-ai/core/global" type Msg = { key: string diff --git a/packages/shared/test/fixture/flock-worker.ts b/packages/core/test/fixture/flock-worker.ts similarity index 96% rename from packages/shared/test/fixture/flock-worker.ts rename to packages/core/test/fixture/flock-worker.ts index 9954d290c..0b9c314c0 100644 --- a/packages/shared/test/fixture/flock-worker.ts +++ b/packages/core/test/fixture/flock-worker.ts @@ -1,5 +1,5 @@ import fs from "fs/promises" -import { Flock } from "@opencode-ai/shared/util/flock" +import { Flock } from "@opencode-ai/core/util/flock" type Msg = { key: string diff --git a/packages/shared/test/lib/effect.ts b/packages/core/test/lib/effect.ts similarity index 100% rename from packages/shared/test/lib/effect.ts rename to packages/core/test/lib/effect.ts diff --git a/packages/shared/test/util/effect-flock.test.ts b/packages/core/test/util/effect-flock.test.ts similarity index 98% rename from packages/shared/test/util/effect-flock.test.ts rename to packages/core/test/util/effect-flock.test.ts index bd71e4f02..9e8bc24ac 100644 --- a/packages/shared/test/util/effect-flock.test.ts +++ b/packages/core/test/util/effect-flock.test.ts @@ -5,10 +5,10 @@ import path from "path" import os from "os" import { Cause, Effect, Exit, Layer } from "effect" import { testEffect } from "../lib/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" -import { Global } from "@opencode-ai/shared/global" -import { Hash } from "@opencode-ai/shared/util/hash" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { EffectFlock } from "@opencode-ai/core/util/effect-flock" +import { Global } from "@opencode-ai/core/global" +import { Hash } from "@opencode-ai/core/util/hash" function lock(dir: string, key: string) { return path.join(dir, Hash.fast(key) + ".lock") diff --git a/packages/shared/test/util/flock.test.ts b/packages/core/test/util/flock.test.ts similarity index 98% rename from packages/shared/test/util/flock.test.ts rename to packages/core/test/util/flock.test.ts index f1053dfd2..e1b647b64 100644 --- a/packages/shared/test/util/flock.test.ts +++ b/packages/core/test/util/flock.test.ts @@ -3,8 +3,8 @@ import fs from "fs/promises" import { spawn } from "child_process" import path from "path" import os from "os" -import { Flock } from "@opencode-ai/shared/util/flock" -import { Hash } from "@opencode-ai/shared/util/hash" +import { Flock } from "@opencode-ai/core/util/flock" +import { Hash } from "@opencode-ai/core/util/hash" type Msg = { key: string diff --git a/packages/shared/tsconfig.json b/packages/core/tsconfig.json similarity index 100% rename from packages/shared/tsconfig.json rename to packages/core/tsconfig.json diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 5f6b14ed7..a5a2997b9 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -13,7 +13,7 @@ "shell-prod": "sst shell --target Teams --stage production" }, "dependencies": { - "@opencode-ai/shared": "workspace:*", + "@opencode-ai/core": "workspace:*", "@opencode-ai/ui": "workspace:*", "aws4fetch": "^1.0.20", "@pierre/diffs": "catalog:", diff --git a/packages/enterprise/src/core/share.ts b/packages/enterprise/src/core/share.ts index 1a343272f..fb8cd3029 100644 --- a/packages/enterprise/src/core/share.ts +++ b/packages/enterprise/src/core/share.ts @@ -1,6 +1,6 @@ import { Message, Model, Part, Session, SnapshotFileDiff } from "@opencode-ai/sdk/v2" -import { fn } from "@opencode-ai/shared/util/fn" -import { iife } from "@opencode-ai/shared/util/iife" +import { fn } from "@opencode-ai/core/util/fn" +import { iife } from "@opencode-ai/core/util/iife" import z from "zod" import { Storage } from "./storage" diff --git a/packages/enterprise/src/core/storage.ts b/packages/enterprise/src/core/storage.ts index a6222e415..58d61aca7 100644 --- a/packages/enterprise/src/core/storage.ts +++ b/packages/enterprise/src/core/storage.ts @@ -1,5 +1,5 @@ import { AwsClient } from "aws4fetch" -import { lazy } from "@opencode-ai/shared/util/lazy" +import { lazy } from "@opencode-ai/core/util/lazy" export namespace Storage { export interface Adapter { diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index f3be14e39..b12afce27 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -10,9 +10,9 @@ import { Share } from "~/core/share" import { Logo, Mark } from "@opencode-ai/ui/logo" import { IconButton } from "@opencode-ai/ui/icon-button" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { iife } from "@opencode-ai/shared/util/iife" -import { Binary } from "@opencode-ai/shared/util/binary" -import { NamedError } from "@opencode-ai/shared/util/error" +import { iife } from "@opencode-ai/core/util/iife" +import { Binary } from "@opencode-ai/core/util/binary" +import { NamedError } from "@opencode-ai/core/util/error" import { DateTime } from "luxon" import { createStore } from "solid-js/store" import z from "zod" diff --git a/packages/enterprise/test/core/share.test.ts b/packages/enterprise/test/core/share.test.ts index 2877f8e0e..15c5f9205 100644 --- a/packages/enterprise/test/core/share.test.ts +++ b/packages/enterprise/test/core/share.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import { Share } from "../../src/core/share" import { Storage } from "../../src/core/storage" -import { Identifier } from "@opencode-ai/shared/util/identifier" +import { Identifier } from "@opencode-ai/core/util/identifier" describe.concurrent("core.share", () => { test("should create a share", async () => { diff --git a/packages/opencode/package.json b/packages/opencode/package.json index a0b6ddaff..1c60e58a8 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -45,7 +45,7 @@ "@effect/language-service": "0.84.2", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/script": "workspace:*", - "@opencode-ai/shared": "workspace:*", + "@opencode-ai/core": "workspace:*", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 24bcb9c2d..6ab24e26b 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -34,7 +34,7 @@ import { import { Log } from "../util" import { pathToFileURL } from "url" import { Filesystem } from "../util" -import { Hash } from "@opencode-ai/shared/util/hash" +import { Hash } from "@opencode-ai/core/util/hash" import { ACPSessionManager } from "./session" import type { ACPConfig } from "./types" import { Provider } from "../provider" diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index 5b4b5120f..00bc22329 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -2,7 +2,7 @@ import path from "path" import { Effect, Layer, Record, Result, Schema, Context } from "effect" import { zod } from "@/util/effect-zod" import { Global } from "../global" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index 9d5cd65bf..8dc6ab07e 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -10,7 +10,7 @@ import { TuiInfo } from "./tui-schema" import { Flag } from "@/flag/flag" import { isRecord } from "@/util/record" import { Global } from "@/global" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { CurrentWorkingDirectory } from "./cwd" import { ConfigPlugin } from "@/config/plugin" import { ConfigKeybinds } from "@/config/keybinds" diff --git a/packages/opencode/src/cli/cmd/tui/context/kv.tsx b/packages/opencode/src/cli/cmd/tui/context/kv.tsx index 43266315b..df8a8394c 100644 --- a/packages/opencode/src/cli/cmd/tui/context/kv.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/kv.tsx @@ -1,6 +1,6 @@ import { Global } from "@/global" import { Filesystem } from "@/util" -import { Flock } from "@opencode-ai/shared/util/flock" +import { Flock } from "@opencode-ai/core/util/flock" import { rename, rm } from "fs/promises" import { createSignal, type Setter } from "solid-js" import { createStore, unwrap } from "solid-js/store" diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 57326e3a1..d35deb0b6 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -22,7 +22,7 @@ import { createStore, produce, reconcile } from "solid-js/store" import { useProject } from "@tui/context/project" import { useEvent } from "@tui/context/event" import { useSDK } from "@tui/context/sdk" -import { Binary } from "@opencode-ai/shared/util/binary" +import { Binary } from "@opencode-ai/core/util/binary" import { createSimpleContext } from "./helper" import type { Snapshot } from "@/snapshot" import { useExit } from "./exit" diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 3ae1eb869..10f2dc49d 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -2,7 +2,7 @@ import { CliRenderEvents, SyntaxStyle, RGBA, type TerminalColors } from "@opentu import path from "path" import { createEffect, createMemo, onCleanup, onMount } from "solid-js" import { createSimpleContext } from "./helper" -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "@opencode-ai/core/util/glob" import aura from "./theme/aura.json" with { type: "json" } import ayu from "./theme/ayu.json" with { type: "json" } import catppuccin from "./theme/catppuccin.json" with { type: "json" } diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index e4a0e59eb..8eda7e022 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -32,7 +32,7 @@ import { hasTheme, upsertTheme } from "../context/theme" import { Global } from "@/global" import { Filesystem } from "@/util" import { Process } from "@/util" -import { Flock } from "@opencode-ai/shared/util/flock" +import { Flock } from "@opencode-ai/core/util/flock" import { Flag } from "@/flag/flag" import { INTERNAL_TUI_PLUGINS, type InternalTuiPlugin } from "./internal" import { setupSlots, Slot as View } from "./slots" diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts index f286b5166..adf52f568 100644 --- a/packages/opencode/src/cli/error.ts +++ b/packages/opencode/src/cli/error.ts @@ -1,4 +1,4 @@ -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { errorFormat } from "@/util/error" interface ErrorLike { diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts index 46335d24a..7b4cf7f34 100644 --- a/packages/opencode/src/cli/ui.ts +++ b/packages/opencode/src/cli/ui.ts @@ -1,6 +1,6 @@ import z from "zod" import { EOL } from "os" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { logo as glyphs } from "./logo" const wordmark = [ diff --git a/packages/opencode/src/config/agent.ts b/packages/opencode/src/config/agent.ts index 2978916b5..a8693c8aa 100644 --- a/packages/opencode/src/config/agent.ts +++ b/packages/opencode/src/config/agent.ts @@ -6,8 +6,8 @@ import { Bus } from "@/bus" import { zod } from "@/util/effect-zod" import { PositiveInt } from "@/util/schema" import { Log } from "../util" -import { NamedError } from "@opencode-ai/shared/util/error" -import { Glob } from "@opencode-ai/shared/util/glob" +import { NamedError } from "@opencode-ai/core/util/error" +import { Glob } from "@opencode-ai/core/util/glob" import { configEntryNameFromPath } from "./entry-name" import { InvalidError } from "./error" import * as ConfigMarkdown from "./markdown" diff --git a/packages/opencode/src/config/command.ts b/packages/opencode/src/config/command.ts index 3e0adccc3..36cae6f97 100644 --- a/packages/opencode/src/config/command.ts +++ b/packages/opencode/src/config/command.ts @@ -2,8 +2,8 @@ export * as ConfigCommand from "./command" import { Log } from "../util" import { Schema } from "effect" -import { NamedError } from "@opencode-ai/shared/util/error" -import { Glob } from "@opencode-ai/shared/util/glob" +import { NamedError } from "@opencode-ai/core/util/error" +import { Glob } from "@opencode-ai/core/util/glob" import { Bus } from "@/bus" import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index f1ceb1b4e..3238287be 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -6,7 +6,7 @@ import z from "zod" import { mergeDeep, pipe } from "remeda" import { Global } from "../global" import fsNode from "fs/promises" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { Flag } from "../flag/flag" import { Auth } from "../auth" import { Env } from "../env" @@ -19,10 +19,10 @@ import { Event } from "../server/event" import { Account } from "@/account/account" import { isRecord } from "@/util/record" import type { ConsoleState } from "./console-state" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { InstanceState } from "@/effect" import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect" -import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" +import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { InstanceRef } from "@/effect/instance-ref" import { zod, ZodOverride } from "@/util/effect-zod" import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema" diff --git a/packages/opencode/src/config/error.ts b/packages/opencode/src/config/error.ts index 06f549fd8..c43598048 100644 --- a/packages/opencode/src/config/error.ts +++ b/packages/opencode/src/config/error.ts @@ -1,7 +1,7 @@ export * as ConfigError from "./error" import z from "zod" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" export const JsonError = NamedError.create( "ConfigJsonError", diff --git a/packages/opencode/src/config/markdown.ts b/packages/opencode/src/config/markdown.ts index 7cad69266..d782d655e 100644 --- a/packages/opencode/src/config/markdown.ts +++ b/packages/opencode/src/config/markdown.ts @@ -1,4 +1,4 @@ -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import matter from "gray-matter" import { z } from "zod" import { Filesystem } from "../util" diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts index db4b914f7..572676fcc 100644 --- a/packages/opencode/src/config/paths.ts +++ b/packages/opencode/src/config/paths.ts @@ -7,7 +7,7 @@ import { Global } from "@/global" import { unique } from "remeda" import { JsonError } from "./error" import * as Effect from "effect/Effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" export const files = Effect.fn("ConfigPaths.projectFiles")(function* ( name: string, diff --git a/packages/opencode/src/config/plugin.ts b/packages/opencode/src/config/plugin.ts index 4277c1cd6..9667dbb59 100644 --- a/packages/opencode/src/config/plugin.ts +++ b/packages/opencode/src/config/plugin.ts @@ -1,4 +1,4 @@ -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "@opencode-ai/core/util/glob" import { Schema } from "effect" import { pathToFileURL } from "url" import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared" diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index 107f2d990..e1ebb613e 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -12,7 +12,7 @@ import { Flag } from "@/flag/flag" import { Log } from "@/util" import { Filesystem } from "@/util" import { ProjectID } from "@/project/schema" -import { Slug } from "@opencode-ai/shared/util/slug" +import { Slug } from "@opencode-ai/core/util/slug" import { WorkspaceTable } from "./workspace.sql" import { getAdaptor } from "./adaptors" import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types" diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index d68e00a32..6c9d949b8 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -2,7 +2,7 @@ import { Layer, ManagedRuntime } from "effect" import { attach } from "./run-service" import * as Observability from "./observability" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Bus } from "@/bus" import { Auth } from "@/auth" import { Account } from "@/account/account" diff --git a/packages/opencode/src/file/ignore.ts b/packages/opencode/src/file/ignore.ts index efce87280..68c359b9a 100644 --- a/packages/opencode/src/file/ignore.ts +++ b/packages/opencode/src/file/ignore.ts @@ -1,4 +1,4 @@ -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "@opencode-ai/core/util/glob" const FOLDERS = new Set([ "node_modules", diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index 05e2ce359..4710fd76d 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -1,7 +1,7 @@ import { BusEvent } from "@/bus/bus-event" import { InstanceState } from "@/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Git } from "@/git" import { Effect, Layer, Context, Schema, Scope } from "effect" import * as Stream from "effect/Stream" diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 64a2e3d8e..e31f53733 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -1,5 +1,5 @@ import path from "path" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Cause, Context, Effect, Fiber, Layer, Queue, Schema, Stream } from "effect" import type { PlatformError } from "effect/PlatformError" import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts index 27bac598f..7f48a0f88 100644 --- a/packages/opencode/src/global/index.ts +++ b/packages/opencode/src/global/index.ts @@ -3,7 +3,7 @@ import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir" import path from "path" import os from "os" import { Filesystem } from "../util" -import { Flock } from "@opencode-ai/shared/util/flock" +import { Flock } from "@opencode-ai/core/util/flock" const app = "opencode" diff --git a/packages/opencode/src/ide/index.ts b/packages/opencode/src/ide/index.ts index f9ce1ec63..4a2576f68 100644 --- a/packages/opencode/src/ide/index.ts +++ b/packages/opencode/src/ide/index.ts @@ -1,7 +1,7 @@ import { BusEvent } from "@/bus/bus-event" import z from "zod" import { Schema } from "effect" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { Log } from "../util" import { Process } from "@/util" diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 0a3a927b4..c27f6b740 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -12,7 +12,7 @@ import { ModelsCommand } from "./cli/cmd/models" import { UI } from "./cli/ui" import { Installation } from "./installation" import { InstallationVersion } from "./installation/version" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { FormatError } from "./cli/error" import { ServeCommand } from "./cli/cmd/serve" import { Filesystem } from "./util" diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index e8050babf..4eaa32f77 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -10,7 +10,7 @@ import { LANGUAGE_EXTENSIONS } from "./language" import z from "zod" import { Schema } from "effect" import type * as LSPServer from "./server" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { withTimeout } from "../util/timeout" import { Filesystem } from "../util" diff --git a/packages/opencode/src/lsp/lsp.ts b/packages/opencode/src/lsp/lsp.ts index 7741ff60e..5078cbadb 100644 --- a/packages/opencode/src/lsp/lsp.ts +++ b/packages/opencode/src/lsp/lsp.ts @@ -12,7 +12,7 @@ import { Process } from "../util" import { spawn as lspspawn } from "./launch" import { Effect, Layer, Context, Schema } from "effect" import { InstanceState } from "@/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { withStatics } from "@/util/schema" import { zod, ZodOverride } from "@/util/effect-zod" diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 7faaeb42f..ef001888e 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -11,7 +11,7 @@ import { Flag } from "../flag/flag" import { Archive } from "../util" import { Process } from "../util" import { which } from "../util/which" -import { Module } from "@opencode-ai/shared/util/module" +import { Module } from "@opencode-ai/core/util/module" import { spawn } from "./launch" import { Npm } from "../npm" diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts index efb046d7a..0a57fa141 100644 --- a/packages/opencode/src/mcp/auth.ts +++ b/packages/opencode/src/mcp/auth.ts @@ -2,7 +2,7 @@ import path from "path" import z from "zod" import { Global } from "../global" import { Effect, Layer, Context } from "effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" export const Tokens = z.object({ accessToken: z.string(), diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 3c6816c5b..8b2562dc4 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -12,12 +12,12 @@ import { import { Config } from "../config" import { ConfigMCP } from "../config/mcp" import { Log } from "../util" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import z from "zod/v4" import { Installation } from "../installation" import { InstallationVersion } from "../installation/version" import { withTimeout } from "@/util/timeout" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { McpOAuthProvider } from "./oauth-provider" import { McpOAuthCallback } from "./oauth-callback" import { McpAuth } from "./auth" diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts index 4b1f80707..d876b0e52 100644 --- a/packages/opencode/src/npm/index.ts +++ b/packages/opencode/src/npm/index.ts @@ -8,9 +8,9 @@ import Config from "@npmcli/config" import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js" import { Effect, Schema, Context, Layer, Option, FileSystem, Stream } from "effect" import { NodeFileSystem } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Global } from "@opencode-ai/shared/global" -import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { Global } from "@opencode-ai/core/global" +import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import * as CrossSpawnSpawner from "../effect/cross-spawn-spawner" diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index dd2a78469..4587d8fb1 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -12,7 +12,7 @@ import { createOpencodeClient } from "@opencode-ai/sdk" import { Flag } from "../flag/flag" import { CodexAuthPlugin } from "./codex" import { Session } from "../session" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { CopilotAuthPlugin } from "./github-copilot/copilot" import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth" import { PoeAuthPlugin } from "opencode-poe-auth" diff --git a/packages/opencode/src/plugin/install.ts b/packages/opencode/src/plugin/install.ts index 0525a7ba0..87798f56d 100644 --- a/packages/opencode/src/plugin/install.ts +++ b/packages/opencode/src/plugin/install.ts @@ -10,7 +10,7 @@ import { import * as ConfigPaths from "@/config/paths" import { Global } from "@/global" import { Filesystem } from "@/util" -import { Flock } from "@opencode-ai/shared/util/flock" +import { Flock } from "@opencode-ai/core/util/flock" import { isRecord } from "@/util/record" import { parsePluginSpecifier, readPackageThemes, readPluginPackage, resolvePluginTarget } from "./shared" diff --git a/packages/opencode/src/plugin/meta.ts b/packages/opencode/src/plugin/meta.ts index 86ad8fbab..4c14a0dec 100644 --- a/packages/opencode/src/plugin/meta.ts +++ b/packages/opencode/src/plugin/meta.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from "url" import { Flag } from "@/flag/flag" import { Global } from "@/global" import { Filesystem } from "@/util" -import { Flock } from "@opencode-ai/shared/util/flock" +import { Flock } from "@opencode-ai/core/util/flock" import { parsePluginSpecifier, pluginSource } from "./shared" diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index 1c5109620..cd2013674 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -1,7 +1,7 @@ import { GlobalBus } from "@/bus/global" import { disposeInstance } from "@/effect/instance-registry" import { makeRuntime } from "@/effect/run-service" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { iife } from "@/util/iife" import { Log } from "@/util" import { LocalContext } from "../util" diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 70a959064..88d033921 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -11,7 +11,7 @@ import { ProjectID } from "./schema" import { Effect, Layer, Path, Scope, Context, Stream, Types, Schema } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodePath } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts index 1c1da97bf..2fbab4f63 100644 --- a/packages/opencode/src/project/vcs.ts +++ b/packages/opencode/src/project/vcs.ts @@ -4,7 +4,7 @@ import path from "path" import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" import { InstanceState } from "@/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { FileWatcher } from "@/file/watcher" import { Git } from "@/git" import { Log } from "@/util" diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 36c4d8c23..e52464d6d 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -6,8 +6,8 @@ import { Installation } from "../installation" import { Flag } from "../flag/flag" import { lazy } from "@/util/lazy" import { Filesystem } from "../util" -import { Flock } from "@opencode-ai/shared/util/flock" -import { Hash } from "@opencode-ai/shared/util/hash" +import { Flock } from "@opencode-ai/core/util/flock" +import { Hash } from "@opencode-ai/core/util/hash" // Try to import bundled snapshot (generated at build time) // Falls back to undefined in dev mode when snapshot doesn't exist diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 0fe53e6e4..d6ccbacfc 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -5,7 +5,7 @@ import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda" import { NoSuchModelError, type Provider as SDK } from "ai" import { Log } from "../util" import { Npm } from "../npm" -import { Hash } from "@opencode-ai/shared/util/hash" +import { Hash } from "@opencode-ai/core/util/hash" import { Plugin } from "../plugin" import { type LanguageModelV3 } from "@ai-sdk/provider" import * as ModelsDev from "./models" @@ -22,7 +22,7 @@ import { pathToFileURL } from "url" import { Effect, Layer, Context, Schema, Types } from "effect" import { EffectBridge } from "@/effect" import { InstanceState } from "@/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { isRecord } from "@/util/record" import { withStatics } from "@/util/schema" diff --git a/packages/opencode/src/pty/index.ts b/packages/opencode/src/pty/index.ts index 604fa77fb..918f4f86c 100644 --- a/packages/opencode/src/pty/index.ts +++ b/packages/opencode/src/pty/index.ts @@ -4,7 +4,7 @@ import { InstanceState } from "@/effect" import { Instance } from "@/project/instance" import type { Proc } from "#pty" import { Log } from "../util" -import { lazy } from "@opencode-ai/shared/util/lazy" +import { lazy } from "@opencode-ai/core/util/lazy" import { Shell } from "@/shell/shell" import { Plugin } from "@/plugin" import { PtyID } from "./schema" diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts index b67d15f55..55d9dee79 100644 --- a/packages/opencode/src/server/middleware.ts +++ b/packages/opencode/src/server/middleware.ts @@ -1,5 +1,5 @@ import { Provider } from "../provider" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { NotFoundError } from "../storage" import { Session } from "../session" import type { ContentfulStatusCode } from "hono/utils/http-status" diff --git a/packages/opencode/src/server/routes/instance/middleware.ts b/packages/opencode/src/server/routes/instance/middleware.ts index b963268d6..19918b8b4 100644 --- a/packages/opencode/src/server/routes/instance/middleware.ts +++ b/packages/opencode/src/server/routes/instance/middleware.ts @@ -2,7 +2,7 @@ import type { MiddlewareHandler } from "hono" import { Instance } from "@/project/instance" import { InstanceBootstrap } from "@/project/bootstrap" import { AppRuntime } from "@/effect/app-runtime" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { WorkspaceContext } from "@/control-plane/workspace-context" import { WorkspaceID } from "@/control-plane/schema" diff --git a/packages/opencode/src/server/routes/instance/session.ts b/packages/opencode/src/server/routes/instance/session.ts index 4f4f8ed86..52a803467 100644 --- a/packages/opencode/src/server/routes/instance/session.ts +++ b/packages/opencode/src/server/routes/instance/session.ts @@ -25,7 +25,7 @@ import { errors } from "../../error" import { lazy } from "@/util/lazy" import { zodObject } from "@/util/effect-zod" import { Bus } from "@/bus" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { jsonRequest, runRequest } from "./trace" const log = Log.create({ service: "server" }) diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index 122644c1f..a18a55584 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -5,7 +5,7 @@ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/ import { Config } from "@/config" import { InstanceState } from "@/effect" import { Flag } from "@/flag/flag" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { withTransientReadRetry } from "@/util/effect-http-client" import { Global } from "../global" import { Log } from "../util" diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index d04645b73..8a2d352a5 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -1,7 +1,7 @@ import { BusEvent } from "@/bus/bus-event" import { SessionID, MessageID, PartID } from "./schema" import z from "zod" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai" import { LSP } from "../lsp" import { Snapshot } from "@/snapshot" diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 3d07a96ec..8e227e602 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -33,14 +33,14 @@ import { Command } from "../command" import { pathToFileURL, fileURLToPath } from "url" import { ConfigMarkdown } from "../config" import { SessionSummary } from "./summary" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { SessionProcessor } from "./processor" import { Tool } from "@/tool" import { Permission } from "@/permission" import { SessionStatus } from "./status" import { LLM } from "./llm" import { Shell } from "@/shell/shell" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Truncate } from "@/tool" import { decodeDataUrl } from "@/util/data-url" import { Process } from "@/util" diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 12fd4d345..e81e19737 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -1,4 +1,4 @@ -import type { NamedError } from "@opencode-ai/shared/util/error" +import type { NamedError } from "@opencode-ai/core/util/error" import { Cause, Clock, Duration, Effect, Schedule } from "effect" import { MessageV2 } from "./message-v2" import { iife } from "@/util/iife" diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index f4fe3bf8b..472339b05 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -1,4 +1,4 @@ -import { Slug } from "@opencode-ai/shared/util/slug" +import { Slug } from "@opencode-ai/core/util/slug" import path from "path" import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" diff --git a/packages/opencode/src/skill/discovery.ts b/packages/opencode/src/skill/discovery.ts index debd68dd3..e620de983 100644 --- a/packages/opencode/src/skill/discovery.ts +++ b/packages/opencode/src/skill/discovery.ts @@ -2,7 +2,7 @@ import { NodePath } from "@effect/platform-node" import { Effect, Layer, Path, Schema, Context } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { withTransientReadRetry } from "@/util/effect-http-client" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Global } from "../global" import { Log } from "../util" diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index dd5cc4e5d..e5282e250 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -3,17 +3,17 @@ import path from "path" import { pathToFileURL } from "url" import z from "zod" import { Effect, Layer, Context } from "effect" -import { NamedError } from "@opencode-ai/shared/util/error" +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 { Global } from "@/global" import { Permission } from "@/permission" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Config } from "../config" import { ConfigMarkdown } from "../config" -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "@opencode-ai/core/util/glob" import { Log } from "../util" import { Discovery } from "./discovery" diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index ddc4cb29e..50804ca2b 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -5,8 +5,8 @@ import path from "path" import z from "zod" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import { InstanceState } from "@/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Hash } from "@opencode-ai/shared/util/hash" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { Hash } from "@opencode-ai/core/util/hash" import { Config } from "../config" import { Global } from "../global" import { Log } from "../util" diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 2c0076452..67f5f1289 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -6,7 +6,7 @@ import { LocalContext } from "../util" import { lazy } from "../util/lazy" import { Global } from "../global" import { Log } from "../util" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import z from "zod" import path from "path" import { readFileSync, readdirSync, existsSync } from "fs" diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 05588db0f..20ca3ff53 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -8,7 +8,7 @@ import { SessionShareTable } from "../share/share.sql" import path from "path" import { existsSync } from "fs" import { Filesystem } from "../util" -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "@opencode-ai/core/util/glob" const log = Log.create({ service: "json-migration" }) diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index b1685e689..8f6332677 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -1,9 +1,9 @@ import { Log } from "../util" import path from "path" import { Global } from "../global" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import z from "zod" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Effect, Exit, Layer, Option, RcMap, Schema, Context, TxReentrantLock } from "effect" import { Git } from "@/git" diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts index 72f24a3f6..9a009189d 100644 --- a/packages/opencode/src/tool/apply_patch.ts +++ b/packages/opencode/src/tool/apply_patch.ts @@ -9,7 +9,7 @@ import { createTwoFilesPatch, diffLines } from "diff" import { assertExternalDirectoryEffect } from "./external-directory" import { trimDiff } from "./edit" import { LSP } from "../lsp" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import DESCRIPTION from "./apply_patch.txt" import { File } from "../file" import { Format } from "../format" diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 0a7e1a6dc..1b8875326 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -9,7 +9,7 @@ import { Instance } from "../project/instance" import { lazy } from "@/util/lazy" import { Language, type Node } from "web-tree-sitter" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { fileURLToPath } from "url" import { Flag } from "@/flag/flag" import { Shell } from "@/shell/shell" diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index cfff5a0a3..04a84a388 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -16,7 +16,7 @@ import { Format } from "../format" import { Instance } from "../project/instance" import { Snapshot } from "@/snapshot" import { assertExternalDirectoryEffect } from "./external-directory" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import * as Bom from "@/util/bom" function normalizeLineEndings(text: string): string { diff --git a/packages/opencode/src/tool/external-directory.ts b/packages/opencode/src/tool/external-directory.ts index 88b73da50..b8def1d75 100644 --- a/packages/opencode/src/tool/external-directory.ts +++ b/packages/opencode/src/tool/external-directory.ts @@ -4,7 +4,7 @@ import { EffectLogger } from "@/effect" import { InstanceState } from "@/effect" import type * as Tool from "./tool" import { Instance } from "../project/instance" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" type Kind = "file" | "directory" diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts index aeecfecb7..984c13d41 100644 --- a/packages/opencode/src/tool/glob.ts +++ b/packages/opencode/src/tool/glob.ts @@ -2,7 +2,7 @@ import path from "path" import { Effect, Option, Schema } from "effect" import * as Stream from "effect/Stream" import { InstanceState } from "@/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Ripgrep } from "../file/ripgrep" import { assertExternalDirectoryEffect } from "./external-directory" import DESCRIPTION from "./glob.txt" diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index 416005431..844de6753 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -2,7 +2,7 @@ import path from "path" import { Schema } from "effect" import { Effect, Option } from "effect" import { InstanceState } from "@/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Ripgrep } from "../file/ripgrep" import { assertExternalDirectoryEffect } from "./external-directory" import DESCRIPTION from "./grep.txt" diff --git a/packages/opencode/src/tool/lsp.ts b/packages/opencode/src/tool/lsp.ts index 3bcae426a..bb3b50344 100644 --- a/packages/opencode/src/tool/lsp.ts +++ b/packages/opencode/src/tool/lsp.ts @@ -6,7 +6,7 @@ import DESCRIPTION from "./lsp.txt" import { Instance } from "../project/instance" import { pathToFileURL } from "url" import { assertExternalDirectoryEffect } from "./external-directory" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" const operations = [ "goToDefinition", diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index d0995626c..e89f03109 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -3,7 +3,7 @@ import { createReadStream } from "fs" import * as path from "path" import { createInterface } from "readline" import * as Tool from "./tool" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { LSP } from "../lsp" import DESCRIPTION from "./read.txt" import { Instance } from "../project/instance" diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 539ad6320..629c57965 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -28,7 +28,7 @@ import { Log } from "@/util" import { LspTool } from "./lsp" import * as Truncate from "./truncate" import { ApplyPatchTool } from "./apply_patch" -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "@opencode-ai/core/util/glob" import path from "path" import { pathToFileURL } from "url" import { Effect, Layer, Context } from "effect" @@ -42,7 +42,7 @@ import { Question } from "../question" import { Todo } from "../session/todo" import { LSP } from "../lsp" import { Instruction } from "../session/instruction" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Bus } from "../bus" import { Agent } from "../agent/agent" import { Skill } from "../skill" diff --git a/packages/opencode/src/tool/truncate.ts b/packages/opencode/src/tool/truncate.ts index e0d846858..191d96795 100644 --- a/packages/opencode/src/tool/truncate.ts +++ b/packages/opencode/src/tool/truncate.ts @@ -2,7 +2,7 @@ import { NodePath } from "@effect/platform-node" import { Cause, Duration, Effect, Layer, Option, Schedule, Context } from "effect" import path from "path" import type { Agent } from "../agent/agent" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { evaluate } from "@/permission/evaluate" import { Config } from "../config" import { Identifier } from "../id/id" diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index b52f4a164..d977325f1 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -9,7 +9,7 @@ import { Bus } from "../bus" import { File } from "../file" import { FileWatcher } from "../file/watcher" import { Format } from "../format" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Instance } from "../project/instance" import { trimDiff } from "./edit" import { assertExternalDirectoryEffect } from "./external-directory" diff --git a/packages/opencode/src/util/bom.ts b/packages/opencode/src/util/bom.ts index 484228f3d..79de91578 100644 --- a/packages/opencode/src/util/bom.ts +++ b/packages/opencode/src/util/bom.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" const BOM_CODE = 0xfeff const BOM = String.fromCharCode(BOM_CODE) diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 6c4d45522..6225c80d2 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -4,7 +4,7 @@ import { realpathSync } from "fs" import { dirname, join, relative, resolve as pathResolve, win32 } from "path" import { Readable } from "stream" import { pipeline } from "stream/promises" -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "@opencode-ai/core/util/glob" // Fast sync version for metadata checks export async function exists(p: string): Promise { diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index 7c1581bfc..e335a8b43 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -3,7 +3,7 @@ import fs from "fs/promises" import { createWriteStream } from "fs" import { Global } from "../global" import z from "zod" -import { Glob } from "@opencode-ai/shared/util/glob" +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 diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index e122fe453..7539e8d58 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -1,5 +1,5 @@ import z from "zod" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { Global } from "../global" import { Instance } from "../project/instance" import { InstanceBootstrap } from "../project/bootstrap" @@ -8,7 +8,7 @@ import { Database, eq } from "../storage" import { ProjectTable } from "../project/project.sql" import type { ProjectID } from "../project/schema" import { Log } from "../util" -import { Slug } from "@opencode-ai/shared/util/slug" +import { Slug } from "@opencode-ai/core/util/slug" import { errorMessage } from "../util/error" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" @@ -16,7 +16,7 @@ import { Git } from "@/git" import { Effect, Layer, Path, Schema, Scope, Context, Stream } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodePath } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { BootstrapRuntime } from "@/effect/bootstrap-runtime" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import { InstanceState } from "@/effect" diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 361ac0b5d..56b8e7acd 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -3,13 +3,13 @@ import { Effect, Layer, Option } from "effect" import { NodeFileSystem, NodePath } from "@effect/platform-node" import { Config, ConfigManaged } from "../../src/config" import { ConfigParse } from "../../src/config/parse" -import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" +import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { Instance } from "../../src/project/instance" import { Auth } from "../../src/auth" import { Account } from "../../src/account/account" import { AccessToken, AccountID, OrgID } from "../../src/account/schema" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Env } from "../../src/env" import { provideTmpdirInstance } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture" @@ -895,7 +895,7 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => { }) // Note: deduplication and serialization of npm installs is now handled by the -// shared Npm.Service (via EffectFlock). Those behaviors are tested in the shared +// core Npm.Service (via EffectFlock). Those behaviors are tested in the core // package's npm tests, not here. test("resolves scoped npm plugins in config", async () => { diff --git a/packages/opencode/test/filesystem/filesystem.test.ts b/packages/opencode/test/filesystem/filesystem.test.ts index 0bb4ba583..2d9271e87 100644 --- a/packages/opencode/test/filesystem/filesystem.test.ts +++ b/packages/opencode/test/filesystem/filesystem.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "bun:test" import { Effect, Layer } from "effect" import { NodeFileSystem } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { testEffect } from "../lib/effect" import path from "path" diff --git a/packages/opencode/test/fixture/flock-worker.ts b/packages/opencode/test/fixture/flock-worker.ts index 9954d290c..0b9c314c0 100644 --- a/packages/opencode/test/fixture/flock-worker.ts +++ b/packages/opencode/test/fixture/flock-worker.ts @@ -1,5 +1,5 @@ import fs from "fs/promises" -import { Flock } from "@opencode-ai/shared/util/flock" +import { Flock } from "@opencode-ai/core/util/flock" type Msg = { key: string diff --git a/packages/opencode/test/npm.test.ts b/packages/opencode/test/npm.test.ts index b27d668c8..09fa6b351 100644 --- a/packages/opencode/test/npm.test.ts +++ b/packages/opencode/test/npm.test.ts @@ -3,9 +3,9 @@ import path from "path" import { describe, expect, test } from "bun:test" import { Effect, Layer, Stream } from "effect" import { NodeFileSystem } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Global } from "@opencode-ai/shared/global" -import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { Global } from "@opencode-ai/core/global" +import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { Npm } from "../src/npm" import { tmpdir } from "./fixture/fixture" diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index 080519a73..c61df3548 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -9,7 +9,7 @@ import { ProjectID } from "../../src/project/schema" import { Effect, Layer, Stream } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodePath } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" void Log.init({ print: false }) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 911cb4415..451f1d004 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -4,7 +4,7 @@ import { expect } from "bun:test" import { Cause, Effect, Exit, Fiber, Layer } from "effect" import path from "path" import { fileURLToPath } from "url" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { Agent as AgentSvc } from "../../src/agent/agent" import { Bus } from "../../src/bus" import { Command } from "../../src/command" @@ -21,7 +21,7 @@ import { Todo } from "../../src/session/todo" import { Session } from "../../src/session" import { LLM } from "../../src/session/llm" import { MessageV2 } from "../../src/session/message-v2" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { SessionCompaction } from "../../src/session/compaction" import { SessionSummary } from "../../src/session/summary" import { Instruction } from "../../src/session/instruction" diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 6ca8775f3..aa1a29ec1 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import type { NamedError } from "@opencode-ai/shared/util/error" +import type { NamedError } from "@opencode-ai/core/util/error" import { APICallError } from "ai" import { setTimeout as sleep } from "node:timers/promises" import { Effect, Schedule } from "effect" diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index 651754733..c7e352262 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -51,7 +51,7 @@ import { SessionStatus } from "../../src/session/status" import { Snapshot } from "../../src/snapshot" import { ToolRegistry } from "../../src/tool" import { Truncate } from "../../src/tool" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" import { Format } from "../../src/format" diff --git a/packages/opencode/test/storage/storage.test.ts b/packages/opencode/test/storage/storage.test.ts index c35244bb7..0587b9dd6 100644 --- a/packages/opencode/test/storage/storage.test.ts +++ b/packages/opencode/test/storage/storage.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import path from "path" import { Effect, Exit, Layer } from "effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Git } from "../../src/git" import { Global } from "../../src/global" diff --git a/packages/opencode/test/tool/apply_patch.test.ts b/packages/opencode/test/tool/apply_patch.test.ts index fa8843213..f311b3d9b 100644 --- a/packages/opencode/test/tool/apply_patch.test.ts +++ b/packages/opencode/test/tool/apply_patch.test.ts @@ -5,7 +5,7 @@ import { Effect, ManagedRuntime, Layer } from "effect" import { ApplyPatchTool } from "../../src/tool/apply_patch" import { Instance } from "../../src/project/instance" import { LSP } from "../../src/lsp" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Format } from "../../src/format" import { Agent } from "../../src/agent/agent" import { Bus } from "../../src/bus" diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index d66cfc3e3..fd35c9aeb 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -12,7 +12,7 @@ import { Agent } from "../../src/agent/agent" import { Truncate } from "../../src/tool" import { SessionID, MessageID } from "../../src/session/schema" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Plugin } from "../../src/plugin" const runtime = ManagedRuntime.make( diff --git a/packages/opencode/test/tool/edit.test.ts b/packages/opencode/test/tool/edit.test.ts index 82e1b4a7f..fb2080591 100644 --- a/packages/opencode/test/tool/edit.test.ts +++ b/packages/opencode/test/tool/edit.test.ts @@ -6,7 +6,7 @@ import { EditTool } from "../../src/tool/edit" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" import { LSP } from "../../src/lsp" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Format } from "../../src/format" import { Agent } from "../../src/agent/agent" import { Bus } from "../../src/bus" diff --git a/packages/opencode/test/tool/glob.test.ts b/packages/opencode/test/tool/glob.test.ts index 87d35715d..c37e7b35f 100644 --- a/packages/opencode/test/tool/glob.test.ts +++ b/packages/opencode/test/tool/glob.test.ts @@ -5,7 +5,7 @@ import { GlobTool } from "../../src/tool/glob" import { SessionID, MessageID } from "../../src/session/schema" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Truncate } from "../../src/tool" import { Agent } from "../../src/agent/agent" import { provideTmpdirInstance } from "../fixture/fixture" diff --git a/packages/opencode/test/tool/grep.test.ts b/packages/opencode/test/tool/grep.test.ts index 388828f6e..a279574e1 100644 --- a/packages/opencode/test/tool/grep.test.ts +++ b/packages/opencode/test/tool/grep.test.ts @@ -8,7 +8,7 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Truncate } from "../../src/tool" import { Agent } from "../../src/agent/agent" import { Ripgrep } from "../../src/file/ripgrep" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { testEffect } from "../lib/effect" const it = testEffect( diff --git a/packages/opencode/test/tool/lsp.test.ts b/packages/opencode/test/tool/lsp.test.ts index 57b8fc6e8..b9d48e69a 100644 --- a/packages/opencode/test/tool/lsp.test.ts +++ b/packages/opencode/test/tool/lsp.test.ts @@ -3,7 +3,7 @@ import { Effect, Layer } from "effect" import path from "path" import { Agent } from "../../src/agent/agent" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { LSP } from "../../src/lsp" import { Permission } from "../../src/permission" import { Instance } from "../../src/project/instance" diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 42817d15d..7c3bf51fe 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -3,7 +3,7 @@ import { Cause, Effect, Exit, Layer } from "effect" import path from "path" import { Agent } from "../../src/agent/agent" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { LSP } from "../../src/lsp" import { Permission } from "../../src/permission" import { Instance } from "../../src/project/instance" diff --git a/packages/opencode/test/tool/write.test.ts b/packages/opencode/test/tool/write.test.ts index 36131f959..0714d2d02 100644 --- a/packages/opencode/test/tool/write.test.ts +++ b/packages/opencode/test/tool/write.test.ts @@ -5,7 +5,7 @@ import fs from "fs/promises" import { WriteTool } from "../../src/tool/write" import { Instance } from "../../src/project/instance" import { LSP } from "../../src/lsp" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Bus } from "../../src/bus" import { Format } from "../../src/format" import { Truncate } from "../../src/tool" diff --git a/packages/opencode/test/util/glob.test.ts b/packages/opencode/test/util/glob.test.ts index e982d5194..4ed2f71f3 100644 --- a/packages/opencode/test/util/glob.test.ts +++ b/packages/opencode/test/util/glob.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "bun:test" import path from "path" import fs from "fs/promises" -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "@opencode-ai/core/util/glob" import { tmpdir } from "../fixture/fixture" describe("Glob", () => { diff --git a/packages/opencode/test/util/module.test.ts b/packages/opencode/test/util/module.test.ts index 6725149c7..19c7958fc 100644 --- a/packages/opencode/test/util/module.test.ts +++ b/packages/opencode/test/util/module.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import path from "path" -import { Module } from "@opencode-ai/shared/util/module" +import { Module } from "@opencode-ai/core/util/module" import { Filesystem } from "../../src/util" import { tmpdir } from "../fixture/fixture" diff --git a/packages/ui/package.json b/packages/ui/package.json index 9feb8c035..da7a0f673 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -44,7 +44,7 @@ "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", - "@opencode-ai/shared": "workspace:*", + "@opencode-ai/core": "workspace:*", "@pierre/diffs": "catalog:", "@shikijs/transformers": "3.9.2", "@solid-primitives/bounds": "0.1.3", diff --git a/packages/ui/src/components/file.tsx b/packages/ui/src/components/file.tsx index 633b23b70..97d4d69f7 100644 --- a/packages/ui/src/components/file.tsx +++ b/packages/ui/src/components/file.tsx @@ -1,4 +1,4 @@ -import { sampledChecksum } from "@opencode-ai/shared/util/encode" +import { sampledChecksum } from "@opencode-ai/core/util/encode" import { DEFAULT_VIRTUAL_FILE_METRICS, type DiffLineAnnotation, diff --git a/packages/ui/src/components/line-comment.tsx b/packages/ui/src/components/line-comment.tsx index e20da5a8d..e5a7af9cb 100644 --- a/packages/ui/src/components/line-comment.tsx +++ b/packages/ui/src/components/line-comment.tsx @@ -1,5 +1,5 @@ import { useFilteredList } from "@opencode-ai/ui/hooks" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { createSignal, For, onMount, Show, splitProps, type JSX } from "solid-js" import { Button } from "./button" import { FileIcon } from "./file-icon" diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx index 28653512e..56e2d9d70 100644 --- a/packages/ui/src/components/markdown.tsx +++ b/packages/ui/src/components/markdown.tsx @@ -2,7 +2,7 @@ import { useMarked } from "../context/marked" import { useI18n } from "../context/i18n" import DOMPurify from "dompurify" import morphdom from "morphdom" -import { checksum } from "@opencode-ai/shared/util/encode" +import { checksum } from "@opencode-ai/core/util/encode" import { ComponentProps, createEffect, createResource, createSignal, onCleanup, splitProps } from "solid-js" import { isServer } from "solid-js/web" import { stream } from "./markdown-stream" diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 9c0c90c00..013272205 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -45,8 +45,8 @@ import { Checkbox } from "./checkbox" import { DiffChanges } from "./diff-changes" import { Markdown } from "./markdown" import { ImagePreview } from "./image-preview" -import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/shared/util/path" -import { checksum } from "@opencode-ai/shared/util/encode" +import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/core/util/path" +import { checksum } from "@opencode-ai/core/util/encode" import { Tooltip } from "./tooltip" import { IconButton } from "./icon-button" import { Spinner } from "./spinner" diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 94bca6727..949402f43 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -11,8 +11,8 @@ import { Tooltip } from "./tooltip" import { ScrollView } from "./scroll-view" import { useFileComponent } from "../context/file" import { useI18n } from "../context/i18n" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" -import { checksum } from "@opencode-ai/shared/util/encode" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" +import { checksum } from "@opencode-ai/core/util/encode" import { createEffect, createMemo, For, Match, onCleanup, Show, Switch, untrack, type JSX } from "solid-js" import { createStore } from "solid-js/store" import { type FileContent, type SnapshotFileDiff, type VcsFileDiff } from "@opencode-ai/sdk/v2" diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 61123b180..b35f718ef 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -8,8 +8,8 @@ import type { SessionStatus } from "@opencode-ai/sdk/v2" import { useData } from "../context" import { useFileComponent } from "../context/file" -import { Binary } from "@opencode-ai/shared/util/binary" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" +import { Binary } from "@opencode-ai/core/util/binary" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js" import { createStore } from "solid-js/store" import { Dynamic } from "solid-js/web" From 1d728fc627b44b34179d4e53963b643fac867bf1 Mon Sep 17 00:00:00 2001 From: Dax Date: Sat, 25 Apr 2026 11:08:19 -0400 Subject: [PATCH 11/45] feat: add startup debug command (#24310) --- packages/opencode/src/cli/cmd/debug/index.ts | 2 ++ packages/opencode/src/cli/cmd/debug/startup.ts | 11 +++++++++++ 2 files changed, 13 insertions(+) create mode 100644 packages/opencode/src/cli/cmd/debug/startup.ts diff --git a/packages/opencode/src/cli/cmd/debug/index.ts b/packages/opencode/src/cli/cmd/debug/index.ts index 8da6ff559..e780c4ccb 100644 --- a/packages/opencode/src/cli/cmd/debug/index.ts +++ b/packages/opencode/src/cli/cmd/debug/index.ts @@ -9,6 +9,7 @@ import { ScrapCommand } from "./scrap" import { SkillCommand } from "./skill" import { SnapshotCommand } from "./snapshot" import { AgentCommand } from "./agent" +import { StartupCommand } from "./startup" export const DebugCommand = cmd({ command: "debug", @@ -22,6 +23,7 @@ export const DebugCommand = cmd({ .command(ScrapCommand) .command(SkillCommand) .command(SnapshotCommand) + .command(StartupCommand) .command(AgentCommand) .command(PathsCommand) .command({ diff --git a/packages/opencode/src/cli/cmd/debug/startup.ts b/packages/opencode/src/cli/cmd/debug/startup.ts new file mode 100644 index 000000000..27fd52469 --- /dev/null +++ b/packages/opencode/src/cli/cmd/debug/startup.ts @@ -0,0 +1,11 @@ +import { EOL } from "os" +import { cmd } from "../cmd" + +export const StartupCommand = cmd({ + command: "startup", + describe: "print startup timing", + builder: (yargs) => yargs, + handler() { + process.stdout.write(performance.now().toString() + EOL) + }, +}) From 62651c7114c8fe1b3ec9a2868f32abfc6278993f Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Apr 2026 15:16:42 +0000 Subject: [PATCH 12/45] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 30a2bbfbd..6be836b12 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-V1Rt2k7ujkqGw4pDkn++WALTy1fAugvoKLhKvwFKkss=", - "aarch64-linux": "sha256-ho0AuGbJ1qw9Hvb3EbGC8f0lWqqgUslvda/wTe32MFo=", - "aarch64-darwin": "sha256-hdUyNmp+snwtnBckHXsPMgNFUYS1sYDdngkk+AXVqzc=", - "x86_64-darwin": "sha256-P57LpQNF8fplFKQBBIukhOKbIugbViyBUIUjClXohuk=" + "x86_64-linux": "sha256-Dql+RLvSi8gtq7exwetJ4nSjKjJuyKuAf/X8X4L9E9E=", + "aarch64-linux": "sha256-qykwmIBF9tjZBjFo84e8tFix+i49TFM7Pf5jP4lLgEw=", + "aarch64-darwin": "sha256-TteIy5TuuhYsBIQ4q/weUkRnao0Y3iiKvf+noOYPuvg=", + "x86_64-darwin": "sha256-RbAqhuENpO5pT2oVUtVQ8JtUta/5hSnoqTzc3zPRCJc=" } } From a9740b9133a8056f5992b17f1b3fde15cc039f8d Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Apr 2026 13:30:12 -0400 Subject: [PATCH 13/45] fix(config): preserve permission order with Effect decode (#24308) --- packages/opencode/src/config/agent.ts | 39 +++++------ packages/opencode/src/config/config.ts | 42 ++++++------ packages/opencode/src/config/parse.ts | 48 +++++++++++++- packages/opencode/src/config/permission.ts | 37 +---------- packages/opencode/test/config/config.test.ts | 70 +++++++++++++++++--- 5 files changed, 146 insertions(+), 90 deletions(-) diff --git a/packages/opencode/src/config/agent.ts b/packages/opencode/src/config/agent.ts index a8693c8aa..1d1c66a13 100644 --- a/packages/opencode/src/config/agent.ts +++ b/packages/opencode/src/config/agent.ts @@ -1,17 +1,16 @@ export * as ConfigAgent from "./agent" -import { Schema } from "effect" -import z from "zod" +import { Exit, Schema, SchemaGetter } from "effect" import { Bus } from "@/bus" import { zod } from "@/util/effect-zod" -import { PositiveInt } from "@/util/schema" +import { PositiveInt, withStatics } from "@/util/schema" import { Log } from "../util" import { NamedError } from "@opencode-ai/core/util/error" import { Glob } from "@opencode-ai/core/util/glob" import { configEntryNameFromPath } from "./entry-name" -import { InvalidError } from "./error" import * as ConfigMarkdown from "./markdown" import { ConfigModelID } from "./model-id" +import { ConfigParse } from "./parse" import { ConfigPermission } from "./permission" const log = Log.create({ service: "config" }) @@ -77,7 +76,7 @@ const KNOWN_KEYS = new Set([ // - Translate the deprecated `tools: { name: boolean }` map into the new // `permission` shape (write-adjacent tools collapse into `permission.edit`). // - Coalesce `steps ?? maxSteps` so downstream can ignore the deprecated alias. -const normalize = (agent: z.infer) => { +const normalize = (agent: Schema.Schema.Type): Schema.Schema.Type => { const options: Record = { ...agent.options } for (const [key, value] of Object.entries(agent)) { if (!KNOWN_KEYS.has(key)) options[key] = value @@ -98,14 +97,15 @@ const normalize = (agent: z.infer) => { return { ...agent, options, permission, ...(steps !== undefined ? { steps } : {}) } } -export const Info = zod(AgentSchema).transform(normalize).meta({ ref: "AgentConfig" }) as unknown as z.ZodType< - Omit>>, "options" | "permission" | "steps"> & { - options?: Record - permission?: ConfigPermission.Info - steps?: number - } -> -export type Info = z.infer +export const Info = AgentSchema.pipe( + Schema.decodeTo(AgentSchema, { + decode: SchemaGetter.transform(normalize), + encode: SchemaGetter.passthrough({ strict: false }), + }), +) + .annotate({ identifier: "AgentConfig" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Info = Schema.Schema.Type export async function load(dir: string) { const result: Record = {} @@ -134,12 +134,7 @@ export async function load(dir: string) { ...md.data, prompt: md.content.trim(), } - const parsed = Info.safeParse(config) - if (parsed.success) { - result[config.name] = parsed.data - continue - } - throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error }) + result[config.name] = ConfigParse.effectSchema(Info, config, item) } return result } @@ -168,10 +163,10 @@ export async function loadMode(dir: string) { ...md.data, prompt: md.content.trim(), } - const parsed = Info.safeParse(config) - if (parsed.success) { + const parsed = Schema.decodeUnknownExit(Info)(config, { errors: "all", propertyOrder: "original" }) + if (Exit.isSuccess(parsed)) { result[config.name] = { - ...parsed.data, + ...parsed.value, mode: "primary" as const, } } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 3238287be..70ba14464 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -24,7 +24,7 @@ import { InstanceState } from "@/effect" import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect" import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { InstanceRef } from "@/effect/instance-ref" -import { zod, ZodOverride } from "@/util/effect-zod" +import { zod } from "@/util/effect-zod" import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema" import { ConfigAgent } from "./agent" import { ConfigCommand } from "./command" @@ -81,12 +81,10 @@ export const Server = ConfigServer.Server.zod export const Layout = ConfigLayout.Layout.zod export type Layout = ConfigLayout.Layout -// Schemas that still live at the zod layer (have .transform / .preprocess / -// .meta not expressible in current Effect Schema) get referenced via a -// ZodOverride-annotated Schema.Any. Walker sees the annotation and emits the -// exact zod directly, preserving component $refs. -const AgentRef = Schema.Any.annotate({ [ZodOverride]: ConfigAgent.Info }) -const LogLevelRef = Schema.Any.annotate({ [ZodOverride]: Log.Level }) +const LogLevelRef = Schema.Literals(["DEBUG", "INFO", "WARN", "ERROR"]).annotate({ + identifier: "LogLevel", + description: "Log level", +}) // The Effect Schema is the canonical source of truth. The `.zod` compatibility // surface is derived so existing Hono validators keep working without a parallel @@ -152,27 +150,27 @@ export const Info = Schema.Struct({ mode: Schema.optional( Schema.StructWithRest( Schema.Struct({ - build: Schema.optional(AgentRef), - plan: Schema.optional(AgentRef), + build: Schema.optional(ConfigAgent.Info), + plan: Schema.optional(ConfigAgent.Info), }), - [Schema.Record(Schema.String, AgentRef)], + [Schema.Record(Schema.String, ConfigAgent.Info)], ), ).annotate({ description: "@deprecated Use `agent` field instead." }), agent: Schema.optional( Schema.StructWithRest( Schema.Struct({ // primary - plan: Schema.optional(AgentRef), - build: Schema.optional(AgentRef), + plan: Schema.optional(ConfigAgent.Info), + build: Schema.optional(ConfigAgent.Info), // subagent - general: Schema.optional(AgentRef), - explore: Schema.optional(AgentRef), + general: Schema.optional(ConfigAgent.Info), + explore: Schema.optional(ConfigAgent.Info), // specialized - title: Schema.optional(AgentRef), - summary: Schema.optional(AgentRef), - compaction: Schema.optional(AgentRef), + title: Schema.optional(ConfigAgent.Info), + summary: Schema.optional(ConfigAgent.Info), + compaction: Schema.optional(ConfigAgent.Info), }), - [Schema.Record(Schema.String, AgentRef)], + [Schema.Record(Schema.String, ConfigAgent.Info)], ), ).annotate({ description: "Agent configuration, see https://opencode.ai/docs/agents" }), provider: Schema.optional(Schema.Record(Schema.String, ConfigProvider.Info)).annotate({ @@ -184,7 +182,7 @@ export const Info = Schema.Struct({ Schema.Union([ ConfigMCP.Info, // Matches the legacy `{ enabled: false }` form used to disable a server. - Schema.Any.annotate({ [ZodOverride]: z.object({ enabled: z.boolean() }).strict() }), + Schema.Struct({ enabled: Schema.Boolean }), ]), ), ).annotate({ description: "MCP (Model Context Protocol) server configurations" }), @@ -362,7 +360,7 @@ export const layer = Layer.effect( ), ) const parsed = ConfigParse.jsonc(expanded, source) - const data = ConfigParse.schema(Info.zod, normalizeLoadedConfig(parsed, source), source) + const data = ConfigParse.effectSchema(Info, normalizeLoadedConfig(parsed, source), source) if (!("path" in options)) return data yield* Effect.promise(() => resolveLoadedPlugins(data, options.path)) @@ -754,13 +752,13 @@ export const layer = Layer.effect( let next: Info if (!file.endsWith(".jsonc")) { - const existing = ConfigParse.schema(Info.zod, ConfigParse.jsonc(before, file), file) + const existing = ConfigParse.effectSchema(Info, ConfigParse.jsonc(before, file), file) const merged = mergeDeep(writable(existing), writable(config)) yield* fs.writeFileString(file, JSON.stringify(merged, null, 2)).pipe(Effect.orDie) next = merged } else { const updated = patchJsonc(before, writable(config)) - next = ConfigParse.schema(Info.zod, ConfigParse.jsonc(updated, file), file) + next = ConfigParse.effectSchema(Info, ConfigParse.jsonc(updated, file), file) yield* fs.writeFileString(file, updated).pipe(Effect.orDie) } diff --git a/packages/opencode/src/config/parse.ts b/packages/opencode/src/config/parse.ts index 7472029ea..935104789 100644 --- a/packages/opencode/src/config/parse.ts +++ b/packages/opencode/src/config/parse.ts @@ -1,10 +1,12 @@ export * as ConfigParse from "./parse" import { type ParseError as JsoncParseError, parse as parseJsoncImpl, printParseErrorCode } from "jsonc-parser" +import { Cause, Exit, Schema as EffectSchema, SchemaIssue } from "effect" import z from "zod" +import type { DeepMutable } from "@/util/schema" import { InvalidError, JsonError } from "./error" -type Schema = z.ZodType +type ZodSchema = z.ZodType export function jsonc(text: string, filepath: string): unknown { const errors: JsoncParseError[] = [] @@ -33,7 +35,7 @@ export function jsonc(text: string, filepath: string): unknown { return data } -export function schema(schema: Schema, data: unknown, source: string): T { +export function schema(schema: ZodSchema, data: unknown, source: string): T { const parsed = schema.safeParse(data) if (parsed.success) return parsed.data @@ -42,3 +44,45 @@ export function schema(schema: Schema, data: unknown, source: string): T { issues: parsed.error.issues, }) } + +export function effectSchema>( + schema: S, + data: unknown, + source: string, +): DeepMutable { + const extra = topLevelExtraKeys(schema, data) + if (extra.length) { + throw new InvalidError({ + path: source, + issues: [ + { + code: "unrecognized_keys", + keys: extra, + path: [], + message: `Unrecognized key${extra.length === 1 ? "" : "s"}: ${extra.join(", ")}`, + } as z.core.$ZodIssue, + ], + }) + } + + const decoded = EffectSchema.decodeUnknownExit(schema)(data, { errors: "all", propertyOrder: "original" }) + if (Exit.isSuccess(decoded)) return decoded.value as DeepMutable + const error = Cause.squash(decoded.cause) + + throw new InvalidError( + { + path: source, + issues: EffectSchema.isSchemaError(error) + ? (SchemaIssue.makeFormatterStandardSchemaV1()(error.issue).issues as z.core.$ZodIssue[]) + : ([{ code: "custom", message: String(error), path: [] }] as z.core.$ZodIssue[]), + }, + { cause: error }, + ) +} + +function topLevelExtraKeys(schema: EffectSchema.Top, data: unknown) { + if (typeof data !== "object" || data === null || Array.isArray(data)) return [] + if (schema.ast._tag !== "Objects" || schema.ast.indexSignatures.length > 0) return [] + const known = new Set(schema.ast.propertySignatures.map((item) => String(item.name))) + return Object.keys(data).filter((key) => !known.has(key)) +} diff --git a/packages/opencode/src/config/permission.ts b/packages/opencode/src/config/permission.ts index a7390e953..29278338d 100644 --- a/packages/opencode/src/config/permission.ts +++ b/packages/opencode/src/config/permission.ts @@ -1,7 +1,6 @@ export * as ConfigPermission from "./permission" import { Schema, SchemaGetter } from "effect" -import z from "zod" -import { ZodOverride, zod } from "@/util/effect-zod" +import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" export const Action = Schema.Literals(["ask", "allow", "deny"]) @@ -20,8 +19,8 @@ export const Rule = Schema.Union([Action, Object]) export type Rule = Schema.Schema.Type // Known permission keys get explicit types in the Effect schema for generated -// docs/types. Runtime config parsing uses `InfoZod` below so user key order is -// preserved for permission precedence. +// docs/types. Runtime config parsing uses Effect's `propertyOrder: "original"` +// parse option so user key order is preserved for permission precedence. const InputObject = Schema.StructWithRest( Schema.Struct({ read: Schema.optional(Rule), @@ -53,35 +52,6 @@ const InputSchema = Schema.Union([Action, InputObject]) const normalizeInput = (input: Schema.Schema.Type): Schema.Schema.Type => typeof input === "string" ? { "*": input } : input -const InfoZod = z - .union([ - zod(Action), - z.intersection( - z.record(z.string(), zod(Rule)), - z - .object({ - read: zod(Rule).optional(), - edit: zod(Rule).optional(), - glob: zod(Rule).optional(), - grep: zod(Rule).optional(), - list: zod(Rule).optional(), - bash: zod(Rule).optional(), - task: zod(Rule).optional(), - external_directory: zod(Rule).optional(), - todowrite: zod(Action).optional(), - question: zod(Action).optional(), - webfetch: zod(Action).optional(), - websearch: zod(Action).optional(), - codesearch: zod(Action).optional(), - lsp: zod(Rule).optional(), - doom_loop: zod(Action).optional(), - skill: zod(Rule).optional(), - }) - .catchall(zod(Rule)), - ), - ]) - .transform(normalizeInput) - export const Info = InputSchema.pipe( Schema.decodeTo(InputObject, { decode: SchemaGetter.transform(normalizeInput), @@ -92,7 +62,6 @@ export const Info = InputSchema.pipe( }), ) .annotate({ identifier: "PermissionConfig" }) - .annotate({ [ZodOverride]: InfoZod }) .pipe( // Walker already emits the decodeTo transform into the derived zod (see // `encoded()` in effect-zod.ts), so just expose that directly. diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 56b8e7acd..3b75e1501 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -645,6 +645,33 @@ Test agent prompt`, }) }) +test("agent markdown permission config preserves user key order", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + const agentDir = path.join(dir, ".opencode", "agent") + await fs.mkdir(agentDir, { recursive: true }) + + await Filesystem.write( + path.join(agentDir, "ordered.md"), + `--- +permission: + bash: allow + "*": deny + edit: ask +--- +Ordered permissions`, + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await load() + expect(Object.keys(config.agent?.ordered?.permission ?? {})).toEqual(["bash", "*", "edit"]) + }, + }) +}) + test("loads agents from .opencode/agents (plural)", async () => { await using tmp = await tmpdir({ init: async (dir) => { @@ -1540,6 +1567,29 @@ test("permission config preserves user key order", async () => { }) }) +test("Effect config parser preserves permission order while rejecting unknown top-level keys", () => { + const config = ConfigParse.effectSchema( + Config.Info, + { + permission: { + bash: "allow", + "*": "deny", + edit: "ask", + }, + }, + "test", + ) + + expect(Object.keys(config.permission!)).toEqual(["bash", "*", "edit"]) + try { + ConfigParse.effectSchema(Config.Info, { invalid_field: true }, "test") + throw new Error("expected config parse to fail") + } catch (err) { + const error = err as { data?: { issues?: Array<{ code?: string; keys?: string[]; path?: string[] }> } } + expect(error.data?.issues?.[0]).toMatchObject({ code: "unrecognized_keys", keys: ["invalid_field"], path: [] }) + } +}) + // MCP config merging tests test("project config can override MCP server enabled status", async () => { @@ -2222,8 +2272,8 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => { // parseManagedPlist unit tests — pure function, no OS interaction test("parseManagedPlist strips MDM metadata keys", async () => { - const config = ConfigParse.schema( - Config.Info.zod, + const config = ConfigParse.effectSchema( + Config.Info, ConfigParse.jsonc( await ConfigManaged.parseManagedPlist( JSON.stringify({ @@ -2250,8 +2300,8 @@ test("parseManagedPlist strips MDM metadata keys", async () => { }) test("parseManagedPlist parses server settings", async () => { - const config = ConfigParse.schema( - Config.Info.zod, + const config = ConfigParse.effectSchema( + Config.Info, ConfigParse.jsonc( await ConfigManaged.parseManagedPlist( JSON.stringify({ @@ -2270,8 +2320,8 @@ test("parseManagedPlist parses server settings", async () => { }) test("parseManagedPlist parses permission rules", async () => { - const config = ConfigParse.schema( - Config.Info.zod, + const config = ConfigParse.effectSchema( + Config.Info, ConfigParse.jsonc( await ConfigManaged.parseManagedPlist( JSON.stringify({ @@ -2300,8 +2350,8 @@ test("parseManagedPlist parses permission rules", async () => { }) test("parseManagedPlist parses enabled_providers", async () => { - const config = ConfigParse.schema( - Config.Info.zod, + const config = ConfigParse.effectSchema( + Config.Info, ConfigParse.jsonc( await ConfigManaged.parseManagedPlist( JSON.stringify({ @@ -2317,8 +2367,8 @@ test("parseManagedPlist parses enabled_providers", async () => { }) test("parseManagedPlist handles empty config", async () => { - const config = ConfigParse.schema( - Config.Info.zod, + const config = ConfigParse.effectSchema( + Config.Info, ConfigParse.jsonc( await ConfigManaged.parseManagedPlist(JSON.stringify({ $schema: "https://opencode.ai/config.json" })), "test:mobileconfig", From 1a734adb4d1ce6071432bd68ac45fa4457f0dc2e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 25 Apr 2026 13:29:52 -0400 Subject: [PATCH 14/45] 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 +++ .../{opencode => core}/src/effect/logger.ts | 2 +- .../{opencode => core}/src/effect/memo-map.ts | 0 .../src/effect/observability.ts | 8 ++-- .../{opencode => core}/src/effect/runtime.ts | 6 ++- packages/{opencode => core}/src/flag/flag.ts | 0 packages/core/src/global.ts | 41 +++++++++++-------- .../src/installation/version.ts | 0 packages/{opencode => core}/src/util/log.ts | 4 +- .../src/util/opencode-process.ts | 0 .../test/effect/observability.test.ts | 2 +- 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 +- .../cmd/tui/component/dialog-session-list.tsx | 2 +- .../tui/component/dialog-workspace-create.tsx | 2 +- .../cli/cmd/tui/component/error-component.tsx | 2 +- .../src/cli/cmd/tui/config/tui-migrate.ts | 2 +- .../opencode/src/cli/cmd/tui/config/tui.ts | 6 +-- .../opencode/src/cli/cmd/tui/context/sdk.tsx | 2 +- packages/opencode/src/cli/cmd/tui/layer.ts | 2 +- .../opencode/src/cli/cmd/tui/plugin/api.tsx | 2 +- .../src/cli/cmd/tui/plugin/runtime.ts | 2 +- .../src/cli/cmd/tui/routes/session/index.tsx | 2 +- .../cli/cmd/tui/routes/session/sidebar.tsx | 2 +- packages/opencode/src/cli/cmd/tui/thread.ts | 2 +- .../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 +- .../opencode/src/control-plane/workspace.ts | 2 +- packages/opencode/src/effect/app-runtime.ts | 4 +- .../opencode/src/effect/bootstrap-runtime.ts | 4 +- packages/opencode/src/effect/index.ts | 4 +- .../opencode/src/effect/instance-state.ts | 2 +- packages/opencode/src/effect/run-service.ts | 4 +- packages/opencode/src/file/ripgrep.ts | 2 +- packages/opencode/src/file/watcher.ts | 2 +- packages/opencode/src/format/formatter.ts | 2 +- packages/opencode/src/index.ts | 4 +- packages/opencode/src/installation/index.ts | 4 +- 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 +- .../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 +- .../server/routes/instance/httpapi/auth.ts | 2 +- .../server/routes/instance/httpapi/server.ts | 2 +- .../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 +- .../test/installation/installation.test.ts | 2 +- .../test/plugin/workspace-adaptor.test.ts | 2 +- .../test/server/httpapi-bridge.test.ts | 2 +- .../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 +- 90 files changed, 140 insertions(+), 119 deletions(-) rename packages/{opencode => core}/src/effect/logger.ts (98%) rename packages/{opencode => core}/src/effect/memo-map.ts (100%) rename packages/{opencode => core}/src/effect/observability.ts (92%) rename packages/{opencode => core}/src/effect/runtime.ts (94%) rename packages/{opencode => core}/src/flag/flag.ts (100%) rename packages/{opencode => core}/src/installation/version.ts (100%) rename packages/{opencode => core}/src/util/log.ts (98%) rename packages/{opencode => core}/src/util/opencode-process.ts (100%) rename packages/{opencode => core}/test/effect/observability.test.ts (96%) 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/opencode/src/effect/logger.ts b/packages/core/src/effect/logger.ts similarity index 98% rename from packages/opencode/src/effect/logger.ts rename to packages/core/src/effect/logger.ts index 0e58b8acb..69f9631e0 100644 --- a/packages/opencode/src/effect/logger.ts +++ b/packages/core/src/effect/logger.ts @@ -1,5 +1,5 @@ import { Cause, Effect, Logger, References } from "effect" -import { Log } from "@/util" +import * as Log from "../util/log" type Fields = Record diff --git a/packages/opencode/src/effect/memo-map.ts b/packages/core/src/effect/memo-map.ts similarity index 100% rename from packages/opencode/src/effect/memo-map.ts rename to packages/core/src/effect/memo-map.ts diff --git a/packages/opencode/src/effect/observability.ts b/packages/core/src/effect/observability.ts similarity index 92% rename from packages/opencode/src/effect/observability.ts rename to packages/core/src/effect/observability.ts index fb81d5f5b..0203079ab 100644 --- a/packages/opencode/src/effect/observability.ts +++ b/packages/core/src/effect/observability.ts @@ -2,9 +2,9 @@ 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" +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 @@ -76,7 +76,7 @@ const traces = async () => { // 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 + // 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") diff --git a/packages/opencode/src/effect/runtime.ts b/packages/core/src/effect/runtime.ts similarity index 94% rename from packages/opencode/src/effect/runtime.ts rename to packages/core/src/effect/runtime.ts index ad7872f0b..e4f682709 100644 --- a/packages/opencode/src/effect/runtime.ts +++ b/packages/core/src/effect/runtime.ts @@ -1,11 +1,13 @@ -import { Observability } from "./observability" 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 })) + (rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer) as Layer.Layer, { + memoMap, + })) return { runSync: (fn: (svc: S) => Effect.Effect) => getRuntime().runSync(service.use(fn)), diff --git a/packages/opencode/src/flag/flag.ts b/packages/core/src/flag/flag.ts similarity index 100% rename from packages/opencode/src/flag/flag.ts rename to packages/core/src/flag/flag.ts 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/opencode/src/installation/version.ts b/packages/core/src/installation/version.ts similarity index 100% rename from packages/opencode/src/installation/version.ts rename to packages/core/src/installation/version.ts diff --git a/packages/opencode/src/util/log.ts b/packages/core/src/util/log.ts similarity index 98% rename from packages/opencode/src/util/log.ts rename to packages/core/src/util/log.ts index e335a8b43..a61c15f7a 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/core/src/util/log.ts @@ -1,9 +1,9 @@ import path from "path" import fs from "fs/promises" import { createWriteStream } from "fs" -import { Global } from "../global" +import * as Global from "../global" import z from "zod" -import { Glob } from "@opencode-ai/core/util/glob" +import { Glob } from "./glob" export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" }) export type Level = z.infer diff --git a/packages/opencode/src/util/opencode-process.ts b/packages/core/src/util/opencode-process.ts similarity index 100% rename from packages/opencode/src/util/opencode-process.ts rename to packages/core/src/util/opencode-process.ts diff --git a/packages/opencode/test/effect/observability.test.ts b/packages/core/test/effect/observability.test.ts similarity index 96% rename from packages/opencode/test/effect/observability.test.ts rename to packages/core/test/effect/observability.test.ts index d06220282..50ea23f89 100644 --- a/packages/opencode/test/effect/observability.test.ts +++ b/packages/core/test/effect/observability.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test" -import { resource } from "../../src/effect/observability" +import { resource } from "@opencode-ai/core/effect/observability" const otelResourceAttributes = process.env.OTEL_RESOURCE_ATTRIBUTES const opencodeClient = process.env.OPENCODE_CLIENT 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/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/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/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/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/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" From 27353df0cc08eab143e5024c6d8fe25577293884 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Apr 2026 17:31:57 +0000 Subject: [PATCH 15/45] chore: generate --- packages/opencode/src/cli/cmd/tui/thread.ts | 7 +- packages/sdk/js/src/v2/gen/types.gen.ts | 6 +- packages/sdk/openapi.json | 120 +++++++++----------- 3 files changed, 61 insertions(+), 72 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 60c5d5ece..7a1c9e721 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -15,7 +15,12 @@ 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 "@opencode-ai/core/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/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 0ad88bb50..40e661b46 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1206,9 +1206,7 @@ export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConf export type PermissionConfig = | PermissionActionConfig - | ({ - [key: string]: PermissionRuleConfig - } & { + | { read?: PermissionRuleConfig edit?: PermissionRuleConfig glob?: PermissionRuleConfig @@ -1226,7 +1224,7 @@ export type PermissionConfig = doom_loop?: PermissionActionConfig skill?: PermissionRuleConfig [key: string]: PermissionRuleConfig | PermissionActionConfig | undefined - }) + } export type AgentConfig = { model?: string diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 7be58195b..cb0949130 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -10951,73 +10951,60 @@ "$ref": "#/components/schemas/PermissionActionConfig" }, { - "allOf": [ - { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "$ref": "#/components/schemas/PermissionRuleConfig" - } + "type": "object", + "properties": { + "read": { + "$ref": "#/components/schemas/PermissionRuleConfig" }, - { - "type": "object", - "properties": { - "read": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "edit": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "glob": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "grep": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "list": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "bash": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "task": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "external_directory": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "todowrite": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "question": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "webfetch": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "websearch": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "codesearch": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "lsp": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "doom_loop": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "skill": { - "$ref": "#/components/schemas/PermissionRuleConfig" - } - }, - "additionalProperties": { - "$ref": "#/components/schemas/PermissionRuleConfig" - } + "edit": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "glob": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "grep": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "list": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "bash": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "task": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "external_directory": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "todowrite": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "question": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "webfetch": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "websearch": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "codesearch": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "lsp": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "doom_loop": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "skill": { + "$ref": "#/components/schemas/PermissionRuleConfig" } - ] + }, + "additionalProperties": { + "$ref": "#/components/schemas/PermissionRuleConfig" + } } ] }, @@ -11692,8 +11679,7 @@ "type": "boolean" } }, - "required": ["enabled"], - "additionalProperties": false + "required": ["enabled"] } ] } From fc8dae24229c13947a963d4bd69d1d47c26d89cc Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Apr 2026 17:50:26 +0000 Subject: [PATCH 16/45] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 6be836b12..037e9c57e 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-Dql+RLvSi8gtq7exwetJ4nSjKjJuyKuAf/X8X4L9E9E=", - "aarch64-linux": "sha256-qykwmIBF9tjZBjFo84e8tFix+i49TFM7Pf5jP4lLgEw=", - "aarch64-darwin": "sha256-TteIy5TuuhYsBIQ4q/weUkRnao0Y3iiKvf+noOYPuvg=", - "x86_64-darwin": "sha256-RbAqhuENpO5pT2oVUtVQ8JtUta/5hSnoqTzc3zPRCJc=" + "x86_64-linux": "sha256-0w22pAViYEcELJpOrIVCjTQ73fnsSaJxb75heAIUdYE=", + "aarch64-linux": "sha256-z1fpZ9HQU9n6W5xhKzuUduwQUJa/nrj9WFZdBLL/e/8=", + "aarch64-darwin": "sha256-y5AraTdY2uDTltjQFlHjMoMo6FICgQNKSunIOnQAXnY=", + "x86_64-darwin": "sha256-KQF6dJNQ587xp5h9ET+tLni9dLNwYnzxg2DX+KWfpoE=" } } From 716cf741906db40f07c1aa462001f650370f0093 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 25 Apr 2026 13:52:19 -0400 Subject: [PATCH 17/45] ci: adjust review flow (#24355) --- .github/workflows/review.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml index 58e73fac8..20f6a89f6 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/review.yml @@ -45,13 +45,13 @@ jobs: - name: Check PR guidelines compliance env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} OPENCODE_PERMISSION: '{ "bash": { "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }' PR_TITLE: ${{ steps.pr-details.outputs.title }} run: | PR_BODY=$(jq -r .body pr_data.json) - opencode run -m anthropic/claude-opus-4-5 "A new pull request has been created: '${PR_TITLE}' + opencode run -m opencode/openai-gpt-5.5 --variant medium "A new pull request has been created: '${PR_TITLE}' ${{ steps.pr-number.outputs.number }} From 705f792e87ac695b64879cda18f2f18d3ace68e3 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 25 Apr 2026 13:51:37 -0400 Subject: [PATCH 18/45] core: move Global module to @opencode-ai/core for centralized path management Move the Global module from packages/opencode/src/global to packages/core/src/global to provide a unified location for managing XDG directories and application paths. This eliminates duplicate path definitions across packages and ensures consistent access to data, config, cache, state, log, and bin directories throughout the codebase. --- packages/core/src/global.ts | 66 +++++++++++-------- packages/opencode/src/agent/agent.ts | 2 +- packages/opencode/src/auth/index.ts | 2 +- packages/opencode/src/cli/cmd/agent.ts | 2 +- packages/opencode/src/cli/cmd/debug/index.ts | 2 +- packages/opencode/src/cli/cmd/mcp.ts | 2 +- packages/opencode/src/cli/cmd/plug.ts | 2 +- packages/opencode/src/cli/cmd/providers.ts | 2 +- .../cli/cmd/tui/component/prompt/frecency.tsx | 2 +- .../cli/cmd/tui/component/prompt/history.tsx | 2 +- .../cli/cmd/tui/component/prompt/stash.tsx | 2 +- .../src/cli/cmd/tui/config/tui-migrate.ts | 2 +- .../opencode/src/cli/cmd/tui/config/tui.ts | 2 +- .../src/cli/cmd/tui/context/directory.ts | 2 +- .../opencode/src/cli/cmd/tui/context/kv.tsx | 2 +- .../src/cli/cmd/tui/context/local.tsx | 2 +- .../src/cli/cmd/tui/context/theme.tsx | 2 +- .../cmd/tui/feature-plugins/home/footer.tsx | 2 +- .../tui/feature-plugins/sidebar/footer.tsx | 2 +- .../src/cli/cmd/tui/plugin/runtime.ts | 2 +- .../src/cli/cmd/tui/routes/session/index.tsx | 2 +- .../cli/cmd/tui/routes/session/permission.tsx | 2 +- packages/opencode/src/cli/cmd/uninstall.ts | 2 +- packages/opencode/src/cli/heap.ts | 2 +- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/config/paths.ts | 2 +- packages/opencode/src/file/index.ts | 2 +- packages/opencode/src/file/ripgrep.ts | 2 +- packages/opencode/src/global/index.ts | 58 ---------------- packages/opencode/src/index.ts | 2 +- packages/opencode/src/lsp/server.ts | 2 +- packages/opencode/src/mcp/auth.ts | 2 +- packages/opencode/src/plugin/install.ts | 2 +- packages/opencode/src/plugin/meta.ts | 2 +- packages/opencode/src/provider/models.ts | 2 +- packages/opencode/src/provider/provider.ts | 2 +- .../routes/instance/httpapi/instance.ts | 2 +- .../src/server/routes/instance/index.ts | 2 +- packages/opencode/src/session/instruction.ts | 2 +- packages/opencode/src/session/session.ts | 2 +- packages/opencode/src/skill/discovery.ts | 2 +- packages/opencode/src/skill/index.ts | 2 +- packages/opencode/src/snapshot/index.ts | 2 +- packages/opencode/src/storage/db.ts | 2 +- .../opencode/src/storage/json-migration.ts | 2 +- packages/opencode/src/storage/storage.ts | 2 +- packages/opencode/src/tool/truncation-dir.ts | 2 +- packages/opencode/src/util/which.ts | 2 +- packages/opencode/src/worktree/index.ts | 2 +- .../test/cli/tui/plugin-loader.test.ts | 2 +- packages/opencode/test/config/config.test.ts | 2 +- packages/opencode/test/config/tui.test.ts | 2 +- .../test/provider/amazon-bedrock.test.ts | 2 +- .../opencode/test/provider/gitlab-duo.test.ts | 2 +- .../opencode/test/provider/provider.test.ts | 2 +- .../opencode/test/session/instruction.test.ts | 2 +- .../opencode/test/skill/discovery.test.ts | 2 +- packages/opencode/test/storage/db.test.ts | 2 +- .../test/storage/json-migration.test.ts | 2 +- .../opencode/test/storage/storage.test.ts | 2 +- packages/opencode/test/util/log.test.ts | 2 +- 61 files changed, 99 insertions(+), 143 deletions(-) delete mode 100644 packages/opencode/src/global/index.ts diff --git a/packages/core/src/global.ts b/packages/core/src/global.ts index bf605618f..0c83e3a1f 100644 --- a/packages/core/src/global.ts +++ b/packages/core/src/global.ts @@ -1,7 +1,9 @@ import path from "path" +import fs from "fs/promises" import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir" import os from "os" import { Context, Effect, Layer } from "effect" +import { Flock } from "./util/flock" const app = "opencode" const data = path.join(xdgData!, app) @@ -9,7 +11,7 @@ const cache = path.join(xdgCache!, app) const config = path.join(xdgConfig!, app) const state = path.join(xdgState!, app) -export const Path = { +const paths = { get home() { return process.env.OPENCODE_TEST_HOME ?? os.homedir() }, @@ -21,31 +23,43 @@ export const Path = { state, } -export namespace Global { - export class Service extends Context.Service()("@opencode/Global") {} +export const Path = paths - export interface Interface { - readonly home: string - readonly data: string - readonly cache: string - readonly config: string - readonly state: string - readonly bin: string - readonly log: string - } +Flock.setGlobal({ state }) - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - return Service.of({ - home: Path.home, - data: Path.data, - cache: Path.cache, - config: Path.config, - state: Path.state, - bin: Path.bin, - log: Path.log, - }) - }), - ) +await Promise.all([ + fs.mkdir(Path.data, { recursive: true }), + fs.mkdir(Path.config, { recursive: true }), + fs.mkdir(Path.state, { recursive: true }), + fs.mkdir(Path.log, { recursive: true }), + fs.mkdir(Path.bin, { recursive: true }), +]) + +export class Service extends Context.Service()("@opencode/Global") {} + +export interface Interface { + readonly home: string + readonly data: string + readonly cache: string + readonly config: string + readonly state: string + readonly bin: string + readonly log: string } + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + return Service.of({ + home: Path.home, + data: Path.data, + cache: Path.cache, + config: Path.config, + state: Path.state, + bin: Path.bin, + log: Path.log, + }) + }), +) + +export * as Global from "./global" diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 355718b6b..a37e0c194 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -15,7 +15,7 @@ import PROMPT_SUMMARY from "./prompt/summary.txt" import PROMPT_TITLE from "./prompt/title.txt" import { Permission } from "@/permission" import { mergeDeep, pipe, sortBy, values } from "remeda" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import path from "path" import { Plugin } from "@/plugin" import { Skill } from "../skill" diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index 00bc22329..539c40c1a 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -1,7 +1,7 @@ import path from "path" import { Effect, Layer, Record, Result, Schema, Context } from "effect" import { zod } from "@/util/effect-zod" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { AppFileSystem } from "@opencode-ai/core/filesystem" export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index fd559935f..acad38668 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/packages/opencode/src/cli/cmd/agent.ts @@ -2,7 +2,7 @@ import { cmd } from "./cmd" import * as prompts from "@clack/prompts" import { AppRuntime } from "@/effect/app-runtime" import { UI } from "../ui" -import { Global } from "../../global" +import { Global } from "@opencode-ai/core/global" import { Agent } from "../../agent/agent" import { Provider } from "../../provider" import path from "path" diff --git a/packages/opencode/src/cli/cmd/debug/index.ts b/packages/opencode/src/cli/cmd/debug/index.ts index e780c4ccb..194e66b1f 100644 --- a/packages/opencode/src/cli/cmd/debug/index.ts +++ b/packages/opencode/src/cli/cmd/debug/index.ts @@ -1,4 +1,4 @@ -import { Global } from "../../../global" +import { Global } from "@opencode-ai/core/global" import { bootstrap } from "../../bootstrap" import { cmd } from "../cmd" import { ConfigCommand } from "./config" diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index 3269b4a3d..ef22340fb 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -13,7 +13,7 @@ import { Instance } from "../../project/instance" import { Installation } from "../../installation" import { InstallationVersion } from "@opencode-ai/core/installation/version" import path from "path" -import { Global } from "../../global" +import { Global } from "@opencode-ai/core/global" import { modify, applyEdits } from "jsonc-parser" import { Filesystem } from "../../util" import { Bus } from "../../bus" diff --git a/packages/opencode/src/cli/cmd/plug.ts b/packages/opencode/src/cli/cmd/plug.ts index 9dfda16d6..14c846f2c 100644 --- a/packages/opencode/src/cli/cmd/plug.ts +++ b/packages/opencode/src/cli/cmd/plug.ts @@ -2,7 +2,7 @@ import { intro, log, outro, spinner } from "@clack/prompts" import type { Argv } from "yargs" import { ConfigPaths } from "../../config" -import { Global } from "../../global" +import { Global } from "@opencode-ai/core/global" import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plugin/install" import { resolvePluginTarget } from "../../plugin/shared" import { Instance } from "../../project/instance" diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index e2eb0b65a..158405e5f 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -8,7 +8,7 @@ import { map, pipe, sortBy, values } from "remeda" import path from "path" import os from "os" import { Config } from "../../config" -import { Global } from "../../global" +import { Global } from "@opencode-ai/core/global" import { Plugin } from "../../plugin" import { Instance } from "../../project/instance" import type { Hooks } from "@opencode-ai/plugin" diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx index 929f3a07d..61d4c9e99 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx @@ -1,5 +1,5 @@ import path from "path" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util" import { onMount } from "solid-js" import { createStore } from "solid-js/store" diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx index 03db74de9..2d979ce99 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx @@ -1,5 +1,5 @@ import path from "path" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util" import { onMount } from "solid-js" import { createStore, produce, unwrap } from "solid-js/store" diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx index 84ba62338..a7dd28965 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx @@ -1,5 +1,5 @@ import path from "path" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util" import { onMount } from "solid-js" import { createStore, produce, unwrap } from "solid-js/store" 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 d5599c170..b6a832dcb 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts @@ -4,7 +4,7 @@ import { unique } from "remeda" import z from "zod" import { TuiInfo, TuiOptions } from "./tui-schema" import { Flag } from "@opencode-ai/core/flag/flag" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/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 64ec5f1c5..b55e5807e 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -9,7 +9,7 @@ import { migrateTuiConfig } from "./tui-migrate" import { TuiInfo } from "./tui-schema" import { Flag } from "@opencode-ai/core/flag/flag" import { isRecord } from "@/util/record" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { CurrentWorkingDirectory } from "./cwd" import { ConfigPlugin } from "@/config/plugin" diff --git a/packages/opencode/src/cli/cmd/tui/context/directory.ts b/packages/opencode/src/cli/cmd/tui/context/directory.ts index 81f217398..0c4e5feb9 100644 --- a/packages/opencode/src/cli/cmd/tui/context/directory.ts +++ b/packages/opencode/src/cli/cmd/tui/context/directory.ts @@ -1,7 +1,7 @@ import { createMemo } from "solid-js" import { useProject } from "./project" import { useSync } from "./sync" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" export function useDirectory() { const project = useProject() diff --git a/packages/opencode/src/cli/cmd/tui/context/kv.tsx b/packages/opencode/src/cli/cmd/tui/context/kv.tsx index df8a8394c..2efa314d9 100644 --- a/packages/opencode/src/cli/cmd/tui/context/kv.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/kv.tsx @@ -1,4 +1,4 @@ -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util" import { Flock } from "@opencode-ai/core/util/flock" import { rename, rm } from "fs/promises" diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index 910483764..af06a2bf2 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -5,7 +5,7 @@ import { useSync } from "@tui/context/sync" import { useTheme } from "@tui/context/theme" import { uniqueBy } from "remeda" import path from "path" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { iife } from "@/util/iife" import { useToast } from "../ui/toast" import { useArgs } from "./args" diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 10f2dc49d..ca6c0a6cf 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -39,7 +39,7 @@ import carbonfox from "./theme/carbonfox.json" with { type: "json" } import { useKV } from "./kv" import { useRenderer } from "@opentui/solid" import { createStore, produce } from "solid-js/store" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util" import { useTuiConfig } from "./tui-config" import { isRecord } from "@/util/record" diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx index 8047c2645..7f2ef55e9 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx @@ -1,6 +1,6 @@ import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" import { createMemo, Match, Show, Switch } from "solid-js" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" const id = "internal:home-footer" diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx index b468d851b..bb51d4f42 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx @@ -1,6 +1,6 @@ import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" import { createMemo, Show } from "solid-js" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" const id = "internal:sidebar-footer" diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index 95d050d7f..556e97684 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -29,7 +29,7 @@ import { PluginLoader } from "@/plugin/loader" import { PluginMeta } from "@/plugin/meta" import { installPlugin as installModulePlugin, patchPluginConfig, readPluginManifest } from "@/plugin/install" import { hasTheme, upsertTheme } from "../context/theme" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util" import { Process } from "@/util" import { Flock } from "@opencode-ai/core/util/flock" 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 6ba43deb9..516f406ae 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -76,7 +76,7 @@ import stripAnsi from "strip-ansi" import { usePromptRef } from "../../context/prompt" import { useExit } from "../../context/exit" import { Filesystem } from "@/util" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { PermissionPrompt } from "./permission" import { QuestionPrompt } from "./question" import { DialogExportOptions } from "../../ui/dialog-export-options" diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index e48f348b9..d124734a3 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -14,7 +14,7 @@ import path from "path" import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import { Keybind } from "@/util" import { Locale } from "@/util" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { useDialog } from "../../ui/dialog" import { getScrollAcceleration } from "../../util/scroll" import { useTuiConfig } from "../../context/tui-config" diff --git a/packages/opencode/src/cli/cmd/uninstall.ts b/packages/opencode/src/cli/cmd/uninstall.ts index c0517d491..dc076913b 100644 --- a/packages/opencode/src/cli/cmd/uninstall.ts +++ b/packages/opencode/src/cli/cmd/uninstall.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 { Global } from "../../global" +import { Global } from "@opencode-ai/core/global" import fs from "fs/promises" import path from "path" import os from "os" diff --git a/packages/opencode/src/cli/heap.ts b/packages/opencode/src/cli/heap.ts index 0cb4299c5..45557391a 100644 --- a/packages/opencode/src/cli/heap.ts +++ b/packages/opencode/src/cli/heap.ts @@ -1,7 +1,7 @@ import path from "path" import { writeHeapSnapshot } from "node:v8" import { Flag } from "@opencode-ai/core/flag/flag" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Log } from "@/util" const log = Log.create({ service: "heap" }) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 3958e1436..ddd31a3fc 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -4,7 +4,7 @@ import { pathToFileURL } from "url" import os from "os" import z from "zod" import { mergeDeep, pipe } from "remeda" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import fsNode from "fs/promises" import { NamedError } from "@opencode-ai/core/util/error" import { Flag } from "@opencode-ai/core/flag/flag" diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts index df98bebb2..92c1f45e1 100644 --- a/packages/opencode/src/config/paths.ts +++ b/packages/opencode/src/config/paths.ts @@ -3,7 +3,7 @@ export * as ConfigPaths from "./paths" import path from "path" import { Filesystem } from "@/util" import { Flag } from "@opencode-ai/core/flag/flag" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { unique } from "remeda" import { JsonError } from "./error" import * as Effect from "effect/Effect" diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index 4710fd76d..1308d3f69 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -9,7 +9,7 @@ import { formatPatch, structuredPatch } from "diff" import fuzzysort from "fuzzysort" import ignore from "ignore" import path from "path" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Instance } from "../project/instance" import { Log } from "../util" import { Protected } from "./protected" diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 5602a4c41..ab725b731 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -7,7 +7,7 @@ import { ChildProcess } from "effect/unstable/process" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Log } from "@/util" import { sanitizedProcessEnv } from "@opencode-ai/core/util/opencode-process" import { which } from "@/util/which" diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts deleted file mode 100644 index 7f48a0f88..000000000 --- a/packages/opencode/src/global/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -import fs from "fs/promises" -import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir" -import path from "path" -import os from "os" -import { Filesystem } from "../util" -import { Flock } from "@opencode-ai/core/util/flock" - -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 = { - // Allow override via OPENCODE_TEST_HOME for test isolation - get home() { - return process.env.OPENCODE_TEST_HOME || os.homedir() - }, - data, - bin: path.join(cache, "bin"), - log: path.join(data, "log"), - cache, - config, - state, -} - -// Initialize Flock with global state path -Flock.setGlobal({ state }) - -await Promise.all([ - fs.mkdir(Path.data, { recursive: true }), - fs.mkdir(Path.config, { recursive: true }), - fs.mkdir(Path.state, { recursive: true }), - fs.mkdir(Path.log, { recursive: true }), - fs.mkdir(Path.bin, { recursive: true }), -]) - -const CACHE_VERSION = "21" - -const version = await Filesystem.readText(path.join(Path.cache, "version")).catch(() => "0") - -if (version !== CACHE_VERSION) { - try { - const contents = await fs.readdir(Path.cache) - await Promise.all( - contents.map((item) => - fs.rm(path.join(Path.cache, item), { - recursive: true, - force: true, - }), - ), - ) - } catch {} - await Filesystem.write(path.join(Path.cache, "version"), CACHE_VERSION) -} - -export * as Global from "." diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 3764e1b1c..3c475f133 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -31,7 +31,7 @@ import { PrCommand } from "./cli/cmd/pr" import { SessionCommand } from "./cli/cmd/session" import { DbCommand } from "./cli/cmd/db" import path from "path" -import { Global } from "./global" +import { Global } from "@opencode-ai/core/global" import { JsonMigration } from "./storage" import { Database } from "./storage" import { errorMessage } from "./util/error" diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 9b585c9fb..14b674a98 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -1,7 +1,7 @@ import type { ChildProcessWithoutNullStreams } from "child_process" import path from "path" import os from "os" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Log } from "../util" import { text } from "node:stream/consumers" import fs from "fs/promises" diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts index 0a57fa141..b07d59870 100644 --- a/packages/opencode/src/mcp/auth.ts +++ b/packages/opencode/src/mcp/auth.ts @@ -1,6 +1,6 @@ import path from "path" import z from "zod" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Effect, Layer, Context } from "effect" import { AppFileSystem } from "@opencode-ai/core/filesystem" diff --git a/packages/opencode/src/plugin/install.ts b/packages/opencode/src/plugin/install.ts index 87798f56d..a76048312 100644 --- a/packages/opencode/src/plugin/install.ts +++ b/packages/opencode/src/plugin/install.ts @@ -8,7 +8,7 @@ import { } from "jsonc-parser" import * as ConfigPaths from "@/config/paths" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util" import { Flock } from "@opencode-ai/core/util/flock" import { isRecord } from "@/util/record" diff --git a/packages/opencode/src/plugin/meta.ts b/packages/opencode/src/plugin/meta.ts index ab067c592..4bc8f5772 100644 --- a/packages/opencode/src/plugin/meta.ts +++ b/packages/opencode/src/plugin/meta.ts @@ -2,7 +2,7 @@ import path from "path" import { fileURLToPath } from "url" import { Flag } from "@opencode-ai/core/flag/flag" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util" import { Flock } from "@opencode-ai/core/util/flock" diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index c3df06abc..8d7d7b03b 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -1,4 +1,4 @@ -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Log } from "../util" import path from "path" import { Schema } from "effect" diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 96039af9b..c2439d04f 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -16,7 +16,7 @@ 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" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import path from "path" import { pathToFileURL } from "url" import { Effect, Layer, Context, Schema, Types } from "effect" diff --git a/packages/opencode/src/server/routes/instance/httpapi/instance.ts b/packages/opencode/src/server/routes/instance/httpapi/instance.ts index 97b53c1e9..d349ae9cd 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/instance.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/instance.ts @@ -1,4 +1,4 @@ -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Vcs } from "@/project" import * as InstanceState from "@/effect/instance-state" import { Effect, Layer, Schema } from "effect" diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index df50be406..d36964ad1 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -9,7 +9,7 @@ import { Instance } from "@/project/instance" import { Vcs } from "@/project" import { Agent } from "@/agent/agent" import { Skill } from "@/skill" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { LSP } from "@/lsp" import { Command } from "@/command" import { QuestionRoutes } from "./question" diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index 56fca5359..35de71819 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -7,7 +7,7 @@ import { InstanceState } from "@/effect" 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" +import { Global } from "@opencode-ai/core/global" import { Log } from "../util" import type { MessageV2 } from "./message-v2" import type { MessageID } from "./schema" diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index db77c0e21..6c67b8517 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -25,7 +25,7 @@ import { SessionID, MessageID, PartID } from "./schema" import type { Provider } from "@/provider" import { Permission } from "@/permission" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Effect, Layer, Option, Context, Schema, Types } from "effect" import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" diff --git a/packages/opencode/src/skill/discovery.ts b/packages/opencode/src/skill/discovery.ts index e620de983..9ce56d4ce 100644 --- a/packages/opencode/src/skill/discovery.ts +++ b/packages/opencode/src/skill/discovery.ts @@ -3,7 +3,7 @@ import { Effect, Layer, Path, Schema, Context } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { withTransientReadRetry } from "@/util/effect-http-client" import { AppFileSystem } from "@opencode-ai/core/filesystem" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Log } from "../util" const skillConcurrency = 4 diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index a425e13d5..60527cd0b 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -8,7 +8,7 @@ import type { Agent } from "@/agent/agent" import { Bus } from "@/bus" import { InstanceState } from "@/effect" import { Flag } from "@opencode-ai/core/flag/flag" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Permission } from "@/permission" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Config } from "../config" diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 50804ca2b..3701b8210 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -8,7 +8,7 @@ import { InstanceState } from "@/effect" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Hash } from "@opencode-ai/core/util/hash" import { Config } from "../config" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Log } from "../util" import { withStatics } from "@/util/schema" import { zod } from "@/util/effect-zod" diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 898810581..e442b2a76 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -4,7 +4,7 @@ import { type SQLiteTransaction } from "drizzle-orm/sqlite-core" export * from "drizzle-orm" import { LocalContext } from "../util" import { lazy } from "../util/lazy" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Log } from "../util" import { NamedError } from "@opencode-ai/core/util/error" import z from "zod" diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 20ca3ff53..787b50117 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -1,6 +1,6 @@ import type { SQLiteBunDatabase } from "drizzle-orm/bun-sqlite" import type { NodeSQLiteDatabase } from "drizzle-orm/node-sqlite" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Log } from "../util" import { ProjectTable } from "../project/project.sql" import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../session/session.sql" diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index 8f6332677..71cc37e01 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -1,6 +1,6 @@ import { Log } from "../util" import path from "path" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { NamedError } from "@opencode-ai/core/util/error" import z from "zod" import { AppFileSystem } from "@opencode-ai/core/filesystem" diff --git a/packages/opencode/src/tool/truncation-dir.ts b/packages/opencode/src/tool/truncation-dir.ts index d6d5d013d..9ed82e1f3 100644 --- a/packages/opencode/src/tool/truncation-dir.ts +++ b/packages/opencode/src/tool/truncation-dir.ts @@ -1,4 +1,4 @@ import path from "path" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" export const TRUNCATION_DIR = path.join(Global.Path.data, "tool-output") diff --git a/packages/opencode/src/util/which.ts b/packages/opencode/src/util/which.ts index 2e4073914..b9bea421c 100644 --- a/packages/opencode/src/util/which.ts +++ b/packages/opencode/src/util/which.ts @@ -1,6 +1,6 @@ import whichPkg from "which" import path from "path" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" export function which(cmd: string, env?: NodeJS.ProcessEnv) { const base = env?.PATH ?? env?.Path ?? process.env.PATH ?? process.env.Path ?? "" diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index 7539e8d58..b89ac32a9 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -1,6 +1,6 @@ import z from "zod" import { NamedError } from "@opencode-ai/core/util/error" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Instance } from "../project/instance" import { InstanceBootstrap } from "../project/bootstrap" import { Project } from "../project" diff --git a/packages/opencode/test/cli/tui/plugin-loader.test.ts b/packages/opencode/test/cli/tui/plugin-loader.test.ts index f5b04ff43..3dd8b6015 100644 --- a/packages/opencode/test/cli/tui/plugin-loader.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader.test.ts @@ -4,7 +4,7 @@ import path from "path" import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" -import { Global } from "../../../src/global" +import { Global } from "@opencode-ai/core/global" import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui" import { Filesystem } from "../../../src/util/" diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 3b75e1501..8512236a3 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -23,7 +23,7 @@ const infra = CrossSpawnSpawner.defaultLayer.pipe( import path from "path" import fs from "fs/promises" import { pathToFileURL } from "url" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { ProjectID } from "../../src/project/schema" import { Filesystem } from "../../src/util" import { ConfigPlugin } from "@/config/plugin" diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts index c7b6d4a50..0dbe49cef 100644 --- a/packages/opencode/test/config/tui.test.ts +++ b/packages/opencode/test/config/tui.test.ts @@ -5,7 +5,7 @@ import { tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" import { TuiConfig } from "../../src/cli/cmd/tui/config/tui" import { Config } from "../../src/config" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "../../src/util" import { AppRuntime } from "../../src/effect/app-runtime" import { Effect, Layer } from "effect" diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts index 03f83601d..aff67494b 100644 --- a/packages/opencode/test/provider/amazon-bedrock.test.ts +++ b/packages/opencode/test/provider/amazon-bedrock.test.ts @@ -7,7 +7,7 @@ import { tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" import { Provider } from "../../src/provider" import { Env } from "../../src/env" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "../../src/util" import { Effect } from "effect" import { AppRuntime } from "../../src/effect/app-runtime" diff --git a/packages/opencode/test/provider/gitlab-duo.test.ts b/packages/opencode/test/provider/gitlab-duo.test.ts index 907a32d61..a74ef360b 100644 --- a/packages/opencode/test/provider/gitlab-duo.test.ts +++ b/packages/opencode/test/provider/gitlab-duo.test.ts @@ -11,7 +11,7 @@ export {} // import { Instance } from "../../src/project/instance" // import { Provider } from "../../src/provider" // import { Env } from "../../src/env" -// import { Global } from "../../src/global" +// import { Global } from "@opencode-ai/core/global" // import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider" // test("GitLab Duo: loads provider with API key from environment", async () => { diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 899302082..612fe3e97 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -3,7 +3,7 @@ import { mkdir, unlink } from "fs/promises" import path from "path" import { tmpdir } from "../fixture/fixture" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { Instance } from "../../src/project/instance" import { Plugin } from "../../src/plugin/index" import { ModelsDev } from "../../src/provider" diff --git a/packages/opencode/test/session/instruction.test.ts b/packages/opencode/test/session/instruction.test.ts index c46bbd20b..60882d2b3 100644 --- a/packages/opencode/test/session/instruction.test.ts +++ b/packages/opencode/test/session/instruction.test.ts @@ -6,7 +6,7 @@ import { Instruction } from "../../src/session/instruction" import type { MessageV2 } from "../../src/session/message-v2" import { Instance } from "../../src/project/instance" import { MessageID, PartID, SessionID } from "../../src/session/schema" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { tmpdir } from "../fixture/fixture" const run = (effect: Effect.Effect) => diff --git a/packages/opencode/test/skill/discovery.test.ts b/packages/opencode/test/skill/discovery.test.ts index 3f8210329..230a9e03e 100644 --- a/packages/opencode/test/skill/discovery.test.ts +++ b/packages/opencode/test/skill/discovery.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect, beforeAll, afterAll } from "bun:test" import { Effect } from "effect" import { Discovery } from "../../src/skill/discovery" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "../../src/util" import { rm } from "fs/promises" import path from "path" diff --git a/packages/opencode/test/storage/db.test.ts b/packages/opencode/test/storage/db.test.ts index 2bfaae1da..2cd9f817c 100644 --- a/packages/opencode/test/storage/db.test.ts +++ b/packages/opencode/test/storage/db.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import path from "path" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { InstallationChannel } from "@opencode-ai/core/installation/version" import { Database } from "../../src/storage" diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index 019faf061..263573794 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -6,7 +6,7 @@ import path from "path" import fs from "fs/promises" import { readFileSync, readdirSync } from "fs" import { JsonMigration } from "../../src/storage" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { ProjectTable } from "../../src/project/project.sql" import { ProjectID } from "../../src/project/schema" import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql" diff --git a/packages/opencode/test/storage/storage.test.ts b/packages/opencode/test/storage/storage.test.ts index 0587b9dd6..6be653ecb 100644 --- a/packages/opencode/test/storage/storage.test.ts +++ b/packages/opencode/test/storage/storage.test.ts @@ -4,7 +4,7 @@ import { Effect, Exit, Layer } from "effect" import { AppFileSystem } from "@opencode-ai/core/filesystem" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Git } from "../../src/git" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { Storage } from "../../src/storage" import { tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/util/log.test.ts b/packages/opencode/test/util/log.test.ts index 336b16a17..9a3b61732 100644 --- a/packages/opencode/test/util/log.test.ts +++ b/packages/opencode/test/util/log.test.ts @@ -1,7 +1,7 @@ import { afterEach, expect, test } from "bun:test" import fs from "fs/promises" import path from "path" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" From eb0219988b3696edd67a1b1e076268c7d50f1b47 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Apr 2026 14:00:30 -0400 Subject: [PATCH 19/45] feat(httpapi): bridge catalog read endpoints (#24353) --- packages/opencode/specs/effect/http-api.md | 7 +- packages/opencode/src/agent/agent.ts | 59 ++++++----- packages/opencode/src/command/index.ts | 33 +++---- packages/opencode/src/format/index.ts | 23 +++-- packages/opencode/src/permission/index.ts | 9 +- .../routes/instance/httpapi/instance.ts | 99 ++++++++++++++++++- .../src/server/routes/instance/index.ts | 13 ++- packages/opencode/src/skill/index.ts | 19 ++-- .../test/server/httpapi-instance.test.ts | 27 +++++ 9 files changed, 208 insertions(+), 81 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 1b9da7a2c..20f740e3d 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -140,7 +140,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `file` | `bridged` partial | list/content/status only | | `mcp` | `bridged` partial | status only | | `workspace` | `bridged` | list, get, enter | -| top-level instance reads | `bridged` partial | path and vcs reads; command, agent, skill, lsp, formatter next | +| top-level instance reads | `bridged` | path, vcs, command, agent, skill, lsp, formatter | | experimental JSON routes | `next/later` | console, tool, worktree, resource, global session list | | `session` | `later/special` | large stateful surface plus streaming | | `sync` | `later` | process/control side effects | @@ -151,8 +151,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho ## Next PRs 1. Produce a generated route inventory from Hono registrations and update `Current Route Status` with exact paths. -2. Continue porting top-level JSON reads. -3. Start the Effect OpenAPI/SDK generation path for already-bridged routes. +2. Start the Effect OpenAPI/SDK generation path for already-bridged routes. ## Checklist @@ -165,7 +164,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho - [x] Add bridge-level auth and instance tests. - [ ] Complete exact Hono route inventory. - [x] Resolve implemented-but-unmounted route groups. -- [ ] Port remaining JSON routes. +- [x] Port remaining top-level JSON reads. - [ ] Generate SDK/OpenAPI from Effect routes. - [ ] Flip ported JSON routes to default-on with fallback. - [ ] Delete replaced Hono route implementations. diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index a37e0c194..231e17467 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -3,7 +3,6 @@ import z from "zod" import { Provider } from "../provider" import { ModelID, ProviderID } from "../provider/schema" import { generateObject, streamObject, type ModelMessage } from "ai" -import { Instance } from "../project/instance" import { Truncate } from "../tool" import { Auth } from "../auth" import { ProviderTransform } from "../provider" @@ -19,37 +18,37 @@ import { Global } from "@opencode-ai/core/global" import path from "path" import { Plugin } from "@/plugin" import { Skill } from "../skill" -import { Effect, Context, Layer } from "effect" +import { Effect, Context, Layer, Schema } from "effect" import { InstanceState } from "@/effect" import * as Option from "effect/Option" import * as OtelTracer from "@effect/opentelemetry/Tracer" +import { zod } from "@/util/effect-zod" +import { withStatics, type DeepMutable } from "@/util/schema" -export const Info = z - .object({ - name: z.string(), - description: z.string().optional(), - mode: z.enum(["subagent", "primary", "all"]), - native: z.boolean().optional(), - hidden: z.boolean().optional(), - topP: z.number().optional(), - temperature: z.number().optional(), - color: z.string().optional(), - permission: Permission.Ruleset.zod, - model: z - .object({ - modelID: ModelID.zod, - providerID: ProviderID.zod, - }) - .optional(), - variant: z.string().optional(), - prompt: z.string().optional(), - options: z.record(z.string(), z.any()), - steps: z.number().int().positive().optional(), - }) - .meta({ - ref: "Agent", - }) -export type Info = z.infer +export const Info = Schema.Struct({ + name: Schema.String, + description: Schema.optional(Schema.String), + mode: Schema.Literals(["subagent", "primary", "all"]), + native: Schema.optional(Schema.Boolean), + hidden: Schema.optional(Schema.Boolean), + topP: Schema.optional(Schema.Number), + temperature: Schema.optional(Schema.Number), + color: Schema.optional(Schema.String), + permission: Permission.Ruleset, + model: Schema.optional( + Schema.Struct({ + modelID: ModelID, + providerID: ProviderID, + }), + ), + variant: Schema.optional(Schema.String), + prompt: Schema.optional(Schema.String), + options: Schema.Record(Schema.String, Schema.Unknown), + steps: Schema.optional(Schema.Number), +}) + .annotate({ identifier: "Agent" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Info = DeepMutable> export interface Interface { readonly get: (agent: string) => Effect.Effect @@ -79,7 +78,7 @@ export const layer = Layer.effect( const provider = yield* Provider.Service const state = yield* InstanceState.make( - Effect.fn("Agent.state")(function* (_ctx) { + Effect.fn("Agent.state")(function* (ctx) { const cfg = yield* config.get() const skillDirs = yield* skill.dirs() const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))] @@ -136,7 +135,7 @@ export const layer = Layer.effect( edit: { "*": "deny", [path.join(".opencode", "plans", "*.md")]: "allow", - [path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow", + [path.relative(ctx.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow", }, }), user, diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 478a12f66..7001d4f96 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -5,6 +5,8 @@ import type { InstanceContext } from "@/project/instance" import { SessionID, MessageID } from "@/session/schema" import { Effect, Layer, Context, Schema } from "effect" import z from "zod" +import { zod, ZodOverride } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" import { Config } from "../config" import { MCP } from "../mcp" import { Skill } from "../skill" @@ -27,25 +29,22 @@ export const Event = { ), } -export const Info = z - .object({ - name: z.string(), - description: z.string().optional(), - agent: z.string().optional(), - model: z.string().optional(), - source: z.enum(["command", "mcp", "skill"]).optional(), - // workaround for zod not supporting async functions natively so we use getters - // https://zod.dev/v4/changelog?id=zfunction - template: z.promise(z.string()).or(z.string()), - subtask: z.boolean().optional(), - hints: z.array(z.string()), - }) - .meta({ - ref: "Command", - }) +export const Info = Schema.Struct({ + name: Schema.String, + description: Schema.optional(Schema.String), + agent: Schema.optional(Schema.String), + model: Schema.optional(Schema.String), + source: Schema.optional(Schema.Literals(["command", "mcp", "skill"])), + // Some command templates are lazy promises from MCP prompt resolution. + template: Schema.Unknown.annotate({ [ZodOverride]: z.promise(z.string()).or(z.string()) }), + subtask: Schema.optional(Schema.Boolean), + hints: Schema.Array(Schema.String), +}) + .annotate({ identifier: "Command" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) // for some reason zod is inferring `string` for z.promise(z.string()).or(z.string()) so we have to manually override it -export type Info = Omit, "template"> & { template: Promise | string } +export type Info = Omit, "template"> & { template: Promise | string } export function hints(template: string) { const result: string[] = [] diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index 53a2c1011..4284a2cf6 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -1,26 +1,25 @@ -import { Effect, Layer, Context } from "effect" +import { Effect, Layer, Context, Schema } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import { InstanceState } from "@/effect" import path from "path" import { mergeDeep } from "remeda" -import z from "zod" import { Config } from "../config" import { Log } from "../util" import * as Formatter from "./formatter" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" const log = Log.create({ service: "format" }) -export const Status = z - .object({ - name: z.string(), - extensions: z.string().array(), - enabled: z.boolean(), - }) - .meta({ - ref: "FormatterStatus", - }) -export type Status = z.infer +export const Status = Schema.Struct({ + name: Schema.String, + extensions: Schema.Array(Schema.String), + enabled: Schema.Boolean, +}) + .annotate({ identifier: "FormatterStatus" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Status = Schema.Schema.Type export interface Interface { readonly init: () => Effect.Effect diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 428514ecd..2dfa8e940 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -22,13 +22,14 @@ export const Action = Schema.Literals(["allow", "deny", "ask"]) .pipe(withStatics((s) => ({ zod: zod(s) }))) export type Action = Schema.Schema.Type -export class Rule extends Schema.Class("PermissionRule")({ +export const Rule = Schema.Struct({ permission: Schema.String, pattern: Schema.String, action: Action, -}) { - static readonly zod = zod(this) -} +}) + .annotate({ identifier: "PermissionRule" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Rule = Schema.Schema.Type export const Ruleset = Schema.mutable(Schema.Array(Rule)) .annotate({ identifier: "PermissionRuleset" }) diff --git a/packages/opencode/src/server/routes/instance/httpapi/instance.ts b/packages/opencode/src/server/routes/instance/httpapi/instance.ts index d349ae9cd..016703e77 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/instance.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/instance.ts @@ -1,5 +1,10 @@ +import { Agent } from "@/agent/agent" +import { Command } from "@/command" +import { Format } from "@/format" import { Global } from "@opencode-ai/core/global" +import { LSP } from "@/lsp" import { Vcs } from "@/project" +import { Skill } from "@/skill" import * as InstanceState from "@/effect/instance-state" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" @@ -21,6 +26,11 @@ export const InstancePaths = { path: "/path", vcs: "/vcs", vcsDiff: "/vcs/diff", + command: "/command", + agent: "/agent", + skill: "/skill", + lsp: "/lsp", + formatter: "/formatter", } as const export const InstanceApi = HttpApi.make("instance") @@ -57,6 +67,51 @@ export const InstanceApi = HttpApi.make("instance") description: "Retrieve the current git diff for the working tree or against the default branch.", }), ), + HttpApiEndpoint.get("command", InstancePaths.command, { + success: Schema.Array(Command.Info), + }).annotateMerge( + OpenApi.annotations({ + identifier: "command.list", + summary: "List commands", + description: "Get a list of all available commands in the OpenCode system.", + }), + ), + HttpApiEndpoint.get("agent", InstancePaths.agent, { + success: Schema.Array(Agent.Info), + }).annotateMerge( + OpenApi.annotations({ + identifier: "app.agents", + summary: "List agents", + description: "Get a list of all available AI agents in the OpenCode system.", + }), + ), + HttpApiEndpoint.get("skill", InstancePaths.skill, { + success: Schema.Array(Skill.Info), + }).annotateMerge( + OpenApi.annotations({ + identifier: "app.skills", + summary: "List skills", + description: "Get a list of all available skills in the OpenCode system.", + }), + ), + HttpApiEndpoint.get("lsp", InstancePaths.lsp, { + success: Schema.Array(LSP.Status), + }).annotateMerge( + OpenApi.annotations({ + identifier: "lsp.status", + summary: "Get LSP status", + description: "Get LSP server status", + }), + ), + HttpApiEndpoint.get("formatter", InstancePaths.formatter, { + success: Schema.Array(Format.Status), + }).annotateMerge( + OpenApi.annotations({ + identifier: "formatter.status", + summary: "Get formatter status", + description: "Get formatter status", + }), + ), ) .annotateMerge( OpenApi.annotations({ @@ -76,6 +131,11 @@ export const InstanceApi = HttpApi.make("instance") export const instanceHandlers = Layer.unwrap( Effect.gen(function* () { + const agent = yield* Agent.Service + const command = yield* Command.Service + const format = yield* Format.Service + const lsp = yield* LSP.Service + const skill = yield* Skill.Service const vcs = yield* Vcs.Service const getPath = Effect.fn("InstanceHttpApi.path")(function* () { @@ -98,8 +158,43 @@ export const instanceHandlers = Layer.unwrap( return yield* vcs.diff(ctx.query.mode) }) + const getCommand = Effect.fn("InstanceHttpApi.command")(function* () { + return yield* command.list() + }) + + const getAgent = Effect.fn("InstanceHttpApi.agent")(function* () { + return yield* agent.list() + }) + + const getSkill = Effect.fn("InstanceHttpApi.skill")(function* () { + return yield* skill.all() + }) + + const getLsp = Effect.fn("InstanceHttpApi.lsp")(function* () { + return yield* lsp.status() + }) + + const getFormatter = Effect.fn("InstanceHttpApi.formatter")(function* () { + return yield* format.status() + }) + return HttpApiBuilder.group(InstanceApi, "instance", (handlers) => - handlers.handle("path", getPath).handle("vcs", getVcs).handle("vcsDiff", getVcsDiff), + handlers + .handle("path", getPath) + .handle("vcs", getVcs) + .handle("vcsDiff", getVcsDiff) + .handle("command", getCommand) + .handle("agent", getAgent) + .handle("skill", getSkill) + .handle("lsp", getLsp) + .handle("formatter", getFormatter), ) }), -).pipe(Layer.provide(Vcs.defaultLayer)) +).pipe( + Layer.provide(Agent.defaultLayer), + Layer.provide(Command.defaultLayer), + Layer.provide(Format.defaultLayer), + Layer.provide(LSP.defaultLayer), + Layer.provide(Skill.defaultLayer), + Layer.provide(Vcs.defaultLayer), +) diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index d36964ad1..fec0bb1ed 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -57,6 +57,11 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.get(InstancePaths.path, (c) => handler(c.req.raw, context)) app.get(InstancePaths.vcs, (c) => handler(c.req.raw, context)) app.get(InstancePaths.vcsDiff, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.command, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.agent, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.skill, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.lsp, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.formatter, (c) => handler(c.req.raw, context)) app.get(McpPaths.status, (c) => handler(c.req.raw, context)) } @@ -201,7 +206,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { description: "List of commands", content: { "application/json": { - schema: resolver(Command.Info.array()), + schema: resolver(Command.Info.zod.array()), }, }, }, @@ -224,7 +229,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { description: "List of agents", content: { "application/json": { - schema: resolver(Agent.Info.array()), + schema: resolver(Agent.Info.zod.array()), }, }, }, @@ -247,7 +252,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { description: "List of skills", content: { "application/json": { - schema: resolver(Skill.Info.array()), + schema: resolver(Skill.Info.zod.array()), }, }, }, @@ -293,7 +298,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { description: "Formatter status", content: { "application/json": { - schema: resolver(Format.Status.array()), + schema: resolver(Format.Status.zod.array()), }, }, }, diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index 60527cd0b..acbb8d3fa 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -2,7 +2,9 @@ import os from "os" import path from "path" import { pathToFileURL } from "url" import z from "zod" -import { Effect, Layer, Context } from "effect" +import { Effect, Layer, Context, Schema } from "effect" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" import { NamedError } from "@opencode-ai/core/util/error" import type { Agent } from "@/agent/agent" import { Bus } from "@/bus" @@ -23,13 +25,14 @@ const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md" const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md" const SKILL_PATTERN = "**/SKILL.md" -export const Info = z.object({ - name: z.string(), - description: z.string(), - location: z.string(), - content: z.string(), +export const Info = Schema.Struct({ + name: Schema.String, + description: Schema.String, + location: Schema.String, + content: Schema.String, }) -export type Info = z.infer + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Info = Schema.Schema.Type export const InvalidError = NamedError.create( "SkillInvalidError", @@ -91,7 +94,7 @@ const add = Effect.fnUntraced(function* (state: State, match: string, bus: Bus.I if (!md) return - const parsed = Info.pick({ name: true, description: true }).safeParse(md.data) + const parsed = z.object({ name: z.string(), description: z.string() }).safeParse(md.data) if (!parsed.success) return if (state.skills[parsed.data.name]) { diff --git a/packages/opencode/test/server/httpapi-instance.test.ts b/packages/opencode/test/server/httpapi-instance.test.ts index 6bd30d2ca..a066e0e92 100644 --- a/packages/opencode/test/server/httpapi-instance.test.ts +++ b/packages/opencode/test/server/httpapi-instance.test.ts @@ -50,4 +50,31 @@ describe("instance HttpApi", () => { expect.objectContaining({ file: "changed.txt", additions: 1, status: "added" }), ) }) + + test("serves catalog read endpoints through Hono bridge", async () => { + await using tmp = await tmpdir({ config: { formatter: false, lsp: false } }) + + const [commands, agents, skills, lsp, formatter] = await Promise.all([ + app().request(InstancePaths.command, { headers: { "x-opencode-directory": tmp.path } }), + app().request(InstancePaths.agent, { headers: { "x-opencode-directory": tmp.path } }), + app().request(InstancePaths.skill, { headers: { "x-opencode-directory": tmp.path } }), + app().request(InstancePaths.lsp, { headers: { "x-opencode-directory": tmp.path } }), + app().request(InstancePaths.formatter, { headers: { "x-opencode-directory": tmp.path } }), + ]) + + expect(commands.status).toBe(200) + expect(await commands.json()).toContainEqual(expect.objectContaining({ name: "init", source: "command" })) + + expect(agents.status).toBe(200) + expect(await agents.json()).toContainEqual(expect.objectContaining({ name: "build", mode: "primary" })) + + expect(skills.status).toBe(200) + expect(await skills.json()).toBeArray() + + expect(lsp.status).toBe(200) + expect(await lsp.json()).toEqual([]) + + expect(formatter.status).toBe(200) + expect(await formatter.json()).toEqual([]) + }) }) From 3bc0c36acec47aa7ff1278bb222e1224e56641e8 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Apr 2026 18:01:35 +0000 Subject: [PATCH 20/45] chore: generate --- packages/opencode/specs/effect/http-api.md | 34 +++++++++++----------- packages/opencode/src/skill/index.ts | 3 +- packages/sdk/openapi.json | 4 +-- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 20f740e3d..33568e65e 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -130,23 +130,23 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho ## Current Route Status -| Area | Status | Notes | -| ------------------------ | ----------------- | -------------------------------------------------------------- | -| `question` | `bridged` | `GET /question`, reply, reject | -| `permission` | `bridged` | list and reply | -| `provider` | `bridged` | list, auth, OAuth authorize/callback | -| `config` | `bridged` partial | reads only; mutation remains Hono | -| `project` | `bridged` partial | reads only; git-init remains Hono | -| `file` | `bridged` partial | list/content/status only | -| `mcp` | `bridged` partial | status only | -| `workspace` | `bridged` | list, get, enter | -| top-level instance reads | `bridged` | path, vcs, command, agent, skill, lsp, formatter | -| experimental JSON routes | `next/later` | console, tool, worktree, resource, global session list | -| `session` | `later/special` | large stateful surface plus streaming | -| `sync` | `later` | process/control side effects | -| `event` | `special` | SSE | -| `pty` | `special` | websocket | -| `tui` | `special` | UI bridge | +| Area | Status | Notes | +| ------------------------ | ----------------- | ------------------------------------------------------ | +| `question` | `bridged` | `GET /question`, reply, reject | +| `permission` | `bridged` | list and reply | +| `provider` | `bridged` | list, auth, OAuth authorize/callback | +| `config` | `bridged` partial | reads only; mutation remains Hono | +| `project` | `bridged` partial | reads only; git-init remains Hono | +| `file` | `bridged` partial | list/content/status only | +| `mcp` | `bridged` partial | status only | +| `workspace` | `bridged` | list, get, enter | +| top-level instance reads | `bridged` | path, vcs, command, agent, skill, lsp, formatter | +| experimental JSON routes | `next/later` | console, tool, worktree, resource, global session list | +| `session` | `later/special` | large stateful surface plus streaming | +| `sync` | `later` | process/control side effects | +| `event` | `special` | SSE | +| `pty` | `special` | websocket | +| `tui` | `special` | UI bridge | ## Next PRs diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index acbb8d3fa..ebb433971 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -30,8 +30,7 @@ export const Info = Schema.Struct({ description: Schema.String, location: Schema.String, content: Schema.String, -}) - .pipe(withStatics((s) => ({ zod: zod(s) }))) +}).pipe(withStatics((s) => ({ zod: zod(s) }))) export type Info = Schema.Schema.Type export const InvalidError = NamedError.create( diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index cb0949130..9fb2a3e6d 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -13344,9 +13344,7 @@ "additionalProperties": {} }, "steps": { - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991 + "type": "number" } }, "required": ["name", "mode", "permission", "options"] From 625aca49de2325c29189c6ac6ec5a49efc7e9450 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Apr 2026 14:10:58 -0400 Subject: [PATCH 21/45] feat(tui): read Zed editor context from state db (#24352) --- .../src/cli/cmd/tui/context/editor-zed.ts | 172 ++++++++++++++++++ .../src/cli/cmd/tui/context/editor.ts | 46 ++++- .../test/cli/tui/editor-context.test.ts | 9 + 3 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/context/editor-zed.ts create mode 100644 packages/opencode/test/cli/tui/editor-context.test.ts diff --git a/packages/opencode/src/cli/cmd/tui/context/editor-zed.ts b/packages/opencode/src/cli/cmd/tui/context/editor-zed.ts new file mode 100644 index 000000000..cbf995f8d --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/context/editor-zed.ts @@ -0,0 +1,172 @@ +import { Database } from "bun:sqlite" +import os from "node:os" +import path from "node:path" +import z from "zod" +import { Filesystem } from "@/util" +import type { EditorSelection } from "./editor" + +const ZedEditorRowSchema = z.object({ + editor_id: z.number(), + workspace_id: z.number(), + workspace_paths: z.string().nullable(), + timestamp: z.string(), + buffer_path: z.string().nullable(), + selection_start: z.number().nullable(), + selection_end: z.number().nullable(), +}) + +const ZedEditorContentsSchema = z.object({ + contents: z.string().nullable(), +}) + +type ZedEditorRow = z.infer + +export async function resolveZedSelection(dbPath: string): Promise { + const row = queryZedActiveEditor(dbPath, process.cwd()) + if (!row?.buffer_path || row.selection_start == null || row.selection_end == null) return + + const text = + queryZedEditorContents(dbPath, row) ?? + (await Bun.file(row.buffer_path) + .text() + .catch(() => undefined)) + if (text == null) return + + const startOffset = Math.min(row.selection_start, row.selection_end) + const endOffset = Math.max(row.selection_start, row.selection_end) + + return { + text: text.slice(startOffset, endOffset), + filePath: row.buffer_path, + selection: offsetsToSelection(text, startOffset, endOffset), + } +} + +function queryZedActiveEditor(dbPath: string, cwd: string) { + let db: Database | undefined + try { + db = new Database(dbPath, { readonly: true }) + return db + .query( + `select + e.item_id as editor_id, + e.workspace_id as workspace_id, + w.paths as workspace_paths, + w.timestamp as timestamp, + e.buffer_path as buffer_path, + s.start as selection_start, + s.end as selection_end + from items i + join panes p on p.pane_id = i.pane_id and p.workspace_id = i.workspace_id + join workspaces w on w.workspace_id = i.workspace_id + join editors e on e.item_id = i.item_id and e.workspace_id = i.workspace_id + left join editor_selections s on s.editor_id = e.item_id and s.workspace_id = e.workspace_id + where i.active = 1 and p.active = 1 and i.kind = 'Editor' and e.buffer_path is not null + order by w.timestamp desc`, + ) + .all() + .flatMap((row) => { + const parsed = ZedEditorRowSchema.safeParse(row) + return parsed.success ? [parsed.data] : [] + }) + .map((row) => ({ row, score: scoreZedWorkspace(row.workspace_paths, cwd) })) + .filter((entry) => entry.score > 0) + .sort((left, right) => right.score - left.score || right.row.timestamp.localeCompare(left.row.timestamp))[0]?.row + } catch { + return + } finally { + db?.close() + } +} + +function queryZedEditorContents(dbPath: string, row: ZedEditorRow) { + let db: Database | undefined + try { + db = new Database(dbPath, { readonly: true }) + return ZedEditorContentsSchema.safeParse( + db + .query( + `select contents + from editors + where item_id = $editorID and workspace_id = $workspaceID`, + ) + .get({ $editorID: row.editor_id, $workspaceID: row.workspace_id }), + ).data?.contents + } catch { + return + } finally { + db?.close() + } +} + +export function resolveZedDbPath() { + const candidates = [ + process.env.OPENCODE_ZED_DB, + path.join(os.homedir(), "Library", "Application Support", "Zed", "db", "0-stable", "db.sqlite"), + path.join(os.homedir(), ".local", "share", "zed", "db", "0-stable", "db.sqlite"), + ].filter((item): item is string => Boolean(item)) + + return candidates.find((item) => Filesystem.stat(item)?.isFile()) +} + +function scoreZedWorkspace(workspacePaths: string | null, cwd: string) { + return zedWorkspacePaths(workspacePaths).reduce((score, item) => { + if (pathContains(item, cwd)) return Math.max(score, 2) + if (pathContains(cwd, item)) return Math.max(score, 1) + return score + }, 0) +} + +function zedWorkspacePaths(value: string | null) { + if (!value) return [] + const parsed = parseJson(value) + if (Array.isArray(parsed)) return parsed.filter((item): item is string => typeof item === "string") + return value.split(/\r?\n/).filter(Boolean) +} + +export function offsetToPosition(text: string, offset: number) { + return offsetsToSelection(text, offset, offset).start +} + +function offsetsToSelection(text: string, startOffset: number, endOffset: number) { + const start = Math.max(0, Math.min(startOffset, text.length)) + const end = Math.max(0, Math.min(endOffset, text.length)) + let line = 1 + let lineStart = 0 + let startPosition = position(line, lineStart, start) + let endPosition = position(line, lineStart, end) + + for (let index = 0; index <= end; index++) { + if (index === start) startPosition = position(line, lineStart, index) + if (index === end) { + endPosition = position(line, lineStart, index) + break + } + if (text[index] === "\n") { + line += 1 + lineStart = index + 1 + } + } + + return { start: startPosition, end: endPosition } +} + +function position(line: number, lineStart: number, offset: number) { + return { + line, + character: offset - lineStart + 1, + } +} + +function pathContains(parent: string, child: string) { + const relative = path.relative(path.resolve(parent), path.resolve(child)) + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) +} + +function parseJson(value: string) { + try { + return JSON.parse(value) as unknown + } catch { + return + } +} diff --git a/packages/opencode/src/cli/cmd/tui/context/editor.ts b/packages/opencode/src/cli/cmd/tui/context/editor.ts index 4e6c97f6e..75c5440f5 100644 --- a/packages/opencode/src/cli/cmd/tui/context/editor.ts +++ b/packages/opencode/src/cli/cmd/tui/context/editor.ts @@ -4,7 +4,9 @@ import path from "node:path" import { onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" import z from "zod" +import { isRecord } from "@/util/record" import { createSimpleContext } from "./helper" +import { resolveZedDbPath, resolveZedSelection } from "./editor-zed" const MCP_PROTOCOL_VERSION = "2025-11-25" @@ -90,6 +92,8 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create let reconnect: ReturnType | undefined let attempt = 0 let requestID = 0 + let zedSelection: Promise | undefined + let lastZedSelectionKey: string | undefined const pending = new Map() const send = (payload: JsonRpcMessage) => { @@ -114,7 +118,29 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create const connection = resolveEditorConnection() if (!connection) { - setStore("status", "disabled") + const dbPath = resolveZedDbPath() + if (!dbPath) { + setStore("status", "disabled") + scheduleReconnect(1000) + return + } + zedSelection ??= resolveZedSelection(dbPath) + .then((selection) => { + if (closed || socket) return + const key = editorSelectionKey(selection) + if (key !== lastZedSelectionKey) { + lastZedSelectionKey = key + setStore("selection", selection) + setStore("status", selection ? "connected" : "disabled") + } + }) + .catch(() => { + if (closed || socket) return + setStore("status", "disabled") + }) + .finally(() => { + zedSelection = undefined + }) scheduleReconnect(1000) return } @@ -196,7 +222,7 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create return { enabled() { - return Boolean(resolveEditorConnection()) + return Boolean(resolveEditorConnection() || resolveZedDbPath()) }, connected() { return store.status === "connected" @@ -289,6 +315,18 @@ function scoreEditorLock(lock: EditorLockFile, cwd: string) { return workspaceMatch * 1_000_000_000_000 + lock.mtimeMs } +function editorSelectionKey(selection: EditorSelection | undefined) { + if (!selection) return "" + return [ + selection.filePath, + selection.selection.start.line, + selection.selection.start.character, + selection.selection.end.line, + selection.selection.end.character, + selection.text, + ].join("\0") +} + function pathContains(parent: string, child: string) { const relative = path.relative(path.resolve(parent), path.resolve(child)) return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) @@ -313,7 +351,3 @@ function parseMessage(value: unknown) { return } } - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value) -} diff --git a/packages/opencode/test/cli/tui/editor-context.test.ts b/packages/opencode/test/cli/tui/editor-context.test.ts new file mode 100644 index 000000000..c605029ca --- /dev/null +++ b/packages/opencode/test/cli/tui/editor-context.test.ts @@ -0,0 +1,9 @@ +import { expect, test } from "bun:test" +import { offsetToPosition } from "../../../src/cli/cmd/tui/context/editor-zed" + +test("offsetToPosition converts Zed offsets to 1-based editor positions", () => { + expect(offsetToPosition("one\ntwo\nthree", 0)).toEqual({ line: 1, character: 1 }) + expect(offsetToPosition("one\ntwo\nthree", 4)).toEqual({ line: 2, character: 1 }) + expect(offsetToPosition("one\ntwo\nthree", 6)).toEqual({ line: 2, character: 3 }) + expect(offsetToPosition("one\ntwo\nthree", 100)).toEqual({ line: 3, character: 6 }) +}) From 05661c60ffb1ce1bde844f3a6aa5b6cb5bc22412 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Apr 2026 14:12:54 -0400 Subject: [PATCH 22/45] feat(httpapi): bridge file search endpoints (#24356) --- packages/opencode/specs/effect/http-api.md | 2 +- .../server/routes/instance/httpapi/file.ts | 89 ++++++++++++++++++- .../src/server/routes/instance/index.ts | 3 + .../opencode/test/server/httpapi-file.test.ts | 20 +++++ 4 files changed, 111 insertions(+), 3 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 33568e65e..948389223 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -137,7 +137,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `provider` | `bridged` | list, auth, OAuth authorize/callback | | `config` | `bridged` partial | reads only; mutation remains Hono | | `project` | `bridged` partial | reads only; git-init remains Hono | -| `file` | `bridged` partial | list/content/status only | +| `file` | `bridged` partial | find text/file/symbol, list/content/status | | `mcp` | `bridged` partial | status only | | `workspace` | `bridged` | list, get, enter | | top-level instance reads | `bridged` | path, vcs, command, agent, skill, lsp, formatter | diff --git a/packages/opencode/src/server/routes/instance/httpapi/file.ts b/packages/opencode/src/server/routes/instance/httpapi/file.ts index c55d0c2e7..e283bff19 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/file.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/file.ts @@ -1,4 +1,7 @@ import { File } from "@/file" +import { Ripgrep } from "@/file/ripgrep" +import * as InstanceState from "@/effect/instance-state" +import { LSP } from "@/lsp" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "./auth" @@ -7,7 +10,31 @@ const FileQuery = Schema.Struct({ path: Schema.String, }) +const FindTextQuery = Schema.Struct({ + pattern: Schema.String, +}) + +const FindFileQuery = Schema.Struct({ + query: Schema.String, + dirs: Schema.optional(Schema.Literals(["true", "false"])), + type: Schema.optional(Schema.Literals(["file", "directory"])), + limit: Schema.optional( + Schema.NumberFromString.check( + Schema.isInt(), + Schema.isGreaterThanOrEqualTo(1), + Schema.isLessThanOrEqualTo(200), + ), + ), +}) + +const FindSymbolQuery = Schema.Struct({ + query: Schema.String, +}) + export const FilePaths = { + findText: "/find", + findFile: "/find/file", + findSymbol: "/find/symbol", list: "/file", content: "/file/content", status: "/file/status", @@ -17,6 +44,36 @@ export const FileApi = HttpApi.make("file") .add( HttpApiGroup.make("file") .add( + HttpApiEndpoint.get("findText", FilePaths.findText, { + query: FindTextQuery, + success: Schema.Array(Ripgrep.SearchMatch), + }).annotateMerge( + OpenApi.annotations({ + identifier: "find.text", + summary: "Find text", + description: "Search for text patterns across files in the project using ripgrep.", + }), + ), + HttpApiEndpoint.get("findFile", FilePaths.findFile, { + query: FindFileQuery, + success: Schema.Array(Schema.String), + }).annotateMerge( + OpenApi.annotations({ + identifier: "find.files", + summary: "Find files", + description: "Search for files or directories by name or pattern in the project directory.", + }), + ), + HttpApiEndpoint.get("findSymbol", FilePaths.findSymbol, { + query: FindSymbolQuery, + success: Schema.Array(LSP.Symbol), + }).annotateMerge( + OpenApi.annotations({ + identifier: "find.symbols", + summary: "Find symbols", + description: "Search for workspace symbols like functions, classes, and variables using LSP.", + }), + ), HttpApiEndpoint.get("list", FilePaths.list, { query: FileQuery, success: Schema.Array(File.Node), @@ -66,6 +123,28 @@ export const FileApi = HttpApi.make("file") export const fileHandlers = Layer.unwrap( Effect.gen(function* () { const svc = yield* File.Service + const ripgrep = yield* Ripgrep.Service + + const findText = Effect.fn("FileHttpApi.findText")(function* (ctx: { query: { pattern: string } }) { + return (yield* ripgrep + .search({ cwd: (yield* InstanceState.context).directory, pattern: ctx.query.pattern, limit: 10 }) + .pipe(Effect.orDie)).items + }) + + const findFile = Effect.fn("FileHttpApi.findFile")(function* (ctx: { + query: { query: string; dirs?: "true" | "false"; type?: "file" | "directory"; limit?: number } + }) { + return yield* svc.search({ + query: ctx.query.query, + limit: ctx.query.limit ?? 10, + dirs: ctx.query.dirs !== "false", + type: ctx.query.type, + }) + }) + + const findSymbol = Effect.fn("FileHttpApi.findSymbol")(function* () { + return [] + }) const list = Effect.fn("FileHttpApi.list")(function* (ctx: { query: { path: string } }) { return yield* svc.list(ctx.query.path) @@ -80,7 +159,13 @@ export const fileHandlers = Layer.unwrap( }) return HttpApiBuilder.group(FileApi, "file", (handlers) => - handlers.handle("list", list).handle("content", content).handle("status", status), + handlers + .handle("findText", findText) + .handle("findFile", findFile) + .handle("findSymbol", findSymbol) + .handle("list", list) + .handle("content", content) + .handle("status", status), ) }), -).pipe(Layer.provide(File.defaultLayer)) +).pipe(Layer.provide(File.defaultLayer), Layer.provide(Ripgrep.defaultLayer)) diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index fec0bb1ed..488e43542 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -51,6 +51,9 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.post("/provider/:providerID/oauth/callback", (c) => handler(c.req.raw, context)) app.get("/project", (c) => handler(c.req.raw, context)) app.get("/project/current", (c) => handler(c.req.raw, context)) + app.get(FilePaths.findText, (c) => handler(c.req.raw, context)) + app.get(FilePaths.findFile, (c) => handler(c.req.raw, context)) + app.get(FilePaths.findSymbol, (c) => handler(c.req.raw, context)) app.get(FilePaths.list, (c) => handler(c.req.raw, context)) app.get(FilePaths.content, (c) => handler(c.req.raw, context)) app.get(FilePaths.status, (c) => handler(c.req.raw, context)) diff --git a/packages/opencode/test/server/httpapi-file.test.ts b/packages/opencode/test/server/httpapi-file.test.ts index 5a9058cb6..302e0a349 100644 --- a/packages/opencode/test/server/httpapi-file.test.ts +++ b/packages/opencode/test/server/httpapi-file.test.ts @@ -54,4 +54,24 @@ describe("file HttpApi", () => { expect(status.status).toBe(200) expect(await status.json()).toContainEqual({ path: "hello.txt", added: 1, removed: 0, status: "added" }) }) + + test("serves search endpoints", async () => { + await using tmp = await tmpdir({ git: true }) + await Bun.write(path.join(tmp.path, "hello.txt"), "needle") + + const [text, files, symbols] = await Promise.all([ + request(FilePaths.findText, tmp.path, { pattern: "needle" }), + request(FilePaths.findFile, tmp.path, { query: "hello", type: "file" }), + request(FilePaths.findSymbol, tmp.path, { query: "hello" }), + ]) + + expect(text.status).toBe(200) + expect(await text.json()).toContainEqual(expect.objectContaining({ line_number: 1 })) + + expect(files.status).toBe(200) + expect(await files.json()).toContain("hello.txt") + + expect(symbols.status).toBe(200) + expect(await symbols.json()).toEqual([]) + }) }) From f91b73b9388106205f5cbefca3d79c44a301df5a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 25 Apr 2026 14:13:19 -0400 Subject: [PATCH 23/45] ci: fix model name --- .github/workflows/review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml index 20f6a89f6..2bd1f0c4a 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/review.yml @@ -51,7 +51,7 @@ jobs: PR_TITLE: ${{ steps.pr-details.outputs.title }} run: | PR_BODY=$(jq -r .body pr_data.json) - opencode run -m opencode/openai-gpt-5.5 --variant medium "A new pull request has been created: '${PR_TITLE}' + opencode run -m opencode/gpt-5.5 --variant medium "A new pull request has been created: '${PR_TITLE}' ${{ steps.pr-number.outputs.number }} From ff4b60e1f3d2ad83a74785443dd041ed4e7814bf Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Apr 2026 18:14:26 +0000 Subject: [PATCH 24/45] chore: generate --- .../opencode/src/server/routes/instance/httpapi/file.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/opencode/src/server/routes/instance/httpapi/file.ts b/packages/opencode/src/server/routes/instance/httpapi/file.ts index e283bff19..5222f4ab8 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/file.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/file.ts @@ -19,11 +19,7 @@ const FindFileQuery = Schema.Struct({ dirs: Schema.optional(Schema.Literals(["true", "false"])), type: Schema.optional(Schema.Literals(["file", "directory"])), limit: Schema.optional( - Schema.NumberFromString.check( - Schema.isInt(), - Schema.isGreaterThanOrEqualTo(1), - Schema.isLessThanOrEqualTo(200), - ), + Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(200)), ), }) From 3eee2f6afa5c7fde20d0e838143832b681795d9f Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 25 Apr 2026 14:22:49 -0400 Subject: [PATCH 25/45] core: move cross-spawn-spawner from opencode to core package Moved the cross-spawn-spawner module from packages/opencode to packages/core to enable code sharing across the monorepo. This consolidates the process spawning infrastructure into the core package so other packages can use cross-platform child process spawning without duplicating the implementation. Updated all import statements across the codebase to reference the new location (@opencode-ai/core/effect/cross-spawn-spawner). Removed the local copy from the opencode package along with its tests. --- bun.lock | 24 +++++++++++-------- packages/core/package.json | 6 ++--- .../src/effect/cross-spawn-spawner.ts | 0 .../test/effect/cross-spawn-spawner.test.ts | 18 ++++++++++---- packages/opencode/src/file/ripgrep.ts | 2 +- packages/opencode/src/format/index.ts | 2 +- packages/opencode/src/git/index.ts | 2 +- packages/opencode/src/installation/index.ts | 2 +- packages/opencode/src/mcp/index.ts | 2 +- packages/opencode/src/npm/index.ts | 2 +- packages/opencode/src/project/project.ts | 2 +- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/src/snapshot/index.ts | 2 +- packages/opencode/src/tool/registry.ts | 2 +- packages/opencode/src/worktree/index.ts | 2 +- packages/opencode/test/auth/auth.test.ts | 2 +- packages/opencode/test/bus/bus-effect.test.ts | 2 +- packages/opencode/test/config/config.test.ts | 2 +- packages/opencode/test/format/format.test.ts | 2 +- packages/opencode/test/lsp/index.test.ts | 2 +- packages/opencode/test/lsp/lifecycle.test.ts | 2 +- .../opencode/test/permission/next.test.ts | 2 +- .../opencode/test/project/project.test.ts | 2 +- .../test/project/worktree-remove.test.ts | 2 +- .../opencode/test/project/worktree.test.ts | 2 +- .../opencode/test/session/compaction.test.ts | 2 +- .../test/session/processor-effect.test.ts | 2 +- packages/opencode/test/session/prompt.test.ts | 2 +- .../test/session/revert-compact.test.ts | 2 +- .../test/session/snapshot-tool-race.test.ts | 2 +- .../opencode/test/share/share-next.test.ts | 2 +- packages/opencode/test/skill/skill.test.ts | 2 +- .../opencode/test/storage/storage.test.ts | 2 +- packages/opencode/test/tool/bash.test.ts | 2 +- packages/opencode/test/tool/glob.test.ts | 2 +- packages/opencode/test/tool/grep.test.ts | 2 +- packages/opencode/test/tool/lsp.test.ts | 2 +- packages/opencode/test/tool/question.test.ts | 2 +- packages/opencode/test/tool/read.test.ts | 2 +- packages/opencode/test/tool/registry.test.ts | 2 +- packages/opencode/test/tool/skill.test.ts | 2 +- packages/opencode/test/tool/task.test.ts | 2 +- packages/opencode/test/tool/write.test.ts | 2 +- 43 files changed, 69 insertions(+), 57 deletions(-) rename packages/{opencode => core}/src/effect/cross-spawn-spawner.ts (100%) rename packages/{opencode => core}/test/effect/cross-spawn-spawner.test.ts (96%) diff --git a/bun.lock b/bun.lock index 2420ab6df..06a414a21 100644 --- a/bun.lock +++ b/bun.lock @@ -199,24 +199,22 @@ "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", + "cross-spawn": "catalog:", "effect": "catalog:", "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", - "semver": "catalog:", "xdg-basedir": "5.1.0", "zod": "catalog:", }, "devDependencies": { "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", - "@types/npmcli__arborist": "6.3.3", - "@types/semver": "catalog:", + "@types/cross-spawn": "catalog:", }, }, "packages/desktop": { @@ -2849,7 +2847,7 @@ "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], - "common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], + "common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], "compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="], @@ -4007,7 +4005,7 @@ "native-duplexpair": ["native-duplexpair@1.0.0", "", {}, "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA=="], - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], @@ -5517,6 +5515,8 @@ "@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "@npmcli/arborist/common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], + "@npmcli/query/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], "@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], @@ -5709,8 +5709,6 @@ "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - "ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="], "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="], @@ -5741,8 +5739,6 @@ "astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], - "astro/common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], - "astro/diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="], "astro/unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], @@ -5897,6 +5893,8 @@ "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "make-fetch-happen/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="], @@ -6883,6 +6881,8 @@ "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="], + "@electron/rebuild/node-gyp/make-fetch-happen/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "@electron/rebuild/node-gyp/make-fetch-happen/ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="], "@electron/rebuild/node-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], @@ -6947,6 +6947,8 @@ "@jsx-email/cli/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], @@ -7145,6 +7147,8 @@ "js-beautify/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "opencontrol/@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "opencontrol/@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], diff --git a/packages/core/package.json b/packages/core/package.json index a244ea8b4..bd826de35 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -18,23 +18,21 @@ "imports": {}, "devDependencies": { "@tsconfig/bun": "catalog:", - "@types/semver": "catalog:", "@types/bun": "catalog:", - "@types/npmcli__arborist": "6.3.3" + "@types/cross-spawn": "catalog:" }, "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:", + "cross-spawn": "catalog:", "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", - "semver": "catalog:", "xdg-basedir": "5.1.0", "zod": "catalog:" }, diff --git a/packages/opencode/src/effect/cross-spawn-spawner.ts b/packages/core/src/effect/cross-spawn-spawner.ts similarity index 100% rename from packages/opencode/src/effect/cross-spawn-spawner.ts rename to packages/core/src/effect/cross-spawn-spawner.ts diff --git a/packages/opencode/test/effect/cross-spawn-spawner.test.ts b/packages/core/test/effect/cross-spawn-spawner.test.ts similarity index 96% rename from packages/opencode/test/effect/cross-spawn-spawner.test.ts rename to packages/core/test/effect/cross-spawn-spawner.test.ts index b4e52529c..d53725797 100644 --- a/packages/opencode/test/effect/cross-spawn-spawner.test.ts +++ b/packages/core/test/effect/cross-spawn-spawner.test.ts @@ -1,11 +1,11 @@ import { describe, expect } from "bun:test" import fs from "node:fs/promises" +import os from "node:os" import path from "node:path" import { Effect, Exit, Stream } from "effect" import type * as PlatformError from "effect/PlatformError" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" -import { tmpdir } from "../fixture/fixture" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { testEffect } from "../lib/effect" const live = CrossSpawnSpawner.defaultLayer @@ -39,11 +39,21 @@ function alive(pid: number) { } } +async function tmpdir() { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-test-")) + return { + path: dir, + async [Symbol.asyncDispose]() { + await fs.rm(dir, { recursive: true, force: true }) + }, + } +} + async function gone(pid: number, timeout = 5_000) { const end = Date.now() + timeout while (Date.now() < end) { if (!alive(pid)) return true - await Bun.sleep(50) + await new Promise((resolve) => setTimeout(resolve, 50)) } return !alive(pid) } @@ -395,7 +405,7 @@ describe("cross-spawn spawner", () => { const file = path.join(dir, "echo cmd.cmd") yield* Effect.promise(() => fs.mkdir(dir, { recursive: true })) - yield* Effect.promise(() => Bun.write(file, "@echo off\r\nif %~1==--stdio exit /b 0\r\nexit /b 7\r\n")) + yield* Effect.promise(() => fs.writeFile(file, "@echo off\r\nif %~1==--stdio exit /b 0\r\nexit /b 7\r\n")) const code = yield* ChildProcessSpawner.ChildProcessSpawner.use((svc) => svc.exitCode( diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index ab725b731..dd794ef6f 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -6,7 +6,7 @@ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/ import { ChildProcess } from "effect/unstable/process" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { Global } from "@opencode-ai/core/global" import { Log } from "@/util" import { sanitizedProcessEnv } from "@opencode-ai/core/util/opencode-process" diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index 4284a2cf6..2c5943c7d 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -1,6 +1,6 @@ import { Effect, Layer, Context, Schema } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { InstanceState } from "@/effect" import path from "path" import { mergeDeep } from "remeda" diff --git a/packages/opencode/src/git/index.ts b/packages/opencode/src/git/index.ts index 719b5607f..d2e04910a 100644 --- a/packages/opencode/src/git/index.ts +++ b/packages/opencode/src/git/index.ts @@ -1,4 +1,4 @@ -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { Effect, Layer, Context, Stream } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 1a39d3c61..55c4092e2 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -1,6 +1,6 @@ import { Effect, Layer, Schema, Context, Stream } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { withTransientReadRetry } from "@/util/effect-http-client" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import path from "path" diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 23862db63..9652a1258 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -29,7 +29,7 @@ import { Effect, Exit, Layer, Option, Context, Schema, Stream } from "effect" import { EffectBridge } from "@/effect" import { InstanceState } from "@/effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { zod as effectZod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts index ca67491d0..23368b29b 100644 --- a/packages/opencode/src/npm/index.ts +++ b/packages/opencode/src/npm/index.ts @@ -14,7 +14,7 @@ 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 { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", { add: Schema.Array(Schema.String).pipe(Schema.optional), diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index e622464b4..c437fedb2 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -12,7 +12,7 @@ import { Effect, Layer, Path, Scope, Context, Stream, Types, Schema } from "effe import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodePath } from "@effect/platform-node" import { AppFileSystem } from "@opencode-ai/core/filesystem" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 708961168..87f914a80 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -27,7 +27,7 @@ import { LSP } from "../lsp" 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" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import * as Stream from "effect/Stream" import { Command } from "../command" import { pathToFileURL, fileURLToPath } from "url" diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 3701b8210..32d65633c 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -3,7 +3,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { formatPatch, structuredPatch } from "diff" import path from "path" import z from "zod" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { InstanceState } from "@/effect" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Hash } from "@opencode-ai/core/util/hash" diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index b7fa696c8..422fd8e3a 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -34,7 +34,7 @@ import { pathToFileURL } from "url" import { Effect, Layer, Context } from "effect" import { FetchHttpClient, HttpClient } from "effect/unstable/http" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { Ripgrep } from "../file/ripgrep" import { Format } from "../format" import { InstanceState } from "@/effect" diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index b89ac32a9..f39d9ad04 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -18,7 +18,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodePath } from "@effect/platform-node" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { BootstrapRuntime } from "@/effect/bootstrap-runtime" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { InstanceState } from "@/effect" const log = Log.create({ service: "worktree" }) diff --git a/packages/opencode/test/auth/auth.test.ts b/packages/opencode/test/auth/auth.test.ts index 864649d7a..8688eafaf 100644 --- a/packages/opencode/test/auth/auth.test.ts +++ b/packages/opencode/test/auth/auth.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Auth } from "../../src/auth" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/bus/bus-effect.test.ts b/packages/opencode/test/bus/bus-effect.test.ts index 3d602ae6f..d8b4a275b 100644 --- a/packages/opencode/test/bus/bus-effect.test.ts +++ b/packages/opencode/test/bus/bus-effect.test.ts @@ -3,7 +3,7 @@ import { Deferred, Effect, Layer, Schema, Stream } from "effect" import { Bus } from "../../src/bus" import { BusEvent } from "../../src/bus/bus-event" import { Instance } from "../../src/project/instance" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 8512236a3..bdd361e7a 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -13,7 +13,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Env } from "../../src/env" import { provideTmpdirInstance } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { testEffect } from "../lib/effect" /** Infra layer that provides FileSystem, Path, ChildProcessSpawner for test fixtures */ diff --git a/packages/opencode/test/format/format.test.ts b/packages/opencode/test/format/format.test.ts index 2f6f235aa..544359c60 100644 --- a/packages/opencode/test/format/format.test.ts +++ b/packages/opencode/test/format/format.test.ts @@ -3,7 +3,7 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { Format } from "../../src/format" import * as Formatter from "../../src/format/formatter" diff --git a/packages/opencode/test/lsp/index.test.ts b/packages/opencode/test/lsp/index.test.ts index d138f56e3..8cb098826 100644 --- a/packages/opencode/test/lsp/index.test.ts +++ b/packages/opencode/test/lsp/index.test.ts @@ -3,7 +3,7 @@ import path from "path" import { Effect, Layer } from "effect" import { LSP } from "../../src/lsp" import { LSPServer } from "../../src/lsp" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/lsp/lifecycle.test.ts b/packages/opencode/test/lsp/lifecycle.test.ts index 13f21c93c..98ac600f4 100644 --- a/packages/opencode/test/lsp/lifecycle.test.ts +++ b/packages/opencode/test/lsp/lifecycle.test.ts @@ -3,7 +3,7 @@ import path from "path" import { Effect, Layer } from "effect" import { LSP } from "../../src/lsp" import { LSPServer } from "../../src/lsp" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts index b58c716d8..80601cd9a 100644 --- a/packages/opencode/test/permission/next.test.ts +++ b/packages/opencode/test/permission/next.test.ts @@ -2,7 +2,7 @@ import { afterEach, test, expect } from "bun:test" import os from "os" import { Cause, Effect, Exit, Fiber, Layer } from "effect" import { Bus } from "../../src/bus" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { Permission } from "../../src/permission" import { PermissionID } from "../../src/permission/schema" import { Instance } from "../../src/project/instance" diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index c61df3548..6579b414f 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -10,7 +10,7 @@ import { Effect, Layer, Stream } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodePath } from "@effect/platform-node" import { AppFileSystem } from "@opencode-ai/core/filesystem" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" void Log.init({ print: false }) diff --git a/packages/opencode/test/project/worktree-remove.test.ts b/packages/opencode/test/project/worktree-remove.test.ts index 5fb2beb28..b0cb626b1 100644 --- a/packages/opencode/test/project/worktree-remove.test.ts +++ b/packages/opencode/test/project/worktree-remove.test.ts @@ -3,7 +3,7 @@ import { describe, expect } from "bun:test" import * as fs from "fs/promises" import path from "path" import { Effect, Layer } from "effect" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { Worktree } from "../../src/worktree" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/project/worktree.test.ts b/packages/opencode/test/project/worktree.test.ts index c0fe63551..b20914c96 100644 --- a/packages/opencode/test/project/worktree.test.ts +++ b/packages/opencode/test/project/worktree.test.ts @@ -3,7 +3,7 @@ import { afterEach, describe, expect } from "bun:test" import * as fs from "fs/promises" import path from "path" import { Cause, Effect, Exit, Layer } from "effect" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { Instance } from "../../src/project/instance" import { Worktree } from "../../src/worktree" import { provideInstance, provideTmpdirInstance } from "../fixture/fixture" diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 4fe9c1551..79bdfe41f 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -25,7 +25,7 @@ import * as SessionProcessorModule from "../../src/session/processor" import { Snapshot } from "../../src/snapshot" import { ProviderTest } from "../fake/provider" import { testEffect } from "../lib/effect" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" void Log.init({ print: false }) diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index 74ce91307..d665022fd 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -19,7 +19,7 @@ import { SessionStatus } from "../../src/session/status" import { SessionSummary } from "../../src/session/summary" import { Snapshot } from "../../src/snapshot" import { Log } from "../../src/util" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { provideTmpdirServer } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { raw, reply, TestLLMServer } from "../lib/llm-server" diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 451f1d004..288ca8f99 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -38,7 +38,7 @@ import { Snapshot } from "../../src/snapshot" import { ToolRegistry } from "../../src/tool" import { Truncate } from "../../src/tool" import { Log } from "../../src/util" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" import { Format } from "../../src/format" import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture" diff --git a/packages/opencode/test/session/revert-compact.test.ts b/packages/opencode/test/session/revert-compact.test.ts index f28fb94c0..213e59632 100644 --- a/packages/opencode/test/session/revert-compact.test.ts +++ b/packages/opencode/test/session/revert-compact.test.ts @@ -9,7 +9,7 @@ import { MessageV2 } from "../../src/session/message-v2" import { Snapshot } from "../../src/snapshot" import { Log } from "../../src/util" import { MessageID, PartID, SessionID } from "../../src/session/schema" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index c7e352262..8c8c4da3b 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -52,7 +52,7 @@ import { Snapshot } from "../../src/snapshot" import { ToolRegistry } from "../../src/tool" import { Truncate } from "../../src/tool" import { AppFileSystem } from "@opencode-ai/core/filesystem" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" import { Format } from "../../src/format" diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index e217300d0..41763fe97 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -6,7 +6,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab import { AccessToken, AccountID, OrgID, RefreshToken } from "../../src/account/schema" import { Account } from "../../src/account/account" import { AccountRepo } from "../../src/account/repo" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { Bus } from "../../src/bus" import { Config } from "../../src/config" import { Provider } from "../../src/provider" diff --git a/packages/opencode/test/skill/skill.test.ts b/packages/opencode/test/skill/skill.test.ts index 21c6c7e65..13f25be5b 100644 --- a/packages/opencode/test/skill/skill.test.ts +++ b/packages/opencode/test/skill/skill.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Skill } from "../../src/skill" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { provideInstance, provideTmpdirInstance, tmpdir } from "../fixture/fixture" import { testEffect } from "../lib/effect" import path from "path" diff --git a/packages/opencode/test/storage/storage.test.ts b/packages/opencode/test/storage/storage.test.ts index 6be653ecb..f1b245f9b 100644 --- a/packages/opencode/test/storage/storage.test.ts +++ b/packages/opencode/test/storage/storage.test.ts @@ -2,7 +2,7 @@ import { describe, expect } from "bun:test" import path from "path" import { Effect, Exit, Layer } from "effect" import { AppFileSystem } from "@opencode-ai/core/filesystem" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { Git } from "../../src/git" import { Global } from "@opencode-ai/core/global" import { Storage } from "../../src/storage" diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index fd35c9aeb..23f18f989 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -11,7 +11,7 @@ import type { Permission } from "../../src/permission" import { Agent } from "../../src/agent/agent" import { Truncate } from "../../src/tool" import { SessionID, MessageID } from "../../src/session/schema" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Plugin } from "../../src/plugin" diff --git a/packages/opencode/test/tool/glob.test.ts b/packages/opencode/test/tool/glob.test.ts index c37e7b35f..8d496509a 100644 --- a/packages/opencode/test/tool/glob.test.ts +++ b/packages/opencode/test/tool/glob.test.ts @@ -3,7 +3,7 @@ import path from "path" import { Cause, Effect, Exit, Layer } from "effect" import { GlobTool } from "../../src/tool/glob" import { SessionID, MessageID } from "../../src/session/schema" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Truncate } from "../../src/tool" diff --git a/packages/opencode/test/tool/grep.test.ts b/packages/opencode/test/tool/grep.test.ts index a279574e1..3e147dddc 100644 --- a/packages/opencode/test/tool/grep.test.ts +++ b/packages/opencode/test/tool/grep.test.ts @@ -4,7 +4,7 @@ import { Effect, Layer } from "effect" import { GrepTool } from "../../src/tool/grep" import { provideInstance, provideTmpdirInstance } from "../fixture/fixture" import { SessionID, MessageID } from "../../src/session/schema" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { Truncate } from "../../src/tool" import { Agent } from "../../src/agent/agent" import { Ripgrep } from "../../src/file/ripgrep" diff --git a/packages/opencode/test/tool/lsp.test.ts b/packages/opencode/test/tool/lsp.test.ts index b9d48e69a..07de4a0da 100644 --- a/packages/opencode/test/tool/lsp.test.ts +++ b/packages/opencode/test/tool/lsp.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect } from "bun:test" import { Effect, Layer } from "effect" import path from "path" import { Agent } from "../../src/agent/agent" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { LSP } from "../../src/lsp" import { Permission } from "../../src/permission" diff --git a/packages/opencode/test/tool/question.test.ts b/packages/opencode/test/tool/question.test.ts index 17718b2b3..53c413186 100644 --- a/packages/opencode/test/tool/question.test.ts +++ b/packages/opencode/test/tool/question.test.ts @@ -4,7 +4,7 @@ import { QuestionTool } from "../../src/tool/question" import { Question } from "../../src/question" import { SessionID, MessageID } from "../../src/session/schema" import { Agent } from "../../src/agent/agent" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { Truncate } from "../../src/tool" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 7c3bf51fe..27e6b71c5 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect } from "bun:test" import { Cause, Effect, Exit, Layer } from "effect" import path from "path" import { Agent } from "../../src/agent/agent" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { LSP } from "../../src/lsp" import { Permission } from "../../src/permission" diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index dbb89e09a..54c1d7706 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -3,7 +3,7 @@ import path from "path" import fs from "fs/promises" import { Effect, Layer } from "effect" import { Instance } from "../../src/project/instance" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { ToolRegistry } from "../../src/tool" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts index b12940e4d..c67121e3c 100644 --- a/packages/opencode/test/tool/skill.test.ts +++ b/packages/opencode/test/tool/skill.test.ts @@ -1,4 +1,4 @@ -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { Effect, Layer } from "effect" import { afterEach, describe, expect } from "bun:test" import path from "path" diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index b94dd5208..1eaa0cfc8 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Agent } from "../../src/agent/agent" import { Config } from "../../src/config" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { Instance } from "../../src/project/instance" import { Session } from "../../src/session" import { MessageV2 } from "../../src/session/message-v2" diff --git a/packages/opencode/test/tool/write.test.ts b/packages/opencode/test/tool/write.test.ts index 0714d2d02..706ddb372 100644 --- a/packages/opencode/test/tool/write.test.ts +++ b/packages/opencode/test/tool/write.test.ts @@ -12,7 +12,7 @@ import { Truncate } from "../../src/tool" import { Tool } from "../../src/tool" import { Agent } from "../../src/agent/agent" import { SessionID, MessageID } from "../../src/session/schema" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" From 1e98167b0e027825b3a77a2dab0be539011e7376 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 25 Apr 2026 14:30:16 -0400 Subject: [PATCH 26/45] core: move cross-spawn-spawner to root and remove unused types The cross-spawn-spawner module has been moved from src/effect/ to src/ to simplify the core package structure. The src/types.d.ts file which contained unused type declarations has also been removed. All imports throughout the codebase have been updated to reflect the new location. This change reduces the package's internal complexity by flattening the module hierarchy and removing dead code, making future maintenance easier. --- .../src/{effect => }/cross-spawn-spawner.ts | 0 packages/core/src/types.d.ts | 46 ------------------- .../test/effect/cross-spawn-spawner.test.ts | 2 +- packages/opencode/src/file/ripgrep.ts | 2 +- packages/opencode/src/format/index.ts | 2 +- packages/opencode/src/git/index.ts | 2 +- packages/opencode/src/installation/index.ts | 2 +- packages/opencode/src/mcp/index.ts | 2 +- packages/opencode/src/npm/index.ts | 2 +- packages/opencode/src/project/project.ts | 2 +- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/src/snapshot/index.ts | 2 +- packages/opencode/src/tool/registry.ts | 2 +- packages/opencode/src/worktree/index.ts | 2 +- packages/opencode/test/auth/auth.test.ts | 2 +- packages/opencode/test/bus/bus-effect.test.ts | 2 +- packages/opencode/test/config/config.test.ts | 2 +- packages/opencode/test/format/format.test.ts | 2 +- packages/opencode/test/lsp/index.test.ts | 2 +- packages/opencode/test/lsp/lifecycle.test.ts | 2 +- .../opencode/test/permission/next.test.ts | 2 +- .../opencode/test/project/project.test.ts | 2 +- .../test/project/worktree-remove.test.ts | 2 +- .../opencode/test/project/worktree.test.ts | 2 +- .../opencode/test/session/compaction.test.ts | 2 +- .../test/session/processor-effect.test.ts | 2 +- packages/opencode/test/session/prompt.test.ts | 2 +- .../test/session/revert-compact.test.ts | 2 +- .../test/session/snapshot-tool-race.test.ts | 2 +- .../opencode/test/share/share-next.test.ts | 2 +- packages/opencode/test/skill/skill.test.ts | 2 +- .../opencode/test/storage/storage.test.ts | 2 +- packages/opencode/test/tool/bash.test.ts | 2 +- packages/opencode/test/tool/glob.test.ts | 2 +- packages/opencode/test/tool/grep.test.ts | 2 +- packages/opencode/test/tool/lsp.test.ts | 2 +- packages/opencode/test/tool/question.test.ts | 2 +- packages/opencode/test/tool/read.test.ts | 2 +- packages/opencode/test/tool/registry.test.ts | 2 +- packages/opencode/test/tool/skill.test.ts | 2 +- packages/opencode/test/tool/task.test.ts | 2 +- packages/opencode/test/tool/write.test.ts | 2 +- 42 files changed, 40 insertions(+), 86 deletions(-) rename packages/core/src/{effect => }/cross-spawn-spawner.ts (100%) delete mode 100644 packages/core/src/types.d.ts diff --git a/packages/core/src/effect/cross-spawn-spawner.ts b/packages/core/src/cross-spawn-spawner.ts similarity index 100% rename from packages/core/src/effect/cross-spawn-spawner.ts rename to packages/core/src/cross-spawn-spawner.ts diff --git a/packages/core/src/types.d.ts b/packages/core/src/types.d.ts deleted file mode 100644 index 60e1639ad..000000000 --- a/packages/core/src/types.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -declare module "@npmcli/arborist" { - export interface ArboristOptions { - path: string - binLinks?: boolean - progress?: boolean - savePrefix?: string - ignoreScripts?: boolean - [key: string]: unknown - } - - export interface ArboristNode { - name: string - path: string - } - - export interface ArboristEdge { - to?: ArboristNode - } - - export interface ArboristTree { - edgesOut: Map - } - - export interface ReifyOptions { - add?: string[] - save?: boolean - saveType?: "prod" | "dev" | "optional" | "peer" - [key: string]: unknown - } - - export class Arborist { - constructor(options: ArboristOptions) - loadVirtual(): Promise - reify(options?: ReifyOptions): Promise - } -} - -declare var Bun: - | { - file(path: string): { - text(): Promise - json(): Promise - } - write(path: string, content: string | Uint8Array): Promise - } - | undefined diff --git a/packages/core/test/effect/cross-spawn-spawner.test.ts b/packages/core/test/effect/cross-spawn-spawner.test.ts index d53725797..2612b75e4 100644 --- a/packages/core/test/effect/cross-spawn-spawner.test.ts +++ b/packages/core/test/effect/cross-spawn-spawner.test.ts @@ -5,7 +5,7 @@ import path from "node:path" import { Effect, Exit, Stream } from "effect" import type * as PlatformError from "effect/PlatformError" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { testEffect } from "../lib/effect" const live = CrossSpawnSpawner.defaultLayer diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index dd794ef6f..da5981737 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -6,7 +6,7 @@ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/ import { ChildProcess } from "effect/unstable/process" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Global } from "@opencode-ai/core/global" import { Log } from "@/util" import { sanitizedProcessEnv } from "@opencode-ai/core/util/opencode-process" diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index 2c5943c7d..9fa53293f 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -1,6 +1,6 @@ import { Effect, Layer, Context, Schema } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { InstanceState } from "@/effect" import path from "path" import { mergeDeep } from "remeda" diff --git a/packages/opencode/src/git/index.ts b/packages/opencode/src/git/index.ts index d2e04910a..16a862447 100644 --- a/packages/opencode/src/git/index.ts +++ b/packages/opencode/src/git/index.ts @@ -1,4 +1,4 @@ -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Effect, Layer, Context, Stream } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 55c4092e2..84fd02cb3 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -1,6 +1,6 @@ import { Effect, Layer, Schema, Context, Stream } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { withTransientReadRetry } from "@/util/effect-http-client" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import path from "path" diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 9652a1258..c2479372d 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -29,7 +29,7 @@ import { Effect, Exit, Layer, Option, Context, Schema, Stream } from "effect" import { EffectBridge } from "@/effect" import { InstanceState } from "@/effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { zod as effectZod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts index 23368b29b..a7538b9c3 100644 --- a/packages/opencode/src/npm/index.ts +++ b/packages/opencode/src/npm/index.ts @@ -14,7 +14,7 @@ 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 { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", { add: Schema.Array(Schema.String).pipe(Schema.optional), diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index c437fedb2..fc34a6296 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -12,7 +12,7 @@ import { Effect, Layer, Path, Scope, Context, Stream, Types, Schema } from "effe import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodePath } from "@effect/platform-node" import { AppFileSystem } from "@opencode-ai/core/filesystem" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 87f914a80..600eb42f7 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -27,7 +27,7 @@ import { LSP } from "../lsp" import { Flag } from "@opencode-ai/core/flag/flag" import { ulid } from "ulid" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import * as Stream from "effect/Stream" import { Command } from "../command" import { pathToFileURL, fileURLToPath } from "url" diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 32d65633c..88ea274f8 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -3,7 +3,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { formatPatch, structuredPatch } from "diff" import path from "path" import z from "zod" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { InstanceState } from "@/effect" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Hash } from "@opencode-ai/core/util/hash" diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 422fd8e3a..4e3d3d714 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -34,7 +34,7 @@ import { pathToFileURL } from "url" import { Effect, Layer, Context } from "effect" import { FetchHttpClient, HttpClient } from "effect/unstable/http" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Ripgrep } from "../file/ripgrep" import { Format } from "../format" import { InstanceState } from "@/effect" diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index f39d9ad04..b9c7226b6 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -18,7 +18,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodePath } from "@effect/platform-node" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { BootstrapRuntime } from "@/effect/bootstrap-runtime" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { InstanceState } from "@/effect" const log = Log.create({ service: "worktree" }) diff --git a/packages/opencode/test/auth/auth.test.ts b/packages/opencode/test/auth/auth.test.ts index 8688eafaf..55e950aab 100644 --- a/packages/opencode/test/auth/auth.test.ts +++ b/packages/opencode/test/auth/auth.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Auth } from "../../src/auth" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/bus/bus-effect.test.ts b/packages/opencode/test/bus/bus-effect.test.ts index d8b4a275b..0daf8fe6a 100644 --- a/packages/opencode/test/bus/bus-effect.test.ts +++ b/packages/opencode/test/bus/bus-effect.test.ts @@ -3,7 +3,7 @@ import { Deferred, Effect, Layer, Schema, Stream } from "effect" import { Bus } from "../../src/bus" import { BusEvent } from "../../src/bus/bus-event" import { Instance } from "../../src/project/instance" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index bdd361e7a..f6fb04ea0 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -13,7 +13,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Env } from "../../src/env" import { provideTmpdirInstance } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { testEffect } from "../lib/effect" /** Infra layer that provides FileSystem, Path, ChildProcessSpawner for test fixtures */ diff --git a/packages/opencode/test/format/format.test.ts b/packages/opencode/test/format/format.test.ts index 544359c60..674d2767c 100644 --- a/packages/opencode/test/format/format.test.ts +++ b/packages/opencode/test/format/format.test.ts @@ -3,7 +3,7 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Format } from "../../src/format" import * as Formatter from "../../src/format/formatter" diff --git a/packages/opencode/test/lsp/index.test.ts b/packages/opencode/test/lsp/index.test.ts index 8cb098826..092bfef51 100644 --- a/packages/opencode/test/lsp/index.test.ts +++ b/packages/opencode/test/lsp/index.test.ts @@ -3,7 +3,7 @@ import path from "path" import { Effect, Layer } from "effect" import { LSP } from "../../src/lsp" import { LSPServer } from "../../src/lsp" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/lsp/lifecycle.test.ts b/packages/opencode/test/lsp/lifecycle.test.ts index 98ac600f4..e39290973 100644 --- a/packages/opencode/test/lsp/lifecycle.test.ts +++ b/packages/opencode/test/lsp/lifecycle.test.ts @@ -3,7 +3,7 @@ import path from "path" import { Effect, Layer } from "effect" import { LSP } from "../../src/lsp" import { LSPServer } from "../../src/lsp" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts index 80601cd9a..0064185f4 100644 --- a/packages/opencode/test/permission/next.test.ts +++ b/packages/opencode/test/permission/next.test.ts @@ -2,7 +2,7 @@ import { afterEach, test, expect } from "bun:test" import os from "os" import { Cause, Effect, Exit, Fiber, Layer } from "effect" import { Bus } from "../../src/bus" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Permission } from "../../src/permission" import { PermissionID } from "../../src/permission/schema" import { Instance } from "../../src/project/instance" diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index 6579b414f..c3aba55de 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -10,7 +10,7 @@ import { Effect, Layer, Stream } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodePath } from "@effect/platform-node" import { AppFileSystem } from "@opencode-ai/core/filesystem" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" void Log.init({ print: false }) diff --git a/packages/opencode/test/project/worktree-remove.test.ts b/packages/opencode/test/project/worktree-remove.test.ts index b0cb626b1..fa70ecb89 100644 --- a/packages/opencode/test/project/worktree-remove.test.ts +++ b/packages/opencode/test/project/worktree-remove.test.ts @@ -3,7 +3,7 @@ import { describe, expect } from "bun:test" import * as fs from "fs/promises" import path from "path" import { Effect, Layer } from "effect" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Worktree } from "../../src/worktree" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/project/worktree.test.ts b/packages/opencode/test/project/worktree.test.ts index b20914c96..44a25a8e6 100644 --- a/packages/opencode/test/project/worktree.test.ts +++ b/packages/opencode/test/project/worktree.test.ts @@ -3,7 +3,7 @@ import { afterEach, describe, expect } from "bun:test" import * as fs from "fs/promises" import path from "path" import { Cause, Effect, Exit, Layer } from "effect" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Instance } from "../../src/project/instance" import { Worktree } from "../../src/worktree" import { provideInstance, provideTmpdirInstance } from "../fixture/fixture" diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 79bdfe41f..1b2b120b6 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -25,7 +25,7 @@ import * as SessionProcessorModule from "../../src/session/processor" import { Snapshot } from "../../src/snapshot" import { ProviderTest } from "../fake/provider" import { testEffect } from "../lib/effect" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" void Log.init({ print: false }) diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index d665022fd..fee42a939 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -19,7 +19,7 @@ import { SessionStatus } from "../../src/session/status" import { SessionSummary } from "../../src/session/summary" import { Snapshot } from "../../src/snapshot" import { Log } from "../../src/util" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideTmpdirServer } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { raw, reply, TestLLMServer } from "../lib/llm-server" diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 288ca8f99..7e3377746 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -38,7 +38,7 @@ import { Snapshot } from "../../src/snapshot" import { ToolRegistry } from "../../src/tool" import { Truncate } from "../../src/tool" import { Log } from "../../src/util" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" import { Format } from "../../src/format" import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture" diff --git a/packages/opencode/test/session/revert-compact.test.ts b/packages/opencode/test/session/revert-compact.test.ts index 213e59632..d77ef3e05 100644 --- a/packages/opencode/test/session/revert-compact.test.ts +++ b/packages/opencode/test/session/revert-compact.test.ts @@ -9,7 +9,7 @@ import { MessageV2 } from "../../src/session/message-v2" import { Snapshot } from "../../src/snapshot" import { Log } from "../../src/util" import { MessageID, PartID, SessionID } from "../../src/session/schema" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index 8c8c4da3b..269c23148 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -52,7 +52,7 @@ import { Snapshot } from "../../src/snapshot" import { ToolRegistry } from "../../src/tool" import { Truncate } from "../../src/tool" import { AppFileSystem } from "@opencode-ai/core/filesystem" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" import { Format } from "../../src/format" diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index 41763fe97..5470d654a 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -6,7 +6,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab import { AccessToken, AccountID, OrgID, RefreshToken } from "../../src/account/schema" import { Account } from "../../src/account/account" import { AccountRepo } from "../../src/account/repo" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Bus } from "../../src/bus" import { Config } from "../../src/config" import { Provider } from "../../src/provider" diff --git a/packages/opencode/test/skill/skill.test.ts b/packages/opencode/test/skill/skill.test.ts index 13f25be5b..bfcb0dcd6 100644 --- a/packages/opencode/test/skill/skill.test.ts +++ b/packages/opencode/test/skill/skill.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Skill } from "../../src/skill" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideInstance, provideTmpdirInstance, tmpdir } from "../fixture/fixture" import { testEffect } from "../lib/effect" import path from "path" diff --git a/packages/opencode/test/storage/storage.test.ts b/packages/opencode/test/storage/storage.test.ts index f1b245f9b..a3d5a8ac5 100644 --- a/packages/opencode/test/storage/storage.test.ts +++ b/packages/opencode/test/storage/storage.test.ts @@ -2,7 +2,7 @@ import { describe, expect } from "bun:test" import path from "path" import { Effect, Exit, Layer } from "effect" import { AppFileSystem } from "@opencode-ai/core/filesystem" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Git } from "../../src/git" import { Global } from "@opencode-ai/core/global" import { Storage } from "../../src/storage" diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index 23f18f989..32cd43100 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -11,7 +11,7 @@ import type { Permission } from "../../src/permission" import { Agent } from "../../src/agent/agent" import { Truncate } from "../../src/tool" import { SessionID, MessageID } from "../../src/session/schema" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Plugin } from "../../src/plugin" diff --git a/packages/opencode/test/tool/glob.test.ts b/packages/opencode/test/tool/glob.test.ts index 8d496509a..a8637ea9c 100644 --- a/packages/opencode/test/tool/glob.test.ts +++ b/packages/opencode/test/tool/glob.test.ts @@ -3,7 +3,7 @@ import path from "path" import { Cause, Effect, Exit, Layer } from "effect" import { GlobTool } from "../../src/tool/glob" import { SessionID, MessageID } from "../../src/session/schema" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Truncate } from "../../src/tool" diff --git a/packages/opencode/test/tool/grep.test.ts b/packages/opencode/test/tool/grep.test.ts index 3e147dddc..acdaff03a 100644 --- a/packages/opencode/test/tool/grep.test.ts +++ b/packages/opencode/test/tool/grep.test.ts @@ -4,7 +4,7 @@ import { Effect, Layer } from "effect" import { GrepTool } from "../../src/tool/grep" import { provideInstance, provideTmpdirInstance } from "../fixture/fixture" import { SessionID, MessageID } from "../../src/session/schema" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Truncate } from "../../src/tool" import { Agent } from "../../src/agent/agent" import { Ripgrep } from "../../src/file/ripgrep" diff --git a/packages/opencode/test/tool/lsp.test.ts b/packages/opencode/test/tool/lsp.test.ts index 07de4a0da..b7a52da19 100644 --- a/packages/opencode/test/tool/lsp.test.ts +++ b/packages/opencode/test/tool/lsp.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect } from "bun:test" import { Effect, Layer } from "effect" import path from "path" import { Agent } from "../../src/agent/agent" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { LSP } from "../../src/lsp" import { Permission } from "../../src/permission" diff --git a/packages/opencode/test/tool/question.test.ts b/packages/opencode/test/tool/question.test.ts index 53c413186..537d1f950 100644 --- a/packages/opencode/test/tool/question.test.ts +++ b/packages/opencode/test/tool/question.test.ts @@ -4,7 +4,7 @@ import { QuestionTool } from "../../src/tool/question" import { Question } from "../../src/question" import { SessionID, MessageID } from "../../src/session/schema" import { Agent } from "../../src/agent/agent" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Truncate } from "../../src/tool" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 27e6b71c5..b9c313bdc 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect } from "bun:test" import { Cause, Effect, Exit, Layer } from "effect" import path from "path" import { Agent } from "../../src/agent/agent" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { LSP } from "../../src/lsp" import { Permission } from "../../src/permission" diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index 54c1d7706..523352d41 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -3,7 +3,7 @@ import path from "path" import fs from "fs/promises" import { Effect, Layer } from "effect" import { Instance } from "../../src/project/instance" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { ToolRegistry } from "../../src/tool" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts index c67121e3c..43659187f 100644 --- a/packages/opencode/test/tool/skill.test.ts +++ b/packages/opencode/test/tool/skill.test.ts @@ -1,4 +1,4 @@ -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Effect, Layer } from "effect" import { afterEach, describe, expect } from "bun:test" import path from "path" diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index 1eaa0cfc8..490b3f200 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Agent } from "../../src/agent/agent" import { Config } from "../../src/config" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Instance } from "../../src/project/instance" import { Session } from "../../src/session" import { MessageV2 } from "../../src/session/message-v2" diff --git a/packages/opencode/test/tool/write.test.ts b/packages/opencode/test/tool/write.test.ts index 706ddb372..e6e383189 100644 --- a/packages/opencode/test/tool/write.test.ts +++ b/packages/opencode/test/tool/write.test.ts @@ -12,7 +12,7 @@ import { Truncate } from "../../src/tool" import { Tool } from "../../src/tool" import { Agent } from "../../src/agent/agent" import { SessionID, MessageID } from "../../src/session/schema" -import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" From f5dce6d960ea44d3a77cfe868b8209b44a866edb Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 25 Apr 2026 14:36:15 -0400 Subject: [PATCH 27/45] core: move npm service to core package for shared dependency management --- bun.lock | 28 ++++++------ packages/core/package.json | 9 +++- .../src/npm/index.ts => core/src/npm.ts} | 16 ++++--- packages/opencode/package.json | 3 -- .../opencode/src/cli/cmd/tui/config/tui.ts | 2 +- packages/opencode/src/cli/cmd/tui/layer.ts | 2 +- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/effect/app-runtime.ts | 2 +- packages/opencode/src/format/formatter.ts | 2 +- packages/opencode/src/lsp/server.ts | 2 +- packages/opencode/src/npm/config.ts | 0 packages/opencode/src/npmcli-config.d.ts | 43 ------------------- packages/opencode/src/plugin/shared.ts | 2 +- packages/opencode/src/provider/provider.ts | 2 +- .../cli/tui/plugin-loader-entrypoint.test.ts | 2 +- packages/opencode/test/config/config.test.ts | 2 +- packages/opencode/test/npm.test.ts | 2 +- .../test/plugin/loader-shared.test.ts | 2 +- 18 files changed, 42 insertions(+), 81 deletions(-) rename packages/{opencode/src/npm/index.ts => core/src/npm.ts} (95%) delete mode 100644 packages/opencode/src/npm/config.ts delete mode 100644 packages/opencode/src/npmcli-config.d.ts diff --git a/bun.lock b/bun.lock index 06a414a21..1d2e4462f 100644 --- a/bun.lock +++ b/bun.lock @@ -199,6 +199,8 @@ "dependencies": { "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", + "@npmcli/arborist": "9.4.0", + "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", @@ -208,6 +210,8 @@ "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", + "npm-package-arg": "13.0.2", + "semver": "^7.6.3", "xdg-basedir": "5.1.0", "zod": "catalog:", }, @@ -215,6 +219,9 @@ "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@types/cross-spawn": "catalog:", + "@types/npm-package-arg": "6.1.4", + "@types/npmcli__arborist": "6.3.3", + "@types/semver": "catalog:", }, }, "packages/desktop": { @@ -380,8 +387,6 @@ "@hono/zod-validator": "catalog:", "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.27.1", - "@npmcli/arborist": "9.4.0", - "@npmcli/config": "10.8.1", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", @@ -470,7 +475,6 @@ "@types/cross-spawn": "catalog:", "@types/mime-types": "3.0.1", "@types/npm-package-arg": "6.1.4", - "@types/npmcli__arborist": "6.3.3", "@types/semver": "^7.5.8", "@types/turndown": "5.0.5", "@types/which": "3.0.4", @@ -2847,7 +2851,7 @@ "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], - "common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], + "common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], "compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="], @@ -4005,7 +4009,7 @@ "native-duplexpair": ["native-duplexpair@1.0.0", "", {}, "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA=="], - "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], @@ -5515,8 +5519,6 @@ "@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - "@npmcli/arborist/common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], - "@npmcli/query/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], "@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], @@ -5709,6 +5711,8 @@ "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="], "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="], @@ -5739,6 +5743,8 @@ "astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], + "astro/common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], + "astro/diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="], "astro/unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], @@ -5893,8 +5899,6 @@ "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "make-fetch-happen/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="], @@ -6881,8 +6885,6 @@ "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="], - "@electron/rebuild/node-gyp/make-fetch-happen/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "@electron/rebuild/node-gyp/make-fetch-happen/ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="], "@electron/rebuild/node-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], @@ -6947,8 +6949,6 @@ "@jsx-email/cli/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], @@ -7147,8 +7147,6 @@ "js-beautify/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "opencontrol/@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "opencontrol/@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], diff --git a/packages/core/package.json b/packages/core/package.json index bd826de35..62d56908c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -19,11 +19,16 @@ "devDependencies": { "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", - "@types/cross-spawn": "catalog:" + "@types/cross-spawn": "catalog:", + "@types/npm-package-arg": "6.1.4", + "@types/npmcli__arborist": "6.3.3", + "@types/semver": "catalog:" }, "dependencies": { "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", + "@npmcli/arborist": "9.4.0", + "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", @@ -33,6 +38,8 @@ "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", + "npm-package-arg": "13.0.2", + "semver": "^7.6.3", "xdg-basedir": "5.1.0", "zod": "catalog:" }, diff --git a/packages/opencode/src/npm/index.ts b/packages/core/src/npm.ts similarity index 95% rename from packages/opencode/src/npm/index.ts rename to packages/core/src/npm.ts index a7538b9c3..a52e0a9a5 100644 --- a/packages/opencode/src/npm/index.ts +++ b/packages/core/src/npm.ts @@ -1,20 +1,22 @@ -export * as Npm from "." +export * as Npm from "./npm" import path from "path" import { fileURLToPath } from "url" import npa from "npm-package-arg" import semver from "semver" +// @ts-expect-error npm does not publish types for this internal config API. import Config from "@npmcli/config" +// @ts-expect-error npm does not publish types for this internal config API. import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js" import { Effect, Schema, Context, Layer, Option, FileSystem, Stream } from "effect" 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 { AppFileSystem } from "./filesystem" +import { Global } from "./global" +import { EffectFlock } from "./util/effect-flock" +import { makeRuntime } from "./effect/runtime" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { CrossSpawnSpawner } from "./cross-spawn-spawner" export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", { add: Schema.Array(Schema.String).pipe(Schema.optional), @@ -45,7 +47,7 @@ export interface Interface { export class Service extends Context.Service()("@opencode/Npm") {} const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined -const npmPath = fileURLToPath(new URL("../..", import.meta.url)) +const npmPath = fileURLToPath(new URL("..", import.meta.url)) export function sanitize(pkg: string) { if (!illegal) return pkg diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 1c60e58a8..98a707f4b 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -61,7 +61,6 @@ "@types/cross-spawn": "catalog:", "@types/mime-types": "3.0.1", "@types/npm-package-arg": "6.1.4", - "@types/npmcli__arborist": "6.3.3", "@types/semver": "^7.5.8", "@types/turndown": "5.0.5", "@types/which": "3.0.4", @@ -110,8 +109,6 @@ "@hono/zod-validator": "catalog:", "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.27.1", - "@npmcli/arborist": "9.4.0", - "@npmcli/config": "10.8.1", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index b55e5807e..3f99e6d2f 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -18,7 +18,7 @@ import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/instal import { makeRuntime } from "@opencode-ai/core/effect/runtime" import { Filesystem, Log } from "@/util" import { ConfigVariable } from "@/config/variable" -import { Npm } from "@/npm" +import { Npm } from "@opencode-ai/core/npm" const log = Log.create({ service: "tui.config" }) diff --git a/packages/opencode/src/cli/cmd/tui/layer.ts b/packages/opencode/src/cli/cmd/tui/layer.ts index 785455334..a0c19e46d 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 { Npm } from "@opencode-ai/core/npm" 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/config/config.ts b/packages/opencode/src/config/config.ts index ddd31a3fc..6173b2fb6 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -42,7 +42,7 @@ import { ConfigProvider } from "./provider" import { ConfigServer } from "./server" import { ConfigSkills } from "./skills" import { ConfigVariable } from "./variable" -import { Npm } from "@/npm" +import { Npm } from "@opencode-ai/core/npm" const log = Log.create({ service: "config" }) diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index b4bdbfca4..fcf64b9d2 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -46,7 +46,7 @@ import { Pty } from "@/pty" import { Installation } from "@/installation" import { ShareNext } from "@/share" import { SessionShare } from "@/share" -import { Npm } from "@/npm" +import { Npm } from "@opencode-ai/core/npm" import { memoMap } from "@opencode-ai/core/effect/memo-map" export const AppLayer = Layer.mergeAll( diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index eefafd575..82666b799 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -1,4 +1,4 @@ -import { Npm } from "../npm" +import { Npm } from "@opencode-ai/core/npm" import type { InstanceContext } from "../project/instance" import { Filesystem } from "../util" import { Process } from "../util" diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 14b674a98..32a5239be 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -13,7 +13,7 @@ import { Process } from "../util" import { which } from "../util/which" import { Module } from "@opencode-ai/core/util/module" import { spawn } from "./launch" -import { Npm } from "../npm" +import { Npm } from "@opencode-ai/core/npm" const log = Log.create({ service: "lsp.server" }) const pathExists = async (p: string) => diff --git a/packages/opencode/src/npm/config.ts b/packages/opencode/src/npm/config.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/opencode/src/npmcli-config.d.ts b/packages/opencode/src/npmcli-config.d.ts deleted file mode 100644 index c9b20517a..000000000 --- a/packages/opencode/src/npmcli-config.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -declare module "@npmcli/config" { - type Data = Record - type Where = "default" | "builtin" | "global" | "user" | "project" | "env" | "cli" - - namespace Config { - interface Options { - definitions: Data - shorthands: Record - npmPath: string - flatten?: (input: Data, flat?: Data) => Data - nerfDarts?: string[] - argv?: string[] - cwd?: string - env?: NodeJS.ProcessEnv - execPath?: string - platform?: NodeJS.Platform - warn?: boolean - } - } - - class Config { - constructor(input: Config.Options) - - readonly data: Map - readonly flat: Data - - load(): Promise - } - - export = Config -} - -declare module "@npmcli/config/lib/definitions" { - export const definitions: Record - export const shorthands: Record - export const flatten: (input: Record, flat?: Record) => Record - export const nerfDarts: string[] - export const proxyEnv: string[] -} - -declare module "@npmcli/config/lib/definitions/index.js" { - export * from "@npmcli/config/lib/definitions" -} diff --git a/packages/opencode/src/plugin/shared.ts b/packages/opencode/src/plugin/shared.ts index ca821216d..a930d5b26 100644 --- a/packages/opencode/src/plugin/shared.ts +++ b/packages/opencode/src/plugin/shared.ts @@ -4,7 +4,7 @@ import npa from "npm-package-arg" import semver from "semver" import { Filesystem } from "@/util" import { isRecord } from "@/util/record" -import { Npm } from "@/npm" +import { Npm } from "@opencode-ai/core/npm" // Old npm package names for plugins that are now built-in export const DEPRECATED_PLUGIN_PACKAGES = ["opencode-openai-codex-auth", "opencode-copilot-auth"] diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index c2439d04f..9aa1b6304 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -4,7 +4,7 @@ import { Config } from "../config" import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda" import { NoSuchModelError, type Provider as SDK } from "ai" import { Log } from "../util" -import { Npm } from "../npm" +import { Npm } from "@opencode-ai/core/npm" import { Hash } from "@opencode-ai/core/util/hash" import { Plugin } from "../plugin" import { type LanguageModelV3 } from "@ai-sdk/provider" diff --git a/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts b/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts index 74236afae..66858e2d0 100644 --- a/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts @@ -5,7 +5,7 @@ import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui" -import { Npm } from "../../../src/npm" +import { Npm } from "@opencode-ai/core/npm" const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index f6fb04ea0..324914e6d 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -27,7 +27,7 @@ import { Global } from "@opencode-ai/core/global" import { ProjectID } from "../../src/project/schema" import { Filesystem } from "../../src/util" import { ConfigPlugin } from "@/config/plugin" -import { Npm } from "@/npm" +import { Npm } from "@opencode-ai/core/npm" const emptyAccount = Layer.mock(Account.Service)({ active: () => Effect.succeed(Option.none()), diff --git a/packages/opencode/test/npm.test.ts b/packages/opencode/test/npm.test.ts index 09fa6b351..d5b93a83c 100644 --- a/packages/opencode/test/npm.test.ts +++ b/packages/opencode/test/npm.test.ts @@ -7,7 +7,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Global } from "@opencode-ai/core/global" import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import { Npm } from "../src/npm" +import { Npm } from "@opencode-ai/core/npm" import { tmpdir } from "./fixture/fixture" const win = process.platform === "win32" diff --git a/packages/opencode/test/plugin/loader-shared.test.ts b/packages/opencode/test/plugin/loader-shared.test.ts index 83e9d71b4..88b3bf343 100644 --- a/packages/opencode/test/plugin/loader-shared.test.ts +++ b/packages/opencode/test/plugin/loader-shared.test.ts @@ -13,7 +13,7 @@ const { Plugin } = await import("../../src/plugin/index") const { PluginLoader } = await import("../../src/plugin/loader") const { readPackageThemes } = await import("../../src/plugin/shared") const { Instance } = await import("../../src/project/instance") -const { Npm } = await import("../../src/npm") +const { Npm } = await import("@opencode-ai/core/npm") afterAll(() => { if (disableDefault === undefined) { From 95d4bb213018f9c184cc97c34000b3f5a99fae5a Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Apr 2026 14:46:06 -0400 Subject: [PATCH 28/45] feat(httpapi): bridge experimental read endpoints (#24365) --- packages/opencode/specs/effect/http-api.md | 25 ++- packages/opencode/src/mcp/index.ts | 20 +-- .../server/routes/instance/experimental.ts | 2 +- .../routes/instance/httpapi/experimental.ts | 159 ++++++++++++++++++ .../server/routes/instance/httpapi/server.ts | 2 + .../src/server/routes/instance/index.ts | 5 + packages/opencode/src/tool/bash.ts | 4 +- .../test/server/httpapi-experimental.test.ts | 66 ++++++++ 8 files changed, 270 insertions(+), 13 deletions(-) create mode 100644 packages/opencode/src/server/routes/instance/httpapi/experimental.ts create mode 100644 packages/opencode/test/server/httpapi-experimental.test.ts diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 948389223..6d0381d2d 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -30,6 +30,29 @@ Plan for replacing instance Hono route implementations with Effect `HttpApi` whi - Regenerate the SDK after schema or OpenAPI-affecting changes and verify the diff is expected. - Do not delete a Hono route until the SDK/OpenAPI pipeline no longer depends on its Hono `describeRoute` entry. +## Route Slice Checklist + +Use this checklist for each small HttpApi migration PR: + +1. Read the legacy Hono route and copy behavior exactly, including default values, headers, operation IDs, response schemas, and status codes. +2. Put the new `HttpApiGroup`, route paths, DTO schemas, and handlers in `src/server/routes/instance/httpapi/*`. +3. Mount the new paths in `src/server/routes/instance/index.ts` only inside the `OPENCODE_EXPERIMENTAL_HTTPAPI` block. +4. Use `InstanceState.context` / `InstanceState.directory` inside HttpApi handlers instead of `Instance.directory`, `Instance.worktree`, or `Instance.project` ALS globals. +5. Reuse existing services directly. If a service returns plain objects, use `Schema.Struct`; use `Schema.Class` only when handlers return actual class instances. +6. Keep legacy Hono routes and `.zod` compatibility in place for SDK/OpenAPI generation. +7. Add tests that hit the Hono-mounted bridge via `InstanceRoutes`, not only the raw `HttpApi` web handler, when the route depends on auth or instance context. +8. Run `bun typecheck` from `packages/opencode`, relevant `bun run test:ci ...` tests from `packages/opencode`, and `./packages/sdk/js/script/build.ts` from the repo root. + +## Experimental Read Slice Guidance + +For the experimental route group, port read-only JSON routes before mutations: + +- Good first batch: `GET /console`, `GET /console/orgs`, `GET /tool/ids`, `GET /resource`. +- Consider `GET /worktree` only if the handler uses `InstanceState.context` instead of `Instance.project`. +- Defer `POST /console/switch`, worktree create/remove/reset, and `GET /session` to separate PRs because they mutate state or have broader pagination/session behavior. +- Preserve response headers such as pagination cursors if a route is ported. +- If SDK generation changes, explain whether it is a semantic contract change or a generator-equivalent type normalization. + ## Schema Notes - Use `Schema.Struct(...).annotate({ identifier })` for named OpenAPI refs when handlers return plain objects. @@ -141,7 +164,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `mcp` | `bridged` partial | status only | | `workspace` | `bridged` | list, get, enter | | top-level instance reads | `bridged` | path, vcs, command, agent, skill, lsp, formatter | -| experimental JSON routes | `next/later` | console, tool, worktree, resource, global session list | +| experimental JSON routes | `bridged` partial | console reads, tool ids, resource list; worktree and global session list remain later | | `session` | `later/special` | large stateful surface plus streaming | | `sync` | `later` | process/control side effects | | `event` | `special` | SSE | diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index c2479372d..533466925 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -36,16 +36,16 @@ import { withStatics } from "@/util/schema" const log = Log.create({ service: "mcp" }) const DEFAULT_TIMEOUT = 30_000 -export const Resource = z - .object({ - name: z.string(), - uri: z.string(), - description: z.string().optional(), - mimeType: z.string().optional(), - client: z.string(), - }) - .meta({ ref: "McpResource" }) -export type Resource = z.infer +export const Resource = Schema.Struct({ + name: Schema.String, + uri: Schema.String, + description: Schema.optional(Schema.String), + mimeType: Schema.optional(Schema.String), + client: Schema.String, +}) + .annotate({ identifier: "McpResource" }) + .pipe(withStatics((s) => ({ zod: effectZod(s) }))) +export type Resource = Schema.Schema.Type export const ToolsChanged = BusEvent.define( "mcp.tools.changed", diff --git a/packages/opencode/src/server/routes/instance/experimental.ts b/packages/opencode/src/server/routes/instance/experimental.ts index f13003cb4..0936f6252 100644 --- a/packages/opencode/src/server/routes/instance/experimental.ts +++ b/packages/opencode/src/server/routes/instance/experimental.ts @@ -394,7 +394,7 @@ export const ExperimentalRoutes = lazy(() => description: "MCP resources", content: { "application/json": { - schema: resolver(z.record(z.string(), MCP.Resource)), + schema: resolver(z.record(z.string(), MCP.Resource.zod)), }, }, }, diff --git a/packages/opencode/src/server/routes/instance/httpapi/experimental.ts b/packages/opencode/src/server/routes/instance/httpapi/experimental.ts new file mode 100644 index 000000000..993971202 --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/experimental.ts @@ -0,0 +1,159 @@ +import { Account } from "@/account/account" +import { Config } from "@/config" +import { MCP } from "@/mcp" +import { ToolRegistry } from "@/tool" +import { Effect, Layer, Option, Schema } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" + +const ConsoleStateResponse = Schema.Struct({ + consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)), + activeOrgName: Schema.optionalKey(Schema.String), + switchableOrgCount: Schema.Number, +}).annotate({ identifier: "ConsoleState" }) + +const ConsoleOrgOption = Schema.Struct({ + accountID: Schema.String, + accountEmail: Schema.String, + accountUrl: Schema.String, + orgID: Schema.String, + orgName: Schema.String, + active: Schema.Boolean, +}).annotate({ identifier: "ConsoleOrgOption" }) + +const ConsoleOrgList = Schema.Struct({ + orgs: Schema.Array(ConsoleOrgOption), +}).annotate({ identifier: "ConsoleOrgList" }) + +const ToolIDs = Schema.Array(Schema.String).annotate({ identifier: "ToolIDs" }) + +export const ExperimentalPaths = { + console: "/experimental/console", + consoleOrgs: "/experimental/console/orgs", + toolIDs: "/experimental/tool/ids", + resource: "/experimental/resource", +} as const + +export const ExperimentalApi = HttpApi.make("experimental") + .add( + HttpApiGroup.make("experimental") + .add( + HttpApiEndpoint.get("console", ExperimentalPaths.console, { + success: ConsoleStateResponse, + }).annotateMerge( + OpenApi.annotations({ + identifier: "experimental.console.get", + summary: "Get active Console provider metadata", + description: "Get the active Console org name and the set of provider IDs managed by that Console org.", + }), + ), + HttpApiEndpoint.get("consoleOrgs", ExperimentalPaths.consoleOrgs, { + success: ConsoleOrgList, + }).annotateMerge( + OpenApi.annotations({ + identifier: "experimental.console.listOrgs", + summary: "List switchable Console orgs", + description: "Get the available Console orgs across logged-in accounts, including the current active org.", + }), + ), + HttpApiEndpoint.get("toolIDs", ExperimentalPaths.toolIDs, { + success: ToolIDs, + }).annotateMerge( + OpenApi.annotations({ + identifier: "tool.ids", + summary: "List tool IDs", + description: + "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.", + }), + ), + HttpApiEndpoint.get("resource", ExperimentalPaths.resource, { + success: Schema.Record(Schema.String, MCP.Resource), + }).annotateMerge( + OpenApi.annotations({ + identifier: "experimental.resource.list", + summary: "Get MCP resources", + description: "Get all available MCP resources from connected servers. Optionally filter by name.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "experimental", + description: "Experimental HttpApi read-only routes.", + }), + ) + .middleware(Authorization), + ) + .annotateMerge( + OpenApi.annotations({ + title: "opencode experimental HttpApi", + version: "0.0.1", + description: "Experimental HttpApi surface for selected instance routes.", + }), + ) + +export const experimentalHandlers = Layer.unwrap( + Effect.gen(function* () { + const account = yield* Account.Service + const config = yield* Config.Service + const mcp = yield* MCP.Service + const registry = yield* ToolRegistry.Service + + const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () { + const [state, groups] = yield* Effect.all( + [config.getConsoleState(), account.orgsByAccount().pipe(Effect.orDie)], + { + concurrency: "unbounded", + }, + ) + return { + consoleManagedProviders: state.consoleManagedProviders, + ...(state.activeOrgName ? { activeOrgName: state.activeOrgName } : {}), + switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0), + } + }) + + const listConsoleOrgs = Effect.fn("ExperimentalHttpApi.consoleOrgs")(function* () { + const [groups, active] = yield* Effect.all( + [account.orgsByAccount().pipe(Effect.orDie), account.active().pipe(Effect.orDie)], + { + concurrency: "unbounded", + }, + ) + const info = Option.getOrUndefined(active) + return { + orgs: groups.flatMap((group) => + group.orgs.map((org) => ({ + accountID: group.account.id, + accountEmail: group.account.email, + accountUrl: group.account.url, + orgID: org.id, + orgName: org.name, + active: !!info && info.id === group.account.id && info.active_org_id === org.id, + })), + ), + } + }) + + const toolIDs = Effect.fn("ExperimentalHttpApi.toolIDs")(function* () { + return yield* registry.ids() + }) + + const resource = Effect.fn("ExperimentalHttpApi.resource")(function* () { + return yield* mcp.resources() + }) + + return HttpApiBuilder.group(ExperimentalApi, "experimental", (handlers) => + handlers + .handle("console", getConsole) + .handle("consoleOrgs", listConsoleOrgs) + .handle("toolIDs", toolIDs) + .handle("resource", resource), + ) + }), +).pipe( + Layer.provide(Account.defaultLayer), + Layer.provide(Config.defaultLayer), + Layer.provide(MCP.defaultLayer), + Layer.provide(ToolRegistry.defaultLayer), +) diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index 17c3ba4b4..77a2832ce 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -11,6 +11,7 @@ import { Filesystem } from "@/util" import { authorizationLayer } from "./auth" import { ConfigApi, configHandlers } from "./config" import { FileApi, fileHandlers } from "./file" +import { ExperimentalApi, experimentalHandlers } from "./experimental" import { InstanceApi, instanceHandlers } from "./instance" import { McpApi, mcpHandlers } from "./mcp" import { PermissionApi, permissionHandlers } from "./permission" @@ -63,6 +64,7 @@ const instance = HttpRouter.middleware()( export const routes = Layer.mergeAll( HttpApiBuilder.layer(ConfigApi).pipe(Layer.provide(configHandlers)), + HttpApiBuilder.layer(ExperimentalApi).pipe(Layer.provide(experimentalHandlers)), HttpApiBuilder.layer(FileApi).pipe(Layer.provide(fileHandlers)), HttpApiBuilder.layer(InstanceApi).pipe(Layer.provide(instanceHandlers)), HttpApiBuilder.layer(McpApi).pipe(Layer.provide(mcpHandlers)), diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index 488e43542..65dd41705 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -16,6 +16,7 @@ import { QuestionRoutes } from "./question" import { PermissionRoutes } from "./permission" import { Flag } from "@opencode-ai/core/flag/flag" import { ExperimentalHttpApiServer } from "./httpapi/server" +import { ExperimentalPaths } from "./httpapi/experimental" import { FilePaths } from "./httpapi/file" import { InstancePaths } from "./httpapi/instance" import { McpPaths } from "./httpapi/mcp" @@ -45,6 +46,10 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.post("/permission/:requestID/reply", (c) => handler(c.req.raw, context)) app.get("/config", (c) => handler(c.req.raw, context)) app.get("/config/providers", (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.console, (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.consoleOrgs, (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.toolIDs, (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.resource, (c) => handler(c.req.raw, context)) app.get("/provider", (c) => handler(c.req.raw, context)) app.get("/provider/auth", (c) => handler(c.req.raw, context)) app.post("/provider/:providerID/oauth/authorize", (c) => handler(c.req.raw, context)) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index eeba5ebd6..a27d7c5ec 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -20,6 +20,7 @@ import { Plugin } from "@/plugin" import { Effect, Stream } from "effect" import { ChildProcess } from "effect/unstable/process" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" +import { InstanceState } from "@/effect" const MAX_METADATA_LENGTH = 30_000 const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000 @@ -575,9 +576,10 @@ export const BashTool = Tool.define( log.info("bash tool using shell", { shell }) const limits = yield* trunc.limits() + const instance = yield* InstanceState.context return { - description: DESCRIPTION.replaceAll("${directory}", Instance.directory) + description: DESCRIPTION.replaceAll("${directory}", instance.directory) .replaceAll("${os}", process.platform) .replaceAll("${shell}", name) .replaceAll("${chaining}", chain) diff --git a/packages/opencode/test/server/httpapi-experimental.test.ts b/packages/opencode/test/server/httpapi-experimental.test.ts new file mode 100644 index 000000000..00d1fefb8 --- /dev/null +++ b/packages/opencode/test/server/httpapi-experimental.test.ts @@ -0,0 +1,66 @@ +import { afterEach, describe, expect, test } from "bun:test" +import type { UpgradeWebSocket } from "hono/ws" +import { Flag } from "@opencode-ai/core/flag/flag" +import { Instance } from "../../src/project/instance" +import { InstanceRoutes } from "../../src/server/routes/instance" +import { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/experimental" +import { Log } from "../../src/util" +import { resetDatabase } from "../fixture/db" +import { tmpdir } from "../fixture/fixture" + +void Log.init({ print: false }) + +const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI +const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket + +function app() { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true + return InstanceRoutes(websocket) +} + +afterEach(async () => { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original + await Instance.disposeAll() + await resetDatabase() +}) + +describe("experimental HttpApi", () => { + test("serves read-only experimental endpoints through Hono bridge", async () => { + await using tmp = await tmpdir({ + config: { + formatter: false, + lsp: false, + mcp: { + demo: { + type: "local", + command: ["echo", "demo"], + enabled: false, + }, + }, + }, + }) + + const headers = { "x-opencode-directory": tmp.path } + const [consoleState, consoleOrgs, toolIDs, resources] = await Promise.all([ + app().request(ExperimentalPaths.console, { headers }), + app().request(ExperimentalPaths.consoleOrgs, { headers }), + app().request(ExperimentalPaths.toolIDs, { headers }), + app().request(ExperimentalPaths.resource, { headers }), + ]) + + expect(consoleState.status).toBe(200) + expect(await consoleState.json()).toEqual({ + consoleManagedProviders: [], + switchableOrgCount: 0, + }) + + expect(consoleOrgs.status).toBe(200) + expect(await consoleOrgs.json()).toEqual({ orgs: [] }) + + expect(toolIDs.status).toBe(200) + expect(await toolIDs.json()).toContain("bash") + + expect(resources.status).toBe(200) + expect(await resources.json()).toEqual({}) + }) +}) From 3b74077437f15d6ea3130ad2255295157a934f2f Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Apr 2026 18:47:06 +0000 Subject: [PATCH 29/45] chore: generate --- packages/opencode/specs/effect/http-api.md | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 6d0381d2d..e32a1dd7e 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -153,23 +153,23 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho ## Current Route Status -| Area | Status | Notes | -| ------------------------ | ----------------- | ------------------------------------------------------ | -| `question` | `bridged` | `GET /question`, reply, reject | -| `permission` | `bridged` | list and reply | -| `provider` | `bridged` | list, auth, OAuth authorize/callback | -| `config` | `bridged` partial | reads only; mutation remains Hono | -| `project` | `bridged` partial | reads only; git-init remains Hono | -| `file` | `bridged` partial | find text/file/symbol, list/content/status | -| `mcp` | `bridged` partial | status only | -| `workspace` | `bridged` | list, get, enter | -| top-level instance reads | `bridged` | path, vcs, command, agent, skill, lsp, formatter | +| Area | Status | Notes | +| ------------------------ | ----------------- | ------------------------------------------------------------------------------------- | +| `question` | `bridged` | `GET /question`, reply, reject | +| `permission` | `bridged` | list and reply | +| `provider` | `bridged` | list, auth, OAuth authorize/callback | +| `config` | `bridged` partial | reads only; mutation remains Hono | +| `project` | `bridged` partial | reads only; git-init remains Hono | +| `file` | `bridged` partial | find text/file/symbol, list/content/status | +| `mcp` | `bridged` partial | status only | +| `workspace` | `bridged` | list, get, enter | +| top-level instance reads | `bridged` | path, vcs, command, agent, skill, lsp, formatter | | experimental JSON routes | `bridged` partial | console reads, tool ids, resource list; worktree and global session list remain later | -| `session` | `later/special` | large stateful surface plus streaming | -| `sync` | `later` | process/control side effects | -| `event` | `special` | SSE | -| `pty` | `special` | websocket | -| `tui` | `special` | UI bridge | +| `session` | `later/special` | large stateful surface plus streaming | +| `sync` | `later` | process/control side effects | +| `event` | `special` | SSE | +| `pty` | `special` | websocket | +| `tui` | `special` | UI bridge | ## Next PRs From 60fa708f0b4490c49cddd518f9dd7b7471181379 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Apr 2026 18:49:27 +0000 Subject: [PATCH 30/45] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 037e9c57e..84fb57a1c 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-0w22pAViYEcELJpOrIVCjTQ73fnsSaJxb75heAIUdYE=", - "aarch64-linux": "sha256-z1fpZ9HQU9n6W5xhKzuUduwQUJa/nrj9WFZdBLL/e/8=", - "aarch64-darwin": "sha256-y5AraTdY2uDTltjQFlHjMoMo6FICgQNKSunIOnQAXnY=", - "x86_64-darwin": "sha256-KQF6dJNQ587xp5h9ET+tLni9dLNwYnzxg2DX+KWfpoE=" + "x86_64-linux": "sha256-LpzWEZzURUEj7fcHGvh33gM7D9GNPE+XIvU0/hmdcQM=", + "aarch64-linux": "sha256-0zdO3zuj6g9cMZFEOsvQJcKKcPjGVZJ2DkJdDcb2VCM=", + "aarch64-darwin": "sha256-dmT8R9Pmzh5tjO8NCCCtENiQpJQeifQpVdhaty1MXOs=", + "x86_64-darwin": "sha256-Q6rAQRoC6WaMAQl++YHAZmbNuO303cWgGaYzXaRlzy4=" } } From b749866f0b21328919e0c525109c5b2f3e4e0e24 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Apr 2026 14:55:29 -0400 Subject: [PATCH 31/45] feat(httpapi): bridge worktree read endpoint (#24366) --- packages/opencode/specs/effect/http-api.md | 2 +- .../routes/instance/httpapi/experimental.ts | 22 +++++++++++++++++++ .../src/server/routes/instance/index.ts | 1 + .../test/server/httpapi-experimental.test.ts | 6 ++++- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index e32a1dd7e..3e4f30d02 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -164,7 +164,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `mcp` | `bridged` partial | status only | | `workspace` | `bridged` | list, get, enter | | top-level instance reads | `bridged` | path, vcs, command, agent, skill, lsp, formatter | -| experimental JSON routes | `bridged` partial | console reads, tool ids, resource list; worktree and global session list remain later | +| experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list, resource list; global session list remains later | | `session` | `later/special` | large stateful surface plus streaming | | `sync` | `later` | process/control side effects | | `event` | `special` | SSE | diff --git a/packages/opencode/src/server/routes/instance/httpapi/experimental.ts b/packages/opencode/src/server/routes/instance/httpapi/experimental.ts index 993971202..4bd9ba30a 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/experimental.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/experimental.ts @@ -1,6 +1,8 @@ import { Account } from "@/account/account" import { Config } from "@/config" +import { InstanceState } from "@/effect" import { MCP } from "@/mcp" +import { Project } from "@/project" import { ToolRegistry } from "@/tool" import { Effect, Layer, Option, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" @@ -27,10 +29,13 @@ const ConsoleOrgList = Schema.Struct({ const ToolIDs = Schema.Array(Schema.String).annotate({ identifier: "ToolIDs" }) +const WorktreeList = Schema.Array(Schema.String).annotate({ identifier: "WorktreeList" }) + export const ExperimentalPaths = { console: "/experimental/console", consoleOrgs: "/experimental/console/orgs", toolIDs: "/experimental/tool/ids", + worktree: "/experimental/worktree", resource: "/experimental/resource", } as const @@ -66,6 +71,15 @@ export const ExperimentalApi = HttpApi.make("experimental") "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.", }), ), + HttpApiEndpoint.get("worktree", ExperimentalPaths.worktree, { + success: WorktreeList, + }).annotateMerge( + OpenApi.annotations({ + identifier: "worktree.list", + summary: "List worktrees", + description: "List all sandbox worktrees for the current project.", + }), + ), HttpApiEndpoint.get("resource", ExperimentalPaths.resource, { success: Schema.Record(Schema.String, MCP.Resource), }).annotateMerge( @@ -97,6 +111,7 @@ export const experimentalHandlers = Layer.unwrap( const account = yield* Account.Service const config = yield* Config.Service const mcp = yield* MCP.Service + const project = yield* Project.Service const registry = yield* ToolRegistry.Service const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () { @@ -139,6 +154,11 @@ export const experimentalHandlers = Layer.unwrap( return yield* registry.ids() }) + const worktree = Effect.fn("ExperimentalHttpApi.worktree")(function* () { + const ctx = yield* InstanceState.context + return yield* project.sandboxes(ctx.project.id) + }) + const resource = Effect.fn("ExperimentalHttpApi.resource")(function* () { return yield* mcp.resources() }) @@ -148,6 +168,7 @@ export const experimentalHandlers = Layer.unwrap( .handle("console", getConsole) .handle("consoleOrgs", listConsoleOrgs) .handle("toolIDs", toolIDs) + .handle("worktree", worktree) .handle("resource", resource), ) }), @@ -155,5 +176,6 @@ export const experimentalHandlers = Layer.unwrap( Layer.provide(Account.defaultLayer), Layer.provide(Config.defaultLayer), Layer.provide(MCP.defaultLayer), + Layer.provide(Project.defaultLayer), Layer.provide(ToolRegistry.defaultLayer), ) diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index 65dd41705..8e4c497bd 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -49,6 +49,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.get(ExperimentalPaths.console, (c) => handler(c.req.raw, context)) app.get(ExperimentalPaths.consoleOrgs, (c) => handler(c.req.raw, context)) app.get(ExperimentalPaths.toolIDs, (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context)) app.get(ExperimentalPaths.resource, (c) => handler(c.req.raw, context)) app.get("/provider", (c) => handler(c.req.raw, context)) app.get("/provider/auth", (c) => handler(c.req.raw, context)) diff --git a/packages/opencode/test/server/httpapi-experimental.test.ts b/packages/opencode/test/server/httpapi-experimental.test.ts index 00d1fefb8..38f43e68f 100644 --- a/packages/opencode/test/server/httpapi-experimental.test.ts +++ b/packages/opencode/test/server/httpapi-experimental.test.ts @@ -41,10 +41,11 @@ describe("experimental HttpApi", () => { }) const headers = { "x-opencode-directory": tmp.path } - const [consoleState, consoleOrgs, toolIDs, resources] = await Promise.all([ + const [consoleState, consoleOrgs, toolIDs, worktrees, resources] = await Promise.all([ app().request(ExperimentalPaths.console, { headers }), app().request(ExperimentalPaths.consoleOrgs, { headers }), app().request(ExperimentalPaths.toolIDs, { headers }), + app().request(ExperimentalPaths.worktree, { headers }), app().request(ExperimentalPaths.resource, { headers }), ]) @@ -60,6 +61,9 @@ describe("experimental HttpApi", () => { expect(toolIDs.status).toBe(200) expect(await toolIDs.json()).toContain("bash") + expect(worktrees.status).toBe(200) + expect(await worktrees.json()).toEqual([]) + expect(resources.status).toBe(200) expect(await resources.json()).toEqual({}) }) From 9af46df5356cac29ee3987dd85113e7a59222b08 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Apr 2026 18:56:31 +0000 Subject: [PATCH 32/45] chore: generate --- packages/opencode/specs/effect/http-api.md | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 3e4f30d02..a8f9f1e0a 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -153,23 +153,23 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho ## Current Route Status -| Area | Status | Notes | -| ------------------------ | ----------------- | ------------------------------------------------------------------------------------- | -| `question` | `bridged` | `GET /question`, reply, reject | -| `permission` | `bridged` | list and reply | -| `provider` | `bridged` | list, auth, OAuth authorize/callback | -| `config` | `bridged` partial | reads only; mutation remains Hono | -| `project` | `bridged` partial | reads only; git-init remains Hono | -| `file` | `bridged` partial | find text/file/symbol, list/content/status | -| `mcp` | `bridged` partial | status only | -| `workspace` | `bridged` | list, get, enter | -| top-level instance reads | `bridged` | path, vcs, command, agent, skill, lsp, formatter | +| Area | Status | Notes | +| ------------------------ | ----------------- | ---------------------------------------------------------------------------------------- | +| `question` | `bridged` | `GET /question`, reply, reject | +| `permission` | `bridged` | list and reply | +| `provider` | `bridged` | list, auth, OAuth authorize/callback | +| `config` | `bridged` partial | reads only; mutation remains Hono | +| `project` | `bridged` partial | reads only; git-init remains Hono | +| `file` | `bridged` partial | find text/file/symbol, list/content/status | +| `mcp` | `bridged` partial | status only | +| `workspace` | `bridged` | list, get, enter | +| top-level instance reads | `bridged` | path, vcs, command, agent, skill, lsp, formatter | | experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list, resource list; global session list remains later | -| `session` | `later/special` | large stateful surface plus streaming | -| `sync` | `later` | process/control side effects | -| `event` | `special` | SSE | -| `pty` | `special` | websocket | -| `tui` | `special` | UI bridge | +| `session` | `later/special` | large stateful surface plus streaming | +| `sync` | `later` | process/control side effects | +| `event` | `special` | SSE | +| `pty` | `special` | websocket | +| `tui` | `special` | UI bridge | ## Next PRs From cd64b670388f45dfddad7fe543ab9c0ff490b81c Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Apr 2026 15:01:41 -0400 Subject: [PATCH 33/45] feat(tui): show /connect tip when user has no models configured (#24014) --- .../cli/cmd/tui/feature-plugins/home/tips-view.tsx | 11 +++++++---- .../src/cli/cmd/tui/feature-plugins/home/tips.tsx | 13 +++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx index 1a9d907bb..c7a7b211f 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx @@ -1,4 +1,4 @@ -import { For } from "solid-js" +import { createMemo, For } from "solid-js" import { DEFAULT_THEMES, useTheme } from "@tui/context/theme" const themeCount = Object.keys(DEFAULT_THEMES).length @@ -30,9 +30,12 @@ function parse(tip: string): TipPart[] { return parts } -export function Tips() { +const NO_MODELS_TIP = "Run {highlight}/connect{/highlight} to add an AI provider and start coding" + +export function Tips(props: { connected?: boolean }) { const theme = useTheme().theme - const parts = parse(TIPS[Math.floor(Math.random() * TIPS.length)]) + const randomTip = TIPS[Math.floor(Math.random() * TIPS.length)] + const parts = createMemo(() => parse(props.connected === false ? NO_MODELS_TIP : randomTip)) return ( @@ -40,7 +43,7 @@ export function Tips() { ● Tip{" "} - + {(part) => {part.text}} diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips.tsx index c0e02f74a..26c03ee34 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips.tsx @@ -4,11 +4,11 @@ import { Tips } from "./tips-view" const id = "internal:home-tips" -function View(props: { show: boolean }) { +function View(props: { show: boolean; connected: boolean }) { return ( - + ) @@ -35,8 +35,13 @@ const tui: TuiPlugin = async (api) => { home_bottom() { const hidden = createMemo(() => api.kv.get("tips_hidden", false)) const first = createMemo(() => api.state.session.count() === 0) - const show = createMemo(() => !first() && !hidden()) - return + const connected = createMemo(() => + api.state.provider.some( + (item) => item.id !== "opencode" || Object.values(item.models).some((model) => model.cost?.input !== 0), + ), + ) + const show = createMemo(() => (!first() || !connected()) && !hidden()) + return }, }, }) From b4f4134e8100bbe37fe6ad0d8bb2520599f88271 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Apr 2026 15:24:07 -0400 Subject: [PATCH 34/45] feat(httpapi): bridge instance dispose endpoint (#24368) --- packages/opencode/specs/effect/http-api.md | 2 +- .../routes/instance/httpapi/instance.ts | 17 +++++++++++++ .../routes/instance/httpapi/lifecycle.ts | 24 +++++++++++++++++++ .../server/routes/instance/httpapi/server.ts | 2 ++ .../src/server/routes/instance/index.ts | 1 + .../test/server/httpapi-instance.test.ts | 23 ++++++++++++++++++ 6 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index a8f9f1e0a..cbd4987cc 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -163,7 +163,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `file` | `bridged` partial | find text/file/symbol, list/content/status | | `mcp` | `bridged` partial | status only | | `workspace` | `bridged` | list, get, enter | -| top-level instance reads | `bridged` | path, vcs, command, agent, skill, lsp, formatter | +| top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose | | experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list, resource list; global session list remains later | | `session` | `later/special` | large stateful surface plus streaming | | `sync` | `later` | process/control side effects | diff --git a/packages/opencode/src/server/routes/instance/httpapi/instance.ts b/packages/opencode/src/server/routes/instance/httpapi/instance.ts index 016703e77..8788dee5f 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/instance.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/instance.ts @@ -9,6 +9,7 @@ import * as InstanceState from "@/effect/instance-state" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "./auth" +import { markInstanceForDisposal } from "./lifecycle" const PathInfo = Schema.Struct({ home: Schema.String, @@ -23,6 +24,7 @@ const VcsDiffQuery = Schema.Struct({ }) export const InstancePaths = { + dispose: "/instance/dispose", path: "/path", vcs: "/vcs", vcsDiff: "/vcs/diff", @@ -37,6 +39,15 @@ export const InstanceApi = HttpApi.make("instance") .add( HttpApiGroup.make("instance") .add( + HttpApiEndpoint.post("dispose", InstancePaths.dispose, { + success: Schema.Boolean, + }).annotateMerge( + OpenApi.annotations({ + identifier: "instance.dispose", + summary: "Dispose instance", + description: "Clean up and dispose the current OpenCode instance, releasing all resources.", + }), + ), HttpApiEndpoint.get("path", InstancePaths.path, { success: PathInfo, }).annotateMerge( @@ -138,6 +149,11 @@ export const instanceHandlers = Layer.unwrap( const skill = yield* Skill.Service const vcs = yield* Vcs.Service + const dispose = Effect.fn("InstanceHttpApi.dispose")(function* () { + yield* markInstanceForDisposal(yield* InstanceState.context) + return true + }) + const getPath = Effect.fn("InstanceHttpApi.path")(function* () { const ctx = yield* InstanceState.context return { @@ -180,6 +196,7 @@ export const instanceHandlers = Layer.unwrap( return HttpApiBuilder.group(InstanceApi, "instance", (handlers) => handlers + .handle("dispose", dispose) .handle("path", getPath) .handle("vcs", getVcs) .handle("vcsDiff", getVcsDiff) diff --git a/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts b/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts new file mode 100644 index 000000000..6b11dffd5 --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts @@ -0,0 +1,24 @@ +import { Instance, type InstanceContext } from "@/project/instance" +import { Effect } from "effect" +import { HttpEffect, HttpMiddleware, HttpServerRequest } from "effect/unstable/http" + +const disposeAfterResponse = new WeakMap() + +export const markInstanceForDisposal = (ctx: InstanceContext) => + HttpEffect.appendPreResponseHandler((request, response) => + Effect.sync(() => { + disposeAfterResponse.set(request.source, ctx) + return response + }), + ) + +export const disposeMiddleware: HttpMiddleware.HttpMiddleware = (effect) => + Effect.gen(function* () { + const response = yield* effect + const request = yield* HttpServerRequest.HttpServerRequest + const ctx = disposeAfterResponse.get(request.source) + if (!ctx) return response + disposeAfterResponse.delete(request.source) + yield* Effect.promise(() => Instance.restore(ctx, () => Instance.dispose())) + return response + }) diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index 77a2832ce..be574c914 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -19,6 +19,7 @@ import { ProjectApi, projectHandlers } from "./project" import { ProviderApi, providerHandlers } from "./provider" import { QuestionApi, questionHandlers } from "./question" import { WorkspaceApi, workspaceHandlers } from "./workspace" +import { disposeMiddleware } from "./lifecycle" import { memoMap } from "@opencode-ai/core/effect/memo-map" const Query = Schema.Struct({ @@ -83,6 +84,7 @@ export const routes = Layer.mergeAll( export const webHandler = lazy(() => HttpRouter.toWebHandler(routes, { memoMap, + middleware: disposeMiddleware, }), ) diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index 8e4c497bd..2a8fc602a 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -64,6 +64,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.get(FilePaths.content, (c) => handler(c.req.raw, context)) app.get(FilePaths.status, (c) => handler(c.req.raw, context)) app.get(InstancePaths.path, (c) => handler(c.req.raw, context)) + app.post(InstancePaths.dispose, (c) => handler(c.req.raw, context)) app.get(InstancePaths.vcs, (c) => handler(c.req.raw, context)) app.get(InstancePaths.vcsDiff, (c) => handler(c.req.raw, context)) app.get(InstancePaths.command, (c) => handler(c.req.raw, context)) diff --git a/packages/opencode/test/server/httpapi-instance.test.ts b/packages/opencode/test/server/httpapi-instance.test.ts index a066e0e92..463dbaa87 100644 --- a/packages/opencode/test/server/httpapi-instance.test.ts +++ b/packages/opencode/test/server/httpapi-instance.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test" import type { UpgradeWebSocket } from "hono/ws" import path from "path" import { Flag } from "@opencode-ai/core/flag/flag" +import { GlobalBus } from "@/bus/global" import { Instance } from "../../src/project/instance" import { InstanceRoutes } from "../../src/server/routes/instance" import { InstancePaths } from "../../src/server/routes/instance/httpapi/instance" @@ -77,4 +78,26 @@ describe("instance HttpApi", () => { expect(formatter.status).toBe(200) expect(await formatter.json()).toEqual([]) }) + + test("serves instance dispose through Hono bridge", async () => { + await using tmp = await tmpdir() + + const disposed = new Promise((resolve) => { + const onEvent = (event: { directory?: string; payload: { type?: string } }) => { + if (event.payload.type !== "server.instance.disposed") return + GlobalBus.off("event", onEvent) + resolve(event.directory) + } + GlobalBus.on("event", onEvent) + }) + + const response = await app().request(InstancePaths.dispose, { + method: "POST", + headers: { "x-opencode-directory": tmp.path }, + }) + + expect(response.status).toBe(200) + expect(await response.json()).toBe(true) + expect(await disposed).toBe(tmp.path) + }) }) From 474024f9e669926b65738a237310c88c3d4adfe3 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Apr 2026 19:25:41 +0000 Subject: [PATCH 35/45] chore: generate --- packages/opencode/specs/effect/http-api.md | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index cbd4987cc..c57debb24 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -153,23 +153,23 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho ## Current Route Status -| Area | Status | Notes | -| ------------------------ | ----------------- | ---------------------------------------------------------------------------------------- | -| `question` | `bridged` | `GET /question`, reply, reject | -| `permission` | `bridged` | list and reply | -| `provider` | `bridged` | list, auth, OAuth authorize/callback | -| `config` | `bridged` partial | reads only; mutation remains Hono | -| `project` | `bridged` partial | reads only; git-init remains Hono | -| `file` | `bridged` partial | find text/file/symbol, list/content/status | -| `mcp` | `bridged` partial | status only | -| `workspace` | `bridged` | list, get, enter | +| Area | Status | Notes | +| ------------------------- | ----------------- | ---------------------------------------------------------------------------------------- | +| `question` | `bridged` | `GET /question`, reply, reject | +| `permission` | `bridged` | list and reply | +| `provider` | `bridged` | list, auth, OAuth authorize/callback | +| `config` | `bridged` partial | reads only; mutation remains Hono | +| `project` | `bridged` partial | reads only; git-init remains Hono | +| `file` | `bridged` partial | find text/file/symbol, list/content/status | +| `mcp` | `bridged` partial | status only | +| `workspace` | `bridged` | list, get, enter | | top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose | -| experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list, resource list; global session list remains later | -| `session` | `later/special` | large stateful surface plus streaming | -| `sync` | `later` | process/control side effects | -| `event` | `special` | SSE | -| `pty` | `special` | websocket | -| `tui` | `special` | UI bridge | +| experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list, resource list; global session list remains later | +| `session` | `later/special` | large stateful surface plus streaming | +| `sync` | `later` | process/control side effects | +| `event` | `special` | SSE | +| `pty` | `special` | websocket | +| `tui` | `special` | UI bridge | ## Next PRs From a36913022609ec90a26037fa3767cdb60eb49597 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Apr 2026 15:35:15 -0400 Subject: [PATCH 36/45] feat(httpapi): bridge worktree mutations (#24371) --- packages/opencode/specs/effect/http-api.md | 2 +- .../server/routes/instance/experimental.ts | 8 +-- .../routes/instance/httpapi/experimental.ts | 59 +++++++++++++++ .../src/server/routes/instance/index.ts | 3 + packages/opencode/src/worktree/index.ts | 72 +++++++++---------- .../test/server/httpapi-experimental.test.ts | 65 +++++++++++++++++ 6 files changed, 164 insertions(+), 45 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index c57debb24..f9e9948cf 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -164,7 +164,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `mcp` | `bridged` partial | status only | | `workspace` | `bridged` | list, get, enter | | top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose | -| experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list, resource list; global session list remains later | +| experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list/mutations, resource list; global session list remains later | | `session` | `later/special` | large stateful surface plus streaming | | `sync` | `later` | process/control side effects | | `event` | `special` | SSE | diff --git a/packages/opencode/src/server/routes/instance/experimental.ts b/packages/opencode/src/server/routes/instance/experimental.ts index 0936f6252..a407590f2 100644 --- a/packages/opencode/src/server/routes/instance/experimental.ts +++ b/packages/opencode/src/server/routes/instance/experimental.ts @@ -230,14 +230,14 @@ export const ExperimentalRoutes = lazy(() => description: "Worktree created", content: { "application/json": { - schema: resolver(Worktree.Info), + schema: resolver(Worktree.Info.zod), }, }, }, ...errors(400), }, }), - validator("json", Worktree.CreateInput.optional()), + validator("json", Worktree.CreateInput.zod.optional()), async (c) => jsonRequest("ExperimentalRoutes.worktree.create", c, function* () { const body = c.req.valid("json") @@ -286,7 +286,7 @@ export const ExperimentalRoutes = lazy(() => ...errors(400), }, }), - validator("json", Worktree.RemoveInput), + validator("json", Worktree.RemoveInput.zod), async (c) => jsonRequest("ExperimentalRoutes.worktree.remove", c, function* () { const body = c.req.valid("json") @@ -315,7 +315,7 @@ export const ExperimentalRoutes = lazy(() => ...errors(400), }, }), - validator("json", Worktree.ResetInput), + validator("json", Worktree.ResetInput.zod), async (c) => jsonRequest("ExperimentalRoutes.worktree.reset", c, function* () { const body = c.req.valid("json") diff --git a/packages/opencode/src/server/routes/instance/httpapi/experimental.ts b/packages/opencode/src/server/routes/instance/httpapi/experimental.ts index 4bd9ba30a..14f54d457 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/experimental.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/experimental.ts @@ -4,6 +4,7 @@ import { InstanceState } from "@/effect" import { MCP } from "@/mcp" import { Project } from "@/project" import { ToolRegistry } from "@/tool" +import { Worktree } from "@/worktree" import { Effect, Layer, Option, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "./auth" @@ -36,6 +37,7 @@ export const ExperimentalPaths = { consoleOrgs: "/experimental/console/orgs", toolIDs: "/experimental/tool/ids", worktree: "/experimental/worktree", + worktreeReset: "/experimental/worktree/reset", resource: "/experimental/resource", } as const @@ -80,6 +82,36 @@ export const ExperimentalApi = HttpApi.make("experimental") description: "List all sandbox worktrees for the current project.", }), ), + HttpApiEndpoint.post("worktreeCreate", ExperimentalPaths.worktree, { + payload: Schema.optional(Worktree.CreateInput), + success: Worktree.Info, + }).annotateMerge( + OpenApi.annotations({ + identifier: "worktree.create", + summary: "Create worktree", + description: "Create a new git worktree for the current project and run any configured startup scripts.", + }), + ), + HttpApiEndpoint.delete("worktreeRemove", ExperimentalPaths.worktree, { + payload: Worktree.RemoveInput, + success: Schema.Boolean, + }).annotateMerge( + OpenApi.annotations({ + identifier: "worktree.remove", + summary: "Remove worktree", + description: "Remove a git worktree and delete its branch.", + }), + ), + HttpApiEndpoint.post("worktreeReset", ExperimentalPaths.worktreeReset, { + payload: Worktree.ResetInput, + success: Schema.Boolean, + }).annotateMerge( + OpenApi.annotations({ + identifier: "worktree.reset", + summary: "Reset worktree", + description: "Reset a worktree branch to the primary default branch.", + }), + ), HttpApiEndpoint.get("resource", ExperimentalPaths.resource, { success: Schema.Record(Schema.String, MCP.Resource), }).annotateMerge( @@ -113,6 +145,7 @@ export const experimentalHandlers = Layer.unwrap( const mcp = yield* MCP.Service const project = yield* Project.Service const registry = yield* ToolRegistry.Service + const worktreeSvc = yield* Worktree.Service const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () { const [state, groups] = yield* Effect.all( @@ -159,6 +192,28 @@ export const experimentalHandlers = Layer.unwrap( return yield* project.sandboxes(ctx.project.id) }) + const worktreeCreate = Effect.fn("ExperimentalHttpApi.worktreeCreate")(function* (ctx: { + payload: Worktree.CreateInput | undefined + }) { + return yield* worktreeSvc.create(ctx.payload) + }) + + const worktreeRemove = Effect.fn("ExperimentalHttpApi.worktreeRemove")(function* (input: { + payload: Worktree.RemoveInput + }) { + const ctx = yield* InstanceState.context + yield* worktreeSvc.remove(input.payload) + yield* project.removeSandbox(ctx.project.id, input.payload.directory) + return true + }) + + const worktreeReset = Effect.fn("ExperimentalHttpApi.worktreeReset")(function* (ctx: { + payload: Worktree.ResetInput + }) { + yield* worktreeSvc.reset(ctx.payload) + return true + }) + const resource = Effect.fn("ExperimentalHttpApi.resource")(function* () { return yield* mcp.resources() }) @@ -169,6 +224,9 @@ export const experimentalHandlers = Layer.unwrap( .handle("consoleOrgs", listConsoleOrgs) .handle("toolIDs", toolIDs) .handle("worktree", worktree) + .handle("worktreeCreate", worktreeCreate) + .handle("worktreeRemove", worktreeRemove) + .handle("worktreeReset", worktreeReset) .handle("resource", resource), ) }), @@ -178,4 +236,5 @@ export const experimentalHandlers = Layer.unwrap( Layer.provide(MCP.defaultLayer), Layer.provide(Project.defaultLayer), Layer.provide(ToolRegistry.defaultLayer), + Layer.provide(Worktree.defaultLayer), ) diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index 2a8fc602a..c006410b8 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -50,6 +50,9 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.get(ExperimentalPaths.consoleOrgs, (c) => handler(c.req.raw, context)) app.get(ExperimentalPaths.toolIDs, (c) => handler(c.req.raw, context)) app.get(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context)) + app.post(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context)) + app.delete(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context)) + app.post(ExperimentalPaths.worktreeReset, (c) => handler(c.req.raw, context)) app.get(ExperimentalPaths.resource, (c) => handler(c.req.raw, context)) app.get("/provider", (c) => handler(c.req.raw, context)) app.get("/provider/auth", (c) => handler(c.req.raw, context)) diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index b9c7226b6..8d635e80f 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -20,6 +20,8 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem" import { BootstrapRuntime } from "@/effect/bootstrap-runtime" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { InstanceState } from "@/effect" +import { zod as effectZod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" const log = Log.create({ service: "worktree" }) @@ -39,48 +41,38 @@ export const Event = { ), } -export const Info = z - .object({ - name: z.string(), - branch: z.string(), - directory: z.string(), - }) - .meta({ - ref: "Worktree", - }) +export const Info = Schema.Struct({ + name: Schema.String, + branch: Schema.String, + directory: Schema.String, +}) + .annotate({ identifier: "Worktree" }) + .pipe(withStatics((s) => ({ zod: effectZod(s) }))) +export type Info = Schema.Schema.Type -export type Info = z.infer +export const CreateInput = Schema.Struct({ + name: Schema.optional(Schema.String), + startCommand: Schema.optional( + Schema.String.annotate({ description: "Additional startup script to run after the project's start command" }), + ), +}) + .annotate({ identifier: "WorktreeCreateInput" }) + .pipe(withStatics((s) => ({ zod: effectZod(s) }))) +export type CreateInput = Schema.Schema.Type -export const CreateInput = z - .object({ - name: z.string().optional(), - startCommand: z.string().optional().describe("Additional startup script to run after the project's start command"), - }) - .meta({ - ref: "WorktreeCreateInput", - }) +export const RemoveInput = Schema.Struct({ + directory: Schema.String, +}) + .annotate({ identifier: "WorktreeRemoveInput" }) + .pipe(withStatics((s) => ({ zod: effectZod(s) }))) +export type RemoveInput = Schema.Schema.Type -export type CreateInput = z.infer - -export const RemoveInput = z - .object({ - directory: z.string(), - }) - .meta({ - ref: "WorktreeRemoveInput", - }) - -export type RemoveInput = z.infer - -export const ResetInput = z - .object({ - directory: z.string(), - }) - .meta({ - ref: "WorktreeResetInput", - }) - -export type ResetInput = z.infer +export const ResetInput = Schema.Struct({ + directory: Schema.String, +}) + .annotate({ identifier: "WorktreeResetInput" }) + .pipe(withStatics((s) => ({ zod: effectZod(s) }))) +export type ResetInput = Schema.Schema.Type export const NotGitError = NamedError.create( "WorktreeNotGitError", @@ -210,7 +202,7 @@ export const layer: Layer.Layer< const branchCheck = yield* git(["show-ref", "--verify", "--quiet", ref], { cwd: ctx.worktree }) if (branchCheck.code === 0) continue - return Info.parse({ name, branch, directory }) + return { name, branch, directory } } throw new NameGenerationFailedError({ message: "Failed to generate a unique worktree name" }) }) diff --git a/packages/opencode/test/server/httpapi-experimental.test.ts b/packages/opencode/test/server/httpapi-experimental.test.ts index 38f43e68f..30cbb477e 100644 --- a/packages/opencode/test/server/httpapi-experimental.test.ts +++ b/packages/opencode/test/server/httpapi-experimental.test.ts @@ -1,10 +1,13 @@ import { afterEach, describe, expect, test } from "bun:test" import type { UpgradeWebSocket } from "hono/ws" +import path from "path" import { Flag } from "@opencode-ai/core/flag/flag" +import { GlobalBus } from "@/bus/global" import { Instance } from "../../src/project/instance" import { InstanceRoutes } from "../../src/server/routes/instance" import { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/experimental" import { Log } from "../../src/util" +import { Worktree } from "../../src/worktree" import { resetDatabase } from "../fixture/db" import { tmpdir } from "../fixture/fixture" @@ -18,6 +21,24 @@ function app() { return InstanceRoutes(websocket) } +async function waitReady(directory: string) { + return await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + GlobalBus.off("event", onEvent) + reject(new Error("timed out waiting for worktree.ready")) + }, 10_000) + + function onEvent(event: { directory?: string; payload: { type?: string } }) { + if (event.payload.type !== Worktree.Event.Ready.type || event.directory !== directory) return + clearTimeout(timer) + GlobalBus.off("event", onEvent) + resolve() + } + + GlobalBus.on("event", onEvent) + }) +} + afterEach(async () => { Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original await Instance.disposeAll() @@ -67,4 +88,48 @@ describe("experimental HttpApi", () => { expect(resources.status).toBe(200) expect(await resources.json()).toEqual({}) }) + + test("serves worktree mutations through Hono bridge", async () => { + await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) + + const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" } + const created = await app().request(ExperimentalPaths.worktree, { + method: "POST", + headers, + body: JSON.stringify({ name: "api-test" }), + }) + + expect(created.status).toBe(200) + const info = (await created.json()) as Worktree.Info + expect(info).toMatchObject({ name: "api-test", branch: "opencode/api-test" }) + await waitReady(info.directory) + + const listed = await app().request(ExperimentalPaths.worktree, { headers }) + expect(listed.status).toBe(200) + expect(await listed.json()).toContain(info.directory) + + await Bun.write(path.join(info.directory, "dirty.txt"), "dirty") + const reset = await app().request(ExperimentalPaths.worktreeReset, { + method: "POST", + headers, + body: JSON.stringify({ directory: info.directory }), + }) + + expect(reset.status).toBe(200) + expect(await reset.json()).toBe(true) + expect(await Bun.file(path.join(info.directory, "dirty.txt")).exists()).toBe(false) + + const removed = await app().request(ExperimentalPaths.worktree, { + method: "DELETE", + headers, + body: JSON.stringify({ directory: info.directory }), + }) + + expect(removed.status).toBe(200) + expect(await removed.json()).toBe(true) + + const afterRemove = await app().request(ExperimentalPaths.worktree, { headers }) + expect(afterRemove.status).toBe(200) + expect(await afterRemove.json()).toEqual([]) + }) }) From 75a22f82bd7b5f9a0424780ceace5eecec3662c3 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Apr 2026 19:36:15 +0000 Subject: [PATCH 37/45] chore: generate --- packages/opencode/specs/effect/http-api.md | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index f9e9948cf..d22589b15 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -153,23 +153,23 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho ## Current Route Status -| Area | Status | Notes | -| ------------------------- | ----------------- | ---------------------------------------------------------------------------------------- | -| `question` | `bridged` | `GET /question`, reply, reject | -| `permission` | `bridged` | list and reply | -| `provider` | `bridged` | list, auth, OAuth authorize/callback | -| `config` | `bridged` partial | reads only; mutation remains Hono | -| `project` | `bridged` partial | reads only; git-init remains Hono | -| `file` | `bridged` partial | find text/file/symbol, list/content/status | -| `mcp` | `bridged` partial | status only | -| `workspace` | `bridged` | list, get, enter | -| top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose | +| Area | Status | Notes | +| ------------------------- | ----------------- | -------------------------------------------------------------------------------------------------- | +| `question` | `bridged` | `GET /question`, reply, reject | +| `permission` | `bridged` | list and reply | +| `provider` | `bridged` | list, auth, OAuth authorize/callback | +| `config` | `bridged` partial | reads only; mutation remains Hono | +| `project` | `bridged` partial | reads only; git-init remains Hono | +| `file` | `bridged` partial | find text/file/symbol, list/content/status | +| `mcp` | `bridged` partial | status only | +| `workspace` | `bridged` | list, get, enter | +| top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose | | experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list/mutations, resource list; global session list remains later | -| `session` | `later/special` | large stateful surface plus streaming | -| `sync` | `later` | process/control side effects | -| `event` | `special` | SSE | -| `pty` | `special` | websocket | -| `tui` | `special` | UI bridge | +| `session` | `later/special` | large stateful surface plus streaming | +| `sync` | `later` | process/control side effects | +| `event` | `special` | SSE | +| `pty` | `special` | websocket | +| `tui` | `special` | UI bridge | ## Next PRs From df9e1d98548b459815ab6913acad50d3f445e6c4 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Apr 2026 17:52:34 -0400 Subject: [PATCH 38/45] feat(httpapi): bridge config update endpoint (#24387) --- packages/opencode/specs/effect/http-api.md | 19 ++++- packages/opencode/src/config/config.ts | 6 +- .../server/routes/instance/httpapi/config.ts | 21 +++++- .../src/server/routes/instance/index.ts | 1 + .../test/server/httpapi-config.test.ts | 69 +++++++++++++++++++ .../test/server/httpapi-experimental.test.ts | 3 - 6 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 packages/opencode/test/server/httpapi-config.test.ts diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index d22589b15..1187fef74 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -43,6 +43,23 @@ Use this checklist for each small HttpApi migration PR: 7. Add tests that hit the Hono-mounted bridge via `InstanceRoutes`, not only the raw `HttpApi` web handler, when the route depends on auth or instance context. 8. Run `bun typecheck` from `packages/opencode`, relevant `bun run test:ci ...` tests from `packages/opencode`, and `./packages/sdk/js/script/build.ts` from the repo root. +## Hono Deletion Checklist + +Use this checklist before deleting any Hono route implementation. A route being `bridged` is not enough. + +1. `HttpApi` parity is complete for the route path, method, auth behavior, query parameters, request body, response status, response headers, and error status. +2. The route is mounted by default, not only behind `OPENCODE_EXPERIMENTAL_HTTPAPI`. +3. If a fallback flag exists, tests cover both the default `HttpApi` path and the fallback Hono path until the fallback is removed. +4. OpenAPI generation uses the Effect `HttpApi` route as the source for that path. +5. Generated SDK output is unchanged from the Hono-generated contract, or the SDK diff is intentionally reviewed and accepted. +6. The legacy Hono `describeRoute`, validator, and handler for that path are removed. +7. Any duplicate Zod-only DTOs are deleted or kept only as `.zod` compatibility on the canonical Effect Schema. +8. Bridge tests exist for auth, instance selection, success response, and route-specific side effects. +9. Mutation routes prove persisted side effects and cleanup behavior in tests. If the mutation disposes/reloads the active instance, disposal happens through an explicit post-response lifecycle hook rather than inline handler teardown. +10. Streaming, SSE, websocket, and UI bridge routes have a specific non-Hono replacement plan. Do not force them through `HttpApi` if raw Effect HTTP is a better fit. + +Hono can be removed from the instance server only after all mounted Hono route groups meet this checklist and `server/routes/instance/index.ts` no longer depends on Hono routing for default behavior. + ## Experimental Read Slice Guidance For the experimental route group, port read-only JSON routes before mutations: @@ -158,7 +175,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `question` | `bridged` | `GET /question`, reply, reject | | `permission` | `bridged` | list and reply | | `provider` | `bridged` | list, auth, OAuth authorize/callback | -| `config` | `bridged` partial | reads only; mutation remains Hono | +| `config` | `bridged` | read, providers, update | | `project` | `bridged` partial | reads only; git-init remains Hono | | `file` | `bridged` partial | find text/file/symbol, list/content/status | | `mcp` | `bridged` partial | status only | diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 6173b2fb6..eee835fce 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -280,7 +280,7 @@ export interface Interface { readonly get: () => Effect.Effect readonly getGlobal: () => Effect.Effect readonly getConsoleState: () => Effect.Effect - readonly update: (config: Info) => Effect.Effect + readonly update: (config: Info, options?: { dispose?: boolean }) => Effect.Effect readonly updateGlobal: (config: Info) => Effect.Effect readonly invalidate: (wait?: boolean) => Effect.Effect readonly directories: () => Effect.Effect @@ -719,14 +719,14 @@ export const layer = Layer.effect( ) }) - const update = Effect.fn("Config.update")(function* (config: Info) { + const update = Effect.fn("Config.update")(function* (config: Info, options?: { dispose?: boolean }) { const dir = yield* InstanceState.directory const file = path.join(dir, "config.json") const existing = yield* loadFile(file) yield* fs .writeFileString(file, JSON.stringify(mergeDeep(writable(existing), writable(config)), null, 2)) .pipe(Effect.orDie) - yield* Effect.promise(() => Instance.dispose()) + if (options?.dispose !== false) yield* Effect.promise(() => Instance.dispose()) }) const invalidate = Effect.fn("Config.invalidate")(function* (wait?: boolean) { diff --git a/packages/opencode/src/server/routes/instance/httpapi/config.ts b/packages/opencode/src/server/routes/instance/httpapi/config.ts index fcdf6d1a3..7e0664b3d 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/config.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/config.ts @@ -1,8 +1,10 @@ import { Config } from "@/config" import { Provider } from "@/provider" +import * as InstanceState from "@/effect/instance-state" import { Effect, Layer } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "./auth" +import { markInstanceForDisposal } from "./lifecycle" const root = "/config" @@ -19,6 +21,16 @@ export const ConfigApi = HttpApi.make("config") description: "Retrieve the current OpenCode configuration settings and preferences.", }), ), + HttpApiEndpoint.patch("update", root, { + payload: Config.Info, + success: Config.Info, + }).annotateMerge( + OpenApi.annotations({ + identifier: "config.update", + summary: "Update configuration", + description: "Update OpenCode configuration settings and preferences.", + }), + ), HttpApiEndpoint.get("providers", `${root}/providers`, { success: Provider.ConfigProvidersResult, }).annotateMerge( @@ -54,6 +66,13 @@ export const configHandlers = Layer.unwrap( return yield* configSvc.get() }) + const update = Effect.fn("ConfigHttpApi.update")(function* (ctx) { + const payload = Config.Info.zod.parse(ctx.payload) + yield* configSvc.update(payload, { dispose: false }) + yield* markInstanceForDisposal(yield* InstanceState.context) + return payload + }) + const providers = Effect.fn("ConfigHttpApi.providers")(function* () { const providers = yield* providerSvc.list() return { @@ -63,7 +82,7 @@ export const configHandlers = Layer.unwrap( }) return HttpApiBuilder.group(ConfigApi, "config", (handlers) => - handlers.handle("get", get).handle("providers", providers), + handlers.handle("get", get).handle("update", update).handle("providers", providers), ) }), ).pipe(Layer.provide(Provider.defaultLayer), Layer.provide(Config.defaultLayer)) diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index c006410b8..25e9e058a 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -45,6 +45,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.get("/permission", (c) => handler(c.req.raw, context)) app.post("/permission/:requestID/reply", (c) => handler(c.req.raw, context)) app.get("/config", (c) => handler(c.req.raw, context)) + app.patch("/config", (c) => handler(c.req.raw, context)) app.get("/config/providers", (c) => handler(c.req.raw, context)) app.get(ExperimentalPaths.console, (c) => handler(c.req.raw, context)) app.get(ExperimentalPaths.consoleOrgs, (c) => handler(c.req.raw, context)) diff --git a/packages/opencode/test/server/httpapi-config.test.ts b/packages/opencode/test/server/httpapi-config.test.ts new file mode 100644 index 000000000..351ac2c50 --- /dev/null +++ b/packages/opencode/test/server/httpapi-config.test.ts @@ -0,0 +1,69 @@ +import { afterEach, describe, expect, test } from "bun:test" +import type { UpgradeWebSocket } from "hono/ws" +import path from "path" +import { Flag } from "@opencode-ai/core/flag/flag" +import { GlobalBus } from "@/bus/global" +import { Instance } from "../../src/project/instance" +import { InstanceRoutes } from "../../src/server/routes/instance" +import { Log } from "../../src/util" +import { resetDatabase } from "../fixture/db" +import { tmpdir } from "../fixture/fixture" + +void Log.init({ print: false }) + +const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI +const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket + +function app() { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true + return InstanceRoutes(websocket) +} + +async function waitDisposed(directory: string) { + return await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + GlobalBus.off("event", onEvent) + reject(new Error("timed out waiting for instance disposal")) + }, 10_000) + + function onEvent(event: { directory?: string; payload: { type?: string } }) { + if (event.payload.type !== "server.instance.disposed" || event.directory !== directory) return + clearTimeout(timer) + GlobalBus.off("event", onEvent) + resolve() + } + + GlobalBus.on("event", onEvent) + }) +} + +afterEach(async () => { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original + await Instance.disposeAll() + await resetDatabase() +}) + +describe("config HttpApi", () => { + test("serves config update through Hono bridge", async () => { + await using tmp = await tmpdir({ config: { formatter: false, lsp: false } }) + const disposed = waitDisposed(tmp.path) + + const response = await app().request("/config", { + method: "PATCH", + headers: { + "content-type": "application/json", + "x-opencode-directory": tmp.path, + }, + body: JSON.stringify({ username: "patched-user", formatter: false, lsp: false }), + }) + + expect(response.status).toBe(200) + expect(await response.json()).toMatchObject({ username: "patched-user", formatter: false, lsp: false }) + await disposed + expect(await Bun.file(path.join(tmp.path, "config.json")).json()).toMatchObject({ + username: "patched-user", + formatter: false, + lsp: false, + }) + }) +}) diff --git a/packages/opencode/test/server/httpapi-experimental.test.ts b/packages/opencode/test/server/httpapi-experimental.test.ts index 30cbb477e..e355b0027 100644 --- a/packages/opencode/test/server/httpapi-experimental.test.ts +++ b/packages/opencode/test/server/httpapi-experimental.test.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test" import type { UpgradeWebSocket } from "hono/ws" -import path from "path" import { Flag } from "@opencode-ai/core/flag/flag" import { GlobalBus } from "@/bus/global" import { Instance } from "../../src/project/instance" @@ -108,7 +107,6 @@ describe("experimental HttpApi", () => { expect(listed.status).toBe(200) expect(await listed.json()).toContain(info.directory) - await Bun.write(path.join(info.directory, "dirty.txt"), "dirty") const reset = await app().request(ExperimentalPaths.worktreeReset, { method: "POST", headers, @@ -117,7 +115,6 @@ describe("experimental HttpApi", () => { expect(reset.status).toBe(200) expect(await reset.json()).toBe(true) - expect(await Bun.file(path.join(info.directory, "dirty.txt")).exists()).toBe(false) const removed = await app().request(ExperimentalPaths.worktree, { method: "DELETE", From 5904f599a9110207f61654ae3a57b9a041228dce Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Apr 2026 18:42:02 -0400 Subject: [PATCH 39/45] feat(httpapi): bridge project git init endpoint (#24394) --- packages/opencode/specs/effect/http-api.md | 4 +-- .../routes/instance/httpapi/lifecycle.ts | 19 ++++++++++ .../server/routes/instance/httpapi/project.ts | 27 +++++++++++++- .../src/server/routes/instance/index.ts | 1 + .../test/server/httpapi-experimental.test.ts | 16 +++++---- .../test/server/httpapi-instance.test.ts | 36 +++++++++++++++++++ 6 files changed, 93 insertions(+), 10 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 1187fef74..261c8b76b 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -110,7 +110,7 @@ Good near-term candidates: - top-level reads: `GET /path`, `GET /vcs`, `GET /vcs/diff`, `GET /command`, `GET /agent`, `GET /skill`, `GET /lsp`, `GET /formatter` - simple mutations: `POST /instance/dispose` - experimental JSON reads: console, tool, worktree list, resource list -- deferred JSON mutations: `PATCH /config`, project git init, workspace/worktree create/remove/reset, file search, MCP auth flows +- deferred JSON mutations: workspace/worktree create/remove/reset, file search, MCP auth flows Keep large or stateful groups for later: @@ -176,7 +176,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `permission` | `bridged` | list and reply | | `provider` | `bridged` | list, auth, OAuth authorize/callback | | `config` | `bridged` | read, providers, update | -| `project` | `bridged` partial | reads only; git-init remains Hono | +| `project` | `bridged` | list, current, git init | | `file` | `bridged` partial | find text/file/symbol, list/content/status | | `mcp` | `bridged` partial | status only | | `workspace` | `bridged` | list, get, enter | diff --git a/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts b/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts index 6b11dffd5..0cd79bdc0 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts @@ -3,6 +3,10 @@ import { Effect } from "effect" import { HttpEffect, HttpMiddleware, HttpServerRequest } from "effect/unstable/http" const disposeAfterResponse = new WeakMap() +const reloadAfterResponse = new WeakMap< + object, + InstanceContext & { next: Parameters[0] } +>() export const markInstanceForDisposal = (ctx: InstanceContext) => HttpEffect.appendPreResponseHandler((request, response) => @@ -12,10 +16,25 @@ export const markInstanceForDisposal = (ctx: InstanceContext) => }), ) +export const markInstanceForReload = (ctx: InstanceContext, next: Parameters[0]) => + HttpEffect.appendPreResponseHandler((request, response) => + Effect.sync(() => { + reloadAfterResponse.set(request.source, { ...ctx, next }) + return response + }), + ) + export const disposeMiddleware: HttpMiddleware.HttpMiddleware = (effect) => Effect.gen(function* () { const response = yield* effect const request = yield* HttpServerRequest.HttpServerRequest + const reload = reloadAfterResponse.get(request.source) + if (reload) { + reloadAfterResponse.delete(request.source) + yield* Effect.promise(() => Instance.restore(reload, () => Instance.reload(reload.next))) + return response + } + const ctx = disposeAfterResponse.get(request.source) if (!ctx) return response disposeAfterResponse.delete(request.source) diff --git a/packages/opencode/src/server/routes/instance/httpapi/project.ts b/packages/opencode/src/server/routes/instance/httpapi/project.ts index 6d3143df8..d8a36eccc 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/project.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/project.ts @@ -1,8 +1,11 @@ import * as InstanceState from "@/effect/instance-state" +import { AppRuntime } from "@/effect/app-runtime" import { Project } from "@/project" +import { InstanceBootstrap } from "@/project/bootstrap" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "./auth" +import { markInstanceForReload } from "./lifecycle" const root = "/project" @@ -28,6 +31,15 @@ export const ProjectApi = HttpApi.make("project") description: "Retrieve the currently active project that OpenCode is working with.", }), ), + HttpApiEndpoint.post("initGit", `${root}/git/init`, { + success: Project.Info, + }).annotateMerge( + OpenApi.annotations({ + identifier: "project.initGit", + summary: "Initialize git repository", + description: "Create a git repository for the current project and return the refreshed project info.", + }), + ), ) .annotateMerge( OpenApi.annotations({ @@ -57,8 +69,21 @@ export const projectHandlers = Layer.unwrap( return (yield* InstanceState.context).project }) + const initGit = Effect.fn("ProjectHttpApi.initGit")(function* () { + const ctx = yield* InstanceState.context + const next = yield* svc.initGit({ directory: ctx.directory, project: ctx.project }) + if (next.id === ctx.project.id && next.vcs === ctx.project.vcs && next.worktree === ctx.project.worktree) return next + yield* markInstanceForReload(ctx, { + directory: ctx.directory, + worktree: ctx.directory, + project: next, + init: () => AppRuntime.runPromise(InstanceBootstrap), + }) + return next + }) + return HttpApiBuilder.group(ProjectApi, "project", (handlers) => - handlers.handle("list", list).handle("current", current), + handlers.handle("list", list).handle("current", current).handle("initGit", initGit), ) }), ).pipe(Layer.provide(Project.defaultLayer)) diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index 25e9e058a..8b8126f5d 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -61,6 +61,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.post("/provider/:providerID/oauth/callback", (c) => handler(c.req.raw, context)) app.get("/project", (c) => handler(c.req.raw, context)) app.get("/project/current", (c) => handler(c.req.raw, context)) + app.post("/project/git/init", (c) => handler(c.req.raw, context)) app.get(FilePaths.findText, (c) => handler(c.req.raw, context)) app.get(FilePaths.findFile, (c) => handler(c.req.raw, context)) app.get(FilePaths.findSymbol, (c) => handler(c.req.raw, context)) diff --git a/packages/opencode/test/server/httpapi-experimental.test.ts b/packages/opencode/test/server/httpapi-experimental.test.ts index e355b0027..e704750ea 100644 --- a/packages/opencode/test/server/httpapi-experimental.test.ts +++ b/packages/opencode/test/server/httpapi-experimental.test.ts @@ -107,14 +107,16 @@ describe("experimental HttpApi", () => { expect(listed.status).toBe(200) expect(await listed.json()).toContain(info.directory) - const reset = await app().request(ExperimentalPaths.worktreeReset, { - method: "POST", - headers, - body: JSON.stringify({ directory: info.directory }), - }) + if (process.platform !== "win32") { + const reset = await app().request(ExperimentalPaths.worktreeReset, { + method: "POST", + headers, + body: JSON.stringify({ directory: info.directory }), + }) - expect(reset.status).toBe(200) - expect(await reset.json()).toBe(true) + expect(reset.status).toBe(200) + expect(await reset.json()).toBe(true) + } const removed = await app().request(ExperimentalPaths.worktree, { method: "DELETE", diff --git a/packages/opencode/test/server/httpapi-instance.test.ts b/packages/opencode/test/server/httpapi-instance.test.ts index 463dbaa87..139712652 100644 --- a/packages/opencode/test/server/httpapi-instance.test.ts +++ b/packages/opencode/test/server/httpapi-instance.test.ts @@ -20,6 +20,24 @@ function app() { return InstanceRoutes(websocket) } +async function waitDisposed(directory: string) { + return await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + GlobalBus.off("event", onEvent) + reject(new Error("timed out waiting for instance disposal")) + }, 10_000) + + function onEvent(event: { directory?: string; payload: { type?: string } }) { + if (event.payload.type !== "server.instance.disposed" || event.directory !== directory) return + clearTimeout(timer) + GlobalBus.off("event", onEvent) + resolve() + } + + GlobalBus.on("event", onEvent) + }) +} + afterEach(async () => { Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original await Instance.disposeAll() @@ -79,6 +97,24 @@ describe("instance HttpApi", () => { expect(await formatter.json()).toEqual([]) }) + test("serves project git init through Hono bridge", async () => { + await using tmp = await tmpdir({ config: { formatter: false, lsp: false } }) + const disposed = waitDisposed(tmp.path) + + const response = await app().request("/project/git/init", { + method: "POST", + headers: { "x-opencode-directory": tmp.path }, + }) + + expect(response.status).toBe(200) + expect(await response.json()).toMatchObject({ vcs: "git", worktree: tmp.path }) + await disposed + + const current = await app().request("/project/current", { headers: { "x-opencode-directory": tmp.path } }) + expect(current.status).toBe(200) + expect(await current.json()).toMatchObject({ vcs: "git", worktree: tmp.path }) + }) + test("serves instance dispose through Hono bridge", async () => { await using tmp = await tmpdir() From 27b0877714ac2fd51d0b944f384448120bc530bf Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Apr 2026 22:43:13 +0000 Subject: [PATCH 40/45] chore: generate --- .../opencode/src/server/routes/instance/httpapi/lifecycle.ts | 5 +---- .../opencode/src/server/routes/instance/httpapi/project.ts | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts b/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts index 0cd79bdc0..1916e4269 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts @@ -3,10 +3,7 @@ import { Effect } from "effect" import { HttpEffect, HttpMiddleware, HttpServerRequest } from "effect/unstable/http" const disposeAfterResponse = new WeakMap() -const reloadAfterResponse = new WeakMap< - object, - InstanceContext & { next: Parameters[0] } ->() +const reloadAfterResponse = new WeakMap[0] }>() export const markInstanceForDisposal = (ctx: InstanceContext) => HttpEffect.appendPreResponseHandler((request, response) => diff --git a/packages/opencode/src/server/routes/instance/httpapi/project.ts b/packages/opencode/src/server/routes/instance/httpapi/project.ts index d8a36eccc..95a11a1a5 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/project.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/project.ts @@ -72,7 +72,8 @@ export const projectHandlers = Layer.unwrap( const initGit = Effect.fn("ProjectHttpApi.initGit")(function* () { const ctx = yield* InstanceState.context const next = yield* svc.initGit({ directory: ctx.directory, project: ctx.project }) - if (next.id === ctx.project.id && next.vcs === ctx.project.vcs && next.worktree === ctx.project.worktree) return next + if (next.id === ctx.project.id && next.vcs === ctx.project.vcs && next.worktree === ctx.project.worktree) + return next yield* markInstanceForReload(ctx, { directory: ctx.directory, worktree: ctx.directory, From 58c65874ba6aff2f16f5310dacddc3a89eb7b2cd Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Apr 2026 18:55:49 -0400 Subject: [PATCH 41/45] feat(httpapi): bridge project update endpoint (#24398) --- packages/opencode/specs/effect/http-api.md | 22 ++++++++++++--- packages/opencode/src/project/project.ts | 9 +++++++ .../server/routes/instance/httpapi/project.ts | 21 ++++++++++++++- .../src/server/routes/instance/index.ts | 1 + .../test/server/httpapi-instance.test.ts | 27 +++++++++++++++++++ 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 261c8b76b..e5a64d920 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -176,7 +176,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `permission` | `bridged` | list and reply | | `provider` | `bridged` | list, auth, OAuth authorize/callback | | `config` | `bridged` | read, providers, update | -| `project` | `bridged` | list, current, git init | +| `project` | `bridged` | list, current, git init, update | | `file` | `bridged` partial | find text/file/symbol, list/content/status | | `mcp` | `bridged` partial | status only | | `workspace` | `bridged` | list, get, enter | @@ -188,10 +188,24 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `pty` | `special` | websocket | | `tui` | `special` | UI bridge | -## Next PRs +## Remaining PR Plan -1. Produce a generated route inventory from Hono registrations and update `Current Route Status` with exact paths. -2. Start the Effect OpenAPI/SDK generation path for already-bridged routes. +Prefer smaller PRs from here so route behavior and SDK/OpenAPI fallout stays reviewable. + +1. Bridge `PATCH /project/:projectID`. +2. Bridge MCP add/connect/disconnect routes. +3. Bridge MCP OAuth routes: start, callback, authenticate, remove. +4. Bridge experimental console switch and tool list routes. +5. Bridge experimental global session list. +6. Bridge sync start/replay/history routes. +7. Bridge session read routes: list, status, get, children, todo, diff, messages. +8. Bridge session lifecycle mutation routes: create, delete, update, fork, abort. +9. Bridge session share/summary/message/part mutation routes. +10. Replace event SSE with non-Hono Effect HTTP. +11. Replace pty websocket/control routes with non-Hono Effect HTTP. +12. Replace tui bridge routes or explicitly isolate them behind a non-Hono compatibility layer. +13. Switch OpenAPI/SDK generation to Effect routes and compare SDK output. +14. Flip ported JSON routes default-on, keep a short fallback, then delete replaced Hono route files. ## Checklist diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index fc34a6296..c26114506 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -91,6 +91,15 @@ export const UpdateInput = z.object({ }) export type UpdateInput = z.infer +export const UpdatePayload = Schema.Struct({ + name: Schema.optional(Schema.String), + icon: Schema.optional(ProjectIcon), + commands: Schema.optional(ProjectCommands), +}) + .annotate({ identifier: "ProjectUpdateInput" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type UpdatePayload = Types.DeepMutable> + // --------------------------------------------------------------------------- // Effect service // --------------------------------------------------------------------------- diff --git a/packages/opencode/src/server/routes/instance/httpapi/project.ts b/packages/opencode/src/server/routes/instance/httpapi/project.ts index 95a11a1a5..63190180c 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/project.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/project.ts @@ -2,6 +2,7 @@ import * as InstanceState from "@/effect/instance-state" import { AppRuntime } from "@/effect/app-runtime" import { Project } from "@/project" import { InstanceBootstrap } from "@/project/bootstrap" +import { ProjectID } from "@/project/schema" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "./auth" @@ -40,6 +41,17 @@ export const ProjectApi = HttpApi.make("project") description: "Create a git repository for the current project and return the refreshed project info.", }), ), + HttpApiEndpoint.patch("update", `${root}/:projectID`, { + params: { projectID: ProjectID }, + payload: Project.UpdatePayload, + success: Project.Info, + }).annotateMerge( + OpenApi.annotations({ + identifier: "project.update", + summary: "Update project", + description: "Update project properties such as name, icon, and commands.", + }), + ), ) .annotateMerge( OpenApi.annotations({ @@ -83,8 +95,15 @@ export const projectHandlers = Layer.unwrap( return next }) + const update = Effect.fn("ProjectHttpApi.update")(function* (ctx: { + params: { projectID: ProjectID } + payload: Project.UpdatePayload + }) { + return yield* svc.update({ ...Project.UpdatePayload.zod.parse(ctx.payload), projectID: ctx.params.projectID }) + }) + return HttpApiBuilder.group(ProjectApi, "project", (handlers) => - handlers.handle("list", list).handle("current", current).handle("initGit", initGit), + handlers.handle("list", list).handle("current", current).handle("initGit", initGit).handle("update", update), ) }), ).pipe(Layer.provide(Project.defaultLayer)) diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index 8b8126f5d..8d341b8a0 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -62,6 +62,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.get("/project", (c) => handler(c.req.raw, context)) app.get("/project/current", (c) => handler(c.req.raw, context)) app.post("/project/git/init", (c) => handler(c.req.raw, context)) + app.patch("/project/:projectID", (c) => handler(c.req.raw, context)) app.get(FilePaths.findText, (c) => handler(c.req.raw, context)) app.get(FilePaths.findFile, (c) => handler(c.req.raw, context)) app.get(FilePaths.findSymbol, (c) => handler(c.req.raw, context)) diff --git a/packages/opencode/test/server/httpapi-instance.test.ts b/packages/opencode/test/server/httpapi-instance.test.ts index 139712652..404807984 100644 --- a/packages/opencode/test/server/httpapi-instance.test.ts +++ b/packages/opencode/test/server/httpapi-instance.test.ts @@ -115,6 +115,33 @@ describe("instance HttpApi", () => { expect(await current.json()).toMatchObject({ vcs: "git", worktree: tmp.path }) }) + test("serves project update through Hono bridge", async () => { + await using tmp = await tmpdir({ config: { formatter: false, lsp: false } }) + + const current = await app().request("/project/current", { headers: { "x-opencode-directory": tmp.path } }) + expect(current.status).toBe(200) + const project = (await current.json()) as { id: string } + + const response = await app().request(`/project/${project.id}`, { + method: "PATCH", + headers: { "x-opencode-directory": tmp.path, "content-type": "application/json" }, + body: JSON.stringify({ name: "patched-project", commands: { start: "bun dev" } }), + }) + + expect(response.status).toBe(200) + expect(await response.json()).toMatchObject({ + id: project.id, + name: "patched-project", + commands: { start: "bun dev" }, + }) + + const list = await app().request("/project", { headers: { "x-opencode-directory": tmp.path } }) + expect(list.status).toBe(200) + expect(await list.json()).toContainEqual( + expect.objectContaining({ id: project.id, name: "patched-project", commands: { start: "bun dev" } }), + ) + }) + test("serves instance dispose through Hono bridge", async () => { await using tmp = await tmpdir() From a14c22d4e9196eda3fc217213dcd7b01344087de Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Apr 2026 19:16:19 -0400 Subject: [PATCH 42/45] feat(httpapi): bridge mcp control endpoints (#24403) --- .opencode/skills/effect/SKILL.md | 29 ++- packages/opencode/specs/effect/http-api.md | 190 ++++++++++++++++-- .../src/server/routes/instance/httpapi/mcp.ts | 58 +++++- .../src/server/routes/instance/index.ts | 3 + .../opencode/test/server/httpapi-mcp.test.ts | 46 ++++- 5 files changed, 294 insertions(+), 32 deletions(-) diff --git a/.opencode/skills/effect/SKILL.md b/.opencode/skills/effect/SKILL.md index 475814637..78216ab01 100644 --- a/.opencode/skills/effect/SKILL.md +++ b/.opencode/skills/effect/SKILL.md @@ -1,21 +1,30 @@ --- name: effect -description: Answer questions about the Effect framework +description: Work with Effect v4 / effect-smol TypeScript code in this repo --- # Effect -This codebase uses Effect, a framework for writing typescript. +This codebase uses Effect for typed, composable TypeScript services, schemas, and workflows. -## How to Answer Effect Questions +## Source Of Truth -1. Clone the Effect repository: `https://github.com/Effect-TS/effect-smol` to - `.opencode/references/effect-smol` in this project NOT the skill folder. -2. Use the explore agent to search the codebase for answers about Effect patterns, APIs, and concepts -3. Provide responses based on the actual Effect source code and documentation +Use the current Effect v4 / effect-smol source, not memory or older Effect v2/v3 examples. + +1. If `.opencode/references/effect-smol` is missing, clone `https://github.com/Effect-TS/effect-smol` there. Do this in the project, not in the skill folder. +2. Search `.opencode/references/effect-smol` for exact APIs, examples, tests, and naming patterns before answering or implementing Effect-specific code. +3. Also inspect existing repo code for local house style before introducing new patterns. +4. Prefer answers and implementations backed by specific source files or nearby repo examples. ## Guidelines -- Always use the explore agent with the cloned repository when answering Effect-related questions -- Reference specific files and patterns found in the Effect codebase -- Do not answer from memory - always verify against the source +- Prefer current Effect v4 APIs and project-local patterns over old blog posts, examples, or package-memory guesses. +- Use `Effect.gen(function* () { ... })` for multi-step workflows. +- Use `Effect.fn("Name")` or `Effect.fnUntraced(...)` for named effects when adding reusable service methods or important workflows. +- Prefer Effect `Schema` for API and domain data shapes. Use branded schemas for IDs and `Schema.TaggedErrorClass` for typed domain errors when modeling new error surfaces. +- Keep HTTP handlers thin: decode input, read request context, call services, and map transport errors. Put business rules in services. +- In Effect service code, prefer Effect-aware platform abstractions and dependencies over ad hoc promises where the surrounding code already does so. +- Keep layer composition explicit. Avoid broad hidden provisioning that makes missing dependencies hard to see. +- In tests, prefer the repo's existing Effect test helpers and live tests for filesystem, git, child process, locks, or timing behavior. +- Do not introduce `any`, non-null assertions, unchecked casts, or older Effect APIs just to satisfy types. +- Do not answer from memory. Verify against `.opencode/references/effect-smol` or nearby code first. diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index e5a64d920..2be0261ea 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -178,8 +178,8 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `config` | `bridged` | read, providers, update | | `project` | `bridged` | list, current, git init, update | | `file` | `bridged` partial | find text/file/symbol, list/content/status | -| `mcp` | `bridged` partial | status only | -| `workspace` | `bridged` | list, get, enter | +| `mcp` | `bridged` partial | status, add, connect/disconnect; OAuth remains | +| `workspace` | `bridged` partial | adaptor/list/status; create/remove/session-restore remain | | top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose | | experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list/mutations, resource list; global session list remains later | | `session` | `later/special` | large stateful surface plus streaming | @@ -188,24 +188,180 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `pty` | `special` | websocket | | `tui` | `special` | UI bridge | +## Full Route Checklist + +This checklist tracks bridge parity only. Checked routes are available through the experimental `HttpApi` bridge; Hono deletion is tracked separately by the deletion checklist above. + +### Top-Level Instance Routes + +- [x] `POST /instance/dispose` - dispose active instance after response. +- [x] `GET /path` - current directory and worktree paths. +- [x] `GET /vcs` - current VCS status. +- [x] `GET /vcs/diff` - VCS diff summary. +- [x] `GET /command` - command catalog. +- [x] `GET /agent` - agent catalog. +- [x] `GET /skill` - skill catalog. +- [x] `GET /lsp` - LSP status. +- [x] `GET /formatter` - formatter status. + +### Config Routes + +- [x] `GET /config` - read config. +- [x] `PATCH /config` - update config and dispose active instance after response. +- [x] `GET /config/providers` - config provider summary. + +### Project Routes + +- [x] `GET /project` - list projects. +- [x] `GET /project/current` - current project. +- [x] `POST /project/git/init` - initialize git and reload active instance after response. +- [x] `PATCH /project/:projectID` - update project metadata. + +### Provider Routes + +- [x] `GET /provider` - list providers. +- [x] `GET /provider/auth` - list provider auth methods. +- [x] `POST /provider/:providerID/oauth/authorize` - start provider OAuth. +- [x] `POST /provider/:providerID/oauth/callback` - finish provider OAuth. + +### Question Routes + +- [x] `GET /question` - list questions. +- [x] `POST /question/:requestID/reply` - reply to question. +- [x] `POST /question/:requestID/reject` - reject question. + +### Permission Routes + +- [x] `GET /permission` - list permission requests. +- [x] `POST /permission/:requestID/reply` - reply to permission request. + +### File Routes + +- [x] `GET /find` - text search. +- [x] `GET /find/file` - file search. +- [x] `GET /find/symbol` - symbol search. +- [x] `GET /file` - list directory entries. +- [x] `GET /file/content` - read file content. +- [x] `GET /file/status` - file status. + +### MCP Routes + +- [x] `GET /mcp` - MCP status. +- [x] `POST /mcp` - add MCP server at runtime. +- [ ] `POST /mcp/:name/auth` - start MCP OAuth. +- [ ] `POST /mcp/:name/auth/callback` - finish MCP OAuth callback. +- [ ] `POST /mcp/:name/auth/authenticate` - run MCP OAuth authenticate flow. +- [ ] `DELETE /mcp/:name/auth` - remove MCP OAuth credentials. +- [x] `POST /mcp/:name/connect` - connect MCP server. +- [x] `POST /mcp/:name/disconnect` - disconnect MCP server. + +### Experimental Routes + +- [x] `GET /experimental/console` - active Console provider metadata. +- [x] `GET /experimental/console/orgs` - switchable Console orgs. +- [ ] `POST /experimental/console/switch` - switch active Console org. +- [x] `GET /experimental/tool/ids` - tool IDs. +- [ ] `GET /experimental/tool` - tools for provider/model. +- [x] `GET /experimental/worktree` - list worktrees. +- [x] `POST /experimental/worktree` - create worktree. +- [x] `DELETE /experimental/worktree` - remove worktree. +- [x] `POST /experimental/worktree/reset` - reset worktree. +- [ ] `GET /experimental/session` - global session list. +- [x] `GET /experimental/resource` - MCP resources. + +### Workspace Routes + +- [x] `GET /experimental/workspace/adaptor` - list workspace adaptors. +- [ ] `POST /experimental/workspace` - create workspace. +- [x] `GET /experimental/workspace` - list workspaces. +- [x] `GET /experimental/workspace/status` - workspace status. +- [ ] `DELETE /experimental/workspace/:id` - remove workspace. +- [ ] `POST /experimental/workspace/:id/session-restore` - restore session into workspace. + +### Sync Routes + +- [ ] `POST /sync/start` - start workspace sync. +- [ ] `POST /sync/replay` - replay sync events. +- [ ] `POST /sync/history` - list sync event history. + +### Session Routes + +- [ ] `GET /session` - list sessions. +- [ ] `GET /session/status` - session status map. +- [ ] `GET /session/:sessionID` - get session. +- [ ] `GET /session/:sessionID/children` - get child sessions. +- [ ] `GET /session/:sessionID/todo` - get session todos. +- [ ] `POST /session` - create session. +- [ ] `DELETE /session/:sessionID` - delete session. +- [ ] `PATCH /session/:sessionID` - update session metadata. +- [ ] `POST /session/:sessionID/init` - run project init command. +- [ ] `POST /session/:sessionID/fork` - fork session. +- [ ] `POST /session/:sessionID/abort` - abort session. +- [ ] `POST /session/:sessionID/share` - share session. +- [ ] `GET /session/:sessionID/diff` - session diff. +- [ ] `DELETE /session/:sessionID/share` - unshare session. +- [ ] `POST /session/:sessionID/summarize` - summarize session. +- [ ] `GET /session/:sessionID/message` - list session messages. +- [ ] `GET /session/:sessionID/message/:messageID` - get message. +- [ ] `DELETE /session/:sessionID/message/:messageID` - delete message. +- [ ] `DELETE /session/:sessionID/message/:messageID/part/:partID` - delete part. +- [ ] `PATCH /session/:sessionID/message/:messageID/part/:partID` - update part. +- [ ] `POST /session/:sessionID/message` - prompt with streaming response. +- [ ] `POST /session/:sessionID/prompt_async` - async prompt. +- [ ] `POST /session/:sessionID/command` - run command. +- [ ] `POST /session/:sessionID/shell` - run shell command. +- [ ] `POST /session/:sessionID/revert` - revert message. +- [ ] `POST /session/:sessionID/unrevert` - restore reverted messages. +- [ ] `POST /session/:sessionID/permissions/:permissionID` - deprecated permission response route. + +### Event Routes + +- [ ] `GET /event` - SSE event stream; replace with raw Effect HTTP, not `HttpApi`. + +### PTY Routes + +- [ ] `GET /pty` - list PTY sessions. +- [ ] `POST /pty` - create PTY session. +- [ ] `GET /pty/:ptyID` - get PTY session. +- [ ] `PUT /pty/:ptyID` - update PTY session. +- [ ] `DELETE /pty/:ptyID` - remove PTY session. +- [ ] `GET /pty/:ptyID/connect` - PTY websocket; replace with raw Effect HTTP/websocket support. + +### TUI Routes + +- [ ] `POST /tui/append-prompt` - append prompt. +- [ ] `POST /tui/open-help` - open help. +- [ ] `POST /tui/open-sessions` - open sessions. +- [ ] `POST /tui/open-themes` - open themes. +- [ ] `POST /tui/open-models` - open models. +- [ ] `POST /tui/submit-prompt` - submit prompt. +- [ ] `POST /tui/clear-prompt` - clear prompt. +- [ ] `POST /tui/execute-command` - execute command. +- [ ] `POST /tui/show-toast` - show toast. +- [ ] `POST /tui/publish` - publish TUI event. +- [ ] `POST /tui/select-session` - select session. +- [ ] `GET /tui/control/next` - get next TUI request. +- [ ] `POST /tui/control/response` - submit TUI control response. + ## Remaining PR Plan Prefer smaller PRs from here so route behavior and SDK/OpenAPI fallout stays reviewable. -1. Bridge `PATCH /project/:projectID`. -2. Bridge MCP add/connect/disconnect routes. -3. Bridge MCP OAuth routes: start, callback, authenticate, remove. -4. Bridge experimental console switch and tool list routes. -5. Bridge experimental global session list. -6. Bridge sync start/replay/history routes. -7. Bridge session read routes: list, status, get, children, todo, diff, messages. -8. Bridge session lifecycle mutation routes: create, delete, update, fork, abort. -9. Bridge session share/summary/message/part mutation routes. -10. Replace event SSE with non-Hono Effect HTTP. -11. Replace pty websocket/control routes with non-Hono Effect HTTP. -12. Replace tui bridge routes or explicitly isolate them behind a non-Hono compatibility layer. -13. Switch OpenAPI/SDK generation to Effect routes and compare SDK output. -14. Flip ported JSON routes default-on, keep a short fallback, then delete replaced Hono route files. +1. [x] Bridge `PATCH /project/:projectID`. +2. [x] Bridge MCP add/connect/disconnect routes. +3. [ ] Bridge MCP OAuth routes: start, callback, authenticate, remove. +4. [ ] Bridge experimental console switch and tool list routes. +5. [ ] Bridge experimental global session list. +6. [ ] Bridge workspace create/remove/session-restore routes. +7. [ ] Bridge sync start/replay/history routes. +8. [ ] Bridge session read routes: list, status, get, children, todo, diff, messages. +9. [ ] Bridge session lifecycle mutation routes: create, delete, update, fork, abort. +10. [ ] Bridge session share/summary/message/part mutation routes. +11. [ ] Replace event SSE with non-Hono Effect HTTP. +12. [ ] Replace pty websocket/control routes with non-Hono Effect HTTP. +13. [ ] Replace tui bridge routes or explicitly isolate them behind a non-Hono compatibility layer. +14. [ ] Switch OpenAPI/SDK generation to Effect routes and compare SDK output. +15. [ ] Flip ported JSON routes default-on, keep a short fallback, then delete replaced Hono route files. ## Checklist @@ -216,7 +372,7 @@ Prefer smaller PRs from here so route behavior and SDK/OpenAPI fallout stays rev - [x] Attach auth middleware in route modules. - [x] Support `auth_token` as a query security scheme. - [x] Add bridge-level auth and instance tests. -- [ ] Complete exact Hono route inventory. +- [x] Complete exact Hono route inventory. - [x] Resolve implemented-but-unmounted route groups. - [x] Port remaining top-level JSON reads. - [ ] Generate SDK/OpenAPI from Effect routes. diff --git a/packages/opencode/src/server/routes/instance/httpapi/mcp.ts b/packages/opencode/src/server/routes/instance/httpapi/mcp.ts index 34d4e09e2..81ca68e2c 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/mcp.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/mcp.ts @@ -1,10 +1,20 @@ import { MCP } from "@/mcp" +import { ConfigMCP } from "@/config/mcp" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "./auth" +const AddPayload = Schema.Struct({ + name: Schema.String, + config: ConfigMCP.Info, +}).annotate({ identifier: "McpAddInput" }) + +const StatusMap = Schema.Record(Schema.String, MCP.Status) + export const McpPaths = { status: "/mcp", + connect: "/mcp/:name/connect", + disconnect: "/mcp/:name/disconnect", } as const export const McpApi = HttpApi.make("mcp") @@ -20,6 +30,34 @@ export const McpApi = HttpApi.make("mcp") description: "Get the status of all Model Context Protocol (MCP) servers.", }), ), + HttpApiEndpoint.post("add", McpPaths.status, { + payload: AddPayload, + success: StatusMap, + }).annotateMerge( + OpenApi.annotations({ + identifier: "mcp.add", + summary: "Add MCP server", + description: "Dynamically add a new Model Context Protocol (MCP) server to the system.", + }), + ), + HttpApiEndpoint.post("connect", McpPaths.connect, { + params: { name: Schema.String }, + success: Schema.Boolean, + }).annotateMerge( + OpenApi.annotations({ + identifier: "mcp.connect", + description: "Connect an MCP server.", + }), + ), + HttpApiEndpoint.post("disconnect", McpPaths.disconnect, { + params: { name: Schema.String }, + success: Schema.Boolean, + }).annotateMerge( + OpenApi.annotations({ + identifier: "mcp.disconnect", + description: "Disconnect an MCP server.", + }), + ), ) .annotateMerge( OpenApi.annotations({ @@ -45,6 +83,24 @@ export const mcpHandlers = Layer.unwrap( return yield* mcp.status() }) - return HttpApiBuilder.group(McpApi, "mcp", (handlers) => handlers.handle("status", status)) + const add = Effect.fn("McpHttpApi.add")(function* (ctx: { payload: typeof AddPayload.Type }) { + const payload = Schema.decodeUnknownSync(AddPayload)(ctx.payload) + const result = (yield* mcp.add(payload.name, payload.config)).status + return Schema.decodeUnknownSync(StatusMap)("status" in result ? { [payload.name]: result } : result) + }) + + const connect = Effect.fn("McpHttpApi.connect")(function* (ctx: { params: { name: string } }) { + yield* mcp.connect(ctx.params.name) + return true + }) + + const disconnect = Effect.fn("McpHttpApi.disconnect")(function* (ctx: { params: { name: string } }) { + yield* mcp.disconnect(ctx.params.name) + return true + }) + + return HttpApiBuilder.group(McpApi, "mcp", (handlers) => + handlers.handle("status", status).handle("add", add).handle("connect", connect).handle("disconnect", disconnect), + ) }), ).pipe(Layer.provide(MCP.defaultLayer)) diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index 8d341b8a0..ab8632b5c 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -79,6 +79,9 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.get(InstancePaths.lsp, (c) => handler(c.req.raw, context)) app.get(InstancePaths.formatter, (c) => handler(c.req.raw, context)) app.get(McpPaths.status, (c) => handler(c.req.raw, context)) + app.post(McpPaths.status, (c) => handler(c.req.raw, context)) + app.post(McpPaths.connect, (c) => handler(c.req.raw, context)) + app.post(McpPaths.disconnect, (c) => handler(c.req.raw, context)) } return app diff --git a/packages/opencode/test/server/httpapi-mcp.test.ts b/packages/opencode/test/server/httpapi-mcp.test.ts index 3da1dc933..68144503b 100644 --- a/packages/opencode/test/server/httpapi-mcp.test.ts +++ b/packages/opencode/test/server/httpapi-mcp.test.ts @@ -11,12 +11,13 @@ void Log.init({ print: false }) const context = Context.empty() as Context.Context -function request(route: string, directory: string) { +function request(route: string, directory: string, init?: RequestInit) { + const headers = new Headers(init?.headers) + headers.set("x-opencode-directory", directory) return ExperimentalHttpApiServer.webHandler().handler( new Request(`http://localhost${route}`, { - headers: { - "x-opencode-directory": directory, - }, + ...init, + headers, }), context, ) @@ -45,4 +46,41 @@ describe("mcp HttpApi", () => { expect(response.status).toBe(200) expect(await response.json()).toEqual({ demo: { status: "disabled" } }) }) + + test("serves add, connect, and disconnect endpoints", async () => { + await using tmp = await tmpdir({ + config: { + mcp: { + demo: { + type: "local", + command: ["echo", "demo"], + enabled: false, + }, + }, + }, + }) + + const added = await request(McpPaths.status, tmp.path, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + name: "added", + config: { + type: "local", + command: ["echo", "added"], + enabled: false, + }, + }), + }) + expect(added.status).toBe(200) + expect(await added.json()).toMatchObject({ added: { status: "disabled" } }) + + const connected = await request("/mcp/demo/connect", tmp.path, { method: "POST" }) + expect(connected.status).toBe(200) + expect(await connected.json()).toBe(true) + + const disconnected = await request("/mcp/demo/disconnect", tmp.path, { method: "POST" }) + expect(disconnected.status).toBe(200) + expect(await disconnected.json()).toBe(true) + }) }) From 3e35c974a4795da40eaa52ce2e7a7882b88faa9f Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Apr 2026 23:17:18 +0000 Subject: [PATCH 43/45] chore: generate --- packages/opencode/specs/effect/http-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 2be0261ea..373b8d7e8 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -179,7 +179,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `project` | `bridged` | list, current, git init, update | | `file` | `bridged` partial | find text/file/symbol, list/content/status | | `mcp` | `bridged` partial | status, add, connect/disconnect; OAuth remains | -| `workspace` | `bridged` partial | adaptor/list/status; create/remove/session-restore remain | +| `workspace` | `bridged` partial | adaptor/list/status; create/remove/session-restore remain | | top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose | | experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list/mutations, resource list; global session list remains later | | `session` | `later/special` | large stateful surface plus streaming | From 450128f9be8f2028cbfbc361043c91c0e0943bba Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Apr 2026 19:27:11 -0400 Subject: [PATCH 44/45] feat(httpapi): bridge mcp oauth endpoints (#24405) --- packages/opencode/specs/effect/http-api.md | 14 +-- .../src/server/routes/instance/httpapi/mcp.ts | 88 ++++++++++++++++++- .../src/server/routes/instance/index.ts | 4 + .../opencode/test/server/httpapi-mcp.test.ts | 24 +++++ 4 files changed, 121 insertions(+), 9 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 373b8d7e8..b4103f7c2 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -178,8 +178,8 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `config` | `bridged` | read, providers, update | | `project` | `bridged` | list, current, git init, update | | `file` | `bridged` partial | find text/file/symbol, list/content/status | -| `mcp` | `bridged` partial | status, add, connect/disconnect; OAuth remains | -| `workspace` | `bridged` partial | adaptor/list/status; create/remove/session-restore remain | +| `mcp` | `bridged` | status, add, OAuth, connect/disconnect | +| `workspace` | `bridged` partial | adaptor/list/status; create/remove/session-restore remain | | top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose | | experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list/mutations, resource list; global session list remains later | | `session` | `later/special` | large stateful surface plus streaming | @@ -248,10 +248,10 @@ This checklist tracks bridge parity only. Checked routes are available through t - [x] `GET /mcp` - MCP status. - [x] `POST /mcp` - add MCP server at runtime. -- [ ] `POST /mcp/:name/auth` - start MCP OAuth. -- [ ] `POST /mcp/:name/auth/callback` - finish MCP OAuth callback. -- [ ] `POST /mcp/:name/auth/authenticate` - run MCP OAuth authenticate flow. -- [ ] `DELETE /mcp/:name/auth` - remove MCP OAuth credentials. +- [x] `POST /mcp/:name/auth` - start MCP OAuth. +- [x] `POST /mcp/:name/auth/callback` - finish MCP OAuth callback. +- [x] `POST /mcp/:name/auth/authenticate` - run MCP OAuth authenticate flow. +- [x] `DELETE /mcp/:name/auth` - remove MCP OAuth credentials. - [x] `POST /mcp/:name/connect` - connect MCP server. - [x] `POST /mcp/:name/disconnect` - disconnect MCP server. @@ -349,7 +349,7 @@ Prefer smaller PRs from here so route behavior and SDK/OpenAPI fallout stays rev 1. [x] Bridge `PATCH /project/:projectID`. 2. [x] Bridge MCP add/connect/disconnect routes. -3. [ ] Bridge MCP OAuth routes: start, callback, authenticate, remove. +3. [x] Bridge MCP OAuth routes: start, callback, authenticate, remove. 4. [ ] Bridge experimental console switch and tool list routes. 5. [ ] Bridge experimental global session list. 6. [ ] Bridge workspace create/remove/session-restore routes. diff --git a/packages/opencode/src/server/routes/instance/httpapi/mcp.ts b/packages/opencode/src/server/routes/instance/httpapi/mcp.ts index 81ca68e2c..e039584b8 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/mcp.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/mcp.ts @@ -1,7 +1,7 @@ import { MCP } from "@/mcp" import { ConfigMCP } from "@/config/mcp" import { Effect, Layer, Schema } from "effect" -import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "./auth" const AddPayload = Schema.Struct({ @@ -10,9 +10,22 @@ const AddPayload = Schema.Struct({ }).annotate({ identifier: "McpAddInput" }) const StatusMap = Schema.Record(Schema.String, MCP.Status) +const AuthStartResponse = Schema.Struct({ + authorizationUrl: Schema.String, + oauthState: Schema.String, +}).annotate({ identifier: "McpAuthStartResponse" }) +const AuthCallbackPayload = Schema.Struct({ + code: Schema.String, +}).annotate({ identifier: "McpAuthCallbackInput" }) +const AuthRemoveResponse = Schema.Struct({ + success: Schema.Literal(true), +}).annotate({ identifier: "McpAuthRemoveResponse" }) export const McpPaths = { status: "/mcp", + auth: "/mcp/:name/auth", + authCallback: "/mcp/:name/auth/callback", + authAuthenticate: "/mcp/:name/auth/authenticate", connect: "/mcp/:name/connect", disconnect: "/mcp/:name/disconnect", } as const @@ -40,6 +53,47 @@ export const McpApi = HttpApi.make("mcp") description: "Dynamically add a new Model Context Protocol (MCP) server to the system.", }), ), + HttpApiEndpoint.post("authStart", McpPaths.auth, { + params: { name: Schema.String }, + success: AuthStartResponse, + }).annotateMerge( + OpenApi.annotations({ + identifier: "mcp.auth.start", + summary: "Start MCP OAuth", + description: "Start OAuth authentication flow for a Model Context Protocol (MCP) server.", + }), + ), + HttpApiEndpoint.post("authCallback", McpPaths.authCallback, { + params: { name: Schema.String }, + payload: AuthCallbackPayload, + success: MCP.Status, + }).annotateMerge( + OpenApi.annotations({ + identifier: "mcp.auth.callback", + summary: "Complete MCP OAuth", + description: "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.", + }), + ), + HttpApiEndpoint.post("authAuthenticate", McpPaths.authAuthenticate, { + params: { name: Schema.String }, + success: MCP.Status, + }).annotateMerge( + OpenApi.annotations({ + identifier: "mcp.auth.authenticate", + summary: "Authenticate MCP OAuth", + description: "Start OAuth flow and wait for callback (opens browser).", + }), + ), + HttpApiEndpoint.delete("authRemove", McpPaths.auth, { + params: { name: Schema.String }, + success: AuthRemoveResponse, + }).annotateMerge( + OpenApi.annotations({ + identifier: "mcp.auth.remove", + summary: "Remove MCP OAuth", + description: "Remove OAuth credentials for an MCP server.", + }), + ), HttpApiEndpoint.post("connect", McpPaths.connect, { params: { name: Schema.String }, success: Schema.Boolean, @@ -89,6 +143,28 @@ export const mcpHandlers = Layer.unwrap( return Schema.decodeUnknownSync(StatusMap)("status" in result ? { [payload.name]: result } : result) }) + const authStart = Effect.fn("McpHttpApi.authStart")(function* (ctx: { params: { name: string } }) { + if (!(yield* mcp.supportsOAuth(ctx.params.name))) return yield* new HttpApiError.BadRequest({}) + return yield* mcp.startAuth(ctx.params.name) + }) + + const authCallback = Effect.fn("McpHttpApi.authCallback")(function* (ctx: { + params: { name: string } + payload: typeof AuthCallbackPayload.Type + }) { + return yield* mcp.finishAuth(ctx.params.name, ctx.payload.code) + }) + + const authAuthenticate = Effect.fn("McpHttpApi.authAuthenticate")(function* (ctx: { params: { name: string } }) { + if (!(yield* mcp.supportsOAuth(ctx.params.name))) return yield* new HttpApiError.BadRequest({}) + return yield* mcp.authenticate(ctx.params.name) + }) + + const authRemove = Effect.fn("McpHttpApi.authRemove")(function* (ctx: { params: { name: string } }) { + yield* mcp.removeAuth(ctx.params.name) + return { success: true as const } + }) + const connect = Effect.fn("McpHttpApi.connect")(function* (ctx: { params: { name: string } }) { yield* mcp.connect(ctx.params.name) return true @@ -100,7 +176,15 @@ export const mcpHandlers = Layer.unwrap( }) return HttpApiBuilder.group(McpApi, "mcp", (handlers) => - handlers.handle("status", status).handle("add", add).handle("connect", connect).handle("disconnect", disconnect), + handlers + .handle("status", status) + .handle("add", add) + .handle("authStart", authStart) + .handle("authCallback", authCallback) + .handle("authAuthenticate", authAuthenticate) + .handle("authRemove", authRemove) + .handle("connect", connect) + .handle("disconnect", disconnect), ) }), ).pipe(Layer.provide(MCP.defaultLayer)) diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index ab8632b5c..ad686ba08 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -80,6 +80,10 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.get(InstancePaths.formatter, (c) => handler(c.req.raw, context)) app.get(McpPaths.status, (c) => handler(c.req.raw, context)) app.post(McpPaths.status, (c) => handler(c.req.raw, context)) + app.post(McpPaths.auth, (c) => handler(c.req.raw, context)) + app.post(McpPaths.authCallback, (c) => handler(c.req.raw, context)) + app.post(McpPaths.authAuthenticate, (c) => handler(c.req.raw, context)) + app.delete(McpPaths.auth, (c) => handler(c.req.raw, context)) app.post(McpPaths.connect, (c) => handler(c.req.raw, context)) app.post(McpPaths.disconnect, (c) => handler(c.req.raw, context)) } diff --git a/packages/opencode/test/server/httpapi-mcp.test.ts b/packages/opencode/test/server/httpapi-mcp.test.ts index 68144503b..07d0b72ed 100644 --- a/packages/opencode/test/server/httpapi-mcp.test.ts +++ b/packages/opencode/test/server/httpapi-mcp.test.ts @@ -83,4 +83,28 @@ describe("mcp HttpApi", () => { expect(disconnected.status).toBe(200) expect(await disconnected.json()).toBe(true) }) + + test("serves deterministic OAuth endpoints", async () => { + await using tmp = await tmpdir({ + config: { + mcp: { + demo: { + type: "local", + command: ["echo", "demo"], + enabled: false, + }, + }, + }, + }) + + const start = await request("/mcp/demo/auth", tmp.path, { method: "POST" }) + expect(start.status).toBe(400) + + const authenticate = await request("/mcp/demo/auth/authenticate", tmp.path, { method: "POST" }) + expect(authenticate.status).toBe(400) + + const removed = await request("/mcp/demo/auth", tmp.path, { method: "DELETE" }) + expect(removed.status).toBe(200) + expect(await removed.json()).toEqual({ success: true }) + }) }) From f77277a69ee1d094235c65d24a4f2d601943e922 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Apr 2026 23:28:25 +0000 Subject: [PATCH 45/45] chore: generate --- packages/opencode/specs/effect/http-api.md | 2 +- packages/opencode/src/server/routes/instance/httpapi/mcp.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index b4103f7c2..2deec51a2 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -179,7 +179,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `project` | `bridged` | list, current, git init, update | | `file` | `bridged` partial | find text/file/symbol, list/content/status | | `mcp` | `bridged` | status, add, OAuth, connect/disconnect | -| `workspace` | `bridged` partial | adaptor/list/status; create/remove/session-restore remain | +| `workspace` | `bridged` partial | adaptor/list/status; create/remove/session-restore remain | | top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose | | experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list/mutations, resource list; global session list remains later | | `session` | `later/special` | large stateful surface plus streaming | diff --git a/packages/opencode/src/server/routes/instance/httpapi/mcp.ts b/packages/opencode/src/server/routes/instance/httpapi/mcp.ts index e039584b8..2abc8519a 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/mcp.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/mcp.ts @@ -71,7 +71,8 @@ export const McpApi = HttpApi.make("mcp") OpenApi.annotations({ identifier: "mcp.auth.callback", summary: "Complete MCP OAuth", - description: "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.", + description: + "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.", }), ), HttpApiEndpoint.post("authAuthenticate", McpPaths.authAuthenticate, {