/** * Worktree metadata storage utilities * Stores worktree-specific data in .automaker/worktrees/:branch/worktree.json */ import * as fs from "fs/promises"; import * as path from "path"; /** Maximum length for sanitized branch names in filesystem paths */ const MAX_SANITIZED_BRANCH_PATH_LENGTH = 200; export interface WorktreePRInfo { number: number; url: string; title: string; state: string; createdAt: string; } export interface WorktreeMetadata { branch: string; createdAt: string; pr?: WorktreePRInfo; } /** * Sanitize branch name for cross-platform filesystem safety */ function sanitizeBranchName(branch: string): string { // Replace characters that are invalid or problematic on various filesystems: // - Forward and backslashes (path separators) // - Windows invalid chars: : * ? " < > | // - Other potentially problematic chars let safeBranch = branch .replace(/[/\\:*?"<>|]/g, "-") // Replace invalid chars with dash .replace(/\s+/g, "_") // Replace spaces with underscores .replace(/\.+$/g, "") // Remove trailing dots (Windows issue) .replace(/-+/g, "-") // Collapse multiple dashes .replace(/^-|-$/g, ""); // Remove leading/trailing dashes // Truncate to safe length (leave room for path components) safeBranch = safeBranch.substring(0, MAX_SANITIZED_BRANCH_PATH_LENGTH); // Handle Windows reserved names (CON, PRN, AUX, NUL, COM1-9, LPT1-9) const windowsReserved = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i; if (windowsReserved.test(safeBranch) || safeBranch.length === 0) { safeBranch = `_${safeBranch || "branch"}`; } return safeBranch; } /** * Get the path to the worktree metadata directory */ function getWorktreeMetadataDir(projectPath: string, branch: string): string { const safeBranch = sanitizeBranchName(branch); return path.join(projectPath, ".automaker", "worktrees", safeBranch); } /** * Get the path to the worktree metadata file */ function getWorktreeMetadataPath(projectPath: string, branch: string): string { return path.join(getWorktreeMetadataDir(projectPath, branch), "worktree.json"); } /** * Read worktree metadata for a branch */ export async function readWorktreeMetadata( projectPath: string, branch: string ): Promise { try { const metadataPath = getWorktreeMetadataPath(projectPath, branch); const content = await fs.readFile(metadataPath, "utf-8"); return JSON.parse(content) as WorktreeMetadata; } catch (error) { // File doesn't exist or can't be read return null; } } /** * Write worktree metadata for a branch */ export async function writeWorktreeMetadata( projectPath: string, branch: string, metadata: WorktreeMetadata ): Promise { const metadataDir = getWorktreeMetadataDir(projectPath, branch); const metadataPath = getWorktreeMetadataPath(projectPath, branch); // Ensure directory exists await fs.mkdir(metadataDir, { recursive: true }); // Write metadata await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8"); } /** * Update PR info in worktree metadata */ export async function updateWorktreePRInfo( projectPath: string, branch: string, prInfo: WorktreePRInfo ): Promise { // Read existing metadata or create new let metadata = await readWorktreeMetadata(projectPath, branch); if (!metadata) { metadata = { branch, createdAt: new Date().toISOString(), }; } // Update PR info metadata.pr = prInfo; // Write back await writeWorktreeMetadata(projectPath, branch, metadata); } /** * Get PR info for a branch from metadata */ export async function getWorktreePRInfo( projectPath: string, branch: string ): Promise { const metadata = await readWorktreeMetadata(projectPath, branch); return metadata?.pr || null; } /** * Read all worktree metadata for a project */ export async function readAllWorktreeMetadata( projectPath: string ): Promise> { const result = new Map(); const worktreesDir = path.join(projectPath, ".automaker", "worktrees"); try { const dirs = await fs.readdir(worktreesDir, { withFileTypes: true }); for (const dir of dirs) { if (dir.isDirectory()) { const metadataPath = path.join(worktreesDir, dir.name, "worktree.json"); try { const content = await fs.readFile(metadataPath, "utf-8"); const metadata = JSON.parse(content) as WorktreeMetadata; result.set(metadata.branch, metadata); } catch { // Skip if file doesn't exist or can't be read } } } } catch { // Directory doesn't exist } return result; } /** * Delete worktree metadata for a branch */ export async function deleteWorktreeMetadata( projectPath: string, branch: string ): Promise { const metadataDir = getWorktreeMetadataDir(projectPath, branch); try { await fs.rm(metadataDir, { recursive: true, force: true }); } catch { // Ignore errors if directory doesn't exist } }