Merge origin/main into feature/shared-packages

Resolved conflicts:
- list.ts: Keep @automaker/git-utils import, add worktree-metadata import
- feature-loader.ts: Use Feature type from @automaker/types
- automaker-paths.test.ts: Import from @automaker/platform
- kanban-card.tsx: Accept deletion (split into components/)
- subprocess.test.ts: Keep libs/platform location

Added missing exports to @automaker/platform:
- getGlobalSettingsPath, getCredentialsPath, getProjectSettingsPath, ensureDataDir

Added title and titleGenerating fields to @automaker/types Feature interface.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kacper
2025-12-20 22:20:17 +01:00
108 changed files with 10834 additions and 3489 deletions

View File

@@ -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
}
}