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

66
Cargo.lock generated
View File

@@ -2110,7 +2110,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947"
dependencies = [
"cfg-if",
"rustix",
"rustix 0.38.44",
"windows-sys 0.52.0",
]
@@ -2123,6 +2123,18 @@ dependencies = [
"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]]
name = "flate2"
version = "1.1.0"
@@ -2611,6 +2623,7 @@ dependencies = [
"serde_json",
"serde_yaml",
"shlex",
"tar",
"temp-env",
"tempfile",
"test-case",
@@ -3649,7 +3662,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -3660,6 +3673,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.9.0",
"libc",
"redox_syscall",
]
[[package]]
@@ -3686,6 +3700,12 @@ version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "litemap"
version = "0.7.5"
@@ -5209,7 +5229,20 @@ dependencies = [
"bitflags 2.9.0",
"errno",
"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",
]
@@ -5945,6 +5978,17 @@ dependencies = [
"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]]
name = "target-lexicon"
version = "0.12.16"
@@ -5971,7 +6015,7 @@ dependencies = [
"fastrand",
"getrandom 0.3.1",
"once_cell",
"rustix",
"rustix 0.38.44",
"windows-sys 0.59.0",
]
@@ -5990,7 +6034,7 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9"
dependencies = [
"rustix",
"rustix 0.38.44",
"windows-sys 0.59.0",
]
@@ -7061,7 +7105,7 @@ dependencies = [
"either",
"home",
"once_cell",
"rustix",
"rustix 0.38.44",
]
[[package]]
@@ -7535,6 +7579,16 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "xcap"
version = "0.0.14"

View File

@@ -54,6 +54,7 @@ base64 = "0.22.1"
regex = "1.11.1"
minijinja = "2.8.0"
nix = { version = "0.30.1", features = ["process", "signal"] }
tar = "0.4"
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["wincred"] }

View File

@@ -1,14 +1,18 @@
use anyhow::Result;
use console::style;
use std::env;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
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 fn retrieve_recipe_from_github(
recipe_name: &str,
recipe_repo_full_name: &str,
) -> Result<String> {
) -> Result<(String, PathBuf)> {
println!(
"retrieving recipe from github repo {}",
recipe_repo_full_name
@@ -16,19 +20,22 @@ pub fn retrieve_recipe_from_github(
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 {
let file_path_in_repo = format!("{}/recipe.{}", recipe_name, ext);
match get_file_content_from_github(&local_repo_path, &file_path_in_repo) {
Ok(content) => {
println!(
"retrieved recipe from github repo {}/{}",
recipe_repo_full_name, file_path_in_repo
);
return Ok(content);
}
Err(_) => continue,
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 {}/{}",
recipe_repo_full_name,
candidate_file_path
.strip_prefix(&download_dir)
.unwrap()
.display()
);
return Ok((content, download_dir));
}
}
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<()> {
// Check authentication status
let status = Command::new("gh")
@@ -129,3 +114,41 @@ fn fetch_origin(local_repo_path: &Path) -> Result<()> {
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(())
}

View File

@@ -3,6 +3,8 @@ use std::collections::HashMap;
use console::style;
use goose::recipe::Recipe;
use crate::recipes::recipe::BUILT_IN_RECIPE_DIR_PARAM;
pub fn print_recipe_explanation(recipe: &Recipe) {
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(
params_for_template: HashMap<String, String>,
missing_params: Vec<String>,
@@ -42,9 +55,7 @@ pub fn print_required_parameters_for_template(
"{}",
style("📥 Parameters used to load this recipe:").bold()
);
for (key, value) in params_for_template {
println!(" {}: {}", key, value);
}
print_parameters_with_values(params_for_template)
}
if !missing_params.is_empty() {
println!(

View File

@@ -2,7 +2,7 @@ use anyhow::Result;
use console::style;
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,
};
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_yaml::Value as YamlValue;
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
///
/// # Arguments
@@ -29,12 +31,12 @@ use std::collections::{HashMap, HashSet};
/// - Recipe is not valid
/// - The required fields are missing
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 (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() {
return Err(anyhow::anyhow!(
"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() {
println!("{}", style("Parameters used to load this recipe:").bold());
for (key, value) in params_for_template {
println!("{}: {}", key, value);
}
print_parameters_with_values(params_for_template);
}
println!();
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 parameter definition does not match the template variables in the recipe file
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)
}
@@ -92,13 +92,13 @@ pub fn explain_recipe_with_parameters(
recipe_name: &str,
params: Vec<(String, String)>,
) -> 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)?;
print_recipe_explanation(&raw_recipe);
let recipe_parameters = raw_recipe.parameters;
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);
Ok(())
@@ -115,7 +115,8 @@ fn validate_parameters_in_template(
recipe_parameters: &Option<Vec<RecipeParameter>>,
recipe_file_content: &str,
) -> 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
.as_ref()
@@ -207,9 +208,17 @@ fn extract_template_variables(template_str: &str) -> Result<HashSet<String>> {
fn apply_values_to_parameters(
user_params: &[(String, String)],
recipe_parameters: Option<Vec<RecipeParameter>>,
recipe_parent_dir: PathBuf,
enable_user_prompt: bool,
) -> Result<(HashMap<String, String>, Vec<String>)> {
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();
for param in recipe_parameters.unwrap_or_default() {
if !param_map.contains_key(&param.key) {

View File

@@ -1,11 +1,11 @@
use anyhow::{anyhow, Context, Result};
use anyhow::{anyhow, Result};
use goose::config::Config;
use std::fs;
use std::path::{Path, PathBuf};
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") || recipe_name.ends_with(".json") {
let path = PathBuf::from(recipe_name);
@@ -14,8 +14,8 @@ pub fn retrieve_recipe_file(recipe_name: &str) -> Result<String> {
// First check current directory
let current_dir = std::env::current_dir()?;
if let Ok(content) = read_recipe_in_dir(&current_dir, recipe_name) {
return Ok(content);
if let Ok((content, recipe_parent_dir)) = read_recipe_in_dir(&current_dir, recipe_name) {
return Ok((content, recipe_parent_dir));
}
read_recipe_in_dir(&current_dir, recipe_name).or_else(|e| {
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();
if path.exists() {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read recipe file: {}", path.display()))?;
Ok(content)
} else {
Err(anyhow!("Recipe file not found: {}", path.display()))
}
let content = fs::read_to_string(path)
.map_err(|e| anyhow!("Failed to read recipe file {}: {}", path.display(), e))?;
let canonical = path.canonicalize().map_err(|e| {
anyhow!(
"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"] {
let recipe_path = dir.join(format!("{}.{}", recipe_name, ext));
match read_recipe_file(recipe_path) {
Ok(content) => return Ok(content),
Ok((content, recipe_parent_dir)) => return Ok((content, recipe_parent_dir)),
Err(_) => continue,
}
}