Files
automaker/apps/server/src/routes/worktree/common.ts
WebDevCody 98381441b9 feat: add GitHub issue fix command and release command
- Introduced a new command for fetching and validating GitHub issues, allowing users to address issues directly from the command line.
- Added a release command to bump the version of the application and build the Electron app, ensuring version consistency across UI and server packages.
- Updated package.json files for both UI and server to version 0.7.1, reflecting the latest changes.
- Implemented version utility in the server to read the version from package.json, enhancing version management across the application.
2025-12-31 23:24:01 -05:00

187 lines
5.9 KiB
TypeScript

/**
* Common utilities for worktree routes
*/
import { createLogger } from '@automaker/utils';
import { exec } from 'child_process';
import { promisify } from 'util';
import path from 'path';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
import { FeatureLoader } from '../../services/feature-loader.js';
const logger = createLogger('Worktree');
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
);
}
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';
/**
* Normalize path separators to forward slashes for cross-platform consistency.
* This ensures paths from `path.join()` (backslashes on Windows) match paths
* from git commands (which may use forward slashes).
*/
export function normalizePath(p: string): string {
return p.replace(/\\/g, '/');
}
/**
* Check if a path is a git repo
*/
export async function isGitRepo(repoPath: string): Promise<boolean> {
try {
await execAsync('git rev-parse --is-inside-work-tree', { cwd: repoPath });
return true;
} catch {
return false;
}
}
/**
* Check if a git repository has at least one commit (i.e., HEAD exists)
* Returns false for freshly initialized repos with no commits
*/
export async function hasCommits(repoPath: string): Promise<boolean> {
try {
await execAsync('git rev-parse --verify HEAD', { cwd: repoPath });
return true;
} catch {
return false;
}
}
/**
* Check if an error is ENOENT (file/path not found or spawn failed)
* These are expected in test environments with mock paths
*/
export function isENOENT(error: unknown): boolean {
return error !== null && typeof error === 'object' && 'code' in error && error.code === 'ENOENT';
}
/**
* Check if a path is a mock/test path that doesn't exist
*/
export function isMockPath(worktreePath: string): boolean {
return worktreePath.startsWith('/mock/') || worktreePath.includes('/mock/');
}
/**
* Conditionally log worktree errors - suppress ENOENT for mock paths
* to reduce noise in test output
*/
export function logWorktreeError(error: unknown, message: string, worktreePath?: string): void {
// Don't log ENOENT errors for mock paths (expected in tests)
if (isENOENT(error) && worktreePath && isMockPath(worktreePath)) {
return;
}
logError(error, message);
}
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
export const logError = createLogError(logger);
/**
* Ensure the repository has at least one commit so git commands that rely on HEAD work.
* Returns true if an empty commit was created, false if the repo already had commits.
* @param repoPath - Path to the git repository
* @param env - Optional environment variables to pass to git (e.g., GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL)
*/
export async function ensureInitialCommit(
repoPath: string,
env?: Record<string, string>
): Promise<boolean> {
try {
await execAsync('git rev-parse --verify HEAD', { cwd: repoPath });
return false;
} catch {
try {
await execAsync(`git commit --allow-empty -m "${AUTOMAKER_INITIAL_COMMIT_MESSAGE}"`, {
cwd: repoPath,
env: { ...process.env, ...env },
});
logger.info(`[Worktree] Created initial empty commit to enable worktrees in ${repoPath}`);
return true;
} catch (error) {
const reason = getErrorMessageShared(error);
throw new Error(
`Failed to create initial git commit. Please commit manually and retry. ${reason}`
);
}
}
}