chore: Used common function to list recipes in local machine (#4974)
This commit is contained in:
@@ -1006,7 +1006,7 @@ pub async fn cli() -> Result<()> {
|
|||||||
.unwrap_or(&recipe_name);
|
.unwrap_or(&recipe_name);
|
||||||
|
|
||||||
let recipe_version =
|
let recipe_version =
|
||||||
crate::recipes::search_recipe::retrieve_recipe_file(&recipe_name)
|
crate::recipes::search_recipe::load_recipe_file(&recipe_name)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|rf| {
|
.and_then(|rf| {
|
||||||
goose::recipe::template_recipe::parse_recipe_content(
|
goose::recipe::template_recipe::parse_recipe_content(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use goose::recipe::SubRecipe;
|
|||||||
|
|
||||||
use crate::recipes::print_recipe::print_recipe_info;
|
use crate::recipes::print_recipe::print_recipe_info;
|
||||||
use crate::recipes::recipe::load_recipe;
|
use crate::recipes::recipe::load_recipe;
|
||||||
use crate::recipes::search_recipe::retrieve_recipe_file;
|
use crate::recipes::search_recipe::load_recipe_file;
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{InputConfig, RecipeInfo},
|
cli::{InputConfig, RecipeInfo},
|
||||||
session::SessionSettings,
|
session::SessionSettings,
|
||||||
@@ -24,7 +24,7 @@ pub fn extract_recipe_info_from_cli(
|
|||||||
let mut all_sub_recipes = recipe.sub_recipes.clone().unwrap_or_default();
|
let mut all_sub_recipes = recipe.sub_recipes.clone().unwrap_or_default();
|
||||||
if !additional_sub_recipes.is_empty() {
|
if !additional_sub_recipes.is_empty() {
|
||||||
for sub_recipe_name in additional_sub_recipes {
|
for sub_recipe_name in additional_sub_recipes {
|
||||||
match retrieve_recipe_file(&sub_recipe_name) {
|
match load_recipe_file(&sub_recipe_name) {
|
||||||
Ok(recipe_file) => {
|
Ok(recipe_file) => {
|
||||||
let name = extract_recipe_name(&sub_recipe_name);
|
let name = extract_recipe_name(&sub_recipe_name);
|
||||||
let recipe_file_path = recipe_file.file_path;
|
let recipe_file_path = recipe_file.file_path;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use console::style;
|
use console::style;
|
||||||
use goose::recipe::template_recipe::parse_recipe_content;
|
use goose::recipe::template_recipe::parse_recipe_content;
|
||||||
|
use goose::recipe::RECIPE_FILE_EXTENSIONS;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::recipes::recipe::RECIPE_FILE_EXTENSIONS;
|
|
||||||
use goose::recipe::read_recipe_file_content::RecipeFile;
|
use goose::recipe::read_recipe_file_content::RecipeFile;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::recipes::print_recipe::{
|
|||||||
missing_parameters_command_line, print_recipe_explanation,
|
missing_parameters_command_line, 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::load_recipe_file;
|
||||||
use crate::recipes::secret_discovery::{discover_recipe_secrets, SecretRequirement};
|
use crate::recipes::secret_discovery::{discover_recipe_secrets, SecretRequirement};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use goose::config::Config;
|
use goose::config::Config;
|
||||||
@@ -15,8 +15,6 @@ use goose::recipe::Recipe;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub const RECIPE_FILE_EXTENSIONS: &[&str] = &["yaml", "json"];
|
|
||||||
|
|
||||||
fn create_user_prompt_callback() -> impl Fn(&str, &str) -> Result<String> {
|
fn create_user_prompt_callback() -> impl Fn(&str, &str) -> Result<String> {
|
||||||
|key: &str, description: &str| -> Result<String> {
|
|key: &str, description: &str| -> Result<String> {
|
||||||
let input_value =
|
let input_value =
|
||||||
@@ -26,7 +24,7 @@ fn create_user_prompt_callback() -> impl Fn(&str, &str) -> Result<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load_recipe_file_with_dir(recipe_name: &str) -> Result<(RecipeFile, String)> {
|
fn load_recipe_file_with_dir(recipe_name: &str) -> Result<(RecipeFile, String)> {
|
||||||
let recipe_file = retrieve_recipe_file(recipe_name)?;
|
let recipe_file = load_recipe_file(recipe_name)?;
|
||||||
let recipe_dir_str = recipe_file
|
let recipe_dir_str = recipe_file
|
||||||
.parent_dir
|
.parent_dir
|
||||||
.to_str()
|
.to_str()
|
||||||
@@ -36,7 +34,7 @@ fn load_recipe_file_with_dir(recipe_name: &str) -> Result<(RecipeFile, String)>
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_recipe(recipe_name: &str, params: Vec<(String, String)>) -> Result<Recipe> {
|
pub fn load_recipe(recipe_name: &str, params: Vec<(String, String)>) -> Result<Recipe> {
|
||||||
let recipe_file = retrieve_recipe_file(recipe_name)?;
|
let recipe_file = load_recipe_file(recipe_name)?;
|
||||||
match build_recipe_from_template(recipe_file, params, Some(create_user_prompt_callback())) {
|
match build_recipe_from_template(recipe_file, params, Some(create_user_prompt_callback())) {
|
||||||
Ok(recipe) => {
|
Ok(recipe) => {
|
||||||
let secret_requirements = discover_recipe_secrets(&recipe);
|
let secret_requirements = discover_recipe_secrets(&recipe);
|
||||||
|
|||||||
@@ -1,35 +1,15 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::Result;
|
||||||
use goose::config::Config;
|
use goose::config::Config;
|
||||||
use goose::recipe::read_recipe_file_content::{read_recipe_file, RecipeFile};
|
use goose::recipe::read_recipe_file_content::RecipeFile;
|
||||||
use goose::recipe::template_recipe::parse_recipe_content;
|
|
||||||
use std::env;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use crate::recipes::recipe::RECIPE_FILE_EXTENSIONS;
|
|
||||||
|
|
||||||
use super::github_recipe::{
|
use super::github_recipe::{
|
||||||
list_github_recipes, retrieve_recipe_from_github, RecipeInfo, RecipeSource,
|
list_github_recipes, retrieve_recipe_from_github, RecipeInfo, RecipeSource,
|
||||||
GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY,
|
GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY,
|
||||||
};
|
};
|
||||||
|
use goose::recipe::local_recipes::{list_local_recipes, load_local_recipe_file};
|
||||||
|
|
||||||
const GOOSE_RECIPE_PATH_ENV_VAR: &str = "GOOSE_RECIPE_PATH";
|
pub fn load_recipe_file(recipe_name: &str) -> Result<RecipeFile> {
|
||||||
|
load_local_recipe_file(recipe_name).or_else(|e| {
|
||||||
pub fn retrieve_recipe_file(recipe_name: &str) -> Result<RecipeFile> {
|
|
||||||
if RECIPE_FILE_EXTENSIONS
|
|
||||||
.iter()
|
|
||||||
.any(|ext| recipe_name.ends_with(&format!(".{}", ext)))
|
|
||||||
{
|
|
||||||
let path = PathBuf::from(recipe_name);
|
|
||||||
return read_recipe_file(path);
|
|
||||||
}
|
|
||||||
if is_file_path(recipe_name) || is_file_name(recipe_name) {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"Recipe file {} is not a json or yaml file",
|
|
||||||
recipe_name
|
|
||||||
));
|
|
||||||
}
|
|
||||||
retrieve_recipe_from_local_path(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() {
|
||||||
retrieve_recipe_from_github(recipe_name, &recipe_repo_full_name)
|
retrieve_recipe_from_github(recipe_name, &recipe_repo_full_name)
|
||||||
} else {
|
} else {
|
||||||
@@ -38,60 +18,6 @@ pub fn retrieve_recipe_file(recipe_name: &str) -> Result<RecipeFile> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_file_path(recipe_name: &str) -> bool {
|
|
||||||
recipe_name.contains('/')
|
|
||||||
|| recipe_name.contains('\\')
|
|
||||||
|| recipe_name.starts_with('~')
|
|
||||||
|| recipe_name.starts_with('.')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_file_name(recipe_name: &str) -> bool {
|
|
||||||
Path::new(recipe_name).extension().is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_recipe_in_dir(dir: &Path, recipe_name: &str) -> Result<RecipeFile> {
|
|
||||||
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<RecipeFile> {
|
|
||||||
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<PathBuf> = 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::<Vec<_>>()
|
|
||||||
.join(":");
|
|
||||||
Err(anyhow!(
|
|
||||||
"ℹ️ Failed to retrieve {}.yaml or {}.json in {}",
|
|
||||||
recipe_name,
|
|
||||||
recipe_name,
|
|
||||||
search_dirs_str
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn configured_github_recipe_repo() -> Option<String> {
|
fn configured_github_recipe_repo() -> Option<String> {
|
||||||
let config = Config::global();
|
let config = Config::global();
|
||||||
match config.get_param(GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY) {
|
match config.get_param(GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY) {
|
||||||
@@ -105,8 +31,22 @@ pub fn list_available_recipes() -> Result<Vec<RecipeInfo>> {
|
|||||||
let mut recipes = Vec::new();
|
let mut recipes = Vec::new();
|
||||||
|
|
||||||
// Search local recipes
|
// Search local recipes
|
||||||
if let Ok(local_recipes) = discover_local_recipes() {
|
if let Ok(local_recipes) = list_local_recipes() {
|
||||||
recipes.extend(local_recipes);
|
recipes.extend(local_recipes.into_iter().map(|(path, recipe)| {
|
||||||
|
let name = path
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("unknown")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
RecipeInfo {
|
||||||
|
name,
|
||||||
|
source: RecipeSource::Local,
|
||||||
|
path: path.to_string_lossy().to_string(),
|
||||||
|
title: Some(recipe.title),
|
||||||
|
description: Some(recipe.description),
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search GitHub recipes if configured
|
// Search GitHub recipes if configured
|
||||||
@@ -118,77 +58,3 @@ pub fn list_available_recipes() -> Result<Vec<RecipeInfo>> {
|
|||||||
|
|
||||||
Ok(recipes)
|
Ok(recipes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn discover_local_recipes() -> Result<Vec<RecipeInfo>> {
|
|
||||||
let mut recipes = Vec::new();
|
|
||||||
let mut search_dirs = vec![PathBuf::from(".")];
|
|
||||||
|
|
||||||
// Add GOOSE_RECIPE_PATH directories
|
|
||||||
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<PathBuf> = 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(dir_recipes) = scan_directory_for_recipes(&dir) {
|
|
||||||
recipes.extend(dir_recipes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(recipes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scan_directory_for_recipes(dir: &Path) -> Result<Vec<RecipeInfo>> {
|
|
||||||
let mut recipes = Vec::new();
|
|
||||||
|
|
||||||
if !dir.exists() || !dir.is_dir() {
|
|
||||||
return Ok(recipes);
|
|
||||||
}
|
|
||||||
|
|
||||||
for entry in fs::read_dir(dir)? {
|
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
|
||||||
|
|
||||||
if path.is_file() {
|
|
||||||
if let Some(extension) = path.extension() {
|
|
||||||
if RECIPE_FILE_EXTENSIONS.contains(&extension.to_string_lossy().as_ref()) {
|
|
||||||
if let Ok(recipe_info) = create_local_recipe_info(&path) {
|
|
||||||
recipes.push(recipe_info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(recipes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_local_recipe_info(path: &Path) -> Result<RecipeInfo> {
|
|
||||||
let content = fs::read_to_string(path)?;
|
|
||||||
let recipe_dir = path
|
|
||||||
.parent()
|
|
||||||
.unwrap_or_else(|| Path::new("."))
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string();
|
|
||||||
let (recipe, _) = parse_recipe_content(&content, recipe_dir)?;
|
|
||||||
|
|
||||||
let name = path
|
|
||||||
.file_stem()
|
|
||||||
.and_then(|s| s.to_str())
|
|
||||||
.unwrap_or("unknown")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let path_str = path.to_string_lossy().to_string();
|
|
||||||
|
|
||||||
Ok(RecipeInfo {
|
|
||||||
name,
|
|
||||||
source: RecipeSource::Local,
|
|
||||||
path: path_str,
|
|
||||||
title: Some(recipe.title),
|
|
||||||
description: Some(recipe.description),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::recipes::search_recipe::retrieve_recipe_file;
|
use crate::recipes::search_recipe::load_recipe_file;
|
||||||
use goose::agents::extension::ExtensionConfig;
|
use goose::agents::extension::ExtensionConfig;
|
||||||
use goose::recipe::Recipe;
|
use goose::recipe::Recipe;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
@@ -116,7 +116,7 @@ fn discover_recipe_secrets_recursive(
|
|||||||
/// For secret discovery, we only need the recipe structure (extensions and env_keys),
|
/// For secret discovery, we only need the recipe structure (extensions and env_keys),
|
||||||
/// not parameter-substituted content, so we parse the raw YAML directly for speed and robustness.
|
/// not parameter-substituted content, so we parse the raw YAML directly for speed and robustness.
|
||||||
fn load_sub_recipe(recipe_path: &str) -> Result<Recipe, Box<dyn std::error::Error>> {
|
fn load_sub_recipe(recipe_path: &str) -> Result<Recipe, Box<dyn std::error::Error>> {
|
||||||
let recipe_file = retrieve_recipe_file(recipe_path)?;
|
let recipe_file = load_recipe_file(recipe_path)?;
|
||||||
let recipe: Recipe = serde_yaml::from_str(&recipe_file.content)?;
|
let recipe: Recipe = serde_yaml::from_str(&recipe_file.content)?;
|
||||||
Ok(recipe)
|
Ok(recipe)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::{extract::State, http::StatusCode, routing::post, Json, Router};
|
use axum::{extract::State, http::StatusCode, routing::post, Json, Router};
|
||||||
use goose::recipe::recipe_library;
|
use goose::recipe::local_recipes;
|
||||||
use goose::recipe::Recipe;
|
use goose::recipe::Recipe;
|
||||||
use goose::recipe_deeplink;
|
use goose::recipe_deeplink;
|
||||||
use goose::session::SessionManager;
|
use goose::session::SessionManager;
|
||||||
@@ -324,7 +324,7 @@ async fn save_recipe(
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
match recipe_library::save_recipe_to_file(request.recipe, request.is_global, file_path) {
|
match local_recipes::save_recipe_to_file(request.recipe, request.is_global, file_path) {
|
||||||
Ok(_) => Ok(StatusCode::NO_CONTENT),
|
Ok(_) => Ok(StatusCode::NO_CONTENT),
|
||||||
Err(e) => Err(ErrorResponse {
|
Err(e) => Err(ErrorResponse {
|
||||||
message: e.to_string(),
|
message: e.to_string(),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use goose::recipe::recipe_library::list_all_recipes_from_library;
|
use goose::recipe::local_recipes::list_local_recipes;
|
||||||
use goose::recipe::Recipe;
|
use goose::recipe::Recipe;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -29,7 +29,7 @@ fn short_id_from_path(path: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_all_recipes_manifests() -> Result<Vec<RecipeManifestWithPath>> {
|
pub fn get_all_recipes_manifests() -> Result<Vec<RecipeManifestWithPath>> {
|
||||||
let recipes_with_path = list_all_recipes_from_library()?;
|
let recipes_with_path = list_local_recipes()?;
|
||||||
let mut recipe_manifests_with_path = Vec::new();
|
let mut recipe_manifests_with_path = Vec::new();
|
||||||
for (file_path, recipe) in recipes_with_path {
|
for (file_path, recipe) in recipes_with_path {
|
||||||
let Ok(last_modified) = fs::metadata(file_path.clone())
|
let Ok(last_modified) = fs::metadata(file_path.clone())
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use etcetera::{choose_app_strategy, AppStrategy};
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::config::APP_STRATEGY;
|
||||||
|
use crate::recipe::read_recipe_file_content::{read_recipe_file, RecipeFile};
|
||||||
|
use crate::recipe::Recipe;
|
||||||
|
use crate::recipe::RECIPE_FILE_EXTENSIONS;
|
||||||
|
use serde_yaml;
|
||||||
|
|
||||||
|
const GOOSE_RECIPE_PATH_ENV_VAR: &str = "GOOSE_RECIPE_PATH";
|
||||||
|
|
||||||
|
pub fn get_recipe_library_dir(is_global: bool) -> PathBuf {
|
||||||
|
if is_global {
|
||||||
|
choose_app_strategy(APP_STRATEGY.clone())
|
||||||
|
.expect("goose requires a home dir")
|
||||||
|
.config_dir()
|
||||||
|
.join("recipes")
|
||||||
|
} else {
|
||||||
|
std::env::current_dir().unwrap().join(".goose/recipes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_recipe_dirs() -> Vec<PathBuf> {
|
||||||
|
let mut local_dirs = vec![PathBuf::from(".")];
|
||||||
|
|
||||||
|
if let Ok(recipe_path_env) = env::var(GOOSE_RECIPE_PATH_ENV_VAR) {
|
||||||
|
let path_separator = if cfg!(windows) { ';' } else { ':' };
|
||||||
|
local_dirs.extend(recipe_path_env.split(path_separator).map(PathBuf::from));
|
||||||
|
}
|
||||||
|
local_dirs.push(get_recipe_library_dir(true));
|
||||||
|
local_dirs.push(get_recipe_library_dir(false));
|
||||||
|
|
||||||
|
local_dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_local_recipe_file(recipe_name: &str) -> Result<RecipeFile> {
|
||||||
|
if RECIPE_FILE_EXTENSIONS
|
||||||
|
.iter()
|
||||||
|
.any(|ext| recipe_name.ends_with(&format!(".{}", ext)))
|
||||||
|
{
|
||||||
|
let path = PathBuf::from(recipe_name);
|
||||||
|
return read_recipe_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_file_path(recipe_name) || is_file_name(recipe_name) {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Recipe file {} is not a json or yaml file",
|
||||||
|
recipe_name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let search_dirs = local_recipe_dirs();
|
||||||
|
for dir in &search_dirs {
|
||||||
|
if let Ok(result) = load_recipe_file_from_dir(dir, recipe_name) {
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let search_dirs_str = search_dirs
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.to_string_lossy())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(":");
|
||||||
|
Err(anyhow!(
|
||||||
|
"ℹ️ Failed to retrieve {}.yaml or {}.json in {}",
|
||||||
|
recipe_name,
|
||||||
|
recipe_name,
|
||||||
|
search_dirs_str
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_local_recipes() -> Result<Vec<(PathBuf, Recipe)>> {
|
||||||
|
let mut recipes = Vec::new();
|
||||||
|
for dir in local_recipe_dirs() {
|
||||||
|
if let Ok(dir_recipes) = scan_directory_for_recipes(&dir) {
|
||||||
|
recipes.extend(dir_recipes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(recipes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_file_path(recipe_name: &str) -> bool {
|
||||||
|
recipe_name.contains('/')
|
||||||
|
|| recipe_name.contains('\\')
|
||||||
|
|| recipe_name.starts_with('~')
|
||||||
|
|| recipe_name.starts_with('.')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_file_name(recipe_name: &str) -> bool {
|
||||||
|
Path::new(recipe_name).extension().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_recipe_file_from_dir(dir: &Path, recipe_name: &str) -> Result<RecipeFile> {
|
||||||
|
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 scan_directory_for_recipes(dir: &Path) -> Result<Vec<(PathBuf, Recipe)>> {
|
||||||
|
let mut recipes = Vec::new();
|
||||||
|
|
||||||
|
if !dir.exists() || !dir.is_dir() {
|
||||||
|
return Ok(recipes);
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in fs::read_dir(dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if path.is_file() {
|
||||||
|
if let Some(extension) = path.extension() {
|
||||||
|
if RECIPE_FILE_EXTENSIONS.contains(&extension.to_string_lossy().as_ref()) {
|
||||||
|
if let Ok(recipe) = Recipe::from_file_path(&path) {
|
||||||
|
recipes.push((path.clone(), recipe));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(recipes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_recipe_filename(title: &str) -> String {
|
||||||
|
let base_name = title
|
||||||
|
.to_lowercase()
|
||||||
|
.chars()
|
||||||
|
.filter(|c| c.is_alphanumeric() || c.is_whitespace() || *c == '-')
|
||||||
|
.collect::<String>()
|
||||||
|
.split_whitespace()
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join("-");
|
||||||
|
|
||||||
|
let filename = if base_name.is_empty() {
|
||||||
|
"untitled-recipe".to_string()
|
||||||
|
} else {
|
||||||
|
base_name
|
||||||
|
};
|
||||||
|
format!("{}.yaml", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_recipe_to_file(
|
||||||
|
recipe: Recipe,
|
||||||
|
is_global: Option<bool>,
|
||||||
|
file_path: Option<PathBuf>,
|
||||||
|
) -> anyhow::Result<PathBuf> {
|
||||||
|
let is_global_value = is_global.unwrap_or(true);
|
||||||
|
|
||||||
|
let default_file_path =
|
||||||
|
get_recipe_library_dir(is_global_value).join(generate_recipe_filename(&recipe.title));
|
||||||
|
|
||||||
|
let file_path_value = match file_path {
|
||||||
|
Some(path) => path,
|
||||||
|
None => {
|
||||||
|
if default_file_path.exists() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Recipe file already exists at: {:?}",
|
||||||
|
default_file_path
|
||||||
|
));
|
||||||
|
}
|
||||||
|
default_file_path
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let all_recipes = list_local_recipes()?;
|
||||||
|
|
||||||
|
for (existing_path, existing_recipe) in &all_recipes {
|
||||||
|
if existing_recipe.title == recipe.title && existing_path != &file_path_value {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Recipe with title '{}' already exists",
|
||||||
|
recipe.title
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let yaml_content = serde_yaml::to_string(&recipe)?;
|
||||||
|
fs::write(&file_path_value, yaml_content)?;
|
||||||
|
Ok(file_path_value)
|
||||||
|
}
|
||||||
@@ -2,20 +2,23 @@ use anyhow::Result;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::agents::extension::ExtensionConfig;
|
use crate::agents::extension::ExtensionConfig;
|
||||||
use crate::agents::types::RetryConfig;
|
use crate::agents::types::RetryConfig;
|
||||||
|
use crate::recipe::read_recipe_file_content::read_recipe_file;
|
||||||
use crate::utils::contains_unicode_tags;
|
use crate::utils::contains_unicode_tags;
|
||||||
use serde::de::Deserializer;
|
use serde::de::Deserializer;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
pub mod build_recipe;
|
pub mod build_recipe;
|
||||||
|
pub mod local_recipes;
|
||||||
pub mod read_recipe_file_content;
|
pub mod read_recipe_file_content;
|
||||||
pub mod recipe_library;
|
|
||||||
pub mod template_recipe;
|
pub mod template_recipe;
|
||||||
|
|
||||||
pub const BUILT_IN_RECIPE_DIR_PARAM: &str = "recipe_dir";
|
pub const BUILT_IN_RECIPE_DIR_PARAM: &str = "recipe_dir";
|
||||||
|
pub const RECIPE_FILE_EXTENSIONS: &[&str] = &["yaml", "json"];
|
||||||
|
|
||||||
fn default_version() -> String {
|
fn default_version() -> String {
|
||||||
"1.0.0".to_string()
|
"1.0.0".to_string()
|
||||||
@@ -308,6 +311,12 @@ impl Recipe {
|
|||||||
retry: None,
|
retry: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_file_path(file_path: &Path) -> Result<Self> {
|
||||||
|
let file = read_recipe_file(file_path)?;
|
||||||
|
Self::from_content(&file.content)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn from_content(content: &str) -> Result<Self> {
|
pub fn from_content(content: &str) -> Result<Self> {
|
||||||
let recipe: Recipe =
|
let recipe: Recipe =
|
||||||
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(content) {
|
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(content) {
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
use crate::config::APP_STRATEGY;
|
|
||||||
use crate::recipe::read_recipe_file_content::read_recipe_file;
|
|
||||||
use crate::recipe::Recipe;
|
|
||||||
use anyhow::Result;
|
|
||||||
use etcetera::{choose_app_strategy, AppStrategy};
|
|
||||||
use serde_yaml;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
pub fn get_recipe_library_dir(is_global: bool) -> PathBuf {
|
|
||||||
if is_global {
|
|
||||||
choose_app_strategy(APP_STRATEGY.clone())
|
|
||||||
.expect("goose requires a home dir")
|
|
||||||
.config_dir()
|
|
||||||
.join("recipes")
|
|
||||||
} else {
|
|
||||||
std::env::current_dir().unwrap().join(".goose/recipes")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_recipes_from_library(is_global: bool) -> Result<Vec<(PathBuf, Recipe)>> {
|
|
||||||
let path = get_recipe_library_dir(is_global);
|
|
||||||
let mut recipes_with_path = Vec::new();
|
|
||||||
if path.exists() {
|
|
||||||
for entry in fs::read_dir(path)? {
|
|
||||||
let path = entry?.path();
|
|
||||||
let extension = path.extension();
|
|
||||||
|
|
||||||
if extension == Some("yaml".as_ref()) || extension == Some("json".as_ref()) {
|
|
||||||
let Ok(recipe_file) = read_recipe_file(path.clone()) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Ok(recipe) = Recipe::from_content(&recipe_file.content) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
recipes_with_path.push((path, recipe));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(recipes_with_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_all_recipes_from_library() -> Result<Vec<(PathBuf, Recipe)>> {
|
|
||||||
let mut recipes_with_path = Vec::new();
|
|
||||||
recipes_with_path.extend(list_recipes_from_library(true)?);
|
|
||||||
recipes_with_path.extend(list_recipes_from_library(false)?);
|
|
||||||
Ok(recipes_with_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_recipe_filename(title: &str) -> String {
|
|
||||||
let base_name = title
|
|
||||||
.to_lowercase()
|
|
||||||
.chars()
|
|
||||||
.filter(|c| c.is_alphanumeric() || c.is_whitespace() || *c == '-')
|
|
||||||
.collect::<String>()
|
|
||||||
.split_whitespace()
|
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.join("-");
|
|
||||||
|
|
||||||
let filename = if base_name.is_empty() {
|
|
||||||
"untitled-recipe".to_string()
|
|
||||||
} else {
|
|
||||||
base_name
|
|
||||||
};
|
|
||||||
format!("{}.yaml", filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_recipe_to_file(
|
|
||||||
recipe: Recipe,
|
|
||||||
is_global: Option<bool>,
|
|
||||||
file_path: Option<PathBuf>,
|
|
||||||
) -> anyhow::Result<PathBuf> {
|
|
||||||
let is_global_value = is_global.unwrap_or(true);
|
|
||||||
|
|
||||||
let default_file_path =
|
|
||||||
get_recipe_library_dir(is_global_value).join(generate_recipe_filename(&recipe.title));
|
|
||||||
|
|
||||||
let file_path_value = match file_path {
|
|
||||||
Some(path) => path,
|
|
||||||
None => {
|
|
||||||
if default_file_path.exists() {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Recipe file already exists at: {:?}",
|
|
||||||
default_file_path
|
|
||||||
));
|
|
||||||
}
|
|
||||||
default_file_path
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let all_recipes = list_all_recipes_from_library()?;
|
|
||||||
|
|
||||||
for (existing_path, existing_recipe) in &all_recipes {
|
|
||||||
if existing_recipe.title == recipe.title && existing_path != &file_path_value {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Recipe with title '{}' already exists",
|
|
||||||
recipe.title
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let yaml_content = serde_yaml::to_string(&recipe)?;
|
|
||||||
fs::write(&file_path_value, yaml_content)?;
|
|
||||||
Ok(file_path_value)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user