use std::{net::SocketAddr, path::PathBuf}; use clap::{FromArgMatches, Parser, Subcommand, crate_authors, crate_description, crate_version}; use colored_json::ToColoredJson; use kdl::KdlDocument; use rusty_s3::{Bucket, Credentials, S3Action}; use crate::{ model::{Context, Plan, Project}, plan_reconciler::PlanReconciler, state::SharedState, }; mod run; mod template; #[derive(Subcommand)] enum Commands { Init {}, Template(template::Template), Info {}, Serve { #[arg(env = "FOREST_HOST", long, default_value = "127.0.0.1:3000")] host: SocketAddr, #[arg(env = "FOREST_S3_ENDPOINT", long = "s3-endpoint")] s3_endpoint: String, #[arg(env = "FOREST_S3_REGION", long = "s3-region")] s3_region: String, #[arg(env = "FOREST_S3_BUCKET", long = "s3-bucket")] s3_bucket: String, #[arg(env = "FOREST_S3_USER", long = "s3-user")] s3_user: String, #[arg(env = "FOREST_S3_PASSWORD", long = "s3-password")] s3_password: String, }, } fn get_root(include_run: bool) -> clap::Command { let mut root_cmd = clap::Command::new("forest") .subcommand_required(true) .author(crate_authors!()) .version(crate_version!()) .about(crate_description!()) .arg( clap::Arg::new("project_path") .long("project-path") .env("FOREST_PROJECT_PATH") .default_value("."), ); if include_run { root_cmd = root_cmd .subcommand(clap::Command::new("run").allow_external_subcommands(true)) .ignore_errors(true); } Commands::augment_subcommands(root_cmd) } pub async fn execute() -> anyhow::Result<()> { let matches = get_root(true).get_matches(); let project_path = PathBuf::from( &matches .get_one::("project_path") .expect("project path always to be set"), ) .canonicalize()?; let project_file_path = project_path.join("forest.kdl"); if !project_file_path.exists() { anyhow::bail!( "no 'forest.kdl' file was found at: {}", project_file_path.display().to_string() ); } let project_file = tokio::fs::read_to_string(&project_file_path).await?; let project_doc: KdlDocument = project_file.parse()?; let project: Project = project_doc.try_into()?; tracing::trace!("found a project name: {}", project.name); let plan = if let Some(plan_file_path) = PlanReconciler::new() .reconcile(&project, &project_path) .await? { let plan_file = tokio::fs::read_to_string(&plan_file_path).await?; let plan_doc: KdlDocument = plan_file.parse()?; let plan: Plan = plan_doc.try_into()?; tracing::trace!("found a plan name: {}", project.name); Some(plan) } else { None }; let context = Context { project, plan }; let matches = if matches.subcommand_matches("run").is_some() { tracing::debug!("run is called, building extra commands, rerunning the parser"); let root = get_root(false); let root = run::Run::augment_command(root, &context); root.get_matches() } else { matches }; match matches.subcommand().unwrap() { ("run", args) => { run::Run::execute(args, &project_path, &context).await?; } _ => match Commands::from_arg_matches(&matches).unwrap() { Commands::Init {} => { tracing::info!("initializing project"); tracing::trace!("found context: {:?}", context); } Commands::Info {} => { let output = serde_json::to_string_pretty(&context)?; println!("{}", output.to_colored_json_auto().unwrap_or(output)); } Commands::Template(template) => { template.execute(&project_path, &context).await?; } Commands::Serve { s3_endpoint, s3_bucket, s3_region, s3_user, s3_password, .. } => { tracing::info!("Starting server"); let creds = Credentials::new(s3_user, s3_password); let bucket = Bucket::new( url::Url::parse(&s3_endpoint)?, rusty_s3::UrlStyle::Path, s3_bucket, s3_region, )?; let put_object = bucket.put_object(Some(&creds), "some-object"); let _url = put_object.sign(std::time::Duration::from_secs(30)); let _state = SharedState::new().await?; } }, } Ok(()) }