tsafe-cli 1.0.20

tsafe CLI — local secret and credential manager (replaces .env files)
Documentation
use std::path::PathBuf;

use anyhow::{ensure, Context};
use clap::{Parser, ValueEnum};
use tsafe_cli::manpages;

const RELEASE_MAN_DIR: &str = "docs/man";
// Sorted to match the compile_time_feature_flags() sort order (sort_unstable on &str).
// Must stay in sync with the `default` feature list in tsafe-cli/Cargo.toml.
// Last updated: ssh promoted to default in commit 79f4a721.
const DEFAULT_CORE_BUILD_PROFILE: &[&str] =
    &["agent", "akv-pull", "biometric", "ssh", "team-core", "tui"];

#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
enum ReleaseProfile {
    #[value(name = "default-core")]
    DefaultCore,
    #[value(name = "compiled")]
    Compiled,
}

impl ReleaseProfile {
    fn as_str(self) -> &'static str {
        match self {
            Self::DefaultCore => "default-core",
            Self::Compiled => "compiled",
        }
    }
}

#[derive(Debug, Parser)]
#[command(
    name = "generate-manpages",
    about = "Generate tsafe CLI manpages from the compiled clap tree",
    long_about = "Generate tsafe CLI manpages from the compiled clap tree.\n\n`docs/man` is reserved for the frozen default-core release surface. Broader/custom builds must opt into `--release-profile compiled` and write to a separate output directory."
)]
struct Args {
    /// Choose which release lane this run is allowed to write for.
    #[arg(long, value_enum, default_value = "default-core")]
    release_profile: ReleaseProfile,
    /// Output directory for the generated `*.1` manpages.
    #[arg(default_value = RELEASE_MAN_DIR)]
    output_dir: PathBuf,
}

fn run(args: Args) -> anyhow::Result<()> {
    validate_generation_boundary(&args)?;

    let compiled_profile = current_build_profile_label();
    let written = manpages::render_to_dir(&args.output_dir).with_context(|| {
        format!(
            "failed to write man pages into {}",
            args.output_dir.display()
        )
    })?;

    println!(
        "Generated {} man page(s) for release lane {} from compiled build {} in {}",
        written.len(),
        args.release_profile.as_str(),
        compiled_profile,
        args.output_dir.display()
    );
    for path in written {
        println!("  {}", path.display());
    }

    Ok(())
}

fn main() -> anyhow::Result<()> {
    let args = Args::parse();

    #[cfg(windows)]
    {
        let worker = std::thread::Builder::new()
            .name("generate-manpages".into())
            .stack_size(32 * 1024 * 1024)
            .spawn(move || run(args))
            .context("failed to start generate-manpages worker")?;

        match worker.join() {
            Ok(result) => result,
            Err(_) => Err(anyhow::anyhow!("generate-manpages worker panicked")),
        }
    }

    #[cfg(not(windows))]
    {
        run(args)
    }
}

fn validate_generation_boundary(args: &Args) -> anyhow::Result<()> {
    validate_generation_boundary_for(
        current_build_profile_label(),
        &args.output_dir,
        args.release_profile,
    )
}

fn validate_generation_boundary_for(
    compiled_profile: &str,
    output_dir: &std::path::Path,
    release_profile: ReleaseProfile,
) -> anyhow::Result<()> {
    if release_profile == ReleaseProfile::Compiled {
        ensure!(
            !is_release_docs_man_dir(output_dir),
            "`{RELEASE_MAN_DIR}` is reserved for the frozen default-core manpage set; use a separate output directory for broader/custom compiled builds",
        );
        return Ok(());
    }

    ensure!(
        compiled_profile == "default-core",
        "default-core manpage generation requires a default-core compiled build, but this binary is `{compiled_profile}`; rebuild with the frozen default feature set or use `--release-profile compiled` with a separate output directory",
    );

    Ok(())
}

