Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 57 additions & 64 deletions packages/opencode/src/cli/cmd/export.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -220,84 +220,77 @@ 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",
})
.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!}`)))
})
Loading