From 1fda414e05976c122102da745425a909c14fd708 Mon Sep 17 00:00:00 2001 From: kjuulh Date: Sun, 23 Mar 2025 21:52:29 +0100 Subject: [PATCH] feat: can execute all subcommands from workspace --- crates/forest/src/cli.rs | 146 ++++++++++++++++-- crates/forest/src/cli/run.rs | 94 ++++++++++- examples/workspace/projects/a/forest.kdl | 4 + .../workspace/projects/a/scripts/hello.sh | 7 + examples/workspace/projects/b/forest.kdl | 4 + .../workspace/projects/b/scripts/hello.sh | 5 + 6 files changed, 249 insertions(+), 11 deletions(-) create mode 100755 examples/workspace/projects/a/scripts/hello.sh create mode 100755 examples/workspace/projects/b/scripts/hello.sh diff --git a/crates/forest/src/cli.rs b/crates/forest/src/cli.rs index ba5fba7..fca2088 100644 --- a/crates/forest/src/cli.rs +++ b/crates/forest/src/cli.rs @@ -48,6 +48,7 @@ fn get_root(include_run: bool) -> clap::Command { .author(crate_authors!()) .version(crate_version!()) .about(crate_description!()) + .ignore_errors(include_run) .arg( clap::Arg::new("project_path") .long("project-path") @@ -56,9 +57,8 @@ fn get_root(include_run: bool) -> clap::Command { ); if include_run { - root_cmd = root_cmd.subcommand(clap::Command::new("run").allow_external_subcommands(true)); + root_cmd = root_cmd.subcommand(clap::Command::new("run").allow_external_subcommands(true)) } - Commands::augment_subcommands(root_cmd) } @@ -108,16 +108,145 @@ pub async fn execute() -> anyhow::Result<()> { &member.path ))?; - workspace_members.push(project); + workspace_members.push((workspace_member_path, project)); } - let output = serde_json::to_string_pretty(&workspace_members)?; - println!("{}", output.to_colored_json_auto().unwrap_or(output)); - // TODO: 1a (optional). Resolve dependencies // 2. Reconcile plans + let mut member_contexts = Vec::new(); + + for (member_path, member) in workspace_members { + match member { + WorkspaceProject::Plan(plan) => { + tracing::warn!("skipping reconcile for plans for now") + } + WorkspaceProject::Project(project) => { + 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 }; + member_contexts.push((member_path, context)); + } + } + } + + tracing::debug!("run is called, building extra commands, rerunning the parser"); + let mut run_cmd = clap::Command::new("run").subcommand_required(true); + // 3. Provide context and aggregated commands for projects + for (_, context) in &member_contexts { + let commands = run::Run::augment_workspace_command(context, &context.project.name); + run_cmd = run_cmd.subcommands(commands); + } + + run_cmd = + run_cmd.subcommand(clap::Command::new("all").allow_external_subcommands(true)); + + let mut root = get_root(false).subcommand(run_cmd); + let matches = root.get_matches_mut(); + + if matches.subcommand().is_none() { + root.print_help()?; + anyhow::bail!("failed to find command"); + } + + match matches + .subcommand() + .expect("forest requires a command to be passed") + { + ("run", args) => { + let (run_args, args) = args.subcommand().expect("run must have subcommands"); + + match run_args { + "all" => { + let (all_cmd, _args) = args + .subcommand() + .expect("to be able to get a subcommand (todo: might not work)"); + + for (member_path, context) in member_contexts { + run::Run::execute_command_if_exists( + all_cmd, + &member_path, + &context, + ) + .await?; + } + } + _ => { + let (project_name, command) = run_args + .split_once("::") + .expect("commands to always be pairs for workspaces"); + + let mut found_context = false; + for (member_path, context) in &member_contexts { + if project_name == context.project.name { + run::Run::execute_command(command, member_path, context) + .await?; + + found_context = true; + } + } + + if !found_context { + anyhow::bail!("no matching context was found") + } + } + } + } + _ => match Commands::from_arg_matches(&matches).unwrap() { + Commands::Init {} => { + tracing::info!("initializing project"); + } + Commands::Info {} => { + let output = serde_json::to_string_pretty(&member_contexts)?; + 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?; + } + Commands::Clean {} => { + todo!(); + // let forest_path = project_path.join(".forest"); + // if forest_path.exists() { + // tokio::fs::remove_dir_all(forest_path).await?; + // tracing::info!("removed .forest"); + // } + } + }, + } } ForestFile::Project(project) => { tracing::trace!("found a project name: {}", project.name); @@ -143,9 +272,8 @@ pub async fn execute() -> anyhow::Result<()> { 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() + let run_cmd = run::Run::augment_command(&context); + root.subcommand(run_cmd).get_matches() } else { matches }; diff --git a/crates/forest/src/cli/run.rs b/crates/forest/src/cli/run.rs index b60ce5e..369f336 100644 --- a/crates/forest/src/cli/run.rs +++ b/crates/forest/src/cli/run.rs @@ -7,7 +7,7 @@ use crate::{model::Context, script::ScriptExecutor}; // create a new sub command that encapsulates all the run complexities pub struct Run {} impl Run { - pub fn augment_command(root: clap::Command, ctx: &Context) -> clap::Command { + pub fn augment_command(ctx: &Context) -> clap::Command { let mut run_cmd = clap::Command::new("run") .subcommand_required(true) .about("runs any kind of script from either the project or plan"); @@ -37,7 +37,37 @@ impl Run { } } - root.subcommand(run_cmd) + run_cmd + } + + pub fn augment_workspace_command(ctx: &Context, prefix: &str) -> Vec { + let mut commands = Vec::new(); + if let Some(scripts) = &ctx.project.scripts { + for name in scripts.items.keys() { + let cmd = clap::Command::new(format!("{prefix}::{name}")); + commands.push(cmd); + } + } + + if let Some(plan) = &ctx.plan { + if let Some(scripts) = &plan.scripts { + let existing_cmds = commands + .iter() + .map(|s| format!("{prefix}::{}", s.get_name())) + .collect::>(); + + for name in scripts.items.keys() { + if existing_cmds.contains(name) { + continue; + } + + let cmd = clap::Command::new(format!("{prefix}::{name}")); + commands.push(cmd) + } + } + } + + commands } pub async fn execute( @@ -73,4 +103,64 @@ impl Run { anyhow::bail!("no scripts were found for command: {}", name) } + + pub async fn execute_command( + command: &str, + project_path: &Path, + ctx: &Context, + ) -> anyhow::Result<()> { + if let Some(scripts_ctx) = &ctx.project.scripts { + if let Some(script_ctx) = scripts_ctx.items.get(command) { + ScriptExecutor::new(project_path.into(), ctx.clone()) + .run(script_ctx, command) + .await?; + + return Ok(()); + } + } + + if let Some(plan) = &ctx.plan { + if let Some(scripts_ctx) = &plan.scripts { + if let Some(script_ctx) = scripts_ctx.items.get(command) { + ScriptExecutor::new(project_path.into(), ctx.clone()) + .run(script_ctx, command) + .await?; + + return Ok(()); + } + } + } + + anyhow::bail!("no scripts were found for command: {}", command) + } + + pub async fn execute_command_if_exists( + command: &str, + project_path: &Path, + ctx: &Context, + ) -> anyhow::Result<()> { + if let Some(scripts_ctx) = &ctx.project.scripts { + if let Some(script_ctx) = scripts_ctx.items.get(command) { + ScriptExecutor::new(project_path.into(), ctx.clone()) + .run(script_ctx, command) + .await?; + + return Ok(()); + } + } + + if let Some(plan) = &ctx.plan { + if let Some(scripts_ctx) = &plan.scripts { + if let Some(script_ctx) = scripts_ctx.items.get(command) { + ScriptExecutor::new(project_path.into(), ctx.clone()) + .run(script_ctx, command) + .await?; + + return Ok(()); + } + } + } + + Ok(()) + } } diff --git a/examples/workspace/projects/a/forest.kdl b/examples/workspace/projects/a/forest.kdl index 52e1cf8..46c4bf6 100644 --- a/examples/workspace/projects/a/forest.kdl +++ b/examples/workspace/projects/a/forest.kdl @@ -1,3 +1,7 @@ project { name a + + scripts { + hello type=shell {} + } } diff --git a/examples/workspace/projects/a/scripts/hello.sh b/examples/workspace/projects/a/scripts/hello.sh new file mode 100755 index 0000000..c439347 --- /dev/null +++ b/examples/workspace/projects/a/scripts/hello.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env zsh + +set -e + +echo "hello from a" + +echo "i am here: $PWD" diff --git a/examples/workspace/projects/b/forest.kdl b/examples/workspace/projects/b/forest.kdl index 005227c..5ea4bd8 100644 --- a/examples/workspace/projects/b/forest.kdl +++ b/examples/workspace/projects/b/forest.kdl @@ -1,3 +1,7 @@ project { name b + + scripts { + hello type=shell {} + } } diff --git a/examples/workspace/projects/b/scripts/hello.sh b/examples/workspace/projects/b/scripts/hello.sh new file mode 100755 index 0000000..7812b9a --- /dev/null +++ b/examples/workspace/projects/b/scripts/hello.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env zsh + +set -e + +echo "hello from b"