From 7ebbfad8f2180c6196f709e761d48229075bec64 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 25 Feb 2026 20:45:52 -0800 Subject: [PATCH] Add ensureSession to recreate missing worktrees --- src/commands/helpers.ts | 39 +++++++++++++++++++++++++++++++++++++-- src/commands/open.ts | 4 ++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index 72c0529..a5dd416 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -1,5 +1,6 @@ -import { join } from "path" -import { unlink } from "fs/promises" +import { basename, join } from "path" +import { homedir } from "os" +import { mkdir, symlink, unlink } from "fs/promises" import { $ } from "bun" import * as git from "../git.ts" import * as vm from "../vm.ts" @@ -18,6 +19,40 @@ export async function requireSession(branch: string): Promise<{ root: string; se return { root, session } } +/** Look up a session by branch, recreating the worktree/session if the branch exists but the session doesn't. */ +export async function ensureSession(branch: string): Promise<{ root: string; session: Session }> { + const root = await git.repoRoot() + const existing = await state.getSession(root, branch) + if (existing) return { root, session: existing } + + // No session — check if the branch exists + const exists = await git.branchExists(branch, root) + if (!exists) { + die(`No session or branch found for "${branch}".`) + } + + // Recreate worktree and session + const worktreeAbs = join(homedir(), '.sandlot', basename(root), branch) + try { + await git.createWorktree(branch, worktreeAbs, root) + await mkdir(join(root, '.sandlot'), { recursive: true }) + await symlink(worktreeAbs, join(root, '.sandlot', branch)) + } catch (err) { + // Clean up on failure — but do NOT delete the branch (it already existed) + await git.removeWorktree(worktreeAbs, root).catch(() => {}) + await unlink(join(root, '.sandlot', branch)).catch(() => {}) + die(`Failed to recreate session: ${(err as Error).message ?? err}`) + } + + const session: Session = { + branch, + worktree: worktreeAbs, + created_at: new Date().toISOString(), + } + await state.setSession(root, session) + return { root, session } +} + /** Tear down a session: clear activity, remove worktree, unlink symlink, remove state. */ export async function teardownSession(root: string, branch: string, worktree: string): Promise { await vm.clearActivity(worktree, branch) diff --git a/src/commands/open.ts b/src/commands/open.ts index 408007b..fa8cb5f 100644 --- a/src/commands/open.ts +++ b/src/commands/open.ts @@ -2,14 +2,14 @@ import * as vm from "../vm.ts" import * as state from "../state.ts" import { spinner } from "../spinner.ts" import { renderMarkdown } from "../markdown.ts" -import { requireSession, saveChanges } from "./helpers.ts" +import { ensureSession, saveChanges } from "./helpers.ts" export async function action( branch: string, prompt: string | undefined, opts: { print?: string; save?: boolean }, ) { - const { root, session } = await requireSession(branch) + const { root, session } = await ensureSession(branch) const effectivePrompt = opts.print || prompt if (effectivePrompt) {