1#[path = "cli_grammar.rs"]
4mod cli_grammar;
5#[path = "cli_render.rs"]
6mod cli_render;
7#[path = "cli_spec.rs"]
8mod cli_spec;
9#[path = "files_cli.rs"]
10mod files_cli;
11#[path = "knowledge_cli.rs"]
12mod knowledge_cli;
13#[path = "map_cli.rs"]
14mod map_cli;
15#[path = "ops_cli.rs"]
16mod ops_cli;
17#[path = "repo_cli.rs"]
18mod repo_cli;
19#[path = "repo_set_cli.rs"]
20mod repo_set_cli;
21#[path = "service_cli.rs"]
22mod service_cli;
23#[path = "setup_cli.rs"]
24mod setup_cli;
25#[path = "version_cli.rs"]
26mod version_cli;
27
28use std::{error::Error, fmt};
29
30use crate::{
31 api::{
32 ApiError, GraphInspectionRequest, HybridRetrievalRequest, IndexRefreshRequest,
33 IngestEvidence, IngestRequest, InterfaceKind, RequestContext,
34 },
35 application::RelayKnowledgeService,
36 domain::{FreshnessPolicy, IndexKind, ProposalState, ServiceManagerAction, WorkerKind},
37};
38
39use cli_render::{render_project_status, render_response, serialize_line};
40
41#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43pub enum OutputFormat {
44 #[default]
45 Text,
46 Json,
47 Markdown,
48 StreamingJson,
49}
50
51impl OutputFormat {
52 fn as_str(self) -> &'static str {
53 match self {
54 Self::Text => "text",
55 Self::Json => "json",
56 Self::Markdown => "markdown",
57 Self::StreamingJson => "streaming-json",
58 }
59 }
60
61 fn is_machine_readable(self) -> bool {
62 matches!(self, Self::Json | Self::StreamingJson)
63 }
64
65 pub fn parse(value: &str) -> Result<Self, CliError> {
67 match value {
68 "text" => Ok(Self::Text),
69 "json" => Ok(Self::Json),
70 "markdown" => Ok(Self::Markdown),
71 "streaming-json" => Ok(Self::StreamingJson),
72 other => Err(CliError::invalid_format(other)),
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct CliCommand {
80 pub action: CliAction,
81 pub format: OutputFormat,
82 pub help: bool,
83}
84
85impl CliCommand {
86 pub fn parse<I, S>(args: I) -> Result<Self, CliError>
88 where
89 I: IntoIterator<Item = S>,
90 S: Into<String>,
91 {
92 let tokens = args.into_iter().map(Into::into).collect::<Vec<_>>();
93 let mut action_tokens = Vec::new();
94 let mut format = OutputFormat::default();
95 let mut help = false;
96 let mut version = false;
97 let mut command_seen = false;
98 let mut delimiter_value = false;
99 let mut index = 0;
100
101 while index < tokens.len() {
102 let arg = &tokens[index];
103 if delimiter_value {
104 action_tokens.push(arg.clone());
105 delimiter_value = false;
106 index += 1;
107 } else if arg == "--format" {
108 let value = tokens
109 .get(index + 1)
110 .ok_or(CliError::MissingFormatValue)?
111 .clone();
112 format = OutputFormat::parse(&value)?;
113 index += 2;
114 } else if let Some(value) = arg.strip_prefix("--format=") {
115 format = OutputFormat::parse(value)?;
116 index += 1;
117 } else if arg == "--help" || arg == "-h" {
118 help = true;
119 index += 1;
120 } else if arg == "--version" && !command_seen {
121 version = true;
122 index += 1;
123 } else if arg == "--" {
124 action_tokens.push(arg.clone());
125 delimiter_value = true;
126 index += 1;
127 } else if option_consumes_value(arg) {
128 action_tokens.push(arg.clone());
129 if let Some(value) = tokens.get(index + 1) {
130 action_tokens.push(value.clone());
131 index += 2;
132 } else {
133 index += 1;
134 }
135 } else {
136 command_seen |= is_command_word(arg);
137 action_tokens.push(arg.clone());
138 index += 1;
139 }
140 }
141
142 let action = if help {
143 CliAction::Help {
144 path: help_path(action_tokens),
145 }
146 } else if version {
147 if let Some(token) = action_tokens.first() {
148 let error = CliError::UnexpectedArgument(token.clone());
149 return Err(cli_grammar::diagnose(&action_tokens, error, format));
150 }
151 CliAction::Version
152 } else {
153 match parse_action(action_tokens.clone()) {
154 Ok(action) => action,
155 Err(error) => return Err(cli_grammar::diagnose(&action_tokens, error, format)),
156 }
157 };
158
159 Ok(Self {
160 action,
161 format,
162 help,
163 })
164 }
165}
166
167fn option_consumes_value(option: &str) -> bool {
168 matches!(
169 option,
170 "--source"
171 | "--content"
172 | "--entity"
173 | "--limit"
174 | "--freshness"
175 | "--kind"
176 | "--alias"
177 | "--path"
178 | "--language"
179 | "--ref"
180 | "--base"
181 | "--head"
182 | "--query"
183 | "--description"
184 | "--id"
185 | "--priority"
186 | "--mcp"
187 | "--state"
188 | "--by"
189 | "--reason"
190 | "--operation"
191 | "--input"
192 | "--root"
193 | "--scope"
194 | "--topic"
195 | "--uri"
196 )
197}
198
199fn is_command_word(token: &str) -> bool {
200 matches!(
201 token,
202 "status"
203 | "ingest"
204 | "query"
205 | "repo"
206 | "repo-set"
207 | "files"
208 | "map"
209 | "graph"
210 | "index"
211 | "worker"
212 | "proposal"
213 | "audit"
214 | "provider"
215 | "health"
216 | "service"
217 | "setup"
218 | "version"
219 | "help"
220 )
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
225pub enum CliAction {
226 Status,
227 Ingest {
228 source_scope: String,
229 content: String,
230 entity_labels: Vec<String>,
231 },
232 Query {
233 query: String,
234 source_scope: Option<String>,
235 limit: usize,
236 freshness: FreshnessPolicy,
237 },
238 FilesIndex {
239 source_scope: Option<String>,
240 roots: Vec<String>,
241 },
242 FilesQuery {
243 query: String,
244 source_scope: Option<String>,
245 root_id: Option<String>,
246 limit: usize,
247 },
248 GraphInspect,
249 IndexRefresh {
250 kinds: Vec<IndexKind>,
251 },
252 Map(map_cli::MapCommand),
253 WorkerStatus {
254 kind: Option<WorkerKind>,
255 },
256 WorkerRunOnce {
257 kind: Option<WorkerKind>,
258 },
259 ProposalList {
260 state: Option<ProposalState>,
261 limit: usize,
262 },
263 ProposalShow {
264 proposal_id: String,
265 },
266 ProposalAccept {
267 proposal_id: String,
268 actor: String,
269 reason: Option<String>,
270 },
271 ProposalReject {
272 proposal_id: String,
273 actor: String,
274 reason: Option<String>,
275 },
276 ProposalSupersede {
277 proposal_id: String,
278 actor: String,
279 reason: Option<String>,
280 },
281 AuditQuery {
282 operation: Option<String>,
283 limit: usize,
284 },
285 ProviderProbe,
286 Repo(repo_cli::RepoCommand),
287 RepoSet(repo_set_cli::RepoSetCommand),
288 Health,
289 ServiceStatus,
290 ServicePlan {
291 action: ServiceManagerAction,
292 },
293 ServiceDefinitionWrite,
294 ServiceOperatorStatus,
295 ServiceOperatorPause,
296 ServiceOperatorResume,
297 ServiceRun {
298 mcp: ServiceMcpTransport,
299 web: bool,
300 },
301 SetupDoctor,
302 SetupProfile {
303 profile: setup_cli::SetupProfile,
304 },
305 Version,
306 VersionCheck,
307 Help {
308 path: Vec<String>,
309 },
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub enum ServiceMcpTransport {
315 Configured,
316 StreamableHttp,
317}
318
319#[derive(Debug, Clone, PartialEq, Eq)]
321pub enum CliError {
322 Diagnostic(Box<CliDiagnostic>),
323 InvalidFormat(String),
324 InvalidCodeQueryKind(String),
325 InvalidSoftwareKind(String),
326 InvalidFreshness(String),
327 InvalidIndexKind(String),
328 InvalidMapSourceKind(String),
329 InvalidWorkerKind(String),
330 InvalidProposalState(String),
331 InvalidServiceAction(String),
332 InvalidLimit(String),
333 MissingFormatValue,
334 MissingValue(&'static str),
335 UnsupportedVersionFormat(OutputFormat),
336 UnknownHelpTopic(String),
337 UnexpectedArgument(String),
338 RuntimeConfigFailed(String),
339 ApiFailed(String),
340 ApiError {
341 error: Box<ApiError>,
342 format: OutputFormat,
343 },
344 ServiceRunFailed(String),
345 RenderFailed(String),
346}
347
348impl CliError {
349 fn invalid_format(format: &str) -> Self {
350 Self::InvalidFormat(format.to_owned())
351 }
352
353 pub(super) fn api_failed(error: ApiError, format: OutputFormat) -> Self {
354 Self::ApiError {
355 error: Box::new(error),
356 format,
357 }
358 }
359
360 pub(super) fn invalid_api_argument(message: impl Into<String>, format: OutputFormat) -> Self {
361 Self::api_failed(ApiError::invalid_argument(message), format)
362 }
363
364 pub fn exit_code(&self) -> i32 {
366 match self {
367 Self::Diagnostic(_)
368 | Self::InvalidFormat(_)
369 | Self::InvalidCodeQueryKind(_)
370 | Self::InvalidSoftwareKind(_)
371 | Self::InvalidFreshness(_)
372 | Self::InvalidIndexKind(_)
373 | Self::InvalidMapSourceKind(_)
374 | Self::InvalidWorkerKind(_)
375 | Self::InvalidProposalState(_)
376 | Self::InvalidServiceAction(_)
377 | Self::InvalidLimit(_)
378 | Self::MissingFormatValue
379 | Self::MissingValue(_)
380 | Self::UnsupportedVersionFormat(_)
381 | Self::UnknownHelpTopic(_)
382 | Self::UnexpectedArgument(_) => 2,
383 Self::RuntimeConfigFailed(_)
384 | Self::ApiFailed(_)
385 | Self::ApiError { .. }
386 | Self::ServiceRunFailed(_)
387 | Self::RenderFailed(_) => 1,
388 }
389 }
390
391 pub fn render_stderr(&self) -> String {
393 match self {
394 Self::Diagnostic(diagnostic) => diagnostic.render_stderr(),
395 Self::ApiError { error, format } if format.is_machine_readable() => {
396 serde_json::to_string(error).unwrap_or_else(|_| error.message.clone())
397 }
398 _ => self.to_string(),
399 }
400 }
401}
402
403impl fmt::Display for CliError {
404 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
405 match self {
406 Self::Diagnostic(diagnostic) => write!(formatter, "{}", diagnostic.render_text()),
407 Self::InvalidFormat(format) => write!(
408 formatter,
409 "invalid --format value '{format}', expected text, json, markdown, or streaming-json"
410 ),
411 Self::InvalidCodeQueryKind(value) => write!(
412 formatter,
413 "invalid --kind value '{value}', expected hybrid, symbol, definition, references, callers, callees, imports, or sbom"
414 ),
415 Self::InvalidSoftwareKind(value) => write!(
416 formatter,
417 "invalid --kind value '{value}', expected dependencies, sdks, build, iac, design, or all"
418 ),
419 Self::InvalidFreshness(value) => write!(
420 formatter,
421 "invalid --freshness value '{value}', expected allow-stale, wait-until-fresh, or graph-only"
422 ),
423 Self::InvalidIndexKind(value) => write!(
424 formatter,
425 "invalid --kind value '{value}', expected bm25, semantic, or vector"
426 ),
427 Self::InvalidMapSourceKind(value) => write!(
428 formatter,
429 "invalid --kind value '{value}', expected repo, file, doc, config, db, ci, runtime, wiki, or monitoring"
430 ),
431 Self::InvalidWorkerKind(value) => write!(
432 formatter,
433 "invalid worker kind '{value}', expected embedding, ocr, vision, or extractor"
434 ),
435 Self::InvalidProposalState(value) => write!(
436 formatter,
437 "invalid proposal state '{value}', expected proposed, accepted, rejected, or superseded"
438 ),
439 Self::InvalidServiceAction(value) => write!(
440 formatter,
441 "invalid service action '{value}', expected install or uninstall"
442 ),
443 Self::InvalidLimit(value) => write!(formatter, "invalid --limit value '{value}'"),
444 Self::MissingFormatValue => write!(formatter, "missing value for --format"),
445 Self::MissingValue(flag) => write!(formatter, "missing value for {flag}"),
446 Self::UnsupportedVersionFormat(format) => {
447 write!(
448 formatter,
449 "version does not support --format {}",
450 format.as_str()
451 )
452 }
453 Self::UnknownHelpTopic(topic) => write!(formatter, "unknown help topic '{topic}'"),
454 Self::UnexpectedArgument(argument) => {
455 write!(formatter, "unexpected argument '{argument}'")
456 }
457 Self::RuntimeConfigFailed(message) => {
458 write!(formatter, "failed to load runtime configuration: {message}")
459 }
460 Self::ApiFailed(message) => write!(formatter, "{message}"),
461 Self::ApiError { error, .. } => write!(formatter, "{}", error.message),
462 Self::ServiceRunFailed(message) => write!(formatter, "{message}"),
463 Self::RenderFailed(message) => write!(formatter, "failed to render output: {message}"),
464 }
465 }
466}
467
468impl Error for CliError {}
469
470#[derive(Debug, Clone, PartialEq, Eq)]
472pub struct CliDiagnostic {
473 message: String,
474 usage: Option<String>,
475 suggestion: Option<String>,
476 matched_path: Vec<String>,
477 unexpected_token: Option<String>,
478 expected: Vec<String>,
479 format: OutputFormat,
480}
481
482impl CliDiagnostic {
483 fn new(
484 message: String,
485 usage: Option<String>,
486 suggestion: Option<String>,
487 matched_path: Vec<String>,
488 unexpected_token: Option<String>,
489 expected: Vec<String>,
490 format: OutputFormat,
491 ) -> Self {
492 Self {
493 message,
494 usage,
495 suggestion,
496 matched_path,
497 unexpected_token,
498 expected,
499 format,
500 }
501 }
502
503 fn render_text(&self) -> String {
504 let mut output = self.message.clone();
505 if let Some(suggestion) = &self.suggestion {
506 output.push_str("\nTry: ");
507 output.push_str(suggestion);
508 }
509 if let Some(usage) = &self.usage {
510 output.push_str("\nUsage: ");
511 output.push_str(usage);
512 }
513
514 output
515 }
516
517 fn render_stderr(&self) -> String {
518 if self.format.is_machine_readable() {
519 return serde_json::json!({
520 "error": self.message,
521 "usage": self.usage,
522 "suggestion": self.suggestion,
523 "matched_path": self.matched_path,
524 "unexpected_token": self.unexpected_token,
525 "expected": self.expected,
526 })
527 .to_string();
528 }
529
530 self.render_text()
531 }
532}
533
534pub async fn run<I, S>(args: I) -> Result<String, CliError>
536where
537 I: IntoIterator<Item = S>,
538 S: Into<String>,
539{
540 let command = CliCommand::parse(args)?;
541 run_command(command).await
542}
543
544#[derive(Debug, Clone, PartialEq, Eq)]
546pub struct CliProcessOutput {
547 pub stdout: String,
548 pub stderr: String,
549}
550
551pub async fn run_process<I, S>(
553 args: I,
554 _interactive_text_output: bool,
555) -> Result<CliProcessOutput, CliError>
556where
557 I: IntoIterator<Item = S>,
558 S: Into<String>,
559{
560 let command = CliCommand::parse(args)?;
561 let stdout = run_command(command).await?;
562
563 Ok(CliProcessOutput {
564 stdout,
565 stderr: String::new(),
566 })
567}
568
569pub async fn process_update_notice<I, S>(args: I, interactive_text_output: bool) -> Option<String>
571where
572 I: IntoIterator<Item = S>,
573 S: Into<String>,
574{
575 let command = CliCommand::parse(args).ok()?;
576 version_cli::update_notice_for_process(&command, interactive_text_output).await
577}
578
579async fn run_command(command: CliCommand) -> Result<String, CliError> {
580 if let CliAction::Help { path } = &command.action {
581 return cli_spec::render_help(path, command.format);
582 }
583 if command.action == CliAction::Version {
584 return version_cli::render_version(command.format);
585 }
586 if let CliAction::ServiceRun { mcp, web } = command.action.clone() {
587 return service_cli::run_service(mcp, web).await;
588 }
589 if let CliAction::Map(map_command) = command.action.clone() {
590 let context = RequestContext::for_interface(InterfaceKind::Cli);
591 return map_cli::run_map(map_command, context, command.format).await;
592 }
593
594 let service = RelayKnowledgeService::from_process_environment()
595 .await
596 .map_err(|error| CliError::RuntimeConfigFailed(error.to_string()))?;
597 let context = RequestContext::for_interface(InterfaceKind::Cli);
598
599 run_with_service(&service, command, context).await
600}
601
602pub async fn run_with_service(
604 service: &RelayKnowledgeService,
605 command: CliCommand,
606 context: RequestContext,
607) -> Result<String, CliError> {
608 let format = command.format;
609 if let Some(output) =
610 ops_cli::run_operational_action(service, &command.action, context.clone(), format).await?
611 {
612 return Ok(output);
613 }
614 if let Some(output) =
615 setup_cli::run_setup_action(service, &command.action, context.clone(), format)?
616 {
617 return Ok(output);
618 }
619 if let Some(output) =
620 files_cli::run_files(service, &command.action, context.clone(), format).await?
621 {
622 return Ok(output);
623 }
624 match command.action {
625 CliAction::Status => {
626 let response = service
627 .project_status(context)
628 .await
629 .map_err(|error| CliError::api_failed(error, format))?;
630
631 render_project_status(&response, format)
632 }
633 CliAction::Ingest {
634 source_scope,
635 content,
636 entity_labels,
637 } => {
638 let response = service
639 .ingest(
640 IngestRequest {
641 source_scope,
642 evidence: vec![IngestEvidence {
643 id: None,
644 source_path: None,
645 span: None,
646 confidence: None,
647 status: None,
648 content,
649 entity_labels,
650 extraction: None,
651 }],
652 relations: Vec::new(),
653 claims: Vec::new(),
654 events: Vec::new(),
655 },
656 context,
657 )
658 .await
659 .map_err(|error| CliError::api_failed(error, format))?;
660
661 render_response(
662 "knowledge.ingest",
663 response.metadata.clone(),
664 &response,
665 format,
666 )
667 }
668 CliAction::Query {
669 query,
670 source_scope,
671 limit,
672 freshness,
673 } => {
674 let response = service
675 .retrieve_context(
676 HybridRetrievalRequest {
677 query,
678 source_scope,
679 limit,
680 freshness,
681 },
682 context,
683 )
684 .await
685 .map_err(|error| CliError::api_failed(error, format))?;
686
687 render_response(
688 "knowledge.retrieve_context",
689 response.metadata.clone(),
690 &response,
691 format,
692 )
693 }
694 CliAction::GraphInspect => {
695 let response = service
696 .inspect_graph(GraphInspectionRequest { source_scope: None }, context)
697 .await
698 .map_err(|error| CliError::api_failed(error, format))?;
699
700 render_response(
701 "graph.inspect",
702 response.metadata.clone(),
703 &response,
704 format,
705 )
706 }
707 CliAction::IndexRefresh { kinds } => {
708 let response = service
709 .refresh_indexes(IndexRefreshRequest { kinds }, context)
710 .await
711 .map_err(|error| CliError::api_failed(error, format))?;
712
713 render_response(
714 "index.refresh",
715 response.metadata.clone(),
716 &response,
717 format,
718 )
719 }
720 CliAction::Map(command) => map_cli::run_map(command, context, format).await,
721 CliAction::Repo(command) => repo_cli::run_repo(service, command, context, format).await,
722 CliAction::RepoSet(command) => {
723 repo_set_cli::run_repo_set(service, command, context, format).await
724 }
725 CliAction::Health => {
726 let response = service
727 .health(context)
728 .await
729 .map_err(|error| CliError::api_failed(error, format))?;
730
731 render_response(
732 "service.health",
733 response.metadata.clone(),
734 &response,
735 format,
736 )
737 }
738 CliAction::ProviderProbe => {
739 let response = service
740 .probe_embedding_provider(context)
741 .await
742 .map_err(|error| CliError::api_failed(error, format))?;
743
744 render_response(
745 "provider.embedding.probe",
746 response.metadata.clone(),
747 &response,
748 format,
749 )
750 }
751 CliAction::VersionCheck => version_cli::run_version_check(service, format).await,
752 CliAction::ServiceRun { .. } => Err(CliError::ServiceRunFailed(
753 "service run requires process runtime".to_owned(),
754 )),
755 CliAction::Help { path } => cli_spec::render_help(&path, format),
756 CliAction::WorkerStatus { .. }
757 | CliAction::FilesIndex { .. }
758 | CliAction::FilesQuery { .. }
759 | CliAction::WorkerRunOnce { .. }
760 | CliAction::ProposalList { .. }
761 | CliAction::ProposalShow { .. }
762 | CliAction::ProposalAccept { .. }
763 | CliAction::ProposalReject { .. }
764 | CliAction::ProposalSupersede { .. }
765 | CliAction::AuditQuery { .. }
766 | CliAction::ServiceStatus
767 | CliAction::ServicePlan { .. }
768 | CliAction::ServiceDefinitionWrite
769 | CliAction::ServiceOperatorStatus
770 | CliAction::ServiceOperatorPause
771 | CliAction::ServiceOperatorResume
772 | CliAction::SetupDoctor
773 | CliAction::SetupProfile { .. } => Err(CliError::ApiFailed(
774 "operational command was not handled by the service adapter".to_owned(),
775 )),
776 CliAction::Version => version_cli::render_version(command.format),
777 }
778}
779
780fn parse_action(tokens: Vec<String>) -> Result<CliAction, CliError> {
781 if tokens.is_empty() || tokens == ["status"] {
782 return Ok(CliAction::Status);
783 }
784
785 match tokens[0].as_str() {
786 "status" => Err(CliError::UnexpectedArgument(
787 tokens
788 .get(1)
789 .cloned()
790 .unwrap_or_else(|| "status".to_owned()),
791 )),
792 "ingest" => knowledge_cli::parse_ingest(&tokens[1..]),
793 "query" => knowledge_cli::parse_query(&tokens[1..]),
794 "files" => files_cli::parse_files(&tokens[1..]),
795 "map" => map_cli::parse_map(&tokens[1..]),
796 "repo" => repo_cli::parse_repo(&tokens[1..]).map(CliAction::Repo),
797 "repo-set" => repo_set_cli::parse_repo_set(&tokens[1..]).map(CliAction::RepoSet),
798 "graph" => knowledge_cli::parse_graph(&tokens[1..]),
799 "index" => knowledge_cli::parse_index(&tokens[1..]),
800 "worker" => ops_cli::parse_worker(&tokens[1..]),
801 "proposal" => ops_cli::parse_proposal(&tokens[1..]),
802 "audit" => ops_cli::parse_audit(&tokens[1..]),
803 "provider" => parse_provider(&tokens[1..]),
804 "health" if tokens.len() == 1 => Ok(CliAction::Health),
805 "service" => ops_cli::parse_service(&tokens[1..]),
806 "setup" => setup_cli::parse_setup(&tokens[1..]),
807 "version" if tokens.len() == 1 => Ok(CliAction::Version),
808 "version" if tokens == ["version", "check"] => Ok(CliAction::VersionCheck),
809 "help" => Ok(CliAction::Help {
810 path: help_path(tokens[1..].to_vec()),
811 }),
812 other => Err(CliError::UnexpectedArgument(other.to_owned())),
813 }
814}
815
816fn help_path(tokens: Vec<String>) -> Vec<String> {
817 tokens
818 .into_iter()
819 .filter(|token| token != "--")
820 .filter(|token| !token.starts_with('-'))
821 .collect()
822}
823
824fn parse_provider(tokens: &[String]) -> Result<CliAction, CliError> {
825 if tokens == ["probe"] {
826 return Ok(CliAction::ProviderProbe);
827 }
828
829 Err(CliError::UnexpectedArgument(
830 tokens
831 .first()
832 .cloned()
833 .unwrap_or_else(|| "provider".to_owned()),
834 ))
835}
836
837pub(super) fn value_after(
838 tokens: &[String],
839 index: usize,
840 flag: &'static str,
841) -> Result<String, CliError> {
842 tokens
843 .get(index + 1)
844 .cloned()
845 .ok_or(CliError::MissingValue(flag))
846}
847
848pub(super) fn parse_freshness(value: &str) -> Result<FreshnessPolicy, CliError> {
849 match value {
850 "allow-stale" => Ok(FreshnessPolicy::AllowStale),
851 "wait-until-fresh" => Ok(FreshnessPolicy::WaitUntilFresh),
852 "graph-only" => Ok(FreshnessPolicy::GraphOnly),
853 other => Err(CliError::InvalidFreshness(other.to_owned())),
854 }
855}
856
857#[cfg(test)]
858use service_cli::ensure_web_remote_bind_allowed;
859
860#[cfg(test)]
861#[path = "cli_naming_tests.rs"]
862mod cli_naming_tests;
863
864#[cfg(test)]
865#[path = "cli_tests.rs"]
866mod cli_tests;
867
868#[cfg(test)]
869#[path = "cli_map_tests.rs"]
870mod cli_map_tests;
871
872#[cfg(test)]
873#[path = "cli_service_tests.rs"]
874mod cli_service_tests;
875
876#[cfg(test)]
877#[path = "cli_version_tests.rs"]
878mod cli_version_tests;