relay_knowledge/interfaces/cli/
mod.rs1mod command;
4mod files;
5mod grammar;
6mod knowledge;
7pub(crate) mod map;
8mod operations;
9mod remote;
10mod render;
11mod repo;
12mod repo_set;
13mod runtime;
14mod service;
15mod setup;
16mod spec;
17mod version;
18
19use crate::{
20 api::ServicePlanRequest,
21 application::{KnowledgeMapService, ProcessRuntimeConfig},
22 domain::{FreshnessPolicy, IndexKind, ProposalState, WorkerKind},
23 paths::discover_repository_root,
24 project::KNOWLEDGE_MAP_RELATIVE_PATH,
25};
26
27pub use command::{CliDiagnostic, CliError};
28use render::{render_response, serialize_line};
29pub(crate) use runtime::run_command;
30pub use runtime::run_with_service;
31
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
34pub enum OutputFormat {
35 #[default]
36 Text,
37 Json,
38 Markdown,
39 StreamingJson,
40}
41
42impl OutputFormat {
43 fn as_str(self) -> &'static str {
44 match self {
45 Self::Text => "text",
46 Self::Json => "json",
47 Self::Markdown => "markdown",
48 Self::StreamingJson => "streaming-json",
49 }
50 }
51
52 fn is_machine_readable(self) -> bool {
53 matches!(self, Self::Json | Self::StreamingJson)
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct CliCommand {
60 pub action: CliAction,
61 pub format: OutputFormat,
62 pub remote_base_url: Option<String>,
63 pub help: bool,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum CliAction {
69 Status,
70 Ingest {
71 source_scope: String,
72 content: String,
73 entity_labels: Vec<String>,
74 },
75 Query {
76 query: String,
77 source_scope: Option<String>,
78 limit: usize,
79 freshness: FreshnessPolicy,
80 },
81 FilesIndex {
82 source_scope: Option<String>,
83 roots: Vec<String>,
84 },
85 FilesQuery {
86 query: String,
87 source_scope: Option<String>,
88 root_id: Option<String>,
89 limit: usize,
90 freshness: FreshnessPolicy,
91 },
92 FilesContentQuery {
93 query: String,
94 source_scope: Option<String>,
95 root_id: Option<String>,
96 limit: usize,
97 freshness: FreshnessPolicy,
98 },
99 GraphInspect,
100 IndexRefresh {
101 kinds: Vec<IndexKind>,
102 },
103 Map(map::MapCommand),
104 WorkerStatus {
105 kind: Option<WorkerKind>,
106 },
107 WorkerRunOnce {
108 kind: Option<WorkerKind>,
109 },
110 ProposalList {
111 state: Option<ProposalState>,
112 limit: usize,
113 },
114 ProposalShow {
115 proposal_id: String,
116 },
117 ProposalAccept {
118 proposal_id: String,
119 actor: String,
120 reason: Option<String>,
121 },
122 ProposalReject {
123 proposal_id: String,
124 actor: String,
125 reason: Option<String>,
126 },
127 ProposalSupersede {
128 proposal_id: String,
129 actor: String,
130 reason: Option<String>,
131 },
132 AuditQuery {
133 operation: Option<String>,
134 limit: usize,
135 },
136 ProviderProbe,
137 Repo(repo::RepoCommand),
138 RepoSet(repo_set::RepoSetCommand),
139 Health,
140 ServiceStatus,
141 ServicePlan(ServicePlanRequest),
142 ServiceDefinitionWrite,
143 ServiceOperatorStatus,
144 ServiceOperatorPause,
145 ServiceOperatorResume,
146 ServiceWorkerRun {
147 task_id: Option<String>,
148 },
149 ServiceRun {
150 mcp: ServiceMcpTransport,
151 web: bool,
152 },
153 SetupDoctor,
154 SetupProfile {
155 profile: setup::SetupProfile,
156 },
157 Version,
158 VersionCheck,
159 Help {
160 path: Vec<String>,
161 },
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum ServiceMcpTransport {
167 Configured,
168 StreamableHttp,
169}
170
171#[deprecated(since = "1.1.14", note = "use bootstrap::cli::run_process")]
173pub async fn run<I, S>(args: I) -> Result<String, CliError>
174where
175 I: IntoIterator<Item = S>,
176 S: Into<String>,
177{
178 let output = legacy_run_process(args, false).await?;
179 Ok(output.stdout)
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct CliProcessOutput {
185 pub stdout: String,
186 pub stderr: String,
187}
188
189#[deprecated(since = "1.1.14", note = "use bootstrap::cli::run_process")]
191pub async fn run_process<I, S>(
192 args: I,
193 interactive_text_output: bool,
194) -> Result<CliProcessOutput, CliError>
195where
196 I: IntoIterator<Item = S>,
197 S: Into<String>,
198{
199 legacy_run_process(args, interactive_text_output).await
200}
201
202async fn legacy_run_process<I, S>(
203 args: I,
204 _interactive_text_output: bool,
205) -> Result<CliProcessOutput, CliError>
206where
207 I: IntoIterator<Item = S>,
208 S: Into<String>,
209{
210 let command = CliCommand::parse(args)?;
211 let service = match &command.action {
212 CliAction::Map(map_command) if map_command.needs_repository_root() => {
213 Some(legacy_knowledge_map_service(command.format)?)
214 }
215 _ => None,
216 };
217 let stdout = run_command(command, service.as_ref(), ProcessRuntimeConfig::default()).await?;
218 Ok(CliProcessOutput {
219 stdout,
220 stderr: String::new(),
221 })
222}
223
224fn legacy_knowledge_map_service(format: OutputFormat) -> Result<KnowledgeMapService, CliError> {
225 let current = std::env::current_dir().map_err(|error| {
226 CliError::invalid_api_argument(
227 format!("failed to resolve current directory: {error}"),
228 format,
229 )
230 })?;
231 let root = discover_repository_root(¤t)
232 .map_err(|error| CliError::invalid_api_argument(error.to_string(), format))?
233 .ok_or_else(|| {
234 CliError::invalid_api_argument(
235 format!("failed to find repository root for {KNOWLEDGE_MAP_RELATIVE_PATH}"),
236 format,
237 )
238 })?;
239 Ok(KnowledgeMapService::new(root))
240}
241
242pub(crate) fn update_notice_command<I, S>(
243 args: I,
244 interactive_text_output: bool,
245) -> Option<CliCommand>
246where
247 I: IntoIterator<Item = S>,
248 S: Into<String>,
249{
250 let command = CliCommand::parse(args).ok()?;
251 (interactive_text_output && version::should_check_after_command(&command)).then_some(command)
252}
253
254pub(crate) async fn update_notice_for_runtime(
255 runtime: &crate::application::RuntimeConfiguration,
256) -> Option<String> {
257 version::update_notice_for_runtime(runtime).await
258}
259
260#[cfg(test)]
261use service::ensure_web_remote_bind_allowed;
262
263#[cfg(test)]
264#[path = "tests/naming.rs"]
265mod cli_naming_tests;
266
267#[cfg(test)]
268#[path = "tests/general.rs"]
269mod cli_tests;
270
271#[cfg(test)]
272#[path = "tests/parse.rs"]
273mod cli_parse_tests;
274
275#[cfg(test)]
276#[path = "tests/remote.rs"]
277mod remote_cli_tests;
278
279#[cfg(test)]
280#[path = "tests/map.rs"]
281mod cli_map_tests;
282
283#[cfg(test)]
284#[path = "tests/service.rs"]
285mod cli_service_tests;
286
287#[cfg(test)]
288#[allow(deprecated)]
289#[path = "tests/version.rs"]
290mod cli_version_tests;