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}