fn is_release_docs_man_dir(path: &std::path::Path) -> bool {
    const RELEASE_MAN_COMPONENTS: &[&str] = &["docs", "man"];

    let components = path
        .components()
        .filter_map(|component| component.as_os_str().to_str())
        .collect::<Vec<_>>();

    components.len() >= RELEASE_MAN_COMPONENTS.len()
        && components[components.len() - RELEASE_MAN_COMPONENTS.len()..] == *RELEASE_MAN_COMPONENTS
}

fn current_build_profile_label() -> &'static str {
    let capabilities = compile_time_feature_flags();
    build_profile_label(&capabilities)
}

fn build_profile_label(capabilities: &[&'static str]) -> &'static str {
    if capabilities.is_empty() {
        "enterprise-minimal"
    } else if capabilities == DEFAULT_CORE_BUILD_PROFILE {
        "default-core"
    } else {
        "custom"
    }
}

fn compile_time_feature_flags() -> Vec<&'static str> {
    let mut feature_flags = Vec::new();

    if cfg!(feature = "agent") {
        feature_flags.push("agent");
    }
    if cfg!(feature = "akv-pull") {
        feature_flags.push("akv-pull");
    }
    if cfg!(feature = "biometric") {
        feature_flags.push("biometric");
    }
    if cfg!(feature = "team-core") {
        feature_flags.push("team-core");
    }
    if cfg!(feature = "tui") {
        feature_flags.push("tui");
    }
    if cfg!(feature = "cloud-pull-aws") {
        feature_flags.push("cloud-pull-aws");
    }
    if cfg!(feature = "cloud-pull-gcp") {
        feature_flags.push("cloud-pull-gcp");
    }
    if cfg!(feature = "cloud-pull-vault") {
        feature_flags.push("cloud-pull-vault");
    }
    if cfg!(feature = "cloud-pull-1password") {
        feature_flags.push("cloud-pull-1password");
    }
    if cfg!(feature = "multi-pull") {
        feature_flags.push("multi-pull");
    }
    if cfg!(feature = "pm-import-extended") {
        feature_flags.push("pm-import-extended");
    }
    if cfg!(feature = "ots-sharing") {
        feature_flags.push("ots-sharing");
    }
    if cfg!(feature = "git-helpers") {
        feature_flags.push("git-helpers");
    }
    if cfg!(feature = "browser") {
        feature_flags.push("browser");
    }
    if cfg!(feature = "nativehost") {
        feature_flags.push("nativehost");
    }
    if cfg!(feature = "ssh") {
        feature_flags.push("ssh");
    }
    if cfg!(feature = "plugins") {
        feature_flags.push("plugins");
    }
    if cfg!(feature = "otel") {
        feature_flags.push("otel");
    }

    feature_flags.sort_unstable();
    feature_flags
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::Path;

    #[test]
    fn release_docs_man_dir_detection_matches_relative_and_absolute_paths() {
        assert!(is_release_docs_man_dir(Path::new("docs/man")));
        assert!(is_release_docs_man_dir(Path::new("./docs/man")));
        assert!(is_release_docs_man_dir(Path::new("C:/repo/docs/man")));
        assert!(!is_release_docs_man_dir(Path::new("target/man")));
    }

    #[test]
    fn default_core_generation_requires_default_core_build() {
        let err = validate_generation_boundary_for(
            "custom",
            Path::new("target/custom-manpages"),
            ReleaseProfile::DefaultCore,
        )
        .unwrap_err();

        assert!(err.to_string().contains("default-core compiled build"));
    }

    #[test]
    fn release_docs_man_dir_rejects_compiled_lane_generation() {
        let err = validate_generation_boundary_for(
            "custom",
            Path::new("docs/man"),
            ReleaseProfile::Compiled,
        )
        .unwrap_err();

        assert!(err
            .to_string()
            .contains("reserved for the frozen default-core"));
    }

    #[test]
    fn broader_compiled_lane_can_write_to_separate_directory() {
        validate_generation_boundary_for(
            "custom",
            Path::new("target/gated-broader-dev-manpages"),
            ReleaseProfile::Compiled,
        )
        .unwrap();
    }
}