aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-29 23:24:36 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-29 23:24:36 +0300
commitc950670dca4983160085b140c125b8ca6d78dd09 (patch)
tree3c662cfe25bdfbe4489ceeb9e5373e746d717de7
parenteec56a56a9f891de7a130536044a3ff875af0567 (diff)
fix: run integration commands without server
-rw-r--r--src/app/api.rs275
-rw-r--r--src/cli.rs67
-rw-r--r--src/integration/mod.rs153
-rw-r--r--tests/cli_wrapper.rs12
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<i32> {
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);
- }
-
- 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}");
+ match crate::integration::install_target(target) {
+ Ok(messages) => {
+ print_integration_messages(messages);
+ Ok(0)
+ }
+ Err(err) => {
+ eprintln!("{err}");
+ Ok(1)
+ }
}
-
- Ok(0)
}
fn integration_uninstall(args: &[String]) -> std::io::Result<i32> {
@@ -989,26 +978,22 @@ fn integration_uninstall(args: &[String]) -> std::io::Result<i32> {
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<String>) {
+ 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<Vec<String>> {
+ 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<Vec<String>> {
+ 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<PathBuf> {
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);