Skip to main content

#!/usr/bin/env python3 """ Check for CODITECT updates - alerts customers when updates are available.

Called by /orient on session start to notify users of available updates.

Usage: python3 scripts/check-updates.py # Check and print status python3 scripts/check-updates.py --quiet # Exit code only (0=up-to-date, 1=updates) python3 scripts/check-updates.py --json # JSON output

Part of ADR-113: Post-Push Protected Installation Sync Created: 2026-01-25 """

import argparse import json import subprocess import sys from datetime import datetime from pathlib import Path

ADR-114: Platform-specific framework installation paths

User data is separate at ~/PROJECTS/.coditect-data (ADR-118)

HOME = Path.home() if sys.platform == "darwin": PROTECTED = HOME / "Library" / "Application Support" / "CODITECT" / "core" else: PROTECTED = Path.home() / ".coditect"

Fallback to ~/.coditect if that's where it is

if not PROTECTED.exists() and (HOME / ".coditect").exists(): PROTECTED = HOME / ".coditect"

def run_git(args: list, cwd: Path) -> tuple: """Run git command and return (success, output).""" try: result = subprocess.run( ["git"] + args, cwd=cwd, capture_output=True, text=True, timeout=30 ) return result.returncode == 0, result.stdout.strip() except Exception as e: return False, str(e)

def check_updates(quiet: bool = False) -> dict: """Check for available updates.""" result = { "protected_path": str(PROTECTED), "is_git_repo": False, "current_commit": None, "latest_commit": None, "behind_count": 0, "updates_available": False, "commits": [], "last_check": datetime.now().isoformat(), "error": None }

# Check if protected is a git repo
if not (PROTECTED / ".git").exists():
result["error"] = "Protected installation is not a git repo"
return result

result["is_git_repo"] = True

# Get current commit
ok, output = run_git(["rev-parse", "--short", "HEAD"], PROTECTED)
if ok:
result["current_commit"] = output

# Fetch (silently)
run_git(["fetch", "origin"], PROTECTED)

# Get latest commit on origin/main
ok, output = run_git(["rev-parse", "--short", "origin/main"], PROTECTED)
if ok:
result["latest_commit"] = output

# Count commits behind
ok, output = run_git(["rev-list", "HEAD..origin/main", "--count"], PROTECTED)
if ok:
result["behind_count"] = int(output) if output.isdigit() else 0
result["updates_available"] = result["behind_count"] > 0

# Get commit messages if updates available
if result["updates_available"]:
ok, output = run_git(
["log", "HEAD..origin/main", "--oneline", "-10"],
PROTECTED
)
if ok and output:
result["commits"] = output.split("\n")

return result

def main(): parser = argparse.ArgumentParser(description="Check for CODITECT updates") parser.add_argument("--quiet", "-q", action="store_true", help="Quiet mode - exit code only") parser.add_argument("--json", action="store_true", help="Output as JSON") args = parser.parse_args()

result = check_updates(args.quiet)

if args.json:
print(json.dumps(result, indent=2))
sys.exit(0 if not result["updates_available"] else 1)

if args.quiet:
sys.exit(0 if not result["updates_available"] else 1)

# Pretty output
if result["error"]:
print(f"⚠ {result['error']}")
print(" Run: python3 scripts/git-push-sync.py --pull-only")
sys.exit(2)

if result["updates_available"]:
print(f"⬆ CODITECT updates available: {result['behind_count']} commit(s)")
print(f" Current: {result['current_commit']}")
print(f" Latest: {result['latest_commit']}")
print()
print(" Recent changes:")
for commit in result["commits"][:5]:
print(f" {commit}")
if len(result["commits"]) > 5:
print(f" ... and {len(result['commits']) - 5} more")
print()
print(" To update: /git-sync --pull-only")
sys.exit(1)
else:
print(f"✓ CODITECT is up to date ({result['current_commit']})")
sys.exit(0)

if name == "main": main()