clish 0.1.0-beta.4

Elegant CLI framework for Rust.
Documentation
use clish::help::{AppStyle, AppStyles};
use clish::prelude::*;

/// Compile source code for the specified target.
///
/// Supports optimization levels 0-3 and size-optimized builds,
/// parallel compilation, debug symbols, and checksum generation.
#[command(
    param(target, help = "Target triple", placeholder = "TRIPLE", value_hint = "string"),
    param(opt_level, help = "Optimization level", short = 'O',
          choices = ["0", "1", "2", "3", "s", "z"], default = "2",
          details = "Controls code generation optimization. \
                     's' optimizes for binary size, \
                     'z' optimizes even more aggressively for size."),
    param(jobs, help = "Parallel compilation jobs", short = 'j', placeholder = "N"),
    param(release, help = "Release mode", short = 'r'),
    param(debug, help = "Include debug symbols", short = 'g',
          conflicts_with = ["release"]),
    param(verbose, help = "Verbose output", short = 'v'),
    param(checksum, help = "Write SHA-256 checksum file", name = "gen-checksum"),
)]
fn build(
    target: Pos<String>,
    opt_level: Named<String>,
    jobs: Named<Option<u32>>,
    release: bool,
    debug: bool,
    verbose: bool,
    checksum: bool,
) {
    println!("Building for target: {target}");
    if release {
        println!("  Mode: release");
    }
    if debug {
        println!("  Debug symbols: enabled");
    }
    println!("  Optimization: -O{opt_level}");
    if let Some(j) = jobs {
        println!("  Parallel jobs: {j}");
    }
    if verbose {
        println!("  Verbose: on");
    }
    if checksum {
        println!("  Writing checksum: {target}.sha256");
    }
    println!("  Build complete.");
}

/// Run the project test suite.
///
/// Executes tests matching the provided filter patterns.
/// Supports multiple output formats and coverage collection.
#[command(
    param(patterns, help = "Test filter patterns (repeatable)", short = 't',
          placeholder = "PATTERN"),
    param(format, help = "Output format", short = 'f',
          choices = ["pretty", "compact", "junit", "tap"], default = "pretty",
          env = "PL_TEST_FORMAT"),
    param(junit_out, help = "JUnit report path", name = "junit-output",
          placeholder = "PATH", value_hint = "file"),
    param(coverage, help = "Collect code coverage", short = 'c'),
    param(jobs, help = "Parallel test jobs", short = 'j', placeholder = "N"),
    param(quiet, help = "Suppress progress output", short = 'q'),
    param(verbose, help = "Verbose test output", short = 'v'),
)]
fn test(
    patterns: Named<Vec<String>>,
    format: Named<String>,
    junit_out: Named<Option<String>>,
    coverage: bool,
    jobs: Named<Option<u16>>,
    quiet: bool,
    verbose: bool,
) {
    if !patterns.is_empty() {
        println!("Test patterns: {}", patterns.join(", "));
    }
    println!("Format: {format}");
    if let Some(path) = junit_out {
        println!("JUnit output: {path}");
    }
    if coverage {
        println!("Coverage: enabled");
    }
    if let Some(j) = jobs {
        println!("Parallel jobs: {j}");
    }
    if quiet {
        println!("Quiet mode: on");
    }
    if verbose {
        println!("Verbose: on");
    }
    println!("All tests passed.");
}

/// Deploy to one or more target environments.
///
/// Supports rolling updates, canary deployments, and
/// rollback automation for multiple target environments.
/// Targets are specified as variadic positional arguments.
#[command(
    param(targets, help = "Target environments (variadic)", placeholder = "ENV"),
    param(region, help = "AWS region", default = "us-east-1",
          short = 'r', placeholder = "REGION"),
    param(strategy, help = "Deployment strategy",
          short = 's', choices = ["rolling", "bluegreen", "recreate"],
          default = "rolling"),
    param(port, help = "Application port", short = 'p'),
    param(tag, help = "Container image tag", name = "tag", default = "latest"),
    param(canary, help = "Canary percentage", short = 'c',
          placeholder = "PCT", requires = ["rollback"]),
    param(rollback, help = "Rollback timeout (seconds)",
          name = "rollback-timeout", short = 'b'),
    param(dry_run, help = "Validate without deploying", name = "dry-run",
          short = 'n'),
    param(verbose, help = "Verbose output", short = 'v'),
)]
fn deploy(
    targets: Pos<Vec<String>>,
    region: Named<String>,
    strategy: Named<String>,
    port: Named<Option<u16>>,
    tag: Named<String>,
    canary: Named<Option<u8>>,
    rollback: Named<Option<u16>>,
    dry_run: bool,
    verbose: bool,
) {
    println!("Deploying to: {}", targets.join(", "));
    println!("  Region: {region}");
    println!("  Strategy: {strategy}");
    println!("  Image tag: {tag}");
    if let Some(p) = port {
        println!("  Port: {p}");
    }
    if let Some(c) = canary {
        println!("  Canary: {c}%");
    }
    if let Some(rb) = rollback {
        println!("  Rollback timeout: {rb}s");
    }
    if verbose {
        println!("  Verbose: on");
    }
    if dry_run {
        println!("  (dry run — no changes made)");
    }
    if !dry_run {
        println!("  Deploy complete.");
    }
}

