From 1cf81139ed467b512649cdc9f875c908cb992473 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 2 May 2026 15:40:20 -0400 Subject: [PATCH] refactor(cli): convert export command to effectCmd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bootstrap() → effectCmd + Effect.ensuring(store.dispose(ctx)) for disposal parity. Session.Service yielded directly (drops three AppRuntime.runPromise wrappers). UI.CancelledError still propagates as a defect; the try/catch around session lookup becomes Effect.catchCause to match legacy semantics (Session.get surfaces NotFoundError as a defect, not a typed failure). --- packages/opencode/src/cli/cmd/export.ts | 121 +++++++++++------------- 1 file changed, 57 insertions(+), 64 deletions(-) diff --git a/packages/opencode/src/cli/cmd/export.ts b/packages/opencode/src/cli/cmd/export.ts index 62ba20e2ca67..5ff282b543cc 100644 --- a/packages/opencode/src/cli/cmd/export.ts +++ b/packages/opencode/src/cli/cmd/export.ts @@ -1,13 +1,13 @@ -import type { Argv } from "yargs" import { Session } from "@/session/session" import { MessageV2 } from "../../session/message-v2" import { SessionID } from "../../session/schema" -import { cmd } from "./cmd" -import { bootstrap } from "../bootstrap" +import { effectCmd, fail } from "../effect-cmd" import { UI } from "../ui" import * as prompts from "@clack/prompts" import { EOL } from "os" -import { AppRuntime } from "@/effect/app-runtime" +import { Effect } from "effect" +import { InstanceRef } from "@/effect/instance-ref" +import { InstanceStore } from "@/project/instance-store" function redact(kind: string, id: string, value: string) { return value.trim() ? `[redacted:${kind}:${id}]` : value @@ -220,11 +220,11 @@ function sanitize(data: { info: Session.Info; messages: MessageV2.WithParts[] }) } } -export const ExportCommand = cmd({ +export const ExportCommand = effectCmd({ command: "export [sessionID]", describe: "export session data as JSON", - builder: (yargs: Argv) => { - return yargs + builder: (yargs) => + yargs .positional("sessionID", { describe: "session id to export", type: "string", @@ -232,72 +232,65 @@ export const ExportCommand = cmd({ .option("sanitize", { describe: "redact sensitive transcript and file data", type: "boolean", - }) - }, - handler: async (args) => { - await bootstrap(process.cwd(), async () => { - let sessionID = args.sessionID ? SessionID.make(args.sessionID) : undefined - process.stderr.write(`Exporting session: ${sessionID ?? "latest"}\n`) + }), + handler: Effect.fn("Cli.export")(function* (args) { + const ctx = yield* InstanceRef + if (!ctx) return + const store = yield* InstanceStore.Service + return yield* run(args).pipe(Effect.ensuring(store.dispose(ctx))) + }), +}) - if (!sessionID) { - UI.empty() - prompts.intro("Export session", { - output: process.stderr, - }) +const run = Effect.fn("Cli.export.body")(function* (args: { sessionID?: string; sanitize?: boolean }) { + const svc = yield* Session.Service + let sessionID = args.sessionID ? SessionID.make(args.sessionID) : undefined + process.stderr.write(`Exporting session: ${sessionID ?? "latest"}\n`) - const sessions = await AppRuntime.runPromise(Session.Service.use((svc) => svc.list())) + if (!sessionID) { + UI.empty() + prompts.intro("Export session", { output: process.stderr }) - if (sessions.length === 0) { - prompts.log.error("No sessions found", { - output: process.stderr, - }) - prompts.outro("Done", { - output: process.stderr, - }) - return - } + const sessions = yield* svc.list() + + if (sessions.length === 0) { + prompts.log.error("No sessions found", { output: process.stderr }) + prompts.outro("Done", { output: process.stderr }) + return + } - sessions.sort((a, b) => b.time.updated - a.time.updated) + sessions.sort((a, b) => b.time.updated - a.time.updated) - const selectedSession = await prompts.autocomplete({ - message: "Select session to export", - maxItems: 10, - options: sessions.map((session) => ({ - label: session.title, - value: session.id, - hint: `${new Date(session.time.updated).toLocaleString()} • ${session.id.slice(-8)}`, - })), - output: process.stderr, - }) + const selectedSession = yield* Effect.promise(() => + prompts.autocomplete({ + message: "Select session to export", + maxItems: 10, + options: sessions.map((session) => ({ + label: session.title, + value: session.id, + hint: `${new Date(session.time.updated).toLocaleString()} • ${session.id.slice(-8)}`, + })), + output: process.stderr, + }), + ) - if (prompts.isCancel(selectedSession)) { - throw new UI.CancelledError() - } + if (prompts.isCancel(selectedSession)) { + return yield* Effect.die(new UI.CancelledError()) + } - sessionID = selectedSession + sessionID = selectedSession - prompts.outro("Exporting session...", { - output: process.stderr, - }) - } + prompts.outro("Exporting session...", { output: process.stderr }) + } - try { - const sessionInfo = await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID!))) - const messages = await AppRuntime.runPromise( - Session.Service.use((svc) => svc.messages({ sessionID: sessionInfo.id })), - ) + // Match legacy try/catch — catches both typed failures and defects + // (Session.Service.get throws NotFoundError as a defect, not a typed E). + return yield* Effect.gen(function* () { + const sessionInfo = yield* svc.get(sessionID!) + const messages = yield* svc.messages({ sessionID: sessionInfo.id }) - const exportData = { - info: sessionInfo, - messages, - } + const exportData = { info: sessionInfo, messages } - process.stdout.write(JSON.stringify(args.sanitize ? sanitize(exportData) : exportData, null, 2)) - process.stdout.write(EOL) - } catch { - UI.error(`Session not found: ${sessionID!}`) - process.exit(1) - } - }) - }, + process.stdout.write(JSON.stringify(args.sanitize ? sanitize(exportData) : exportData, null, 2)) + process.stdout.write(EOL) + }).pipe(Effect.catchCause(() => fail(`Session not found: ${sessionID!}`))) })