flake-update-20260505
  1#!/usr/bin/env python3
  2"""List git repositories with GitHub remotes under ~/src/ for raffi script filter.
  3
  4Outputs Alfred Script Filter JSON format.
  5Results are cached for 2 hours.
  6Usage: lazypr-raffi [query]
  7"""
  8
  9import json
 10import os
 11import subprocess
 12import sys
 13import tempfile
 14import time
 15
 16CACHE_MAX_AGE = 7200  # 2 hours
 17SRC_DIR = os.path.expanduser("~/src")
 18
 19
 20def cache_paths():
 21    cache_home = os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))
 22    cache_dir = os.path.join(cache_home, "lazypr-raffi")
 23    os.makedirs(cache_dir, exist_ok=True)
 24    return cache_dir, os.path.join(cache_dir, "repos.json")
 25
 26
 27def needs_rebuild(cache_file):
 28    try:
 29        age = time.time() - os.path.getmtime(cache_file)
 30        return age > CACHE_MAX_AGE
 31    except FileNotFoundError:
 32        return True
 33
 34
 35def has_github_remote(repo_path):
 36    """Check if a git repo has a GitHub remote."""
 37    try:
 38        result = subprocess.run(
 39            ["git", "-C", repo_path, "remote", "-v"],
 40            capture_output=True, text=True, timeout=5,
 41        )
 42        return "github.com" in result.stdout
 43    except (subprocess.TimeoutExpired, OSError):
 44        return False
 45
 46
 47def find_repos():
 48    repos = []
 49    for root, dirs, _files in os.walk(SRC_DIR):
 50        # Skip worktree internal directories
 51        if "worktrees" in root.split(os.sep):
 52            dirs.clear()
 53            continue
 54        if ".git" in dirs or ".git" in _files:
 55            if has_github_remote(root):
 56                repos.append(root)
 57            dirs.clear()  # Don't descend into nested repos
 58    repos.sort()
 59    return [
 60        {
 61            "title": os.path.relpath(r, SRC_DIR),
 62            "subtitle": r,
 63            "arg": r,
 64        }
 65        for r in repos
 66    ]
 67
 68
 69def rebuild_cache(cache_dir, cache_file):
 70    repos = find_repos()
 71    fd, tmpfile = tempfile.mkstemp(suffix=".json", dir=cache_dir)
 72    try:
 73        with os.fdopen(fd, "w") as f:
 74            json.dump(repos, f)
 75        os.replace(tmpfile, cache_file)
 76    except BaseException:
 77        os.unlink(tmpfile)
 78        raise
 79
 80
 81def load_cache(cache_file):
 82    try:
 83        with open(cache_file) as f:
 84            data = f.read()
 85            if not data:
 86                return []
 87            return json.loads(data)
 88    except (FileNotFoundError, json.JSONDecodeError):
 89        return []
 90
 91
 92def main():
 93    cache_dir, cache_file = cache_paths()
 94
 95    if needs_rebuild(cache_file):
 96        rebuild_cache(cache_dir, cache_file)
 97
 98    items = load_cache(cache_file)
 99
100    query = " ".join(sys.argv[1:]).strip().lower()
101    if query:
102        items = [i for i in items if query in i["title"].lower()]
103
104    json.dump({"items": items}, sys.stdout)
105
106
107if __name__ == "__main__":
108    main()