diff --git a/packages/opencode/BUN_SHELL_MIGRATION_PLAN.md b/packages/opencode/BUN_SHELL_MIGRATION_PLAN.md deleted file mode 100644 index 569045c0640..00000000000 --- a/packages/opencode/BUN_SHELL_MIGRATION_PLAN.md +++ /dev/null @@ -1,136 +0,0 @@ -# Bun shell migration plan - -Practical phased replacement of Bun `$` calls. - -## Goal - -Replace runtime Bun shell template-tag usage in `packages/opencode/src` with a unified `Process` API in `util/process.ts`. - -Keep behavior stable while improving safety, testability, and observability. - -Current baseline from audit: - -- 143 runtime command invocations across 17 files -- 84 are git commands -- Largest hotspots: - - `src/cli/cmd/github.ts` (33) - - `src/worktree/index.ts` (22) - - `src/lsp/server.ts` (21) - - `src/installation/index.ts` (20) - - `src/snapshot/index.ts` (18) - -## Decisions - -- Extend `src/util/process.ts` (do not create a separate exec module). -- Proceed with phased migration for both git and non-git paths. -- Keep plugin `$` compatibility in 1.x and remove in 2.0. - -## Non-goals - -- Do not remove plugin `$` compatibility in this effort. -- Do not redesign command semantics beyond what is needed to preserve behavior. - -## Constraints - -- Keep migration phased, not big-bang. -- Minimize behavioral drift. -- Keep these explicit shell-only exceptions: - - `src/session/prompt.ts` raw command execution - - worktree start scripts in `src/worktree/index.ts` - -## Process API proposal (`src/util/process.ts`) - -Add higher-level wrappers on top of current spawn support. - -Core methods: - -- `Process.run(cmd, opts)` -- `Process.text(cmd, opts)` -- `Process.lines(cmd, opts)` -- `Process.status(cmd, opts)` -- `Process.shell(command, opts)` for intentional shell execution - -Git helpers: - -- `Process.git(args, opts)` -- `Process.gitText(args, opts)` - -Shared options: - -- `cwd`, `env`, `stdin`, `stdout`, `stderr`, `abort`, `timeout`, `kill` -- `allowFailure` / non-throw mode -- optional redaction + trace metadata - -Standard result shape: - -- `code`, `stdout`, `stderr`, `duration_ms`, `cmd` -- helpers like `text()` and `arrayBuffer()` where useful - -## Phased rollout - -### Phase 0: Foundation - -- Implement Process wrappers in `src/util/process.ts`. -- Refactor `src/util/git.ts` to use Process only. -- Add tests for exit handling, timeout, abort, and output capture. - -### Phase 1: High-impact hotspots - -Migrate these first: - -- `src/cli/cmd/github.ts` -- `src/worktree/index.ts` -- `src/lsp/server.ts` -- `src/installation/index.ts` -- `src/snapshot/index.ts` - -Within each file, migrate git paths first where applicable. - -### Phase 2: Remaining git-heavy files - -Migrate git-centric call sites to `Process.git*` helpers: - -- `../core/src/filesystem.ts` -- `src/project/vcs.ts` -- `../core/src/filesystem/watcher.ts` -- `src/storage/storage.ts` -- `src/cli/cmd/pr.ts` - -### Phase 3: Remaining non-git files - -Migrate residual non-git usages: - -- `src/cli/cmd/tui/util/clipboard.ts` -- `src/util/archive.ts` -- `../core/src/filesystem/ripgrep.ts` -- `src/tool/bash.ts` -- `src/cli/cmd/uninstall.ts` - -### Phase 4: Stabilize - -- Remove dead wrappers and one-off patterns. -- Keep plugin `$` compatibility isolated and documented as temporary. -- Create linked 2.0 task for plugin `$` removal. - -## Validation strategy - -- Unit tests for new `Process` methods and options. -- Integration tests on hotspot modules. -- Smoke tests for install, snapshot, worktree, and GitHub flows. -- Regression checks for output parsing behavior. - -## Risk mitigation - -- File-by-file PRs with small diffs. -- Preserve behavior first, simplify second. -- Keep shell-only exceptions explicit and documented. -- Add consistent error shaping and logging at Process layer. - -## Definition of done - -- Runtime Bun `$` usage in `packages/opencode/src` is removed except: - - approved shell-only exceptions - - temporary plugin compatibility path (1.x) -- Git paths use `Process.git*` consistently. -- CI and targeted smoke tests pass. -- 2.0 issue exists for plugin `$` removal. diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts index 3ede517150e..21b53aba756 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts @@ -1,4 +1,5 @@ import { NamedError } from "@opencode-ai/core/util/error" +import { ConfigErrorV1 } from "@opencode-ai/core/v1/config/error" import { Cause, Effect } from "effect" import { HttpRouter, HttpServerError, HttpServerRespondable, HttpServerResponse } from "effect/unstable/http" @@ -15,6 +16,15 @@ export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect) if (!defect) return Effect.failCause(cause) const error = defect.defect + if ( + ConfigErrorV1.JsonError.isInstance(error) || + ConfigErrorV1.InvalidError.isInstance(error) || + ConfigErrorV1.FrontmatterError.isInstance(error) || + ConfigErrorV1.DirectoryTypoError.isInstance(error) + ) { + return Effect.succeed(HttpServerResponse.jsonUnsafe(error.toObject(), { status: 400 })) + } + const ref = `err_${crypto.randomUUID().slice(0, 8)}` return Effect.logError("failed", { ref, error, cause: Cause.pretty(cause) }).pipe( diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index aa274142c15..96780426657 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -264,7 +264,7 @@ export function createRoutes( ]), Layer.provide(Layer.succeed(CorsConfig)(corsOptions)), Layer.provide(InstanceLayer.layer), - Layer.provide(Observability.layer), + Layer.provideMerge(Observability.layer), ) } diff --git a/packages/opencode/test/server/httpapi-error-middleware.test.ts b/packages/opencode/test/server/httpapi-error-middleware.test.ts index a78e0aafe56..7bfc6da032f 100644 --- a/packages/opencode/test/server/httpapi-error-middleware.test.ts +++ b/packages/opencode/test/server/httpapi-error-middleware.test.ts @@ -53,7 +53,7 @@ describe("HttpApi error middleware", () => { }), ) - it.live("does not expose config defects from generic middleware", () => + it.live("returns invalid config defects as structured client errors", () => Effect.gen(function* () { const configError = new ConfigErrorV1.InvalidError({ path: "/tmp/opencode.json", @@ -70,11 +70,16 @@ describe("HttpApi error middleware", () => { const body = yield* response.json const serialized = JSON.stringify(body) - expect(response.status).toBe(500) - expectUnknownErrorBody(body) - expect(serialized).not.toContain("/tmp/opencode.json") - expect(serialized).not.toContain("provider") - expect(serialized).not.toContain("anthropic") + expect(response.status).toBe(400) + expect(body).toMatchObject({ + name: "ConfigInvalidError", + data: { + path: "/tmp/opencode.json", + issues: [{ message: "Expected object", path: ["provider", "anthropic", "options"] }], + }, + }) + expect(serialized).toContain("/tmp/opencode.json") + expect(serialized).toContain("anthropic") }), ) diff --git a/packages/tui/src/util/renderer.ts b/packages/tui/src/util/renderer.ts index 7bcd28ab655..66c1318f383 100644 --- a/packages/tui/src/util/renderer.ts +++ b/packages/tui/src/util/renderer.ts @@ -1,6 +1,8 @@ import type { CliRenderer } from "@opentui/core" export function destroyRenderer(renderer: Pick) { - renderer.setTerminalTitle("") - if (!renderer.isDestroyed) renderer.destroy() + if (!renderer.isDestroyed) { + renderer.setTerminalTitle("") + renderer.destroy() + } }