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}