From 41c67eb2c374a0c2587e1d55286b2b172a37fba8 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Mon, 19 May 2025 16:57:45 +1000 Subject: [PATCH] feat: discover recipe in the paths from environment variable GOOSE_RECIPE_PATH (#2561) --- crates/goose-cli/src/recipes/github_recipe.rs | 11 +-- crates/goose-cli/src/recipes/recipe.rs | 1 + crates/goose-cli/src/recipes/search_recipe.rs | 78 +++++++++++++------ 3 files changed, 61 insertions(+), 29 deletions(-) diff --git a/crates/goose-cli/src/recipes/github_recipe.rs b/crates/goose-cli/src/recipes/github_recipe.rs index 3f0c0ec657..0dc0fb5220 100644 --- a/crates/goose-cli/src/recipes/github_recipe.rs +++ b/crates/goose-cli/src/recipes/github_recipe.rs @@ -8,27 +8,28 @@ use std::process::Command; use std::process::Stdio; use tar::Archive; +use crate::recipes::recipe::RECIPE_FILE_EXTENSIONS; + pub const GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY: &str = "GOOSE_RECIPE_GITHUB_REPO"; pub fn retrieve_recipe_from_github( recipe_name: &str, recipe_repo_full_name: &str, ) -> Result<(String, PathBuf)> { println!( - "retrieving recipe from github repo {}", - recipe_repo_full_name + "📦 Looking for recipe \"{}\" in github repo: {}", + recipe_name, recipe_repo_full_name ); ensure_gh_authenticated()?; let local_repo_path = ensure_repo_cloned(recipe_repo_full_name)?; fetch_origin(&local_repo_path)?; let download_dir = get_folder_from_github(&local_repo_path, recipe_name)?; - let file_extensions = ["yaml", "json"]; - for ext in file_extensions { + for ext in RECIPE_FILE_EXTENSIONS { let candidate_file_path = download_dir.join(format!("recipe.{}", ext)); if candidate_file_path.exists() { let content = std::fs::read_to_string(&candidate_file_path)?; println!( - "retrieved recipe from github repo {}/{}", + "⬇️ Retrieved recipe from github repo {}/{}", recipe_repo_full_name, candidate_file_path .strip_prefix(&download_dir) diff --git a/crates/goose-cli/src/recipes/recipe.rs b/crates/goose-cli/src/recipes/recipe.rs index 5d4fe61cdc..6b9f8e8f48 100644 --- a/crates/goose-cli/src/recipes/recipe.rs +++ b/crates/goose-cli/src/recipes/recipe.rs @@ -14,6 +14,7 @@ use std::collections::{HashMap, HashSet}; use std::path::PathBuf; pub const BUILT_IN_RECIPE_DIR_PARAM: &str = "recipe_dir"; +pub const RECIPE_FILE_EXTENSIONS: &[&str] = &["yaml", "json"]; /// Loads, validates a recipe from a YAML or JSON file, and renders it with the given parameters /// /// # Arguments diff --git a/crates/goose-cli/src/recipes/search_recipe.rs b/crates/goose-cli/src/recipes/search_recipe.rs index 4280f1e93e..398e5a0167 100644 --- a/crates/goose-cli/src/recipes/search_recipe.rs +++ b/crates/goose-cli/src/recipes/search_recipe.rs @@ -1,23 +1,24 @@ use anyhow::{anyhow, Result}; use goose::config::Config; -use std::fs; use std::path::{Path, PathBuf}; +use std::{env, fs}; + +use crate::recipes::recipe::RECIPE_FILE_EXTENSIONS; use super::github_recipe::{retrieve_recipe_from_github, GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY}; +const GOOSE_RECIPE_PATH_ENV_VAR: &str = "GOOSE_RECIPE_PATH"; + pub fn retrieve_recipe_file(recipe_name: &str) -> Result<(String, PathBuf)> { - // If recipe_name ends with yaml or json, treat it as a direct path - if recipe_name.ends_with(".yaml") || recipe_name.ends_with(".json") { + // If recipe_name ends with yaml or json, treat it as a direct file path + if RECIPE_FILE_EXTENSIONS + .iter() + .any(|ext| recipe_name.ends_with(&format!(".{}", ext))) + { let path = PathBuf::from(recipe_name); return read_recipe_file(path); } - - // First check current directory - let current_dir = std::env::current_dir()?; - if let Ok((content, recipe_parent_dir)) = read_recipe_in_dir(¤t_dir, recipe_name) { - return Ok((content, recipe_parent_dir)); - } - read_recipe_in_dir(¤t_dir, recipe_name).or_else(|e| { + retrieve_recipe_from_local_path(recipe_name).or_else(|e| { if let Some(recipe_repo_full_name) = configured_github_recipe_repo() { retrieve_recipe_from_github(recipe_name, &recipe_repo_full_name) } else { @@ -26,6 +27,49 @@ pub fn retrieve_recipe_file(recipe_name: &str) -> Result<(String, PathBuf)> { }) } +fn read_recipe_in_dir(dir: &Path, recipe_name: &str) -> Result<(String, PathBuf)> { + for ext in RECIPE_FILE_EXTENSIONS { + let recipe_path = dir.join(format!("{}.{}", recipe_name, ext)); + if let Ok(result) = read_recipe_file(recipe_path) { + return Ok(result); + } + } + Err(anyhow!(format!( + "No {}.yaml or {}.json recipe file found in directory: {}", + recipe_name, + recipe_name, + dir.display() + ))) +} + +fn retrieve_recipe_from_local_path(recipe_name: &str) -> Result<(String, PathBuf)> { + let mut search_dirs = vec![PathBuf::from(".")]; + if let Ok(recipe_path_env) = env::var(GOOSE_RECIPE_PATH_ENV_VAR) { + let path_separator = if cfg!(windows) { ';' } else { ':' }; + let recipe_path_env_dirs: Vec = recipe_path_env + .split(path_separator) + .map(PathBuf::from) + .collect(); + search_dirs.extend(recipe_path_env_dirs); + } + for dir in &search_dirs { + if let Ok(result) = read_recipe_in_dir(dir, recipe_name) { + return Ok(result); + } + } + let search_dirs_str = search_dirs + .iter() + .map(|p| p.to_string_lossy()) + .collect::>() + .join(":"); + Err(anyhow!( + "ℹ️ Failed to retrieve {}.yaml or {}.json in {}", + recipe_name, + recipe_name, + search_dirs_str + )) +} + fn configured_github_recipe_repo() -> Option { let config = Config::global(); match config.get_param(GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY) { @@ -55,17 +99,3 @@ fn read_recipe_file>(recipe_path: P) -> Result<(String, PathBuf)> Ok((content, parent_dir)) } - -fn read_recipe_in_dir(dir: &Path, recipe_name: &str) -> Result<(String, PathBuf)> { - for ext in &["yaml", "json"] { - let recipe_path = dir.join(format!("{}.{}", recipe_name, ext)); - match read_recipe_file(recipe_path) { - Ok((content, recipe_parent_dir)) => return Ok((content, recipe_parent_dir)), - Err(_) => continue, - } - } - Err(anyhow!(format!( - "No {}.yaml or {}.json recipe file found in current directory.", - recipe_name, recipe_name - ))) -}