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}