b0229b6332
Scans codebases for TODO/FIXME/HACK/XXX comments and generates Markdown notes in an Obsidian vault. Includes Zed editor integration that runs on file save, plus a shell wrapper for CLI usage. Supports customizable vault paths, project names, and output directories.
117 lines
3.5 KiB
Python
117 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
||
"""Obsidian TODO Scanner
|
||
|
||
Scan a project directory for TODO / FIXME / HACK / XXX comments and
|
||
emit a Markdown note into an Obsidian vault.
|
||
|
||
Usage:
|
||
python main.py /path/to/project
|
||
python main.py /path/to/project --vault ~/Obsidian/MyVault
|
||
python main.py /path/to/project --name "My Project"
|
||
"""
|
||
|
||
import argparse
|
||
import sys
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import List
|
||
|
||
from utils.config import ensure_unique_path, get_vault_path, sanitize_filename
|
||
from utils.scanner import TodoItem, count_tags, group_by_file, scan_directory
|
||
|
||
|
||
def generate_markdown(project_name: str, todos: List[TodoItem]) -> str:
|
||
lines = []
|
||
lines.append(f"# TODOs – {project_name}")
|
||
lines.append("")
|
||
lines.append(f"_Scanned on {datetime.now().strftime('%Y-%m-%d %H:%M')}_")
|
||
lines.append("")
|
||
|
||
if not todos:
|
||
lines.append("No TODO comments found. 🎉")
|
||
return "\n".join(lines)
|
||
|
||
tag_counts = count_tags(todos)
|
||
lines.append("## Summary")
|
||
for tag, count in sorted(tag_counts.items()):
|
||
lines.append(f"- **{tag}**: {count}")
|
||
lines.append("")
|
||
|
||
groups = group_by_file(todos)
|
||
lines.append("## By File")
|
||
lines.append("")
|
||
|
||
for file_path, items in sorted(groups.items(), key=lambda x: str(x[0])):
|
||
lines.append(f"### `{file_path}`")
|
||
for item in items:
|
||
# Obsidian checkbox + link to line in backticks
|
||
lines.append(f"- [ ] **{item.tag}** (line {item.line}) – {item.text}")
|
||
lines.append("")
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(
|
||
description="Scan a project for TODOs and write them to an Obsidian vault."
|
||
)
|
||
parser.add_argument("project", help="Path to the project directory to scan.")
|
||
parser.add_argument(
|
||
"--vault",
|
||
dest="vault",
|
||
default=None,
|
||
help="Path to the Obsidian vault. Overrides OBSIDIAN_VAULT env var.",
|
||
)
|
||
parser.add_argument(
|
||
"--name",
|
||
dest="name",
|
||
default=None,
|
||
help="Project name for the note title. Defaults to the directory name.",
|
||
)
|
||
parser.add_argument(
|
||
"--output-dir",
|
||
dest="output_dir",
|
||
default="Todos",
|
||
help="Subdirectory inside the vault where the note is created (default: Todos).",
|
||
)
|
||
args = parser.parse_args()
|
||
|
||
project_path = Path(args.project).expanduser().resolve()
|
||
if not project_path.exists():
|
||
print(f"Error: project path does not exist: {project_path}", file=sys.stderr)
|
||
sys.exit(1)
|
||
if not project_path.is_dir():
|
||
print(f"Error: not a directory: {project_path}", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
try:
|
||
vault_path = get_vault_path(args.vault)
|
||
except Exception as e:
|
||
print(f"Error: {e}", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
project_name = args.name or project_path.name
|
||
print(f"🔍 Scanning {project_path} ...")
|
||
todos = scan_directory(project_path)
|
||
print(f" Found {len(todos)} item(s).")
|
||
|
||
md_content = generate_markdown(project_name, todos)
|
||
|
||
note_dir = vault_path / args.output_dir
|
||
note_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
safe_name = sanitize_filename(f"TODOs {project_name}")
|
||
note_path = note_dir / f"{safe_name}.md"
|
||
note_path = ensure_unique_path(note_path)
|
||
|
||
try:
|
||
note_path.write_text(md_content, encoding="utf-8")
|
||
print(f"✅ Note written to: {note_path}")
|
||
except OSError as e:
|
||
print(f"Error writing note: {e}", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|