demons 0.1.0

Run a project's development commands side-by-side in one terminal
mod cli;
mod config;
mod init;
mod layout;
mod runtime;

use std::{
    env,
    io::{self, IsTerminal},
    path::PathBuf,
    process::ExitCode,
};

use anyhow::{Context, Result, bail};
use clap::Parser;

use crate::{
    cli::{Cli, Command},
    config::{CONFIG_FILE, LoadedConfig, explicit_or_discover},
};

fn main() -> ExitCode {
    match try_main() {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            eprintln!("error: {error:#}");
            ExitCode::FAILURE
        }
    }
}

fn try_main() -> Result<()> {
    let cli = Cli::parse();
    let cwd = env::current_dir().context("failed to determine current directory")?;

    if matches!(cli.command, Some(Command::Init)) {
        let path = init_path(cli.config, &cwd);
        let result = init::run(path)?;
        if result.start {
            runtime::run(LoadedConfig::load(result.path)?)?;
        }
        return Ok(());
    }

    let path = match explicit_or_discover(cli.config, &cwd)? {
        Some(path) => path,
        None if io::stdin().is_terminal() && io::stdout().is_terminal() => {
            let should_init = {
                let mut input = io::stdin().lock();
                let mut output = io::stdout().lock();
                let question = format!(
                    "No {CONFIG_FILE} found in {} or its parents. Run 'demons init' here?",
                    cwd.display()
                );
                init::prompt_yes_no(&mut input, &mut output, &question, true)?
            };
            if should_init {
                let result = init::run(cwd.join(CONFIG_FILE))?;
                if !result.start {
                    return Ok(());
                }
                result.path
            } else {
                return Ok(());
            }
        }
        None => {
            bail!("no {CONFIG_FILE} found in {} or its parents", cwd.display())
        }
    };

    let loaded = LoadedConfig::load(path)?;
    if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
        bail!("demons requires an interactive terminal");
    }
    runtime::run(loaded)
}

fn init_path(explicit: Option<PathBuf>, cwd: &std::path::Path) -> PathBuf {
    match explicit {
        Some(path) if path.is_absolute() => path,
        Some(path) => cwd.join(path),
        None => cwd.join(CONFIG_FILE),
    }
}