diff --git a/apps/backend/cli/batch_commands.py b/apps/backend/cli/batch_commands.py index 28a82ea90a..d1e68c42dc 100644 --- a/apps/backend/cli/batch_commands.py +++ b/apps/backend/cli/batch_commands.py @@ -6,8 +6,10 @@ """ import json +import os from pathlib import Path +from core.config import get_worktree_base_path from ui import highlight, print_status @@ -184,7 +186,8 @@ def handle_batch_cleanup_command(project_dir: str, dry_run: bool = True) -> bool True if successful """ 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,7 +212,7 @@ 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") diff --git a/apps/backend/cli/main.py b/apps/backend/cli/main.py index 9b910b5311..fc29001456 100644 --- a/apps/backend/cli/main.py +++ b/apps/backend/cli/main.py @@ -15,6 +15,8 @@ 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, @@ -276,6 +278,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") diff --git a/apps/backend/cli/utils.py b/apps/backend/cli/utils.py index f18954654a..820d3178a4 100644 --- a/apps/backend/cli/utils.py +++ b/apps/backend/cli/utils.py @@ -15,6 +15,7 @@ sys.path.insert(0, str(_PARENT_DIR)) from core.auth import get_auth_token, get_auth_token_source +from core.config import get_worktree_base_path from dotenv import load_dotenv from graphiti_config import get_graphiti_status from linear_integration import LinearManager @@ -82,7 +83,8 @@ def find_spec(project_dir: Path, spec_identifier: str) -> Path | None: return spec_folder # Check worktree specs (for merge-preview, merge, review, discard operations) - worktree_base = project_dir / ".worktrees" + worktree_base_path = get_worktree_base_path(project_dir) + worktree_base = project_dir / worktree_base_path if worktree_base.exists(): # Try exact match in worktree worktree_spec = ( diff --git a/apps/backend/core/config.py b/apps/backend/core/config.py new file mode 100644 index 0000000000..fb24c832d8 --- /dev/null +++ b/apps/backend/core/config.py @@ -0,0 +1,118 @@ +""" +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: + """ + Get the worktree base path from environment variables with security validation. + + This function reads the WORKTREE_BASE_PATH environment variable and validates + it to prevent security issues. It supports both relative and absolute paths, + enabling users to place worktrees on external drives or temporary directories. + + Supported path types: + - Relative paths: 'worktrees', '.cache/worktrees', '../shared-worktrees' + - Absolute paths: '/tmp/worktrees', '/Volumes/FastSSD/worktrees', 'C:\\worktrees' + + Security validations: + - Prevents paths inside .auto-claude/ directory (avoids data loss during cleanup) + - Prevents paths inside .git/ directory (avoids repository corruption) + - Falls back to DEFAULT_WORKTREE_PATH if validation fails + + Args: + project_dir (Path | None): Project root directory for full validation. + If None, only basic pattern validation is performed (checks for + .auto-claude and .git in path string). If provided, performs + full path resolution and validation. + + Returns: + str: The validated worktree base path string. Returns DEFAULT_WORKTREE_PATH + ('.worktrees') if the configured path is invalid or dangerous. + + Examples: + >>> # Basic usage without validation + >>> path = get_worktree_base_path() + >>> print(path) + '.worktrees' + + >>> # Full validation with project directory + >>> from pathlib import Path + >>> project = Path("/home/user/my-project") + >>> path = get_worktree_base_path(project) + >>> full_path = project / path + + >>> # With WORKTREE_BASE_PATH="/tmp/worktrees" in environment + >>> os.environ['WORKTREE_BASE_PATH'] = '/tmp/worktrees' + >>> path = get_worktree_base_path(project) + >>> print(path) + '/tmp/worktrees' + + Note: + The function intentionally allows absolute paths outside the project + directory to support use cases like external drives or shared build + directories. This is a design decision, not a security flaw. + """ + 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 .auto-claude or .git as path components, not substrings + parts = set(Path(worktree_base_path).parts) + if ".auto-claude" in parts or ".git" in parts: + 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 diff --git a/apps/backend/core/workspace/git_utils.py b/apps/backend/core/workspace/git_utils.py index d460dc9728..3407d2e358 100644 --- a/apps/backend/core/workspace/git_utils.py +++ b/apps/backend/core/workspace/git_utils.py @@ -7,9 +7,12 @@ """ import json +import os import subprocess from pathlib import Path +from core.config import get_worktree_base_path + # Constants for merge limits MAX_FILE_LINES_FOR_AI = 5000 # Skip AI for files larger than this MAX_PARALLEL_AI_MERGES = 5 # Limit concurrent AI merge operations @@ -221,8 +224,9 @@ def get_existing_build_worktree(project_dir: Path, spec_name: str) -> Path | Non Returns: Path to the worktree if it exists for this spec, None otherwise """ - # Per-spec worktree path: .worktrees/{spec-name}/ - worktree_path = project_dir / ".worktrees" / spec_name + # Per-spec worktree path: .worktrees/{spec-name}/ (or custom WORKTREE_BASE_PATH) + worktree_base_path = get_worktree_base_path(project_dir) + worktree_path = project_dir / worktree_base_path / spec_name if worktree_path.exists(): return worktree_path return None diff --git a/apps/backend/core/workspace/models.py b/apps/backend/core/workspace/models.py index cc94413e54..f538bb2458 100644 --- a/apps/backend/core/workspace/models.py +++ b/apps/backend/core/workspace/models.py @@ -6,10 +6,13 @@ Data classes and enums for workspace management. """ +import os from dataclasses import dataclass from enum import Enum from pathlib import Path +from core.config import get_worktree_base_path + class WorkspaceMode(Enum): """How auto-claude should work.""" @@ -249,7 +252,8 @@ def get_next_spec_number(self) -> int: max_number = max(max_number, self._scan_specs_dir(main_specs_dir)) # 2. Scan all worktree specs - worktrees_dir = self.project_dir / ".worktrees" + 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(): diff --git a/apps/backend/core/worktree.py b/apps/backend/core/worktree.py index ab3b89e3b3..7b11b7b162 100644 --- a/apps/backend/core/worktree.py +++ b/apps/backend/core/worktree.py @@ -22,6 +22,8 @@ from dataclasses import dataclass from pathlib import Path +from core.config import get_worktree_base_path + class WorktreeError(Exception): """Error during worktree operations.""" @@ -55,7 +57,11 @@ class WorktreeManager: def __init__(self, project_dir: Path, base_branch: str | None = None): 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: diff --git a/apps/frontend/package-lock.json b/apps/frontend/package-lock.json index 6b22c98327..586608d869 100644 --- a/apps/frontend/package-lock.json +++ b/apps/frontend/package-lock.json @@ -1066,45 +1066,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@epic-web/invariant": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", @@ -6279,15 +6240,6 @@ "buffer": "^5.1.0" } }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/cross-env": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", @@ -12626,36 +12578,6 @@ "dev": true, "license": "MIT" }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/apps/frontend/src/main/ipc-handlers/env-handlers.ts b/apps/frontend/src/main/ipc-handlers/env-handlers.ts index 9574215b9e..744a898ef8 100644 --- a/apps/frontend/src/main/ipc-handlers/env-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/env-handlers.ts @@ -98,6 +98,9 @@ export function registerEnvHandlers( if (config.defaultBranch !== undefined) { existingVars['DEFAULT_BRANCH'] = config.defaultBranch; } + if (config.worktreePath !== undefined) { + existingVars['WORKTREE_BASE_PATH'] = config.worktreePath; + } if (config.graphitiEnabled !== undefined) { existingVars['GRAPHITI_ENABLED'] = config.graphitiEnabled ? 'true' : 'false'; } @@ -228,6 +231,11 @@ ${envLine(existingVars, GITLAB_ENV_KEYS.AUTO_SYNC, 'false')} # If not set, Auto Claude will auto-detect main/master, or fall back to current branch ${existingVars['DEFAULT_BRANCH'] ? `DEFAULT_BRANCH=${existingVars['DEFAULT_BRANCH']}` : '# DEFAULT_BRANCH=main'} +# Worktree base path (OPTIONAL) +# Supports relative paths (e.g., worktrees) or absolute paths (e.g., /tmp/worktrees) +# If not set, defaults to .worktrees in your project root +${existingVars['WORKTREE_BASE_PATH'] ? `WORKTREE_BASE_PATH=${existingVars['WORKTREE_BASE_PATH']}` : '# WORKTREE_BASE_PATH=.worktrees'} + # ============================================================================= # UI SETTINGS (OPTIONAL) # ============================================================================= @@ -411,6 +419,9 @@ ${existingVars['GRAPHITI_DB_PATH'] ? `GRAPHITI_DB_PATH=${existingVars['GRAPHITI_ if (vars['DEFAULT_BRANCH']) { config.defaultBranch = vars['DEFAULT_BRANCH']; } + if (vars['WORKTREE_BASE_PATH']) { + config.worktreePath = vars['WORKTREE_BASE_PATH']; + } if (vars['GRAPHITI_ENABLED']?.toLowerCase() === 'true') { config.graphitiEnabled = true; diff --git a/apps/frontend/src/main/project-store.ts b/apps/frontend/src/main/project-store.ts index be1bf529a5..8685bc59d8 100644 --- a/apps/frontend/src/main/project-store.ts +++ b/apps/frontend/src/main/project-store.ts @@ -264,7 +264,9 @@ export class ProjectStore { // NOTE FOR MAINTAINERS: Worktree tasks are only included if the spec also exists in main. // This prevents deleted tasks from "coming back" when the worktree isn't cleaned up. // Alternative behavior: include all worktree tasks (remove the mainSpecIds check below). - const worktreesDir = path.join(project.path, '.worktrees'); + import { WORKTREE_BASE_PATH_VAR, DEFAULT_WORKTREE_PATH } from '../shared/constants'; + const worktreeBasePath = process.env[WORKTREE_BASE_PATH_VAR] || DEFAULT_WORKTREE_PATH; + const worktreesDir = path.join(project.path, worktreeBasePath); if (existsSync(worktreesDir)) { try { const worktrees = readdirSync(worktreesDir, { withFileTypes: true }); diff --git a/apps/frontend/src/renderer/components/project-settings/GeneralSettings.tsx b/apps/frontend/src/renderer/components/project-settings/GeneralSettings.tsx index 7899f6dbb8..0e7f33330f 100644 --- a/apps/frontend/src/renderer/components/project-settings/GeneralSettings.tsx +++ b/apps/frontend/src/renderer/components/project-settings/GeneralSettings.tsx @@ -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,6 +34,8 @@ interface GeneralSettingsProps { isCheckingVersion: boolean; isUpdating: boolean; handleInitialize: () => Promise; + envConfig: ProjectEnvConfig | null; + updateEnvConfig: (updates: Partial) => void; } export function GeneralSettings({ @@ -41,7 +45,9 @@ export function GeneralSettings({ versionInfo, isCheckingVersion, isUpdating, - handleInitialize + handleInitialize, + envConfig, + updateEnvConfig }: GeneralSettingsProps) { const { t } = useTranslation(['settings']); @@ -150,6 +156,18 @@ export function GeneralSettings({ + {/* Worktree Location */} +
+

