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