Merge branch 'main' into feature-dependency-improvements

This commit is contained in:
Cody Seibert
2025-12-17 00:23:59 -05:00
98 changed files with 13081 additions and 767 deletions

View File

@@ -0,0 +1,84 @@
/**
* Automaker Paths - Utilities for managing automaker data storage
*
* Stores project data inside the project directory at {projectPath}/.automaker/
*/
import fs from "fs/promises";
import path from "path";
/**
* Get the automaker data directory for a project
* This is stored inside the project at .automaker/
*/
export function getAutomakerDir(projectPath: string): string {
return path.join(projectPath, ".automaker");
}
/**
* Get the features directory for a project
*/
export function getFeaturesDir(projectPath: string): string {
return path.join(getAutomakerDir(projectPath), "features");
}
/**
* Get the directory for a specific feature
*/
export function getFeatureDir(projectPath: string, featureId: string): string {
return path.join(getFeaturesDir(projectPath), featureId);
}
/**
* Get the images directory for a feature
*/
export function getFeatureImagesDir(
projectPath: string,
featureId: string
): string {
return path.join(getFeatureDir(projectPath, featureId), "images");
}
/**
* Get the board directory for a project (board backgrounds, etc.)
*/
export function getBoardDir(projectPath: string): string {
return path.join(getAutomakerDir(projectPath), "board");
}
/**
* Get the images directory for a project (general images)
*/
export function getImagesDir(projectPath: string): string {
return path.join(getAutomakerDir(projectPath), "images");
}
/**
* Get the worktrees metadata directory for a project
*/
export function getWorktreesDir(projectPath: string): string {
return path.join(getAutomakerDir(projectPath), "worktrees");
}
/**
* Get the app spec file path for a project
*/
export function getAppSpecPath(projectPath: string): string {
return path.join(getAutomakerDir(projectPath), "app_spec.txt");
}
/**
* Get the branch tracking file path for a project
*/
export function getBranchTrackingPath(projectPath: string): string {
return path.join(getAutomakerDir(projectPath), "active-branches.json");
}
/**
* Ensure the automaker directory structure exists for a project
*/
export async function ensureAutomakerDir(projectPath: string): Promise<string> {
const automakerDir = getAutomakerDir(projectPath);
await fs.mkdir(automakerDir, { recursive: true });
return automakerDir;
}

View File

@@ -0,0 +1,67 @@
/**
* File system utilities that handle symlinks safely
*/
import fs from "fs/promises";
import path from "path";
/**
* Create a directory, handling symlinks safely to avoid ELOOP errors.
* If the path already exists as a directory or symlink, returns success.
*/
export async function mkdirSafe(dirPath: string): Promise<void> {
const resolvedPath = path.resolve(dirPath);
// Check if path already exists using lstat (doesn't follow symlinks)
try {
const stats = await fs.lstat(resolvedPath);
// Path exists - if it's a directory or symlink, consider it success
if (stats.isDirectory() || stats.isSymbolicLink()) {
return;
}
// It's a file - can't create directory
throw new Error(`Path exists and is not a directory: ${resolvedPath}`);
} catch (error: any) {
// ENOENT means path doesn't exist - we should create it
if (error.code !== "ENOENT") {
// Some other error (could be ELOOP in parent path)
// If it's ELOOP, the path involves symlinks - don't try to create
if (error.code === "ELOOP") {
console.warn(`[fs-utils] Symlink loop detected at ${resolvedPath}, skipping mkdir`);
return;
}
throw error;
}
}
// Path doesn't exist, create it
try {
await fs.mkdir(resolvedPath, { recursive: true });
} catch (error: any) {
// Handle race conditions and symlink issues
if (error.code === "EEXIST" || error.code === "ELOOP") {
return;
}
throw error;
}
}
/**
* Check if a path exists, handling symlinks safely.
* Returns true if the path exists as a file, directory, or symlink.
*/
export async function existsSafe(filePath: string): Promise<boolean> {
try {
await fs.lstat(filePath);
return true;
} catch (error: any) {
if (error.code === "ENOENT") {
return false;
}
// ELOOP or other errors - path exists but is problematic
if (error.code === "ELOOP") {
return true; // Symlink exists, even if looping
}
throw error;
}
}