44
55use anyhow:: { Context , Result } ;
66use clap:: { Args , Subcommand } ;
7+ use indicatif:: { ProgressBar , ProgressStyle } ;
8+ use serde:: Deserialize ;
79use std:: fs;
810use std:: path:: PathBuf ;
11+ use std:: time:: Duration ;
12+
13+ use crate :: config:: load_config;
914
1015/// Arguments for deployment commands
1116#[ derive( Subcommand , Debug ) ]
1217pub enum DeployArgs {
18+ /// Deploy to RustAPI Cloud (managed hosting)
19+ Cloud ( CloudArgs ) ,
20+
1321 /// Generate a Dockerfile for the project
1422 Docker ( DockerArgs ) ,
1523
@@ -23,6 +31,17 @@ pub enum DeployArgs {
2331 Shuttle ( ShuttleArgs ) ,
2432}
2533
34+ #[ derive( Args , Debug ) ]
35+ pub struct CloudArgs {
36+ /// Project name (defaults to Cargo.toml package name)
37+ #[ arg( short, long) ]
38+ pub name : Option < String > ,
39+
40+ /// Do not wait for deployment to complete
41+ #[ arg( long) ]
42+ pub no_wait : bool ,
43+ }
44+
2645#[ derive( Args , Debug ) ]
2746pub struct DockerArgs {
2847 /// Output path for Dockerfile
@@ -82,6 +101,7 @@ pub struct ShuttleArgs {
82101/// Execute deployment command
83102pub async fn deploy ( args : DeployArgs ) -> Result < ( ) > {
84103 match args {
104+ DeployArgs :: Cloud ( cloud_args) => deploy_cloud ( cloud_args) . await ,
85105 DeployArgs :: Docker ( docker_args) => generate_dockerfile ( docker_args) . await ,
86106 DeployArgs :: Fly ( fly_args) => deploy_fly ( fly_args) . await ,
87107 DeployArgs :: Railway ( railway_args) => deploy_railway ( railway_args) . await ,
@@ -300,3 +320,154 @@ fn get_package_name() -> Result<String> {
300320
301321 anyhow:: bail!( "Could not find package name in Cargo.toml" )
302322}
323+
324+ // --- Cloud Deploy ---
325+
326+ #[ derive( Deserialize ) ]
327+ struct DeployResponse {
328+ deploy_id : String ,
329+ status : String ,
330+ #[ serde( default ) ]
331+ url : Option < String > ,
332+ }
333+
334+ async fn deploy_cloud ( args : CloudArgs ) -> Result < ( ) > {
335+ let config = load_config ( ) ?;
336+
337+ let token = config
338+ . token
339+ . as_ref ( )
340+ . ok_or_else ( || anyhow:: anyhow!( "Not logged in. Run `rustapi login` first." ) ) ?;
341+
342+ let cloud_url = config
343+ . cloud_url
344+ . as_deref ( )
345+ . unwrap_or ( "https://api.rustapi.cloud" )
346+ . trim_end_matches ( '/' ) ;
347+
348+ let project_name = args
349+ . name
350+ . unwrap_or_else ( || get_package_name ( ) . unwrap_or_else ( |_| "rustapi-app" . to_string ( ) ) ) ;
351+
352+ // Verify project uses RustAPI
353+ let cargo_toml =
354+ fs:: read_to_string ( "Cargo.toml" ) . context ( "Not in a Rust project (no Cargo.toml found)" ) ?;
355+
356+ if !cargo_toml. contains ( "rustapi" ) {
357+ println ! ( " ⚠️ This project doesn't appear to use RustAPI." ) ;
358+ println ! ( " Continuing anyway..." ) ;
359+ }
360+
361+ // Build
362+ println ! ( " 🔨 Building {} (release)..." , project_name) ;
363+
364+ let build = std:: process:: Command :: new ( "cargo" )
365+ . args ( [ "build" , "--release" ] )
366+ . stdout ( std:: process:: Stdio :: piped ( ) )
367+ . stderr ( std:: process:: Stdio :: piped ( ) )
368+ . spawn ( )
369+ . context ( "Failed to start cargo build" ) ?;
370+
371+ let output = build
372+ . wait_with_output ( )
373+ . context ( "Failed to wait for cargo build" ) ?;
374+
375+ if !output. status . success ( ) {
376+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
377+ return Err ( anyhow:: anyhow!( "Build failed:\n {}" , stderr) ) ;
378+ }
379+
380+ println ! ( " ✅ Build complete" ) ;
381+
382+ // Find binary
383+ let binary_path = PathBuf :: from ( "target/release" )
384+ . join ( & project_name)
385+ . with_extension ( std:: env:: consts:: EXE_SUFFIX ) ;
386+
387+ if !binary_path. exists ( ) {
388+ return Err ( anyhow:: anyhow!(
389+ "Binary not found at {}. Make sure the package name matches the binary name." ,
390+ binary_path. display( )
391+ ) ) ;
392+ }
393+
394+ // Package binary
395+ println ! ( " 📦 Packaging..." ) ;
396+ let binary_data = fs:: read ( & binary_path) . context ( "Failed to read binary" ) ?;
397+
398+ // Upload
399+ println ! ( " ☁️ Uploading to RustAPI Cloud..." ) ;
400+
401+ let client = reqwest:: Client :: new ( ) ;
402+ let form = reqwest:: multipart:: Form :: new ( )
403+ . text ( "project_name" , project_name. clone ( ) )
404+ . part (
405+ "binary" ,
406+ reqwest:: multipart:: Part :: bytes ( binary_data) . file_name ( format ! ( "{}.bin" , project_name) ) ,
407+ ) ;
408+
409+ let deploy_resp: DeployResponse = client
410+ . post ( format ! ( "{}/deploy" , cloud_url) )
411+ . header ( "Authorization" , format ! ( "Bearer {}" , token) )
412+ . multipart ( form)
413+ . send ( )
414+ . await
415+ . context ( "Failed to connect to RustAPI Cloud" ) ?
416+ . json ( )
417+ . await
418+ . context ( "Invalid response from deploy endpoint" ) ?;
419+
420+ let deploy_id = deploy_resp. deploy_id ;
421+
422+ if args. no_wait {
423+ println ! ( " ✅ Deploy queued: {}" , deploy_id) ;
424+ println ! ( " Check status: rustapi deploy status {}" , deploy_id) ;
425+ return Ok ( ( ) ) ;
426+ }
427+
428+ // Poll for status
429+ let spinner = ProgressBar :: new_spinner ( ) ;
430+ spinner. set_style ( ProgressStyle :: with_template ( " {spinner} Deploying..." ) . unwrap ( ) ) ;
431+ spinner. enable_steady_tick ( Duration :: from_millis ( 100 ) ) ;
432+
433+ for _ in 0 ..120 {
434+ tokio:: time:: sleep ( Duration :: from_secs ( 3 ) ) . await ;
435+
436+ let status_resp: DeployResponse = match client
437+ . get ( format ! ( "{}/deploy/{}/status" , cloud_url, deploy_id) )
438+ . header ( "Authorization" , format ! ( "Bearer {}" , token) )
439+ . send ( )
440+ . await
441+ {
442+ Ok ( resp) => match resp. json ( ) . await {
443+ Ok ( body) => body,
444+ Err ( _) => continue ,
445+ } ,
446+ Err ( _) => continue ,
447+ } ;
448+
449+ match status_resp. status . as_str ( ) {
450+ "running" | "live" => {
451+ spinner. finish_and_clear ( ) ;
452+ println ! (
453+ " 🚀 Deployed: {}" ,
454+ status_resp. url. as_deref( ) . unwrap_or( "(url pending)" )
455+ ) ;
456+ return Ok ( ( ) ) ;
457+ }
458+ "failed" => {
459+ spinner. finish_with_message ( "Deploy failed" ) ;
460+ return Err ( anyhow:: anyhow!(
461+ "Deployment failed. Check logs in the dashboard."
462+ ) ) ;
463+ }
464+ _ => continue ,
465+ }
466+ }
467+
468+ spinner. finish_with_message ( "Deploy timed out" ) ;
469+ println ! ( " Deploy ID: {} (still processing)" , deploy_id) ;
470+ println ! ( " Check: rustapi deploy status {}" , deploy_id) ;
471+
472+ Ok ( ( ) )
473+ }
0 commit comments