osp-cli 1.5.1

CLI and REPL for querying and managing OSP infrastructure data
Documentation
# Embedding And Product Wrappers

This guide is for teams building a site-specific product crate on top of
`osp-cli`.

Use this when your product wants:

- the upstream CLI/REPL host
- the upstream config system and output/rendering behavior
- site-specific commands, policy, auth, or integrations

Do not use this guide if you are only writing external plugins. That path is
covered by [USING_PLUGINS.md](USING_PLUGINS.md),
[WRITING_PLUGINS.md](WRITING_PLUGINS.md), and
[PLUGIN_PROTOCOL.md](PLUGIN_PROTOCOL.md).

If you want the shortest copy-and-adapt path, read `Recommended Shape` first
and then jump straight to `Worked Recipe`.

The matching runnable example lives in
[`examples/product-wrapper`](../examples/product-wrapper).

## Fast Path

If you want the shortest successful wrapper:

1. Copy [`examples/product-wrapper/src/lib.rs`]../examples/product-wrapper/src/lib.rs
   and [`examples/product-wrapper/src/main.rs`]../examples/product-wrapper/src/main.rs.
2. Rename `site_*` and `SiteApp` to your product name.
3. Move defaults under `extensions.<product>.*`.
4. Replace `SiteStatusCommand` with one real native command.
5. Keep `main.rs` thin and let `SiteApp::builder()` own host wiring.

You can ignore for now:

- [`osp_cli::app::AppStateBuilder`]
- manual runtime/session assembly
- the optional `site_runtime_config_for(...)` helper if you do not need
  wrapper-owned tooling/tests outside the host

## Ownership Split

Keep the split boring:

- `osp-cli` owns generic mechanism
- your product crate owns site-specific facts

Generic mechanism includes:

- CLI and REPL host behavior
- config loading, precedence, and explanation
- rendering and output formatting
- completion/help infrastructure
- native-command registration mechanics

Site-specific facts include:

- auth and policy inputs
- domain integrations
- native commands that expose those integrations
- site-only config under your own namespace

If a change could make sense for another site without changing meaning, it
probably belongs upstream.

## Recommended Shape

The normal wrapper shape is:

1. Keep a thin product-level app type that contains `osp_cli::App`.
2. Expose one wrapper-owned `builder()` that applies your defaults and native
   commands.
3. Build a `NativeCommandRegistry` for your site-specific commands.
4. Build one `ConfigLayer` containing your product-owned defaults under
   `extensions.<site>.*`.
5. Inject both through `App::builder()`.
6. Expose a small product API such as `run_process`, `builder`, or
   `assembly`.

Minimal sketch:

```rust
use std::ffi::OsString;

fn site_defaults() -> osp_cli::config::ConfigLayer {
    let mut defaults = osp_cli::config::ConfigLayer::default();
    defaults.set("extensions.site.enabled", true);
    defaults.set_for_terminal("cli", "extensions.site.banner", "cli-wrapper");
    defaults
}

#[derive(Clone)]
pub struct SiteApp {
    inner: osp_cli::App,
}

impl SiteApp {
    pub fn builder() -> osp_cli::AppBuilder {
        osp_cli::App::builder()
            .with_native_commands(site_native_registry())
            .with_product_defaults(site_defaults())
    }

    pub fn new() -> Self {
        Self {
            inner: Self::builder().build(),
        }
    }

    pub fn run_process<I, T>(&self, args: I) -> i32
    where
        I: IntoIterator<Item = T>,
        T: Into<OsString> + Clone,
    {
        self.inner.run_process(args)
    }
}

fn site_native_registry() -> osp_cli::NativeCommandRegistry {
    osp_cli::NativeCommandRegistry::new()
    // .with_command(MyNativeCommand)
}
```

That shape keeps the generic host untouched while giving the product crate one
obvious place to add its own commands and state.

## Wrapper Checklist

For a minimal but honest wrapper crate, keep this checklist true:

- one wrapper app type owns `osp_cli::App`
- one wrapper `builder()` applies product defaults and native commands
- one namespace owns product-only config: `extensions.<product>.*`
- one registration point owns native commands
- `src/main.rs` delegates into the wrapper crate instead of rebuilding host
  setup inline
- any extra config helper is optional and used only by wrapper-owned tooling,
  tests, or validation

## Worked Recipe

This is the end-to-end pattern to copy and adapt.

It shows:

1. product-owned defaults injected into the same host bootstrap path used by
   native commands and `config` commands
2. the optional matching helper for product-owned tooling that wants to
   resolve config outside the host
3. one complete native command
4. a thin product app wrapper that keeps upstream host startup behavior

Keep the file split simple:

- `src/lib.rs` owns defaults, native command registration, and the wrapper app
- `src/main.rs` should usually be a one-line delegate into that wrapper
- the config helper is for tooling/tests, not for ordinary startup

