osp-cli 1.5.1

CLI and REPL for querying and managing OSP infrastructure data
Documentation
#[cfg(unix)]
use crate::temp_support::make_temp_dir;
#[cfg(unix)]
use osp_cli::core::command_policy::{CommandPath, VisibilityMode};
#[cfg(unix)]
use osp_cli::plugin::{PluginManager, PluginSource};

#[cfg(unix)]
fn write_executable_script(path: &std::path::Path, script: &str) {
    use std::os::unix::fs::PermissionsExt;

    std::fs::write(path, script).expect("plugin script should be written");
    let mut perms = std::fs::metadata(path)
        .expect("plugin metadata should be readable")
        .permissions();
    perms.set_mode(0o755);
    std::fs::set_permissions(path, perms).expect("plugin script should be executable");
}

#[cfg(unix)]
fn write_provider_plugin(dir: &std::path::Path, plugin_id: &str, command_name: &str) {
    let plugin_path = dir.join(format!("osp-{plugin_id}"));
    let script = format!(
        r#"#!/bin/sh
PATH=/usr/bin:/bin:$PATH
if [ "$1" = "--describe" ]; then
  cat <<'JSON'
{{"protocol_version":1,"plugin_id":"{plugin_id}","plugin_version":"0.1.0","min_osp_version":"0.1.0","commands":[{{"name":"{command_name}","about":"{plugin_id} plugin","args":[],"flags":{{}},"subcommands":[]}}]}}
JSON
  exit 0
fi

cat <<'JSON'
{{"protocol_version":1,"ok":true,"data":{{"message":"ok"}},"error":null,"meta":{{"format_hint":"table","columns":["message"]}}}}
JSON
"#,
        plugin_id = plugin_id,
        command_name = command_name,
    );
    write_executable_script(&plugin_path, &script);
}

#[cfg(unix)]
fn write_auth_plugin(dir: &std::path::Path, plugin_id: &str) {
    let plugin_path = dir.join(format!("osp-{plugin_id}"));
    let script = format!(
        r#"#!/bin/sh
PATH=/usr/bin:/bin:$PATH
if [ "$1" = "--describe" ]; then
  cat <<'JSON'
{{"protocol_version":1,"plugin_id":"{plugin_id}","plugin_version":"0.1.0","min_osp_version":"0.1.0","commands":[{{"name":"{plugin_id}","about":"{plugin_id} plugin","auth":{{"visibility":"authenticated"}},"args":[],"flags":{{}},"subcommands":[{{"name":"approval","about":"approval commands","args":[],"flags":{{}},"subcommands":[{{"name":"decide","about":"decide approvals","auth":{{"visibility":"capability_gated","required_capabilities":["{plugin_id}.approval.decide"],"feature_flags":["{plugin_id}"]}},"args":[],"flags":{{}},"subcommands":[]}}]}}]}}]}}
JSON
  exit 0
fi

cat <<'JSON'
{{"protocol_version":1,"ok":true,"data":{{"message":"ok"}},"error":null,"meta":{{"format_hint":"table","columns":["message"]}}}}
JSON
"#,
        plugin_id = plugin_id,
    );
    write_executable_script(&plugin_path, &script);
}