{t('projectSections.worktree.title')}

+ +
+ + + {/* Notifications */}

Notifications

diff --git a/apps/frontend/src/renderer/components/project-settings/WorktreeSettings.tsx b/apps/frontend/src/renderer/components/project-settings/WorktreeSettings.tsx new file mode 100644 index 0000000000..19ca3d476e --- /dev/null +++ b/apps/frontend/src/renderer/components/project-settings/WorktreeSettings.tsx @@ -0,0 +1,160 @@ +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) => void; + projectPath: string; +} + +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 ( +
+
+ +
+

{t('settings:worktree.description')}

+

+ + Default: .worktrees/ in your project root + +

+
+
+ +
+ +
+ updateEnvConfig({ worktreePath: e.target.value })} + /> + +
+

+ + Supports relative paths (e.g., worktrees) or absolute paths (e.g., /tmp/worktrees) + +

+
+ +
+

{t('settings:worktree.resolvedPath')}

+ {resolvedPath} +
+ +
+

{t('settings:worktree.commonUseCases')}

+
    +
  • + + External drive: /Volumes/FastSSD/worktrees + +
  • +
  • + + Temp directory: /tmp/my-project-worktrees + +
  • +
  • + + Shared builds: ../shared-worktrees + +
  • +
+
+
+ ); +} diff --git a/apps/frontend/src/renderer/components/settings/sections/SectionRouter.tsx b/apps/frontend/src/renderer/components/settings/sections/SectionRouter.tsx index 663f6d2700..7d1a969a89 100644 --- a/apps/frontend/src/renderer/components/settings/sections/SectionRouter.tsx +++ b/apps/frontend/src/renderer/components/settings/sections/SectionRouter.tsx @@ -100,6 +100,8 @@ export function SectionRouter({ isCheckingVersion={isCheckingVersion} isUpdating={isUpdating} handleInitialize={handleInitialize} + envConfig={envConfig} + updateEnvConfig={updateEnvConfig} /> ); diff --git a/apps/frontend/src/shared/constants/index.ts b/apps/frontend/src/shared/constants/index.ts index ea90dce632..89579d5179 100644 --- a/apps/frontend/src/shared/constants/index.ts +++ b/apps/frontend/src/shared/constants/index.ts @@ -32,3 +32,6 @@ export * from './github'; // Configuration and paths export * from './config'; + +// Worktree configuration constants +export * from './worktree'; diff --git a/apps/frontend/src/shared/constants/worktree.ts b/apps/frontend/src/shared/constants/worktree.ts new file mode 100644 index 0000000000..3841d6d438 --- /dev/null +++ b/apps/frontend/src/shared/constants/worktree.ts @@ -0,0 +1,12 @@ +/** + * Worktree configuration constants. + * + * Centralized constants for worktree path configuration to ensure consistency + * across frontend components and avoid duplication. + */ + +/** Environment variable name for custom worktree base path */ +export const WORKTREE_BASE_PATH_VAR = 'WORKTREE_BASE_PATH'; + +/** Default worktree base path (relative to project root) */ +export const DEFAULT_WORKTREE_PATH = '.worktrees'; diff --git a/apps/frontend/src/shared/i18n/locales/en/settings.json b/apps/frontend/src/shared/i18n/locales/en/settings.json index f9196432c5..0b86ffd266 100644 --- a/apps/frontend/src/shared/i18n/locales/en/settings.json +++ b/apps/frontend/src/shared/i18n/locales/en/settings.json @@ -214,6 +214,20 @@ "integrationTitle": "Memory", "integrationDescription": "Configure persistent cross-session memory for agents", "syncDescription": "Configure persistent memory" + }, + "worktree": { + "title": "Worktree Location", + "description": "Worktrees are isolated Git branches where Auto Claude builds features safely.", + "defaultInfo": "Default: <1>.worktrees/ in your project root", + "basePathLabel": "Worktree Base Path", + "basePathPlaceholder": ".worktrees (default)", + "selectLocation": "Select Worktree Location", + "pathTypeDescription": "Supports relative paths (e.g., <1>worktrees) or absolute paths (e.g., <2>/tmp/worktrees)", + "resolvedPath": "Resolved Path:", + "commonUseCases": "Common Use Cases:", + "externalDrive": "External drive: <1>/Volumes/FastSSD/worktrees", + "tempDirectory": "Temp directory: <1>/tmp/my-project-worktrees", + "sharedBuilds": "Shared builds: <1>../shared-worktrees" } }, "agentProfile": { diff --git a/apps/frontend/src/shared/i18n/locales/fr/settings.json b/apps/frontend/src/shared/i18n/locales/fr/settings.json index 5adab02d22..5875afb08d 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/settings.json +++ b/apps/frontend/src/shared/i18n/locales/fr/settings.json @@ -214,6 +214,20 @@ "integrationTitle": "Mémoire", "integrationDescription": "Configurer la mémoire persistante inter-sessions pour les agents", "syncDescription": "Configurer la mémoire persistante" + }, + "worktree": { + "title": "Emplacement des Worktrees", + "description": "Les worktrees sont des branches Git isolées où Auto Claude construit les fonctionnalités en toute sécurité.", + "defaultInfo": "Par défaut : <1>.worktrees/ à la racine de votre projet", + "basePathLabel": "Chemin de Base des Worktrees", + "basePathPlaceholder": ".worktrees (par défaut)", + "selectLocation": "Sélectionner l'Emplacement des Worktrees", + "pathTypeDescription": "Prend en charge les chemins relatifs (par exemple, <1>worktrees) ou absolus (par exemple, <2>/tmp/worktrees)", + "resolvedPath": "Chemin Résolu :", + "commonUseCases": "Cas d'Usage Courants :", + "externalDrive": "Disque externe : <1>/Volumes/FastSSD/worktrees", + "tempDirectory": "Répertoire temporaire : <1>/tmp/my-project-worktrees", + "sharedBuilds": "Builds partagés : <1>../shared-worktrees" } }, "agentProfile": { diff --git a/apps/frontend/src/shared/types/project.ts b/apps/frontend/src/shared/types/project.ts index 2e2e4b0c31..7bb2e114b1 100644 --- a/apps/frontend/src/shared/types/project.ts +++ b/apps/frontend/src/shared/types/project.ts @@ -304,6 +304,7 @@ export interface ProjectEnvConfig { // Git/Worktree Settings defaultBranch?: string; // Base branch for worktree creation (e.g., 'main', 'develop') + worktreePath?: string; // Custom worktree base path (relative or absolute) // Graphiti Memory Integration (V2 - Multi-provider support) // Uses LadybugDB embedded database (no Docker required, Python 3.12+)