feat: 添加 gsd-openclaw 技能
PR Test / lint (pull_request) Successful in 2s

This commit is contained in:
2026-03-11 10:41:01 +08:00
parent 3909bb0321
commit 96f06f3751
35 changed files with 7272 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
"""
gsd-openclaw: GSD to OpenClaw bridge skill.
This package provides the bridge between OpenClaw and GSD (Get Shit Done),
enabling users to interact with GSD through messaging platforms.
"""
__version__ = "0.1.0"
from gsd_openclaw.state_manager import SessionState, StateManager, save_state, load_state
from gsd_openclaw.workspace import Workspace, WorkspaceManager
from gsd_openclaw.tmux_session import TmuxSession, TmuxSessionManager, SessionInfo
__all__ = [
"SessionState",
"StateManager",
"save_state",
"load_state",
"Workspace",
"WorkspaceManager",
"TmuxSession",
"TmuxSessionManager",
"SessionInfo",
]
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""CLI for gsd-openclaw session management."""
from __future__ import annotations
import argparse
import sys
from typing import List
from gsd_openclaw.tmux_session import (
TmuxSessionManager,
TmuxSessionError,
CleanupResult,
)
def cmd_create(args):
"""Create a new session for a project."""
manager = TmuxSessionManager()
try:
session = manager.create_session(
project_name=args.project,
display_name=args.display_name
)
print(f"Created session: {session.tmux_session_name}")
print(f"Session ID: {session.session_id}")
print(f"Project: {session.project_name}")
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def cmd_list(args):
"""List all gsd sessions."""
manager = TmuxSessionManager()
sessions = manager.list_gsd_sessions()
if not sessions:
print("No active sessions.")
return
print(f"{'Project':<20} {'Session ID':<10} {'Last Active':<20} {'Status'}")
print("-" * 70)
for s in sessions:
status = "ORPHAN" if s.is_orphan else "active"
display = s.display_name or s.project_name
print(f"{display:<20} {s.session_id:<10} {s.last_active_at:<20} {status}")
def cmd_destroy(args):
"""Destroy a session."""
manager = TmuxSessionManager()
result = manager.destroy_session(
project_name=args.project,
cleanup_workspace=args.cleanup
)
if result:
print(f"Destroyed session for: {args.project}")
else:
print(f"No session found for: {args.project}")
sys.exit(1)
def cmd_resume(args):
"""Resume an existing session."""
manager = TmuxSessionManager()
try:
session = manager.resume_session(project_name=args.project)
print(f"Resumed session: {session.tmux_session_name}")
print(f"Session ID: {session.session_id}")
print(f"Project: {session.project_name}")
except TmuxSessionError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def cmd_cleanup(args):
"""Clean up orphan sessions."""
manager = TmuxSessionManager()
result = manager.cleanup_orphans()
if result.cleaned_sessions:
print(f"Cleaned {len(result.cleaned_sessions)} orphan session(s):")
for name in result.cleaned_sessions:
print(f" - {name}")
if result.errors:
print(f"Errors ({len(result.errors)}):")
for err in result.errors:
print(f" - {err}")
if not result.cleaned_sessions and not result.errors:
print("No orphan sessions found.")
def main():
"""Main entry point for CLI."""
parser = argparse.ArgumentParser(
prog="gsd-ocl",
description="GSD-OpenClaw session management"
)
subparsers = parser.add_subparsers(dest="command", required=True)
# create
p_create = subparsers.add_parser("create", help="Create a new session")
p_create.add_argument("project", help="Project name")
p_create.add_argument("--display-name", "-d", help="Friendly display name")
p_create.set_defaults(func=cmd_create)
# list
p_list = subparsers.add_parser("list", help="List all sessions")
p_list.set_defaults(func=cmd_list)
# destroy
p_destroy = subparsers.add_parser("destroy", help="Destroy a session")
p_destroy.add_argument("project", help="Project name")
p_destroy.add_argument("--cleanup", "-c", action="store_true",
help="Also remove workspace directory")
p_destroy.set_defaults(func=cmd_destroy)
# resume
p_resume = subparsers.add_parser("resume", help="Resume a session")
p_resume.add_argument("project", help="Project name")
p_resume.set_defaults(func=cmd_resume)
# cleanup
p_cleanup = subparsers.add_parser("cleanup", help="Clean up orphan sessions")
p_cleanup.set_defaults(func=cmd_cleanup)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()
@@ -0,0 +1,247 @@
"""
State persistence layer for gsd-openclaw.
Provides atomic state saves with backup rotation for crash-safe session checkpointing.
"""
from __future__ import annotations
import dataclasses
import json
import os
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Optional
@dataclass
class SessionState:
"""Represents the persistent state of a GSD session.
Attributes:
schema_version: Version of the state schema (default "1.0")
session_id: Unique identifier for this session
project_name: Name of the project
tmux_session_name: Name of the tmux session
tmux_pane_id: ID of the tmux pane
created_at: ISO format timestamp when session was created
last_active_at: ISO format timestamp of last activity
command_count: Number of commands executed in this session
last_output_snapshot: Last captured output from the session
display_name: User-friendly display name for the session
"""
schema_version: str = "1.0"
session_id: str = ""
project_name: str = ""
tmux_session_name: str = ""
tmux_pane_id: str = ""
created_at: str = ""
last_active_at: str = ""
command_count: int = 0
last_output_snapshot: Optional[str] = None
display_name: Optional[str] = None
def to_dict(self) -> dict[str, Any]:
"""Convert SessionState to a dictionary.
Returns:
Dictionary representation of the session state.
"""
return dataclasses.asdict(self)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> SessionState:
"""Create SessionState from a dictionary.
Unknown fields in the dictionary are ignored.
Args:
data: Dictionary containing session state data.
Returns:
SessionState instance.
"""
# Get field names from the dataclass
field_names = {f.name for f in dataclasses.fields(cls)}
# Filter to only known fields
filtered_data = {k: v for k, v in data.items() if k in field_names}
return cls(**filtered_data)
# Placeholder functions - to be implemented in Task 2 and 3
def save_state(state: SessionState, state_path: Path) -> None:
"""Save session state to file atomically with backup rotation.
This function implements crash-safe state persistence:
1. Creates parent directories if they don't exist
2. Rotates existing state file to backup (state.json -> state.json.bak)
3. Writes to a temporary file first
4. Atomically renames temp file to final path
The atomic rename ensures that:
- Readers never see a partially written file
- Crash during write leaves either old or new file intact
Args:
state: The SessionState to save.
state_path: Path where state.json should be written.
Raises:
OSError: If file operations fail (permissions, disk full, etc.)
"""
state_path = Path(state_path)
backup_path = state_path.with_suffix('.json.bak')
# 1. Create parent directories if missing
state_path.parent.mkdir(parents=True, exist_ok=True)
# 2. Rotate backup: if state.json exists, move it to state.json.bak
if state_path.exists():
# Remove old backup if exists
if backup_path.exists():
backup_path.unlink()
# Rename current state to backup
state_path.rename(backup_path)
# 3. Write to temp file in same directory (for atomic rename)
fd, temp_path = tempfile.mkstemp(dir=state_path.parent, suffix='.tmp')
try:
with os.fdopen(fd, 'w', encoding='utf-8') as f:
json.dump(state.to_dict(), f, indent=2, ensure_ascii=False)
# 4. Atomic replace
os.replace(temp_path, state_path)
except Exception:
# Clean up temp file on failure
if os.path.exists(temp_path):
os.unlink(temp_path)
raise
def load_state(state_path: Path) -> Optional[SessionState]:
"""Load session state from file with validation and backup fallback.
This function implements robust state loading:
1. If state.json doesn't exist, try state.json.bak
2. If neither exists, return None
3. Read and parse JSON with utf-8 encoding
4. Validate schema version (must be "1.0")
5. Validate required fields (session_id, project_name, tmux_session_name)
6. On JSONDecodeError or ValueError, fall back to backup
Args:
state_path: Path to state.json file.
Returns:
SessionState if loaded successfully, None if no state file exists.
Raises:
ValueError: If schema version is unsupported or required fields are missing
(only raised if backup also fails).
"""
state_path = Path(state_path)
backup_path = state_path.with_suffix('.json.bak')
def _validate_and_load(path: Path) -> SessionState:
"""Internal: Load and validate state from a specific path."""
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Validate schema version
schema_version = data.get('schema_version', '')
if schema_version != '1.0':
raise ValueError(f"Unsupported schema version: {schema_version}")
# Validate required fields
required_fields = ['session_id', 'project_name', 'tmux_session_name']
for field in required_fields:
if field not in data or not data[field]:
raise ValueError(f"Missing required field: {field}")
return SessionState.from_dict(data)
# Try main file first, then backup
paths_to_try = []
if state_path.exists():
paths_to_try.append(state_path)
if backup_path.exists() and backup_path != state_path:
paths_to_try.append(backup_path)
if not paths_to_try:
return None
last_error = None
for path in paths_to_try:
try:
return _validate_and_load(path)
except json.JSONDecodeError as e:
last_error = e
continue # Try next path (backup)
except ValueError as e:
last_error = e
continue # Try next path (backup)
# If we get here, all paths failed
if last_error:
raise last_error
return None
class StateManager:
"""Manager class for state persistence operations.
Provides a clean interface for state persistence without requiring
callers to manage paths directly. Encapsulates workspace path resolution
and delegates to save_state/load_state functions.
Attributes:
WORKSPACES_ROOT: Default root directory for all workspaces.
project_name: Name of the project.
workspaces_root: Root directory for workspaces (configurable for testing).
workspace_path: Full path to the project's workspace.
state_path: Full path to the project's state.json file.
"""
WORKSPACES_ROOT = Path.home() / "gsd-workspaces"
def __init__(self, project_name: str, workspaces_root: Optional[Path] = None):
"""Initialize StateManager for a project.
Args:
project_name: Name of the project (used for workspace directory).
workspaces_root: Optional custom root for workspaces (useful for testing).
Defaults to ~/gsd-workspaces.
"""
self.project_name = project_name
self.workspaces_root = Path(workspaces_root) if workspaces_root else self.WORKSPACES_ROOT
self.workspace_path = self.workspaces_root / project_name
self.state_path = self.workspace_path / ".gsd" / "state.json"
def save(self, state: SessionState) -> None:
"""Save session state to the project's state file.
Args:
state: The SessionState to save.
Raises:
OSError: If file operations fail.
"""
save_state(state, self.state_path)
def load(self) -> Optional[SessionState]:
"""Load session state from the project's state file.
Returns:
SessionState if loaded successfully, None if no state file exists.
Raises:
ValueError: If schema version is unsupported or required fields are missing.
"""
return load_state(self.state_path)
def get_state_path(self) -> Path:
"""Get the path to the project's state file.
Returns:
Path to state.json file.
"""
return self.state_path
@@ -0,0 +1,467 @@
"""Tmux session lifecycle management using libtmux.
This module provides the core functionality for creating, listing, and destroying
tmux sessions. It is the primary interface to tmux that all other components use.
"""
from __future__ import annotations
import re
import uuid
from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional
import libtmux
import libtmux.exc
from gsd_openclaw.state_manager import StateManager, SessionState
from gsd_openclaw.workspace import WorkspaceManager
class TmuxSessionError(Exception):
"""Errors related to tmux session management."""
pass
@dataclass
class TmuxSession:
"""Represents a managed tmux session.
Attributes:
session_id: Unique identifier for this session.
project_name: Name of the project this session belongs to.
tmux_session_name: Name of the tmux session (gsd-{project} format).
tmux_session: Runtime reference to the actual tmux session (optional).
"""
session_id: str
project_name: str
tmux_session_name: str
tmux_session: Optional[libtmux.Session] = None
@property
def exists(self) -> bool:
"""Check if the tmux session still exists.
Returns:
True if the session exists in tmux, False otherwise.
"""
if self.tmux_session is None:
return False
try:
# Check if session still exists in tmux by accessing its ID
_ = self.tmux_session.session_id
return True
except libtmux.exc.LibTmuxException:
return False
@dataclass
class SessionInfo:
"""Session info for display.
Attributes:
tmux_name: Name of the tmux session.
project_name: Name of the project.
session_id: Unique session identifier.
created_at: ISO timestamp when session was created.
last_active_at: ISO timestamp of last activity.
command_count: Number of commands executed.
display_name: User-friendly display name.
is_orphan: True if tmux session exists but no state file.
"""
tmux_name: str
project_name: str
session_id: str
created_at: str
last_active_at: str
command_count: int
display_name: Optional[str]
is_orphan: bool = False
@dataclass
class CleanupResult:
"""Result of orphan cleanup.
Attributes:
cleaned_sessions: List of tmux session names that were cleaned up.
errors: List of error messages for sessions that failed to clean.
"""
cleaned_sessions: List[str]
errors: List[str]
class TmuxSessionManager:
"""Manages tmux session lifecycle using libtmux.
This class provides methods for creating, listing, and destroying tmux
sessions. It handles session naming with collision detection and integrates
with StateManager for persistence and WorkspaceManager for project isolation.
Attributes:
SESSION_PREFIX: Prefix for all gsd-managed sessions ("gsd-").
server: libtmux.Server instance for tmux operations.
"""
SESSION_PREFIX = "gsd-"
def __init__(self, server: Optional[libtmux.Server] = None):
"""Initialize TmuxSessionManager.
Args:
server: Optional libtmux.Server instance. If not provided, a new
server connection will be created.
"""
self.server = server or libtmux.Server()
def _generate_session_name(self, project_name: str) -> str:
"""Generate tmux session name from project name.
Args:
project_name: Name of the project.
Returns:
Session name in format "gsd-{project_name}".
"""
return f"{self.SESSION_PREFIX}{project_name}"
def _resolve_collision(self, base_name: str) -> str:
"""Add timestamp suffix if session name already exists.
Args:
base_name: The base session name to check.
Returns:
The original name if no collision, or name with timestamp suffix
in format "gsd-{project}-YYYYMMDD-HHMMSS".
"""
existing = self.server.sessions.filter(session_name=base_name)
if not existing:
return base_name
suffix = datetime.now().strftime("-%Y%m%d-%H%M%S")
return f"{base_name}{suffix}"
def _extract_project_name(self, tmux_name: str) -> str:
"""Extract project name from tmux session name.
Handles both standard format (gsd-{project}) and collision format
(gsd-{project}-YYYYMMDD-HHMMSS).
Args:
tmux_name: The tmux session name.
Returns:
Extracted project name.
"""
# Remove prefix
name = tmux_name[len(self.SESSION_PREFIX):]
# Remove timestamp suffix if present (format: -YYYYMMDD-HHMMSS)
match = re.match(r'^(.+?)(-\d{8}-\d{6})?$', name)
return match.group(1) if match else name
def list_sessions(self) -> List[libtmux.Session]:
"""List all gsd-managed sessions.
Returns:
List of libtmux.Session objects with names starting with "gsd-".
"""
return list(self.server.sessions.filter(
session_name__startswith=self.SESSION_PREFIX
))
def create_session(
self,
project_name: str,
workspace_manager: Optional[WorkspaceManager] = None,
display_name: Optional[str] = None
) -> TmuxSession:
"""Create a new tmux session for a project.
This method:
1. Ensures the workspace directory exists
2. Generates a unique session name (with collision handling)
3. Creates the tmux session (detached)
4. Persists session state
5. Returns a TmuxSession object
Args:
project_name: Name of the project.
workspace_manager: Optional WorkspaceManager instance. If not provided,
a new one will be created for the project.
display_name: Optional user-friendly display name for the session.
Returns:
TmuxSession object with all metadata populated.
"""
# Ensure workspace exists
ws_manager = workspace_manager or WorkspaceManager(project_name)
ws_manager.create()
# Generate unique session name
base_name = self._generate_session_name(project_name)
tmux_name = self._resolve_collision(base_name)
# Create tmux session (detached)
tmux_session = self.server.new_session(
session_name=tmux_name,
attach=False,
window_name='main'
)
# Create session state
session_id = str(uuid.uuid4())[:8]
now = datetime.now().isoformat()
state = SessionState(
session_id=session_id,
project_name=project_name,
tmux_session_name=tmux_name,
tmux_pane_id=tmux_session.active_pane.pane_id,
created_at=now,
last_active_at=now,
display_name=display_name
)
# Persist state
state_manager = StateManager(project_name)
state_manager.save(state)
return TmuxSession(
session_id=session_id,
project_name=project_name,
tmux_session_name=tmux_name,
tmux_session=tmux_session
)
def destroy_session(
self,
project_name: str,
cleanup_workspace: bool = False
) -> bool:
"""Destroy a tmux session and optionally cleanup workspace.
This method:
1. Loads session state to find tmux session name
2. Kills the tmux session
3. Removes state files
4. Optionally removes the entire workspace
Args:
project_name: Name of the project.
cleanup_workspace: If True, also remove the workspace directory.
Returns:
True if session was destroyed, False if no session existed.
"""
state_manager = StateManager(project_name)
state = state_manager.load()
if state is None:
return False # No session to destroy
# Kill tmux session
try:
tmux_session = self.server.sessions.get(
session_name=state.tmux_session_name
)
if tmux_session:
tmux_session.kill()
except libtmux.exc.LibTmuxException:
pass # Session already gone
# Remove state file
if state_manager.state_path.exists():
state_manager.state_path.unlink()
backup_path = state_manager.state_path.with_suffix('.json.bak')
if backup_path.exists():
backup_path.unlink()
# Optionally cleanup workspace
if cleanup_workspace:
workspace_manager = WorkspaceManager(project_name)
workspace_manager.destroy()
return True
def list_gsd_sessions(self) -> List[SessionInfo]:
"""List all gsd sessions with project associations.
This method combines tmux session info with persisted state to provide
a complete view of all gsd sessions.
Returns:
List of SessionInfo objects for all gsd-* sessions.
"""
sessions_info = []
# Get all gsd-* tmux sessions
tmux_sessions = self.list_sessions()
for tmux_session in tmux_sessions:
tmux_name = tmux_session.name
# Extract project name from tmux session name
project_name = self._extract_project_name(tmux_name)
# Try to load state
state_manager = StateManager(project_name)
state = state_manager.load()
if state and state.tmux_session_name == tmux_name:
# Matched session
sessions_info.append(SessionInfo(
tmux_name=tmux_name,
project_name=project_name,
session_id=state.session_id,
created_at=state.created_at,
last_active_at=state.last_active_at,
command_count=state.command_count,
display_name=state.display_name,
is_orphan=False
))
else:
# Orphan session (no matching state)
sessions_info.append(SessionInfo(
tmux_name=tmux_name,
project_name=project_name,
session_id="?",
created_at="?",
last_active_at="?",
command_count=0,
display_name=None,
is_orphan=True
))
return sessions_info
def get_session(self, project_name: str) -> Optional[TmuxSession]:
"""Get existing session without creating new one.
Args:
project_name: Name of the project.
Returns:
TmuxSession if state exists, None otherwise.
"""
state_manager = StateManager(project_name)
state = state_manager.load()
if state is None:
return None
# Find tmux session
tmux_session = None
try:
tmux_session = self.server.sessions.get(
session_name=state.tmux_session_name
)
except libtmux.exc.LibTmuxException:
pass
return TmuxSession(
session_id=state.session_id,
project_name=project_name,
tmux_session_name=state.tmux_session_name,
tmux_session=tmux_session
)
def resume_session(self, project_name: str) -> TmuxSession:
"""Resume an existing session, recreating tmux if needed.
Args:
project_name: Name of the project.
Returns:
TmuxSession with all metadata populated.
Raises:
TmuxSessionError: If no session found, unsupported state version,
or workspace missing.
"""
state_manager = StateManager(project_name)
state = state_manager.load()
if state is None:
raise TmuxSessionError(f"No session found for project: {project_name}")
# Validate state
if state.schema_version != "1.0":
raise TmuxSessionError(
f"Unsupported state version: {state.schema_version}"
)
# Check if tmux session exists
tmux_session = None
try:
tmux_session = self.server.sessions.get(
session_name=state.tmux_session_name
)
except libtmux.exc.LibTmuxException:
pass
# Recreate tmux session if missing
if tmux_session is None:
workspace_manager = WorkspaceManager(project_name)
if not workspace_manager.exists():
raise TmuxSessionError(
f"Workspace missing for project: {project_name}"
)
# Create new tmux session with same name
tmux_session = self.server.new_session(
session_name=state.tmux_session_name,
attach=False,
window_name='main',
start_directory=str(workspace_manager.get_workspace().root)
)
# Update pane ID in state
state.tmux_pane_id = tmux_session.active_pane.pane_id
state.last_active_at = datetime.now().isoformat()
state_manager.save(state)
return TmuxSession(
session_id=state.session_id,
project_name=project_name,
tmux_session_name=state.tmux_session_name,
tmux_session=tmux_session
)
def cleanup_orphans(self) -> CleanupResult:
"""Remove tmux sessions without corresponding state files.
This method identifies orphan tmux sessions (gsd-* sessions without
valid state files) and kills them.
Returns:
CleanupResult with lists of cleaned sessions and errors.
"""
cleaned = []
errors = []
# Get all gsd-* tmux sessions
tmux_sessions = self.list_sessions()
for tmux_session in tmux_sessions:
tmux_name = tmux_session.name
project_name = self._extract_project_name(tmux_name)
# Check for state file
state_manager = StateManager(project_name)
state = state_manager.load()
should_clean = False
if state is None:
# No state file - orphan
should_clean = True
elif state.tmux_session_name != tmux_name:
# State exists but references different tmux session
# This could be a collision case, treat as orphan
should_clean = True
if should_clean:
try:
tmux_session.kill()
cleaned.append(tmux_name)
except libtmux.exc.LibTmuxException as e:
errors.append(f"Failed to kill {tmux_name}: {e}")
return CleanupResult(cleaned_sessions=cleaned, errors=errors)
+143
View File
@@ -0,0 +1,143 @@
"""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