Compare commits

...

1 Commits

Author SHA1 Message Date
coderabbitai[bot] a0f538624f 📝 Add docstrings to feature/worktree-ui-config
Docstrings generation was requested by @sbeardsley.

* https://github.com/AndyMik90/Auto-Claude/pull/456#issuecomment-3703000464

The following files were modified:

* `apps/backend/cli/batch_commands.py`
* `apps/backend/cli/main.py`
* `apps/backend/core/config.py`
* `apps/backend/core/workspace/models.py`
* `apps/backend/core/worktree.py`
* `apps/frontend/src/renderer/components/project-settings/GeneralSettings.tsx`
* `apps/frontend/src/renderer/components/project-settings/WorktreeSettings.tsx`
* `apps/frontend/src/renderer/components/settings/sections/SectionRouter.tsx`
2025-12-31 23:00:13 +00:00
8 changed files with 355 additions and 27 deletions
+17 -10
View File
@@ -6,6 +6,7 @@ Commands for creating and managing multiple tasks from batch files.
"""
import json
import os
from pathlib import Path
from ui import highlight, print_status
@@ -174,17 +175,23 @@ def handle_batch_status_command(project_dir: str) -> bool:
def handle_batch_cleanup_command(project_dir: str, dry_run: bool = True) -> bool:
"""
Clean up completed specs and worktrees.
Args:
project_dir: Project directory
dry_run: If True, show what would be deleted
Clean up completed spec directories and their associated worktree paths.
Finds spec directories under <project_dir>/.auto-claude/specs that contain a `qa_report.md` (treated as completed),
and, when run in dry-run mode, prints the specs and corresponding worktree paths that would be removed.
Parameters:
project_dir (str): Path to the project root.
dry_run (bool): If True, print what would be removed instead of performing deletions.
Returns:
True if successful
True if the command completed.
"""
from core.config import get_worktree_base_path
specs_dir = Path(project_dir) / ".auto-claude" / "specs"
worktrees_dir = Path(project_dir) / ".worktrees"
worktree_base_path = get_worktree_base_path(Path(project_dir))
worktrees_dir = Path(project_dir) / worktree_base_path
if not specs_dir.exists():
print_status("No specs directory found", "info")
@@ -209,8 +216,8 @@ def handle_batch_cleanup_command(project_dir: str, dry_run: bool = True) -> bool
print(f" - {spec_name}")
wt_path = worktrees_dir / spec_name
if wt_path.exists():
print(f" └─ .worktrees/{spec_name}/")
print(f" └─ {worktree_base_path}/{spec_name}/")
print()
print("Run with --no-dry-run to actually delete")
return True
return True
+14 -2
View File
@@ -15,6 +15,8 @@ _PARENT_DIR = Path(__file__).parent.parent
if str(_PARENT_DIR) not in sys.path:
sys.path.insert(0, str(_PARENT_DIR))
from dotenv import load_dotenv
from .batch_commands import (
handle_batch_cleanup_command,
@@ -259,7 +261,11 @@ Environment Variables:
def main() -> None:
"""Main CLI entry point."""
"""
Entry point for the CLI: sets up the environment, parses arguments, and dispatches the requested command.
This function initializes runtime environment and debugging, resolves the project directory (and loads a project-specific .auto-claude/.env file if present), determines the model choice from the CLI or the AUTO_BUILD_MODEL environment variable, and routes control to the appropriate handler based on parsed CLI flags (examples include listing specs, worktree management, batch operations, merge/preview/review/discard flows, QA and follow-up commands, or the normal build flow). Exits the process with a non-zero status when required by invalid input or failing command outcomes.
"""
# Set up environment first
setup_environment()
@@ -276,6 +282,12 @@ def main() -> None:
project_dir = get_project_dir(args.project_dir)
debug("run.py", f"Using project directory: {project_dir}")
# Load project-specific .env file (overrides backend .env)
project_env = project_dir / ".auto-claude" / ".env"
if project_env.exists():
load_dotenv(project_env, override=True)
debug("run.py", f"Loaded project .env from: {project_env}")
# Get model from CLI arg or env var (None if not explicitly set)
# This allows get_phase_model() to fall back to task_metadata.json
model = args.model or os.environ.get("AUTO_BUILD_MODEL")
@@ -410,4 +422,4 @@ def main() -> None:
if __name__ == "__main__":
main()
main()
+78
View File
@@ -0,0 +1,78 @@
"""
Core configuration for Auto Claude.
This module provides centralized configuration management for Auto Claude,
including worktree path resolution and validation. It ensures consistent
configuration access across the entire backend codebase.
Constants:
WORKTREE_BASE_PATH_VAR (str): Environment variable name for custom worktree base path.
DEFAULT_WORKTREE_PATH (str): Default worktree directory name relative to project root.
Example:
>>> from core.config import get_worktree_base_path
>>> from pathlib import Path
>>>
>>> # Get worktree path with validation
>>> project_dir = Path("/path/to/project")
>>> worktree_path = get_worktree_base_path(project_dir)
>>> full_path = project_dir / worktree_path
"""
import os
from pathlib import Path
# Environment variable names
WORKTREE_BASE_PATH_VAR = "WORKTREE_BASE_PATH"
"""str: Environment variable name for configuring custom worktree base path.
Users can set this environment variable in their project's .env file to specify
a custom location for worktree directories, supporting both relative and absolute paths.
"""
# Default values
DEFAULT_WORKTREE_PATH = ".worktrees"
"""str: Default worktree directory name.
This is the fallback value used when WORKTREE_BASE_PATH is not set or when
validation fails (e.g., path points to .auto-claude/ or .git/ directories).
"""
def get_worktree_base_path(project_dir: Path | None = None) -> str:
"""
Determine the validated worktree base path from the WORKTREE_BASE_PATH environment variable or the default.
Parameters:
project_dir (Path | None): Optional project root used to resolve relative paths and perform stricter validation. If omitted, only basic pattern checks are applied.
Returns:
str: The configured worktree base path string, or DEFAULT_WORKTREE_PATH ('.worktrees') if the configured value is invalid or points inside the project's `.auto-claude` or `.git` directories.
"""
worktree_base_path = os.getenv(WORKTREE_BASE_PATH_VAR, DEFAULT_WORKTREE_PATH)
# If no project_dir provided, return as-is (basic validation only)
if not project_dir:
# Check for obviously dangerous patterns
normalized = Path(worktree_base_path).as_posix()
if ".auto-claude" in normalized or ".git" in normalized:
return DEFAULT_WORKTREE_PATH
return worktree_base_path
# Resolve the absolute path
if Path(worktree_base_path).is_absolute():
resolved = Path(worktree_base_path).resolve()
else:
resolved = (project_dir / worktree_base_path).resolve()
# Prevent paths inside .auto-claude/ or .git/
auto_claude_dir = (project_dir / ".auto-claude").resolve()
git_dir = (project_dir / ".git").resolve()
resolved_str = str(resolved)
if resolved_str.startswith(str(auto_claude_dir)) or resolved_str.startswith(
str(git_dir)
):
return DEFAULT_WORKTREE_PATH
return worktree_base_path
+14 -7
View File
@@ -6,6 +6,7 @@ Workspace Models
Data classes and enums for workspace management.
"""
import os
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
@@ -227,12 +228,15 @@ class SpecNumberLock:
def get_next_spec_number(self) -> int:
"""
Scan all spec locations and return the next available spec number.
Must be called while lock is held.
Compute the next global spec number by scanning the project's specs and all worktree specs.
Requires the spec-numbering lock to be held; caches the computed maximum for subsequent calls.
Returns:
Next available spec number (global max + 1)
int: The next available spec number (highest existing spec number + 1).
Raises:
SpecNumberLockError: If the lock has not been acquired when called.
"""
if not self.acquired:
raise SpecNumberLockError(
@@ -249,7 +253,10 @@ class SpecNumberLock:
max_number = max(max_number, self._scan_specs_dir(main_specs_dir))
# 2. Scan all worktree specs
worktrees_dir = self.project_dir / ".worktrees"
from core.config import get_worktree_base_path
worktree_base_path = get_worktree_base_path(self.project_dir)
worktrees_dir = self.project_dir / worktree_base_path
if worktrees_dir.exists():
for worktree in worktrees_dir.iterdir():
if worktree.is_dir():
@@ -272,4 +279,4 @@ class SpecNumberLock:
except ValueError:
pass
return max_num
return max_num
+15 -2
View File
@@ -53,9 +53,22 @@ class WorktreeManager:
"""
def __init__(self, project_dir: Path, base_branch: str | None = None):
"""
Initialize the WorktreeManager for a repository, determining base branch and worktrees directory.
Parameters:
project_dir (Path): Root path of the repository managed by this instance.
base_branch (str | None): Optional explicit base branch to use; if omitted, the base branch is auto-detected.
"""
from core.config import get_worktree_base_path
self.project_dir = project_dir
self.base_branch = base_branch or self._detect_base_branch()
self.worktrees_dir = project_dir / ".worktrees"
# Use custom worktree path from environment variable with validation
worktree_base_path = get_worktree_base_path(project_dir)
self.worktrees_dir = project_dir / worktree_base_path
self._merge_lock = asyncio.Lock()
def _detect_base_branch(self) -> str:
@@ -664,4 +677,4 @@ class WorktreeManager:
# Keep STAGING_WORKTREE_NAME for backward compatibility in imports
STAGING_WORKTREE_NAME = "auto-claude"
STAGING_WORKTREE_NAME = "auto-claude"
@@ -21,8 +21,10 @@ import { AVAILABLE_MODELS } from '../../../shared/constants';
import type {
Project,
ProjectSettings as ProjectSettingsType,
AutoBuildVersionInfo
AutoBuildVersionInfo,
ProjectEnvConfig
} from '../../../shared/types';
import { WorktreeSettings } from './WorktreeSettings';
interface GeneralSettingsProps {
project: Project;
@@ -32,8 +34,24 @@ interface GeneralSettingsProps {
isCheckingVersion: boolean;
isUpdating: boolean;
handleInitialize: () => Promise<void>;
envConfig: ProjectEnvConfig | null;
updateEnvConfig: (updates: Partial<ProjectEnvConfig>) => void;
}
/**
* Render general project settings, including auto-build integration, agent configuration, worktree location, and notification toggles.
*
* @param project - Project data used to determine auto-build state and provide paths.
* @param settings - Current project settings that drive form controls and toggles.
* @param setSettings - State updater used to replace the project settings object.
* @param versionInfo - Optional auto-build version and initialization status displayed when available.
* @param isCheckingVersion - When true, displays a loading indicator while checking auto-build status.
* @param isUpdating - When true, disables initialization controls and shows an initializing state.
* @param handleInitialize - Called to initialize the auto-build integration.
* @param envConfig - Optional environment configuration passed into worktree settings.
* @param updateEnvConfig - Function to apply partial updates to the environment configuration.
* @returns The rendered settings UI as JSX.
*/
export function GeneralSettings({
project,
settings,
@@ -41,7 +59,9 @@ export function GeneralSettings({
versionInfo,
isCheckingVersion,
isUpdating,
handleInitialize
handleInitialize,
envConfig,
updateEnvConfig
}: GeneralSettingsProps) {
const { t } = useTranslation(['settings']);
@@ -150,6 +170,18 @@ export function GeneralSettings({
<Separator />
{/* Worktree Location */}
<section className="space-y-4">
<h3 className="text-sm font-semibold text-foreground">{t('projectSections.worktree.title')}</h3>
<WorktreeSettings
envConfig={envConfig}
updateEnvConfig={updateEnvConfig}
projectPath={project.path}
/>
</section>
<Separator />
{/* Notifications */}
<section className="space-y-4">
<h3 className="text-sm font-semibold text-foreground">Notifications</h3>
@@ -220,4 +252,4 @@ export function GeneralSettings({
)}
</>
);
}
}
@@ -0,0 +1,176 @@
import { FolderOpen, Info } from 'lucide-react';
import { Trans, useTranslation } from 'react-i18next';
import { Label } from '../ui/label';
import { Input } from '../ui/input';
import { Button } from '../ui/button';
import type { ProjectEnvConfig } from '../../../shared/types';
import { DEFAULT_WORKTREE_PATH } from '../../../shared/constants';
// Browser-compatible path utilities
const isAbsolutePath = (p: string): boolean => {
// Unix absolute path starts with /
// Windows absolute path starts with drive letter (C:\) or UNC path (\\)
return p.startsWith('/') || /^[a-zA-Z]:[/\\]/.test(p) || p.startsWith('\\\\');
};
const joinPath = (...parts: string[]): string => {
// Simple path join that works in browser
return parts.join('/').replace(/\/+/g, '/');
};
const getRelativePath = (from: string, to: string): string => {
// Normalize paths
const fromParts = from.split(/[/\\]/).filter(Boolean);
const toParts = to.split(/[/\\]/).filter(Boolean);
// Find common base
let commonLength = 0;
const minLength = Math.min(fromParts.length, toParts.length);
for (let i = 0; i < minLength; i++) {
if (fromParts[i] === toParts[i]) {
commonLength = i + 1;
} else {
break;
}
}
// If no common base, paths are on different roots
if (commonLength === 0) {
return to; // Return absolute path
}
// Build relative path
const upCount = fromParts.length - commonLength;
const downParts = toParts.slice(commonLength);
if (upCount === 0 && downParts.length === 0) {
return '.';
}
const ups = Array(upCount).fill('..');
return [...ups, ...downParts].join('/');
};
interface WorktreeSettingsProps {
envConfig: ProjectEnvConfig | null;
updateEnvConfig: (updates: Partial<ProjectEnvConfig>) => void;
projectPath: string;
}
/**
* Render the worktree settings UI that lets the user view and edit the project's worktree base path.
*
* The component displays an input for the worktree base path, a browse button that opens a directory picker,
* and a resolved-path readout. When a directory is chosen via browse, it updates the env config with a path
* relative to the project root if the selection is inside the project, otherwise with the absolute path.
* The resolved path is computed as:
* - the absolute `worktreePath` when it is an absolute path;
* - `joinPath(projectPath, worktreePath)` when `worktreePath` is relative;
* - `joinPath(projectPath, DEFAULT_WORKTREE_PATH)` when `worktreePath` is empty.
*
* @param envConfig - The current environment configuration for the project, or `null` when unset
* @param updateEnvConfig - Callback to apply a partial update to the project's environment configuration
* @param projectPath - Absolute filesystem path to the project root
* @returns A React element rendering controls and information for configuring the project's worktree path
*/
export function WorktreeSettings({
envConfig,
updateEnvConfig,
projectPath
}: WorktreeSettingsProps) {
const { t } = useTranslation(['settings']);
const worktreePath = envConfig?.worktreePath || '';
// Resolve the actual path that will be used
const resolvedPath = worktreePath
? (isAbsolutePath(worktreePath)
? worktreePath
: joinPath(projectPath, worktreePath))
: joinPath(projectPath, DEFAULT_WORKTREE_PATH);
const handleBrowse = async () => {
const result = await window.electronAPI.dialog.showOpenDialog({
properties: ['openDirectory', 'createDirectory'],
defaultPath: projectPath,
title: t('settings:worktree.selectLocation')
});
if (!result.canceled && result.filePaths.length > 0) {
const selectedPath = result.filePaths[0];
// Convert to relative path if inside project
const relativePath = getRelativePath(projectPath, selectedPath);
const finalPath = relativePath.startsWith('..')
? selectedPath // Absolute if outside project
: relativePath; // Relative if inside project
updateEnvConfig({ worktreePath: finalPath });
}
};
return (
<section className="space-y-4">
<div className="flex items-start gap-2 rounded-lg border border-border bg-muted/50 p-3">
<Info className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
<div className="text-xs text-muted-foreground space-y-1">
<p>{t('settings:worktree.description')}</p>
<p className="font-medium">
<Trans i18nKey="settings:worktree.defaultInfo">
Default: <code>.worktrees/</code> in your project root
</Trans>
</p>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="worktreePath">{t('settings:worktree.basePathLabel')}</Label>
<div className="flex gap-2">
<Input
id="worktreePath"
placeholder={t('settings:worktree.basePathPlaceholder')}
value={worktreePath}
onChange={(e) => updateEnvConfig({ worktreePath: e.target.value })}
/>
<Button
type="button"
variant="outline"
size="icon"
onClick={handleBrowse}
>
<FolderOpen className="h-4 w-4" />
</Button>
</div>
<p className="text-xs text-muted-foreground">
<Trans i18nKey="settings:worktree.pathTypeDescription">
Supports relative paths (e.g., <code>worktrees</code>) or absolute paths (e.g., <code>/tmp/worktrees</code>)
</Trans>
</p>
</div>
<div className="rounded-lg border border-border bg-muted/50 p-3">
<p className="text-xs font-medium text-foreground">{t('settings:worktree.resolvedPath')}</p>
<code className="text-xs text-muted-foreground break-all">{resolvedPath}</code>
</div>
<div className="text-xs text-muted-foreground space-y-1">
<p className="font-medium">{t('settings:worktree.commonUseCases')}</p>
<ul className="list-disc list-inside space-y-0.5 ml-2">
<li>
<Trans i18nKey="settings:worktree.externalDrive">
External drive: <code>/Volumes/FastSSD/worktrees</code>
</Trans>
</li>
<li>
<Trans i18nKey="settings:worktree.tempDirectory">
Temp directory: <code>/tmp/my-project-worktrees</code>
</Trans>
</li>
<li>
<Trans i18nKey="settings:worktree.sharedBuilds">
Shared builds: <code>../shared-worktrees</code>
</Trans>
</li>
</ul>
</div>
</section>
);
}
@@ -46,8 +46,9 @@ interface SectionRouterProps {
}
/**
* Routes to the appropriate settings section based on activeSection.
* Handles initialization guards and section-specific configurations.
* Selects and renders the settings subsection corresponding to `activeSection`.
*
* @returns The React element for the selected settings subsection, or `null` if `activeSection` does not match a known section.
*/
export function SectionRouter({
activeSection,
@@ -100,6 +101,8 @@ export function SectionRouter({
isCheckingVersion={isCheckingVersion}
isUpdating={isUpdating}
handleInitialize={handleInitialize}
envConfig={envConfig}
updateEnvConfig={updateEnvConfig}
/>
</SettingsSection>
);
@@ -232,4 +235,4 @@ export function SectionRouter({
default:
return null;
}
}
}