mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-23 17:14:22 +01:00
feat: support configurable way to retrieve recipes via github (#2400)
This commit is contained in:
@@ -146,19 +146,21 @@ pub enum BenchCommand {
|
|||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum RecipeCommand {
|
enum RecipeCommand {
|
||||||
/// Validate a recipe file
|
/// Validate a recipe file
|
||||||
#[command(about = "Validate a recipe file")]
|
#[command(about = "Validate a recipe")]
|
||||||
Validate {
|
Validate {
|
||||||
/// Path to the recipe file to validate
|
/// Recipe name to get recipe file to validate
|
||||||
#[arg(help = "Path to the recipe file to validate")]
|
#[arg(help = "recipe name to get recipe file or full path to the recipe file to validate")]
|
||||||
file: String,
|
recipe_name: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Generate a deeplink for a recipe file
|
/// Generate a deeplink for a recipe file
|
||||||
#[command(about = "Generate a deeplink for a recipe file")]
|
#[command(about = "Generate a deeplink for a recipe")]
|
||||||
Deeplink {
|
Deeplink {
|
||||||
/// Path to the recipe file
|
/// Recipe name to get recipe file to generate deeplink
|
||||||
#[arg(help = "Path to the recipe file")]
|
#[arg(
|
||||||
file: String,
|
help = "recipe name to get recipe file or full path to the recipe file to generate deeplink"
|
||||||
|
)]
|
||||||
|
recipe_name: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,13 +268,13 @@ enum Command {
|
|||||||
)]
|
)]
|
||||||
input_text: Option<String>,
|
input_text: Option<String>,
|
||||||
|
|
||||||
/// Path to recipe.yaml file
|
/// Recipe name or full path to the recipe file
|
||||||
#[arg(
|
#[arg(
|
||||||
short = None,
|
short = None,
|
||||||
long = "recipe",
|
long = "recipe",
|
||||||
value_name = "FILE",
|
value_name = "RECIPE_NAME or FULL_PATH_TO_RECIPE_FILE",
|
||||||
help = "Path to recipe.yaml file",
|
help = "Recipe name to get recipe file or the full path of the recipe file",
|
||||||
long_help = "Path to a recipe.yaml file that defines a custom agent configuration",
|
long_help = "Recipe name to get recipe file or the full path of the recipe file that defines a custom agent configuration",
|
||||||
conflicts_with = "instructions",
|
conflicts_with = "instructions",
|
||||||
conflicts_with = "input_text"
|
conflicts_with = "input_text"
|
||||||
)]
|
)]
|
||||||
@@ -496,11 +498,12 @@ pub async fn cli() -> Result<()> {
|
|||||||
extensions_override: None,
|
extensions_override: None,
|
||||||
additional_system_prompt: None,
|
additional_system_prompt: None,
|
||||||
},
|
},
|
||||||
(_, _, Some(file)) => {
|
(_, _, Some(recipe_name)) => {
|
||||||
let recipe = load_recipe(&file, true, Some(params)).unwrap_or_else(|err| {
|
let recipe =
|
||||||
eprintln!("{}: {}", console::style("Error").red().bold(), err);
|
load_recipe(&recipe_name, true, Some(params)).unwrap_or_else(|err| {
|
||||||
std::process::exit(1);
|
eprintln!("{}: {}", console::style("Error").red().bold(), err);
|
||||||
});
|
std::process::exit(1);
|
||||||
|
});
|
||||||
InputConfig {
|
InputConfig {
|
||||||
contents: recipe.prompt,
|
contents: recipe.prompt,
|
||||||
extensions_override: recipe.extensions,
|
extensions_override: recipe.extensions,
|
||||||
@@ -568,11 +571,11 @@ pub async fn cli() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Some(Command::Recipe { command }) => {
|
Some(Command::Recipe { command }) => {
|
||||||
match command {
|
match command {
|
||||||
RecipeCommand::Validate { file } => {
|
RecipeCommand::Validate { recipe_name } => {
|
||||||
handle_validate(file)?;
|
handle_validate(&recipe_name)?;
|
||||||
}
|
}
|
||||||
RecipeCommand::Deeplink { file } => {
|
RecipeCommand::Deeplink { recipe_name } => {
|
||||||
handle_deeplink(file)?;
|
handle_deeplink(&recipe_name)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ use serde_json::{json, Value};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
|
use crate::recipes::github_recipe::GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY;
|
||||||
|
|
||||||
// useful for light themes where there is no dicernible colour contrast between
|
// useful for light themes where there is no dicernible colour contrast between
|
||||||
// cursor-selected and cursor-unselected items.
|
// cursor-selected and cursor-unselected items.
|
||||||
const MULTISELECT_VISIBILITY_HINT: &str = "<";
|
const MULTISELECT_VISIBILITY_HINT: &str = "<";
|
||||||
@@ -193,7 +195,7 @@ pub async fn handle_configure() -> Result<(), Box<dyn Error>> {
|
|||||||
.item(
|
.item(
|
||||||
"settings",
|
"settings",
|
||||||
"Goose Settings",
|
"Goose Settings",
|
||||||
"Set the Goose Mode, Tool Output, Tool Permissions, Experiment and more",
|
"Set the Goose Mode, Tool Output, Tool Permissions, Experiment, Goose recipe github repo and more",
|
||||||
)
|
)
|
||||||
.interact()?;
|
.interact()?;
|
||||||
|
|
||||||
@@ -808,6 +810,11 @@ pub async fn configure_settings_dialog() -> Result<(), Box<dyn Error>> {
|
|||||||
"Toggle Experiment",
|
"Toggle Experiment",
|
||||||
"Enable or disable an experiment feature",
|
"Enable or disable an experiment feature",
|
||||||
)
|
)
|
||||||
|
.item(
|
||||||
|
"recipe",
|
||||||
|
"Goose recipe github repo",
|
||||||
|
"Goose will pull recipes from this repo if not found locally.",
|
||||||
|
)
|
||||||
.interact()?;
|
.interact()?;
|
||||||
|
|
||||||
match setting_type {
|
match setting_type {
|
||||||
@@ -823,6 +830,9 @@ pub async fn configure_settings_dialog() -> Result<(), Box<dyn Error>> {
|
|||||||
"experiment" => {
|
"experiment" => {
|
||||||
toggle_experiments_dialog()?;
|
toggle_experiments_dialog()?;
|
||||||
}
|
}
|
||||||
|
"recipe" => {
|
||||||
|
configure_recipe_dialog()?;
|
||||||
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1104,3 +1114,26 @@ pub async fn configure_tool_permissions_dialog() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn configure_recipe_dialog() -> Result<(), Box<dyn Error>> {
|
||||||
|
let key_name = GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY;
|
||||||
|
let config = Config::global();
|
||||||
|
let default_recipe_repo = std::env::var(key_name)
|
||||||
|
.ok()
|
||||||
|
.or_else(|| config.get_param(key_name).unwrap_or(None));
|
||||||
|
let mut recipe_repo_input = cliclack::input(
|
||||||
|
"Enter your Goose Recipe Github repo (owner/repo): eg: my_org/goose-recipes",
|
||||||
|
)
|
||||||
|
.required(false);
|
||||||
|
if let Some(recipe_repo) = default_recipe_repo {
|
||||||
|
recipe_repo_input = recipe_repo_input.default_input(&recipe_repo);
|
||||||
|
}
|
||||||
|
let input_value: String = recipe_repo_input.interact()?;
|
||||||
|
// if input is blank, it clears the recipe github repo settings in the config file
|
||||||
|
if input_value.clone().trim().is_empty() {
|
||||||
|
config.delete(key_name)?;
|
||||||
|
} else {
|
||||||
|
config.set_param(key_name, Value::String(input_value))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use console::style;
|
use console::style;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use crate::recipe::load_recipe;
|
use crate::recipe::load_recipe;
|
||||||
|
|
||||||
@@ -14,9 +13,9 @@ use crate::recipe::load_recipe;
|
|||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// Result indicating success or failure
|
/// Result indicating success or failure
|
||||||
pub fn handle_validate<P: AsRef<Path>>(file_path: P) -> Result<()> {
|
pub fn handle_validate(recipe_name: &str) -> Result<()> {
|
||||||
// Load and validate the recipe file
|
// Load and validate the recipe file
|
||||||
match load_recipe(&file_path, false, None) {
|
match load_recipe(recipe_name, false, None) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
println!("{} recipe file is valid", style("✓").green().bold());
|
println!("{} recipe file is valid", style("✓").green().bold());
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -37,9 +36,9 @@ pub fn handle_validate<P: AsRef<Path>>(file_path: P) -> Result<()> {
|
|||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// Result indicating success or failure
|
/// Result indicating success or failure
|
||||||
pub fn handle_deeplink<P: AsRef<Path>>(file_path: P) -> Result<()> {
|
pub fn handle_deeplink(recipe_name: &str) -> Result<()> {
|
||||||
// Load the recipe file first to validate it
|
// Load the recipe file first to validate it
|
||||||
match load_recipe(&file_path, false, None) {
|
match load_recipe(recipe_name, false, None) {
|
||||||
Ok(recipe) => {
|
Ok(recipe) => {
|
||||||
if let Ok(recipe_json) = serde_json::to_string(&recipe) {
|
if let Ok(recipe_json) = serde_json::to_string(&recipe) {
|
||||||
let deeplink = base64::engine::general_purpose::STANDARD.encode(recipe_json);
|
let deeplink = base64::engine::general_purpose::STANDARD.encode(recipe_json);
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ pub mod cli;
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
pub mod recipe;
|
pub mod recipe;
|
||||||
|
pub mod recipes;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
|
|
||||||
// Re-export commonly used types
|
// Re-export commonly used types
|
||||||
pub use session::Session;
|
pub use session::Session;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::Result;
|
||||||
use console::style;
|
use console::style;
|
||||||
|
|
||||||
use goose::recipe::Recipe;
|
use goose::recipe::Recipe;
|
||||||
use minijinja::UndefinedBehavior;
|
use minijinja::UndefinedBehavior;
|
||||||
use std::{collections::HashMap, path::Path};
|
use serde_json::Value as JsonValue;
|
||||||
|
use serde_yaml::Value as YamlValue;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::recipes::search_recipe::retrieve_recipe_file;
|
||||||
|
|
||||||
/// Loads and validates a recipe from a YAML or JSON file
|
/// Loads and validates a recipe from a YAML or JSON file
|
||||||
///
|
///
|
||||||
@@ -23,46 +28,29 @@ use std::{collections::HashMap, path::Path};
|
|||||||
/// - The file can't be read
|
/// - The file can't be read
|
||||||
/// - The YAML/JSON is invalid
|
/// - The YAML/JSON is invalid
|
||||||
/// - The required fields are missing
|
/// - The required fields are missing
|
||||||
pub fn load_recipe<P: AsRef<Path>>(
|
pub fn load_recipe(
|
||||||
path: P,
|
recipe_name: &str,
|
||||||
log: bool,
|
log: bool,
|
||||||
params: Option<Vec<(String, String)>>,
|
params: Option<Vec<(String, String)>>,
|
||||||
) -> Result<Recipe> {
|
) -> Result<Recipe> {
|
||||||
let path = path.as_ref();
|
let content = retrieve_recipe_file(recipe_name)?;
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
if !path.exists() {
|
|
||||||
return Err(anyhow::anyhow!("recipe file not found: {}", path.display()));
|
|
||||||
}
|
|
||||||
// Read file content
|
|
||||||
let content = std::fs::read_to_string(path)
|
|
||||||
.with_context(|| format!("Failed to read recipe file: {}", path.display()))?;
|
|
||||||
// Check if any parameters were provided
|
// Check if any parameters were provided
|
||||||
let rendered_content = match params {
|
let rendered_content = match params {
|
||||||
None => content,
|
None => content,
|
||||||
Some(params) => render_content_with_params(&content, ¶ms)?,
|
Some(params) => render_content_with_params(&content, ¶ms)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine file format based on extension and parse accordingly
|
let recipe: Recipe;
|
||||||
let recipe: Recipe = if let Some(extension) = path.extension() {
|
if serde_json::from_str::<JsonValue>(&rendered_content).is_ok() {
|
||||||
match extension.to_str().unwrap_or("").to_lowercase().as_str() {
|
recipe = serde_json::from_str(&rendered_content)?
|
||||||
"json" => serde_json::from_str(&rendered_content)
|
} else if serde_yaml::from_str::<YamlValue>(&rendered_content).is_ok() {
|
||||||
.with_context(|| format!("Failed to parse JSON recipe file: {}", path.display()))?,
|
recipe = serde_yaml::from_str(&rendered_content)?
|
||||||
"yaml" => serde_yaml::from_str(&rendered_content)
|
|
||||||
.with_context(|| format!("Failed to parse YAML recipe file: {}", path.display()))?,
|
|
||||||
_ => {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Unsupported file format for recipe file: {}. Expected .yaml or .json",
|
|
||||||
path.display()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"File has no extension: {}. Expected .yaml or .json",
|
"Unsupported file format for recipe file. Expected .yaml or .json"
|
||||||
path.display()
|
|
||||||
));
|
));
|
||||||
};
|
}
|
||||||
|
|
||||||
if log {
|
if log {
|
||||||
// Display information about the loaded recipe
|
// Display information about the loaded recipe
|
||||||
|
|||||||
131
crates/goose-cli/src/recipes/github_recipe.rs
Normal file
131
crates/goose-cli/src/recipes/github_recipe.rs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::env;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
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> {
|
||||||
|
println!(
|
||||||
|
"retrieving recipe from github repo {}",
|
||||||
|
recipe_repo_full_name
|
||||||
|
);
|
||||||
|
ensure_gh_authenticated()?;
|
||||||
|
let local_repo_path = ensure_repo_cloned(recipe_repo_full_name)?;
|
||||||
|
fetch_origin(&local_repo_path)?;
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"Failed to retrieve recipe.yaml or recipe.json in path {} in github repo {} ",
|
||||||
|
recipe_name,
|
||||||
|
recipe_repo_full_name,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
.args(["auth", "status"])
|
||||||
|
.status()
|
||||||
|
.map_err(|_| {
|
||||||
|
anyhow::anyhow!("Failed to run `gh auth status`. Make sure you have `gh` installed.")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if status.success() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!("GitHub CLI is not authenticated. Launching `gh auth login`...");
|
||||||
|
// Run `gh auth login` interactively
|
||||||
|
let login_status = Command::new("gh")
|
||||||
|
.args(["auth", "login"])
|
||||||
|
.status()
|
||||||
|
.map_err(|_| anyhow::anyhow!("Failed to run `gh auth login`"))?;
|
||||||
|
|
||||||
|
if !login_status.success() {
|
||||||
|
Err(anyhow::anyhow!("Failed to authenticate using GitHub CLI."))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_repo_cloned(recipe_repo_full_name: &str) -> Result<PathBuf> {
|
||||||
|
let local_repo_parent_path = env::temp_dir();
|
||||||
|
let (_, repo_name) = recipe_repo_full_name
|
||||||
|
.split_once('/')
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Invalid repository name format"))?;
|
||||||
|
|
||||||
|
let local_repo_path = local_repo_parent_path.clone().join(repo_name);
|
||||||
|
if local_repo_path.join(".git").exists() {
|
||||||
|
Ok(local_repo_path)
|
||||||
|
} else {
|
||||||
|
// Create the local repo parent directory if it doesn't exist
|
||||||
|
if !local_repo_parent_path.exists() {
|
||||||
|
std::fs::create_dir_all(local_repo_parent_path.clone())?;
|
||||||
|
}
|
||||||
|
let error_message: String = format!("Failed to clone repo: {}", recipe_repo_full_name);
|
||||||
|
let status = Command::new("gh")
|
||||||
|
.args(["repo", "clone", recipe_repo_full_name])
|
||||||
|
.current_dir(local_repo_parent_path.clone())
|
||||||
|
.status()
|
||||||
|
.map_err(|_: std::io::Error| anyhow::anyhow!(error_message.clone()))?;
|
||||||
|
|
||||||
|
if status.success() {
|
||||||
|
Ok(local_repo_path)
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!(error_message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_origin(local_repo_path: &Path) -> Result<()> {
|
||||||
|
let error_message: String = format!("Failed to fetch at {}", local_repo_path.to_str().unwrap());
|
||||||
|
let status = Command::new("git")
|
||||||
|
.args(["fetch", "origin"])
|
||||||
|
.current_dir(local_repo_path)
|
||||||
|
.status()
|
||||||
|
.map_err(|_| anyhow::anyhow!(error_message.clone()))?;
|
||||||
|
|
||||||
|
if status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!(error_message))
|
||||||
|
}
|
||||||
|
}
|
||||||
2
crates/goose-cli/src/recipes/mod.rs
Normal file
2
crates/goose-cli/src/recipes/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod github_recipe;
|
||||||
|
pub mod search_recipe;
|
||||||
61
crates/goose-cli/src/recipes/search_recipe.rs
Normal file
61
crates/goose-cli/src/recipes/search_recipe.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
use anyhow::{anyhow, Context, 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> {
|
||||||
|
// 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);
|
||||||
|
return read_recipe_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
read_recipe_in_dir(¤t_dir, 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 {
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configured_github_recipe_repo() -> Option<String> {
|
||||||
|
let config = Config::global();
|
||||||
|
match config.get_param(GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY) {
|
||||||
|
Ok(Some(recipe_repo_full_name)) => Some(recipe_repo_full_name),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_recipe_file<P: AsRef<Path>>(recipe_path: P) -> Result<String> {
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_recipe_in_dir(dir: &Path, recipe_name: &str) -> Result<String> {
|
||||||
|
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),
|
||||||
|
Err(_) => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(anyhow!(format!(
|
||||||
|
"No {}.yaml or {}.json recipe file found in current directory.",
|
||||||
|
recipe_name, recipe_name
|
||||||
|
)))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user