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
9 changes: 8 additions & 1 deletion crates/cli/src/agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,9 +521,11 @@ pub(crate) fn detected_install_integrations(candidates: &[CodingAgent]) -> Vec<C
.collect()
}

/// Returns hosts with persisted state, optionally including force-cleanup targets.
pub(crate) fn installed_integrations(
candidates: &[CodingAgent],
install_dir: Option<&Path>,
include_local_install: bool,
) -> Vec<CodingAgent> {
let install_dir = install_dir
.map(Path::to_path_buf)
Expand All @@ -533,6 +535,11 @@ pub(crate) fn installed_integrations(
.copied()
.filter(|agent| {
crate::installation::marketplace::persisted_state_exists(*agent, &install_dir)
|| (include_local_install
&& crate::installation::marketplace::force_cleanup_target_exists(
*agent,
&install_dir,
))
})
.collect()
}
Expand Down Expand Up @@ -562,7 +569,7 @@ pub(crate) fn collect_default_integration_readiness()
const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);

let install_dir = crate::installation::marketplace::default_marketplace_install_dir();
let agents = installed_integrations(&CodingAgent::ALL, Some(&install_dir));
let agents = installed_integrations(&CodingAgent::ALL, Some(&install_dir), false);
let pending = agents
.into_iter()
.map(|agent| spawn_integration_readiness(agent, install_dir.clone()))
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/commands/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ fn execute_plugin_doctor(
) -> Result<ExitCode, CliError> {
let candidates = plugin.agents();
let agents = if plugin.is_all() {
crate::agents::installed_integrations(&candidates, install_dir.as_deref())
crate::agents::installed_integrations(&candidates, install_dir.as_deref(), false)
} else {
candidates
};
Expand Down
10 changes: 9 additions & 1 deletion crates/cli/src/commands/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ pub(crate) struct UninstallCommand {
pub(crate) host: InstallTarget,
#[arg(long)]
pub(crate) install_dir: Option<PathBuf>,
/// Attempt all Relay-owned cleanup steps even when normal uninstall safety checks fail.
#[arg(long)]
pub(crate) force: bool,
#[arg(long)]
pub(crate) dry_run: bool,
}
Expand Down Expand Up @@ -71,6 +74,7 @@ impl UninstallCommand {
pub(crate) fn into_runtime(self) -> crate::installation::UninstallRequest {
crate::installation::UninstallRequest {
install_dir: self.install_dir,
force: self.force,
dry_run: self.dry_run,
}
}
Expand Down Expand Up @@ -103,7 +107,11 @@ pub(super) fn uninstall(command: UninstallCommand) -> Result<ExitCode, CliError>
let request = command.into_runtime();
let candidates = target.agents();
let agents = if target.is_all() {
crate::agents::installed_integrations(&candidates, request.install_dir.as_deref())
crate::agents::installed_integrations(
&candidates,
request.install_dir.as_deref(),
request.force,
)
} else {
candidates
};
Expand Down
28 changes: 21 additions & 7 deletions crates/cli/src/commands/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,26 @@ impl LoggingArgs {
&self,
explicit_config: Option<&Path>,
) -> Result<LoggingConfig, CliError> {
if let Some(config) = self.resolve_explicit()? {
return Ok(config);
}

crate::configuration::resolve_logging_config(explicit_config)
}

/// Resolves direct logging settings without consulting Relay configuration files discovered
/// from the environment. Commands that repair or remove Relay state use this so malformed
/// ambient configuration cannot block the operation.
pub(super) fn resolve_without_ambient_config(&self) -> Result<LoggingConfig, CliError> {
Ok(self.resolve_explicit()?.unwrap_or_default())
}

/// Resolves only command-line, log-file, and environment logging sources.
fn resolve_explicit(&self) -> Result<Option<LoggingConfig>, CliError> {
if let Some(path) = &self.config_path {
return LoggingConfig::from_file_path(path).map_err(logging_config_error);
return LoggingConfig::from_file_path(path)
.map(Some)
.map_err(logging_config_error);
}

if self.level.is_some() || self.stderr_format.is_some() {
Expand All @@ -55,14 +73,10 @@ impl LoggingArgs {
config.stderr_format =
LogFormat::parse(stderr_format).map_err(logging_config_error)?;
}
return Ok(config);
}

if let Some(config) = LoggingConfig::from_environment().map_err(logging_config_error)? {
return Ok(config);
return Ok(Some(config));
}

crate::configuration::resolve_logging_config(explicit_config)
LoggingConfig::from_environment().map_err(logging_config_error)
}
}

Expand Down
17 changes: 11 additions & 6 deletions crates/cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,18 @@ fn configure_logging(cli: &Cli) -> Result<LoggingSetup, error::CliError> {
});
}

