Ctrl K
ComunidadGuías…

scripts/check_upstream.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Check upstream version drift for all tracked dependencies.

Compares local clones (external test-deps and absorbed repo snapshots)
against their upstream remotes. Outputs a markdown table showing what's
behind.

Usage:
    python scripts/check_upstream.py
    python scripts/check_upstream.py --format json
"""

from __future__ import annotations

import argparse
import json
import subprocess
import sys
from dataclasses import asdict, dataclass
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent


@dataclass
class RepoStatus:
    name: str
    category: str  # "external-test-dep" | "reference" | "npm" | "pip"
    local_version: str = ""
    remote_version: str = ""
    commits_behind: int = 0
    url: str = ""
    notes: str = ""


def _run(cmd: list[str], cwd: Path | None = None) -> str:
    """Run a command and return stdout, empty string on failure."""
    try:
        result = subprocess.run(
            cmd, capture_output=True, text=True, timeout=30, cwd=cwd
        )
        return result.stdout.strip()
    except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
        return ""


def check_npm_deps() -> list[RepoStatus]:
    """Check key npm dependencies for version drift."""
    results: list[RepoStatus] = []

    pkg_json_path = ROOT / "packages" / "terminal" / "package.json"
    if not pkg_json_path.exists():
        return results

    with open(pkg_json_path) as f:
        pkg = json.load(f)

    deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}

    # Key deps to track
    key_deps = [
        "react", "dockview-react", "lightweight-charts",
        "@glideapps/glide-data-grid", "@tanstack/react-query",
        "zustand", "jotai", "tailwindcss", "vite", "react-resizable-panels",
    ]

    for dep_name in key_deps:
        local_ver = deps.get(dep_name, "not installed")
        # Strip ^ or ~ prefix
        local_clean = local_ver.lstrip("^~")

        # Check latest from npm registry
        latest = _run(["npm", "view", dep_name, "version"])

        behind = 0
        if latest and local_clean and latest != local_clean:
            behind = 1  # simplified — just flag as "update available"

        results.append(RepoStatus(
            name=dep_name,
            category="npm",
            local_version=local_clean,
            remote_version=latest or "unknown",
            commits_behind=behind,
            url=f"https://www.npmjs.com/package/{dep_name}",
            notes="UP TO DATE" if behind == 0 else f"Update available: {latest}",
        ))

    return results


def check_pip_deps() -> list[RepoStatus]:
    """Check key pip dependencies for version drift."""
    results: list[RepoStatus] = []

    key_deps = [
        "flask", "httpx", "pydantic", "duckdb",
        "lightgbm", "ruff", "pytest",
    ]

    for dep_name in key_deps:
        # Get installed version
        local = _run([sys.executable, "-m", "pip", "show", dep_name])
        local_ver = ""
        for line in local.splitlines():
            if line.startswith("Version:"):
                local_ver = line.split(":", 1)[1].strip()
                break

        # Get latest from PyPI
        latest = _run(
            [sys.executable, "-m", "pip", "index", "versions", dep_name]
        )
        latest_ver = ""
        if latest:
            # Format: "dep_name (X.Y.Z)"
            parts = latest.split("(")
            if len(parts) > 1:
                latest_ver = parts[1].split(")")[0].strip()

        behind = 0
        if latest_ver and local_ver and latest_ver != local_ver:
            behind = 1

        results.append(RepoStatus(
            name=dep_name,
            category="pip",
            local_version=local_ver or "not installed",
            remote_version=latest_ver or "unknown",
            commits_behind=behind,
            notes="UP TO DATE" if behind == 0 else f"Update available: {latest_ver}",
        ))

    return results


def format_markdown(results: list[RepoStatus]) -> str:
    """Format results as markdown table."""
    lines = [
        "# FlintTrade — Upstream Version Drift Report",
        "",
        f"> Generated: {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M')}",
        "",
    ]

    # Group by category
    categories = {}
    for r in results:
        categories.setdefault(r.category, []).append(r)

    for cat, items in categories.items():
        behind_count = sum(1 for i in items if i.commits_behind > 0)
        lines.append(f"## {cat.upper()} ({behind_count}/{len(items)} need updates)")
        lines.append("")
        lines.append("| Name | Local | Latest | Status |")
        lines.append("|------|-------|--------|--------|")

        for item in sorted(items, key=lambda x: -x.commits_behind):
            status = item.notes
            if item.commits_behind > 0:
                status = f"**{status}**"
            lines.append(f"| {item.name} | {item.local_version} | {item.remote_version} | {status} |")

        lines.append("")

    return "\n".join(lines)


def main() -> None:
    parser = argparse.ArgumentParser(description="Check upstream version drift")
    parser.add_argument("--format", choices=["markdown", "json"], default="markdown")
    parser.add_argument("--skip-pip", action="store_true", help="Skip pip checks (slow)")
    parser.add_argument("--skip-npm", action="store_true", help="Skip npm checks (slow)")
    args = parser.parse_args()

    results: list[RepoStatus] = []

    if not args.skip_npm:
        print("Checking npm dependencies...", file=sys.stderr)
        results.extend(check_npm_deps())

    if not args.skip_pip:
        print("Checking pip dependencies...", file=sys.stderr)
        results.extend(check_pip_deps())

    if args.format == "json":
        print(json.dumps([asdict(r) for r in results], indent=2))
    else:
        print(format_markdown(results))


if __name__ == "__main__":
    main()