Skip to content

Commit 7696a62

Browse files
committed
feat(cli): add rustapi deploy cloud, whoami, logout commands
1 parent 16784e5 commit 7696a62

10 files changed

Lines changed: 351 additions & 59 deletions

File tree

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cargo-rustapi/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ serde_yaml = "0.9"
3838
toml = "1.1"
3939

4040
# HTTP client for fetching remote specs
41-
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false, optional = true }
41+
reqwest = { version = "0.12", features = ["json", "multipart", "rustls-tls"], default-features = false, optional = true }
4242

4343
# MCP support (OpenAPI → MCP server generator)
4444
rustapi-mcp = { workspace = true, optional = true }

crates/cargo-rustapi/src/cli.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use crate::commands::ReplayArgs;
55
use crate::commands::{
66
self, AddArgs, BenchArgs, ClientArgs, DeployArgs, DoctorArgs, GenerateArgs, LoginArgs,
7-
MigrateArgs, NewArgs, ObservabilityArgs, RunArgs, WatchArgs,
7+
LogoutArgs, MigrateArgs, NewArgs, ObservabilityArgs, RunArgs, WatchArgs, WhoamiArgs,
88
};
99

1010
#[cfg(feature = "mcp")]
@@ -77,6 +77,12 @@ enum Commands {
7777
/// Login to RustAPI Cloud
7878
Login(LoginArgs),
7979

80+
/// Show current login status
81+
Whoami(WhoamiArgs),
82+
83+
/// Logout from RustAPI Cloud
84+
Logout(LogoutArgs),
85+
8086
/// Deploy to various platforms
8187
#[command(subcommand)]
8288
Deploy(DeployArgs),
@@ -105,6 +111,8 @@ impl Cli {
105111
#[cfg(feature = "mcp")]
106112
Commands::Mcp(McpCommands::Generate(args)) => commands::mcp_generate(args).await,
107113
Commands::Login(args) => commands::login(args).await,
114+
Commands::Whoami(args) => commands::whoami(args).await,
115+
Commands::Logout(args) => commands::logout(args).await,
108116
Commands::Deploy(args) => commands::deploy(args).await,
109117
#[cfg(feature = "replay")]
110118
Commands::Replay(args) => commands::replay(args).await,

crates/cargo-rustapi/src/commands/deploy.rs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,20 @@
44
55
use anyhow::{Context, Result};
66
use clap::{Args, Subcommand};
7+
use indicatif::{ProgressBar, ProgressStyle};
8+
use serde::Deserialize;
79
use std::fs;
810
use 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)]
1217
pub 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)]
2746
pub struct DockerArgs {
2847
/// Output path for Dockerfile
@@ -82,6 +101,7 @@ pub struct ShuttleArgs {
82101
/// Execute deployment command
83102
pub 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

Comments
 (0)