1use crate::{
2 api::{
3 GraphInspectionRequest, HybridRetrievalRequest, IndexRefreshRequest, IngestEvidence,
4 IngestRequest, InterfaceKind, RequestContext,
5 },
6 application::{
7 KnowledgeMapService, ProcessRuntimeConfig, RelayKnowledgeService, RuntimeConfiguration,
8 },
9 env::{EnvironmentConfig, RemoteCliEnvironmentConfig},
10};
11
12use super::{
13 super::{
14 CliAction, CliCommand, CliError, files, map, operations, remote,
15 render::{render_project_status, render_response},
16 repo, repo_set, service, setup, spec, version,
17 },
18 selection::{remote_environment_needed, select_remote_base_url},
19};
20
21#[cfg(test)]
22#[path = "dispatch_tests.rs"]
23mod tests;
24
25pub(crate) async fn run_command(
26 command: CliCommand,
27 knowledge_map_service: Option<&KnowledgeMapService>,
28 process: ProcessRuntimeConfig,
29) -> Result<String, CliError> {
30 if let CliAction::Help { path } = &command.action {
31 return spec::render_help(path, command.format);
32 }
33 if command.action == CliAction::Version {
34 return version::render_version(command.format);
35 }
36 if let CliAction::ServiceRun { mcp, web } = command.action.clone() {
37 return service::run_service(mcp, web, process).await;
38 }
39 if let CliAction::Map(map_command) = command.action.clone() {
40 let context = RequestContext::for_interface(InterfaceKind::Cli);
41 return map::run_map(map_command, knowledge_map_service, context, command.format).await;
42 }
43
44 let context = RequestContext::for_interface(InterfaceKind::Cli);
45 if remote_environment_needed(&command) {
46 let remote_environment = RemoteCliEnvironmentConfig::from_process()
47 .map_err(|error| CliError::RuntimeConfigFailed(error.to_string()))?;
48 if let Some(base_url) =
49 select_remote_base_url(&command, remote_environment.remote_cli.base_url)
50 {
51 let remote_output = remote::run_remote(
52 &remote_environment.network,
53 &base_url,
54 &command.action,
55 context.clone(),
56 command.format,
57 )
58 .await?;
59 if let Some(output) = remote_output {
60 return Ok(output);
61 }
62 return Err(remote_unsupported_error());
63 }
64 }
65
66 let environment = EnvironmentConfig::from_process()
67 .map_err(|error| CliError::RuntimeConfigFailed(error.to_string()))?;
68 if let Some(base_url) =
69 select_remote_base_url(&command, environment.remote_cli.base_url.clone())
70 {
71 let remote_output = remote::run_remote(
72 &environment.network,
73 &base_url,
74 &command.action,
75 context.clone(),
76 command.format,
77 )
78 .await?;
79 if let Some(output) = remote_output {
80 return Ok(output);
81 }
82 return Err(remote_unsupported_error());
83 }
84
85 let runtime = RuntimeConfiguration::from_environment_with_process(&environment, process)
86 .await
87 .map_err(|error| CliError::RuntimeConfigFailed(error.to_string()))?;
88 let service = RelayKnowledgeService::new(runtime);
89
90 run_with_service(&service, command, context).await
91}
92
93fn remote_unsupported_error() -> CliError {
94 CliError::ApiFailed(
95 "remote CLI mode supports repo list, repo index, repo scope preview, repo status, repo query, repo graph, repo context, repo feature-flags, repo impact, repo report, repo software, and repo view"
96 .to_owned(),
97 )
98}
99
100pub async fn run_with_service(
102 service: &RelayKnowledgeService,
103 command: CliCommand,
104 context: RequestContext,
105) -> Result<String, CliError> {
106 let format = command.format;
107 if let Some(output) =
108 operations::run_operational_action(service, &command.action, context.clone(), format)
109 .await?
110 {
111 return Ok(output);
112 }
113 if let Some(output) =
114 setup::run_setup_action(service, &command.action, context.clone(), format)?
115 {
116 return Ok(output);
117 }
118 if let Some(output) =
119 files::run_files(service, &command.action, context.clone(), format).await?
120 {
121 return Ok(output);
122 }
123 match command.action {
124 CliAction::Status => {
125 let response = service
126 .project_status(context)
127 .await
128 .map_err(|error| CliError::api_failed(error, format))?;
129
130 render_project_status(&response, format)
131 }
132 CliAction::Ingest {
133 source_scope,
134 content,
135 entity_labels,
136 } => {
137 let response = service
138 .ingest(
139 IngestRequest {
140 source_scope,
141 evidence: vec![IngestEvidence {
142 id: None,
143 source_path: None,
144 span: None,
145 confidence: None,
146 status: None,
147 content,
148 entity_labels,
149 extraction: None,
150 }],
151 relations: Vec::new(),
152 claims: Vec::new(),
153 events: Vec::new(),
154 },
155 context,
156 )
157 .await
158 .map_err(|error| CliError::api_failed(error, format))?;
159
160 render_response(
161 "knowledge.ingest",
162 response.metadata.clone(),
163 &response,
164 format,
165 )
166 }
167 CliAction::Query {
168 query,
169 source_scope,
170 limit,
171 freshness,
172 } => {
173 let response = service
174 .retrieve_context(
175 HybridRetrievalRequest {
176 query,
177 source_scope,
178 limit,
179 freshness,
180 },
181 context,
182 )
183 .await
184 .map_err(|error| CliError::api_failed(error, format))?;
185
186 render_response(
187 "knowledge.retrieve_context",
188 response.metadata.clone(),
189 &response,
190 format,
191 )
192 }
193 CliAction::GraphInspect => {
194 let response = service
195 .inspect_graph(GraphInspectionRequest { source_scope: None }, context)
196 .await
197 .map_err(|error| CliError::api_failed(error, format))?;
198
199 render_response(
200 "graph.inspect",
201 response.metadata.clone(),
202 &response,
203 format,
204 )
205 }
206 CliAction::IndexRefresh { kinds } => {
207 let response = service
208 .refresh_indexes(IndexRefreshRequest { kinds }, context)
209 .await
210 .map_err(|error| CliError::api_failed(error, format))?;
211
212 render_response(
213 "index.refresh",
214 response.metadata.clone(),
215 &response,
216 format,
217 )
218 }
219 CliAction::Map(command) => map::run_map(command, None, context, format).await,
220 CliAction::Repo(command) => repo::run_repo(service, command, context, format).await,
221 CliAction::RepoSet(command) => {
222 repo_set::run_repo_set(service, command, context, format).await
223 }
224 CliAction::Health => {
225 let response = service
226 .health(context)
227 .await
228 .map_err(|error| CliError::api_failed(error, format))?;
229
230 render_response(
231 "service.health",
232 response.metadata.clone(),
233 &response,
234 format,
235 )
236 }
237 CliAction::ProviderProbe => {
238 let response = service
239 .probe_embedding_provider(context)
240 .await
241 .map_err(|error| CliError::api_failed(error, format))?;
242
243 render_response(
244 "provider.embedding.probe",
245 response.metadata.clone(),
246 &response,
247 format,
248 )
249 }
250 CliAction::VersionCheck => version::run_version_check(service, format).await,
251 CliAction::ServiceRun { .. } => Err(CliError::ServiceRunFailed(
252 "service run requires process runtime".to_owned(),
253 )),
254 CliAction::Help { path } => spec::render_help(&path, format),
255 CliAction::WorkerStatus { .. }
256 | CliAction::FilesIndex { .. }
257 | CliAction::FilesQuery { .. }
258 | CliAction::FilesContentQuery { .. }
259 | CliAction::WorkerRunOnce { .. }
260 | CliAction::ProposalList { .. }
261 | CliAction::ProposalShow { .. }
262 | CliAction::ProposalAccept { .. }
263 | CliAction::ProposalReject { .. }
264 | CliAction::ProposalSupersede { .. }
265 | CliAction::AuditQuery { .. }
266 | CliAction::ServiceStatus
267 | CliAction::ServicePlan(_)
268 | CliAction::ServiceDefinitionWrite
269 | CliAction::ServiceOperatorStatus
270 | CliAction::ServiceOperatorPause
271 | CliAction::ServiceOperatorResume
272 | CliAction::ServiceWorkerRun { .. }
273 | CliAction::SetupDoctor
274 | CliAction::SetupProfile { .. } => Err(CliError::ApiFailed(
275 "operational command was not handled by the service adapter".to_owned(),
276 )),
277 CliAction::Version => version::render_version(command.format),
278 }
279}