- 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
129 lines
3.2 KiB
Python
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)
|