expri 0.1.1

Repo-local remote experiment workflow tools
mod archive;
mod config;
mod context;
mod controller;
mod error;
mod filter;
mod git;
mod node;
mod protocol;
mod shell;

use std::path::PathBuf;

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

use crate::context::CommandContext;
use crate::controller::download::{DownloadOptions, download_target};
use crate::controller::setup::{SetupOptions, setup_target};
use crate::controller::sync::{SyncOptions, sync_target};
use crate::controller::task::{
  LocalTaskOptions, RemoteTaskOptions, run_local_task, run_remote_task,
};
use crate::error::{ExpriError, Result};
use crate::node::cli::NodeCommand;

#[derive(Debug, Parser)]
#[command(version, about = "Repo-local remote workflow tools")]
struct Cli {
  #[arg(short = 'T', long)]
  target: Option<String>,

  #[arg(short, long, global = true, action = clap::ArgAction::Count)]
  verbose: u8,

  #[arg(short, long, global = true)]
  quiet: bool,

  #[command(subcommand)]
  command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
  Sync(SyncCommand),
  Download(DownloadCommand),
  Setup(SetupCommand),
  Task(TaskCommand),
  Node {
    #[command(subcommand)]
    command: NodeCommand,
  },
}

#[derive(Debug, Args)]
struct SyncCommand {
  #[arg(long)]
  config: Option<PathBuf>,

  #[arg(long)]
  repo: Option<PathBuf>,

  #[arg(long)]
  control_path: Option<String>,

  #[arg(long, default_value = "30m")]
  control_persist: String,

  #[arg(long)]
  dry_run: bool,

  #[arg(long)]
  force: bool,

  #[arg(long)]
  pull: bool,

  #[arg(value_name = "PATH", last = true)]
  paths: Vec<PathBuf>,
}

#[derive(Debug, Args)]
struct SetupCommand {
  #[arg(long)]
  config: Option<PathBuf>,

  #[arg(long)]
  repo: Option<PathBuf>,

  #[arg(long)]
  control_path: Option<String>,

  #[arg(long, default_value = "30m")]
  control_persist: String,

  #[arg(long)]
  dry_run: bool,

  #[arg(long)]
  force: bool,
}

#[derive(Debug, Args)]
struct DownloadCommand {
  #[arg(long)]
  config: Option<PathBuf>,

  #[arg(long)]
  repo: Option<PathBuf>,

  #[arg(long)]
  control_path: Option<String>,

  #[arg(long, default_value = "30m")]
  control_persist: String,

  #[arg(long)]
  dry_run: bool,

  #[arg(value_name = "NAME", last = true)]
  names: Vec<String>,
}

#[derive(Debug, Args)]
struct TaskCommand {
  name: String,

  #[arg(long)]
  config: Option<PathBuf>,

  #[arg(long)]
  repo: Option<PathBuf>,

  #[arg(long)]
  control_path: Option<String>,

  #[arg(long, default_value = "30m")]
  control_persist: String,

  #[arg(long)]
  dry_run: bool,

  #[arg(value_name = "ARG", last = true)]
  args: Vec<String>,
}

fn main() {
  if let Err(error) = run() {
    eprintln!("error: {error}");
    std::process::exit(error.exit_code());
  }
}

fn run() -> Result<()> {
  let cli = Cli::parse();
  match cli.command {
    Command::Sync(command) => run_sync(command, cli.target.as_deref(), cli.verbose, cli.quiet),
    Command::Download(command) => {
      run_download(command, cli.target.as_deref(), cli.verbose, cli.quiet)
    }
    Command::Setup(command) => run_setup(command, cli.target.as_deref(), cli.verbose, cli.quiet),
    Command::Task(command) => run_task(command, cli.target.as_deref(), cli.verbose, cli.quiet),
    Command::Node { command } => {
      if cli.target.is_some() {
        return Err(ExpriError::Message(
          "--target is only valid for controller commands".to_string(),
        ));
      }
      node::cli::run(command)
    }
  }
}

fn run_sync(command: SyncCommand, target: Option<&str>, verbosity: u8, quiet: bool) -> Result<()> {
  let context = CommandContext::load(command.config, command.repo)?
    .into_target(target, command.control_path)?;
  let sync = context.config.sync_rules()?;

  sync_target(SyncOptions {
    repo_root: context.repo_root,
    project_name: context.project_name,
    target_name: context.target_name,
    target: context.target,
    sync,
    control_path: context.control_path,
    control_persist: command.control_persist,
    dry_run: command.dry_run,
    force: command.force,
    pull: command.pull,
    paths: command.paths,
    verbosity,
    quiet,
  })
}

fn run_download(
  command: DownloadCommand,
  target: Option<&str>,
  verbosity: u8,
  quiet: bool,
) -> Result<()> {
  let context = CommandContext::load(command.config, command.repo)?
    .into_target(target, command.control_path)?;
  let results_dir = context.config.download_results_dir();
  let mappings = context.config.download_mappings();

  download_target(DownloadOptions {
    repo_root: context.repo_root,
    project_name: context.project_name,
    target_name: context.target_name,
    target: context.target,
    results_dir,
    mappings,
    names: command.names,
    control_path: context.control_path,
    control_persist: command.control_persist,
    dry_run: command.dry_run,
    verbosity,
    quiet,
  })
}

fn run_setup(
  command: SetupCommand,
  target: Option<&str>,
  verbosity: u8,
  quiet: bool,
) -> Result<()> {
  let context = CommandContext::load(command.config, command.repo)?
    .into_target(target, command.control_path)?;
  let steps = context.config.setup_steps();

  setup_target(SetupOptions {
    repo_root: context.repo_root,
    project_name: context.project_name,
    target_name: context.target_name,
    target: context.target,
    steps,
    control_path: context.control_path,
    control_persist: command.control_persist,
    dry_run: command.dry_run,
    force: command.force,
    verbosity,
    quiet,
  })
}

fn run_task(command: TaskCommand, target: Option<&str>, verbosity: u8, quiet: bool) -> Result<()> {
  let context = CommandContext::load(command.config, command.repo)?;
  let task = context.config.task(&command.name)?;
  if target.is_some() {
    let context = context.into_target(target, command.control_path)?;
    return run_remote_task(RemoteTaskOptions {
      repo_root: context.repo_root,
      project_name: context.project_name,
      target_name: context.target_name,
      target: context.target,
      control_path: context.control_path,
      control_persist: command.control_persist,
      name: command.name,
      task,
      args: command.args,
      dry_run: command.dry_run,
      verbosity,
      quiet,
    });
  }

  run_local_task(LocalTaskOptions {
    repo_root: context.repo_root,
    project_name: context.project_name,
    name: command.name,
    task,
    args: command.args,
    dry_run: command.dry_run,
    verbosity,
    quiet,
  })
}