#!/usr/bin/env python3 """Pin the latest stable upstream CLI releases without updating NixOS.""" import json import re import urllib.request from pathlib import Path def fetch_json(url): request = urllib.request.Request( url, headers={"User-Agent": "nixos-config-ai-cli-updater"} ) with urllib.request.urlopen(request, timeout=60) as response: return json.load(response) def latest_release(repository): release = fetch_json(f"https://api.github.com/repos/{repository}/releases/latest") if release["prerelease"] or release["draft"]: raise ValueError(f"Expected a stable release from {repository}") return release codex = latest_release("openai/codex") codex_version = codex["tag_name"].removeprefix("rust-v") claude = latest_release("anthropics/claude-code") claude_version = claude["tag_name"].removeprefix("v") for version in (codex_version, claude_version): if not re.fullmatch(r"\d+\.\d+\.\d+", version): raise ValueError(f"Unexpected release version: {version}") asset = next( item for item in codex["assets"] if item["name"] == "codex-package-x86_64-unknown-linux-musl.tar.gz" ) digest = asset["digest"] if not re.fullmatch(r"sha256:[0-9a-f]{64}", digest): raise ValueError("Codex release asset has no valid SHA-256 digest") manifest = fetch_json( f"https://downloads.claude.ai/claude-code-releases/{claude_version}/manifest.json" ) if manifest["version"] != claude_version: raise ValueError("Claude release and download manifest versions differ") claude_hash = manifest["platforms"]["linux-x64"]["checksum"] if not re.fullmatch(r"[0-9a-f]{64}", claude_hash): raise ValueError("Claude manifest has no valid SHA-256 checksum") versions = { "codex": {"version": codex_version, "sha256": digest.removeprefix("sha256:")}, "claude-code": {"version": claude_version, "sha256": claude_hash}, } destination = Path(__file__).resolve().parent.parent / "packages" / "ai-cli-versions.json" destination.write_text(json.dumps(versions, indent=2) + "\n") print(f"Pinned Codex {codex_version} and Claude Code {claude_version}.") print("Review the diff, build the packages, rebuild your host and commit the pins.")