Files
superwork/vscode/src/cc/sessionBundle.ts
T

155 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* claude / codex 会话文件的搬运原语。
*
* claude 会话 = `<projectsDir>/<sid>.jsonl` + 可选 `<projectsDir>/<sid>/`
* (subagents、tool-results),两者必须一起搬。新版 claude 在任意目录
* `--resume` 时会搜全部 project 目录,同一 sid 出现两份就拒绝 resume——
* 所以「安装」总是先拷再删本机其它副本。
*/
import { promises as fsp } from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { ROLLOUT_UUID_REGEX } from './codexSessionWatcher'
export interface ClaudeSessionFiles {
projectsDir: string
jsonl: string
dir?: string
}
export function claudeProjectsRoot(): string {
return path.join(os.homedir(), '.claude', 'projects')
}
async function exists(p: string): Promise<boolean> {
try {
await fsp.stat(p)
return true
}
catch {
return false
}
}
async function sessionFilesIn(projectsDir: string, sid: string): Promise<ClaudeSessionFiles | undefined> {
const jsonl = path.join(projectsDir, `${sid}.jsonl`)
if (!(await exists(jsonl)))
return undefined
const dir = path.join(projectsDir, sid)
return (await exists(dir)) ? { projectsDir, jsonl, dir } : { projectsDir, jsonl }
}
export async function listClaudeSessionCopies(sid: string, projectsRoot: string): Promise<ClaudeSessionFiles[]> {
let entries: string[]
try {
entries = await fsp.readdir(projectsRoot)
}
catch {
return []
}
const out: ClaudeSessionFiles[] = []
for (const name of entries) {
const found = await sessionFilesIn(path.join(projectsRoot, name), sid)
if (found)
out.push(found)
}
return out
}
export async function findClaudeSessionFiles(sid: string, projectsRoot: string): Promise<ClaudeSessionFiles | undefined> {
const copies = await listClaudeSessionCopies(sid, projectsRoot)
if (copies.length === 0)
return undefined
const stamped = await Promise.all(copies.map(async c => ({ c, mtime: (await fsp.stat(c.jsonl)).mtimeMs })))
stamped.sort((a, b) => b.mtime - a.mtime)
return stamped[0].c
}
export async function copyClaudeSessionFiles(files: ClaudeSessionFiles, dstDir: string): Promise<void> {
await fsp.mkdir(dstDir, { recursive: true })
await fsp.copyFile(files.jsonl, path.join(dstDir, path.basename(files.jsonl)))
if (files.dir)
await fsp.cp(files.dir, path.join(dstDir, path.basename(files.dir)), { recursive: true, force: true })
}
async function removeClaudeSessionFiles(files: ClaudeSessionFiles): Promise<void> {
await fsp.rm(files.jsonl, { force: true })
if (files.dir)
await fsp.rm(files.dir, { recursive: true, force: true })
}
export async function installClaudeSession(opts: {
sid: string
srcDir: string
dstProjectsDir: string
projectsRoot: string
}): Promise<void> {
const src = await sessionFilesIn(path.resolve(opts.srcDir), opts.sid)
if (!src)
throw new Error(`源目录缺少会话文件 ${opts.sid}.jsonl:${opts.srcDir}`)
const dst = path.resolve(opts.dstProjectsDir)
if (src.projectsDir !== dst)
await copyClaudeSessionFiles(src, dst)
for (const copy of await listClaudeSessionCopies(opts.sid, opts.projectsRoot)) {
if (path.resolve(copy.projectsDir) !== dst)
await removeClaudeSessionFiles(copy)
}
}
/** codex rollout 全局存放,按文件名尾部 uuid 定位;返回相对 codexRoot 的路径。 */
export async function codexRolloutRelPathFor(sid: string, codexRoot: string): Promise<string | undefined> {
const want = sid.toLowerCase()
let entries: string[]
try {
entries = await fsp.readdir(codexRoot, { recursive: true })
}
catch {
return undefined
}
for (const entry of entries) {
const m = path.basename(entry).match(ROLLOUT_UUID_REGEX)
if (m && m[1].toLowerCase() === want)
return entry
}
return undefined
}
export async function installCodexSession(relPath: string, srcFile: string, codexRoot: string): Promise<void> {
const dst = path.join(codexRoot, relPath)
await fsp.mkdir(path.dirname(dst), { recursive: true })
await fsp.copyFile(srcFile, dst)
}
/**
* 迁移会话里发送方的 worktree 路径重写成接收方的路径。
* split/join 而非正则替换——路径里的 `.` `(` 等字符不用转义,出现次数就是替换次数。
*/
export async function rewritePathInFile(file: string, from: string, to: string): Promise<number> {
const content = await fsp.readFile(file, 'utf8')
const parts = content.split(from)
const count = parts.length - 1
if (count > 0)
await fsp.writeFile(file, parts.join(to))
return count
}
export async function rewritePathInTree(dir: string, from: string, to: string): Promise<number> {
let entries: Array<{ name: string, isDirectory: () => boolean, isFile: () => boolean }>
try {
entries = await fsp.readdir(dir, { withFileTypes: true })
}
catch {
return 0
}
let total = 0
for (const entry of entries) {
const full = path.join(dir, entry.name)
if (entry.isDirectory())
total += await rewritePathInTree(full, from, to)
else if (entry.isFile())
total += await rewritePathInFile(full, from, to)
}
return total
}