-
Notifications
You must be signed in to change notification settings - Fork 578
pull-request #173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
pull-request #173
Changes from all commits
b8afb6c
6a8f5c6
9bfcb91
d4365de
6c25680
ec7c289
bb5f68c
3842eb1
ff6a5a5
19fd23c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| /** | ||
| * 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<WorktreeMetadata | null> { | ||
| 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<void> { | ||
| 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<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 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<void> { | ||
| const metadataDir = getWorktreeMetadataDir(projectPath, branch); | ||
| try { | ||
| await fs.rm(metadataDir, { recursive: true, force: true }); | ||
| } catch { | ||
| // Ignore errors if directory doesn't exist | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,9 +14,87 @@ import { | |||||||||||||||||||||||||||||||||
| import { FeatureLoader } from "../../services/feature-loader.js"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const logger = createLogger("Worktree"); | ||||||||||||||||||||||||||||||||||
| const execAsync = promisify(exec); | ||||||||||||||||||||||||||||||||||
| export const execAsync = promisify(exec); | ||||||||||||||||||||||||||||||||||
| const featureLoader = new FeatureLoader(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // ============================================================================ | ||||||||||||||||||||||||||||||||||
| // Constants | ||||||||||||||||||||||||||||||||||
| // ============================================================================ | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** Maximum allowed length for git branch names */ | ||||||||||||||||||||||||||||||||||
| export const MAX_BRANCH_NAME_LENGTH = 250; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // ============================================================================ | ||||||||||||||||||||||||||||||||||
| // Extended PATH configuration for Electron apps | ||||||||||||||||||||||||||||||||||
| // ============================================================================ | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const pathSeparator = process.platform === "win32" ? ";" : ":"; | ||||||||||||||||||||||||||||||||||
| const additionalPaths: string[] = []; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (process.platform === "win32") { | ||||||||||||||||||||||||||||||||||
| // Windows paths | ||||||||||||||||||||||||||||||||||
| if (process.env.LOCALAPPDATA) { | ||||||||||||||||||||||||||||||||||
| additionalPaths.push(`${process.env.LOCALAPPDATA}\\Programs\\Git\\cmd`); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if (process.env.PROGRAMFILES) { | ||||||||||||||||||||||||||||||||||
| additionalPaths.push(`${process.env.PROGRAMFILES}\\Git\\cmd`); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if (process.env["ProgramFiles(x86)"]) { | ||||||||||||||||||||||||||||||||||
| additionalPaths.push(`${process.env["ProgramFiles(x86)"]}\\Git\\cmd`); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| // Unix/Mac paths | ||||||||||||||||||||||||||||||||||
| additionalPaths.push( | ||||||||||||||||||||||||||||||||||
| "/opt/homebrew/bin", // Homebrew on Apple Silicon | ||||||||||||||||||||||||||||||||||
| "/usr/local/bin", // Homebrew on Intel Mac, common Linux location | ||||||||||||||||||||||||||||||||||
| "/home/linuxbrew/.linuxbrew/bin", // Linuxbrew | ||||||||||||||||||||||||||||||||||
| `${process.env.HOME}/.local/bin`, // pipx, other user installs | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+46
to
+52
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard against undefined Line 51 uses 🔎 Proposed fix } else {
// Unix/Mac paths
additionalPaths.push(
"/opt/homebrew/bin", // Homebrew on Apple Silicon
"/usr/local/bin", // Homebrew on Intel Mac, common Linux location
"/home/linuxbrew/.linuxbrew/bin", // Linuxbrew
- `${process.env.HOME}/.local/bin`, // pipx, other user installs
);
+ if (process.env.HOME) {
+ additionalPaths.push(`${process.env.HOME}/.local/bin`);
+ }
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const extendedPath = [ | ||||||||||||||||||||||||||||||||||
| process.env.PATH, | ||||||||||||||||||||||||||||||||||
| ...additionalPaths.filter(Boolean), | ||||||||||||||||||||||||||||||||||
| ].filter(Boolean).join(pathSeparator); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Environment variables with extended PATH for executing shell commands. | ||||||||||||||||||||||||||||||||||
| * Electron apps don't inherit the user's shell PATH, so we need to add | ||||||||||||||||||||||||||||||||||
| * common tool installation locations. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| export const execEnv = { | ||||||||||||||||||||||||||||||||||
| ...process.env, | ||||||||||||||||||||||||||||||||||
| PATH: extendedPath, | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // ============================================================================ | ||||||||||||||||||||||||||||||||||
| // Validation utilities | ||||||||||||||||||||||||||||||||||
| // ============================================================================ | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Validate branch name to prevent command injection. | ||||||||||||||||||||||||||||||||||
| * Git branch names cannot contain: space, ~, ^, :, ?, *, [, \, or control chars. | ||||||||||||||||||||||||||||||||||
| * We also reject shell metacharacters for safety. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| export function isValidBranchName(name: string): boolean { | ||||||||||||||||||||||||||||||||||
| return /^[a-zA-Z0-9._\-/]+$/.test(name) && name.length < MAX_BRANCH_NAME_LENGTH; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Check if gh CLI is available on the system | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| export async function isGhCliAvailable(): Promise<boolean> { | ||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| const checkCommand = process.platform === "win32" | ||||||||||||||||||||||||||||||||||
| ? "where gh" | ||||||||||||||||||||||||||||||||||
| : "command -v gh"; | ||||||||||||||||||||||||||||||||||
| await execAsync(checkCommand, { env: execEnv }); | ||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export const AUTOMAKER_INITIAL_COMMIT_MESSAGE = | ||||||||||||||||||||||||||||||||||
| "chore: automaker initial commit"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.