let explicit_config = match cli.command.as_ref() {
Some(Command::Mcp) => None,
Some(Command::Run(command)) => command.config.as_deref().or(cli.server.config.as_deref()),
_ => cli.server.config.as_deref(),
};
let mut fallback_error = None;
let config = match cli.logging.resolve(explicit_config) {
let config = match cli.command.as_ref() {
// Uninstall uses persisted integration state, not Relay runtime configuration. Preserve
// direct logging settings while avoiding ambient config discovery that could block cleanup.
Some(Command::Uninstall(_)) => cli.logging.resolve_without_ambient_config(),
Some(Command::Mcp) => cli.logging.resolve(None),
Some(Command::Run(command)) => cli
.logging
.resolve(command.config.as_deref().or(cli.server.config.as_deref())),
_ => cli.logging.resolve(cli.server.config.as_deref()),
};
let config = match config {
Ok(config) => config,
Err(error) if matches!(cli.command.as_ref(), Some(Command::Doctor(_))) => {
fallback_error = Some(error);
Expand Down
104 changes: 103 additions & 1 deletion crates/cli/src/installation/marketplace/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ pub(crate) fn uninstall(
.unwrap_or_else(default_install_dir)
.canonicalize_or_self(),
operation_lock_dir,
force: false,
force: command.force,
dry_run: command.dry_run,
skip_doctor: true,
};
Expand Down Expand Up @@ -282,6 +282,37 @@ pub(crate) fn default_marketplace_install_dir() -> PathBuf {
default_install_dir().canonicalize_or_self()
}

/// Returns whether deterministic Relay artifacts for the host still exist locally.
pub(crate) fn local_install_exists(host: impl MarketplaceHost, install_dir: &Path) -> bool {
let layout = PluginLayout::new(host, install_dir);
layout.marketplace_root.exists() || layout.generation_lock.exists()
}

/// Returns whether force cleanup has a local artifact or live host registration to remove.
pub(crate) fn force_cleanup_target_exists(host: impl MarketplaceHost, install_dir: &Path) -> bool {
force_cleanup_target_exists_with_runner(host, install_dir, &RealCommandRunner)
}

fn force_cleanup_target_exists_with_runner(
host: impl MarketplaceHost,
install_dir: &Path,
runner: &dyn CommandRunner,
) -> bool {
if local_install_exists(host, install_dir) {
return true;
}
let options = PluginInstallOptions {
install_dir: install_dir.to_path_buf(),
operation_lock_dir: PathBuf::new(),
force: true,
dry_run: false,
skip_doctor: true,
};
host_registration_report(host, &options, runner).is_ok_and(|registration| {
registration.host_plugin_registered || registration.host_marketplace_registered
})
}

pub(crate) fn persisted_state_exists(host: impl MarketplaceHost, install_dir: &Path) -> bool {
state_path(host, install_dir).exists()
}
Expand Down Expand Up @@ -868,6 +899,9 @@ fn uninstall_host_locked(
runner: &dyn CommandRunner,
setup_runner: &dyn PluginSetupRunner,
) -> Result<(), String> {
if options.force {
return force_uninstall_host_locked(host, options, runner, setup_runner);
}
let state = read_state(host, &options.install_dir);
let layout = PluginLayout::new(host, &options.install_dir);
if let Some(state) = state.as_ref() {
Expand Down Expand Up @@ -923,6 +957,74 @@ fn uninstall_host_locked(
result
}

/// Attempts every deterministic Relay-owned cleanup step and reports all failures together.
fn force_uninstall_host_locked(
host: impl MarketplaceHost,
options: &PluginInstallOptions,
runner: &dyn CommandRunner,
setup_runner: &dyn PluginSetupRunner,
) -> Result<(), String> {
let layout = PluginLayout::new(host, &options.install_dir);
let mut errors = Vec::new();

if !options.dry_run
&& let Err(error) = setup_runner.refresh_gateway()
{
errors.push(format!("failed to stop the Relay-owned gateway: {error}"));
}
let host_setup_removed =
match run_plugin_uninstall(host, &layout.plugin_root, options, setup_runner) {
Ok(()) => true,
Err(error) => {
errors.push(format!("failed to remove Relay host setup: {error}"));
false
}
};
if let Err(error) = run_host_plugin_removal(host, options, runner) {
errors.push(format!("failed to unregister the host plugin: {error}"));
}
if let Err(error) = run_host_marketplace_removal(host, options, runner) {
errors.push(format!(
"failed to unregister the host marketplace: {error}"
));
}
if let Err(error) = remove_path(&layout.marketplace_root, options) {
errors.push(error);
}
if host_setup_removed && let Err(error) = remove_path(&layout.state_path, options) {
errors.push(error);
}
if !host_setup_removed
&& !layout.state_path.exists()
&& let Err(error) = write_state(&layout, options)
{
errors.push(format!(
"failed to preserve Relay cleanup retry state: {error}"
));
}
if !options.dry_run {
match fs::remove_file(&layout.generation_lock) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => errors.push(format!(
"failed to remove MCP generation lock {}: {error}",
layout.generation_lock.display()
)),
}
}

if errors.is_empty() {
println!("force-uninstalled {} plugin", host.label());
Ok(())
} else {
Err(format!(
"forced {} cleanup completed with errors: {}",
host.label(),
errors.join("; ")
))
}
}

fn retire_installed_generation(
host: impl MarketplaceHost,
plugin_root: &Path,
Expand Down
1 change: 1 addition & 0 deletions crates/cli/src/installation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ pub(crate) struct InstallRequest {
#[derive(Debug, Clone)]
pub(crate) struct UninstallRequest {
pub(crate) install_dir: Option<PathBuf>,
pub(crate) force: bool,
pub(crate) dry_run: bool,
}
Loading
Loading