spacetimedb-cli 0.2.4

A command line interface for SpacetimeDB
Documentation
use clap::Arg;
use clap::ArgMatches;
use duckscript::types::runtime::{Context, StateValue};
use serde::Deserialize;
use serde::Serialize;
use std::fs;
use std::path::Path;

use crate::config::Config;
use crate::util;
use crate::util::init_default;

pub fn cli() -> clap::Command<'static> {
    clap::Command::new("publish")
        .about("Create and update a SpacetimeDB database.")
        .arg(
            Arg::new("host_type")
                .takes_value(true)
                .required(false)
                .long("host_type")
                .short('t')
                .possible_values(["wasmer"]),
        )
        .arg(
            Arg::new("clear_database")
                .long("clear-database")
                .short('c')
                .takes_value(false),
        )
        .arg(
            Arg::new("path_to_project")
                .required(false)
                .default_value(".")
                .long("project-path")
                .short('p'),
        )
        .arg(
            Arg::new("identity")
                .takes_value(true)
                .long("identity")
                .short('i')
                .required(false),
        )
        .arg(Arg::new("name|address").takes_value(true).required(false))
        .after_help("Run `spacetime help publish` for more detailed information.")
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct InitDatabaseResponse {
    address: String,
}

pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
    let mut url_args = String::new();

    // Identity is required
    if let Some(identity) = args.value_of("identity") {
        url_args.push_str(format!("?identity={}", identity).as_str());
    } else {
        let identity_config = init_default(&mut config, None).await?.identity_config;
        url_args.push_str(format!("?identity={}", identity_config.identity).as_str());
    }

    let name_or_address = args.value_of("name|address");
    if let Some(name_or_address) = name_or_address {
        url_args.push_str(format!("&name_or_address={}", name_or_address).as_str());
    }

    let path_to_project_str = args.value_of("path_to_project").unwrap();
    if path_to_project_str.trim().is_empty() {
        return Err(anyhow::anyhow!("Project path is required!"));
    }
    let path_to_project = Path::new(path_to_project_str);
    if !path_to_project.exists() {
        return Err(anyhow::anyhow!("Project path does not exist: {}", path_to_project_str));
    }

    if args.is_present("clear_database") {
        url_args.push_str("&clear=true");
    }

    url_args.push_str(format!("&host_type={}", args.value_of("host_type").unwrap_or("wasmer")).as_str());

    let mut context = Context::new();
    duckscriptsdk::load(&mut context.commands)?;
    context
        .variables
        .insert("PATH".to_string(), std::env::var("PATH").unwrap());
    context
        .variables
        .insert("PROJECT_PATH".to_string(), path_to_project_str.to_string());

    match util::invoke_duckscript(include_str!("project/pre-publish.duck"), context) {
        Ok(ok) => {
            let mut error = false;
            for entry in ok.state {
                if let StateValue::SubState(sub_state) = entry.1 {
                    for entry in sub_state {
                        match entry.1 {
                            StateValue::String(a) => {
                                error = true;
                                println!("{}|{}", entry.0, a)
                            }
                            _ => {}
                        }
                    }
                }
            }

            if !error {
                println!("Publish finished successfully.");
            } else {
                println!("Publish finished with errors, check the console for more information.");
            }
        }
        Err(e) => {
            println!("Script execution error: {}", e);
        }
    }

    let path_to_wasm = util::find_wasm_file(path_to_project)?;
    let path_to_wasm = path_to_wasm.to_str().unwrap();
    let program_bytes = fs::read(fs::canonicalize(path_to_wasm).unwrap())?;

    let url = format!("http://{}/database/publish{}", config.host, url_args);
    let client = reqwest::Client::new();
    let res = client.post(url).body(program_bytes).send().await?;
    let res = res.error_for_status()?;
    let bytes = res.bytes().await.unwrap();

    let response: InitDatabaseResponse = serde_json::from_slice(&bytes[..]).unwrap();
    println!("Created new database with address: {}", response.address);

    Ok(())
}