/// Manage pipeline configuration.
///
/// Reads from and writes to the local configuration store.
/// Use `pl config <key>` to read a value or
/// `pl config <key> <value>` to set it.
#[command(
    deprecated = true,
    deprecation_note = "use the 'pl config --cli' subcommands instead",
    aliases = ["conf", "cfg"],
    param(key, help = "Configuration key", placeholder = "KEY"),
    param(value, help = "Configuration value (omit to read)", placeholder = "VALUE"),
    param(file, help = "Config file path", short = 'f', hide = true),
    param(port, help = "Config service port", short = 'p'),
)]
fn config(
    key: Pos<String>,
    value: Pos<Option<String>>,
    file: Named<Option<String>>,
    port: Named<Option<u16>>,
) {
    if let Some(v) = value {
        println!("Setting config: {key} = {v}");
    } else {
        println!("Config {key} = (stored value)");
    }
    if let Some(f) = file {
        println!("  File: {f}");
    }
    if let Some(p) = port {
        println!("  Port: {p}");
    }
}

/// Execute a command in a pipeline container.
///
/// Spins up a container, runs the specified command and arguments,
/// and streams the output. Use `--` to separate command arguments
/// from pipeline options.
#[command(
    help = "Run a command in a container",
    details = "Executes the given command inside a fresh \
               container using the specified image. All arguments \
               after the first positional are treated as the command \
               and its arguments. Use -- to separate container \
               options from the command.",
    param(command, help = "Command and arguments (use -- to separate)",
          placeholder = "CMD"),
    param(image, help = "Container image", default = "alpine:latest",
          short = 'i', placeholder = "IMAGE"),
    param(timeout, help = "Execution timeout (seconds)", short = 't',
          placeholder = "SECS",
          details = "Kills the container if execution exceeds this limit. \
                     No timeout is applied when omitted."),
    param(label, help = "Container labels (repeatable)", short = 'l',
          placeholder = "KEY=VAL"),
    param(env_var, help = "Environment variables (repeatable)",
          name = "env", short = 'e', placeholder = "VAR=VAL"),
    param(detach, help = "Run in background", short = 'd',
          conflicts_with = ["wait"]),
    param(wait, help = "Wait for completion", short = 'w'),
    param(tty, help = "Allocate a pseudo-TTY", short = 'T'),
    param(verbose, help = "Verbose output", short = 'v'),
)]
fn exec(
    command: Pos<Vec<String>>,
    image: Named<String>,
    timeout: Named<Option<u32>>,
    label: Named<Vec<String>>,
    env_var: Named<Vec<String>>,
    detach: bool,
    wait: bool,
    tty: bool,
    verbose: bool,
) {
    let cmd_str = command.join(" ");
    println!("Executing: {cmd_str}");
    println!("  Image: {image}");
    if let Some(t) = timeout {
        println!("  Timeout: {t}s");
    }
    for l in &label {
        println!("  Label: {l}");
    }
    for e in &env_var {
        println!("  Env: {e}");
    }
    if detach {
        println!("  Background: yes");
    }
    if wait {
        println!("  Wait: yes");
    }
    if tty {
        println!("  TTY: allocated");
    }
    if verbose {
        println!("  Verbose: on");
    }
    println!("  Exit code: 0");
}

/// Run a pipeline script.
///
/// Executes the given pipeline script with an optional
/// timeout limit in minutes.
#[command(
    hidden = true,
    aliases = ["execute", "invoke"],
    param(script, help = "Script to run", placeholder = "SCRIPT"),
    param(timeout, help = "Execution timeout (minutes)", placeholder = "MINS"),
    param(log_file, help = "Log output path", name = "log-file",
          placeholder = "PATH", value_hint = "file",
          details = "Writes all output to the specified file \
                     in addition to stdout. The file is created \
                     if it does not exist."),
    param(retries, help = "Retry on failure count", short = 'r'),
    param(verbose, help = "Verbose output", short = 'v'),
    param(env_var, help = "Environment variables (repeatable)",
          name = "env", short = 'e', placeholder = "VAR=VAL"),
)]
fn run(
    script: Pos<String>,
    timeout: Pos<Option<u8>>,
    log_file: Named<Option<String>>,
    retries: Named<Option<u16>>,
    verbose: bool,
    env_var: Named<Vec<String>>,
) {
    println!("Running script: {script}");
    if let Some(t) = timeout {
        println!("  Timeout: {t} min");
    }
    if let Some(lf) = log_file {
        println!("  Log file: {lf}");
    }
    if let Some(r) = retries {
        println!("  Retries: {r}");
    }
    if verbose {
        println!("  Verbose: on");
    }
    for e in &env_var {
        println!("  Env: {e}");
    }
}

fn main() {
    app!()
        .name("pl")
        .version("1.0.0")
        .description("Pipeline DevOps toolkit")
        .details("A feature-rich CLI for building, testing, deploying, \
                   and managing pipeline workflows across environments.")
        .styles(AppStyles {
            header: AppStyle::Markup("[bold cyan underline]"),
            brand: AppStyle::Markup("[bold magenta]"),
            ..Default::default()
        })
        .run();
}