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 [`App::builder`]
19//! - embedders may inspect runtime/session state, but the preferred
20//!   construction path still flows through a small number of builders and
21//!   constructors here such as [`crate::app::AppStateBuilder`]
22//! - lower-level semantic payloads live in modules like [`crate::guide`] and
23//!   [`crate::completion`]; this module owns the heavier host machinery
24//!
25//! Use this module when you want:
26//!
27//! - the full CLI / REPL host in-process
28//! - the same dispatch/help/completion/config behavior as `osp`
29//! - a wrapper crate that injects native commands or product defaults
30//!
31//! Skip this module and start lower if you only need:
32//!
33//! - LDAP service execution plus DSL stages: [`crate::services`]
34//! - rendering rows/documents: [`crate::ui`]
35//! - pure completion trees or guide payloads: [`crate::completion`] or
36//!   [`crate::guide`]
37//!
38//! Broad-strokes host flow:
39//!
40//! ```text
41//! argv / REPL request
42//!      │
43//!      ▼ [ app ]    build host state, load config, assemble command catalog
44//!      ▼ [ dispatch ] choose native or plugin command, run it
45//!      ▼ [ dsl ]    apply trailing pipeline stages to command output
46//!      ▼ [ ui ]     render text to a UiSink or process stdio
47//! ```
48//!
49//! Most callers only need one of these shapes:
50//!
51//! - [`App::run_from`] when they want a structured `Result<i32>`
52//! - [`App::run_process`] when they want process-style exit code conversion
53//! - [`App::with_sink`] or [`App::builder`] plus
54//!   [`AppBuilder::build_with_sink`] when a test or outer host wants captured
55//!   stdout/stderr instead of touching process stdio
56//! - [`crate::services`] when this full host layer is more machinery than the
57//!   integration needs
58//!
59//! Downstream product-wrapper pattern:
60//!
61//! - keep site-specific auth, policy, and integration state in the wrapper
62//!   crate rather than in [`crate::app`]
63//! - build a [`crate::NativeCommandRegistry`] for product-specific commands
64//! - build one product-owned defaults layer under `extensions.<site>.*`
65//! - inject both through [`App::builder`], then
66//!   [`AppBuilder::with_native_commands`] and
67//!   [`AppBuilder::with_product_defaults`]
68//! - expose a thin product-level `run_process` or `builder()` API on top
69//!   instead of forking generic host behavior
70
71use crate::config::ConfigLayer;
72use crate::native::NativeCommandRegistry;
73use crate::ui::messages::{MessageBuffer, MessageLevel, adjust_verbosity};
74use std::ffi::OsString;
75
76pub(crate) mod assembly;
77pub(crate) mod bootstrap;
78pub(crate) mod command_output;
79pub(crate) mod config_explain;
80pub(crate) mod dispatch;
81pub(crate) mod external;
82pub(crate) mod help;
83pub(crate) mod host;
84pub(crate) mod logging;
85pub(crate) mod rebuild;
86pub(crate) mod repl_lifecycle;
87pub(crate) mod runtime;
88pub(crate) mod session;
89/// UI sink abstractions used by the host entrypoints.
90pub(crate) mod sink;
91#[cfg(test)]
92mod tests;
93pub(crate) mod timing;
94
95pub(crate) use bootstrap::*;
96pub(crate) use command_output::*;
97pub use host::run_from;
98pub(crate) use host::*;
99pub(crate) use repl_lifecycle::rebuild_repl_in_place;
100pub use runtime::{
101    AppClients, AppRuntime, AuthState, ConfigState, LaunchContext, RuntimeContext, TerminalKind,
102    UiState,
103};
104#[cfg(test)]
105pub(crate) use session::AppStateInit;
106pub use session::{
107    AppSession, AppSessionBuilder, AppState, AppStateBuilder, DebugTimingBadge, DebugTimingState,
108    LastFailure, ReplScopeFrame, ReplScopeStack,
109};
110pub use sink::{BufferedUiSink, StdIoUiSink, UiSink};
111
112#[derive(Clone, Default)]
113pub(crate) struct AppDefinition {
114    native_commands: NativeCommandRegistry,
115    product_defaults: ConfigLayer,
116}
117
118impl AppDefinition {
119    fn with_native_commands(mut self, native_commands: NativeCommandRegistry) -> Self {
120        self.native_commands = native_commands;
121        self
122    }
123
124    fn with_product_defaults(mut self, product_defaults: ConfigLayer) -> Self {
125        self.product_defaults = product_defaults;
126        self
127    }
128}
129
130#[derive(Clone, Default)]
131/// Top-level application entrypoint for CLI and REPL execution.
132///
133/// Most embedders should start here or with [`AppBuilder`] instead of trying
134/// to assemble runtime/session machinery directly.
135#[must_use]
136pub struct App {
137    definition: AppDefinition,
138}
139
140impl App {
141    /// Starts the canonical public builder for host construction.
142    pub fn builder() -> AppBuilder {
143        AppBuilder::default()
144    }
145
146    /// Creates an application with the default native command registry and no
147    /// wrapper-owned defaults.
148    pub fn new() -> Self {
149        Self::builder().build()
150    }
151
152    /// Replaces the native command registry used for command dispatch.
153    ///
154    /// Use this when an embedder wants extra in-process commands to participate
155    /// in the same command surface as the built-in host commands. When omitted,
156    /// the application uses the crate's default native-command registry.
157    ///
158    /// This is the main extension seam for downstream product crates that wrap
159    /// `osp-cli` and add site-specific native commands while keeping the rest
160    /// of the host/runtime behavior unchanged.
161    ///
162    /// # Examples
163    ///
164    /// ```
165    /// use anyhow::Result;
166    /// use clap::Command;
167    /// use osp_cli::app::BufferedUiSink;
168    /// use osp_cli::{
169    ///     App, NativeCommand, NativeCommandContext, NativeCommandOutcome, NativeCommandRegistry,
170    /// };
171    ///
172    /// struct VersionCommand;
173    ///
174    /// impl NativeCommand for VersionCommand {
175    ///     fn command(&self) -> Command {
176    ///         Command::new("version").about("Show custom version")
177    ///     }
178    ///
179    ///     fn execute(
180    ///         &self,
181    ///         _args: &[String],
182    ///         _context: &NativeCommandContext<'_>,
183    ///     ) -> Result<NativeCommandOutcome> {
184    ///         Ok(NativeCommandOutcome::Exit(0))
185    ///     }
186    /// }
187    ///
188    /// let app = App::new().with_native_commands(
189    ///     NativeCommandRegistry::new().with_command(VersionCommand),
190    /// );
191    /// let mut sink = BufferedUiSink::default();
192    /// let exit = app.run_process_with_sink(["osp", "--help"], &mut sink);
193    ///
194    /// assert_eq!(exit, 0);
195    /// assert!(sink.stdout.contains("version"));
196    /// ```
197    pub fn with_native_commands(mut self, native_commands: NativeCommandRegistry) -> Self {
198        self.definition = self.definition.with_native_commands(native_commands);
199        self
200    }
201
202    /// Replaces the product-owned defaults layered into runtime bootstrap.
203    ///
204    /// Use this when a wrapper crate owns extension keys such as
205    /// `extensions.<site>.*` and wants them resolved through the normal host
206    /// bootstrap path instead of maintaining a side-channel config helper.
207    ///
208    /// When omitted, the application uses only the built-in runtime defaults.
209    pub fn with_product_defaults(mut self, product_defaults: ConfigLayer) -> Self {
210        self.definition = self.definition.with_product_defaults(product_defaults);
211        self
212    }
213
214    /// Runs the application and returns a structured exit status.
215    ///
216    /// Use this when your caller wants ordinary Rust error handling instead of
217    /// the process-style exit code conversion performed by
218    /// [`App::run_process`].
219    ///
220    /// # Examples
221    ///
222    /// ```
223    /// use osp_cli::App;
224    ///
225    /// let exit = App::new().run_from(["osp", "--help"])?;
226    ///
227    /// assert_eq!(exit, 0);
228    /// # Ok::<(), miette::Report>(())
229    /// ```
230    pub fn run_from<I, T>(&self, args: I) -> miette::Result<i32>
231    where
232        I: IntoIterator<Item = T>,
233        T: Into<OsString> + Clone,
234    {
235        host::run_from_with_sink_and_app(args, &mut StdIoUiSink, &self.definition)
236    }
237
238    /// Binds the application to a specific UI sink for repeated invocations.
239    ///
240    /// Prefer this in tests, editor integrations, or foreign hosts that need
241    /// the same host behavior as `osp` but want the rendered text captured in a
242    /// buffer instead of written to process stdio.
243    ///
244    /// # Examples
245    ///
246    /// ```
247    /// use osp_cli::app::BufferedUiSink;
248    /// use osp_cli::App;
249    ///
250    /// let mut sink = BufferedUiSink::default();
251    /// let exit = App::new()
252    ///     .with_sink(&mut sink)
253    ///     .run_process(["osp", "--help"]);
254    ///
255    /// assert_eq!(exit, 0);
256    /// assert!(!sink.stdout.is_empty());
257    /// assert!(sink.stderr.is_empty());
258    /// ```
259    pub fn with_sink<'a>(self, sink: &'a mut dyn UiSink) -> AppRunner<'a> {
260        AppRunner { app: self, sink }
261    }
262
263    /// Runs the application with the provided UI sink.
264    ///
265    /// Prefer this over [`App::with_sink`] when the caller only needs one
266    /// invocation. Use [`App::with_sink`] and [`AppRunner`] when the same sink
267    /// should be reused across multiple calls.
268    ///
269    /// # Examples
270    ///
271    /// ```
272    /// use osp_cli::App;
273    /// use osp_cli::app::BufferedUiSink;
274    ///
275    /// let mut sink = BufferedUiSink::default();
276    /// let exit = App::new().run_with_sink(["osp", "--help"], &mut sink)?;
277    ///
278    /// assert_eq!(exit, 0);
279    /// assert!(!sink.stdout.is_empty());
280    /// assert!(sink.stderr.is_empty());
281    /// # Ok::<(), miette::Report>(())
282    /// ```
283    pub fn run_with_sink<I, T>(&self, args: I, sink: &mut dyn UiSink) -> miette::Result<i32>
284    where
285        I: IntoIterator<Item = T>,
286        T: Into<OsString> + Clone,
287    {
288        host::run_from_with_sink_and_app(args, sink, &self.definition)
289    }
290
291    /// Runs the application and converts execution failures into process exit
292    /// codes.
293    ///
294    /// Use this when the caller wants `osp`-style process behavior rather than
295    /// structured error propagation. User-facing failures are rendered to the
296    /// process stdio streams before the exit code is returned.
297    ///
298    /// # Examples
299    ///
300    /// ```
301    /// use osp_cli::App;
302    ///
303    /// let exit = App::new().run_process(["osp", "--help"]);
304    ///
305    /// assert_eq!(exit, 0);
306    /// ```
307    pub fn run_process<I, T>(&self, args: I) -> i32
308    where
309        I: IntoIterator<Item = T>,
310        T: Into<OsString> + Clone,
311    {
312        let mut sink = StdIoUiSink;
313        self.run_process_with_sink(args, &mut sink)
314    }
315
316    /// Runs the application with the provided sink and returns a process exit
317    /// code.
318    ///
319    /// This mirrors [`App::run_process`] but writes all rendered output and
320    /// user-facing errors through the supplied sink instead of touching process
321    /// stdio.
322    ///
323    /// # Examples
324    ///
325    /// ```
326    /// use osp_cli::App;
327    /// use osp_cli::app::BufferedUiSink;
328    ///
329    /// let mut sink = BufferedUiSink::default();
330    /// let exit = App::new().run_process_with_sink(["osp", "--help"], &mut sink);
331    ///
332    /// assert_eq!(exit, 0);
333    /// assert!(!sink.stdout.is_empty());
334    /// assert!(sink.stderr.is_empty());
335    /// ```
336    pub fn run_process_with_sink<I, T>(&self, args: I, sink: &mut dyn UiSink) -> i32
337    where
338        I: IntoIterator<Item = T>,
339        T: Into<OsString> + Clone,
340    {
341        let args = args.into_iter().map(Into::into).collect::<Vec<OsString>>();
342        let message_verbosity = bootstrap_message_verbosity(&args);
343
344        match host::run_from_with_sink_and_app(args, sink, &self.definition) {
345            Ok(code) => code,
346            Err(err) => {
347                let mut messages = MessageBuffer::default();
348                messages.error(render_report_message(&err, message_verbosity));
349                sink.write_stderr(&messages.render_grouped(message_verbosity));
350                classify_exit_code(&err)
351            }
352        }
353    }
354}
355
356/// Reusable runner that keeps an [`App`] paired with a borrowed UI sink.
357///
358/// Prefer [`App::run_with_sink`] for one-shot calls. This type exists for
359/// scoped reuse when the same sink should back multiple invocations.
360///
361/// # Lifetime
362///
363/// `'a` is the lifetime of the mutable borrow of the sink passed to
364/// [`App::with_sink`]. That means the runner cannot outlive the borrowed sink
365/// and is mainly useful as a stack-scoped helper. It is not a good cross-thread
366/// or async handoff type in its current form because it stores `&'a mut dyn
367/// UiSink`.
368///
369/// # Examples
370///
371/// ```
372/// use osp_cli::App;
373/// use osp_cli::app::BufferedUiSink;
374///
375/// let mut sink = BufferedUiSink::default();
376/// let mut runner = App::new().with_sink(&mut sink);
377///
378/// let first = runner.run_from(["osp", "--help"])?;
379/// let second = runner.run_process(["osp", "--help"]);
380///
381/// assert_eq!(first, 0);
382/// assert_eq!(second, 0);
383/// assert!(!sink.stdout.is_empty());
384/// assert!(sink.stderr.is_empty());
385/// # Ok::<(), miette::Report>(())
386/// ```
387#[must_use = "AppRunner only has an effect when you call run_from or run_process on it"]
388pub struct AppRunner<'a> {
389    app: App,
390    sink: &'a mut dyn UiSink,
391}
392
393impl<'a> AppRunner<'a> {
394    /// Runs the application and returns a structured exit status.
395    ///
396    /// This is the bound-sink counterpart to [`App::run_with_sink`]. The
397    /// borrowed sink stays attached so later calls on the same runner append to
398    /// the same buffered or redirected output destination.
399    pub fn run_from<I, T>(&mut self, args: I) -> miette::Result<i32>
400    where
401        I: IntoIterator<Item = T>,
402        T: Into<OsString> + Clone,
403    {
404        self.app.run_with_sink(args, self.sink)
405    }
406
407    /// Runs the application and converts execution failures into process exit
408    /// codes.
409    ///
410    /// This is the bound-sink counterpart to [`App::run_process_with_sink`].
411    /// User-facing failures are rendered into the already-bound sink before the
412    /// numeric exit code is returned.
413    pub fn run_process<I, T>(&mut self, args: I) -> i32
414    where
415        I: IntoIterator<Item = T>,
416        T: Into<OsString> + Clone,
417    {
418        self.app.run_process_with_sink(args, self.sink)
419    }
420}
421
422#[derive(Clone, Default)]
423/// Builder for configuring an [`App`] before construction.
424///
425/// This is the canonical public composition surface for host-level setup.
426///
427/// # Examples
428///
429/// Minimal embedder `main.rs`:
430///
431/// ```no_run
432/// use osp_cli::App;
433///
434/// fn main() {
435///     std::process::exit(
436///         App::builder().build().run_process(std::env::args_os()),
437///     );
438/// }
439/// ```
440#[must_use]
441pub struct AppBuilder {
442    definition: AppDefinition,
443}
444
445impl AppBuilder {
446    /// Replaces the native command registry used by the built application.
447    ///
448    /// This is the builder-friendly way to extend the host with extra native
449    /// commands before calling [`AppBuilder::build`]. When omitted, the built
450    /// app uses the crate's default native-command registry.
451    ///
452    /// This is the builder-side equivalent of [`App::with_native_commands`].
453    /// Prefer it when a wrapper crate wants to finish command registration
454    /// before deciding whether to build an owned [`App`] or a sink-bound
455    /// [`AppRunner`].
456    pub fn with_native_commands(mut self, native_commands: NativeCommandRegistry) -> Self {
457        self.definition = self.definition.with_native_commands(native_commands);
458        self
459    }
460
461    /// Replaces the product-owned defaults layered into runtime bootstrap.
462    ///
463    /// Wrapper crates should put site-owned keys under `extensions.<site>.*`
464    /// and inject that layer here so native commands, `config get`, `config
465    /// explain`, help rendering, and REPL rebuilds all see the same resolved
466    /// config.
467    ///
468    /// If omitted, the built app uses only the crate's built-in runtime
469    /// defaults.
470    pub fn with_product_defaults(mut self, product_defaults: ConfigLayer) -> Self {
471        self.definition = self.definition.with_product_defaults(product_defaults);
472        self
473    }
474
475    /// Builds an [`App`] from the current builder state.
476    ///
477    /// Choose this when you want an owned application value that can be reused
478    /// across many calls. Use [`AppBuilder::build_with_sink`] when binding the
479    /// output sink is part of the setup.
480    ///
481    /// # Examples
482    ///
483    /// ```
484    /// use osp_cli::App;
485    ///
486    /// let app = App::builder().build();
487    /// let exit = app.run_process(["osp", "--help"]);
488    ///
489    /// assert_eq!(exit, 0);
490    /// ```
491    pub fn build(self) -> App {
492        App {
493            definition: self.definition,
494        }
495    }
496
497    /// Builds an [`AppRunner`] bound to the provided UI sink.
498    ///
499    /// This is the shortest path for tests and embedders that want one sink
500    /// binding plus the full host behavior.
501    ///
502    /// # Examples
503    ///
504    /// ```
505    /// use osp_cli::App;
506    /// use osp_cli::app::BufferedUiSink;
507    ///
508    /// let mut sink = BufferedUiSink::default();
509    /// let exit = App::builder()
510    ///     .build_with_sink(&mut sink)
511    ///     .run_process(["osp", "--help"]);
512    ///
513    /// assert_eq!(exit, 0);
514    /// assert!(!sink.stdout.is_empty());
515    /// assert!(sink.stderr.is_empty());
516    /// ```
517    pub fn build_with_sink<'a>(self, sink: &'a mut dyn UiSink) -> AppRunner<'a> {
518        self.build().with_sink(sink)
519    }
520}
521
522/// Runs the default application instance and returns a process exit code.
523///
524/// This is shorthand for building [`App::new`] and calling
525/// [`App::run_process`], using process stdio for rendered output and
526/// user-facing errors.
527///
528/// # Examples
529///
530/// ```
531/// let exit = osp_cli::app::run_process(["osp", "--help"]);
532///
533/// assert_eq!(exit, 0);
534/// ```
535pub fn run_process<I, T>(args: I) -> i32
536where
537    I: IntoIterator<Item = T>,
538    T: Into<OsString> + Clone,
539{
540    let mut sink = StdIoUiSink;
541    run_process_with_sink(args, &mut sink)
542}
543
544/// Runs the default application instance with the provided sink.
545///
546/// This is shorthand for building [`App::new`] and calling
547/// [`App::run_process_with_sink`].
548///
549/// # Examples
550///
551/// ```
552/// use osp_cli::app::{BufferedUiSink, run_process_with_sink};
553///
554/// let mut sink = BufferedUiSink::default();
555/// let exit = run_process_with_sink(["osp", "--help"], &mut sink);
556///
557/// assert_eq!(exit, 0);
558/// assert!(!sink.stdout.is_empty());
559/// assert!(sink.stderr.is_empty());
560/// ```
561pub fn run_process_with_sink<I, T>(args: I, sink: &mut dyn UiSink) -> i32
562where
563    I: IntoIterator<Item = T>,
564    T: Into<OsString> + Clone,
565{
566    let args = args.into_iter().map(Into::into).collect::<Vec<OsString>>();
567    let message_verbosity = bootstrap_message_verbosity(&args);
568
569    match host::run_from_with_sink(args, sink) {
570        Ok(code) => code,
571        Err(err) => {
572            let mut messages = MessageBuffer::default();
573            messages.error(render_report_message(&err, message_verbosity));
574            sink.write_stderr(&messages.render_grouped(message_verbosity));
575            classify_exit_code(&err)
576        }
577    }
578}
579
580fn bootstrap_message_verbosity(args: &[OsString]) -> MessageLevel {
581    let mut verbose = 0u8;
582    let mut quiet = 0u8;
583
584    for token in args.iter().skip(1) {
585        let Some(value) = token.to_str() else {
586            continue;
587        };
588
589        if value == "--" {
590            break;
591        }
592
593        match value {
594            "--verbose" => {
595                verbose = verbose.saturating_add(1);
596                continue;
597            }
598            "--quiet" => {
599                quiet = quiet.saturating_add(1);
600                continue;
601            }
602            _ => {}
603        }
604
605        if value.starts_with('-') && !value.starts_with("--") {
606            for ch in value.chars().skip(1) {
607                match ch {
608                    'v' => verbose = verbose.saturating_add(1),
609                    'q' => quiet = quiet.saturating_add(1),
610                    _ => {}
611                }
612            }
613        }
614    }
615
616    adjust_verbosity(MessageLevel::Success, verbose, quiet)
617}