cufflink-cli 0.11.7

CLI for the Cufflink CRUD microservice platform — deploy, init, and manage services
use std::path::{Path, PathBuf};

/// Result of checking whether a directory is covered by a pnpm workspace.
#[derive(Debug)]
pub enum WorkspaceCheck {
    /// No pnpm workspace was found above this directory.
    NoWorkspace,
    /// A workspace was found and this directory is included in its `packages` globs.
    Included,
    /// A workspace was found but this directory is NOT included.
    NotIncluded {
        workspace_file: PathBuf,
        /// The relative path segment that should be added (e.g. `"cufflink-packages/*"`).
        suggested_glob: String,
    },
}

/// Check whether `dir` is covered by the nearest pnpm workspace.
///
/// Walks up from `dir` looking for `pnpm-workspace.yaml`. If found, parses
/// the `packages` globs and checks whether `dir` matches any of them (relative
/// to the workspace root).
pub fn check_workspace_membership(dir: &Path) -> WorkspaceCheck {
    let dir = match dir.canonicalize() {
        Ok(d) => d,
        Err(_) => return WorkspaceCheck::NoWorkspace,
    };

    let (workspace_root, workspace_file, globs) = match find_workspace_config(&dir) {
        Some(result) => result,
        None => return WorkspaceCheck::NoWorkspace,
    };

    let rel_path = match dir.strip_prefix(&workspace_root) {
        Ok(rel) => rel.to_string_lossy().to_string(),
        Err(_) => return WorkspaceCheck::NoWorkspace,
    };

    // Normalise to forward slashes for glob matching
    let rel_path = rel_path.replace('\\', "/");

    for glob in &globs {
        if glob_match::glob_match(glob, &rel_path) {
            return WorkspaceCheck::Included;
        }
    }

    // Build a suggested glob from the first path component of the relative path.
    // e.g. if rel_path is "cufflink-packages/cufflink-web", suggest "cufflink-packages/*".
    let suggested_glob = rel_path
        .split('/')
        .next()
        .map(|first| format!("{}/*", first))
        .unwrap_or_else(|| format!("{}/*", rel_path));

    WorkspaceCheck::NotIncluded {
        workspace_file,
        suggested_glob,
    }
}

/// Parsed pnpm-workspace.yaml structure.
#[derive(serde::Deserialize, Default)]
struct PnpmWorkspace {
    #[serde(default)]
    packages: Vec<String>,
}

/// Walk up from `start` to find `pnpm-workspace.yaml`, parse it, and return
/// (workspace_root, workspace_file_path, package_globs).
fn find_workspace_config(start: &Path) -> Option<(PathBuf, PathBuf, Vec<String>)> {
    let mut dir = start.to_path_buf();
    loop {
        let candidate = dir.join("pnpm-workspace.yaml");
        if candidate.is_file() {
            let content = std::fs::read_to_string(&candidate).ok()?;
            let workspace: PnpmWorkspace = serde_yaml::from_str(&content).unwrap_or_default();
            return Some((dir, candidate, workspace.packages));
        }
        if !dir.pop() {
            break;
        }
    }
    None
}