```rust
use std::ffi::OsString;

use anyhow::Result;
use clap::Command;
use osp_cli::config::{
    ConfigError, ConfigLayer, ResolveOptions, ResolvedConfig, RuntimeConfigPaths,
    RuntimeDefaults, RuntimeLoadOptions, build_runtime_pipeline,
};
use osp_cli::{
    App, AppBuilder, NativeCommand, NativeCommandContext, NativeCommandOutcome,
    NativeCommandRegistry,
};

fn site_defaults() -> ConfigLayer {
    let mut layer = ConfigLayer::default();
    layer.set("extensions.site.enabled", true);
    layer.set_for_terminal("cli", "extensions.site.banner", "cli-wrapper");
    layer
}

fn site_runtime_config_for(terminal: &str) -> Result<ResolvedConfig, ConfigError> {
    let paths = RuntimeConfigPaths::discover();
    let mut defaults = RuntimeDefaults::from_process_env("dracula", "site> ").to_layer();
    defaults.extend_from_layer(&site_defaults());

    build_runtime_pipeline(
        defaults,
        None,
        &paths,
        RuntimeLoadOptions::default(),
        None,
        None,
    )
    .resolve(ResolveOptions::new().with_terminal(terminal))
}

struct SiteStatusCommand;

impl NativeCommand for SiteStatusCommand {
    fn command(&self) -> Command {
        Command::new("site-status").about("Show site-specific status")
    }

    fn execute(
        &self,
        _args: &[String],
        context: &NativeCommandContext<'_>,
    ) -> Result<NativeCommandOutcome> {
        Ok(NativeCommandOutcome::Help(format!(
            "site enabled: {}",
            context
                .config
                .get_bool("extensions.site.enabled")
                .unwrap_or(false)
        )))
    }
}

fn site_native_registry() -> NativeCommandRegistry {
    NativeCommandRegistry::new().with_command(SiteStatusCommand)
}

#[derive(Clone)]
pub struct SiteApp {
    inner: App,
}

impl SiteApp {
    pub fn builder() -> AppBuilder {
        App::builder()
            .with_native_commands(site_native_registry())
            .with_product_defaults(site_defaults())
    }

    pub fn new() -> Self {
        Self {
            inner: Self::builder().build(),
        }
    }

    pub fn run_process<I, T>(&self, args: I) -> i32
    where
        I: IntoIterator<Item = T>,
        T: Into<OsString> + Clone,
    {
        self.inner.run_process(args)
    }
}
```

`src/main.rs` should usually stay boring:

```rust
fn main() {
    std::process::exit(site_product::SiteApp::new().run_process(std::env::args_os()));
}
```

Notes:

- the wrapper crate owns `extensions.site.*` defaults and native commands
- `osp_cli::App` still owns the generic host behavior
- `with_product_defaults(...)` pushes wrapper defaults into the same runtime
  bootstrap path used by native commands, help rendering, `config get`, and
  `config explain`
- `site_runtime_config_for(terminal)` is optional and meant for product-owned
  tests, validation, and adjacent tooling that need the same merged defaults
  outside the host
- `SiteApp::builder()` is the clean downstream seam when your product wants to
  keep the upstream host but still expose one wrapper-owned construction path
- if the product does not need its own preflight config step, keep the wrapper
  thinner and only inject the native registry and product defaults

## Common Mistakes

Avoid these wrapper-crate mistakes:

- putting product-only keys at the top level instead of under
  `extensions.<product>.*`
- resolving wrapper config in a side channel while the host reads a different
  config snapshot
- putting startup wiring directly in `main.rs`
- leaking site-specific state into `osp-cli::app` instead of keeping it in the
  wrapper crate
- forking generic help/completion/config behavior when native commands and
  product defaults are enough

## Config Strategy

Do not create a second config system in the wrapper crate.

The intended approach is:

- keep product-only keys under a dedicated namespace such as
  `extensions.<site>.*`
- continue using upstream `ConfigLayer`, `LoaderPipeline`,
  `ConfigResolver`, and `ResolvedConfig`
- use `RuntimeDefaults`, `RuntimeConfigPaths`, and
  `build_runtime_pipeline` when you need stock host-style bootstrap

If you need product-specific defaults, inject them as normal config layers.
Do not fork source precedence or file-discovery rules unless the product
really needs a different contract.

## Native Commands

Use native commands when the product wants built-in commands that participate
in the same help, completion, policy, and dispatch surfaces as the rest of the
host.

Keep the boundary small:

- command implementations should consume a `NativeCommandContext`
- product-specific state should live in the wrapper crate, not in
  `osp-cli::app`
- registration should happen in one place, usually a product integrations
  module

## What Not To Fork

Avoid downstream copies of:

- config precedence logic
- CLI parsing and REPL shell behavior
- help/completion/catalog wiring
- plugin protocol or subprocess dispatch mechanics
- renderer decisions and output shape rules

If those need to change generically, upstream is the right place.

## Pointers

For rendered API docs, prefer docs.rs or `cargo doc --open`.

For a runnable rustdoc version of the minimal wrapper pattern, see the crate
root in [`src/lib.rs`](../src/lib.rs).

For a copyable wrapper crate, start with
[`examples/product-wrapper/src/lib.rs`](../examples/product-wrapper/src/lib.rs)
and [`examples/product-wrapper/src/main.rs`](../examples/product-wrapper/src/main.rs).

For code-level entrypoints, start with:

- [`src/lib.rs`]../src/lib.rs
- [`src/app/mod.rs`]../src/app/mod.rs
- [`src/native.rs`]../src/native.rs
- [`src/config/mod.rs`]../src/config/mod.rs
- [`src/config/runtime.rs`]../src/config/runtime.rs