wsb 0.291.8591

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! `wsb plugin` — which plugins this workshop lets use wsb's services.
//!
//! Enabling is the owner's act and a file's worth of state: the list is read
//! by the daemon at every registration and every call, so a change here takes
//! effect at once, daemon running or not. See [`crate::plugin`].

use std::path::PathBuf;

use crate::plugin::{PluginName, ENABLED_FILE};

pub const USAGE: &str = "\
Usage: wsb plugin <command>

  list               the plugins enabled on this host, and which are running
  add <name> [--program <path>]
                     enable a plugin: it may register its services with the
                     daemon, and use the peer bridge. The program it is —
                     the <name> first on this PATH, or <path> — is recorded,
                     and a stamp hook that names the plugin runs that program
  remove <name>      disable it; it stops answering at once

A plugin's services are carried between paired workshops over the connection
they already hold, each switched per workshop and off until switched on:
`wsb peer service <peer> <plugin>.<service> on`.";

pub fn dispatch(args: &[String]) -> u8 {
    let verb = args.get(2).map(String::as_str);
    let rest = if args.len() > 3 { &args[3..] } else { &[] };
    let result = match verb {
        None | Some("list") => list(),
        Some("add") => add(rest),
        Some("remove") => remove(rest),
        Some("help") | Some("--help") | Some("-h") => {
            crate::outln!("{USAGE}");
            return 0;
        }
        Some(other) => {
            crate::errln!("wsb plugin: unknown command '{other}'\n\n{USAGE}");
            return 2;
        }
    };
    match result {
        Ok(()) => 0,
        Err(message) => {
            crate::errln!("wsb plugin: {message}");
            1
        }
    }
}

fn name(rest: &[String]) -> Result<PluginName, String> {
    match rest {
        [one] => PluginName::parse(one),
        _ => Err(format!("name one plugin\n\n{USAGE}")),
    }
}

fn add(rest: &[String]) -> Result<(), String> {
    let (plugin, given) = match rest {
        [one] => (PluginName::parse(one)?, None),
        [one, flag, path] if flag == "--program" => (PluginName::parse(one)?, Some(PathBuf::from(path))),
        _ => return Err(format!("name one plugin, and its program with --program <path> if you like\n\n{USAGE}")),
    };
    let host_dir = crate::daemon::host_dir()?;
    let program = match given {
        Some(path) if crate::platform::is_executable(&path) => Some(path),
        Some(path) => return Err(format!("{} is not a program this account can run", path.display())),
        None => crate::report::which(plugin.as_str()).map(PathBuf::from),
    }
    .map(|path| crate::platform::canonicalize(&path).map_err(|e| format!("cannot resolve {}: {e}", path.display())))
    .transpose()?;
    if crate::plugin::enable(&host_dir, &plugin)? {
        crate::outln!("enabled plugin {plugin} on this host");
    } else {
        crate::outln!("plugin {plugin} was already enabled on this host");
    }
    match program {
        Some(program) => {
            crate::plugin::set_program(&host_dir, &plugin, &program)?;
            crate::outln!("  its program is {}; a stamp hook that names {plugin} runs it", program.display());
        }
        None => crate::outln!(
            "  no {plugin} is on this PATH, so a stamp hook that names it is refused until it is \
             named: wsb plugin add {plugin} --program <path>"
        ),
    }
    Ok(())
}

fn remove(rest: &[String]) -> Result<(), String> {
    let plugin = name(rest)?;
    let host_dir = crate::daemon::host_dir()?;
    if crate::plugin::disable(&host_dir, &plugin)? {
        crate::outln!("disabled plugin {plugin}: it answers nothing from now on");
    } else {
        crate::outln!(
            "plugin {plugin} is not enabled on this host ({})",
            host_dir.join(ENABLED_FILE).display()
        );
    }
    Ok(())
}

/// What is enabled, from the file, and what is running, from the daemon.
///
/// The daemon is asked only if it is already running: a list of what runs
/// does not start anything, and "no daemon" answers it.
fn list() -> Result<(), String> {
    let host_dir = crate::daemon::host_dir()?;
    let enabled = crate::plugin::enabled(&host_dir)?;
    if enabled.is_empty() {
        crate::outln!("no plugin is enabled on this host; `wsb plugin add <name>` enables one");
        return Ok(());
    }
    let running: Vec<serde_json::Value> = match crate::daemon::WorkshopRendezvous::read(&host_dir)? {
        None => Vec::new(),
        Some(rendezvous) => {
            let client = crate::daemon::Client::from_rendezvous(&rendezvous)?;
            let answer: serde_json::Value = client.operator_get_json(crate::plugin::route::LIST)?;
            answer["running"].as_array().cloned().unwrap_or_default()
        }
    };
    for plugin in &enabled {
        let entry = running.iter().find(|entry| entry["plugin"] == plugin.as_str());
        match entry {
            Some(entry) => {
                let services: Vec<&str> = entry["services"]
                    .as_array()
                    .map(|services| services.iter().filter_map(|s| s.as_str()).collect())
                    .unwrap_or_default();
                crate::outln!(
                    "{plugin}  running (pid {}): {}",
                    entry["pid"],
                    services.join(", ")
                );
            }
            None => crate::outln!("{plugin}  enabled, not running"),
        }
        match crate::plugin::program(&host_dir, plugin.as_str())? {
            Some(Some(program)) => crate::outln!("  program {}", program.display()),
            _ => crate::outln!("  no program recorded: wsb plugin add {plugin} --program <path>"),
        }
    }
    Ok(())
}