feat: Add delete_notes parameter to remove project endpoint (#391)

Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Paul Hernandez
2025-10-21 14:09:20 -05:00
committed by GitHub
parent bb8da31472
commit c7e6eab02f
5 changed files with 225 additions and 7 deletions
@@ -1,7 +1,7 @@
"""Router for project management."""
import os
from fastapi import APIRouter, HTTPException, Path, Body, BackgroundTasks, Response
from fastapi import APIRouter, HTTPException, Path, Body, BackgroundTasks, Response, Query
from typing import Optional
from loguru import logger
@@ -247,11 +247,15 @@ async def add_project(
async def remove_project(
project_service: ProjectServiceDep,
name: str = Path(..., description="Name of the project to remove"),
delete_notes: bool = Query(
False, description="If True, delete project directory from filesystem"
),
) -> ProjectStatusResponse:
"""Remove a project from configuration and database.
Args:
name: The name of the project to remove
delete_notes: If True, delete the project directory from the filesystem
Returns:
Response confirming the project was removed
@@ -276,7 +280,7 @@ async def remove_project(
detail += "This is the only project in your configuration."
raise HTTPException(status_code=400, detail=detail)
await project_service.remove_project(name)
await project_service.remove_project(name, delete_notes=delete_notes)
return ProjectStatusResponse(
message=f"Project '{name}' removed successfully",
+9 -3
View File
@@ -119,13 +119,18 @@ def add_project(
@project_app.command("remove")
def remove_project(
name: str = typer.Argument(..., help="Name of the project to remove"),
delete_notes: bool = typer.Option(
False, "--delete-notes", help="Delete project files from disk"
),
) -> None:
"""Remove a project."""
async def _remove_project():
async with get_client() as client:
project_permalink = generate_permalink(name)
response = await call_delete(client, f"/projects/{project_permalink}")
response = await call_delete(
client, f"/projects/{project_permalink}?delete_notes={delete_notes}"
)
return ProjectStatusResponse.model_validate(response.json())
try:
@@ -135,8 +140,9 @@ def remove_project(
console.print(f"[red]Error removing project: {str(e)}[/red]")
raise typer.Exit(1)
# Show this message regardless of method used
console.print("[yellow]Note: The project files have not been deleted from disk.[/yellow]")
# Show this message only if files were not deleted
if not delete_notes:
console.print("[yellow]Note: The project files have not been deleted from disk.[/yellow]")
@project_app.command("default")
+22 -2
View File
@@ -1,7 +1,9 @@
"""Project management service for Basic Memory."""
import asyncio
import json
import os
import shutil
from datetime import datetime
from pathlib import Path
from typing import Dict, Optional, Sequence
@@ -219,11 +221,12 @@ class ProjectService:
logger.info(f"Project '{name}' added at {resolved_path}")
async def remove_project(self, name: str) -> None:
async def remove_project(self, name: str, delete_notes: bool = False) -> None:
"""Remove a project from configuration and database.
Args:
name: The name of the project to remove
delete_notes: If True, delete the project directory from filesystem
Raises:
ValueError: If the project doesn't exist or is the default project
@@ -231,16 +234,33 @@ class ProjectService:
if not self.repository: # pragma: no cover
raise ValueError("Repository is required for remove_project")
# Get project path before removing from config
project = await self.get_project(name)
project_path = project.path if project else None
# First remove from config (this will validate the project exists and is not default)
self.config_manager.remove_project(name)
# Then remove from database using robust lookup
project = await self.get_project(name)
if project:
await self.repository.delete(project.id)
logger.info(f"Project '{name}' removed from configuration and database")
# Optionally delete the project directory
if delete_notes and project_path:
try:
path_obj = Path(project_path)
if path_obj.exists() and path_obj.is_dir():
await asyncio.to_thread(shutil.rmtree, project_path)
logger.info(f"Deleted project directory: {project_path}")
else:
logger.warning(
f"Project directory not found or not a directory: {project_path}"
)
except Exception as e:
logger.warning(f"Failed to delete project directory {project_path}: {e}")
async def set_default_project(self, name: str) -> None:
"""Set the default project in configuration and database.