From c950670dca4983160085b140c125b8ca6d78dd09 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Wed, 29 Apr 2026 23:24:36 +0300 Subject: [PATCH] fix: run integration commands without server --- src/app/api.rs | 275 ++++------------------------------------- src/cli.rs | 67 ++++------ src/integration/mod.rs | 153 +++++++++++++++++++++++ tests/cli_wrapper.rs | 12 +- 4 files changed, 210 insertions(+), 297 deletions(-) diff --git a/src/app/api.rs b/src/app/api.rs index 5cfa3409..e689d89f 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -276,9 +276,9 @@ impl App { use bytes::Bytes; use crate::api::schema::{ - ErrorBody, ErrorResponse, IntegrationInstallResult, IntegrationTarget, - IntegrationUninstallResult, Method, PaneListParams, PaneReadResult, ReadSource, - ResponseResult, SuccessResponse, TabListParams, + ErrorBody, ErrorResponse, IntegrationInstallResult, IntegrationUninstallResult, Method, + PaneListParams, PaneReadResult, ReadSource, ResponseResult, SuccessResponse, + TabListParams, }; let response = match request.method { @@ -1252,107 +1252,17 @@ impl App { } Method::IntegrationInstall(params) => { let target = params.target; - let messages = match target { - IntegrationTarget::Pi => { - let path = crate::integration::install_pi().map_err(|err| { - serde_json::to_string(&ErrorResponse { - id: request.id.clone(), - error: ErrorBody { - code: "integration_install_failed".into(), - message: err.to_string(), - }, - }) - .unwrap() - }); - match path { - Ok(path) => { - crate::logging::integration_action("install", "pi", "ok"); - vec![format!("installed pi integration to {}", path.display())] - } - Err(response) => return response, - } - } - IntegrationTarget::Claude => { - let installed = crate::integration::install_claude().map_err(|err| { - serde_json::to_string(&ErrorResponse { - id: request.id.clone(), - error: ErrorBody { - code: "integration_install_failed".into(), - message: err.to_string(), - }, - }) - .unwrap() - }); - match installed { - Ok(installed) => { - crate::logging::integration_action("install", "claude", "ok"); - vec![ - format!( - "installed claude integration hook to {}", - installed.hook_path.display() - ), - format!( - "ensured claude settings at {}", - installed.settings_path.display() - ), - ] - } - Err(response) => return response, - } - } - IntegrationTarget::Codex => { - let installed = crate::integration::install_codex().map_err(|err| { - serde_json::to_string(&ErrorResponse { - id: request.id.clone(), - error: ErrorBody { - code: "integration_install_failed".into(), - message: err.to_string(), - }, - }) - .unwrap() - }); - match installed { - Ok(installed) => { - crate::logging::integration_action("install", "codex", "ok"); - vec![ - format!( - "installed codex integration hook to {}", - installed.hook_path.display() - ), - format!( - "ensured codex hooks at {}", - installed.hooks_path.display() - ), - format!( - "ensured codex config at {}", - installed.config_path.display() - ), - ] - } - Err(response) => return response, - } - } - IntegrationTarget::Opencode => { - let installed = crate::integration::install_opencode().map_err(|err| { - serde_json::to_string(&ErrorResponse { - id: request.id.clone(), - error: ErrorBody { - code: "integration_install_failed".into(), - message: err.to_string(), - }, - }) - .unwrap() - }); - match installed { - Ok(installed) => { - crate::logging::integration_action("install", "opencode", "ok"); - vec![format!( - "installed opencode integration plugin to {}", - installed.plugin_path.display() - )] - } - Err(response) => return response, - } + let messages = match crate::integration::install_target(target) { + Ok(messages) => messages, + Err(err) => { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "integration_install_failed".into(), + message: err.to_string(), + }, + }) + .unwrap(); } }; @@ -1366,152 +1276,17 @@ impl App { } Method::IntegrationUninstall(params) => { let target = params.target; - let messages = match target { - IntegrationTarget::Pi => { - let result = crate::integration::uninstall_pi().map_err(|err| { - serde_json::to_string(&ErrorResponse { - id: request.id.clone(), - error: ErrorBody { - code: "integration_uninstall_failed".into(), - message: err.to_string(), - }, - }) - .unwrap() - }); - match result { - Ok(result) => { - crate::logging::integration_action("uninstall", "pi", "ok"); - if result.removed_extension { - vec![format!( - "removed pi integration extension at {}", - result.extension_path.display() - )] - } else { - vec![format!( - "no pi integration extension found at {}", - result.extension_path.display() - )] - } - } - Err(response) => return response, - } - } - IntegrationTarget::Claude => { - let result = crate::integration::uninstall_claude().map_err(|err| { - serde_json::to_string(&ErrorResponse { - id: request.id.clone(), - error: ErrorBody { - code: "integration_uninstall_failed".into(), - message: err.to_string(), - }, - }) - .unwrap() - }); - match result { - Ok(result) => { - crate::logging::integration_action("uninstall", "claude", "ok"); - let mut messages = Vec::new(); - if result.removed_hook_file { - messages.push(format!( - "removed claude hook at {}", - result.hook_path.display() - )); - } else { - messages.push(format!( - "no claude hook found at {}", - result.hook_path.display() - )); - } - if result.updated_settings { - messages.push(format!( - "removed herdr claude hook entries from {}", - result.settings_path.display() - )); - } else { - messages.push(format!( - "no herdr claude hook entries found in {}", - result.settings_path.display() - )); - } - messages - } - Err(response) => return response, - } - } - IntegrationTarget::Codex => { - let result = crate::integration::uninstall_codex().map_err(|err| { - serde_json::to_string(&ErrorResponse { - id: request.id.clone(), - error: ErrorBody { - code: "integration_uninstall_failed".into(), - message: err.to_string(), - }, - }) - .unwrap() - }); - match result { - Ok(result) => { - crate::logging::integration_action("uninstall", "codex", "ok"); - let mut messages = Vec::new(); - if result.removed_hook_file { - messages.push(format!( - "removed codex hook at {}", - result.hook_path.display() - )); - } else { - messages.push(format!( - "no codex hook found at {}", - result.hook_path.display() - )); - } - if result.updated_hooks { - messages.push(format!( - "removed herdr codex hook entries from {}", - result.hooks_path.display() - )); - } else { - messages.push(format!( - "no herdr codex hook entries found in {}", - result.hooks_path.display() - )); - } - messages.push(format!( - "left codex config unchanged at {}", - result.config_path.display() - )); - messages - } - Err(response) => return response, - } - } - IntegrationTarget::Opencode => { - let result = crate::integration::uninstall_opencode().map_err(|err| { - serde_json::to_string(&ErrorResponse { - id: request.id.clone(), - error: ErrorBody { - code: "integration_uninstall_failed".into(), - message: err.to_string(), - }, - }) - .unwrap() - }); - match result { - Ok(result) => { - crate::logging::integration_action("uninstall", "opencode", "ok"); - if result.removed_plugin { - vec![format!( - "removed opencode integration plugin at {}", - result.plugin_path.display() - )] - } else { - vec![format!( - "no opencode integration plugin found at {}", - result.plugin_path.display() - )] - } - } - Err(response) => return response, - } + let messages = match crate::integration::uninstall_target(target) { + Ok(messages) => messages, + Err(err) => { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "integration_uninstall_failed".into(), + message: err.to_string(), + }, + }) + .unwrap(); } }; diff --git a/src/cli.rs b/src/cli.rs index 13482e69..1260ee6f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,12 +6,11 @@ use serde::Serialize; use crate::api; use crate::api::schema::{ - AgentStatus, EmptyParams, IntegrationInstallParams, IntegrationTarget, - IntegrationUninstallParams, Method, OutputMatch, PaneListParams, PaneReadParams, - PaneSendInputParams, PaneSendKeysParams, PaneSendTextParams, PaneSplitParams, PaneTarget, - PaneWaitForOutputParams, PingParams, ReadSource, Request, SplitDirection, Subscription, - TabCreateParams, TabListParams, TabRenameParams, TabTarget, WorkspaceCreateParams, - WorkspaceRenameParams, WorkspaceTarget, + AgentStatus, EmptyParams, IntegrationTarget, Method, OutputMatch, PaneListParams, + PaneReadParams, PaneSendInputParams, PaneSendKeysParams, PaneSendTextParams, PaneSplitParams, + PaneTarget, PaneWaitForOutputParams, PingParams, ReadSource, Request, SplitDirection, + Subscription, TabCreateParams, TabListParams, TabRenameParams, TabTarget, + WorkspaceCreateParams, WorkspaceRenameParams, WorkspaceTarget, }; pub enum CommandOutcome { @@ -962,26 +961,16 @@ fn integration_install(args: &[String]) -> std::io::Result { return Ok(2); }; - let response = send_request(&Request { - id: "cli:integration:install".into(), - method: Method::IntegrationInstall(IntegrationInstallParams { target }), - })?; - - if let Some(error) = response.get("error") { - eprintln!("{}", serde_json::to_string(error).unwrap()); - return Ok(1); + match crate::integration::install_target(target) { + Ok(messages) => { + print_integration_messages(messages); + Ok(0) + } + Err(err) => { + eprintln!("{err}"); + Ok(1) + } } - - let Some(messages) = response["result"]["details"]["messages"].as_array() else { - eprintln!("invalid integration install response"); - return Ok(1); - }; - - for message in messages.iter().filter_map(|entry| entry.as_str()) { - println!("{message}"); - } - - Ok(0) } fn integration_uninstall(args: &[String]) -> std::io::Result { @@ -989,26 +978,22 @@ fn integration_uninstall(args: &[String]) -> std::io::Result { return Ok(2); }; - let response = send_request(&Request { - id: "cli:integration:uninstall".into(), - method: Method::IntegrationUninstall(IntegrationUninstallParams { target }), - })?; - - if let Some(error) = response.get("error") { - eprintln!("{}", serde_json::to_string(error).unwrap()); - return Ok(1); + match crate::integration::uninstall_target(target) { + Ok(messages) => { + print_integration_messages(messages); + Ok(0) + } + Err(err) => { + eprintln!("{err}"); + Ok(1) + } } +} - let Some(messages) = response["result"]["details"]["messages"].as_array() else { - eprintln!("invalid integration uninstall response"); - return Ok(1); - }; - - for message in messages.iter().filter_map(|entry| entry.as_str()) { +fn print_integration_messages(messages: Vec) { + for message in messages { println!("{message}"); } - - Ok(0) } fn parse_integration_target( diff --git a/src/integration/mod.rs b/src/integration/mod.rs index 5d478d0e..402e29a7 100644 --- a/src/integration/mod.rs +++ b/src/integration/mod.rs @@ -69,6 +69,159 @@ pub(crate) fn apply_pane_env(cmd: &mut CommandBuilder, pane_id: PaneId) { cmd.env(HERDR_PANE_ID_ENV_VAR, format!("p_{}", pane_id.raw())); } +pub(crate) fn install_target( + target: crate::api::schema::IntegrationTarget, +) -> io::Result> { + let messages = match target { + crate::api::schema::IntegrationTarget::Pi => { + let path = install_pi()?; + vec![format!("installed pi integration to {}", path.display())] + } + crate::api::schema::IntegrationTarget::Claude => { + let installed = install_claude()?; + vec![ + format!( + "installed claude integration hook to {}", + installed.hook_path.display() + ), + format!( + "ensured claude settings at {}", + installed.settings_path.display() + ), + ] + } + crate::api::schema::IntegrationTarget::Codex => { + let installed = install_codex()?; + vec![ + format!( + "installed codex integration hook to {}", + installed.hook_path.display() + ), + format!("ensured codex hooks at {}", installed.hooks_path.display()), + format!( + "ensured codex config at {}", + installed.config_path.display() + ), + ] + } + crate::api::schema::IntegrationTarget::Opencode => { + let installed = install_opencode()?; + vec![format!( + "installed opencode integration plugin to {}", + installed.plugin_path.display() + )] + } + }; + + crate::logging::integration_action("install", integration_target_label(target), "ok"); + Ok(messages) +} + +pub(crate) fn uninstall_target( + target: crate::api::schema::IntegrationTarget, +) -> io::Result> { + let messages = match target { + crate::api::schema::IntegrationTarget::Pi => { + let result = uninstall_pi()?; + if result.removed_extension { + vec![format!( + "removed pi integration extension at {}", + result.extension_path.display() + )] + } else { + vec![format!( + "no pi integration extension found at {}", + result.extension_path.display() + )] + } + } + crate::api::schema::IntegrationTarget::Claude => { + let result = uninstall_claude()?; + let mut messages = Vec::new(); + if result.removed_hook_file { + messages.push(format!( + "removed claude hook at {}", + result.hook_path.display() + )); + } else { + messages.push(format!( + "no claude hook found at {}", + result.hook_path.display() + )); + } + if result.updated_settings { + messages.push(format!( + "removed herdr claude hook entries from {}", + result.settings_path.display() + )); + } else { + messages.push(format!( + "no herdr claude hook entries found in {}", + result.settings_path.display() + )); + } + messages + } + crate::api::schema::IntegrationTarget::Codex => { + let result = uninstall_codex()?; + let mut messages = Vec::new(); + if result.removed_hook_file { + messages.push(format!( + "removed codex hook at {}", + result.hook_path.display() + )); + } else { + messages.push(format!( + "no codex hook found at {}", + result.hook_path.display() + )); + } + if result.updated_hooks { + messages.push(format!( + "removed herdr codex hook entries from {}", + result.hooks_path.display() + )); + } else { + messages.push(format!( + "no herdr codex hook entries found in {}", + result.hooks_path.display() + )); + } + messages.push(format!( + "left codex config unchanged at {}", + result.config_path.display() + )); + messages + } + crate::api::schema::IntegrationTarget::Opencode => { + let result = uninstall_opencode()?; + if result.removed_plugin { + vec![format!( + "removed opencode integration plugin at {}", + result.plugin_path.display() + )] + } else { + vec![format!( + "no opencode integration plugin found at {}", + result.plugin_path.display() + )] + } + } + }; + + crate::logging::integration_action("uninstall", integration_target_label(target), "ok"); + Ok(messages) +} + +fn integration_target_label(target: crate::api::schema::IntegrationTarget) -> &'static str { + match target { + crate::api::schema::IntegrationTarget::Pi => "pi", + crate::api::schema::IntegrationTarget::Claude => "claude", + crate::api::schema::IntegrationTarget::Codex => "codex", + crate::api::schema::IntegrationTarget::Opencode => "opencode", + } +} + pub(crate) fn install_pi() -> io::Result { let dir = pi_extension_dir()?; if !dir.is_dir() { diff --git a/tests/cli_wrapper.rs b/tests/cli_wrapper.rs index e0dbeebb..035264df 100644 --- a/tests/cli_wrapper.rs +++ b/tests/cli_wrapper.rs @@ -796,7 +796,7 @@ fn named_sessions_use_separate_servers_and_workspace_state() { } #[test] -fn integration_commands_honor_socket_override_when_server_is_missing() { +fn integration_commands_run_locally_when_server_is_missing() { let base = unique_test_dir(); let home_dir = base.join("home"); let extensions_dir = home_dir.join(".pi/agent/extensions"); @@ -827,10 +827,10 @@ fn integration_commands_honor_socket_override_when_server_is_missing() { .env("HOME", &home_dir) .output() .unwrap(); - assert_eq!(integration_install.status.code(), Some(1)); + assert_eq!(integration_install.status.code(), Some(0)); assert!( - !expected_extension.exists(), - "integration install should not run local install logic when socket is missing" + expected_extension.exists(), + "integration install should write local files without a server" ); let integration_uninstall = Command::new(env!("CARGO_BIN_EXE_herdr")) @@ -839,10 +839,10 @@ fn integration_commands_honor_socket_override_when_server_is_missing() { .env("HOME", &home_dir) .output() .unwrap(); - assert_eq!(integration_uninstall.status.code(), Some(1)); + assert_eq!(integration_uninstall.status.code(), Some(0)); assert!( !expected_extension.exists(), - "integration uninstall should also be socket-backed when socket is missing" + "integration uninstall should remove local files without a server" ); cleanup_test_base(&base);