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    application::{KnowledgeMapService, ProcessRuntimeConfig},
9    env::windows_system_root_from_process,
10    interfaces::cli::{CliAction, CliCommand, OutputFormat},
11    paths::discover_repository_root,
12    project::{KNOWLEDGE_MAP_RELATIVE_PATH, PROJECT_NAME},
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    let _command = crate::interfaces::cli::update_notice_command(args, interactive_text_output)?;
51    let runtime = super::runtime_configuration_from_process_environment()
52        .await
53        .ok()?;
54    crate::interfaces::cli::update_notice_for_runtime(&runtime).await
55}
56
57async fn run_command(
58    command: CliCommand,
59    _interactive_text_output: bool,
60) -> Result<String, CliError> {
61    let service = match &command.action {
62        CliAction::Map(map_command) if map_command.needs_repository_root() => {
63            Some(knowledge_map_service(command.format)?)
64        }
65        _ => None,
66    };
67    crate::interfaces::cli::run_command(command, service.as_ref(), process_context()).await
68}
69
70fn process_context() -> ProcessRuntimeConfig {
71    let current_executable =
72        std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from(PROJECT_NAME));
73    ProcessRuntimeConfig::from_bootstrap_inputs(
74        current_executable,
75        windows_system_root_from_process(),
76    )
77}
78
79fn knowledge_map_service(format: OutputFormat) -> Result<KnowledgeMapService, CliError> {
80    let current = std::env::current_dir().map_err(|error| {
81        CliError::invalid_api_argument(
82            format!("failed to resolve current directory: {error}"),
83            format,
84        )
85    })?;
86    let root = discover_repository_root(&current)
87        .map_err(|error| CliError::invalid_api_argument(error.to_string(), format))?
88        .ok_or_else(|| {
89            CliError::invalid_api_argument(
90                format!("failed to find repository root for {KNOWLEDGE_MAP_RELATIVE_PATH}"),
91                format,
92            )
93        })?;
94
95    Ok(KnowledgeMapService::new(root))
96}
97
98#[cfg(test)]
99#[allow(deprecated)]
100#[path = "cli_tests.rs"]
101mod tests;