relay_knowledge/interfaces/cli/command/
parse.rs1use super::super::{
2 CliAction, CliCommand, CliError, OutputFormat, files, grammar, knowledge, map, operations,
3 repo, repo_set, setup,
4};
5
6#[cfg(test)]
7#[path = "parse_tests.rs"]
8mod tests;
9
10impl OutputFormat {
11 pub fn parse(value: &str) -> Result<Self, CliError> {
13 match value {
14 "text" => Ok(Self::Text),
15 "json" => Ok(Self::Json),
16 "markdown" => Ok(Self::Markdown),
17 "streaming-json" => Ok(Self::StreamingJson),
18 other => Err(CliError::InvalidFormat(other.to_owned())),
19 }
20 }
21}
22
23impl CliCommand {
24 pub fn parse<I, S>(args: I) -> Result<Self, CliError>
26 where
27 I: IntoIterator<Item = S>,
28 S: Into<String>,
29 {
30 let tokens = args.into_iter().map(Into::into).collect::<Vec<_>>();
31 let mut action_tokens = Vec::new();
32 let mut format = OutputFormat::default();
33 let mut remote_base_url = None;
34 let mut help = false;
35 let mut version = false;
36 let mut command_seen = false;
37 let mut delimiter_value = false;
38 let mut index = 0;
39
40 while index < tokens.len() {
41 let arg = &tokens[index];
42 if delimiter_value {
43 action_tokens.push(arg.clone());
44 delimiter_value = false;
45 index += 1;
46 } else if arg == "--format" {
47 let value = tokens
48 .get(index + 1)
49 .ok_or(CliError::MissingFormatValue)?
50 .clone();
51 format = OutputFormat::parse(&value)?;
52 index += 2;
53 } else if let Some(value) = arg.strip_prefix("--format=") {
54 format = OutputFormat::parse(value)?;
55 index += 1;
56 } else if arg == "--remote" {
57 remote_base_url = Some(
58 tokens
59 .get(index + 1)
60 .ok_or(CliError::MissingValue("--remote"))?
61 .clone(),
62 );
63 index += 2;
64 } else if let Some(value) = arg.strip_prefix("--remote=") {
65 if value.trim().is_empty() {
66 return Err(CliError::MissingValue("--remote"));
67 }
68 remote_base_url = Some(value.to_owned());
69 index += 1;
70 } else if arg == "--help" || arg == "-h" {
71 help = true;
72 index += 1;
73 } else if arg == "--version" && !command_seen {
74 version = true;
75 index += 1;
76 } else if arg == "--" {
77 action_tokens.push(arg.clone());
78 delimiter_value = true;
79 index += 1;
80 } else if option_consumes_value(arg) {
81 action_tokens.push(arg.clone());
82 if let Some(value) = tokens.get(index + 1) {
83 action_tokens.push(value.clone());
84 index += 2;
85 } else {
86 index += 1;
87 }
88 } else {
89 command_seen |= is_command_word(arg);
90 action_tokens.push(arg.clone());
91 index += 1;
92 }
93 }
94
95 let action = if help {
96 CliAction::Help {
97 path: help_path(action_tokens),
98 }
99 } else if version {
100 if let Some(token) = action_tokens.first() {
101 let error = CliError::UnexpectedArgument(token.clone());
102 return Err(grammar::diagnose(&action_tokens, error, format));
103 }
104 CliAction::Version
105 } else {
106 match parse_action(action_tokens.clone()) {
107 Ok(action) => action,
108 Err(error) => return Err(grammar::diagnose(&action_tokens, error, format)),
109 }
110 };
111
112 Ok(Self {
113 action,
114 format,
115 remote_base_url,
116 help,
117 })
118 }
119}
120
121fn option_consumes_value(option: &str) -> bool {
122 matches!(
123 option,
124 "--source"
125 | "--content"
126 | "--entity"
127 | "--limit"
128 | "--freshness"
129 | "--kind"
130 | "--alias"
131 | "--path"
132 | "--language"
133 | "--ref"
134 | "--base"
135 | "--head"
136 | "--changed-path"
137 | "--query"
138 | "--description"
139 | "--id"
140 | "--priority"
141 | "--mcp"
142 | "--state"
143 | "--by"
144 | "--reason"
145 | "--operation"
146 | "--task-id"
147 | "--input"
148 | "--root"
149 | "--scope"
150 | "--topic"
151 | "--uri"
152 | "--target-version"
153 | "--install-dir"
154 )
155}
156
157fn is_command_word(token: &str) -> bool {
158 matches!(
159 token,
160 "status"
161 | "ingest"
162 | "query"
163 | "repo"
164 | "repo-set"
165 | "files"
166 | "map"
167 | "graph"
168 | "index"
169 | "worker"
170 | "proposal"
171 | "audit"
172 | "provider"
173 | "health"
174 | "service"
175 | "setup"
176 | "version"
177 | "help"
178 )
179}
180
181fn parse_action(tokens: Vec<String>) -> Result<CliAction, CliError> {
182 if tokens.is_empty() || tokens == ["status"] {
183 return Ok(CliAction::Status);
184 }
185
186 match tokens[0].as_str() {
187 "status" => Err(CliError::UnexpectedArgument(
188 tokens
189 .get(1)
190 .cloned()
191 .unwrap_or_else(|| "status".to_owned()),
192 )),
193 "ingest" => knowledge::parse_ingest(&tokens[1..]),
194 "query" => knowledge::parse_query(&tokens[1..]),
195 "files" => files::parse_files(&tokens[1..]),
196 "map" => map::parse_map(&tokens[1..]),
197 "repo" => repo::parse_repo(&tokens[1..]).map(CliAction::Repo),
198 "repo-set" => repo_set::parse_repo_set(&tokens[1..]).map(CliAction::RepoSet),
199 "graph" => knowledge::parse_graph(&tokens[1..]),
200 "index" => knowledge::parse_index(&tokens[1..]),
201 "worker" => operations::parse_worker(&tokens[1..]),
202 "proposal" => operations::parse_proposal(&tokens[1..]),
203 "audit" => operations::parse_audit(&tokens[1..]),
204 "provider" => parse_provider(&tokens[1..]),
205 "health" if tokens.len() == 1 => Ok(CliAction::Health),
206 "service" => operations::parse_service(&tokens[1..]),
207 "setup" => setup::parse_setup(&tokens[1..]),
208 "version" if tokens.len() == 1 => Ok(CliAction::Version),
209 "version" if tokens == ["version", "check"] => Ok(CliAction::VersionCheck),
210 "help" => Ok(CliAction::Help {
211 path: help_path(tokens[1..].to_vec()),
212 }),
213 other => Err(CliError::UnexpectedArgument(other.to_owned())),
214 }
215}
216
217fn help_path(tokens: Vec<String>) -> Vec<String> {
218 tokens
219 .into_iter()
220 .filter(|token| token != "--")
221 .filter(|token| !token.starts_with('-'))
222 .collect()
223}
224
225fn parse_provider(tokens: &[String]) -> Result<CliAction, CliError> {
226 if tokens == ["probe"] {
227 return Ok(CliAction::ProviderProbe);
228 }
229
230 Err(CliError::UnexpectedArgument(
231 tokens
232 .first()
233 .cloned()
234 .unwrap_or_else(|| "provider".to_owned()),
235 ))
236}