#[cfg(unix)]
#[test]
fn plugin_manager_surfaces_provider_selection_across_catalog_help_and_completion() {
    let root = make_temp_dir("osp-cli-plugin-manager-integration-selection");
    let plugins_dir = root.join("plugins");
    std::fs::create_dir_all(&plugins_dir).expect("plugin dir should be created");

    write_provider_plugin(&plugins_dir, "alpha", "shared");
    write_provider_plugin(&plugins_dir, "beta", "shared");
    let manager = PluginManager::new(vec![plugins_dir]);

    let mut providers = manager.command_providers("shared");
    providers.sort();
    assert_eq!(
        providers,
        vec![
            "alpha (explicit)".to_string(),
            "beta (explicit)".to_string()
        ]
    );

    let ambiguous_catalog = manager.command_catalog();
    let ambiguous_entry = ambiguous_catalog
        .iter()
        .find(|entry| entry.name == "shared")
        .expect("shared command should exist");
    assert_eq!(ambiguous_entry.provider, None);
    assert!(ambiguous_entry.conflicted);
    assert!(ambiguous_entry.requires_selection);
    assert!(!ambiguous_entry.selected_explicitly);
    assert!(
        ambiguous_entry
            .about
            .contains("provider selection required")
    );
    assert_eq!(manager.selected_provider_label("shared"), None);

    let ambiguous_help = manager.repl_help_text();
    assert!(ambiguous_help.contains("Plugin commands:"));
    assert!(ambiguous_help.contains("shared"));
    assert!(ambiguous_help.contains("providers: alpha (explicit), beta (explicit)"));

    let words = manager.completion_words();
    assert!(words.contains(&"shared".to_string()));
    assert!(words.contains(&"help".to_string()));
    assert!(words.contains(&"|".to_string()));

    let doctor = manager.doctor();
    assert!(
        doctor
            .conflicts
            .iter()
            .any(|conflict| conflict.command == "shared"
                && conflict.providers.contains(&"alpha (explicit)".to_string())
                && conflict.providers.contains(&"beta (explicit)".to_string()))
    );

    manager
        .select_provider("shared", "beta")
        .expect("provider selection should be applied");

    let selected_catalog = manager.command_catalog();
    let selected_entry = selected_catalog
        .iter()
        .find(|entry| entry.name == "shared")
        .expect("shared command should exist");
    assert_eq!(selected_entry.provider.as_deref(), Some("beta"));
    assert_eq!(selected_entry.source, Some(PluginSource::Explicit));
    assert!(selected_entry.conflicted);
    assert!(!selected_entry.requires_selection);
    assert!(selected_entry.selected_explicitly);
    assert_eq!(
        manager.selected_provider_label("shared").as_deref(),
        Some("beta (explicit)")
    );

    let selected_help = manager.repl_help_text();
    assert!(selected_help.contains("shared - beta plugin"));
    assert!(selected_help.contains("(beta/explicit)"));
    assert!(selected_help.contains("conflicts: alpha (explicit), beta (explicit)"));

    assert!(
        manager
            .clear_provider_selection("shared")
            .expect("provider selection should clear")
    );
    assert_eq!(manager.selected_provider_label("shared"), None);
}

#[cfg(unix)]
#[test]
fn plugin_manager_projects_recursive_auth_metadata_into_catalog_and_policy_registry() {
    let root = make_temp_dir("osp-cli-plugin-manager-integration-policy");
    let plugins_dir = root.join("plugins");
    std::fs::create_dir_all(&plugins_dir).expect("plugin dir should be created");

    write_auth_plugin(&plugins_dir, "orch");
    let manager = PluginManager::new(vec![plugins_dir]);

    let catalog = manager.command_catalog();
    let orch = catalog
        .iter()
        .find(|entry| entry.name == "orch")
        .expect("orch command should exist");
    assert_eq!(orch.auth_hint().as_deref(), Some("auth"));
    assert_eq!(orch.subcommands, vec!["approval".to_string()]);

    let help = manager.repl_help_text();
    assert!(help.contains("orch [approval] - orch plugin [auth] (orch/explicit)"));

    let registry = manager.command_policy_registry();
    let root_policy = registry
        .resolved_policy(&CommandPath::new(["orch"]))
        .expect("root command policy should exist");
    assert_eq!(root_policy.visibility, VisibilityMode::Authenticated);

    let nested_policy = registry
        .resolved_policy(&CommandPath::new(["orch", "approval", "decide"]))
        .expect("nested command policy should exist");
    assert_eq!(nested_policy.visibility, VisibilityMode::CapabilityGated);
    assert!(
        nested_policy
            .required_capabilities
            .contains("orch.approval.decide")
    );
    assert!(nested_policy.feature_flags.contains("orch"));
}