diff --git a/Cargo.lock b/Cargo.lock index d78f5eb..af73036 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -236,6 +236,7 @@ dependencies = [ "anyhow", "clap", "dotenvy", + "kdl", "rusty-s3", "serde", "tokio", @@ -448,6 +449,18 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "kdl" +version = "6.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "412e2cf22cb560469db5b211c594ff9dcd490c6964e284ea64eddffe41c2249c" +dependencies = [ + "miette", + "num", + "thiserror", + "winnow", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -498,6 +511,29 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "miette" +version = "7.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484" +dependencies = [ + "cfg-if", + "miette-derive", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "7.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf45bf44ab49be92fd1227a3be6fc6f617f1a337c06af54981048574d8783147" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "miniz_oxide" version = "0.8.3" @@ -528,12 +564,85 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.7" @@ -787,6 +896,26 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -937,6 +1066,12 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "url" version = "2.5.4" @@ -1106,6 +1241,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.33.0" diff --git a/crates/forest/Cargo.toml b/crates/forest/Cargo.toml index 499db8b..0122329 100644 --- a/crates/forest/Cargo.toml +++ b/crates/forest/Cargo.toml @@ -15,3 +15,4 @@ uuid.workspace = true rusty-s3 = "0.7.0" url = "2.5.4" +kdl = "6.3.3" diff --git a/crates/forest/src/cli.rs b/crates/forest/src/cli.rs index 5e7a3c8..b481865 100644 --- a/crates/forest/src/cli.rs +++ b/crates/forest/src/cli.rs @@ -1,6 +1,7 @@ -use std::net::SocketAddr; +use std::{net::SocketAddr, path::PathBuf}; use clap::{Parser, Subcommand}; +use kdl::{KdlDocument, KdlNode, KdlValue}; use rusty_s3::{Bucket, Credentials, S3Action}; use crate::state::SharedState; @@ -14,6 +15,15 @@ struct Command { #[derive(Subcommand)] enum Commands { + Init { + #[arg( + env = "FOREST_PROJECT_PATH", + long = "project-path", + default_value = "." + )] + project_path: PathBuf, + }, + Serve { #[arg(env = "FOREST_HOST", long, default_value = "127.0.0.1:3000")] host: SocketAddr, @@ -35,32 +45,124 @@ enum Commands { }, } +#[derive(Debug, Clone)] +pub enum ProjectPlan { + Local { path: PathBuf }, + NoPlan, +} + +impl TryFrom<&KdlNode> for ProjectPlan { + type Error = anyhow::Error; + + fn try_from(value: &KdlNode) -> Result { + let Some(children) = value.children() else { + return Ok(Self::NoPlan); + }; + + if let Some(local) = children.get_arg("local") { + return Ok(Self::Local { + path: local + .as_string() + .map(|l| l.to_string()) + .ok_or(anyhow::anyhow!("local must have an arg with a valid path"))? + .into(), + }); + } + + Ok(Self::NoPlan) + } +} + +#[derive(Debug, Clone)] +pub struct Project { + name: String, + description: Option, + plan: Option, +} + +impl TryFrom for Project { + type Error = anyhow::Error; + + fn try_from(value: KdlDocument) -> Result { + let project_section = value.get("project").ok_or(anyhow::anyhow!( + "forest.kdl project file must have a project object" + ))?; + + let project_children = project_section + .children() + .ok_or(anyhow::anyhow!("a forest project must have children"))?; + + let project_plan: Option = if let Some(project) = project_children.get("plan") + { + Some(project.try_into()?) + } else { + None + }; + + Ok(Self { + name: project_children + .get_arg("name") + .and_then(|n| match n { + KdlValue::String(s) => Some(s), + _ => None, + }) + .cloned() + .ok_or(anyhow::anyhow!("a forest kuddle project must have a name"))?, + description: project_children + .get_arg("description") + .and_then(|n| match n { + KdlValue::String(s) => Some(s.trim().to_string()), + _ => None, + }), + plan: project_plan, + }) + } +} + pub async fn execute() -> anyhow::Result<()> { let cli = Command::parse(); - if let Some(Commands::Serve { - host, - s3_endpoint, - s3_bucket, - s3_region, - s3_user, - s3_password, - }) = cli.command - { - tracing::info!("Starting server"); + match cli.command.unwrap() { + Commands::Init { project_path } => { + tracing::info!("initializing project"); - let creds = Credentials::new(s3_user, s3_password); - let bucket = Bucket::new( - url::Url::parse(&s3_endpoint)?, - rusty_s3::UrlStyle::Path, + 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, project); + } + + Commands::Serve { + host, + s3_endpoint, 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?; + 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(()) diff --git a/examples/project/forest.kdl b/examples/project/forest.kdl new file mode 100644 index 0000000..5a5b862 --- /dev/null +++ b/examples/project/forest.kdl @@ -0,0 +1,10 @@ +project { + name local + description """ + A simple local project that depends on ../plan for its utility scripts + """ + + plan { + local "../plan" + } +} diff --git a/todos/hyperlog/graph.json b/todos/hyperlog/graph.json index 5f25646..3298afe 100644 --- a/todos/hyperlog/graph.json +++ b/todos/hyperlog/graph.json @@ -10,10 +10,25 @@ "state": "not-done" }, "projects": { - "type": "item", - "title": "projects", - "description": "", - "state": "not-done" + "type": "section", + "should be able to download a remote plan": { + "type": "item", + "title": "should be able to download a remote plan", + "description": "", + "state": "not-done" + }, + "should be able to template from a remote plan": { + "type": "item", + "title": "should be able to template from a remote plan", + "description": "", + "state": "not-done" + }, + "should be able to use scripts from a remote plan": { + "type": "item", + "title": "should be able to use scripts from a remote plan", + "description": "", + "state": "not-done" + } } } } diff --git a/todos/hyperlog/graph.lock b/todos/hyperlog/graph.lock new file mode 100644 index 0000000..016d04c --- /dev/null +++ b/todos/hyperlog/graph.lock @@ -0,0 +1 @@ +hyperlog-lock \ No newline at end of file