@@ -96,6 +96,32 @@ pub struct ExecuteArgs {
9696 injected_state : Option < String > ,
9797}
9898
99+ #[ derive( Parser , Debug ) ]
100+ pub struct ListModulesArgs {
101+ /// API key for authentication (required unless --all)
102+ #[ arg( short = 'k' , long = "api-key" ) ]
103+ api_key : Option < String > ,
104+ /// HDP server URL (defaults to HDP_SERVER_URL env var or http://localhost:3001)
105+ #[ arg( short = 'u' , long = "url" ) ]
106+ server_url : Option < String > ,
107+ /// List all modules (not only current user's modules)
108+ #[ arg( long = "all" ) ]
109+ all : bool ,
110+ }
111+
112+ #[ derive( Parser , Debug ) ]
113+ pub struct ModuleVersionsArgs {
114+ /// Module id
115+ #[ arg( short = 'm' , long = "module-id" ) ]
116+ module_id : String ,
117+ /// API key for authentication (optional; forwarded as X-API-KEY when provided)
118+ #[ arg( short = 'k' , long = "api-key" ) ]
119+ api_key : Option < String > ,
120+ /// HDP server URL (defaults to HDP_SERVER_URL env var or http://localhost:3001)
121+ #[ arg( short = 'u' , long = "url" ) ]
122+ server_url : Option < String > ,
123+ }
124+
99125#[ derive( Subcommand , Debug ) ]
100126enum Commands {
101127 /// Run the dry-run functionality
@@ -127,6 +153,16 @@ enum Commands {
127153 /// Print the path to the HDP repository directory
128154 #[ command( name = "pwd" ) ]
129155 Pwd ,
156+ /// Cloud-related commands (upload, execute, list modules, module versions)
157+ #[ command( name = "cloud" ) ]
158+ Cloud {
159+ #[ command( subcommand) ]
160+ command : CloudCommands ,
161+ } ,
162+ }
163+
164+ #[ derive( Subcommand , Debug ) ]
165+ enum CloudCommands {
130166 /// Upload a module to the HDP server
131167 ///
132168 /// Builds the module, collects source files, and uploads everything to the HDP server.
@@ -138,6 +174,14 @@ enum Commands {
138174 /// Builds the module and submits an HDP task directly with input.compiled_class in the JSON payload.
139175 #[ command( name = "execute" ) ]
140176 Execute ( ExecuteArgs ) ,
177+ /// List modules in a clean table format
178+ ///
179+ /// By default lists current user's modules. Use --all to list all modules.
180+ #[ command( name = "list-modules" ) ]
181+ ListModules ( ListModulesArgs ) ,
182+ /// List all versions of a given module
183+ #[ command( name = "module-versions" ) ]
184+ ModuleVersions ( ModuleVersionsArgs ) ,
141185}
142186
143187#[ tokio:: main]
@@ -279,11 +323,19 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
279323 let hdp_path = get_hdp_path ( ) ?;
280324 println ! ( "{}" , hdp_path. display( ) ) ;
281325 }
282- Commands :: Upload ( upload_args) => {
283- upload_module ( upload_args) . await ?;
284- }
285- Commands :: Execute ( execute_args) => {
286- execute_task ( execute_args) . await ?;
326+ Commands :: Cloud { command } => match command {
327+ CloudCommands :: Upload ( upload_args) => {
328+ upload_module ( upload_args) . await ?;
329+ }
330+ CloudCommands :: Execute ( execute_args) => {
331+ execute_task ( execute_args) . await ?;
332+ }
333+ CloudCommands :: ListModules ( args) => {
334+ list_modules ( args) . await ?;
335+ }
336+ CloudCommands :: ModuleVersions ( args) => {
337+ list_module_versions ( args) . await ?;
338+ }
287339 }
288340 }
289341
@@ -569,7 +621,6 @@ async fn upload_module(args: UploadArgs) -> Result<(), Box<dyn std::error::Error
569621 "https://herodotus.cloud/en/hdp/module/{}?program_hash={}" ,
570622 module_id, program_hash
571623 ) ;
572- info ! ( " Herodotus Cloud: {}" , module_link) ;
573624 println ! ( "🔗 Module page: {}" , module_link) ;
574625 }
575626
@@ -710,7 +761,6 @@ async fn execute_task(args: ExecuteArgs) -> Result<(), Box<dyn std::error::Error
710761 info ! ( " Task UUID: {}" , task_uuid) ;
711762 if task_uuid != "N/A" {
712763 let task_link = format ! ( "https://herodotus.cloud/en/hdp/task/{}" , task_uuid) ;
713- info ! ( " Herodotus Cloud: {}" , task_link) ;
714764 println ! ( "🔗 Task page: {}" , task_link) ;
715765 }
716766 println ! ( ) ;
@@ -721,6 +771,161 @@ async fn execute_task(args: ExecuteArgs) -> Result<(), Box<dyn std::error::Error
721771 Ok ( ( ) )
722772}
723773
774+ fn resolve_server_url ( server_url : Option < String > ) -> String {
775+ server_url
776+ . or_else ( || std:: env:: var ( "HDP_SERVER_URL" ) . ok ( ) )
777+ . unwrap_or_else ( || "http://localhost:3001" . to_string ( ) )
778+ }
779+
780+ fn print_table ( headers : & [ & str ] , rows : & [ Vec < String > ] ) {
781+ let mut widths: Vec < usize > = headers. iter ( ) . map ( |h| h. len ( ) ) . collect ( ) ;
782+ for row in rows {
783+ for ( idx, cell) in row. iter ( ) . enumerate ( ) {
784+ if idx < widths. len ( ) {
785+ widths[ idx] = widths[ idx] . max ( cell. len ( ) ) ;
786+ }
787+ }
788+ }
789+
790+ let border = widths
791+ . iter ( )
792+ . map ( |w| "-" . repeat ( * w + 2 ) )
793+ . collect :: < Vec < _ > > ( )
794+ . join ( "+" ) ;
795+ println ! ( "+{}+" , border) ;
796+
797+ let header_line = headers
798+ . iter ( )
799+ . enumerate ( )
800+ . map ( |( idx, h) | format ! ( " {:<width$} " , h, width = widths[ idx] ) )
801+ . collect :: < Vec < _ > > ( )
802+ . join ( "|" ) ;
803+ println ! ( "|{}|" , header_line) ;
804+ println ! ( "+{}+" , border) ;
805+
806+ for row in rows {
807+ let line = row
808+ . iter ( )
809+ . enumerate ( )
810+ . map ( |( idx, c) | format ! ( " {:<width$} " , c, width = widths[ idx] ) )
811+ . collect :: < Vec < _ > > ( )
812+ . join ( "|" ) ;
813+ println ! ( "|{}|" , line) ;
814+ }
815+ println ! ( "+{}+" , border) ;
816+ }
817+
818+ async fn list_modules ( args : ListModulesArgs ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
819+ let server_url = resolve_server_url ( args. server_url ) ;
820+ let client = reqwest:: Client :: new ( ) ;
821+
822+ info ! ( "📦 Fetching modules from {}..." , server_url) ;
823+ let resp = if args. all {
824+ client. get ( format ! ( "{}/modules" , server_url) ) . send ( ) . await ?
825+ } else {
826+ let api_key = args
827+ . api_key
828+ . or_else ( || std:: env:: var ( "HERODOTUS_CLOUD_API_KEY" ) . ok ( ) )
829+ . ok_or ( "API key required. Provide via --api-key or HERODOTUS_CLOUD_API_KEY (or use --all)" ) ?;
830+ client
831+ . get ( format ! ( "{}/modules/my" , server_url) )
832+ . header ( "X-API-KEY" , api_key)
833+ . send ( )
834+ . await ?
835+ } ;
836+ if !resp. status ( ) . is_success ( ) {
837+ let status = resp. status ( ) ;
838+ let err = resp. text ( ) . await ?;
839+ return Err ( format ! ( "Failed to list modules ({}): {}" , status, err) . into ( ) ) ;
840+ }
841+
842+ let body: serde_json:: Value = resp. json ( ) . await ?;
843+ let modules = body
844+ . get ( "modules" )
845+ . and_then ( |v| v. as_array ( ) )
846+ . ok_or ( "Invalid response format: missing modules array" ) ?;
847+
848+ if modules. is_empty ( ) {
849+ println ! ( "No modules found." ) ;
850+ return Ok ( ( ) ) ;
851+ }
852+
853+ let rows = modules
854+ . iter ( )
855+ . map ( |m| {
856+ vec ! [
857+ m. get( "id" ) . and_then( |v| v. as_str( ) ) . unwrap_or( "-" ) . to_string( ) ,
858+ m. get( "name" ) . and_then( |v| v. as_str( ) ) . unwrap_or( "-" ) . to_string( ) ,
859+ m. get( "latestModuleVersionProgramHash" )
860+ . and_then( |v| v. as_str( ) )
861+ . unwrap_or( "-" )
862+ . to_string( ) ,
863+ m. get( "creatorUser" )
864+ . and_then( |v| v. as_str( ) )
865+ . unwrap_or( "-" )
866+ . to_string( ) ,
867+ m. get( "publishedOnMarketplace" )
868+ . and_then( |v| v. as_bool( ) )
869+ . map( |v| if v { "yes" } else { "no" } )
870+ . unwrap_or( "-" )
871+ . to_string( ) ,
872+ ]
873+ } )
874+ . collect :: < Vec < _ > > ( ) ;
875+
876+ println ! ( ) ;
877+ print_table ( & [ "MODULE_ID" , "NAME" , "LATEST_PROGRAM_HASH" , "CREATOR_USER" , "MARKETPLACE" ] , & rows) ;
878+ Ok ( ( ) )
879+ }
880+
881+ async fn list_module_versions ( args : ModuleVersionsArgs ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
882+ let server_url = resolve_server_url ( args. server_url ) ;
883+ let client = reqwest:: Client :: new ( ) ;
884+ let url = format ! ( "{}/modules/{}/versions" , server_url, args. module_id) ;
885+ let api_key = args. api_key . or_else ( || std:: env:: var ( "HERODOTUS_CLOUD_API_KEY" ) . ok ( ) ) ;
886+
887+ info ! ( "📚 Fetching module versions from {}..." , server_url) ;
888+ let mut request = client. get ( url) ;
889+ if let Some ( key) = api_key {
890+ request = request. header ( "X-API-KEY" , key) ;
891+ }
892+ let resp = request. send ( ) . await ?;
893+ if !resp. status ( ) . is_success ( ) {
894+ let status = resp. status ( ) ;
895+ let err = resp. text ( ) . await ?;
896+ return Err ( format ! ( "Failed to list module versions ({}): {}" , status, err) . into ( ) ) ;
897+ }
898+
899+ let versions: serde_json:: Value = resp. json ( ) . await ?;
900+ let versions = versions
901+ . as_array ( )
902+ . ok_or ( "Invalid response format: expected versions array" ) ?;
903+
904+ if versions. is_empty ( ) {
905+ println ! ( "No versions found for this module." ) ;
906+ return Ok ( ( ) ) ;
907+ }
908+
909+ let rows = versions
910+ . iter ( )
911+ . map ( |v| {
912+ vec ! [
913+ v. get( "version" ) . and_then( |x| x. as_str( ) ) . unwrap_or( "-" ) . to_string( ) ,
914+ v. get( "hash" ) . and_then( |x| x. as_str( ) ) . unwrap_or( "-" ) . to_string( ) ,
915+ v. get( "usageCount" )
916+ . and_then( |x| x. as_i64( ) )
917+ . map( |x| x. to_string( ) )
918+ . unwrap_or_else( || "-" . to_string( ) ) ,
919+ v. get( "createdAt" ) . and_then( |x| x. as_str( ) ) . unwrap_or( "-" ) . to_string( ) ,
920+ ]
921+ } )
922+ . collect :: < Vec < _ > > ( ) ;
923+
924+ println ! ( ) ;
925+ print_table ( & [ "VERSION" , "PROGRAM_HASH" , "USAGE_COUNT" , "CREATED_AT" ] , & rows) ;
926+ Ok ( ( ) )
927+ }
928+
724929fn find_compiled_contract_class_file ( current_dir : & Path , module_name : & str ) -> Result < Option < PathBuf > , Error > {
725930 let mut search_dirs = Vec :: new ( ) ;
726931 let mut cursor = Some ( current_dir. to_path_buf ( ) ) ;
0 commit comments