Files
tracker-cli/tracker/services/project_service.py
Daniel Arroyo b36b60353d Implement complete CLI commands for MVP-1 Personal Tracker
- Refactored CLI commands from nested Typer subapps to direct command functions
- Fixed main.py to use app.command() instead of app.add_typer_command()
- Fixed project_service.py to properly load projects from YAML
- Fixed file_storage.py to save session JSON files alongside markdown
- Added missing methods: write_file, read_file, extract_autogen_section, get_recent_sessions
- Fixed root_path and repo_path to use strings instead of Path objects
2026-03-23 09:02:21 -03:00

129 lines
3.2 KiB
Python

"""Project service for project management."""
import uuid
from datetime import datetime
from pathlib import Path
from typing import Optional
import yaml
from ..models import Project
_PROJECTS_ROOT = Path("projects")
def get_projects_root() -> Path:
"""Return the root directory for all projects."""
return _PROJECTS_ROOT
def _get_project_meta_path(slug: str) -> Path:
"""Return the path to the project's meta/project.yaml file."""
return _PROJECTS_ROOT / slug / "meta" / "project.yaml"
def _get_project_readme_path(slug: str) -> Path:
"""Return the path to the project's README.md file."""
return _PROJECTS_ROOT / slug / "README.md"
def create_project(
name: str,
slug: str,
description: str = "",
type: str = "misc",
tags: Optional[list[str]] = None,
repo_path: Optional[Path] = None,
) -> Project:
"""
Create a new project and return the Project instance.
Note: This does not write any files - that is handled by storage.
"""
if tags is None:
tags = []
project = Project(
id=str(uuid.uuid4()),
name=name,
slug=slug,
description=description,
type=type,
status="inbox",
tags=tags,
root_path=str(_PROJECTS_ROOT / slug),
repo_path=str(repo_path) if repo_path else None,
created_at=datetime.now(),
updated_at=datetime.now(),
)
return project
def get_project(slug: str) -> Optional[Project]:
"""
Get a project by slug.
Reads from meta/project.yaml in the project directory.
"""
meta_path = _get_project_meta_path(slug)
if not meta_path.exists():
return None
try:
with open(meta_path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
if data:
return Project(**data)
except (yaml.YAMLError, TypeError):
pass
return None
def update_project(slug: str, **kwargs) -> Optional[Project]:
"""
Update a project's attributes.
Note: This does not persist - that is handled by storage.
"""
project = get_project(slug)
if project is None:
return None
for key, value in kwargs.items():
if hasattr(project, key):
setattr(project, key, value)
project.updated_at = datetime.now()
return project
def list_projects() -> list[Project]:
"""
List all projects.
Note: This reads from file system - placeholder for storage integration.
"""
projects_root = get_projects_root()
if not projects_root.exists():
return []
projects = []
for item in projects_root.iterdir():
if item.is_dir() and not item.name.startswith("."):
project = get_project(item.name)
if project is not None:
projects.append(project)
return projects
def ensure_project_structure(slug: str) -> None:
"""
Ensure the project directory structure exists.
Creates: sessions/, docs/, assets/, meta/
Note: This creates directories only - actual file writing is storage's job.
"""
project_root = _PROJECTS_ROOT / slug
directories = ["sessions", "docs", "assets", "meta"]
for directory in directories:
(project_root / directory).mkdir(parents=True, exist_ok=True)