Skip to main content

relay_knowledge/interfaces/cli/command/
diagnostics.rs

1use std::{error::Error, fmt};
2
3use crate::api::ApiError;
4
5use super::super::OutputFormat;
6
7#[cfg(test)]
8#[path = "diagnostics_tests.rs"]
9mod tests;
10
11/// CLI adapter error.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum CliError {
14    Diagnostic(Box<CliDiagnostic>),
15    InvalidFormat(String),
16    InvalidCodeQueryKind(String),
17    InvalidSoftwareKind(String),
18    InvalidFreshness(String),
19    InvalidIndexKind(String),
20    InvalidMapSourceKind(String),
21    InvalidWorkerKind(String),
22    InvalidProposalState(String),
23    InvalidServiceAction(String),
24    InvalidLimit(String),
25    MissingFormatValue,
26    MissingValue(&'static str),
27    UnsupportedVersionFormat(OutputFormat),
28    UnknownHelpTopic(String),
29    UnexpectedArgument(String),
30    RuntimeConfigFailed(String),
31    ApiFailed(String),
32    ApiError {
33        error: Box<ApiError>,
34        format: OutputFormat,
35    },
36    ServiceRunFailed(String),
37    RenderFailed(String),
38}
39
40impl CliError {
41    pub(crate) fn api_failed(error: ApiError, format: OutputFormat) -> Self {
42        Self::ApiError {
43            error: Box::new(error),
44            format,
45        }
46    }
47
48    pub(crate) fn invalid_api_argument(message: impl Into<String>, format: OutputFormat) -> Self {
49        Self::api_failed(ApiError::invalid_argument(message), format)
50    }
51
52    /// Returns the process exit code for the error.
53    pub fn exit_code(&self) -> i32 {
54        match self {
55            Self::Diagnostic(_)
56            | Self::InvalidFormat(_)
57            | Self::InvalidCodeQueryKind(_)
58            | Self::InvalidSoftwareKind(_)
59            | Self::InvalidFreshness(_)
60            | Self::InvalidIndexKind(_)
61            | Self::InvalidMapSourceKind(_)
62            | Self::InvalidWorkerKind(_)
63            | Self::InvalidProposalState(_)
64            | Self::InvalidServiceAction(_)
65            | Self::InvalidLimit(_)
66            | Self::MissingFormatValue
67            | Self::MissingValue(_)
68            | Self::UnsupportedVersionFormat(_)
69            | Self::UnknownHelpTopic(_)
70            | Self::UnexpectedArgument(_) => 2,
71            Self::RuntimeConfigFailed(_)
72            | Self::ApiFailed(_)
73            | Self::ApiError { .. }
74            | Self::ServiceRunFailed(_)
75            | Self::RenderFailed(_) => 1,
76        }
77    }
78
79    /// Renders the process stderr payload for this error.
80    pub fn render_stderr(&self) -> String {
81        match self {
82            Self::Diagnostic(diagnostic) => diagnostic.render_stderr(),
83            Self::ApiError { error, format } if format.is_machine_readable() => {
84                serde_json::to_string(error).unwrap_or_else(|_| error.message.clone())
85            }
86            _ => self.to_string(),
87        }
88    }
89}
90
91impl fmt::Display for CliError {
92    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
93        match self {
94            Self::Diagnostic(diagnostic) => write!(formatter, "{}", diagnostic.render_text()),
95            Self::InvalidFormat(format) => write!(
96                formatter,
97                "invalid --format value '{format}', expected text, json, markdown, or streaming-json"
98            ),
99            Self::InvalidCodeQueryKind(value) => write!(
100                formatter,
101                "invalid --kind value '{value}', expected hybrid, symbol, definition, references, callers, callees, imports, or sbom"
102            ),
103            Self::InvalidSoftwareKind(value) => write!(
104                formatter,
105                "invalid --kind value '{value}', expected dependencies, sdks, files, topics, relationships, build, iac, design, or all"
106            ),
107            Self::InvalidFreshness(value) => write!(
108                formatter,
109                "invalid --freshness value '{value}', expected allow-stale, wait-until-fresh, or graph-only"
110            ),
111            Self::InvalidIndexKind(value) => write!(
112                formatter,
113                "invalid --kind value '{value}', expected bm25, semantic, or vector"
114            ),
115            Self::InvalidMapSourceKind(value) => write!(
116                formatter,
117                "invalid --kind value '{value}', expected repo, file, doc, config, db, ci, runtime, wiki, or monitoring"
118            ),
119            Self::InvalidWorkerKind(value) => write!(
120                formatter,
121                "invalid worker kind '{value}', expected embedding, ocr, vision, or extractor"
122            ),
123            Self::InvalidProposalState(value) => write!(
124                formatter,
125                "invalid proposal state '{value}', expected proposed, accepted, rejected, or superseded"
126            ),
127            Self::InvalidServiceAction(value) => write!(
128                formatter,
129                "invalid service action '{value}', expected install, upgrade, rollback, or uninstall"
130            ),
131            Self::InvalidLimit(value) => write!(formatter, "invalid --limit value '{value}'"),
132            Self::MissingFormatValue => write!(formatter, "missing value for --format"),
133            Self::MissingValue(flag) => write!(formatter, "missing value for {flag}"),
134            Self::UnsupportedVersionFormat(format) => {
135                write!(
136                    formatter,
137                    "version does not support --format {}",
138                    format.as_str()
139                )
140            }
141            Self::UnknownHelpTopic(topic) => write!(formatter, "unknown help topic '{topic}'"),
142            Self::UnexpectedArgument(argument) => {
143                write!(formatter, "unexpected argument '{argument}'")
144            }
145            Self::RuntimeConfigFailed(message) => {
146                write!(formatter, "failed to load runtime configuration: {message}")
147            }
148            Self::ApiFailed(message) => write!(formatter, "{message}"),
149            Self::ApiError { error, .. } => write!(formatter, "{}", error.message),
150            Self::ServiceRunFailed(message) => write!(formatter, "{message}"),
151            Self::RenderFailed(message) => write!(formatter, "failed to render output: {message}"),
152        }
153    }
154}
155
156impl Error for CliError {}
157
158/// Structured parse diagnostic produced from the CLI grammar.
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct CliDiagnostic {
161    message: String,
162    usage: Option<String>,
163    suggestion: Option<String>,
164    matched_path: Vec<String>,
165    unexpected_token: Option<String>,
166    expected: Vec<String>,
167    format: OutputFormat,
168}
169
170impl CliDiagnostic {
171    pub(in crate::interfaces::cli) fn new(
172        message: String,
173        usage: Option<String>,
174        suggestion: Option<String>,
175        matched_path: Vec<String>,
176        unexpected_token: Option<String>,
177        expected: Vec<String>,
178        format: OutputFormat,
179    ) -> Self {
180        Self {
181            message,
182            usage,
183            suggestion,
184            matched_path,
185            unexpected_token,
186            expected,
187            format,
188        }
189    }
190
191    fn render_text(&self) -> String {
192        let mut output = self.message.clone();
193        if let Some(suggestion) = &self.suggestion {
194            output.push_str("\nTry: ");
195            output.push_str(suggestion);
196        }
197        if let Some(usage) = &self.usage {
198            output.push_str("\nUsage: ");
199            output.push_str(usage);
200        }
201
202        output
203    }
204
205    fn render_stderr(&self) -> String {
206        if self.format.is_machine_readable() {
207            return serde_json::json!({
208                "error": self.message,
209                "usage": self.usage,
210                "suggestion": self.suggestion,
211                "matched_path": self.matched_path,
212                "unexpected_token": self.unexpected_token,
213                "expected": self.expected,
214            })
215            .to_string();
216        }
217
218        self.render_text()
219    }
220}