use clap::ValueEnum;
use color_eyre::{Result, eyre::Context};
use dialoguer::{Input, Password, Select, theme::ColorfulTheme};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, ValueEnum, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum CloudProvider {
#[value(name = "aws")]
AWS,
#[value(name = "gcp")]
GCP,
#[value(name = "azure")]
Azure,
#[value(name = "digitalocean", alias = "do")]
DigitalOcean,
#[value(name = "vultr")]
Vultr,
#[value(name = "hetzner")]
Hetzner,
#[value(name = "runpod")]
RunPod,
#[value(name = "lambda", alias = "lambda-labs")]
LambdaLabs,
#[value(name = "prime-intellect", alias = "pi")]
PrimeIntellect,
#[value(name = "vast", alias = "vast-ai")]
VastAi,
#[value(name = "crusoe")]
Crusoe,
}
impl std::fmt::Display for CloudProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AWS => write!(f, "AWS"),
Self::GCP => write!(f, "Google Cloud"),
Self::Azure => write!(f, "Azure"),
Self::DigitalOcean => write!(f, "DigitalOcean"),
Self::Vultr => write!(f, "Vultr"),
Self::Hetzner => write!(f, "Hetzner"),
Self::RunPod => write!(f, "RunPod"),
Self::LambdaLabs => write!(f, "Lambda Labs"),
Self::PrimeIntellect => write!(f, "Prime Intellect"),
Self::VastAi => write!(f, "Vast.ai"),
Self::Crusoe => write!(f, "Crusoe"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloudConfig {
pub default_provider: Option<CloudProvider>,
pub providers: HashMap<CloudProvider, ProviderSettings>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderSettings {
pub region: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub project_id: Option<String>,
#[serde(skip)]
pub configured: bool,
}
impl CloudConfig {
pub fn load() -> Result<Self> {
let path = Self::config_path()?;
if path.exists() {
let content = std::fs::read_to_string(&path).context("Failed to read cloud config")?;
serde_json::from_str(&content)
.or_else(|_| toml::from_str(&content))
.context("Failed to parse cloud config")
} else {
Ok(Self::default())
}
}
pub fn save(&self) -> Result<()> {
let path = Self::config_path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).context("Failed to create config directory")?;
}
let content = serde_json::to_string_pretty(self).context("Failed to serialize config")?;
std::fs::write(&path, content).context("Failed to write cloud config")?;
Ok(())
}
fn config_path() -> Result<PathBuf> {
let config_dir = dirs::config_dir()
.ok_or_else(|| color_eyre::eyre::eyre!("Could not find config directory"))?;
Ok(config_dir.join("tangle").join("cloud.json"))
}
}
impl Default for CloudConfig {
fn default() -> Self {
Self {
default_provider: None,
providers: HashMap::new(),
}
}
}
pub async fn configure(
provider: CloudProvider,
region: Option<String>,
set_default: bool,
) -> Result<()> {
println!("🔧 Configuring {}...\n", provider);
let mut config = CloudConfig::load()?;
let region = if let Some(r) = region {
r
} else {
prompt_region(provider)?
};
match provider {
CloudProvider::AWS => configure_aws().await?,
CloudProvider::GCP => configure_gcp().await?,
CloudProvider::Azure => configure_azure().await?,
CloudProvider::DigitalOcean => configure_digitalocean().await?,
CloudProvider::Vultr => configure_vultr().await?,
CloudProvider::Hetzner => configure_hetzner().await?,
CloudProvider::RunPod => configure_runpod().await?,
CloudProvider::LambdaLabs => configure_lambda_labs().await?,
CloudProvider::PrimeIntellect => configure_prime_intellect().await?,
CloudProvider::VastAi => configure_vast_ai().await?,
CloudProvider::Crusoe => configure_crusoe().await?,
}
let mut settings = ProviderSettings {
region,
project_id: None,
configured: true,
};
if provider == CloudProvider::GCP {
settings.project_id = Some(Input::new().with_prompt("GCP Project ID").interact()?);
}
config.providers.insert(provider, settings);
if set_default || config.default_provider.is_none() {
config.default_provider = Some(provider);
}
config.save()?;
println!("\n✅ {} configured successfully!", provider);
if config.default_provider == Some(provider) {
println!(" Set as default provider");
}
Ok(())
}
async fn configure_aws() -> Result<()> {
let aws_config = dirs::home_dir()
.map(|h| h.join(".aws").join("credentials"))
.filter(|p| p.exists());
if aws_config.is_some() {
println!("✓ Found AWS credentials in ~/.aws/credentials");
return Ok(());
}
if std::env::var("AWS_ACCESS_KEY_ID").is_ok() {
println!("✓ Found AWS credentials in environment");
return Ok(());
}
println!("No AWS credentials found. Please provide:");
println!("(These will be stored in ~/.aws/credentials)");
let access_key = Input::<String>::new()
.with_prompt("AWS Access Key ID")
.interact()?;
let secret_key = Password::new()
.with_prompt("AWS Secret Access Key")
.interact()?;
let aws_dir = dirs::home_dir()
.ok_or_else(|| color_eyre::eyre::eyre!("Could not find home directory"))?
.join(".aws");
std::fs::create_dir_all(&aws_dir)?;
let credentials = format!(
"[default]\naws_access_key_id = {}\naws_secret_access_key = {}\n",
access_key, secret_key
);
std::fs::write(aws_dir.join("credentials"), credentials)?;
Ok(())
}
async fn configure_gcp() -> Result<()> {
if std::process::Command::new("gcloud")
.arg("--version")
.output()
.is_ok()
{
println!("✓ Found gcloud CLI");
let output = std::process::Command::new("gcloud")
.args(&[
"auth",
"list",
"--filter=status:ACTIVE",
"--format=value(account)",
])
.output()?;
if !output.stdout.is_empty() {
let account = String::from_utf8_lossy(&output.stdout);
println!("✓ Authenticated as {}", account.trim());
return Ok(());
}
println!("Running gcloud auth login...");
std::process::Command::new("gcloud")
.args(&["auth", "application-default", "login"])
.status()?;
} else {
println!("⚠️ gcloud CLI not found");
println!(" Please install: https://cloud.google.com/sdk/docs/install");
println!(" Or set GOOGLE_APPLICATION_CREDENTIALS to a service account key file");
}
Ok(())
}
async fn configure_azure() -> Result<()> {
if std::process::Command::new("az")
.arg("--version")
.output()
.is_ok()
{
println!("✓ Found Azure CLI");
let output = std::process::Command::new("az")
.args(&["account", "show"])
.output()?;
if output.status.success() {
println!("✓ Already logged in to Azure");
return Ok(());
}
println!("Running az login...");
std::process::Command::new("az").arg("login").status()?;
} else {
println!("⚠️ Azure CLI not found");
println!(" Please install: https://aka.ms/azure-cli");
}
Ok(())
}
async fn configure_digitalocean() -> Result<()> {
if std::env::var("DIGITALOCEAN_TOKEN").is_ok() {
println!("✓ Found DigitalOcean token in environment");
return Ok(());
}
println!("Get your API token from: https://cloud.digitalocean.com/account/api/tokens");
let token = Password::new()
.with_prompt("DigitalOcean API Token")
.interact()?;
let env_file = std::env::current_dir()?.join(".env");
let mut content = if env_file.exists() {
std::fs::read_to_string(&env_file)?
} else {
String::new()
};
if !content.contains("DIGITALOCEAN_TOKEN") {
content.push_str(&format!("\nDIGITALOCEAN_TOKEN={}\n", token));
std::fs::write(env_file, content)?;
println!("✓ Saved to .env file");
}
Ok(())
}
async fn configure_vultr() -> Result<()> {
if std::env::var("VULTR_API_KEY").is_ok() {
println!("✓ Found Vultr API key in environment");
return Ok(());
}
println!("Get your API key from: https://my.vultr.com/settings/#settingsapi");
let api_key = Password::new().with_prompt("Vultr API Key").interact()?;
let env_file = std::env::current_dir()?.join(".env");
let mut content = if env_file.exists() {
std::fs::read_to_string(&env_file)?
} else {
String::new()
};
if !content.contains("VULTR_API_KEY") {
content.push_str(&format!("\nVULTR_API_KEY={}\n", api_key));
std::fs::write(env_file, content)?;
println!("✓ Saved to .env file");
}
Ok(())
}
async fn configure_hetzner() -> Result<()> {
if std::env::var("HETZNER_API_TOKEN").is_ok() {
println!("✓ Found Hetzner API token in environment");
return Ok(());
}
println!(
"Get your API token from: https://console.hetzner.cloud/projects/default/security/tokens"
);
let token = Password::new()
.with_prompt("Hetzner API Token")
.interact()?;
let env_file = std::env::current_dir()?.join(".env");
let mut content = if env_file.exists() {
std::fs::read_to_string(&env_file)?
} else {
String::new()
};
if !content.contains("HETZNER_API_TOKEN") {
content.push_str(&format!("\nHETZNER_API_TOKEN={}\n", token));
std::fs::write(env_file, content)?;
println!("✓ Saved to .env file");
}
Ok(())
}
async fn configure_runpod() -> Result<()> {
if std::env::var("RUNPOD_API_KEY").is_ok() {
println!("✓ Found RunPod API key in environment");
return Ok(());
}
println!("Get your API key from: https://www.runpod.io/console/user/settings");
let api_key = Password::new().with_prompt("RunPod API Key").interact()?;
let env_file = std::env::current_dir()?.join(".env");
let mut content = if env_file.exists() {
std::fs::read_to_string(&env_file)?
} else {
String::new()
};
if !content.contains("RUNPOD_API_KEY") {
content.push_str(&format!("\nRUNPOD_API_KEY={}\n", api_key));
std::fs::write(env_file, content)?;
println!("✓ Saved to .env file");
}
Ok(())
}
async fn configure_lambda_labs() -> Result<()> {
if std::env::var("LAMBDA_LABS_API_KEY").is_ok() {
println!("✓ Found Lambda Labs API key in environment");
return Ok(());
}
println!("Get your API key from: https://cloud.lambdalabs.com/api-keys");
let api_key = Password::new()
.with_prompt("Lambda Labs API Key")
.interact()?;
let env_file = std::env::current_dir()?.join(".env");
let mut content = if env_file.exists() {
std::fs::read_to_string(&env_file)?
} else {
String::new()
};
if !content.contains("LAMBDA_LABS_API_KEY") {
content.push_str(&format!("\nLAMBDA_LABS_API_KEY={}\n", api_key));
std::fs::write(env_file, content)?;
println!("✓ Saved to .env file");
}
Ok(())
}
async fn configure_prime_intellect() -> Result<()> {
if std::env::var("PRIME_INTELLECT_API_KEY").is_ok() {
println!("✓ Found Prime Intellect API key in environment");
return Ok(());
}
println!("Get your API key from: https://app.primeintellect.ai/settings/api-keys");
let api_key = Password::new()
.with_prompt("Prime Intellect API Key")
.interact()?;
let env_file = std::env::current_dir()?.join(".env");
let mut content = if env_file.exists() {
std::fs::read_to_string(&env_file)?
} else {
String::new()
};
if !content.contains("PRIME_INTELLECT_API_KEY") {
content.push_str(&format!("\nPRIME_INTELLECT_API_KEY={}\n", api_key));
std::fs::write(env_file, content)?;
println!("✓ Saved to .env file");
}
Ok(())
}
async fn configure_vast_ai() -> Result<()> {
if std::env::var("VAST_AI_API_KEY").is_ok() {
println!("✓ Found Vast.ai API key in environment");
return Ok(());
}
println!("Get your API key from: https://cloud.vast.ai/account/");
let api_key = Password::new().with_prompt("Vast.ai API Key").interact()?;
let env_file = std::env::current_dir()?.join(".env");
let mut content = if env_file.exists() {
std::fs::read_to_string(&env_file)?
} else {
String::new()
};
if !content.contains("VAST_AI_API_KEY") {
content.push_str(&format!("\nVAST_AI_API_KEY={}\n", api_key));
std::fs::write(env_file, content)?;
println!("✓ Saved to .env file");
}
Ok(())
}
async fn configure_crusoe() -> Result<()> {
if std::env::var("CRUSOE_API_KEY").is_ok() && std::env::var("CRUSOE_API_SECRET").is_ok() {
println!("✓ Found Crusoe credentials in environment");
return Ok(());
}
println!("Get your API credentials from: https://console.crusoecloud.com/settings/api-keys");
let api_key = Input::<String>::new()
.with_prompt("Crusoe API Key")
.interact()?;
let api_secret = Password::new()
.with_prompt("Crusoe API Secret")
.interact()?;
let env_file = std::env::current_dir()?.join(".env");
let mut content = if env_file.exists() {
std::fs::read_to_string(&env_file)?
} else {
String::new()
};
if !content.contains("CRUSOE_API_KEY") {
content.push_str(&format!("\nCRUSOE_API_KEY={}\n", api_key));
content.push_str(&format!("CRUSOE_API_SECRET={}\n", api_secret));
std::fs::write(env_file, content)?;
println!("✓ Saved to .env file");
}
Ok(())
}
fn prompt_region(provider: CloudProvider) -> Result<String> {
let regions = match provider {
CloudProvider::AWS => vec![
("us-east-1", "US East (N. Virginia)"),
("us-west-2", "US West (Oregon)"),
("eu-west-1", "Europe (Ireland)"),
("ap-northeast-1", "Asia Pacific (Tokyo)"),
],
CloudProvider::GCP => vec![
("us-central1", "US Central (Iowa)"),
("us-west1", "US West (Oregon)"),
("europe-west1", "Europe (Belgium)"),
("asia-northeast1", "Asia (Tokyo)"),
],
CloudProvider::Azure => vec![
("eastus", "East US"),
("westus2", "West US 2"),
("northeurope", "North Europe"),
("japaneast", "Japan East"),
],
CloudProvider::DigitalOcean => vec![
("nyc3", "New York 3"),
("sfo3", "San Francisco 3"),
("ams3", "Amsterdam 3"),
("sgp1", "Singapore 1"),
],
CloudProvider::Vultr => vec![
("ewr", "New Jersey"),
("lax", "Los Angeles"),
("ams", "Amsterdam"),
("nrt", "Tokyo"),
],
CloudProvider::Hetzner => vec![
("fsn1", "Falkenstein (DE)"),
("nbg1", "Nuremberg (DE)"),
("hel1", "Helsinki (FI)"),
("ash", "Ashburn (US)"),
("hil", "Hillsboro (US)"),
],
CloudProvider::RunPod => vec![("US", "United States"), ("EU", "Europe"), ("CA", "Canada")],
CloudProvider::LambdaLabs => vec![
("us-west-1", "US West"),
("us-east-1", "US East"),
("us-south-1", "US South"),
("europe-central-1", "Europe Central"),
],
CloudProvider::PrimeIntellect => vec![
("us-east", "US East"),
("us-west", "US West"),
("eu-west", "EU West"),
],
CloudProvider::VastAi => vec![
("any", "Any (cheapest)"),
("US", "United States"),
("EU", "Europe"),
],
CloudProvider::Crusoe => vec![
("us-east1", "US East"),
("us-central1", "US Central"),
("us-northwest1", "US Northwest"),
],
};
let display_regions: Vec<String> = regions
.iter()
.map(|(code, name)| format!("{} ({})", name, code))
.collect();
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Select region")
.items(&display_regions)
.default(0)
.interact()?;
Ok(regions[selection].0.to_string())
}
pub async fn list_providers() -> Result<()> {
let config = CloudConfig::load()?;
if config.providers.is_empty() {
println!("No cloud providers configured.");
println!("Run `cargo tangle cloud configure <provider>` to get started.");
return Ok(());
}
println!("Configured providers:\n");
for (provider, settings) in &config.providers {
let default = if Some(*provider) == config.default_provider {
" (default)"
} else {
""
};
println!(" {} {}", provider, default);
println!(" Region: {}", settings.region);
if let Some(project) = &settings.project_id {
println!(" Project: {}", project);
}
println!();
}
Ok(())
}