now-runner 0.1.0

Nix-based distributed command runner
// now: A Nix-based distributed command runner
// Copyright (C) 2026 Eric Rodrigues Pires
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
// more details.
//
// You should have received a copy of the GNU Affero General Public License along
// with this program. If not, see <https://www.gnu.org/licenses/>.

use std::path::PathBuf;

use clap::{CommandFactory, Parser, ValueEnum};

use crate::{environment::NowEnvironment, workflow::NowWorkflowParams};

mod builder;
mod environment;
mod job;
mod project;
mod secret;
mod utils;
mod workflow;

#[doc(hidden)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub(crate) enum CheckoutStrategy {
    /// Don't checkout; create a fresh directory for every job.
    None,
    /// On the local builder, run commands at the local directory.
    /// On remote builders, copy non-ignored files from the local directory via rsync.
    Default,
}

static LONG_ABOUT: &str = "now - Nix-based distributed command runner.

\x1b[1;4mExamples:\x1b[0m
  \x1b[2m# Initialize a basic workflow in ./now.nix\x1b[0m
  now init

  \x1b[2m# Load envvars from a dotenv file\x1b[0m
  now run --env-file .env .now/workflow-with-secrets.nix

  \x1b[2m# Run the \"deploy\" job (and all dependencies), and specify a remote builder for the run\x1b[0m
  now run \\
    --job deploy \\
    --builders \"ssh://mac aarch64-darwin\" \\
    .now/remote.nix";

#[derive(Parser)]
#[command(version, about, long_about = LONG_ABOUT)]
enum Command {
    /// Initialize a basic workflow.
    Init {
        /// Path to the workflow.
        workflow: Option<PathBuf>,
    },

    /// Run a workflow.
    Run {
        /// Path to the workflow.
        workflow: PathBuf,

        /// Jobs to target in this run.
        /// If unspecified, the default jobs of the workflow are run.
        /// If there are no default jobs in the workflow, all jobs are run.
        ///
        /// Cannot be used together with the `--all-jobs` option.
        #[arg(short, long = "job", value_name = "JOB")]
        jobs: Option<Vec<String>>,

        /// If set, all jobs are run.
        ///
        /// Cannot be used together with the `--job` option.
        #[arg(long)]
        all_jobs: bool,

        /// Optional dotenv file to read environment variables from.
        #[arg(short, long, value_name = "FILE")]
        env_file: Option<PathBuf>,

        /// Evaluate but don't run the workflow.
        #[arg(long)]
        eval: bool,

        /// Strategy for checking out the current working directory.
        #[arg(
            long = "checkout",
            value_enum,
            default_value_t = CheckoutStrategy::Default,
            value_name = "STRATEGY",
        )]
        checkout_strategy: CheckoutStrategy,

        /// A semicolon-separated list of build machines.
        /// When specified, overrides the remote builders configuration of the host.
        ///
        /// For more information on the syntax, see:
        /// <https://nix.dev/manual/nix/latest/command-ref/conf-file#conf-builders>
        #[arg(short, long)]
        builders: Option<String>,

        /// Nix expression that evaluates to nixpkgs.
        #[arg(
            short,
            long = "nixpkgs",
            default_value = "<nixpkgs>",
            value_name = "EXPRESSION"
        )]
        nixpkgs_expr: String,
    },

    /// Generate shell completions.
    Completions {
        /// Which shell to generate completions for.
        shell: clap_complete::Shell,
    },
}

fn main() -> color_eyre::Result<()> {
    match Command::parse() {
        Command::Init { workflow } => {
            let mut path = workflow.unwrap_or(PathBuf::from("."));
            if path.is_dir()
                || (!path.exists() && path.extension().is_none_or(|extension| extension != "nix"))
            {
                path.push("now.nix");
            }
            if path.exists() {
                return Err(color_eyre::eyre::eyre!(
                    "'{}' already exists",
                    path.to_string_lossy(),
                ));
            }
            if let Some(parent) = path.parent() {
                std::fs::create_dir_all(parent)?;
            }
            std::fs::write(&path, include_bytes!("init.nix"))?;
            eprintln!(
                "'{}' has been initialized with a basic workflow",
                path.to_string_lossy(),
            )
        }
        Command::Run {
            mut workflow,
            nixpkgs_expr,
            jobs,
            all_jobs,
            env_file,
            eval,
            checkout_strategy,
            builders,
        } => {
            if all_jobs && jobs.is_some() {
                return Err(color_eyre::eyre::eyre!(
                    "Conflicting --job and --all-jobs options"
                ));
            }
            if workflow.is_dir() {
                let now = workflow.join("now.nix");
                if now.exists() && !now.is_dir() {
                    workflow = now;
                } else {
                    return Err(color_eyre::eyre::eyre!(
                        "Workflow 'now.nix' not found in directory '{}'",
                        workflow.to_string_lossy()
                    ));
                }
            } else if !workflow.exists() {
                return Err(color_eyre::eyre::eyre!(
                    "Workflow '{}' not found",
                    workflow.to_string_lossy()
                ));
            }
            let mut environment = NowEnvironment::get(&workflow, env_file.as_ref(), &nixpkgs_expr)?;
            environment.run_workflow(NowWorkflowParams {
                workflow,
                nixpkgs_expr,
                eval,
                jobs,
                all_jobs,
                checkout_strategy,
                builders,
            })?;
        }
        Command::Completions { shell } => {
            clap_complete::generate(
                shell,
                &mut Command::command(),
                env!("CARGO_BIN_NAME"),
                &mut std::io::stdout(),
            );
        }
    }
    Ok(())
}