Skip to content

Instantly share code, notes, and snippets.

@isuke01
Created May 26, 2026 14:09
Show Gist options
  • Select an option

  • Save isuke01/9db3e65de7038cb90865bf346d8bf371 to your computer and use it in GitHub Desktop.

Select an option

Save isuke01/9db3e65de7038cb90865bf346d8bf371 to your computer and use it in GitHub Desktop.
Compare installed vs latest Composer package versions.
#!/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