use anyhow::Result;
use clap::{Args, ValueEnum};
use colored::Colorize;
use std::str::FromStr;
use super::spawn::{self, SpawnArgs};
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum ReplicationMode {
None,
Checkpointed,
WarmStandby,
}
impl ReplicationMode {
pub const fn as_str(self) -> &'static str {
match self {
ReplicationMode::None => "none",
ReplicationMode::Checkpointed => "checkpointed",
ReplicationMode::WarmStandby => "warm-standby",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum Template {
NostrRelay,
InferenceEndpoint,
HeadlessBrowser,
BitcoinNode,
AgentSandbox,
#[value(name = "openclaw")]
OpenClaw,
}
pub struct TemplateDefaults {
pub tier: &'static str,
pub image: &'static str,
pub replication: ReplicationMode,
pub summary: &'static str,
}
pub const fn template_defaults(t: Template) -> TemplateDefaults {
match t {
Template::NostrRelay => TemplateDefaults {
tier: "basic",
image: "ubuntu:22.04",
replication: ReplicationMode::WarmStandby,
summary: "Censorship-resistant Nostr relay; warm-standby across two providers.",
},
Template::InferenceEndpoint => TemplateDefaults {
tier: "basic",
image: "ubuntu:22.04",
replication: ReplicationMode::Checkpointed,
summary: "OpenAI-compatible inference endpoint; checkpointed.",
},
Template::HeadlessBrowser => TemplateDefaults {
tier: "basic",
image: "ubuntu:22.04",
replication: ReplicationMode::None,
summary: "Disposable headless browser; agent-driven scraping.",
},
Template::BitcoinNode => TemplateDefaults {
tier: "basic",
image: "ubuntu:22.04",
replication: ReplicationMode::Checkpointed,
summary: "Bitcoin full node; checkpointed (long sync).",
},
Template::AgentSandbox => TemplateDefaults {
tier: "basic",
image: "nikolaik/python-nodejs:python3.12-nodejs20",
replication: ReplicationMode::None,
summary: "Python + Node + git sandbox for agents, CI, and map-reduce shards.",
},
Template::OpenClaw => TemplateDefaults {
tier: "standard",
image: "ghcr.io/openclaw/openclaw:latest",
replication: ReplicationMode::Checkpointed,
summary: "OpenClaw personal AI assistant Gateway; checkpointed.",
},
}
}
fn parse_cashu_token(s: &str) -> Result<String, String> {
cdk::nuts::Token::from_str(s)
.map(|_| s.to_string())
.map_err(|e| format!("not a valid Cashu token: {}", e))
}
#[derive(Args)]
pub struct DeployArgs {
#[arg(value_enum)]
pub template: Template,
#[arg(short = 'k', long, value_parser = parse_cashu_token)]
pub token: String,
#[arg(long)]
pub provider: Option<String>,
#[arg(short, long)]
pub tier: Option<String>,
#[arg(long, value_enum)]
pub replication: Option<ReplicationMode>,
#[arg(long)]
pub image: Option<String>,
#[arg(long)]
pub nostr_key: Option<String>,
#[arg(long)]
pub relays: Option<String>,
}
pub async fn execute(args: DeployArgs, verbose: bool) -> Result<()> {
let defaults = template_defaults(args.template);
let tier = args.tier.unwrap_or_else(|| defaults.tier.to_string());
let image = args.image.unwrap_or_else(|| defaults.image.to_string());
let replication = args.replication.unwrap_or(defaults.replication);
println!("{}", "Deploying Template".blue().bold());
println!(" Template: {}", format!("{:?}", args.template).cyan());
println!(" Summary: {}", defaults.summary);
println!(" Tier: {}", tier);
println!(" Image: {}", image);
println!(" Replication: {}", replication.as_str());
println!();
if replication != ReplicationMode::None {
println!(
"{}",
" Note: replication != none is parsed but not yet enforced;".yellow()
);
println!(
"{}",
" Units 5/6 wire it through to the provider.".yellow()
);
println!();
}
if args.provider.is_none() {
anyhow::bail!(
"auto-selection of providers lands with the observatory (Unit 12). \
Pass --provider <npub> for now."
);
}
let template_slug = match args.template {
Template::NostrRelay => "nostr-relay",
Template::InferenceEndpoint => "inference-endpoint",
Template::HeadlessBrowser => "headless-browser",
Template::BitcoinNode => "bitcoin-node",
Template::AgentSandbox => "agent-sandbox",
Template::OpenClaw => "openclaw",
};
let replication_str = match replication {
ReplicationMode::None => "none",
ReplicationMode::Checkpointed => "checkpointed",
ReplicationMode::WarmStandby => "none", }
.to_string();
let spawn_args = SpawnArgs {
provider: args.provider,
server: None,
tier,
token: args.token,
image,
ssh_user: None,
ssh_pass: None,
nostr_key: args.nostr_key,
relays: args.relays,
template_slug: Some(template_slug.to_string()),
replication: replication_str,
standby: None,
primary_npub: None,
workload_id: None,
encrypt_volume: false,
no_encrypt_volume: false,
isolation_level: None,
};
spawn::execute(spawn_args, verbose).await
}