Add new subcommand for opening recipes in desktop app (#4970)
This commit is contained in:
@@ -58,6 +58,7 @@ tokio-util = { version = "0.7.15", features = ["compat"] }
|
|||||||
is-terminal = "0.4.16"
|
is-terminal = "0.4.16"
|
||||||
anstream = "0.6.18"
|
anstream = "0.6.18"
|
||||||
url = "2.5.7"
|
url = "2.5.7"
|
||||||
|
open = "5.3.2"
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
winapi = { version = "0.3", features = ["wincred"] }
|
winapi = { version = "0.3", features = ["wincred"] }
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::commands::bench::agent_generator;
|
|||||||
use crate::commands::configure::handle_configure;
|
use crate::commands::configure::handle_configure;
|
||||||
use crate::commands::info::handle_info;
|
use crate::commands::info::handle_info;
|
||||||
use crate::commands::project::{handle_project_default, handle_projects_interactive};
|
use crate::commands::project::{handle_project_default, handle_projects_interactive};
|
||||||
use crate::commands::recipe::{handle_deeplink, handle_list, handle_validate};
|
use crate::commands::recipe::{handle_deeplink, handle_list, handle_open, handle_validate};
|
||||||
// Import the new handlers from commands::schedule
|
// Import the new handlers from commands::schedule
|
||||||
use crate::commands::schedule::{
|
use crate::commands::schedule::{
|
||||||
handle_schedule_add, handle_schedule_cron_help, handle_schedule_list, handle_schedule_remove,
|
handle_schedule_add, handle_schedule_cron_help, handle_schedule_list, handle_schedule_remove,
|
||||||
@@ -283,6 +283,14 @@ enum RecipeCommand {
|
|||||||
recipe_name: String,
|
recipe_name: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Open a recipe in Goose Desktop
|
||||||
|
#[command(about = "Open a recipe in Goose Desktop")]
|
||||||
|
Open {
|
||||||
|
/// Recipe name to get recipe file to open
|
||||||
|
#[arg(help = "recipe name or full path to the recipe file")]
|
||||||
|
recipe_name: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// List available recipes
|
/// List available recipes
|
||||||
#[command(about = "List available recipes")]
|
#[command(about = "List available recipes")]
|
||||||
List {
|
List {
|
||||||
@@ -1215,6 +1223,9 @@ pub async fn cli() -> Result<()> {
|
|||||||
RecipeCommand::Deeplink { recipe_name } => {
|
RecipeCommand::Deeplink { recipe_name } => {
|
||||||
handle_deeplink(&recipe_name)?;
|
handle_deeplink(&recipe_name)?;
|
||||||
}
|
}
|
||||||
|
RecipeCommand::Open { recipe_name } => {
|
||||||
|
handle_open(&recipe_name)?;
|
||||||
|
}
|
||||||
RecipeCommand::List { format, verbose } => {
|
RecipeCommand::List { format, verbose } => {
|
||||||
handle_list(&format, verbose)?;
|
handle_list(&format, verbose)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,36 +33,75 @@ pub fn handle_validate(recipe_name: &str) -> Result<()> {
|
|||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `file_path` - Path to the recipe file
|
/// * `recipe_name` - Path to the recipe file
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// Result indicating success or failure
|
/// Result indicating success or failure
|
||||||
pub fn handle_deeplink(recipe_name: &str) -> Result<String> {
|
pub fn handle_deeplink(recipe_name: &str) -> Result<String> {
|
||||||
// Load the recipe file first to validate it
|
match generate_deeplink(recipe_name) {
|
||||||
match load_recipe_for_validation(recipe_name) {
|
Ok((deeplink_url, recipe)) => {
|
||||||
Ok(recipe) => match recipe_deeplink::encode(&recipe) {
|
println!(
|
||||||
Ok(encoded) => {
|
"{} Generated deeplink for: {}",
|
||||||
println!(
|
style("✓").green().bold(),
|
||||||
"{} Generated deeplink for: {}",
|
recipe.title
|
||||||
style("✓").green().bold(),
|
);
|
||||||
recipe.title
|
println!("{}", deeplink_url);
|
||||||
);
|
Ok(deeplink_url)
|
||||||
let full_url = format!("goose://recipe?config={}", encoded);
|
}
|
||||||
println!("{}", full_url);
|
|
||||||
Ok(full_url)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
println!(
|
|
||||||
"{} Failed to encode recipe: {}",
|
|
||||||
style("✗").red().bold(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
Err(anyhow::anyhow!("Failed to encode recipe: {}", err))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("{} {}", style("✗").red().bold(), err);
|
println!(
|
||||||
|
"{} Failed to encode recipe: {}",
|
||||||
|
style("✗").red().bold(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens a recipe in Goose Desktop
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `recipe_name` - Path to the recipe file
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Result indicating success or failure
|
||||||
|
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)
|
Err(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,6 +168,27 @@ pub fn handle_list(format: &str, verbose: bool) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper function to generate a deeplink
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `recipe_name` - Path to the recipe file
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Result containing the deeplink URL and recipe
|
||||||
|
fn generate_deeplink(recipe_name: &str) -> Result<(String, goose::recipe::Recipe)> {
|
||||||
|
// Load the recipe file first to validate it
|
||||||
|
let recipe = load_recipe_for_validation(recipe_name)?;
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -204,6 +264,28 @@ response:
|
|||||||
assert!(result.is_err());
|
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]
|
#[test]
|
||||||
fn test_handle_validation_valid_recipe() {
|
fn test_handle_validation_valid_recipe() {
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||||
@@ -239,4 +321,30 @@ response:
|
|||||||
.to_string()
|
.to_string()
|
||||||
.contains("JSON schema validation failed"));
|
.contains("JSON schema validation failed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user