standard-plugin-cli 0.1.1

standard-plugin: scaffold, build, check and pack Standard Code plugins
Documentation
//! Finding plugins on disk: a plugin directory holds a crate and its
//! manifest (`bundle/standard-plugin.json`, or `standard-plugin.json` beside
//! `Cargo.toml`); `bundle/` receives what `build` makes.

use std::path::{Path, PathBuf};

use standard_plugin_manifest::{MANIFEST_MAX_BYTES, Manifest};

/// One plugin crate.
#[derive(Clone, Debug)]
pub struct Plugin {
    pub dir: PathBuf,
    /// Where the manifest source lives.
    pub manifest_path: PathBuf,
    /// Where the bundle is assembled.
    pub bundle: PathBuf,
}

impl Plugin {
    fn at(dir: &Path) -> Option<Self> {
        let bundle = dir.join("bundle");
        [
            bundle.join("standard-plugin.json"),
            dir.join("standard-plugin.json"),
        ]
        .into_iter()
        .find(|path| path.is_file())
        .map(|manifest_path| Self {
            dir: dir.to_path_buf(),
            manifest_path,
            bundle,
        })
    }

    pub fn manifest_bytes(&self) -> Result<Vec<u8>, String> {
        let bytes = std::fs::read(&self.manifest_path)
            .map_err(|error| format!("{}: {error}", self.manifest_path.display()))?;
        if bytes.len() as u64 > MANIFEST_MAX_BYTES {
            return Err(format!(
                "{} is larger than {MANIFEST_MAX_BYTES} bytes",
                self.manifest_path.display()
            ));
        }
        Ok(bytes)
    }

    pub fn manifest(&self) -> Result<Manifest, String> {
        Manifest::parse(&self.manifest_bytes()?)
            .map_err(|error| format!("{}: {error}", self.manifest_path.display()))
    }

    /// Copies a manifest kept beside `Cargo.toml` into the bundle.
    pub fn stage_manifest(&self) -> std::io::Result<()> {
        std::fs::create_dir_all(&self.bundle)?;
        let staged = self.bundle.join("standard-plugin.json");
        if self.manifest_path != staged {
            std::fs::copy(&self.manifest_path, staged)?;
        }
        Ok(())
    }
}

/// The plugins in `dir`: `dir` itself, or else its immediate
/// subdirectories that are plugins (a companion pair's `ui/` and
/// `companion/`).
pub fn plugins(dir: &Path) -> Result<Vec<Plugin>, String> {
    if let Some(plugin) = Plugin::at(dir) {
        return Ok(vec![plugin]);
    }
    let mut found: Vec<Plugin> = std::fs::read_dir(dir)
        .map_err(|error| format!("{}: {error}", dir.display()))?
        .filter_map(Result::ok)
        .map(|entry| entry.path())
        .filter(|path| path.is_dir())
        .filter_map(|path| Plugin::at(&path))
        .collect();
    found.sort_by(|a, b| a.dir.cmp(&b.dir));
    if found.is_empty() {
        return Err(format!(
            "no plugin in {}: expected bundle/standard-plugin.json or standard-plugin.json",
            dir.display()
        ));
    }
    Ok(found)
}