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
7 changes: 7 additions & 0 deletions crates/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ After setup, inspect local readiness:
nemo-relay doctor
```

To troubleshoot a specific configuration file, pass it explicitly. Doctor reports a missing or
invalid file in its configuration checks instead of stopping before diagnostics:

```bash
nemo-relay --config /path/to/config.toml doctor
```

Run a supported agent through the gateway:

```bash
Expand Down
13 changes: 11 additions & 2 deletions crates/cli/src/commands/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,20 @@ pub(crate) struct AgentsCommand {
pub(crate) json: bool,
}

pub(super) async fn execute(command: DoctorCommand) -> Result<ExitCode, CliError> {
pub(super) async fn execute(
command: DoctorCommand,
server: &super::serve::ServerArgs,
) -> Result<ExitCode, CliError> {
if let Some(plugin) = command.plugin {
return execute_plugin_doctor(plugin, command.install_dir, command.json);
}
crate::diagnostics::run_doctor(command.agent.map(Into::into), command.json).await
let gateway_overrides = server.to_runtime();
crate::diagnostics::run_doctor(
command.agent.map(Into::into),
command.json,
&gateway_overrides,
)
.await
}

fn execute_plugin_doctor(
Expand Down
22 changes: 19 additions & 3 deletions crates/cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,24 @@ async fn dispatch(bootstrap_shutdown_token: Option<String>) -> Result<ExitCode,
_ => cli.server.config.as_deref(),
}
};
let config = cli.logging.resolve(explicit_config, user_only)?;
let mut logging_fallback_error = None;
let config = match cli.logging.resolve(explicit_config, user_only) {
Ok(config) => config,
Err(error) if matches!(cli.command.as_ref(), Some(Command::Doctor(_))) => {
logging_fallback_error = Some(error);
nemo_relay::logging::LoggingConfig::default()
}
Err(error) => return Err(error),
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let runtime = nemo_relay::logging::LoggingRuntime::configure(config)?;
if let Some(error) = logging_fallback_error {
log::warn!(
target: "nemo_relay.cli",
event = "doctor_logging_fallback",
error_kind = error.log_kind();
"Doctor fell back to default logging after resolution failure"
);
}
Some(runtime)
} else {
None
Expand Down Expand Up @@ -147,7 +163,7 @@ async fn run_command(command: Command, server: &ServerArgs) -> Result<ExitCode,
Command::Config(command) => configure::execute(command).await,
Command::Plugins(command) => plugins::execute(command, server),
Command::ModelPricing(command) => model_pricing::execute(command),
Command::Doctor(command) => diagnostics::execute(command).await,
Command::Doctor(command) => diagnostics::execute(command, server).await,
Command::Agents(command) => runtime_diagnostics::run_agents(command.json).await,
Command::Completions(command) => completions::execute(command),
}
Expand Down Expand Up @@ -195,7 +211,7 @@ async fn run_default(
.await?;
Ok(ExitCode::SUCCESS)
} else if runtime_configuration::any_config_file_exists() {
runtime_diagnostics::run_doctor(None, false).await
runtime_diagnostics::run_doctor(None, false, &runtime_args).await
} else {
configure::run(None).await?;
Ok(ExitCode::SUCCESS)
Expand Down
25 changes: 17 additions & 8 deletions crates/cli/src/configuration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1108,11 +1108,6 @@ fn user_config_scope() -> bool {
std::env::var("NEMO_RELAY_CONFIG_SCOPE").ok().as_deref() == Some("user")
}

/// Returns the implicit `plugins.toml` discovery paths used by the gateway and doctor.
pub(crate) fn default_plugin_config_paths() -> Vec<PathBuf> {
plugin_config_paths(None, None)
}

fn implicit_plugin_config_paths(
cwd: Option<&std::path::Path>,
user_config_dir: Option<PathBuf>,
Expand Down Expand Up @@ -1288,10 +1283,24 @@ fn load_plugin_toml_config_scoped(
))
}

