""" Paths - Centralized path resolution for configuration and environment variables. This module provides centralized path resolution for all configurable paths in the application. All paths can be overridden via environment variables or config.toml. Environment Variables: SLOP_CONFIG: Path to config.toml SLOP_LOGS_DIR: Path to logs directory SLOP_SCRIPTS_DIR: Path to generated scripts directory Configuration (config.toml): [paths] logs_dir = "logs/sessions" scripts_dir = "scripts/generated" Path Functions: get_config_path() -> Path to config.toml get_conductor_dir(project_path=None) -> Path to conductor directory get_logs_dir() -> Path to logs/sessions get_scripts_dir() -> Path to scripts/generated get_tracks_dir(project_path=None) -> Path to conductor/tracks get_track_state_dir(track_id, project_path=None) -> Path to conductor/tracks/ get_archive_dir(project_path=None) -> Path to conductor/archive Resolution Order: 1. Check project-specific manual_slop.toml (for conductor paths) 2. Check environment variable (for logs/scripts) 3. Check config.toml [paths] section (for logs/scripts) 4. Fall back to default Usage: from src.paths import get_logs_dir, get_scripts_dir logs_dir = get_logs_dir() scripts_dir = get_scripts_dir() See Also: - docs/guide_tools.md for configuration documentation - src/session_logger.py for logging paths - src/project_manager.py for project paths """ from pathlib import Path import os import tomllib from typing import Optional, Any _RESOLVED: dict[str, Path] = {} def get_config_path() -> Path: root_dir = Path(__file__).resolve().parent.parent return Path(os.environ.get("SLOP_CONFIG", root_dir / "config.toml")) def get_global_presets_path() -> Path: root_dir = Path(__file__).resolve().parent.parent return Path(os.environ.get("SLOP_GLOBAL_PRESETS", root_dir / "presets.toml")) def get_project_presets_path(project_root: Path) -> Path: return project_root / "project_presets.toml" def get_global_tool_presets_path() -> Path: root_dir = Path(__file__).resolve().parent.parent return Path(os.environ.get("SLOP_GLOBAL_TOOL_PRESETS", root_dir / "tool_presets.toml")) def get_project_tool_presets_path(project_root: Path) -> Path: return project_root / "project_tool_presets.toml" def get_global_personas_path() -> Path: root_dir = Path(__file__).resolve().parent.parent return Path(os.environ.get("SLOP_GLOBAL_PERSONAS", root_dir / "personas.toml")) def get_project_personas_path(project_root: Path) -> Path: return project_root / "project_personas.toml" def _resolve_path(env_var: str, config_key: str, default: str) -> Path: root_dir = Path(__file__).resolve().parent.parent p = None if env_var in os.environ: p = Path(os.environ[env_var]) else: try: with open(get_config_path(), "rb") as f: cfg = tomllib.load(f) if "paths" in cfg and config_key in cfg["paths"]: p = Path(cfg["paths"][config_key]) except (FileNotFoundError, tomllib.TOMLDecodeError): pass if p is None: p = Path(default) if not p.is_absolute(): return root_dir / p return p def _get_project_conductor_dir_from_toml(project_root: Path) -> Optional[Path]: # Look for manual_slop.toml in project_root toml_path = project_root / 'manual_slop.toml' if not toml_path.exists(): return None try: with open(toml_path, 'rb') as f: data = tomllib.load(f) # Check [conductor] dir = '...' c_dir = data.get('conductor', {}).get('dir') if c_dir: p = Path(c_dir) if not p.is_absolute(): p = project_root / p return p.resolve() except: pass return None def get_conductor_dir(project_path: Optional[str] = None) -> Path: if not project_path: # Fallback for legacy/tests, but we should avoid this return Path('conductor').resolve() project_root = Path(project_path).resolve() p = _get_project_conductor_dir_from_toml(project_root) if p: return p return (project_root / "conductor").resolve() def get_logs_dir() -> Path: if "logs_dir" not in _RESOLVED: _RESOLVED["logs_dir"] = _resolve_path("SLOP_LOGS_DIR", "logs_dir", "logs/sessions") return _RESOLVED["logs_dir"] def get_scripts_dir() -> Path: if "scripts_dir" not in _RESOLVED: _RESOLVED["scripts_dir"] = _resolve_path("SLOP_SCRIPTS_DIR", "scripts_dir", "scripts/generated") return _RESOLVED["scripts_dir"] def get_tracks_dir(project_path: Optional[str] = None) -> Path: return get_conductor_dir(project_path) / "tracks" def get_track_state_dir(track_id: str, project_path: Optional[str] = None) -> Path: return get_tracks_dir(project_path) / track_id def get_archive_dir(project_path: Optional[str] = None) -> Path: return get_conductor_dir(project_path) / "archive" def _resolve_path_info(env_var: str, config_key: str, default: str) -> dict[str, Any]: if env_var in os.environ: return {'path': Path(os.environ[env_var]).resolve(), 'source': f'env:{env_var}'} try: with open(get_config_path(), 'rb') as f: cfg = tomllib.load(f) if 'paths' in cfg and config_key in cfg['paths']: p = Path(cfg['paths'][config_key]) if not p.is_absolute(): p = (Path(__file__).resolve().parent.parent / p).resolve() return {'path': p, 'source': 'config.toml'} except: pass root_dir = Path(__file__).resolve().parent.parent p = (root_dir / default).resolve() return {'path': p, 'source': 'default'} def get_full_path_info() -> dict[str, dict[str, Any]]: return { 'logs_dir': _resolve_path_info('SLOP_LOGS_DIR', 'logs_dir', 'logs/sessions'), 'scripts_dir': _resolve_path_info('SLOP_SCRIPTS_DIR', 'scripts_dir', 'scripts/generated') } def reset_resolved() -> None: """For testing only - clear cached resolutions.""" _RESOLVED.clear()