| |
| |
| |
| |
|
|
| import * as secureFs from './secure-fs.js'; |
| import * as path from 'path'; |
| import type { PRState, WorktreePRInfo } from '@automaker/types'; |
|
|
| |
| export type { PRState, WorktreePRInfo }; |
|
|
| |
| const MAX_SANITIZED_BRANCH_PATH_LENGTH = 200; |
|
|
| export interface WorktreeMetadata { |
| branch: string; |
| createdAt: string; |
| pr?: WorktreePRInfo; |
| |
| initScriptRan?: boolean; |
| |
| initScriptStatus?: 'running' | 'success' | 'failed'; |
| |
| initScriptError?: string; |
| } |
|
|
| |
| |
| |
| function sanitizeBranchName(branch: string): string { |
| |
| |
| |
| |
| let safeBranch = branch |
| .replace(/[/\\:*?"<>|]/g, '-') |
| .replace(/\s+/g, '_') |
| .replace(/\.+$/g, '') |
| .replace(/-+/g, '-') |
| .replace(/^-|-$/g, ''); |
|
|
| |
| safeBranch = safeBranch.substring(0, MAX_SANITIZED_BRANCH_PATH_LENGTH); |
|
|
| |
| 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; |
| } |
|
|
| |
| |
| |
| function getWorktreeMetadataDir(projectPath: string, branch: string): string { |
| const safeBranch = sanitizeBranchName(branch); |
| return path.join(projectPath, '.automaker', 'worktrees', safeBranch); |
| } |
|
|
| |
| |
| |
| function getWorktreeMetadataPath(projectPath: string, branch: string): string { |
| return path.join(getWorktreeMetadataDir(projectPath, branch), 'worktree.json'); |
| } |
|
|
| |
| |
| |
| export async function readWorktreeMetadata( |
| projectPath: string, |
| branch: string |
| ): Promise<WorktreeMetadata | null> { |
| try { |
| const metadataPath = getWorktreeMetadataPath(projectPath, branch); |
| const content = (await secureFs.readFile(metadataPath, 'utf-8')) as string; |
| return JSON.parse(content) as WorktreeMetadata; |
| } catch (_error) { |
| |
| return null; |
| } |
| } |
|
|
| |
| |
| |
| export async function writeWorktreeMetadata( |
| projectPath: string, |
| branch: string, |
| metadata: WorktreeMetadata |
| ): Promise<void> { |
| const metadataDir = getWorktreeMetadataDir(projectPath, branch); |
| const metadataPath = getWorktreeMetadataPath(projectPath, branch); |
|
|
| |
| await secureFs.mkdir(metadataDir, { recursive: true }); |
|
|
| |
| await secureFs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8'); |
| } |
|
|
| |
| |
| |
| export async function updateWorktreePRInfo( |
| projectPath: string, |
| branch: string, |
| prInfo: WorktreePRInfo |
| ): Promise<void> { |
| |
| let metadata = await readWorktreeMetadata(projectPath, branch); |
|
|
| if (!metadata) { |
| metadata = { |
| branch, |
| createdAt: new Date().toISOString(), |
| }; |
| } |
|
|
| |
| metadata.pr = prInfo; |
|
|
| |
| await writeWorktreeMetadata(projectPath, branch, metadata); |
| } |
|
|
| |
| |
| |
| export async function getWorktreePRInfo( |
| projectPath: string, |
| branch: string |
| ): Promise<WorktreePRInfo | null> { |
| const metadata = await readWorktreeMetadata(projectPath, branch); |
| return metadata?.pr || null; |
| } |
|
|
| |
| |
| |
| export async function readAllWorktreeMetadata( |
| projectPath: string |
| ): Promise<Map<string, WorktreeMetadata>> { |
| const result = new Map<string, WorktreeMetadata>(); |
| const worktreesDir = path.join(projectPath, '.automaker', 'worktrees'); |
|
|
| try { |
| const dirs = await secureFs.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 secureFs.readFile(metadataPath, 'utf-8')) as string; |
| const metadata = JSON.parse(content) as WorktreeMetadata; |
| result.set(metadata.branch, metadata); |
| } catch { |
| |
| } |
| } |
| } |
| } catch { |
| |
| } |
|
|
| return result; |
| } |
|
|
| |
| |
| |
| export async function deleteWorktreeMetadata(projectPath: string, branch: string): Promise<void> { |
| const metadataDir = getWorktreeMetadataDir(projectPath, branch); |
| try { |
| await secureFs.rm(metadataDir, { recursive: true, force: true }); |
| } catch { |
| |
| } |
| } |
|
|