diff --git a/packages/cli/src/commands/commands.ts b/packages/cli/src/commands/commands.ts index 99bc62528d3..ef069516eb3 100644 --- a/packages/cli/src/commands/commands.ts +++ b/packages/cli/src/commands/commands.ts @@ -79,6 +79,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME commands: [ Spec.make("agents", { description: "List all agents" }), Spec.make("config", { description: "List configuration sources" }), + Spec.make("paths", { description: "Show global paths (data, config, cache, state)" }), ], }), Spec.make("console", { diff --git a/packages/cli/src/commands/handlers/debug/paths.ts b/packages/cli/src/commands/handlers/debug/paths.ts new file mode 100644 index 00000000000..732dae9ff2e --- /dev/null +++ b/packages/cli/src/commands/handlers/debug/paths.ts @@ -0,0 +1,17 @@ +import { EOL } from "os" +import { Effect } from "effect" +import { Global } from "@opencode-ai/util/global" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" + +export default Runtime.handler( + Commands.commands.debug.commands.paths, + Effect.fn("cli.debug.paths")(function* () { + const global = yield* Global.Service + process.stdout.write( + Object.entries(global) + .map(([key, value]) => `${key.padEnd(10)} ${value}${EOL}`) + .join(""), + ) + }), +) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 49eb0162d34..bbc4595b077 100755 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -27,6 +27,7 @@ const Handlers = Runtime.handlers(Commands, { debug: { agents: () => import("./commands/handlers/debug/agents"), config: () => import("./commands/handlers/debug/config"), + paths: () => import("./commands/handlers/debug/paths"), }, console: { login: () => import("./commands/handlers/console/login"), diff --git a/packages/cli/test/debug-paths.test.ts b/packages/cli/test/debug-paths.test.ts new file mode 100644 index 00000000000..60e00ec4db7 --- /dev/null +++ b/packages/cli/test/debug-paths.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test } from "bun:test" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" + +describe("debug paths command", () => { + test("is included in troubleshooting help", async () => { + const [debug, paths] = await Promise.all([cli(["debug", "--help"]), cli(["debug", "paths", "--help"])]) + + expect(debug.exitCode).toBe(0) + expect(debug.stdout).toContain("paths") + expect(debug.stdout).toContain("Show global paths (data, config, cache, state)") + expect(paths.exitCode).toBe(0) + expect(paths.stdout).toContain("opencode debug paths [flags]") + }) + + test("prints resolved global paths without starting a server", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-debug-paths-")) + + try { + const result = await cli(["debug", "paths"], { + XDG_DATA_HOME: path.join(root, "data"), + XDG_CONFIG_HOME: path.join(root, "config"), + XDG_CACHE_HOME: path.join(root, "cache"), + XDG_STATE_HOME: path.join(root, "state"), + }) + const paths = Object.fromEntries( + result.stdout + .trim() + .split("\n") + .map((line) => line.trim().split(/\s+/, 2)), + ) + + expect({ exitCode: result.exitCode, stderr: result.stderr }).toEqual({ exitCode: 0, stderr: "" }) + expect(paths).toMatchObject({ + home: os.homedir(), + data: path.join(root, "data", "opencode"), + config: path.join(root, "config", "opencode"), + cache: path.join(root, "cache", "opencode"), + state: path.join(root, "state", "opencode"), + bin: path.join(root, "cache", "opencode", "bin"), + log: path.join(root, "data", "opencode", "log"), + repos: path.join(root, "data", "opencode", "repos"), + }) + expect(paths.tmp).toBeTruthy() + expect(await Bun.file(path.join(root, "state", "opencode", "service-local.json")).exists()).toBe(false) + } finally { + await fs.rm(root, { recursive: true, force: true }) + } + }) +}) + +async function cli(args: string[], env?: Record) { + const child = Bun.spawn([process.execPath, "run", path.join(import.meta.dir, "../src/index.ts"), ...args], { + cwd: path.join(import.meta.dir, ".."), + env: { ...process.env, ...env }, + stdout: "pipe", + stderr: "pipe", + }) + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]) + return { stdout, stderr, exitCode } +}