@@ -811,22 +811,11 @@ def cmd_versions(args: argparse.Namespace) -> int:
811811
812812 Displays all runtimes that support multiple concurrent versions (PHP, Python,
813813 Node.js, Ruby, Go) and shows which versions are installed vs available.
814+
815+ Supports JSON output via CLI_AUDIT_JSON=1 environment variable.
814816 """
815817 from cli_audit .catalog import ToolCatalog
816818
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-
830819 catalog = ToolCatalog ()
831820
832821 # Find all tools with multi_version enabled
@@ -838,7 +827,10 @@ def cmd_versions(args: argparse.Namespace) -> int:
838827 multi_version_tools .append ((tool_name , data , mv_config ))
839828
840829 if not multi_version_tools :
841- print ("No multi-version runtimes configured." , file = sys .stderr )
830+ if JSON_MODE :
831+ print (json .dumps ({"runtimes" : [], "total" : 0 }))
832+ else :
833+ print ("No multi-version runtimes configured." , file = sys .stderr )
842834 return 0
843835
844836 # Filter to specific tools if requested
@@ -849,39 +841,91 @@ def cmd_versions(args: argparse.Namespace) -> int:
849841 if name in requested
850842 ]
851843
852- # Process each runtime
844+ # Collect all runtime data
845+ runtimes_data = []
853846 for tool_name , data , mv_config in multi_version_tools :
854847 product = mv_config .get ("product" , tool_name )
855848 max_versions = mv_config .get ("max_versions" , 4 )
856849 display_name = data .get ("description" , tool_name .upper ())
857850
858- print (f"{ BOLD } 🔧 { display_name } { RESET } " , file = sys .stderr )
859- print ("-" * 40 , file = sys .stderr )
851+ runtime_info = {
852+ "name" : tool_name ,
853+ "display_name" : display_name ,
854+ "product" : product ,
855+ "versions" : [],
856+ "error" : None ,
857+ }
860858
861859 # Fetch supported versions from endoflife.date
862860 try :
863861 supported = collect_endoflife (product , max_versions = max_versions )
864862 except Exception as e :
865- print ( f" { RED } ✗ Failed to fetch version info: { e } { RESET } " , file = sys . stderr )
866- print ( "" , file = sys . stderr )
863+ runtime_info [ "error" ] = str ( e )
864+ runtimes_data . append ( runtime_info )
867865 continue
868866
869867 if not supported :
870- print ( f" { YELLOW } ⚠ No supported versions found{ RESET } " , file = sys . stderr )
871- print ( "" , file = sys . stderr )
868+ runtime_info [ "error" ] = " No supported versions found"
869+ runtimes_data . append ( runtime_info )
872870 continue
873871
874872 # Detect installed versions
875873 detected = detect_multi_versions (tool_name , mv_config , supported )
876874
877- # Display results
878875 for version_info in detected :
879- cycle = version_info .get ("cycle" , "?" )
880- latest = version_info .get ("latest_upstream" , "" )
881876 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" , "" )
877+ latest = version_info .get ("latest_upstream" , "" )
878+ runtime_info ["versions" ].append ({
879+ "cycle" : version_info .get ("cycle" , "" ),
880+ "latest_upstream" : latest ,
881+ "installed" : installed ,
882+ "is_installed" : bool (installed ),
883+ "is_up_to_date" : installed == latest if installed else False ,
884+ "needs_upgrade" : bool (installed and installed != latest ),
885+ "path" : version_info .get ("path" , "" ),
886+ "install_method" : version_info .get ("install_method" , "" ),
887+ "status" : version_info .get ("status" , "unknown" ),
888+ })
889+
890+ runtimes_data .append (runtime_info )
891+
892+ # JSON output mode
893+ if JSON_MODE :
894+ output = {
895+ "runtimes" : runtimes_data ,
896+ "total" : len (runtimes_data ),
897+ }
898+ print (json .dumps (output , indent = 2 , ensure_ascii = False ))
899+ return 0
900+
901+ # Table output mode
902+ GREEN = "\033 [32m"
903+ YELLOW = "\033 [33m"
904+ RED = "\033 [31m"
905+ BLUE = "\033 [34m"
906+ BOLD = "\033 [1m"
907+ RESET = "\033 [0m"
908+
909+ print ("=" * 80 , file = sys .stderr )
910+ print ("Runtime Versions" , file = sys .stderr )
911+ print ("=" * 80 , file = sys .stderr )
912+ print ("" , file = sys .stderr )
913+
914+ for runtime in runtimes_data :
915+ print (f"{ BOLD } 🔧 { runtime ['display_name' ]} { RESET } " , file = sys .stderr )
916+ print ("-" * 40 , file = sys .stderr )
917+
918+ if runtime ["error" ]:
919+ print (f" { RED } ✗ { runtime ['error' ]} { RESET } " , file = sys .stderr )
920+ print ("" , file = sys .stderr )
921+ continue
922+
923+ for v in runtime ["versions" ]:
924+ cycle = v ["cycle" ]
925+ latest = v ["latest_upstream" ]
926+ installed = v ["installed" ]
927+ status = v ["status" ]
928+ method = v ["install_method" ]
885929
886930 # Status indicator
887931 if status == "active" :
@@ -908,7 +952,7 @@ def cmd_versions(args: argparse.Namespace) -> int:
908952
909953 # Summary
910954 print ("=" * 80 , file = sys .stderr )
911- print (f"Total: { len (multi_version_tools )} runtimes configured" , file = sys .stderr )
955+ print (f"Total: { len (runtimes_data )} runtimes configured" , file = sys .stderr )
912956
913957 return 0
914958
0 commit comments