Skip to main content

relay_knowledge/bootstrap/
cli.rs

1//! CLI process bootstrap entry points.
2//!
3//! These functions are the documented process-level facade for CLI execution.
4//! They own process inputs such as cwd and delegate command behavior to the
5//! interface layer after process-local contracts are resolved.
6
7use crate::{
8    api::{InterfaceKind, RequestContext},
9    application::KnowledgeMapService,
10    interfaces::cli::{CliAction, CliCommand, OutputFormat},
11    paths::discover_repository_root,
12    project::KNOWLEDGE_MAP_RELATIVE_PATH,
13};
14
15pub use crate::interfaces::cli::{CliError, CliProcessOutput};
16
17/// Runs a CLI process invocation through the outer bootstrap layer.
18///
19/// `args` are the command-line arguments after the binary name. The
20/// `interactive_text_output` flag captures terminal capability detection from
21/// the binary entry point so output-only tests can run without reading process
22/// terminal state.
23pub async fn run_process<I, S>(
24    args: I,
25    interactive_text_output: bool,
26) -> Result<CliProcessOutput, CliError>
27where
28    I: IntoIterator<Item = S>,
29    S: Into<String>,
30{
31    let command = CliCommand::parse(args)?;
32    let stdout = run_command(command, interactive_text_output).await?;
33
34    Ok(CliProcessOutput {
35        stdout,
36        stderr: String::new(),
37    })
38}
39
40/// Renders best-effort process notices after primary CLI output is emitted.
41///
42/// The notice lifecycle is process-level behavior because it depends on the
43/// final command, terminal mode, and post-command update checks. It is exposed
44/// here so `main.rs` no longer calls the interface adapter directly.
45pub async fn process_update_notice<I, S>(args: I, interactive_text_output: bool) -> Option<String>
46where
47    I: IntoIterator<Item = S>,
48    S: Into<String>,
49{
50    crate::interfaces::cli::process_update_notice(args, interactive_text_output).await
51}
52
53async fn run_command(
54    command: CliCommand,
55    _interactive_text_output: bool,
56) -> Result<String, CliError> {
57    if let CliAction::Map(map_command) = command.action.clone() {
58        let context = RequestContext::for_interface(InterfaceKind::Cli);
59        let service = if map_command.needs_repository_root() {
60            Some(knowledge_map_service(command.format)?)
61        } else {
62            None
63        };
64        return crate::interfaces::cli::map_cli::run_map(
65            map_command,
66            service.as_ref(),
67            context,
68            command.format,
69        )
70        .await;
71    }
72
73    crate::interfaces::cli::run_command(command).await
74}
75
76fn knowledge_map_service(format: OutputFormat) -> Result<KnowledgeMapService, CliError> {
77    let current = std::env::current_dir().map_err(|error| {
78        CliError::invalid_api_argument(
79            format!("failed to resolve current directory: {error}"),
80            format,
81        )
82    })?;
83    let root = discover_repository_root(&current)
84        .map_err(|error| CliError::invalid_api_argument(error.to_string(), format))?
85        .ok_or_else(|| {
86            CliError::invalid_api_argument(
87                format!("failed to find repository root for {KNOWLEDGE_MAP_RELATIVE_PATH}"),
88                format,
89            )
90        })?;
91
92    Ok(KnowledgeMapService::new(root))
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[tokio::test]
100    async fn process_entry_delegates_to_existing_cli_behavior() {
101        let bootstrap = run_process(["--version"], false)
102            .await
103            .expect("bootstrap CLI process should render version");
104        let interface = crate::interfaces::cli::run_process(["--version"], false)
105            .await
106            .expect("interface CLI process should render version");
107
108        assert_eq!(bootstrap, interface);
109    }
110}