Skip to main content

oneiros_engine/
engine.rs

1//! Engine — the formal consumer surface for oneiros.
2//!
3//! Every consumer of the engine (binary, tests, xtask, programmatic)
4//! goes through this type. It owns a `Config` and provides entry points
5//! for each use case:
6//!
7//! - `run()` — full CLI lifecycle: parse, configure, execute, render
8//! - `from_cli()` — parse CLI args and merge config file
9//! - `new(config)` — explicit config (tests, programmatic use)
10//! - `execute(cli)` — run a parsed CLI command
11//!
12//! Server lifetime is owned by `Server` (`http::server`), not `Engine`.
13//! Consumers that need to serve HTTP/MCP construct a `Server` directly:
14//! `Server::new(config).serve()` blocks the calling task; `spawn()`
15//! returns a handle.
16
17use anstream::{stderr, stdout};
18use clap::Parser;
19use std::{io::Write, process::ExitCode};
20
21use crate::*;
22
23/// The engine — entry point for all consumers.
24pub struct Engine {
25    config: Config,
26}
27
28impl Engine {
29    /// Run the full CLI lifecycle — parse, configure, execute, render.
30    ///
31    /// This is the canonical entrypoint for the binary. It owns
32    /// tracing setup, color configuration, command execution, and
33    /// output rendering. Errors are rendered through `ErrorView`
34    /// to stderr with styled formatting and proper exit codes.
35    pub async fn run() -> ExitCode {
36        let (engine, cli) = match Self::from_cli() {
37            Ok(pair) => pair,
38            Err(error) => {
39                let _ = writeln!(stderr().lock(), "{}", ErrorView::new(error));
40                return ExitCode::FAILURE;
41            }
42        };
43
44        engine.config().color.apply_global();
45
46        let _logging_guard = match Logging.install(engine.config(), cli.command.is_server()) {
47            Ok(guard) => guard,
48            Err(error) => {
49                let _ = writeln!(stderr().lock(), "{}", ErrorView::new(error.into()));
50                return ExitCode::FAILURE;
51            }
52        };
53
54        let result: Rendered<Responses> = match engine.execute(&cli).await {
55            Ok(rendered) => rendered,
56            Err(error) => {
57                let _ = writeln!(stderr().lock(), "{}", ErrorView::new(error));
58                return ExitCode::FAILURE;
59            }
60        };
61
62        // Silent results have already produced their output (e.g. binary
63        // bytes streamed to stdout by `storage get`). Skip the render
64        // dispatch entirely so we don't append JSON to the stream.
65        if result.is_silent() {
66            return ExitCode::SUCCESS;
67        }
68
69        let as_json = match serde_json::to_string(result.response()) {
70            Ok(json) => json,
71            Err(error) => {
72                let _ = writeln!(stderr().lock(), "{}", ErrorView::new(error.into()));
73                return ExitCode::FAILURE;
74            }
75        };
76
77        let mut out = stdout().lock();
78
79        let write_result = match (
80            &engine.config().output,
81            result.has_prompt(),
82            result.has_text(),
83        ) {
84            (OutputMode::Prompt, true, _) => write!(out, "{}", result.prompt()),
85            (OutputMode::Text, _, true) => write!(out, "{}", result.text()),
86            (OutputMode::Json, _, _) | (_, false, _) | (_, _, false) => {
87                writeln!(out, "{as_json}")
88            }
89        };
90
91        if let Err(error) = write_result {
92            let _ = writeln!(stderr().lock(), "{}", ErrorView::new(error.into()));
93            return ExitCode::FAILURE;
94        }
95
96        ExitCode::SUCCESS
97    }
98
99    /// From CLI args — parses arguments and resolves configuration.
100    ///
101    /// Layers config from defaults, config file, env vars, and CLI flags.
102    /// Returns the engine and the parsed CLI so the caller can execute
103    /// and render. Tracing setup is the caller's responsibility.
104    pub(crate) fn from_cli() -> Result<(Self, Cli), Error> {
105        let cli = Cli::parse();
106        let config = Config::resolve(&cli.overrides).map_err(|e| Error::Config(e.to_string()))?;
107
108        Ok((Self::new(config), cli))
109    }
110
111    /// From explicit config — tests and programmatic consumers.
112    pub(crate) fn new(config: Config) -> Self {
113        Self { config }
114    }
115
116    /// Execute a parsed CLI command against this engine's config.
117    pub(crate) async fn execute(&self, cli: &Cli) -> Result<Rendered<Responses>, Error> {
118        cli.execute(&self.config).await
119    }
120
121    /// The resolved configuration.
122    pub(crate) fn config(&self) -> &Config {
123        &self.config
124    }
125}