File size: 5,460 Bytes
1dbc34b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/**
 * Worktree metadata storage utilities
 * Stores worktree-specific data in .automaker/worktrees/:branch/worktree.json
 */

import * as secureFs from './secure-fs.js';
import * as path from 'path';
import type { PRState, WorktreePRInfo } from '@automaker/types';

// Re-export types for backwards compatibility
export type { PRState, WorktreePRInfo };

/** Maximum length for sanitized branch names in filesystem paths */
const MAX_SANITIZED_BRANCH_PATH_LENGTH = 200;

export interface WorktreeMetadata {
  branch: string;
  createdAt: string;
  pr?: WorktreePRInfo;
  /** Whether the init script has been executed for this worktree */
  initScriptRan?: boolean;
  /** Status of the init script execution */
  initScriptStatus?: 'running' | 'success' | 'failed';
  /** Error message if init script failed */
  initScriptError?: string;
}

/**
 * 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<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) {
    // 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<void> {
  const metadataDir = getWorktreeMetadataDir(projectPath, branch);
  const metadataPath = getWorktreeMetadataPath(projectPath, branch);

  // Ensure directory exists
  await secureFs.mkdir(metadataDir, { recursive: true });

  // Write metadata
  await secureFs.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<void> {
  // 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<WorktreePRInfo | null> {
  const metadata = await readWorktreeMetadata(projectPath, branch);
  return metadata?.pr || null;
}

/**
 * Read all worktree metadata for a project
 */
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 {
          // 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<void> {
  const metadataDir = getWorktreeMetadataDir(projectPath, branch);
  try {
    await secureFs.rm(metadataDir, { recursive: true, force: true });
  } catch {
    // Ignore errors if directory doesn't exist
  }
}