osp-cli 1.5.1

CLI and REPL for querying and managing OSP infrastructure data
Documentation
use miette::Result;

use crate::app::CliCommandResult;
use crate::app::{AppClients, AppRuntime, AppSession, authorized_command_catalog_for};
use crate::cli::IntroArgs;
use crate::repl::ReplViewContext;
use crate::repl::presentation::build_repl_intro_payload;
use crate::repl::surface::ReplSurface;

pub(crate) struct IntroCommandContext<'a> {
    pub(crate) view: ReplViewContext<'a>,
    pub(crate) surface: ReplSurface,
}

impl<'a> IntroCommandContext<'a> {
    pub(crate) fn from_parts(
        runtime: &'a AppRuntime,
        session: &'a AppSession,
        clients: &'a AppClients,
        ui: &'a crate::app::UiState,
    ) -> Self {
        let view = ReplViewContext {
            config: runtime.config.resolved(),
            ui,
            auth: &runtime.auth,
            themes: &runtime.themes,
            scope: &session.scope,
        };
        let surface = authorized_command_catalog_for(&runtime.auth, clients)
            .ok()
            .map(|catalog| crate::repl::surface::build_repl_surface(view, &catalog))
            .unwrap_or_else(|| crate::repl::surface::ReplSurface {
                root_words: Vec::new(),
                intro_commands: Vec::new(),
                specs: Vec::new(),
                aliases: Vec::new(),
                overview_entries: Vec::new(),
            });

        Self { view, surface }
    }
}

pub(crate) fn run_intro_command(
    context: IntroCommandContext<'_>,
    _args: IntroArgs,
) -> Result<CliCommandResult> {
    Ok(CliCommandResult::guide(build_repl_intro_payload(
        context.view,
        &context.surface,
        None,
    )))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::app::{AppState, ReplCommandOutput, RuntimeContext, TerminalKind};
    use crate::config::{ConfigLayer, ConfigResolver, ResolveOptions};
    use crate::ui::build_presentation_defaults_layer;

    fn resolved_repl_config() -> crate::config::ResolvedConfig {
        let mut defaults = ConfigLayer::default();
        defaults.set("profile.default", "default");

        let mut resolver = ConfigResolver::default();
        resolver.set_defaults(defaults);

        let options = ResolveOptions::default().with_terminal("repl");
        let base = resolver
            .resolve(options.clone())
            .expect("base REPL config should resolve");
        resolver.set_presentation(build_presentation_defaults_layer(&base));
        resolver
            .resolve(options)
            .expect("presentation-seeded REPL config should resolve")
    }

    fn intro_state() -> AppState {
        AppState::from_resolved_config(
            RuntimeContext::new(None, TerminalKind::Repl, None),
            resolved_repl_config(),
        )
        .expect("intro test state should build")
    }

    #[test]
    fn intro_context_projects_visible_surface_for_repl_intro_unit() {
        let state = intro_state();

        let context = IntroCommandContext::from_parts(
            &state.runtime,
            &state.session,
            &state.clients,
            state.runtime.ui(),
        );

        assert!(context.surface.root_words.iter().any(|word| word == "help"));
        assert!(
            context
                .surface
                .intro_commands
                .iter()
                .all(|word| word != "exit" && word != "quit")
        );
        assert!(
            context
                .surface
                .overview_entries
                .iter()
                .any(|entry| entry.name == "help")
        );
    }

    #[test]
    fn intro_command_returns_structured_guide_output_unit() {
        let state = intro_state();
        let context = IntroCommandContext::from_parts(
            &state.runtime,
            &state.session,
            &state.clients,
            state.runtime.ui(),
        );

        let result =
            run_intro_command(context, IntroArgs::default()).expect("intro command should succeed");

        assert_eq!(result.exit_code, 0);
        let ReplCommandOutput::Output(structured) = result
            .output
            .expect("intro command should emit structured output")
        else {
            panic!("intro command should return guide output");
        };
        assert!(structured.source_guide.is_some());
    }
}