165 lines
5.5 KiB
Python
165 lines
5.5 KiB
Python
"""
|
|
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/<track_id>
|
|
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()
|