use aquila_client::AquilaClient;
use aquila_core::manifest::{AssetInfo, AssetManifest};
use chrono::Utc;
use clap::{Parser, Subcommand};
use rand::Rng;
use rand::distr::Alphanumeric;
use std::collections::HashMap;
use std::path::PathBuf;
use walkdir::WalkDir;
#[derive(Parser)]
#[command(name = "aquila")]
#[command(about = "CLI for Bevy Aquila Asset Server")]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short, long, default_value = "http://localhost:3000")]
url: String,
#[arg(short, long, env = "AQUILA_TOKEN")]
token: Option<String>,
}
#[derive(Subcommand)]
enum Commands {
Upload {
path: PathBuf,
},
Publish {
#[arg(long)]
dir: PathBuf,
#[arg(long)]
version: String,
},
Download {
hash: String,
#[arg(short, long)]
output: PathBuf,
},
GetManifest {
version: String,
},
Login,
GenerateSecret,
MintToken {
#[arg(short, long)]
subject: String,
#[arg(long)]
duration: Option<u64>,
#[arg(long, value_delimiter = ',', default_value = "read")]
scopes: Vec<String>,
},
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let client = AquilaClient::new(cli.url.clone(), cli.token.clone());
match cli.command {
Commands::GenerateSecret => {
let secret: String = rand::rng()
.sample_iter(&Alphanumeric)
.take(64)
.map(char::from)
.collect();
println!("🔑 Generated JWT Secret:");
println!("\n {}\n", secret);
println!("Copy this value and set it on your server:");
println!("set AQUILA_JWT_SECRET=\"{}\"", secret);
}
Commands::Login => {
let login_url = format!("{}/auth/login", cli.url.trim_end_matches('/'));
println!("🌐 To authenticate, please visit:");
println!("\n {}\n", login_url);
println!("After logging in, copy the 'token' from the JSON response and set it:");
println!("set AQUILA_TOKEN=\"...\"");
}
Commands::Upload { path } => {
let hash = client.upload_file(&path).await?;
println!("✅ Upload successful! Hash: {hash}");
}
Commands::Publish { dir, version } => {
println!("🚀 Publishing version '{version}' from {dir:?}...");
let mut assets = HashMap::new();
let mut count = 0;
for entry in WalkDir::new(&dir) {
let entry = entry?;
if entry.file_type().is_dir() {
continue;
}
let path = entry.path();
let relative_path = path
.strip_prefix(&dir)?
.to_string_lossy()
.replace('\\', "/");
println!("Processing: {relative_path}");
let hash = client.upload_file(path).await?;
let size = entry.metadata()?.len();
let mime_type = Some(
mime_guess::from_path(path)
.first_or_octet_stream()
.to_string(),
);
assets.insert(
relative_path,
AssetInfo {
hash,
size,
mime_type,
},
);
count += 1;
}
let manifest = AssetManifest {
version: version.clone(),
published_at: Utc::now(),
published_by: whoami::username()?,
assets,
};
client.publish_manifest(&manifest).await?;
println!("✅ Successfully published version {version} with {count} assets.",);
}
Commands::Download { hash, output } => {
println!("Downloading {hash}...");
let data = client.download_file(&hash).await?;
if let Some(parent) = output.parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::write(&output, data).await?;
println!("✅ Saved to {output:?}");
}
Commands::GetManifest { version } => {
println!("🔍 Fetching manifest for version '{}'...", version);
let manifest = client.fetch_manifest(&version).await?;
println!("{}", serde_json::to_string_pretty(&manifest)?);
}
Commands::MintToken {
subject,
duration,
scopes,
} => {
let o_scopes = if scopes.is_empty() {
None
} else {
Some(scopes)
};
println!("🔑 Minting token for '{}'...", subject);
let token = client.mint_token(&subject, duration, o_scopes).await?;
println!("✅ SUCCESS! Here is your new token:\n");
println!("{token}");
println!("\n(Keep this token safe! It cannot be retrieved again.)");
}
}
Ok(())
}