11use std:: fmt:: Write ;
2+ use std:: path:: PathBuf ;
23
34use anyhow:: Result ;
45use futures:: StreamExt ;
56use itertools:: Itertools ;
67use owo_colors:: OwoColorize ;
78use rustc_hash:: FxHashMap ;
89
10+ use serde:: Serialize ;
911use uv_cache:: { Cache , Refresh } ;
1012use uv_cache_info:: Timestamp ;
13+ use uv_cli:: ToolListFormat ;
1114use uv_client:: { BaseClientBuilder , RegistryClientBuilder } ;
1215use uv_configuration:: Concurrency ;
1316use uv_distribution_filename:: DistFilename ;
1417use uv_distribution_types:: { IndexCapabilities , RequiresPython } ;
1518use uv_fs:: Simplified ;
1619use uv_normalize:: PackageName ;
20+ use uv_pep440:: Version ;
21+ use uv_pep508:: StringVersion ;
1722use uv_python:: LenientImplementationName ;
1823use uv_settings:: { Combine , ResolverInstallerOptions } ;
19- use uv_tool:: InstalledTools ;
24+ use uv_tool:: { InstalledTools , Tool , ToolEnvironment } ;
2025use uv_warnings:: warn_user;
2126
2227use crate :: commands:: ExitStatus ;
@@ -25,6 +30,49 @@ use crate::commands::reporters::LatestVersionReporter;
2530use crate :: printer:: Printer ;
2631use crate :: settings:: ResolverInstallerSettings ;
2732
33+ #[ derive( Debug , Serialize , Default ) ]
34+ struct Schema {
35+ version : SchemaVersion ,
36+ }
37+
38+ #[ derive( Debug , Serialize , Default ) ]
39+ #[ serde( rename_all = "snake_case" ) ]
40+ enum SchemaVersion {
41+ #[ default]
42+ Preview ,
43+ }
44+
45+ #[ derive( Debug , Serialize ) ]
46+ struct CommandPrintData {
47+ name : String ,
48+ path : PathBuf ,
49+ }
50+
51+ #[ derive( Debug , Serialize ) ]
52+ struct PythonPrintData {
53+ implementation : String ,
54+ version : StringVersion ,
55+ }
56+
57+ #[ derive( Debug , Serialize ) ]
58+ struct ToolPrintData {
59+ name : String ,
60+ version : Version ,
61+ latest_version : Option < Version > ,
62+ path : PathBuf ,
63+ commands : Vec < CommandPrintData > ,
64+ extras : Vec < String > ,
65+ version_specifiers : String ,
66+ with : Vec < String > ,
67+ python : PythonPrintData ,
68+ }
69+
70+ #[ derive( Debug , Serialize ) ]
71+ struct PrintData {
72+ schema : Schema ,
73+ tools : Vec < ToolPrintData > ,
74+ }
75+
2876/// List installed tools.
2977#[ expect( clippy:: fn_params_excessive_bools) ]
3078pub ( crate ) async fn list (
@@ -34,6 +82,7 @@ pub(crate) async fn list(
3482 show_extras : bool ,
3583 show_python : bool ,
3684 outdated : bool ,
85+ output_format : ToolListFormat ,
3786 args : ResolverInstallerOptions ,
3887 filesystem : ResolverInstallerOptions ,
3988 client_builder : BaseClientBuilder < ' _ > ,
@@ -183,6 +232,121 @@ pub(crate) async fn list(
183232 FxHashMap :: default ( )
184233 } ;
185234
235+ match output_format {
236+ ToolListFormat :: Text => render_list_text (
237+ outdated,
238+ show_version_specifiers,
239+ show_extras,
240+ show_python,
241+ show_with,
242+ show_paths,
243+ latest,
244+ printer,
245+ valid_tools,
246+ installed_tools,
247+ ) ?,
248+ ToolListFormat :: Json => {
249+ render_list_json ( outdated, latest, printer, valid_tools, installed_tools) ?
250+ }
251+ }
252+
253+ Ok ( ExitStatus :: Success )
254+ }
255+
256+ fn render_list_json (
257+ outdated : bool ,
258+ latest : FxHashMap < PackageName , Option < DistFilename > > ,
259+ printer : Printer ,
260+ valid_tools : Vec < ( PackageName , Tool , ToolEnvironment , Version ) > ,
261+ installed_tools : InstalledTools ,
262+ ) -> Result < ( ) > {
263+ let tools = valid_tools
264+ . iter ( )
265+ . filter ( |( name, _, _, version) | {
266+ // skip up-to-date entries if we want outdated tools
267+ !outdated
268+ || latest
269+ . get ( & name)
270+ . and_then ( Option :: as_ref)
271+ . is_some_and ( |filename| filename. version ( ) > & version)
272+ } )
273+ . map ( |( name, tool, tool_env, version) | ToolPrintData {
274+ name : name. to_string ( ) ,
275+ version : version. clone ( ) ,
276+ latest_version : if outdated {
277+ latest
278+ . get ( & name)
279+ . and_then ( Option :: as_ref)
280+ . map ( |filename| filename. version ( ) . clone ( ) )
281+ } else {
282+ // when we don't list outdated tools, we do not know the latest version
283+ None
284+ } ,
285+ path : installed_tools. tool_dir ( & name) ,
286+ commands : tool
287+ . entrypoints ( )
288+ . iter ( )
289+ . map ( |ep| CommandPrintData {
290+ name : ep. name . clone ( ) ,
291+ path : ep. install_path . clone ( ) ,
292+ } )
293+ . collect :: < Vec < _ > > ( ) ,
294+ extras : tool
295+ . requirements ( )
296+ . iter ( )
297+ . filter ( |req| req. name == * name)
298+ . flat_map ( |req| req. extras . iter ( ) ) // Flatten the extras from all matching requirements
299+ . map ( ToString :: to_string)
300+ . collect :: < Vec < _ > > ( ) ,
301+ version_specifiers : tool
302+ . requirements ( )
303+ . iter ( )
304+ . filter ( |req| req. name == * name)
305+ . map ( |req| req. source . to_string ( ) )
306+ . filter ( |s| !s. is_empty ( ) )
307+ // XXX I don't think we'll ever have more than 1 element here, since multiple
308+ // specifiers like in 'twine[keyring]>=6.2.0,!=69' are all in the same string
309+ . join ( ", " ) ,
310+ with : tool
311+ . requirements ( )
312+ . iter ( )
313+ . filter ( |req| req. name != * name)
314+ . map ( |req| format ! ( "{}{}" , req. name, req. source) )
315+ . collect :: < Vec < _ > > ( ) ,
316+ python : {
317+ let interpreter = tool_env. environment ( ) . interpreter ( ) ;
318+ let implementation =
319+ LenientImplementationName :: from ( interpreter. implementation_name ( ) ) ;
320+ PythonPrintData {
321+ implementation : implementation. to_string ( ) ,
322+ version : interpreter. python_full_version ( ) . clone ( ) ,
323+ }
324+ } ,
325+ } )
326+ . collect :: < Vec < _ > > ( ) ;
327+
328+ let data = PrintData {
329+ schema : Schema {
330+ version : SchemaVersion :: Preview ,
331+ } ,
332+ tools,
333+ } ;
334+ writeln ! ( printer. stdout( ) , "{}" , serde_json:: to_string( & data) ?) ?;
335+ Ok ( ( ) )
336+ }
337+
338+ fn render_list_text (
339+ outdated : bool ,
340+ show_version_specifiers : bool ,
341+ show_extras : bool ,
342+ show_python : bool ,
343+ show_with : bool ,
344+ show_paths : bool ,
345+ latest : FxHashMap < PackageName , Option < DistFilename > > ,
346+ printer : Printer ,
347+ valid_tools : Vec < ( PackageName , Tool , ToolEnvironment , Version ) > ,
348+ installed_tools : InstalledTools ,
349+ ) -> Result < ( ) > {
186350 for ( name, tool, tool_env, version) in valid_tools {
187351 // If `--outdated` is set, skip tools that are up-to-date.
188352 if outdated {
@@ -295,5 +459,5 @@ pub(crate) async fn list(
295459 }
296460 }
297461
298- Ok ( ExitStatus :: Success )
462+ Ok ( ( ) )
299463}
0 commit comments