Refactor symlink removal into unlinkSessionSymlink helper

This commit is contained in:
Chris Wanstrath 2026-03-09 12:04:04 -07:00
parent 38d5b1c719
commit 385673f420
3 changed files with 32 additions and 16 deletions

View File

@ -1,8 +1,7 @@
import { join } from "path"
import { existsSync } from "fs"
import { unlink } from "fs/promises"
import * as git from "../git.ts"
import * as state from "../state.ts"
import { unlinkSessionSymlink } from "./helpers.ts"
export async function action() {
const root = await git.repoRoot()
@ -23,7 +22,7 @@ export async function action() {
for (const s of stale) {
await state.removeSession(root, s.branch)
await unlink(join(root, '.sandlot', s.branch)).catch(() => {})
await unlinkSessionSymlink(root, s.branch)
console.log(`✔ Removed stale session: ${s.branch}`)
}
}

View File

@ -1,6 +1,6 @@
import { basename, join } from "path"
import { basename, dirname, join } from "path"
import { homedir } from "os"
import { mkdir, symlink, unlink } from "fs/promises"
import { mkdir, readdir, rmdir, symlink, unlink } from "fs/promises"
import { $ } from "bun"
import * as git from "../git.ts"
import * as vm from "../vm.ts"
@ -9,6 +9,22 @@ import { spinner } from "../spinner.ts"
import { die } from "../fmt.ts"
import type { Session } from "../state.ts"
/** Remove a .sandlot/<branch> symlink and prune empty parent dirs up to .sandlot/. */
export async function unlinkSessionSymlink(root: string, branch: string): Promise<void> {
const sandlotDir = join(root, '.sandlot')
const symlinkPath = join(sandlotDir, branch)
await unlink(symlinkPath).catch(() => {})
// Walk up from the symlink's parent, removing empty dirs, stopping at .sandlot/ itself
let dir = dirname(symlinkPath)
while (dir !== sandlotDir) {
const entries = await readdir(dir).catch(() => null)
if (!entries || entries.length > 0) break
await rmdir(dir).catch(() => {})
dir = dirname(dir)
}
}
/** Look up a session by branch, dying if it doesn't exist. */
export async function requireSession(branch: string): Promise<{ root: string; session: Session }> {
const root = await git.repoRoot()
@ -35,12 +51,13 @@ export async function ensureSession(branch: string): Promise<{ root: string; ses
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))
const symlinkPath = join(root, '.sandlot', branch)
await mkdir(dirname(symlinkPath), { recursive: true })
await symlink(worktreeAbs, symlinkPath)
} 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(() => {})
await unlinkSessionSymlink(root, branch)
die(`Failed to recreate session: ${(err as Error).message ?? err}`)
}
@ -60,8 +77,7 @@ export async function teardownSession(root: string, branch: string, worktree: st
await git.removeWorktree(worktree, root)
.catch((e) => console.warn(`⚠ Failed to remove worktree: ${e.message}`))
await unlink(join(root, '.sandlot', branch))
.catch(() => {}) // symlink may not exist
await unlinkSessionSymlink(root, branch)
await state.removeSession(root, branch)
}

View File

@ -1,6 +1,6 @@
import { basename, join } from "path"
import { basename, dirname, join } from "path"
import { homedir } from "os"
import { mkdir, symlink, unlink } from "fs/promises"
import { mkdir, symlink } from "fs/promises"
import * as git from "../git.ts"
import * as vm from "../vm.ts"
import * as state from "../state.ts"
@ -8,7 +8,7 @@ import { spinner } from "../spinner.ts"
import { die } from "../fmt.ts"
import { requireApiKey } from "../env.ts"
import { renderMarkdown } from "../markdown.ts"
import { saveChanges } from "./helpers.ts"
import { saveChanges, unlinkSessionSymlink } from "./helpers.ts"
const ADJECTIVES = [
"calm", "bold", "warm", "cool", "keen", "soft", "fast", "wild", "fair", "rare",
@ -99,8 +99,9 @@ export async function action(
const spin = spinner("Creating worktree", branch)
try {
await git.createWorktree(branch, worktreeAbs, root)
await mkdir(join(root, '.sandlot'), { recursive: true })
await symlink(worktreeAbs, join(root, '.sandlot', branch))
const symlinkPath = join(root, '.sandlot', branch)
await mkdir(dirname(symlinkPath), { recursive: true })
await symlink(worktreeAbs, symlinkPath)
spin.text = "Starting container"
await vm.ensure((msg) => { spin.text = msg })
@ -109,7 +110,7 @@ export async function action(
spin.fail(String((err as Error).message ?? err))
await git.removeWorktree(worktreeAbs, root).catch(() => {})
await git.deleteLocalBranch(branch, root).catch(() => {})
await unlink(join(root, '.sandlot', branch)).catch(() => {})
await unlinkSessionSymlink(root, branch)
process.exit(1)
}