alba 0.2.1

Alba, the Beamfile task runner: parallel beams, caching, watch mode, and an interactive interface
//! Clap derive types for the `alba` binary's command line.
//!
//! Kept deliberately thin: `Cli` holds the flags that apply regardless of
//! subcommand (today, only `--file`), `Command` is the subcommand enum, and
//! `RunFlags` groups everything that shapes a run. Nothing here executes
//! anything — `main.rs` is the only place a parsed `Cli` is acted on.

use std::path::PathBuf;

use clap::{Args, Parser, Subcommand, ValueEnum};

/// Parsed command line for the `alba` binary.
#[derive(Debug, Parser)]
#[command(name = "alba", version, about = "A task runner for Beamfiles")]
pub struct Cli {
    /// Path to the Beamfile to load. Defaults to `./Beamfile` in the
    /// current directory when omitted.
    #[arg(long, global = true, value_name = "PATH")]
    pub file: Option<PathBuf>,

    /// The subcommand to run. `None` is bare `alba`: runs the declared
    /// `default` beam, or lists beams when there is none.
    #[command(subcommand)]
    pub command: Option<Command>,
}

/// A subcommand of the `alba` binary.
#[derive(Debug, Subcommand)]
pub enum Command {
    /// Load and validate the Beamfile without running anything.
    Check,
    /// Run a beam and everything it needs.
    Run {
        /// The beam to run. Defaults to the Beamfile's `default` beam.
        #[arg(value_name = "BEAM")]
        beam: Option<String>,
        /// Positional arguments bound, in order, to the parameters the
        /// beam declares (`beam deploy(target)` takes one).
        #[arg(value_name = "PARAM")]
        params: Vec<String>,
        #[command(flatten)]
        flags: RunFlags,
    },
    /// List the beams affected by what changed since a git reference,
    /// without running anything.
    Affected {
        /// The git reference to diff the working tree against.
        #[arg(value_name = "REF")]
        reference: String,
        /// `text`: one beam per line; `json`: `{"beams": [...]}`.
        #[arg(long, value_name = "FORMAT", value_enum, default_value_t = LogFormat::Text)]
        log_format: LogFormat,
    },
    /// Manage the project's cache.
    Cache {
        #[command(subcommand)]
        command: CacheCommand,
    },
    /// Work with executor plugins.
    Plugin {
        #[command(subcommand)]
        command: PluginCommand,
    },
    /// Run the beam a declared git hook points at. Called by the scripts
    /// `alba hooks install` writes; usable by hand to debug a hook.
    #[command(hide = true)]
    Hook {
        /// The git hook name (`pre-commit`, `commit-msg`, ...).
        #[arg(value_name = "NAME")]
        name: String,
        /// The arguments git passed to the hook.
        #[arg(
            value_name = "ARG",
            trailing_var_arg = true,
            allow_hyphen_values = true
        )]
        args: Vec<String>,
    },
    /// Install or remove the git hooks this Beamfile declares.
    Hooks {
        #[command(subcommand)]
        command: HooksCommand,
    },
    /// Update alba to the latest release. Only for an alba the release
    /// installers put in place; a `cargo install alba` updates itself the
    /// same way it was installed.
    Update,
}

/// A subcommand of `alba hooks`.
#[derive(Debug, Subcommand)]
pub enum HooksCommand {
    /// Point `core.hooksPath` at `.alba/hooks` and write the hook scripts.
    Install,
    /// Remove the scripts and the `core.hooksPath` setting, if Alba set it.
    Uninstall,
}

/// A subcommand of `alba cache`. `clean` is deliberately the only one for
/// now; the subcommand level exists so `alba cache status` can join it
/// without breaking the CLI's shape.
#[derive(Debug, Subcommand)]
pub enum CacheCommand {
    /// Remove every cache entry for this project.
    Clean,
}

/// A subcommand of `alba plugin`. `check` is deliberately the only one for
/// now; the subcommand level exists so a future `alba plugin scaffold` (or
/// similar) can join it without breaking the CLI's shape.
#[derive(Debug, Subcommand)]
pub enum PluginCommand {
    /// Drive an executor plugin binary through protocol v1 and report
    /// whether it conforms.
    Check {
        /// Path to the plugin binary to check.
        #[arg(value_name = "BINARY")]
        binary: PathBuf,
        /// The command sent in the execution check.
        #[arg(long, value_name = "CMD", default_value = "echo alba-plugin-check")]
        command: String,
        /// A long-running command for the cancellation check; the plugin
        /// must answer the cancel within the 5-second grace.
        ///
        /// Must run well past the 100ms the check waits before sending
        /// `cancel` — otherwise the command finishes on its own first and
        /// the check cannot tell that apart from a prompt answer. 30
        /// seconds comfortably outlives both that delay and the grace.
        #[arg(long, value_name = "CMD", default_value = "sleep 30000")]
        cancel_command: String,
    },
}

/// Everything that shapes a run, as opposed to what the Beamfile declares.
///
/// A struct of its own (rather than fields on `Command::Run`) because bare
/// `alba` runs the declared `default` beam through the same code path and
/// needs a value for all of it — which is exactly `RunFlags::default()`.
#[derive(Debug, Default, Args)]
pub struct RunFlags {
    /// How many beams may run at once
    ///
    /// At least 1. Defaults to the machine's available parallelism.
    // `0` is rejected rather than accepted and reinterpreted: a run with no
    // slots at all is not a run, and quietly reading it as `1` would turn a
    // typo into a silently sequential build.
    #[arg(long, value_name = "N", value_parser = clap::value_parser!(u64).range(1..))]
    pub jobs: Option<u64>,

    /// Keep going after a beam fails, instead of cancelling the beams that
    /// have not started
    #[arg(long)]
    pub keep_going: bool,

    /// Ignore the cache when reading: run every beam, and rewrite the
    /// cache entries of the ones that succeed
    #[arg(long)]
    pub force: bool,

    /// How text output is laid out
    ///
    /// Defaults to `interleaved` on a terminal and `grouped` otherwise.
    /// Ignored with `--log-format json`.
    #[arg(long, value_name = "STYLE", value_enum)]
    pub output: Option<OutputStyle>,

    /// What stdout carries
    ///
    /// Either human-readable text, laid out by `--output`, or one JSON
    /// object per event and per line.
    #[arg(long, value_name = "FORMAT", value_enum, default_value_t = LogFormat::Text)]
    pub log_format: LogFormat,

    /// Keep running: re-run the beam whenever the files its subgraph
    /// declares as `inputs` change. Ctrl-C ends the session.
    #[arg(long)]
    pub watch: bool,

    /// Run only the beams affected by what changed since this git
    /// reference (their `inputs`, or their Beamfile), and their dependents.
    /// With a beam, that beam runs if it is affected and nothing runs
    /// otherwise; without one, every affected beam is a target.
    #[arg(long, value_name = "REF")]
    pub affected: Option<String>,

    /// Force the interactive interface on, even where it would default off
    ///
    /// It still needs a real terminal to draw on: down a pipe this is
    /// refused rather than honoured.
    #[arg(long, conflicts_with = "no_ui")]
    pub ui: bool,

    /// Force the headless output, even on a terminal
    #[arg(long)]
    pub no_ui: bool,
}

/// How the text renderers lay a run out.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OutputStyle {
    /// Every line as it happens, prefixed with the beam it came from.
    Interleaved,
    /// Each beam's output held back and printed as one block when it ends.
    Grouped,
}

/// What stdout carries during a run.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
pub enum LogFormat {
    /// Human-readable, laid out by `--output`.
    #[default]
    Text,
    /// One JSON object per event, one per line.
    Json,
}