Created
May 26, 2026 14:09
-
-
Save isuke01/9db3e65de7038cb90865bf346d8bf371 to your computer and use it in GitHub Desktop.
Compare installed vs latest Composer package versions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| composer-versions — Compare installed vs latest Composer package versions. | |
| Usage: | |
| composer-versions # defaults: dekode + wpackagist-plugin | |
| composer-versions dekode wpackagist-plugin | |
| composer-versions t2 | |
| """ | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| ICONS = { | |
| "up-to-date": "✅", | |
| "semver-safe-update": "⬆ ", | |
| "update-possible": "⚠ ", | |
| } | |
| COLORS = { | |
| "up-to-date": "\033[32m", # green | |
| "semver-safe-update": "\033[33m", # yellow | |
| "update-possible": "\033[31m", # red | |
| "reset": "\033[0m", | |
| "bold": "\033[1m", | |
| "dim": "\033[2m", | |
| } | |
| def colorize(text, *keys): | |
| if not sys.stdout.isatty(): | |
| return text | |
| codes = "".join(COLORS.get(k, "") for k in keys) | |
| return f"{codes}{text}{COLORS['reset']}" | |
| def main(): | |
| prefixes = sys.argv[1:] if len(sys.argv) > 1 else [ | |
| "t2", "dekode", "dekode-library", "wpackagist-plugin", "johnpbloch", "roots", | |
| ] | |
| if not os.path.exists("composer.json"): | |
| print("Error: No composer.json found in current directory.", file=sys.stderr) | |
| sys.exit(1) | |
| print(colorize("Fetching package versions…", "dim")) | |
| try: | |
| result = subprocess.run( | |
| ["composer", "show", "--latest", "--format=json"], | |
| capture_output=True, | |
| text=True, | |
| ) | |
| data = json.loads(result.stdout) | |
| except FileNotFoundError: | |
| print("Error: 'composer' not found in PATH.", file=sys.stderr) | |
| sys.exit(1) | |
| except json.JSONDecodeError as e: | |
| print(f"Error parsing composer output: {e}", file=sys.stderr) | |
| sys.exit(1) | |
| packages = data.get("installed", []) | |
| rows = [] | |
| for p in packages: | |
| name = p.get("name", "") | |
| if any(name.startswith(pf + "/") for pf in prefixes): | |
| version = p.get("version", "n/a") | |
| latest = p.get("latest", "n/a") | |
| status = p.get("latest-status", "") | |
| icon = ICONS.get(status, "? ") | |
| rows.append((name, version, latest, icon, status)) | |
| if not rows: | |
| print("No matching packages found.") | |
| return | |
| col1 = max(len(r[0]) for r in rows) | |
| col2 = max(len(r[1]) for r in rows) | |
| col3 = max(len(r[2]) for r in rows) | |
| sep = "─" * (col1 + col2 + col3 + 24) | |
| header = ( | |
| f" {colorize(f'{'Package':<{col1}}', 'bold')}" | |
| f" {colorize(f'{'Installed':<{col2}}', 'bold')}" | |
| f" {colorize(f'{'Latest':<{col3}}', 'bold')}" | |
| f" {colorize('Status', 'bold')}" | |
| ) | |
| print() | |
| print(colorize(sep, "dim")) | |
| print(header) | |
| print(colorize(sep, "dim")) | |
| current_group = "" | |
| for name, version, latest, icon, status in rows: | |
| group = name.split("/")[0] | |
| if group != current_group: | |
| if current_group: | |
| print() | |
| print(colorize(f" [{group}]", "bold")) | |
| current_group = group | |
| line = f" {name:<{col1}} {version:<{col2}} {latest:<{col3}} {icon} {status}" | |
| print(colorize(line, status)) | |
| print() | |
| print(colorize(sep, "dim")) | |
| total = len(rows) | |
| outdated = sum(1 for r in rows if r[4] != "up-to-date") | |
| up_to_date = total - outdated | |
| print( | |
| f"\n {colorize(str(total), 'bold')} packages — " | |
| f"{colorize(str(up_to_date) + ' up-to-date', 'up-to-date')} " | |
| f"{colorize(str(outdated) + ' with updates', 'update-possible' if outdated else 'up-to-date')}\n" | |
| ) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment