mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-31 14:43:35 +00:00
feat: Add arbitrary directory project storage with registry system
This major update replaces the fixed `generations/` directory with support for storing projects in any directory on the filesystem. Projects are now tracked via a cross-platform registry system. ## New Features ### Project Registry (`registry.py`) - Cross-platform registry storing project name-to-path mappings - Platform-specific config locations: - Windows: %APPDATA%\autonomous-coder\projects.json - macOS: ~/Library/Application Support/autonomous-coder/projects.json - Linux: ~/.config/autonomous-coder/projects.json - POSIX path format for cross-platform compatibility - File locking for concurrent access safety (fcntl/msvcrt) - Atomic writes via temp file + rename to prevent corruption - Fixed Windows file locking issue with tempfile.mkstemp() ### Filesystem Browser API (`server/routers/filesystem.py`) - REST endpoints for browsing directories server-side - Cross-platform support with blocked system paths: - Windows: C:\Windows, Program Files, ProgramData, etc. - macOS: /System, /Library, /private, etc. - Linux: /etc, /var, /usr, /bin, etc. - Universal blocked paths: .ssh, .aws, .gnupg, .docker, etc. - Hidden file detection (Unix dot-prefix + Windows attributes) - UNC path blocking for security - Windows drive enumeration via ctypes - Directory creation with validation - Added `has_children` field to DirectoryEntry schema ### UI Folder Browser (`ui/src/components/FolderBrowser.tsx`) - React component for selecting project directories - Breadcrumb navigation with clickable segments - Windows drive selector - New folder creation inline - Fixed text visibility with explicit color values ## Updated Components ### Server Routers - `projects.py`: Uses registry instead of fixed generations/ directory - `agent.py`: Uses registry for project path lookups - `features.py`: Uses registry for database path resolution - `spec_creation.py`: Uses registry for WebSocket project resolution ### Process Manager (`server/services/process_manager.py`) - Fixed sandbox issue: subprocess now uses project_dir as cwd - This allows the Claude SDK sandbox to access external project directories ### Schemas (`server/schemas.py`) - Added `has_children` to DirectoryEntry - Added `in_progress` to ProjectStats - Added path field to ProjectSummary and ProjectDetail ### UI Components - `NewProjectModal.tsx`: Multi-step wizard with folder selection - Added clarifying text about subfolder creation - Fixed text color visibility issues ### API Client (`ui/src/lib/api.ts`) - Added filesystem API functions (listDirectory, createDirectory) - Fixed Windows path splitting for directory creation ### Documentation - Updated CLAUDE.md with registry system details - Updated command examples for absolute paths ## Security Improvements - Blocked `.` and `..` in directory names to prevent traversal - Added path blocking check in project creation - UNC path blocking throughout filesystem API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,8 @@ import type {
|
||||
AgentStatusResponse,
|
||||
AgentActionResponse,
|
||||
SetupStatus,
|
||||
DirectoryListResponse,
|
||||
PathValidationResponse,
|
||||
} from './types'
|
||||
|
||||
const API_BASE = '/api'
|
||||
@@ -41,10 +43,14 @@ export async function listProjects(): Promise<ProjectSummary[]> {
|
||||
return fetchJSON('/projects')
|
||||
}
|
||||
|
||||
export async function createProject(name: string, specMethod: 'claude' | 'manual' = 'manual'): Promise<ProjectSummary> {
|
||||
export async function createProject(
|
||||
name: string,
|
||||
path: string,
|
||||
specMethod: 'claude' | 'manual' = 'manual'
|
||||
): Promise<ProjectSummary> {
|
||||
return fetchJSON('/projects', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, spec_method: specMethod }),
|
||||
body: JSON.stringify({ name, path, spec_method: specMethod }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -146,3 +152,59 @@ export async function getSetupStatus(): Promise<SetupStatus> {
|
||||
export async function healthCheck(): Promise<{ status: string }> {
|
||||
return fetchJSON('/health')
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Filesystem API
|
||||
// ============================================================================
|
||||
|
||||
export async function listDirectory(path?: string): Promise<DirectoryListResponse> {
|
||||
const params = path ? `?path=${encodeURIComponent(path)}` : ''
|
||||
return fetchJSON(`/filesystem/list${params}`)
|
||||
}
|
||||
|
||||
export async function createDirectory(fullPath: string): Promise<{ success: boolean; path: string }> {
|
||||
// Backend expects { parent_path, name }, not { path }
|
||||
// Split the full path into parent directory and folder name
|
||||
|
||||
// Remove trailing slash if present
|
||||
const normalizedPath = fullPath.endsWith('/') ? fullPath.slice(0, -1) : fullPath
|
||||
|
||||
// Find the last path separator
|
||||
const lastSlash = normalizedPath.lastIndexOf('/')
|
||||
|
||||
let parentPath: string
|
||||
let name: string
|
||||
|
||||
// Handle Windows drive root (e.g., "C:/newfolder")
|
||||
if (lastSlash === 2 && /^[A-Za-z]:/.test(normalizedPath)) {
|
||||
// Path like "C:/newfolder" - parent is "C:/"
|
||||
parentPath = normalizedPath.substring(0, 3) // "C:/"
|
||||
name = normalizedPath.substring(3)
|
||||
} else if (lastSlash > 0) {
|
||||
parentPath = normalizedPath.substring(0, lastSlash)
|
||||
name = normalizedPath.substring(lastSlash + 1)
|
||||
} else if (lastSlash === 0) {
|
||||
// Unix root path like "/newfolder"
|
||||
parentPath = '/'
|
||||
name = normalizedPath.substring(1)
|
||||
} else {
|
||||
// No slash - invalid path
|
||||
throw new Error('Invalid path: must be an absolute path')
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
throw new Error('Invalid path: directory name is empty')
|
||||
}
|
||||
|
||||
return fetchJSON('/filesystem/create-directory', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ parent_path: parentPath, name }),
|
||||
})
|
||||
}
|
||||
|
||||
export async function validatePath(path: string): Promise<PathValidationResponse> {
|
||||
return fetchJSON('/filesystem/validate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ path }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface ProjectStats {
|
||||
|
||||
export interface ProjectSummary {
|
||||
name: string
|
||||
path: string
|
||||
has_spec: boolean
|
||||
stats: ProjectStats
|
||||
}
|
||||
@@ -20,6 +21,35 @@ export interface ProjectDetail extends ProjectSummary {
|
||||
prompts_dir: string
|
||||
}
|
||||
|
||||
// Filesystem types
|
||||
export interface DriveInfo {
|
||||
letter: string
|
||||
label: string
|
||||
available?: boolean
|
||||
}
|
||||
|
||||
export interface DirectoryEntry {
|
||||
name: string
|
||||
path: string
|
||||
is_directory: boolean
|
||||
has_children: boolean
|
||||
}
|
||||
|
||||
export interface DirectoryListResponse {
|
||||
current_path: string
|
||||
parent_path: string | null
|
||||
entries: DirectoryEntry[]
|
||||
drives: DriveInfo[] | null
|
||||
}
|
||||
|
||||
export interface PathValidationResponse {
|
||||
valid: boolean
|
||||
exists: boolean
|
||||
is_directory: boolean
|
||||
can_write: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ProjectPrompts {
|
||||
app_spec: string
|
||||
initializer_prompt: string
|
||||
|
||||
Reference in New Issue
Block a user