Skip to content
Open
Show file tree
Hide file tree
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
53 changes: 42 additions & 11 deletions packages/opencode/src/cli/cmd/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,39 @@ export type ShareData =
| { type: "session_diff"; data: unknown }
| { type: "model"; data: unknown }

type SessionImportData = {
info: SDKSession
messages: Array<{
info: Message
parts: Part[]
}>
}

export async function readSessionImportFile(file: string) {
if (!(await Filesystem.exists(file))) {
return {
type: "not_found" as const,
file,
}
}

const content = await Filesystem.readText(file)
const data = await Promise.resolve()
.then(() => JSON.parse(content) as SessionImportData)
.catch(() => undefined)
if (!data) {
return {
type: "unsupported" as const,
file,
}
}

return {
type: "data" as const,
data,
}
}

/** Extract share ID from a share URL like https://opncd.ai/share/abc123 */
export function parseShareUrl(url: string): string | null {
const match = url.match(/^https?:\/\/[^/]+\/share\/([a-zA-Z0-9_-]+)$/)
Expand Down Expand Up @@ -86,15 +119,7 @@ export const ImportCommand = cmd({
},
handler: async (args) => {
await bootstrap(process.cwd(), async () => {
let exportData:
| {
info: SDKSession
messages: Array<{
info: Message
parts: Part[]
}>
}
| undefined
let exportData: SessionImportData | undefined

const isUrl = args.file.startsWith("http://") || args.file.startsWith("https://")

Expand Down Expand Up @@ -140,12 +165,18 @@ export const ImportCommand = cmd({

exportData = transformed
} else {
exportData = await Filesystem.readJson<NonNullable<typeof exportData>>(args.file).catch(() => undefined)
if (!exportData) {
const result = await readSessionImportFile(args.file)
if (result.type === "not_found") {
process.stdout.write(`File not found: ${args.file}`)
process.stdout.write(EOL)
return
}
if (result.type === "unsupported") {
process.stdout.write(`Unsupported format: ${args.file}. Session data must be a JSON file.`)
process.stdout.write(EOL)
return
}
exportData = result.data
}

if (!exportData) {
Expand Down
30 changes: 30 additions & 0 deletions packages/opencode/test/cli/import.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { test, expect } from "bun:test"
import {
readSessionImportFile,
parseShareUrl,
shouldAttachShareAuthHeaders,
transformShareData,
type ShareData,
} from "../../src/cli/cmd/import"
import { tmpdir } from "../fixture/fixture"

// parseShareUrl tests
test("parses valid share URLs", () => {
Expand Down Expand Up @@ -52,3 +54,31 @@ test("returns null for invalid share data", () => {
expect(transformShareData([{ type: "message", data: {} as any }])).toBeNull()
expect(transformShareData([{ type: "session", data: { id: "s" } as any }])).toBeNull() // no messages
})

test("reports existing markdown import files as unsupported format", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const file = `${dir}/session-ses_24ad.md`
await Bun.write(file, "# Session transcript")
return file
},
})

const result = await readSessionImportFile(tmp.extra)

expect(result).toEqual({
type: "unsupported",
file: tmp.extra,
})
})

test("reports missing import files as not found", async () => {
await using tmp = await tmpdir()
const file = `${tmp.path}/missing-session-file.json`
const result = await readSessionImportFile(file)

expect(result).toEqual({
type: "not_found",
file,
})
})
Loading