Skip to main content

osp_cli/app/
mod.rs

1//! The app module exists to turn the library pieces into a running program.
2//!
3//! This is the process-facing layer of the crate. It wires together CLI
4//! parsing, config loading, plugin/catalog setup, rendering, and REPL startup
5//! into the public [`App`] entrypoints. Lower-level modules like
6//! [`crate::config`], [`crate::ui`], and [`crate::repl`] stay reusable because
7//! this module is where the product-level orchestration happens.
8//!
9//! Contract:
10//!
11//! - this is allowed to depend on the rest of the crate because it is the host
12//!   composition layer
13//! - the dependency should not point the other way; lower-level modules should
14//!   not import [`crate::app`] to get work done
15//!
16//! Public API shape:
17//!
18//! - most callers should start with [`App`] or [`AppBuilder`]
19//! - embedders may inspect runtime/session state, but the preferred
20//!   construction path still flows through builders and constructors here such
21//!   as [`crate::app::AppStateBuilder`], [`crate::app::UiStateBuilder`], and
22//!   [`crate::app::LaunchContextBuilder`]
23//! - lower-level semantic payloads live in modules like [`crate::guide`] and
24//!   [`crate::completion`]; this module owns the heavier host machinery
25
26use crate::native::NativeCommandRegistry;
27use crate::ui::messages::{MessageBuffer, MessageLevel, adjust_verbosity};
28use std::ffi::OsString;
29
30pub(crate) mod assembly;
31pub(crate) mod bootstrap;
32pub(crate) mod command_output;
33pub(crate) mod config_explain;
34pub(crate) mod dispatch;
35pub(crate) mod external;
36pub(crate) mod help;
37pub(crate) mod host;
38pub(crate) mod logging;
39pub(crate) mod rebuild;
40pub(crate) mod repl_lifecycle;
41pub(crate) mod runtime;
42pub(crate) mod session;
43/// UI sink abstractions used by the host entrypoints.
44pub(crate) mod sink;
45#[cfg(test)]
46mod tests;
47pub(crate) mod timing;
48
49pub(crate) use bootstrap::*;
50pub(crate) use command_output::*;
51pub use host::run_from;
52pub(crate) use host::*;
53pub(crate) use repl_lifecycle::rebuild_repl_in_place;
54pub use runtime::{
55    AppClients, AppClientsBuilder, AppRuntime, AuthState, ConfigState, LaunchContext,
56    LaunchContextBuilder, RuntimeContext, TerminalKind, UiState, UiStateBuilder,
57};
58#[cfg(test)]
59pub(crate) use session::AppStateInit;
60pub use session::{
61    AppSession, AppSessionBuilder, AppState, AppStateBuilder, DebugTimingBadge, DebugTimingState,
62    LastFailure, ReplScopeFrame, ReplScopeStack,
63};
64pub use sink::{BufferedUiSink, StdIoUiSink, UiSink};
65
66#[derive(Clone, Default)]
67/// Top-level application entrypoint for CLI and REPL execution.
68///
69/// Most embedders should start here or with [`AppBuilder`] instead of trying
70/// to assemble runtime/session machinery directly.
71pub struct App {
72    native_commands: NativeCommandRegistry,
73}
74
75impl App {
76    /// Creates an application with the default native command registry.
77    pub fn new() -> Self {
78        Self {
79            native_commands: NativeCommandRegistry::default(),
80        }
81    }
82
83    /// Replaces the native command registry used for command dispatch.
84    pub fn with_native_commands(mut self, native_commands: NativeCommandRegistry) -> Self {
85        self.native_commands = native_commands;
86        self
87    }
88
89    /// Runs the application and returns a structured exit status.
90    pub fn run_from<I, T>(&self, args: I) -> miette::Result<i32>
91    where
92        I: IntoIterator<Item = T>,
93        T: Into<OsString> + Clone,
94    {
95        host::run_from_with_sink_and_native(args, &mut StdIoUiSink, &self.native_commands)
96    }
97
98    /// Binds the application to a specific UI sink for repeated invocations.
99    pub fn with_sink<'a>(self, sink: &'a mut dyn UiSink) -> AppRunner<'a> {
100        AppRunner { app: self, sink }
101    }
102
103    /// Runs the application with the provided UI sink.
104    pub fn run_with_sink<I, T>(&self, args: I, sink: &mut dyn UiSink) -> miette::Result<i32>
105    where
106        I: IntoIterator<Item = T>,
107        T: Into<OsString> + Clone,
108    {
109        host::run_from_with_sink_and_native(args, sink, &self.native_commands)
110    }
111
112    /// Runs the application and converts execution failures into process exit codes.
113    pub fn run_process<I, T>(&self, args: I) -> i32
114    where
115        I: IntoIterator<Item = T>,
116        T: Into<OsString> + Clone,
117    {
118        let mut sink = StdIoUiSink;
119        self.run_process_with_sink(args, &mut sink)
120    }
121
122    /// Runs the application with the provided sink and returns a process exit code.
123    pub fn run_process_with_sink<I, T>(&self, args: I, sink: &mut dyn UiSink) -> i32
124    where
125        I: IntoIterator<Item = T>,
126        T: Into<OsString> + Clone,
127    {
128        let args = args.into_iter().map(Into::into).collect::<Vec<OsString>>();
129        let message_verbosity = bootstrap_message_verbosity(&args);
130
131        match host::run_from_with_sink_and_native(args, sink, &self.native_commands) {
132            Ok(code) => code,
133            Err(err) => {
134                let mut messages = MessageBuffer::default();
135                messages.error(render_report_message(&err, message_verbosity));
136                sink.write_stderr(&messages.render_grouped(message_verbosity));
137                classify_exit_code(&err)
138            }
139        }
140    }
141}
142
143/// Reusable runner that keeps an [`App`] paired with a UI sink.
144pub struct AppRunner<'a> {
145    app: App,
146    sink: &'a mut dyn UiSink,
147}
148
149impl<'a> AppRunner<'a> {
150    /// Runs the application and returns a structured exit status.
151    pub fn run_from<I, T>(&mut self, args: I) -> miette::Result<i32>
152    where
153        I: IntoIterator<Item = T>,
154        T: Into<OsString> + Clone,
155    {
156        self.app.run_with_sink(args, self.sink)
157    }
158
159    /// Runs the application and converts execution failures into process exit codes.
160    pub fn run_process<I, T>(&mut self, args: I) -> i32
161    where
162        I: IntoIterator<Item = T>,
163        T: Into<OsString> + Clone,
164    {
165        self.app.run_process_with_sink(args, self.sink)
166    }
167}
168
169#[derive(Clone, Default)]
170/// Builder for configuring an [`App`] before construction.
171///
172/// This is the canonical public composition surface for host-level setup.
173pub struct AppBuilder {
174    native_commands: NativeCommandRegistry,
175}
176
177impl AppBuilder {
178    /// Creates a builder with the default native command registry.
179    pub fn new() -> Self {
180        Self {
181            native_commands: NativeCommandRegistry::default(),
182        }
183    }
184
185    /// Replaces the native command registry used by the built application.
186    pub fn with_native_commands(mut self, native_commands: NativeCommandRegistry) -> Self {
187        self.native_commands = native_commands;
188        self
189    }
190
191    /// Builds an [`App`] from the current builder state.
192    pub fn build(self) -> App {
193        App::new().with_native_commands(self.native_commands)
194    }
195
196    /// Builds an [`AppRunner`] bound to the provided UI sink.
197    pub fn build_with_sink<'a>(self, sink: &'a mut dyn UiSink) -> AppRunner<'a> {
198        self.build().with_sink(sink)
199    }
200}
201
202/// Runs the default application instance and returns a process exit code.
203pub fn run_process<I, T>(args: I) -> i32
204where
205    I: IntoIterator<Item = T>,
206    T: Into<OsString> + Clone,
207{
208    let mut sink = StdIoUiSink;
209    run_process_with_sink(args, &mut sink)
210}
211
212/// Runs the default application instance with the provided sink.
213pub fn run_process_with_sink<I, T>(args: I, sink: &mut dyn UiSink) -> i32
214where
215    I: IntoIterator<Item = T>,
216    T: Into<OsString> + Clone,
217{
218    let args = args.into_iter().map(Into::into).collect::<Vec<OsString>>();
219    let message_verbosity = bootstrap_message_verbosity(&args);
220
221    match host::run_from_with_sink(args, sink) {
222        Ok(code) => code,
223        Err(err) => {
224            let mut messages = MessageBuffer::default();
225            messages.error(render_report_message(&err, message_verbosity));
226            sink.write_stderr(&messages.render_grouped(message_verbosity));
227            classify_exit_code(&err)
228        }
229    }
230}
231
232fn bootstrap_message_verbosity(args: &[OsString]) -> MessageLevel {
233    let mut verbose = 0u8;
234    let mut quiet = 0u8;
235
236    for token in args.iter().skip(1) {
237        let Some(value) = token.to_str() else {
238            continue;
239        };
240
241        if value == "--" {
242            break;
243        }
244
245        match value {
246            "--verbose" => {
247                verbose = verbose.saturating_add(1);
248                continue;
249            }
250            "--quiet" => {
251                quiet = quiet.saturating_add(1);
252                continue;
253            }
254            _ => {}
255        }
256
257        if value.starts_with('-') && !value.starts_with("--") {
258            for ch in value.chars().skip(1) {
259                match ch {
260                    'v' => verbose = verbose.saturating_add(1),
261                    'q' => quiet = quiet.saturating_add(1),
262                    _ => {}
263                }
264            }
265        }
266    }
267
268    adjust_verbosity(MessageLevel::Success, verbose, quiet)
269}