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
This commit is contained in:
2026-03-23 09:02:21 -03:00
parent 40a33d773b
commit b36b60353d
4 changed files with 148 additions and 66 deletions

View File

@@ -1,6 +1,7 @@
"""Storage layer for file-based persistence."""
import json
from datetime import datetime
from pathlib import Path
from typing import Optional
@@ -117,22 +118,34 @@ class FileStorage:
f.write(new_content)
def write_session_file(self, session: Session) -> None:
"""Crea projects/<slug>/sessions/YYYY-MM-DD_HHMM.md"""
"""Crea projects/<slug>/sessions/YYYY-MM-DD_HHMM.md y el JSON correspondiente."""
from tracker.storage.markdown_writer import MarkdownWriter
sessions_path = self._sessions_path(session.project_slug)
sessions_path.mkdir(parents=True, exist_ok=True)
started = session.started_at
filename = started.strftime("%Y-%m-%d_%H%M.md")
session_path = sessions_path / filename
md_filename = started.strftime("%Y-%m-%d_%H%M.md")
json_filename = f"{session.id}.json"
# Write markdown file
writer = MarkdownWriter()
content = writer.format_session_file(session)
with open(session_path, "w", encoding="utf-8") as f:
md_path = sessions_path / md_filename
with open(md_path, "w", encoding="utf-8") as f:
f.write(content)
# Write JSON file for tracking
json_path = sessions_path / json_filename
session_data = session.model_dump(mode="json")
# Serialize datetime objects to ISO format
if isinstance(session_data.get("started_at"), datetime):
session_data["started_at"] = session_data["started_at"].isoformat()
if isinstance(session_data.get("ended_at"), datetime):
session_data["ended_at"] = session_data["ended_at"].isoformat()
with open(json_path, "w", encoding="utf-8") as f:
json.dump(session_data, f, indent=2, ensure_ascii=False, default=str)
def active_session_path(self) -> Path:
"""Returns Path to projects/.active_session.json"""
return self.projects_root / ".active_session.json"
@@ -156,3 +169,98 @@ class FileStorage:
path = self.active_session_path()
if path.exists():
path.unlink()
def write_file(self, slug: str, relative_path: str, content: str) -> None:
"""Escribe contenido a un archivo en el proyecto.
Args:
slug: Project slug.
relative_path: Relative path within the project.
content: Content to write.
"""
file_path = self._project_path(slug) / relative_path
file_path.parent.mkdir(parents=True, exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
def read_file(self, slug: str, relative_path: str) -> str:
"""Lee contenido de un archivo en el proyecto.
Args:
slug: Project slug.
relative_path: Relative path within the project.
Returns:
File content or empty string if not found.
"""
file_path = self._project_path(slug) / relative_path
if not file_path.exists():
return ""
with open(file_path, "r", encoding="utf-8") as f:
return f.read()
def extract_autogen_section(self, slug: str, section: str) -> str:
"""Extrae contenido de una seccion AUTOGEN del README.md.
Args:
slug: Project slug.
section: Section name (e.g., "SESSIONS", "NEXT_STEPS").
Returns:
Content between AUTOGEN markers, or empty string if not found.
"""
from tracker.storage.markdown_reader import MarkdownReader
reader = MarkdownReader()
content = self.read_readme(slug)
return reader.extract_autogen_section(content, section)
def get_recent_sessions(self, slug: str, limit: int = 5) -> list:
"""Obtiene las sesiones mas recientes de un proyecto.
Args:
slug: Project slug.
limit: Maximum number of sessions to return.
Returns:
List of Session objects sorted by date (most recent first).
"""
from tracker.models.session import Session
sessions_path = self._sessions_path(slug)
if not sessions_path.exists():
return []
sessions = []
for json_file in sessions_path.glob("*.json"):
try:
with open(json_file, "r", encoding="utf-8") as f:
data = json.load(f)
session = Session(**data)
sessions.append(session)
except (json.JSONDecodeError, TypeError):
continue
# Sort by started_at descending
sessions.sort(key=lambda s: s.started_at, reverse=True)
return sessions[:limit]
def list_projects(self) -> list[str]:
"""Lista todos los slugs de proyectos.
Returns:
List of project slugs.
"""
if not self.projects_root.exists():
return []
return [d.name for d in self.projects_root.iterdir() if d.is_dir() and not d.name.startswith(".")]
def project_exists(self, slug: str) -> bool:
"""Verifica si un proyecto existe.
Args:
slug: Project slug.
Returns:
True if project exists.
"""
return self._project_path(slug).exists()