275 lines
9.5 KiB
Rust
275 lines
9.5 KiB
Rust
use anyhow::Result;
|
|
use console::style;
|
|
use goose::recipe::validate_recipe::validate_recipe_template_from_file;
|
|
|
|
use crate::recipes::github_recipe::RecipeSource;
|
|
use crate::recipes::search_recipe::{list_available_recipes, load_recipe_file};
|
|
use goose::recipe_deeplink;
|
|
|
|
pub fn handle_validate(recipe_name: &str) -> Result<()> {
|
|
// Load and validate the recipe file
|
|
let recipe_file = load_recipe_file(recipe_name)?;
|
|
validate_recipe_template_from_file(&recipe_file).map_err(|err| {
|
|
anyhow::anyhow!(
|
|
"{} recipe file is invalid: {}",
|
|
style("✗").red().bold(),
|
|
err
|
|
)
|
|
})?;
|
|
println!("{} recipe file is valid", style("✓").green().bold());
|
|
Ok(())
|
|
}
|
|
|
|
pub fn handle_deeplink(recipe_name: &str) -> Result<String> {
|
|
match generate_deeplink(recipe_name) {
|
|
Ok((deeplink_url, recipe)) => {
|
|
println!(
|
|
"{} Generated deeplink for: {}",
|
|
style("✓").green().bold(),
|
|
recipe.title
|
|
);
|
|
println!("{}", deeplink_url);
|
|
Ok(deeplink_url)
|
|
}
|
|
Err(err) => {
|
|
println!(
|
|
"{} Failed to encode recipe: {}",
|
|
style("✗").red().bold(),
|
|
err
|
|
);
|
|
Err(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn handle_open(recipe_name: &str) -> Result<()> {
|
|
// Generate the deeplink using the helper function (no printing)
|
|
// This reuses all the validation and encoding logic
|
|
match generate_deeplink(recipe_name) {
|
|
Ok((deeplink_url, recipe)) => {
|
|
// Attempt to open the deeplink
|
|
match open::that(&deeplink_url) {
|
|
Ok(_) => {
|
|
println!(
|
|
"{} Opened recipe '{}' in Goose Desktop",
|
|
style("✓").green().bold(),
|
|
recipe.title
|
|
);
|
|
Ok(())
|
|
}
|
|
Err(err) => {
|
|
println!(
|
|
"{} Failed to open recipe in Goose Desktop: {}",
|
|
style("✗").red().bold(),
|
|
err
|
|
);
|
|
println!("Generated deeplink: {}", deeplink_url);
|
|
println!("You can manually copy and open the URL above, or ensure Goose Desktop is installed.");
|
|
Err(anyhow::anyhow!("Failed to open recipe: {}", err))
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
println!(
|
|
"{} Failed to encode recipe: {}",
|
|
style("✗").red().bold(),
|
|
err
|
|
);
|
|
Err(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn handle_list(format: &str, verbose: bool) -> Result<()> {
|
|
let recipes = match list_available_recipes() {
|
|
Ok(recipes) => recipes,
|
|
Err(e) => {
|
|
return Err(anyhow::anyhow!("Failed to list recipes: {}", e));
|
|
}
|
|
};
|
|
|
|
match format {
|
|
"json" => {
|
|
println!("{}", serde_json::to_string(&recipes)?);
|
|
}
|
|
_ => {
|
|
if recipes.is_empty() {
|
|
println!("No recipes found");
|
|
return Ok(());
|
|
} else {
|
|
println!("Available recipes:");
|
|
for recipe in recipes {
|
|
let source_info = match recipe.source {
|
|
RecipeSource::Local => format!("local: {}", recipe.path),
|
|
RecipeSource::GitHub => format!("github: {}", recipe.path),
|
|
};
|
|
|
|
let description = if let Some(desc) = &recipe.description {
|
|
if desc.is_empty() {
|
|
"(none)"
|
|
} else {
|
|
desc
|
|
}
|
|
} else {
|
|
"(none)"
|
|
};
|
|
|
|
let output = format!("{} - {} - {}", recipe.name, description, source_info);
|
|
if verbose {
|
|
println!(" {}", output);
|
|
if let Some(title) = &recipe.title {
|
|
println!(" Title: {}", title);
|
|
}
|
|
println!(" Path: {}", recipe.path);
|
|
} else {
|
|
println!("{}", output);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn generate_deeplink(recipe_name: &str) -> Result<(String, goose::recipe::Recipe)> {
|
|
let recipe_file = load_recipe_file(recipe_name)?;
|
|
// Load the recipe file first to validate it
|
|
let recipe = validate_recipe_template_from_file(&recipe_file)?;
|
|
match recipe_deeplink::encode(&recipe) {
|
|
Ok(encoded) => {
|
|
let full_url = format!("goose://recipe?config={}", encoded);
|
|
Ok((full_url, recipe))
|
|
}
|
|
Err(err) => Err(anyhow::anyhow!("Failed to encode recipe: {}", err)),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::fs;
|
|
use tempfile::TempDir;
|
|
|
|
fn create_test_recipe_file(dir: &TempDir, filename: &str, content: &str) -> String {
|
|
let file_path = dir.path().join(filename);
|
|
fs::write(&file_path, content).expect("Failed to write test recipe file");
|
|
file_path.to_string_lossy().into_owned()
|
|
}
|
|
|
|
const VALID_RECIPE_CONTENT: &str = r#"
|
|
title: "Test Recipe with Valid JSON Schema"
|
|
description: "A test recipe with valid JSON schema"
|
|
prompt: "Test prompt content"
|
|
instructions: "Test instructions"
|
|
response:
|
|
json_schema:
|
|
type: object
|
|
properties:
|
|
result:
|
|
type: string
|
|
description: "The result"
|
|
count:
|
|
type: number
|
|
description: "A count value"
|
|
required:
|
|
- result
|
|
"#;
|
|
|
|
const INVALID_RECIPE_CONTENT: &str = r#"
|
|
title: "Test Recipe"
|
|
description: "A test recipe for deeplink generation"
|
|
prompt: "Test prompt content {{ name }}"
|
|
instructions: "Test instructions"
|
|
"#;
|
|
|
|
#[test]
|
|
fn test_handle_deeplink_valid_recipe() {
|
|
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
|
let recipe_path =
|
|
create_test_recipe_file(&temp_dir, "test_recipe.yaml", VALID_RECIPE_CONTENT);
|
|
|
|
let result = handle_deeplink(&recipe_path);
|
|
assert!(result.is_ok());
|
|
let url = result.unwrap();
|
|
assert!(url.starts_with("goose://recipe?config="));
|
|
let encoded_part = url.strip_prefix("goose://recipe?config=").unwrap();
|
|
assert!(!encoded_part.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_handle_deeplink_invalid_recipe() {
|
|
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
|
let recipe_path =
|
|
create_test_recipe_file(&temp_dir, "test_recipe.yaml", INVALID_RECIPE_CONTENT);
|
|
let result = handle_deeplink(&recipe_path);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_handle_open_recipe() {
|
|
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
|
let recipe_path =
|
|
create_test_recipe_file(&temp_dir, "test_recipe.yaml", VALID_RECIPE_CONTENT);
|
|
|
|
// Test handle_open - should attempt to open but may fail (that's expected in test environment)
|
|
// We just want to ensure it doesn't panic and handles the error gracefully
|
|
let result = handle_open(&recipe_path);
|
|
// The result may be Ok or Err depending on whether the system can open the URL
|
|
// In a test environment, it will likely fail to open, but that's fine
|
|
// We're mainly testing that the function doesn't panic and processes the recipe correctly
|
|
match result {
|
|
Ok(_) => {
|
|
// Successfully opened (unlikely in test environment)
|
|
}
|
|
Err(_) => {
|
|
// Failed to open (expected in test environment) - this is fine
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_handle_validation_valid_recipe() {
|
|
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
|
let recipe_path =
|
|
create_test_recipe_file(&temp_dir, "test_recipe.yaml", VALID_RECIPE_CONTENT);
|
|
|
|
let result = handle_validate(&recipe_path);
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_handle_validation_invalid_recipe() {
|
|
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
|
let recipe_path =
|
|
create_test_recipe_file(&temp_dir, "test_recipe.yaml", INVALID_RECIPE_CONTENT);
|
|
let result = handle_validate(&recipe_path);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_generate_deeplink_valid_recipe() {
|
|
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
|
let recipe_path =
|
|
create_test_recipe_file(&temp_dir, "test_recipe.yaml", VALID_RECIPE_CONTENT);
|
|
|
|
let result = generate_deeplink(&recipe_path);
|
|
assert!(result.is_ok());
|
|
let (url, recipe) = result.unwrap();
|
|
assert!(url.starts_with("goose://recipe?config="));
|
|
assert_eq!(recipe.title, "Test Recipe with Valid JSON Schema");
|
|
assert_eq!(recipe.description, "A test recipe with valid JSON schema");
|
|
let encoded_part = url.strip_prefix("goose://recipe?config=").unwrap();
|
|
assert!(!encoded_part.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_generate_deeplink_invalid_recipe() {
|
|
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
|
let recipe_path =
|
|
create_test_recipe_file(&temp_dir, "test_recipe.yaml", INVALID_RECIPE_CONTENT);
|
|
|
|
let result = generate_deeplink(&recipe_path);
|
|
assert!(result.is_err());
|
|
}
|
|
}
|