2525
2626# Import modules
2727from cli_audit .tools import Tool , all_tools , filter_tools , tool_homepage_url , latest_target_url # noqa: E402
28- from cli_audit .detection import audit_tool_installation # noqa: E402
28+ from cli_audit .detection import audit_tool_installation , detect_multi_versions # noqa: E402
2929from cli_audit .snapshot import load_snapshot , write_snapshot , render_from_snapshot , get_snapshot_path # noqa: E402
3030from cli_audit .render import render_table , print_summary , status_icon # noqa: E402
31- from cli_audit .collectors import get_github_rate_limit , get_github_rate_limit_help , get_gitlab_rate_limit , is_wsl # noqa: E402
31+ from cli_audit .collectors import get_github_rate_limit , get_github_rate_limit_help , get_gitlab_rate_limit , is_wsl , collect_endoflife # noqa: E402
3232from cli_audit import collectors # noqa: E402
3333from cli_audit .logging_config import setup_logging # noqa: E402
3434# Split file support (Phase 2.1)
@@ -806,6 +806,113 @@ def cmd_upgrade(args: argparse.Namespace) -> int:
806806 return 1
807807
808808
809+ def cmd_versions (args : argparse .Namespace ) -> int :
810+ """Show multi-version runtime status.
811+
812+ Displays all runtimes that support multiple concurrent versions (PHP, Python,
813+ Node.js, Ruby, Go) and shows which versions are installed vs available.
814+ """
815+ from cli_audit .catalog import ToolCatalog
816+
817+ # ANSI colors
818+ GREEN = "\033 [32m"
819+ YELLOW = "\033 [33m"
820+ RED = "\033 [31m"
821+ BLUE = "\033 [34m"
822+ BOLD = "\033 [1m"
823+ RESET = "\033 [0m"
824+
825+ print ("=" * 80 , file = sys .stderr )
826+ print ("Runtime Versions" , file = sys .stderr )
827+ print ("=" * 80 , file = sys .stderr )
828+ print ("" , file = sys .stderr )
829+
830+ catalog = ToolCatalog ()
831+
832+ # Find all tools with multi_version enabled
833+ multi_version_tools = []
834+ for tool_name in catalog .all_tools ():
835+ data = catalog .get_raw_data (tool_name )
836+ mv_config = data .get ("multi_version" , {})
837+ if mv_config .get ("enabled" ):
838+ multi_version_tools .append ((tool_name , data , mv_config ))
839+
840+ if not multi_version_tools :
841+ print ("No multi-version runtimes configured." , file = sys .stderr )
842+ return 0
843+
844+ # Filter to specific tools if requested
845+ if args .tools :
846+ requested = set (args .tools )
847+ multi_version_tools = [
848+ (name , data , config ) for name , data , config in multi_version_tools
849+ if name in requested
850+ ]
851+
852+ # Process each runtime
853+ for tool_name , data , mv_config in multi_version_tools :
854+ product = mv_config .get ("product" , tool_name )
855+ max_versions = mv_config .get ("max_versions" , 4 )
856+ display_name = data .get ("description" , tool_name .upper ())
857+
858+ print (f"{ BOLD } 🔧 { display_name } { RESET } " , file = sys .stderr )
859+ print ("-" * 40 , file = sys .stderr )
860+
861+ # Fetch supported versions from endoflife.date
862+ try :
863+ supported = collect_endoflife (product , max_versions = max_versions )
864+ except Exception as e :
865+ print (f" { RED } ✗ Failed to fetch version info: { e } { RESET } " , file = sys .stderr )
866+ print ("" , file = sys .stderr )
867+ continue
868+
869+ if not supported :
870+ print (f" { YELLOW } ⚠ No supported versions found{ RESET } " , file = sys .stderr )
871+ print ("" , file = sys .stderr )
872+ continue
873+
874+ # Detect installed versions
875+ detected = detect_multi_versions (tool_name , mv_config , supported )
876+
877+ # Display results
878+ for version_info in detected :
879+ cycle = version_info .get ("cycle" , "?" )
880+ latest = version_info .get ("latest_upstream" , "" )
881+ installed = version_info .get ("installed" )
882+ status = version_info .get ("status" , "unknown" )
883+ path = version_info .get ("path" , "" )
884+ method = version_info .get ("install_method" , "" )
885+
886+ # Status indicator
887+ if status == "active" :
888+ status_str = f"{ GREEN } active{ RESET } "
889+ elif status == "security" :
890+ status_str = f"{ YELLOW } security{ RESET } "
891+ else :
892+ status_str = f"{ RED } eol{ RESET } "
893+
894+ # Installed indicator
895+ if installed :
896+ if installed == latest :
897+ inst_str = f"{ GREEN } ✓ { installed } { RESET } "
898+ else :
899+ inst_str = f"{ YELLOW } ⚠ { installed } { RESET } → { GREEN } { latest } { RESET } "
900+ method_str = f" ({ method } )" if method else ""
901+ else :
902+ inst_str = f"{ BLUE } ○ not installed{ RESET } "
903+ method_str = ""
904+
905+ print (f" { cycle :6} [{ status_str :16} ] { inst_str } { method_str } " , file = sys .stderr )
906+
907+ print ("" , file = sys .stderr )
908+
909+ # Summary
910+ print ("=" * 80 , file = sys .stderr )
911+ print (f"Total: { len (multi_version_tools )} runtimes configured" , file = sys .stderr )
912+
913+ return 0
914+
915+
809916def main () -> int :
810917 """Main entry point for audit system."""
811918 parser = argparse .ArgumentParser (
@@ -838,6 +945,11 @@ def main() -> int:
838945 action = "store_true" ,
839946 help = "Upgrade outdated tools" ,
840947 )
948+ parser .add_argument (
949+ "--versions" ,
950+ action = "store_true" ,
951+ help = "Show multi-version runtime status (PHP, Python, Node.js, Ruby, Go)" ,
952+ )
841953 parser .add_argument (
842954 "--verbose" , "-v" ,
843955 action = "store_true" ,
@@ -855,7 +967,9 @@ def main() -> int:
855967 setup_logging (verbose = args .verbose )
856968
857969 # Route to appropriate command
858- if args .update :
970+ if args .versions :
971+ return cmd_versions (args )
972+ elif args .update :
859973 # Explicit --update flag: full update of all tools
860974 return cmd_update (args )
861975 elif getattr (args , 'update_local' , False ) or UPDATE_LOCAL_ONLY :
0 commit comments