Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4

- name: Check for file changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # pin@v3
id: filter
Expand Down Expand Up @@ -101,7 +101,7 @@ jobs:
just check-openapi-schema

desktop-lint:
name: Lint Electron Desktop App
name: Test and Lint Electron Desktop App
runs-on: macos-latest
needs: changes
if: needs.changes.outputs.code == 'true' || github.event_name != 'pull_request'
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ jobs:
secrets:
WINDOW_SIGNING_ROLE: ${{ secrets.WINDOW_SIGNING_ROLE }}
WINDOW_SIGNING_ROLE_TAG: ${{ secrets.WINDOW_SIGNING_ROLE_TAG }}

WINDOWS_CODESIGN_CERTIFICATE: ${{ secrets.WINDOWS_CODESIGN_CERTIFICATE }}

release:
name: Release
runs-on: ubuntu-latest
Expand Down
42 changes: 42 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ run-ui:
@echo "Running UI..."
cd ui/desktop && npm install && npm run start-gui

run-ui-playwright:
#!/usr/bin/env sh
just release-binary
echo "Running UI with Playwright debugging..."
RUN_DIR="$HOME/goose-runs/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$RUN_DIR"
echo "Using isolated directory: $RUN_DIR"
cd ui/desktop && ENABLE_PLAYWRIGHT=true GOOSE_PATH_ROOT="$RUN_DIR" npm run start-gui

run-ui-only:
@echo "Running UI..."
cd ui/desktop && npm install && npm run start-gui
Expand Down Expand Up @@ -463,4 +472,4 @@ build-test-tools:

record-mcp-tests: build-test-tools
GOOSE_RECORD_MCP=1 cargo test --package goose --test mcp_integration_test
git add crates/goose/tests/mcp_replays/
git add crates/goose/tests/mcp_replays/
1 change: 1 addition & 0 deletions crates/goose-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ tokio-util = { version = "0.7.15", features = ["compat"] }
is-terminal = "0.4.16"
anstream = "0.6.18"
url = "2.5.7"
open = "5.3.2"

[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["wincred"] }
Expand Down
13 changes: 12 additions & 1 deletion crates/goose-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::commands::bench::agent_generator;
use crate::commands::configure::handle_configure;
use crate::commands::info::handle_info;
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
use crate::commands::schedule::{
handle_schedule_add, handle_schedule_cron_help, handle_schedule_list, handle_schedule_remove,
Expand Down Expand Up @@ -283,6 +283,14 @@ enum RecipeCommand {
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
#[command(about = "List available recipes")]
List {
Expand Down Expand Up @@ -1215,6 +1223,9 @@ pub async fn cli() -> Result<()> {
RecipeCommand::Deeplink { recipe_name } => {
handle_deeplink(&recipe_name)?;
}
RecipeCommand::Open { recipe_name } => {
handle_open(&recipe_name)?;
}
RecipeCommand::List { format, verbose } => {
handle_list(&format, verbose)?;
}
Expand Down
9 changes: 3 additions & 6 deletions crates/goose-cli/src/commands/info.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use anyhow::Result;
use console::style;
use etcetera::{choose_app_strategy, AppStrategy};
use goose::config::paths::Paths;
use goose::config::Config;
use serde_yaml;

Expand All @@ -9,11 +9,8 @@ fn print_aligned(label: &str, value: &str, width: usize) {
}

pub fn handle_info(verbose: bool) -> Result<()> {
let data_dir = choose_app_strategy(crate::APP_STRATEGY.clone())?;
let logs_dir = data_dir
.in_state_dir("logs")
.unwrap_or_else(|| data_dir.in_data_dir("logs"));
let sessions_dir = data_dir.in_data_dir("sessions");
let logs_dir = Paths::in_state_dir("logs");
let sessions_dir = Paths::in_data_dir("sessions");

// Get paths using a stored reference to the global config
let config = Config::global();
Expand Down
154 changes: 131 additions & 23 deletions crates/goose-cli/src/commands/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,36 +33,75 @@ pub fn handle_validate(recipe_name: &str) -> Result<()> {
///
/// # Arguments
///
/// * `file_path` - Path to the recipe file
/// * `recipe_name` - Path to the recipe file
///
/// # Returns
///
/// Result indicating success or failure
pub fn handle_deeplink(recipe_name: &str) -> Result<String> {
// Load the recipe file first to validate it
match load_recipe_for_validation(recipe_name) {
Ok(recipe) => match recipe_deeplink::encode(&recipe) {
Ok(encoded) => {
println!(
"{} Generated deeplink for: {}",
style("✓").green().bold(),
recipe.title
);
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))
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)
}
}
}

/// 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!("{} {}", style("✗").red().bold(), err);
println!(
"{} Failed to encode recipe: {}",
style("✗").red().bold(),
err
);
Err(err)
}
}
Expand Down Expand Up @@ -129,6 +168,27 @@ pub fn handle_list(format: &str, verbose: bool) -> Result<()> {
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)]
mod tests {
use super::*;
Expand Down Expand Up @@ -204,6 +264,28 @@ response:
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");
Expand Down Expand Up @@ -239,4 +321,30 @@ response:
.to_string()
.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());
}
}
Loading
Loading