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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user