Skip to main content

relay_knowledge/interfaces/cli/
mod.rs

1//! CLI adapter for the shared application service.
2
3mod 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/// Supported CLI output formats.
33#[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/// Parsed CLI command.
58#[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/// CLI action after global options are removed.
67#[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/// MCP transport option for foreground service mode.
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum ServiceMcpTransport {
167    Configured,
168    StreamableHttp,
169}
170
171/// Runs the CLI command and renders its response.
172#[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/// Rendered stdout/stderr for the process entry point.
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct CliProcessOutput {
185    pub stdout: String,
186    pub stderr: String,
187}
188
189/// Runs the CLI command and renders only the command result.
190#[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(&current)
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;