refactor(cli): convert export command to effectCmd (#25471)

This commit is contained in:
Kit Langton
2026-05-02 17:45:41 -04:00
committed by GitHub
parent b314781a1a
commit e318e173d8
+68 -75
View File
@@ -1,13 +1,13 @@
import type { Argv } from "yargs"
import { Session } from "@/session/session" import { Session } from "@/session/session"
import { MessageV2 } from "../../session/message-v2" import { MessageV2 } from "../../session/message-v2"
import { SessionID } from "../../session/schema" import { SessionID } from "../../session/schema"
import { cmd } from "./cmd" import { effectCmd, fail } from "../effect-cmd"
import { bootstrap } from "../bootstrap"
import { UI } from "../ui" import { UI } from "../ui"
import * as prompts from "@clack/prompts" import * as prompts from "@clack/prompts"
import { EOL } from "os" 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) { function redact(kind: string, id: string, value: string) {
return value.trim() ? `[redacted:${kind}:${id}]` : value 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]", command: "export [sessionID]",
describe: "export session data as JSON", describe: "export session data as JSON",
builder: (yargs: Argv) => { builder: (yargs) =>
return yargs yargs
.positional("sessionID", { .positional("sessionID", {
describe: "session id to export", describe: "session id to export",
type: "string", type: "string",
@@ -232,72 +232,65 @@ export const ExportCommand = cmd({
.option("sanitize", { .option("sanitize", {
describe: "redact sensitive transcript and file data", describe: "redact sensitive transcript and file data",
type: "boolean", type: "boolean",
}) }),
}, handler: Effect.fn("Cli.export")(function* (args) {
handler: async (args) => { const ctx = yield* InstanceRef
await bootstrap(process.cwd(), async () => { if (!ctx) return
let sessionID = args.sessionID ? SessionID.make(args.sessionID) : undefined const store = yield* InstanceStore.Service
process.stderr.write(`Exporting session: ${sessionID ?? "latest"}\n`) return yield* run(args).pipe(Effect.ensuring(store.dispose(ctx)))
}),
if (!sessionID) { })
UI.empty()
prompts.intro("Export session", { const run = Effect.fn("Cli.export.body")(function* (args: { sessionID?: string; sanitize?: boolean }) {
output: process.stderr, 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) {
if (sessions.length === 0) { UI.empty()
prompts.log.error("No sessions found", { prompts.intro("Export session", { output: process.stderr })
output: process.stderr,
}) const sessions = yield* svc.list()
prompts.outro("Done", {
output: process.stderr, if (sessions.length === 0) {
}) prompts.log.error("No sessions found", { output: process.stderr })
return 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", const selectedSession = yield* Effect.promise(() =>
maxItems: 10, prompts.autocomplete({
options: sessions.map((session) => ({ message: "Select session to export",
label: session.title, maxItems: 10,
value: session.id, options: sessions.map((session) => ({
hint: `${new Date(session.time.updated).toLocaleString()}${session.id.slice(-8)}`, label: session.title,
})), value: session.id,
output: process.stderr, 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)) {
sessionID = selectedSession return yield* Effect.die(new UI.CancelledError())
}
prompts.outro("Exporting session...", {
output: process.stderr, sessionID = selectedSession
})
} prompts.outro("Exporting session...", { output: process.stderr })
}
try {
const sessionInfo = await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID!))) // Match legacy try/catch — catches both typed failures and defects
const messages = await AppRuntime.runPromise( // (Session.Service.get throws NotFoundError as a defect, not a typed E).
Session.Service.use((svc) => svc.messages({ sessionID: sessionInfo.id })), return yield* Effect.gen(function* () {
) const sessionInfo = yield* svc.get(sessionID!)
const messages = yield* svc.messages({ sessionID: sessionInfo.id })
const exportData = {
info: sessionInfo, const exportData = { info: sessionInfo, messages }
messages,
} process.stdout.write(JSON.stringify(args.sanitize ? sanitize(exportData) : exportData, null, 2))
process.stdout.write(EOL)
process.stdout.write(JSON.stringify(args.sanitize ? sanitize(exportData) : exportData, null, 2)) }).pipe(Effect.catchCause(() => fail(`Session not found: ${sessionID!}`)))
process.stdout.write(EOL)
} catch {
UI.error(`Session not found: ${sessionID!}`)
process.exit(1)
}
})
},
}) })