added recipe_dir (#2543)

This commit is contained in:
Lifei Zhou
2025-05-16 18:04:38 +10:00
committed by GitHub
parent b52af12910
commit a60cef233b
6 changed files with 174 additions and 66 deletions
Generated
+60 -6
View File
@@ -2110,7 +2110,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"rustix", "rustix 0.38.44",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@@ -2123,6 +2123,18 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "filetime"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.0" version = "1.1.0"
@@ -2611,6 +2623,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"shlex", "shlex",
"tar",
"temp-env", "temp-env",
"tempfile", "tempfile",
"test-case", "test-case",
@@ -3649,7 +3662,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.48.5", "windows-targets 0.52.6",
] ]
[[package]] [[package]]
@@ -3660,6 +3673,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"libc", "libc",
"redox_syscall",
] ]
[[package]] [[package]]
@@ -3686,6 +3700,12 @@ version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.7.5" version = "0.7.5"
@@ -5209,7 +5229,20 @@ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
]
[[package]]
name = "rustix"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags 2.9.0",
"errno",
"libc",
"linux-raw-sys 0.9.4",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -5945,6 +5978,17 @@ dependencies = [
"version-compare", "version-compare",
] ]
[[package]]
name = "tar"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.12.16" version = "0.12.16"
@@ -5971,7 +6015,7 @@ dependencies = [
"fastrand", "fastrand",
"getrandom 0.3.1", "getrandom 0.3.1",
"once_cell", "once_cell",
"rustix", "rustix 0.38.44",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -5990,7 +6034,7 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9"
dependencies = [ dependencies = [
"rustix", "rustix 0.38.44",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -7061,7 +7105,7 @@ dependencies = [
"either", "either",
"home", "home",
"once_cell", "once_cell",
"rustix", "rustix 0.38.44",
] ]
[[package]] [[package]]
@@ -7535,6 +7579,16 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "xattr"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e"
dependencies = [
"libc",
"rustix 1.0.7",
]
[[package]] [[package]]
name = "xcap" name = "xcap"
version = "0.0.14" version = "0.0.14"
+1
View File
@@ -54,6 +54,7 @@ base64 = "0.22.1"
regex = "1.11.1" regex = "1.11.1"
minijinja = "2.8.0" minijinja = "2.8.0"
nix = { version = "0.30.1", features = ["process", "signal"] } nix = { version = "0.30.1", features = ["process", "signal"] }
tar = "0.4"
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["wincred"] } winapi = { version = "0.3", features = ["wincred"] }
+56 -33
View File
@@ -1,14 +1,18 @@
use anyhow::Result; use anyhow::Result;
use console::style;
use std::env; use std::env;
use std::fs;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use std::process::Stdio;
use tar::Archive;
pub const GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY: &str = "GOOSE_RECIPE_GITHUB_REPO"; pub const GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY: &str = "GOOSE_RECIPE_GITHUB_REPO";
pub fn retrieve_recipe_from_github( pub fn retrieve_recipe_from_github(
recipe_name: &str, recipe_name: &str,
recipe_repo_full_name: &str, recipe_repo_full_name: &str,
) -> Result<String> { ) -> Result<(String, PathBuf)> {
println!( println!(
"retrieving recipe from github repo {}", "retrieving recipe from github repo {}",
recipe_repo_full_name recipe_repo_full_name
@@ -16,19 +20,22 @@ pub fn retrieve_recipe_from_github(
ensure_gh_authenticated()?; ensure_gh_authenticated()?;
let local_repo_path = ensure_repo_cloned(recipe_repo_full_name)?; let local_repo_path = ensure_repo_cloned(recipe_repo_full_name)?;
fetch_origin(&local_repo_path)?; fetch_origin(&local_repo_path)?;
let download_dir = get_folder_from_github(&local_repo_path, recipe_name)?;
let file_extensions = ["yaml", "json"]; let file_extensions = ["yaml", "json"];
for ext in file_extensions { for ext in file_extensions {
let file_path_in_repo = format!("{}/recipe.{}", recipe_name, ext); let candidate_file_path = download_dir.join(format!("recipe.{}", ext));
match get_file_content_from_github(&local_repo_path, &file_path_in_repo) { if candidate_file_path.exists() {
Ok(content) => { let content = std::fs::read_to_string(&candidate_file_path)?;
println!( println!(
"retrieved recipe from github repo {}/{}", "retrieved recipe from github repo {}/{}",
recipe_repo_full_name, file_path_in_repo recipe_repo_full_name,
); candidate_file_path
return Ok(content); .strip_prefix(&download_dir)
} .unwrap()
Err(_) => continue, .display()
);
return Ok((content, download_dir));
} }
} }
Err(anyhow::anyhow!( Err(anyhow::anyhow!(
@@ -38,28 +45,6 @@ pub fn retrieve_recipe_from_github(
)) ))
} }
pub fn get_file_content_from_github(
local_repo_path: &Path,
file_path_in_repo: &str,
) -> Result<String> {
let ref_and_path = format!("origin/main:{}", file_path_in_repo);
let error_message: String = format!(
"Failed to get content from {} in github repo",
file_path_in_repo
);
let output = Command::new("git")
.args(["show", &ref_and_path])
.current_dir(local_repo_path)
.output()
.map_err(|_: std::io::Error| anyhow::anyhow!(error_message.clone()))?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
} else {
Err(anyhow::anyhow!(error_message.clone()))
}
}
fn ensure_gh_authenticated() -> Result<()> { fn ensure_gh_authenticated() -> Result<()> {
// Check authentication status // Check authentication status
let status = Command::new("gh") let status = Command::new("gh")
@@ -129,3 +114,41 @@ fn fetch_origin(local_repo_path: &Path) -> Result<()> {
Err(anyhow::anyhow!(error_message)) Err(anyhow::anyhow!(error_message))
} }
} }
fn get_folder_from_github(local_repo_path: &Path, recipe_name: &str) -> Result<PathBuf> {
let ref_and_path = format!("origin/main:{}", recipe_name);
let output_dir = env::temp_dir().join(recipe_name);
if output_dir.exists() {
fs::remove_dir_all(&output_dir)?;
}
fs::create_dir_all(&output_dir)?;
let archive_output = Command::new("git")
.args(["archive", &ref_and_path])
.current_dir(local_repo_path)
.stdout(Stdio::piped())
.spawn()?;
let stdout = archive_output
.stdout
.ok_or_else(|| anyhow::anyhow!("Failed to capture stdout from git archive"))?;
let mut archive = Archive::new(stdout);
archive.unpack(&output_dir)?;
list_files(&output_dir)?;
Ok(output_dir)
}
fn list_files(dir: &Path) -> Result<()> {
println!("{}", style("Files downloaded from github:").bold());
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
println!(" - {}", path.display());
}
}
Ok(())
}
+14 -3
View File
@@ -3,6 +3,8 @@ use std::collections::HashMap;
use console::style; use console::style;
use goose::recipe::Recipe; use goose::recipe::Recipe;
use crate::recipes::recipe::BUILT_IN_RECIPE_DIR_PARAM;
pub fn print_recipe_explanation(recipe: &Recipe) { pub fn print_recipe_explanation(recipe: &Recipe) {
println!( println!(
"{} {}", "{} {}",
@@ -33,6 +35,17 @@ pub fn print_recipe_explanation(recipe: &Recipe) {
} }
} }
pub fn print_parameters_with_values(params: HashMap<String, String>) {
for (key, value) in params {
let label = if key == BUILT_IN_RECIPE_DIR_PARAM {
" (built-in)"
} else {
""
};
println!(" {}{}: {}", key, label, value);
}
}
pub fn print_required_parameters_for_template( pub fn print_required_parameters_for_template(
params_for_template: HashMap<String, String>, params_for_template: HashMap<String, String>,
missing_params: Vec<String>, missing_params: Vec<String>,
@@ -42,9 +55,7 @@ pub fn print_required_parameters_for_template(
"{}", "{}",
style("📥 Parameters used to load this recipe:").bold() style("📥 Parameters used to load this recipe:").bold()
); );
for (key, value) in params_for_template { print_parameters_with_values(params_for_template)
println!(" {}: {}", key, value);
}
} }
if !missing_params.is_empty() { if !missing_params.is_empty() {
println!( println!(
+19 -10
View File
@@ -2,7 +2,7 @@ use anyhow::Result;
use console::style; use console::style;
use crate::recipes::print_recipe::{ use crate::recipes::print_recipe::{
missing_parameters_command_line, print_recipe_explanation, missing_parameters_command_line, print_parameters_with_values, print_recipe_explanation,
print_required_parameters_for_template, print_required_parameters_for_template,
}; };
use crate::recipes::search_recipe::retrieve_recipe_file; use crate::recipes::search_recipe::retrieve_recipe_file;
@@ -11,7 +11,9 @@ use minijinja::{Environment, Error, Template, UndefinedBehavior};
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use serde_yaml::Value as YamlValue; use serde_yaml::Value as YamlValue;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
pub const BUILT_IN_RECIPE_DIR_PARAM: &str = "recipe_dir";
/// Loads, validates a recipe from a YAML or JSON file, and renders it with the given parameters /// Loads, validates a recipe from a YAML or JSON file, and renders it with the given parameters
/// ///
/// # Arguments /// # Arguments
@@ -29,12 +31,12 @@ use std::collections::{HashMap, HashSet};
/// - Recipe is not valid /// - Recipe is not valid
/// - The required fields are missing /// - The required fields are missing
pub fn load_recipe_as_template(recipe_name: &str, params: Vec<(String, String)>) -> Result<Recipe> { pub fn load_recipe_as_template(recipe_name: &str, params: Vec<(String, String)>) -> Result<Recipe> {
let recipe_file_content = retrieve_recipe_file(recipe_name)?; let (recipe_file_content, recipe_parent_dir) = retrieve_recipe_file(recipe_name)?;
let recipe = validate_recipe_file_parameters(&recipe_file_content)?; let recipe = validate_recipe_file_parameters(&recipe_file_content)?;
let (params_for_template, missing_params) = let (params_for_template, missing_params) =
apply_values_to_parameters(&params, recipe.parameters, true)?; apply_values_to_parameters(&params, recipe.parameters, recipe_parent_dir, true)?;
if !missing_params.is_empty() { if !missing_params.is_empty() {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"Please provide the following parameters in the command line: {}", "Please provide the following parameters in the command line: {}",
@@ -56,9 +58,7 @@ pub fn load_recipe_as_template(recipe_name: &str, params: Vec<(String, String)>)
if !params_for_template.is_empty() { if !params_for_template.is_empty() {
println!("{}", style("Parameters used to load this recipe:").bold()); println!("{}", style("Parameters used to load this recipe:").bold());
for (key, value) in params_for_template { print_parameters_with_values(params_for_template);
println!("{}: {}", key, value);
}
} }
println!(); println!();
Ok(recipe) Ok(recipe)
@@ -83,7 +83,7 @@ pub fn load_recipe_as_template(recipe_name: &str, params: Vec<(String, String)>)
/// - The YAML/JSON is invalid /// - The YAML/JSON is invalid
/// - The parameter definition does not match the template variables in the recipe file /// - The parameter definition does not match the template variables in the recipe file
pub fn load_recipe(recipe_name: &str) -> Result<Recipe> { pub fn load_recipe(recipe_name: &str) -> Result<Recipe> {
let recipe_file_content = retrieve_recipe_file(recipe_name)?; let (recipe_file_content, _) = retrieve_recipe_file(recipe_name)?;
validate_recipe_file_parameters(&recipe_file_content) validate_recipe_file_parameters(&recipe_file_content)
} }
@@ -92,13 +92,13 @@ pub fn explain_recipe_with_parameters(
recipe_name: &str, recipe_name: &str,
params: Vec<(String, String)>, params: Vec<(String, String)>,
) -> Result<()> { ) -> Result<()> {
let recipe_file_content = retrieve_recipe_file(recipe_name)?; let (recipe_file_content, recipe_parent_dir) = retrieve_recipe_file(recipe_name)?;
let raw_recipe = validate_recipe_file_parameters(&recipe_file_content)?; let raw_recipe = validate_recipe_file_parameters(&recipe_file_content)?;
print_recipe_explanation(&raw_recipe); print_recipe_explanation(&raw_recipe);
let recipe_parameters = raw_recipe.parameters; let recipe_parameters = raw_recipe.parameters;
let (params_for_template, missing_params) = let (params_for_template, missing_params) =
apply_values_to_parameters(&params, recipe_parameters, false)?; apply_values_to_parameters(&params, recipe_parameters, recipe_parent_dir, false)?;
print_required_parameters_for_template(params_for_template, missing_params); print_required_parameters_for_template(params_for_template, missing_params);
Ok(()) Ok(())
@@ -115,7 +115,8 @@ fn validate_parameters_in_template(
recipe_parameters: &Option<Vec<RecipeParameter>>, recipe_parameters: &Option<Vec<RecipeParameter>>,
recipe_file_content: &str, recipe_file_content: &str,
) -> Result<()> { ) -> Result<()> {
let template_variables = extract_template_variables(recipe_file_content)?; let mut template_variables = extract_template_variables(recipe_file_content)?;
template_variables.remove(BUILT_IN_RECIPE_DIR_PARAM);
let param_keys: HashSet<String> = recipe_parameters let param_keys: HashSet<String> = recipe_parameters
.as_ref() .as_ref()
@@ -207,9 +208,17 @@ fn extract_template_variables(template_str: &str) -> Result<HashSet<String>> {
fn apply_values_to_parameters( fn apply_values_to_parameters(
user_params: &[(String, String)], user_params: &[(String, String)],
recipe_parameters: Option<Vec<RecipeParameter>>, recipe_parameters: Option<Vec<RecipeParameter>>,
recipe_parent_dir: PathBuf,
enable_user_prompt: bool, enable_user_prompt: bool,
) -> Result<(HashMap<String, String>, Vec<String>)> { ) -> Result<(HashMap<String, String>, Vec<String>)> {
let mut param_map: HashMap<String, String> = user_params.iter().cloned().collect(); let mut param_map: HashMap<String, String> = user_params.iter().cloned().collect();
let recipe_parent_dir_str = recipe_parent_dir
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid UTF-8 in recipe_dir"))?;
param_map.insert(
BUILT_IN_RECIPE_DIR_PARAM.to_string(),
recipe_parent_dir_str.to_string(),
);
let mut missing_params: Vec<String> = Vec::new(); let mut missing_params: Vec<String> = Vec::new();
for param in recipe_parameters.unwrap_or_default() { for param in recipe_parameters.unwrap_or_default() {
if !param_map.contains_key(&param.key) { if !param_map.contains_key(&param.key) {
+24 -14
View File
@@ -1,11 +1,11 @@
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Result};
use goose::config::Config; use goose::config::Config;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use super::github_recipe::{retrieve_recipe_from_github, GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY}; use super::github_recipe::{retrieve_recipe_from_github, GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY};
pub fn retrieve_recipe_file(recipe_name: &str) -> Result<String> { 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 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") || recipe_name.ends_with(".json") {
let path = PathBuf::from(recipe_name); let path = PathBuf::from(recipe_name);
@@ -14,8 +14,8 @@ pub fn retrieve_recipe_file(recipe_name: &str) -> Result<String> {
// First check current directory // First check current directory
let current_dir = std::env::current_dir()?; let current_dir = std::env::current_dir()?;
if let Ok(content) = read_recipe_in_dir(&current_dir, recipe_name) { if let Ok((content, recipe_parent_dir)) = read_recipe_in_dir(&current_dir, recipe_name) {
return Ok(content); return Ok((content, recipe_parent_dir));
} }
read_recipe_in_dir(&current_dir, recipe_name).or_else(|e| { read_recipe_in_dir(&current_dir, recipe_name).or_else(|e| {
if let Some(recipe_repo_full_name) = configured_github_recipe_repo() { if let Some(recipe_repo_full_name) = configured_github_recipe_repo() {
@@ -34,23 +34,33 @@ fn configured_github_recipe_repo() -> Option<String> {
} }
} }
fn read_recipe_file<P: AsRef<Path>>(recipe_path: P) -> Result<String> { fn read_recipe_file<P: AsRef<Path>>(recipe_path: P) -> Result<(String, PathBuf)> {
let path = recipe_path.as_ref(); let path = recipe_path.as_ref();
if path.exists() { let content = fs::read_to_string(path)
let content = fs::read_to_string(path) .map_err(|e| anyhow!("Failed to read recipe file {}: {}", path.display(), e))?;
.with_context(|| format!("Failed to read recipe file: {}", path.display()))?;
Ok(content) let canonical = path.canonicalize().map_err(|e| {
} else { anyhow!(
Err(anyhow!("Recipe file not found: {}", path.display())) "Failed to resolve absolute path for {}: {}",
} path.display(),
e
)
})?;
let parent_dir = canonical
.parent()
.ok_or_else(|| anyhow!("Resolved path has no parent: {}", canonical.display()))?
.to_path_buf();
Ok((content, parent_dir))
} }
fn read_recipe_in_dir(dir: &Path, recipe_name: &str) -> Result<String> { fn read_recipe_in_dir(dir: &Path, recipe_name: &str) -> Result<(String, PathBuf)> {
for ext in &["yaml", "json"] { for ext in &["yaml", "json"] {
let recipe_path = dir.join(format!("{}.{}", recipe_name, ext)); let recipe_path = dir.join(format!("{}.{}", recipe_name, ext));
match read_recipe_file(recipe_path) { match read_recipe_file(recipe_path) {
Ok(content) => return Ok(content), Ok((content, recipe_parent_dir)) => return Ok((content, recipe_parent_dir)),
Err(_) => continue, Err(_) => continue,
} }
} }