/// Returns the plugin configuration paths selected by the same rules as runtime resolution.
///
/// Diagnostics use this so an explicit gateway configuration reports only its sibling
/// `plugins.toml`, rather than unrelated discovered plugin configuration.
pub(crate) fn diagnostic_plugin_config_paths(
explicit: Option<&PathBuf>,
plugin_config_path: Option<&PathBuf>,
) -> Vec<PathBuf> {
plugin_config_paths(explicit, plugin_config_path)
}

/// Returns the physical `plugins.toml` files that contribute effective runtime or dynamic
/// plugin configuration under the default discovery rules.
pub(crate) fn effective_plugin_toml_sources() -> Result<Vec<PathBuf>, CliError> {
let Some(config) = load_plugin_toml_config(None, None)? else {
/// plugin configuration for the selected gateway configuration scope.
pub(crate) fn effective_plugin_toml_sources(
explicit: Option<&PathBuf>,
plugin_config_path: Option<&PathBuf>,
) -> Result<Vec<PathBuf>, CliError> {
let Some(config) = load_plugin_toml_config(explicit, plugin_config_path)? else {
return Ok(Vec::new());
};
let mut sources = config.contributing_sources;
Expand Down
73 changes: 55 additions & 18 deletions crates/cli/src/diagnostics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use uuid::Uuid;
use crate::agents::CodingAgent;
use crate::configuration::{
AgentConfigs, DynamicPluginHostConfigStatus, GatewayConfig, ResolvedConfig,
default_plugin_config_paths, effective_plugin_toml_sources, resolve_server_config,
diagnostic_plugin_config_paths, effective_plugin_toml_sources, resolve_server_config,
};
use crate::error::CliError;
use crate::server::{GatewayOverrides, register_and_validate_plugin_components};
Expand All @@ -54,8 +54,9 @@ struct PluginConfigurationDiagnostics {
/// the first missing directory.
pub(crate) async fn collect_report(
target_agent: Option<CodingAgent>,
gateway_overrides: &GatewayOverrides,
) -> Result<DoctorReport, CliError> {
let (resolved, resolution) = match resolve_server_config(&GatewayOverrides::default()) {
let (resolved, resolution) = match resolve_server_config(gateway_overrides) {
Ok(resolved) => (
resolved,
Check {
Expand All @@ -77,15 +78,20 @@ pub(crate) async fn collect_report(
Check {
name: "Resolution",
status: Status::Fail,
details: format!("could not resolve merged config: {err}"),
details: format!(
"could not resolve merged config: {err}; repair or recreate the named configuration file, or rerun the setup or installer that manages it"
),
},
)
}
};
let cwd = std::env::current_dir().ok();
let home = home_dir();
let configured_agents = configured_agent_names(&resolved.agents);
let (plugin_sources, plugin_error) = match effective_plugin_toml_sources() {
let (plugin_sources, plugin_error) = match effective_plugin_toml_sources(
gateway_overrides.config.as_ref(),
gateway_overrides.plugin_config_path.as_ref(),
) {
Ok(sources) => (sources, None),
Err(error) => {
log::warn!(
Expand Down Expand Up @@ -114,6 +120,7 @@ pub(crate) async fn collect_report(
configuration: collect_configuration(
cwd.as_deref(),
home.as_deref(),
gateway_overrides,
resolution,
configured_agents,
&resolved.dynamic_plugins,
Expand All @@ -129,11 +136,13 @@ pub(crate) async fn collect_report(
fn collect_configuration(
cwd: Option<&Path>,
home: Option<&Path>,
gateway_overrides: &GatewayOverrides,
resolution: Check,
configured_agents: Vec<String>,
dynamic_plugins: &[crate::configuration::ResolvedDynamicPluginConfig],
plugin_diagnostics: &PluginConfigurationDiagnostics,
) -> ConfigurationInfo {
let explicit_config = gateway_overrides.config.is_some();
let workspace_path = cwd
.map(|p| p.join(".nemo-relay").join("config.toml"))
.unwrap_or_else(|| PathBuf::from(".nemo-relay/config.toml"));
Expand All @@ -144,21 +153,39 @@ fn collect_configuration(
.or_else(|| home.map(|h| h.join(".config").join("nemo-relay").join("config.toml")))
.unwrap_or_else(|| PathBuf::from("~/.config/nemo-relay/config.toml"));
let system_path = PathBuf::from("/etc/nemo-relay/config.toml");
let workspace = gateway_overrides
.config
.as_deref()
.map_or_else(|| layer_status(&workspace_path), layer_status);
let global = if explicit_config {
ignored_layer_status(&global_path)
} else {
layer_status(&global_path)
};
let system = if explicit_config {
ignored_layer_status(&system_path)
} else {
layer_status(&system_path)
};

ConfigurationInfo {
workspace: layer_status(&workspace_path),
global: layer_status(&global_path),
system: layer_status(&system_path),
plugin_configs: default_plugin_config_paths()
.iter()
.map(|path| {
plugin_layer_status(
path,
&plugin_diagnostics.sources,
plugin_diagnostics.error.as_deref(),
)
})
.collect(),
explicit_config,
workspace,
global,
system,
plugin_configs: diagnostic_plugin_config_paths(
gateway_overrides.config.as_ref(),
gateway_overrides.plugin_config_path.as_ref(),
)
.iter()
.map(|path| {
plugin_layer_status(
path,
&plugin_diagnostics.sources,
plugin_diagnostics.error.as_deref(),
)
})
.collect(),
plugin_resolution: plugin_diagnostics.resolution.clone(),
resolution,
// `default_agent` is reserved in the design for Phase 2 dispatch; not currently parsed
Expand Down Expand Up @@ -285,6 +312,15 @@ fn layer_status(path: &Path) -> ConfigLayer {
}
}

fn ignored_layer_status(path: &Path) -> ConfigLayer {
ConfigLayer {
path: path.to_path_buf(),
status: Status::Info,
active: false,
details: "not selected because --config scopes configuration".into(),
}
}

fn plugin_layer_status(
path: &Path,
contributing_paths: &[PathBuf],
Expand Down Expand Up @@ -1183,8 +1219,9 @@ pub(crate) fn format_agents_json(agents: &[AgentInfo]) -> Result<String, CliErro
pub(crate) async fn run_doctor(
target_agent: Option<CodingAgent>,
json: bool,
gateway_overrides: &GatewayOverrides,
) -> Result<std::process::ExitCode, CliError> {
let report = collect_report(target_agent).await?;
let report = collect_report(target_agent, gateway_overrides).await?;
log::info!(
target: "nemo_relay.diagnostics",
event = "diagnostics_completed",
Expand Down
2 changes: 2 additions & 0 deletions crates/cli/src/diagnostics/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ pub(crate) struct EnvironmentInfo {

#[derive(Debug, Clone, Serialize)]
pub(crate) struct ConfigurationInfo {
#[serde(skip)]
pub explicit_config: bool,
pub workspace: ConfigLayer,
pub global: ConfigLayer,
pub system: ConfigLayer,
Expand Down
7 changes: 6 additions & 1 deletion crates/cli/src/diagnostics/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,13 @@ pub(super) fn format_human_environment(out: &mut String, report: &DoctorReport)

pub(super) fn format_human_configuration(out: &mut String, report: &DoctorReport) {
out.push_str(" Configuration\n");
let workspace_label = if report.configuration.explicit_config {
"Explicit"
} else {
"Workspace"
};
out.push_str(&format!(
" Workspace {}\n",
" {workspace_label:<11}{}\n",
format_layer(&report.configuration.workspace)
));
out.push_str(&format!(
Expand Down
Loading
Loading