diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 00000000000..02d42ac9220 --- /dev/null +++ b/SUMMARY.md @@ -0,0 +1,34 @@ +# opencode + +**Open source AI coding agent** — a monorepo for the `opencode` CLI/agent and its surrounding products. + +## What it is + +- AI-powered terminal coding agent (`opencode` CLI, npm: `opencode-ai`), plus a desktop app and web/console sites. +- Built on **Bun**, **TypeScript**, **Effect v4** (smol), **SolidJS**, **Hono**, **Drizzle** (SQLite), **OpenTUI/Solid** for the TUI, and the Vercel **AI SDK** for LMs. +- Two built-in agents: `build` (default, full-access) and `plan` (read-only), with a `general` subagent. + +## Environment variables + +- `OPENCODE_INSTALL_DIR`, `XDG_BIN_DIR` — installer location (see README). + +## Repo layout (Bun workspaces + Turborepo) + +- `packages/opencode` — main CLI/agent (`bun run dev`). +- `packages/app` — web app, `packages/web` — marketing/lander, `packages/desktop` — Electron desktop app. +- `packages/console` + `packages/console/app` + `packages/identity` — hosted console (auth, billing-ish UI) deployed via SST (`sst.config.ts`). +- `packages/stats` — stats service (Cloudflare Worker, see `wrangler.jsonc`). +- `packages/core` — shared core logic (config, sessions, tools, providers, MCP). +- `packages/llm` — model provider adapters on top of the AI SDK. +- `packages/plugin` — plugin/extension system (`@opencode-ai/plugin`). +- `packages/sdk` (+ `sdks/vscode`, `packages/slack`) — JS SDK and integrations. +- `packages/script` — build/CI scripts. +- Supporting libs: `packages/function`, `packages/ui`, `packages/containers`, `packages/extensions`, `packages/docs`, `packages/storybook`, `packages/enterprise`, `packages/cli`, `packages/effect-drizzle-sqlite`, `packages/effect-sqlite-node`, `packages/http-recorder`. +- `infra/` — SST infra; `github/` — GitHub workflow assets; `patches/` — patched deps; `nix/` + `flake.nix` — Nix packaging; `specs/` — specs; `install` — install script. + +## Install + +```bash +curl -fsSL https://opencode.ai/install | bash +# or: npm i -g opencode-ai@latest, brew install anomalyco/tap/opencode, etc. +``` 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 5cbfb347279..7153aca8057 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -999,8 +999,8 @@ export function Session() { variant: error ? "warning" : "success", }) } - } catch { - toast.show({ message: "Failed to export session", variant: "error" }) + } catch (error) { + toast.show({ message: `Failed to export session: ${errorMessage(error)}`, variant: "error" }) } dialog.clear() }, diff --git a/packages/opencode/src/cli/cmd/tui/util/editor.ts b/packages/opencode/src/cli/cmd/tui/util/editor.ts index 4660aafb540..81c17828281 100644 --- a/packages/opencode/src/cli/cmd/tui/util/editor.ts +++ b/packages/opencode/src/cli/cmd/tui/util/editor.ts @@ -5,6 +5,7 @@ import { join } from "node:path" import type { CliRenderer } from "@opentui/core" import { Filesystem } from "@/util/filesystem" import { Process } from "@/util/process" +import { errorMessage } from "@/util/error" import systemOpen from "open" export async function open(opts: { value: string; renderer: CliRenderer; cwd: string }): Promise { @@ -48,10 +49,14 @@ async function createDraft(value: string) { async function openPath(filepath: string, opts: { renderer: CliRenderer; cwd: string }) { const editor = configuredEditor() if (editor) { - await openEditor(filepath, opts, editor) + await openEditor(filepath, opts, editor).catch((error) => { + throw new Error(`Failed to open file with ${editor}: ${errorMessage(error)}. Check $VISUAL or $EDITOR.`) + }) return "editor" as const } - await systemOpen(filepath) + await systemOpen(filepath).catch((error) => { + throw new Error(`Failed to open file: ${errorMessage(error)}. Set $VISUAL or $EDITOR.`) + }) return "system" as const } diff --git a/packages/opencode/test/cli/tui/editor.test.ts b/packages/opencode/test/cli/tui/editor.test.ts index 74c21454f5d..43aec545283 100644 --- a/packages/opencode/test/cli/tui/editor.test.ts +++ b/packages/opencode/test/cli/tui/editor.test.ts @@ -9,9 +9,11 @@ const originalVisual = process.env.VISUAL const originalEditor = process.env.EDITOR const systemOpened: string[] = [] const retained = new Set() +let systemOpenError: Error | undefined void mock.module("open", () => ({ default: async (filepath: string) => { + if (systemOpenError) throw systemOpenError systemOpened.push(filepath) }, })) @@ -24,6 +26,7 @@ afterEach(async () => { if (originalEditor === undefined) delete process.env.EDITOR else process.env.EDITOR = originalEditor systemOpened.length = 0 + systemOpenError = undefined await Promise.all([...retained].map((dir) => rm(dir, { force: true, recursive: true }))) retained.clear() }) @@ -156,6 +159,24 @@ test("openFile uses the platform application when no editor is configured", asyn expect(systemOpened).toEqual([target]) }) +test("openFile suggests configuring an editor when the platform application fails", async () => { + await using tmp = await tmpdir() + delete process.env.VISUAL + delete process.env.EDITOR + systemOpenError = new Error("spawn xdg-open ENOENT") + + const message = await Editor.openFile({ + filepath: "target.md", + renderer: renderer().value, + cwd: tmp.path, + directory: tmp.path, + }) + .then(() => undefined) + .catch(errorMessage) + + expect(message).toBe("Failed to open file: spawn xdg-open ENOENT. Set $VISUAL or $EDITOR.") +}) + test("openFile restores the renderer when the editor exits unsuccessfully", async () => { await using tmp = await tmpdir() process.env.VISUAL = await editor(tmp.path, "editor", "process.exit(7)") @@ -171,7 +192,9 @@ test("openFile restores the renderer when the editor exits unsuccessfully", asyn }) .then(() => undefined) .catch(errorMessage) - expect(message).toBe("Editor exited with code 7") + expect(message).toBe( + `Failed to open file with ${process.execPath} ${join(tmp.path, "editor.ts")}: Editor exited with code 7. Check $VISUAL or $EDITOR.`, + ) expect(render.events).toEqual(["suspend", "clear", "clear", "resume", "render"]) })