fix: run integration commands without server

This commit is contained in:
Ogulcan Celik 2026-04-29 23:24:36 +03:00
parent eec56a56a9
commit c950670dca
4 changed files with 210 additions and 297 deletions

View File

@ -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();
}
};

View File

@ -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);
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<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(

View File

@ -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() {

View File

@ -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);