"""Workspace directory management for project isolation.""" from dataclasses import dataclass from pathlib import Path from typing import Optional import shutil @dataclass class Workspace: """Represents a project workspace with defined directory structure. Each project has its own isolated workspace directory following the layout: ~/gsd-workspaces/{project}/ ├── .gsd/ │ ├── state.json # Session state │ ├── state.json.bak # State backup │ └── session-dump/ # Session dump ├── code/ # Project code (optional) └── io-logs/ └── {session-id}.log # I/O logs """ project_name: str root: Optional[Path] = None def __post_init__(self): """Compute root path if not provided.""" if self.root is None: self.root = Path.home() / "gsd-workspaces" / self.project_name elif isinstance(self.root, str): self.root = Path(self.root) @property def gsd_dir(self) -> Path: """Path to .gsd directory containing state files.""" return self.root / ".gsd" @property def state_path(self) -> Path: """Path to state.json file.""" return self.gsd_dir / "state.json" @property def state_backup_path(self) -> Path: """Path to state.json.bak file.""" return self.gsd_dir / "state.json.bak" @property def session_dump_dir(self) -> Path: """Path to session-dump directory.""" return self.gsd_dir / "session-dump" @property def code_dir(self) -> Path: """Path to code directory.""" return self.root / "code" @property def io_logs_dir(self) -> Path: """Path to io-logs directory.""" return self.root / "io-logs" def io_log_path(self, session_id: str) -> Path: """Get path to I/O log file for a specific session. Args: session_id: The session identifier. Returns: Path to the session's I/O log file. """ return self.io_logs_dir / f"{session_id}.log" class WorkspaceManager: """Manages workspace creation, destruction, and lifecycle. The WorkspaceManager handles: - Creating workspace directory structure - Destroying workspaces (cleanup) - Checking workspace existence - Providing workspace objects """ WORKSPACES_ROOT = Path.home() / "gsd-workspaces" def __init__(self, project_name: str, root: Optional[Path] = None): """Initialize workspace manager. Args: project_name: Name of the project (used for workspace directory). root: Optional custom root path for workspaces (useful for testing). """ self.project_name = project_name self.root = root or self.WORKSPACES_ROOT if isinstance(self.root, str): self.root = Path(self.root) self.workspace = Workspace(project_name, self.root / project_name) def create(self) -> Workspace: """Create workspace directory structure. Creates all required directories: - .gsd/ (with session-dump/ subdirectory) - code/ - io-logs/ This operation is idempotent - calling multiple times will not fail. Returns: The Workspace object with paths configured. """ # Create all directories self.workspace.gsd_dir.mkdir(parents=True, exist_ok=True) self.workspace.session_dump_dir.mkdir(exist_ok=True) self.workspace.code_dir.mkdir(exist_ok=True) self.workspace.io_logs_dir.mkdir(exist_ok=True) return self.workspace def destroy(self) -> None: """Remove entire workspace directory. Completely removes the workspace and all its contents. Safe to call even if workspace doesn't exist. """ if self.workspace.root.exists(): shutil.rmtree(self.workspace.root) def exists(self) -> bool: """Check if workspace exists. Returns: True if workspace root directory exists, False otherwise. """ return self.workspace.root.exists() def get_workspace(self) -> Workspace: """Get workspace object (does not create). Returns: The Workspace object with paths configured. """ return self.workspace