mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
b86dd6fb53
Signed-off-by: phernandez <paul@basicmachines.co>
199 lines
6.5 KiB
Python
199 lines
6.5 KiB
Python
"""Format command for basic-memory CLI."""
|
|
|
|
from pathlib import Path
|
|
from typing import Annotated, Optional
|
|
|
|
import typer
|
|
from loguru import logger
|
|
from rich.console import Console
|
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
|
|
from basic_memory.cli.app import app
|
|
from basic_memory.cli.commands.command_utils import run_with_cleanup
|
|
from basic_memory.config import ConfigManager, get_project_config
|
|
from basic_memory.file_utils import format_file
|
|
|
|
console = Console()
|
|
|
|
|
|
def is_markdown_extension(path: Path) -> bool:
|
|
"""Check if file has a markdown extension."""
|
|
return path.suffix.lower() in (".md", ".markdown")
|
|
|
|
|
|
async def format_single_file(file_path: Path, app_config) -> tuple[Path, bool, Optional[str]]:
|
|
"""Format a single file.
|
|
|
|
Returns:
|
|
Tuple of (path, success, error_message)
|
|
"""
|
|
try:
|
|
result = await format_file(
|
|
file_path, app_config, is_markdown=is_markdown_extension(file_path)
|
|
)
|
|
if result is not None:
|
|
return (file_path, True, None)
|
|
else:
|
|
return (file_path, False, "No formatter configured or formatting skipped")
|
|
except Exception as e:
|
|
return (file_path, False, str(e))
|
|
|
|
|
|
async def format_files(
|
|
paths: list[Path], app_config, show_progress: bool = True
|
|
) -> tuple[int, int, list[tuple[Path, str]]]:
|
|
"""Format multiple files.
|
|
|
|
Returns:
|
|
Tuple of (formatted_count, skipped_count, errors)
|
|
"""
|
|
formatted = 0
|
|
skipped = 0
|
|
errors: list[tuple[Path, str]] = []
|
|
|
|
if show_progress:
|
|
with Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
console=console,
|
|
) as progress:
|
|
task = progress.add_task("Formatting files...", total=len(paths))
|
|
|
|
for file_path in paths:
|
|
path, success, error = await format_single_file(file_path, app_config)
|
|
if success:
|
|
formatted += 1
|
|
elif error and "No formatter configured" not in error:
|
|
errors.append((path, error))
|
|
else:
|
|
skipped += 1
|
|
progress.update(task, advance=1)
|
|
else:
|
|
for file_path in paths:
|
|
path, success, error = await format_single_file(file_path, app_config)
|
|
if success:
|
|
formatted += 1
|
|
elif error and "No formatter configured" not in error:
|
|
errors.append((path, error))
|
|
else:
|
|
skipped += 1
|
|
|
|
return formatted, skipped, errors
|
|
|
|
|
|
async def run_format(
|
|
path: Optional[Path] = None,
|
|
project: Optional[str] = None,
|
|
) -> None:
|
|
"""Run the format command."""
|
|
app_config = ConfigManager().config
|
|
|
|
# Check if formatting is enabled
|
|
if (
|
|
not app_config.format_on_save
|
|
and not app_config.formatter_command
|
|
and not app_config.formatters
|
|
):
|
|
console.print(
|
|
"[yellow]No formatters configured. Set format_on_save=true and "
|
|
"formatter_command or formatters in your config.[/yellow]"
|
|
)
|
|
console.print(
|
|
"\nExample config (~/.basic-memory/config.json):\n"
|
|
' "format_on_save": true,\n'
|
|
' "formatter_command": "prettier --write {file}"\n'
|
|
)
|
|
raise typer.Exit(1)
|
|
|
|
# Temporarily enable format_on_save for this command
|
|
# (so format_file actually runs the formatter)
|
|
original_format_on_save = app_config.format_on_save
|
|
app_config.format_on_save = True
|
|
|
|
try:
|
|
# Determine which files to format
|
|
if path:
|
|
# Format specific file or directory
|
|
if path.is_file():
|
|
files = [path]
|
|
elif path.is_dir():
|
|
# Find all markdown and json files
|
|
files = (
|
|
list(path.rglob("*.md"))
|
|
+ list(path.rglob("*.json"))
|
|
+ list(path.rglob("*.canvas"))
|
|
)
|
|
else:
|
|
console.print(f"[red]Path not found: {path}[/red]")
|
|
raise typer.Exit(1)
|
|
else:
|
|
# Format all files in project
|
|
project_config = get_project_config(project)
|
|
project_path = Path(project_config.home)
|
|
|
|
if not project_path.exists():
|
|
console.print(f"[red]Project path not found: {project_path}[/red]")
|
|
raise typer.Exit(1)
|
|
|
|
# Find all markdown and json files
|
|
files = (
|
|
list(project_path.rglob("*.md"))
|
|
+ list(project_path.rglob("*.json"))
|
|
+ list(project_path.rglob("*.canvas"))
|
|
)
|
|
|
|
if not files:
|
|
console.print("[yellow]No files found to format.[/yellow]")
|
|
return
|
|
|
|
console.print(f"Found {len(files)} file(s) to format...")
|
|
|
|
formatted, skipped, errors = await format_files(files, app_config)
|
|
|
|
# Print summary
|
|
console.print()
|
|
if formatted > 0:
|
|
console.print(f"[green]Formatted: {formatted} file(s)[/green]")
|
|
if skipped > 0:
|
|
console.print(f"[dim]Skipped: {skipped} file(s) (no formatter for extension)[/dim]")
|
|
if errors:
|
|
console.print(f"[red]Errors: {len(errors)} file(s)[/red]")
|
|
for path, error in errors:
|
|
console.print(f" [red]{path}[/red]: {error}")
|
|
|
|
finally:
|
|
# Restore original setting
|
|
app_config.format_on_save = original_format_on_save
|
|
|
|
|
|
@app.command()
|
|
def format(
|
|
path: Annotated[
|
|
Optional[Path],
|
|
typer.Argument(help="File or directory to format. Defaults to current project."),
|
|
] = None,
|
|
project: Annotated[
|
|
Optional[str],
|
|
typer.Option("--project", "-p", help="Project name to format."),
|
|
] = None,
|
|
) -> None:
|
|
"""Format files using configured formatters.
|
|
|
|
Uses the formatter_command or formatters settings from your config.
|
|
By default, formats all .md, .json, and .canvas files in the current project.
|
|
|
|
Examples:
|
|
bm format # Format all files in current project
|
|
bm format --project research # Format files in specific project
|
|
bm format notes/meeting.md # Format a specific file
|
|
bm format notes/ # Format all files in directory
|
|
"""
|
|
try:
|
|
run_with_cleanup(run_format(path, project))
|
|
except Exception as e:
|
|
if not isinstance(e, typer.Exit):
|
|
logger.error(f"Error formatting files: {e}")
|
|
console.print(f"[red]Error formatting files: {e}[/red]")
|
|
raise typer.Exit(code=1)
|
|
raise
|