( repoRoot: string, signal?: AbortSignal, )
| 427 | * Not wired into the loop here (out of scope); exported for the caller to use. |
| 428 | */ |
| 429 | export async function recoverOrphanedSandbox( |
| 430 | repoRoot: string, |
| 431 | signal?: AbortSignal, |
| 432 | ): Promise<OrphanedSandbox | null> { |
| 433 | try { |
| 434 | if (!(await isGitRepo(repoRoot, signal))) return null; |
| 435 | |
| 436 | // 1. The persisted recovery record, if any. |
| 437 | let record: SandboxRecoveryRecord | null = null; |
| 438 | try { |
| 439 | const raw = await fs.readFile(recoveryRecordPath(repoRoot), 'utf-8'); |
| 440 | const parsed = JSON.parse(raw); |
| 441 | if (parsed && parsed.version === 1 && typeof parsed.branch === 'string') { |
| 442 | record = parsed as SandboxRecoveryRecord; |
| 443 | } |
| 444 | } catch { |
| 445 | /* no record (clean shutdown) or unreadable — fall through to git scan */ |
| 446 | } |
| 447 | |
| 448 | // 2. Leftover sandbox branches. |
| 449 | const br = await git(['branch', '--list', 'qodex/sandbox-*', '--format=%(refname:short)'], { cwd: repoRoot, signal }); |
| 450 | const branches = br.exitCode === 0 |
| 451 | ? br.stdout.split('\n').map((s) => s.trim()).filter(Boolean) |
| 452 | : []; |
| 453 | |
| 454 | // 3. Leftover WIP stashes from a killed sandbox. |
| 455 | const sl = await git(['stash', 'list', '--format=%gd %gs'], { cwd: repoRoot, signal }); |
| 456 | const stashes = sl.exitCode === 0 |
| 457 | ? sl.stdout.split('\n') |
| 458 | .map((l) => l.trim()) |
| 459 | .filter((l) => l.includes('qodex-sandbox-wip-')) |
| 460 | .map((l) => { |
| 461 | const sp = l.split(/\s+/); |
| 462 | return { ref: sp[0] ?? '', message: l.slice((sp[0] ?? '').length).trim() }; |
| 463 | }) |
| 464 | .filter((s) => s.ref) |
| 465 | : []; |
| 466 | |
| 467 | if (!record && branches.length === 0 && stashes.length === 0) return null; |
| 468 | return { record, branches, stashes }; |
| 469 | } catch (e: any) { |
| 470 | logger.debug('recoverOrphanedSandbox failed', { err: e?.message }); |
| 471 | return null; |
| 472 | } |
| 473 | } |
nothing calls this directly
no test coverage detected