cargo-tangle 0.5.0-alpha.13

A command-line tool to create and deploy blueprints on Tangle Network
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
//! Cloud provider configuration management.
//!
//! This module handles the configuration and authentication setup for various cloud providers.
//! It provides interactive setup flows, credential management, and persistent configuration storage.

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;

/// Supported cloud providers for Blueprint deployment.
#[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"),
        }
    }
}

/// Cloud configuration storage.
///
/// Persisted to ~/.config/tangle/cloud.json
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloudConfig {
    /// The default provider to use when none is specified
    pub default_provider: Option<CloudProvider>,
    /// Per-provider configuration settings
    pub providers: HashMap<CloudProvider, ProviderSettings>,
}

/// Provider-specific configuration settings.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderSettings {
    /// Default region for deployments
    pub region: String,
    /// GCP project ID (only used for Google Cloud)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub project_id: Option<String>,
    /// Internal flag indicating if provider is fully configured
    #[serde(skip)]
    pub configured: bool,
}

impl CloudConfig {
    /// Load config from disk or create default
    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")?;
            // Try to parse as JSON first, fall back to TOML for backwards compatibility
            serde_json::from_str(&content)
                .or_else(|_| toml::from_str(&content))
                .context("Failed to parse cloud config")
        } else {
            Ok(Self::default())
        }
    }

    /// Save config to disk
    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")?;
        }

        // For now, serialize to JSON (since toml serialization is not straightforward in v0.9)
        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(),
        }
    }
}

/// Configure a cloud provider with interactive setup.
///
/// This function guides the user through provider-specific authentication setup,
/// including credential configuration, region selection, and default settings.
///
/// # Arguments
///
/// * `provider` - The cloud provider to configure
/// * `region` - Optional region override (otherwise prompts user)
/// * `set_default` - Whether to set this as the default provider
///
/// # Errors
///
/// Returns an error if:
/// * Configuration directory cannot be created
/// * Credentials are invalid or cannot be saved
/// * Provider-specific CLI tools are not available
///
/// # Examples
///
/// ```no_run
/// # use cargo_tangle::command::cloud::{configure, CloudProvider};
/// # async fn example() -> color_eyre::Result<()> {
/// // Configure AWS as default provider
/// configure(CloudProvider::AWS, Some("us-east-1".to_string()), true).await?;
/// # Ok(())
/// # }
/// ```
pub async fn configure(
    provider: CloudProvider,
    region: Option<String>,
    set_default: bool,
) -> Result<()> {
    println!("🔧 Configuring {}...\n", provider);

    let mut config = CloudConfig::load()?;

    // Get or prompt for region
    let region = if let Some(r) = region {
        r
    } else {
        prompt_region(provider)?
    };

    // Provider-specific setup
    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?,
    }

    // Save settings
    let mut settings = ProviderSettings {
        region,
        project_id: None,
        configured: true,
    };

    // GCP needs project ID
    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(())
}

/// Configure AWS credentials
async fn configure_aws() -> Result<()> {
    // Check for existing AWS CLI config
    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(());
    }

    // Check environment variables
    if std::env::var("AWS_ACCESS_KEY_ID").is_ok() {
        println!("✓ Found AWS credentials in environment");
        return Ok(());
    }

    // Prompt for credentials
    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()?;

    // Save to ~/.aws/credentials
    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(())
}

/// Configure GCP credentials  
async fn configure_gcp() -> Result<()> {
    // Check for gcloud CLI
    if std::process::Command::new("gcloud")
        .arg("--version")
        .output()
        .is_ok()
    {
        println!("✓ Found gcloud CLI");

        // Check if already authenticated
        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(());
        }

        // Run gcloud auth
        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(())
}

/// Configure Azure credentials
async fn configure_azure() -> Result<()> {
    // Check for az CLI
    if std::process::Command::new("az")
        .arg("--version")
        .output()
        .is_ok()
    {
        println!("✓ Found Azure CLI");

        // Check if logged in
        let output = std::process::Command::new("az")
            .args(&["account", "show"])
            .output()?;

        if output.status.success() {
            println!("✓ Already logged in to Azure");
            return Ok(());
        }

        // Run az login
        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(())
}

/// Configure DigitalOcean credentials
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()?;

    // Save to .env file
    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(())
}

/// Configure Vultr credentials
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()?;

    // Save to .env file
    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(())
}

/// Configure Hetzner Cloud credentials
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(())
}

/// Configure RunPod credentials
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(())
}

/// Configure Lambda Labs credentials
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(())
}

/// Configure Prime Intellect credentials
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(())
}

/// Configure Vast.ai credentials
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(())
}

/// Configure Crusoe Cloud credentials
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(())
}

/// Prompt for region selection
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())
}

/// List all configured cloud providers.
///
/// Displays a formatted list of all configured providers with their settings,
/// including region, default status, and project IDs where applicable.
///
/// # Errors
///
/// Returns an error if the configuration file cannot be read.
///
/// # Examples
///
/// ```bash
/// cargo tangle cloud list
/// ```
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(())
}