diff --git a/pkgs/opencode/plugins/lazy-mcp/default.nix b/pkgs/opencode/plugins/lazy-mcp/default.nix index 4125628..00b32d8 100644 --- a/pkgs/opencode/plugins/lazy-mcp/default.nix +++ b/pkgs/opencode/plugins/lazy-mcp/default.nix @@ -7,7 +7,7 @@ }: mkOpencodePlugin rec { pname = "lazy-mcp"; - version = "1.0.3"; + version = "1.0.3-unstable-2026-03-24"; src = fetchFromGitHub { owner = "orionpax1997"; diff --git a/updater/cli.py b/updater/cli.py index 762d613..c1a2392 100644 --- a/updater/cli.py +++ b/updater/cli.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +import logging import os from .discovery import discover_packages, filter_packages @@ -9,6 +10,8 @@ from .models import UpdateResult from .package_backend import update_package from .report import print_report +logger = logging.getLogger(__name__) + def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Update NUR packages with per-package validation") @@ -19,16 +22,25 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--report", choices=["text", "json"], default="text") parser.add_argument("--fail-on-invalid", action="store_true") parser.add_argument("--timeout", default=os.environ.get("UPDATE_TIMEOUT", "10m")) + parser.add_argument("--verbose", action="store_true", help="show updater progress logs") args = parser.parse_args(argv) + logging.basicConfig( + format="%(levelname)s %(name)s: %(message)s", + level=logging.INFO if args.verbose else logging.WARNING, + ) + results: list[UpdateResult] = [] if args.backend in {"nix-update", "all"}: refs = filter_packages(discover_packages(args.system), args.package) + logger.info("updating %d nix package(s)", len(refs)) for ref in refs: results.append(update_package(ref, dry_run=args.dry_run, timeout=args.timeout)) if args.backend in {"manifest", "all"}: - for manifest in list_release_asset_manifests(): + manifests = list_release_asset_manifests() + logger.info("updating %d release asset manifest(s)", len(manifests)) + for manifest in manifests: if args.package and not any(name in str(manifest) for name in args.package): continue results.append(update_release_asset_manifest(manifest, dry_run=args.dry_run)) diff --git a/updater/discovery.py b/updater/discovery.py index df1e209..7ed512a 100644 --- a/updater/discovery.py +++ b/updater/discovery.py @@ -1,10 +1,13 @@ from __future__ import annotations +import logging from collections.abc import Iterable from .models import PackageRef, SourceKind from .nix import attr_file_path, flake_attrsets, list_derivations, list_file_attrsets +logger = logging.getLogger(__name__) + def discover_packages(system: str) -> list[PackageRef]: refs: list[PackageRef] = [] @@ -12,6 +15,7 @@ def discover_packages(system: str) -> list[PackageRef]: refs.extend(_refs_for_attrset("flake", attrset)) for attrset in list_file_attrsets(): refs.extend(_refs_for_attrset("file", attrset)) + logger.info("discovered %d package(s) for %s", len(refs), system) return refs @@ -24,9 +28,12 @@ def filter_packages(refs: Iterable[PackageRef], selected: list[str]) -> list[Pac def _refs_for_attrset(source_kind: SourceKind, attrset: str) -> list[PackageRef]: refs: list[PackageRef] = [] - for attr in list_derivations(source_kind, attrset): + attrs = list_derivations(source_kind, attrset) + logger.info("found %d derivation(s) in %s", len(attrs), attrset) + for attr in attrs: file_path = attr_file_path(source_kind, attrset, attr) if not file_path: + logger.info("skipping %s.%s: no source file", attrset, attr) continue refs.append( PackageRef( diff --git a/updater/manifest.py b/updater/manifest.py index 3bc396c..374f2b6 100644 --- a/updater/manifest.py +++ b/updater/manifest.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import logging import tempfile import urllib.request from pathlib import Path @@ -11,6 +12,7 @@ from .process import ROOT, run_json from .versions import should_block_downgrade, version_is_older DEFAULT_URL_TEMPLATE = "https://github.com/{{owner}}/{{repo}}/releases/download/{{tag}}/{{asset}}" +logger = logging.getLogger(__name__) def manifest_has_release_asset_updater(path: Path) -> bool: @@ -38,10 +40,12 @@ def list_release_asset_manifests() -> list[Path]: def latest_github_release_tag(owner: str, repo: str) -> str | None: url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" + logger.info("fetching latest GitHub release for %s/%s", owner, repo) try: with urllib.request.urlopen(url, timeout=30) as response: data = json.loads(response.read().decode()) - except OSError: + except OSError as error: + logger.info("failed to fetch latest GitHub release for %s/%s: %s", owner, repo, error) return None return data.get("tag_name") or None @@ -59,6 +63,8 @@ def latest_release_prefix_for_url(src_url: str | None) -> str | None: def update_release_asset_manifest(manifest: Path, *, dry_run: bool = False) -> UpdateResult: + name = str(manifest.relative_to(ROOT)) + logger.info("updating release asset manifest %s", name) data = _read_manifest(manifest) updater = data.get("updater", {}) owner = updater.get("owner") @@ -69,7 +75,7 @@ def update_release_asset_manifest(manifest: Path, *, dry_run: bool = False) -> U if not owner or not repo or not assets: return UpdateResult( - str(manifest.relative_to(ROOT)), + name, "skipped", "incomplete release asset manifest", ) @@ -78,19 +84,19 @@ def update_release_asset_manifest(manifest: Path, *, dry_run: bool = False) -> U latest = strip_tag_prefix(latest_tag or "", tag_prefix) if not latest: return UpdateResult( - str(manifest.relative_to(ROOT)), + name, "failed", f"failed to determine latest release for {owner}/{repo}", ) if should_block_downgrade(current, latest) and version_is_older(latest, current): return UpdateResult( - str(manifest.relative_to(ROOT)), + name, "skipped", f"apparent downgrade {current} -> {latest}", ) if current == latest: return UpdateResult( - str(manifest.relative_to(ROOT)), + name, "skipped", f"already up to date at {current}", ) @@ -98,7 +104,7 @@ def update_release_asset_manifest(manifest: Path, *, dry_run: bool = False) -> U hashes = _prefetch_asset_hashes(data, latest_tag or latest) if dry_run: return UpdateResult( - str(manifest.relative_to(ROOT)), + name, "updated", f"manifest {current} -> {latest} (dry-run)", ) @@ -108,7 +114,7 @@ def update_release_asset_manifest(manifest: Path, *, dry_run: bool = False) -> U updated["hashes"] = hashes _atomic_write_json(manifest, updated) return UpdateResult( - str(manifest.relative_to(ROOT)), + name, "updated", f"release asset manifest {current} -> {latest}", [manifest], @@ -134,6 +140,7 @@ def _prefetch_asset_hashes(data: dict[str, Any], tag: str) -> dict[str, str]: hashes: dict[str, str] = {} for system, asset in sorted(data.get("updater", {}).get("assets", {}).items()): url = render_asset_url(data, tag, asset) + logger.info("prefetching %s asset %s", system, asset) result = run_json(["nix", "store", "prefetch-file", "--json", "--hash-type", "sha256", url]) hashes[system] = result["hash"] return hashes diff --git a/updater/package_backend.py b/updater/package_backend.py index f304548..b841d1f 100644 --- a/updater/package_backend.py +++ b/updater/package_backend.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import re from pathlib import Path @@ -11,12 +12,16 @@ from .transactions import FileTransaction, paths_owned_by from .validation import validate_transition from .versions import branch_parts +logger = logging.getLogger(__name__) + def update_package( ref: PackageRef, *, dry_run: bool = False, timeout: str | None = None ) -> UpdateResult: + logger.info("updating %s from %s", ref.attr_path, ref.file_path) manifest = package_has_manifest_updater(ref.file_path) if manifest: + logger.info("skipping %s: manifest updater owns %s", ref.attr_path, manifest) return UpdateResult(ref.attr_path, "skipped", f"manifest updater owns {manifest}") owned_roots = package_owned_roots(ref.file_path) @@ -27,6 +32,7 @@ def update_package( _run_nix_update(ref, before.version_mode, timeout=timeout) except CommandError as error: transaction.restore() + logger.info("nix-update failed for %s:\n%s", ref.attr_path, error.details) return UpdateResult(ref.attr_path, "skipped", f"nix-update failed: {error}") after = read_state(ref.source_kind, ref.attrset, ref.attr) @@ -55,6 +61,9 @@ def update_package( _refresh_dependency_hash(ref, timeout=timeout) except CommandError as error: transaction.restore() + logger.info( + "dependencyHash refresh failed for %s:\n%s", ref.attr_path, error.details + ) return UpdateResult( ref.attr_path, "failed", f"dependencyHash refresh failed: {error}" ) diff --git a/updater/process.py b/updater/process.py index 9bd04dc..42a2684 100644 --- a/updater/process.py +++ b/updater/process.py @@ -1,19 +1,21 @@ from __future__ import annotations import json -import os +import logging import subprocess from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] +logger = logging.getLogger(__name__) class CommandError(RuntimeError): def __init__(self, command: list[str], result: subprocess.CompletedProcess[str]) -> None: self.command = command self.result = result - super().__init__(result.stderr.strip() or result.stdout.strip() or "command failed") + self.details = result.stderr.strip() or result.stdout.strip() or "command failed" + super().__init__(_summarize_error(self.details)) def run( @@ -23,20 +25,20 @@ def run( timeout: str | None = None, cwd: Path = ROOT, ) -> subprocess.CompletedProcess[str]: - env = os.environ.copy() actual = command if timeout and timeout != "0": actual = ["timeout", "--foreground", timeout, *command] + logger.info("running: %s", " ".join(actual)) result = subprocess.run( actual, cwd=cwd, - env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, ) + logger.info("command exited %d: %s", result.returncode, " ".join(actual)) if check and result.returncode != 0: raise CommandError(actual, result) return result @@ -44,3 +46,14 @@ def run( def run_json(command: list[str], **kwargs: Any) -> Any: return json.loads(run(command, **kwargs).stdout) + + +def _summarize_error(details: str) -> str: + lines = [line.strip() for line in details.splitlines() if line.strip()] + if not lines: + return "command failed" + + for line in reversed(lines): + if "Error:" in line: + return line + return lines[-1]