feat: Fix new branch issues and address code review comments

This commit is contained in:
gsxdsm
2026-02-18 21:36:00 -08:00
parent 2d907938cc
commit 53d07fefb8
30 changed files with 1604 additions and 367 deletions

View File

@@ -22,6 +22,7 @@
"node": ">=22.0.0 <23.0.0"
},
"dependencies": {
"@automaker/platform": "1.0.0",
"@automaker/types": "1.0.0",
"@automaker/utils": "1.0.0"
},

View File

@@ -0,0 +1,70 @@
/**
* Git command execution utilities
*/
import { spawnProcess } from '@automaker/platform';
/**
* Execute git command with array arguments to prevent command injection.
* Uses spawnProcess from @automaker/platform for secure, cross-platform execution.
*
* @param args - Array of git command arguments (e.g., ['worktree', 'add', path])
* @param cwd - Working directory to execute the command in
* @param env - Optional additional environment variables to pass to the git process.
* These are merged on top of the current process environment. Pass
* `{ LC_ALL: 'C' }` to force git to emit English output regardless of the
* system locale so that text-based output parsing remains reliable.
* @param abortController - Optional AbortController to cancel the git process.
* When the controller is aborted the underlying process is sent SIGTERM and
* the returned promise rejects with an Error whose message is 'Process aborted'.
* @returns Promise resolving to stdout output
* @throws Error with stderr/stdout message if command fails. The thrown error
* also has `stdout` and `stderr` string properties for structured access.
*
* @example
* ```typescript
* // Safe: no injection possible
* await execGitCommand(['branch', '-D', branchName], projectPath);
*
* // Force English output for reliable text parsing:
* await execGitCommand(['rebase', '--', 'main'], worktreePath, { LC_ALL: 'C' });
*
* // With a process-level timeout:
* const controller = new AbortController();
* const timerId = setTimeout(() => controller.abort(), 30_000);
* try {
* await execGitCommand(['fetch', '--all', '--quiet'], cwd, undefined, controller);
* } finally {
* clearTimeout(timerId);
* }
*
* // Instead of unsafe:
* // await execAsync(`git branch -D ${branchName}`, { cwd });
* ```
*/
export async function execGitCommand(
args: string[],
cwd: string,
env?: Record<string, string>,
abortController?: AbortController
): Promise<string> {
const result = await spawnProcess({
command: 'git',
args,
cwd,
...(env !== undefined ? { env } : {}),
...(abortController !== undefined ? { abortController } : {}),
});
// spawnProcess returns { stdout, stderr, exitCode }
if (result.exitCode === 0) {
return result.stdout;
} else {
const errorMessage =
result.stderr || result.stdout || `Git command failed with code ${result.exitCode}`;
throw Object.assign(new Error(errorMessage), {
stdout: result.stdout,
stderr: result.stderr,
});
}
}

View File

@@ -3,6 +3,9 @@
* Git operations utilities for AutoMaker
*/
// Export command execution utilities
export { execGitCommand } from './exec.js';
// Export types and constants
export { BINARY_EXTENSIONS, GIT_STATUS_MAP, type FileStatus } from './types.js';

View File

@@ -0,0 +1,43 @@
/**
* Git validation utilities
*
* Canonical validators for git-related inputs (branch names, etc.)
* used across the server codebase.
*/
/** Maximum allowed length for git branch names */
export const MAX_BRANCH_NAME_LENGTH = 250;
/**
* Validate a git branch name to prevent command injection and ensure
* it conforms to safe git ref naming rules.
*
* Enforces:
* - Allowed characters: alphanumeric, dot (.), underscore (_), slash (/), dash (-)
* - First character must NOT be a dash (prevents git argument injection via
* names like "-flag" or "--option")
* - Rejects path-traversal sequences (..)
* - Rejects NUL bytes (\0)
* - Enforces a maximum length of {@link MAX_BRANCH_NAME_LENGTH} characters
*
* @param name - The branch name to validate
* @returns `true` when the name is safe to pass to git commands
*
* @example
* ```typescript
* isValidBranchName('feature/my-branch'); // true
* isValidBranchName('-flag'); // false (starts with dash)
* isValidBranchName('a..b'); // false (contains ..)
* isValidBranchName('a\0b'); // false (contains NUL)
* ```
*/
export function isValidBranchName(name: string): boolean {
// Must not contain NUL bytes
if (name.includes('\0')) return false;
// Must not contain path-traversal sequences
if (name.includes('..')) return false;
// First char must be alphanumeric, dot, underscore, or slash (not dash).
// Remaining chars may also include dash.
// Must be within the length limit.
return /^[a-zA-Z0-9._/][a-zA-Z0-9._\-/]*$/.test(name) && name.length < MAX_BRANCH_NAME_LENGTH;
}

View File

@@ -117,3 +117,6 @@ export {
type ThrottleOptions,
type DebouncedFunction,
} from './debounce.js';
// Git validation utilities
export { isValidBranchName, MAX_BRANCH_NAME_LENGTH } from './git-validation.js';