mirror of
https://github.com/aljazceru/goose.git
synced 2026-02-23 07:24:24 +01:00
added recipe_dir (#2543)
This commit is contained in:
66
Cargo.lock
generated
66
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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(¶ms, recipe.parameters, true)?;
|
||||
apply_values_to_parameters(¶ms, 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(¶ms, recipe_parameters, false)?;
|
||||
apply_values_to_parameters(¶ms, 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(¶m.key) {
|
||||
|
||||
@@ -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(¤t_dir, recipe_name) {
|
||||
return Ok(content);
|
||||
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| {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user