relay_knowledge/interfaces/cli/command/
diagnostics.rs1use 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#[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 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 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#[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}