darq 0.1.0

darq CLI + TUI — autonomous issue → PR pipeline with SAT and a learning loop.
Documentation
use clap::Parser;

pub const EXIT_NOT_INITIALIZED: i32 = 2;
pub const EXIT_CHECKS_FAILED: i32 = 1;
pub const EXIT_SUCCESS: i32 = 0;

#[derive(Parser, Debug)]
pub struct CheckArgs {
    /// Project directory to validate (defaults to the current directory)
    path: Option<std::path::PathBuf>,
}

struct CheckItem {
    label: String,
    status: CheckStatus,
    detail: Option<String>,
    fix: Option<String>,
}

#[derive(PartialEq)]
enum CheckStatus {
    Pass,
    Warn,
    Fail,
    Info,
}

impl CheckItem {
    fn icon(&self) -> &str {
        match self.status {
            CheckStatus::Pass => "PASS",
            CheckStatus::Warn => "WARN",
            CheckStatus::Fail => "FAIL",
            CheckStatus::Info => "INFO",
        }
    }
}

pub async fn handle(args: CheckArgs) -> anyhow::Result<i32> {
    let path = args
        .path
        .unwrap_or_else(|| std::env::current_dir().unwrap());
    let mut required: Vec<CheckItem> = Vec::new();
    let mut recommended: Vec<CheckItem> = Vec::new();
    let mut daemon_items: Vec<CheckItem> = Vec::new();

    // ── Required checks ──

    let config_path = path.join(".darq").join("config.yaml");
    let legacy_path = path.join("darq.yaml");
    if config_path.exists() {
        match darq_core::config::load(&config_path) {
            Ok(_config) => {
                required.push(CheckItem {
                    label: ".darq/config.yaml".into(),
                    status: CheckStatus::Pass,
                    detail: None,
                    fix: None,
                });
            }
            Err(e) => {
                required.push(CheckItem {
                    label: ".darq/config.yaml".into(),
                    status: CheckStatus::Fail,
                    detail: Some(format!("parse error: {e}")),
                    fix: Some("Fix syntax errors in .darq/config.yaml".into()),
                });
            }
        }
    } else if legacy_path.exists() {
        required.push(CheckItem {
            label: "config (legacy darq.yaml)".into(),
            status: CheckStatus::Warn,
            detail: Some("darq.yaml at root is deprecated".into()),
            fix: Some("Run `darq init --migrate` to migrate to .darq/config.yaml".into()),
        });
    } else {
        required.push(CheckItem {
            label: ".darq/config.yaml".into(),
            status: CheckStatus::Fail,
            detail: None,
            fix: Some("Run `darq init` to initialize the project".into()),
        });
        print_section("Required", &required);
        return Ok(EXIT_NOT_INITIALIZED);
    }

    // Git repo
    if path.join(".git").exists() {
        required.push(CheckItem {
            label: "git repo".into(),
            status: CheckStatus::Pass,
            detail: None,
            fix: None,
        });
    } else {
        required.push(CheckItem {
            label: "git repo".into(),
            status: CheckStatus::Fail,
            detail: None,
            fix: Some("Run `git init` to initialize a git repository".into()),
        });
    }

    // GitHub auth
    if check_gh_auth() {
        required.push(CheckItem {
            label: "GitHub auth".into(),
            status: CheckStatus::Pass,
            detail: None,
            fix: None,
        });
    } else {
        required.push(CheckItem {
            label: "GitHub auth".into(),
            status: CheckStatus::Fail,
            detail: None,
            fix: Some("Run `gh auth login` to authenticate with GitHub".into()),
        });
    }

    // Agent binary
    if check_binary_exists("opencode") {
        required.push(CheckItem {
            label: "opencode agent".into(),
            status: CheckStatus::Pass,
            detail: None,
            fix: None,
        });
    } else {
        required.push(CheckItem {
            label: "opencode agent".into(),
            status: CheckStatus::Fail,
            detail: None,
            fix: Some("Install opencode: https://opencode.ai".into()),
        });
    }

    // Git CLI
    if check_binary_exists("git") {
        required.push(CheckItem {
            label: "git CLI".into(),
            status: CheckStatus::Pass,
            detail: None,
            fix: None,
        });
    } else {
        required.push(CheckItem {
            label: "git CLI".into(),
            status: CheckStatus::Fail,
            detail: None,
            fix: Some("Install git".into()),
        });
    }

    // ── Recommended checks ──

    let sat_path = path.join(".darq").join("sat.yaml");
    if sat_path.exists() {
        recommended.push(CheckItem {
            label: "SAT config (.darq/sat.yaml)".into(),
            status: CheckStatus::Pass,
            detail: None,
            fix: None,
        });
    } else {
        recommended.push(CheckItem {
            label: "SAT config (.darq/sat.yaml)".into(),
            status: CheckStatus::Warn,
            detail: None,
            fix: Some("Create .darq/sat.yaml with persona definitions for SAT scoring".into()),
        });
    }

    // Remote origin
    let remote_output = std::process::Command::new("git")
        .args(["remote", "get-url", "origin"])
        .current_dir(&path)
        .output();
    match remote_output {
        Ok(output) if output.status.success() => {
            let remote = String::from_utf8_lossy(&output.stdout).trim().to_string();
            recommended.push(CheckItem {
                label: "remote origin".into(),
                status: CheckStatus::Pass,
                detail: Some(remote),
                fix: None,
            });
        }
        _ => {
            recommended.push(CheckItem {
                label: "remote origin".into(),
                status: CheckStatus::Warn,
                detail: None,
                fix: Some("Run `git remote add origin <url>`".into()),
            });
        }
    }

    // Learnings
    let ruvector_path = path.join(".darq").join("ruvector");
    if ruvector_path.exists() {
        recommended.push(CheckItem {
            label: "learning store".into(),
            status: CheckStatus::Pass,
            detail: None,
            fix: None,
        });
    } else {
        recommended.push(CheckItem {
            label: "learning store".into(),
            status: CheckStatus::Warn,
            detail: Some("not initialized".into()),
            fix: Some("Will initialize on first successful run with learning enabled".into()),
        });
    }

    // ── Daemon checks ──

    if crate::daemon::lifecycle::is_daemon_running() {
        let pid = std::fs::read_to_string(crate::daemon::lifecycle::pid_path())
            .unwrap_or_default()
            .trim()
            .to_string();
        daemon_items.push(CheckItem {
            label: "daemon".into(),
            status: CheckStatus::Pass,
            detail: Some(format!("running (PID {})", pid)),
            fix: None,
        });
    } else {
        daemon_items.push(CheckItem {
            label: "daemon".into(),
            status: CheckStatus::Info,
            detail: Some("not running".into()),
            fix: Some("Run `darq daemon start` to start the daemon".into()),
        });
    }

    // ── Print results ──
    println!();
    print_section("Required", &required);
    println!();
    print_section("Recommended", &recommended);
    println!();
    print_section("Daemon", &daemon_items);

    // ── Next steps ──
    let failures: Vec<&CheckItem> = required
        .iter()
        .chain(recommended.iter())
        .chain(daemon_items.iter())
        .filter(|c| c.status == CheckStatus::Fail || c.status == CheckStatus::Warn)
        .collect();

    if failures.is_empty() {
        println!();
        println!("  All checks passed. Ready to run: darq run issue <N> --full");
    } else {
        println!();
        println!("  Next steps:");
        for (i, item) in failures.iter().enumerate() {
            if let Some(ref fix) = item.fix {
                println!("  {}. {}", i + 1, fix);
            }
        }
    }

    let has_failures = required.iter().any(|c| c.status == CheckStatus::Fail);
    Ok(if has_failures {
        EXIT_CHECKS_FAILED
    } else {
        EXIT_SUCCESS
    })
}

fn print_section(title: &str, items: &[CheckItem]) {
    println!("  {title}");
    println!("  {}", "".repeat(title.len()));
    for item in items {
        let detail = item
            .detail
            .as_ref()
            .map(|d| format!(" ({d})"))
            .unwrap_or_default();
        println!("  [{}] {}{}", item.icon(), item.label, detail);
        if let Some(ref fix) = item.fix
            && (item.status == CheckStatus::Fail || item.status == CheckStatus::Warn)
        {
            println!("       Fix: {fix}");
        }
    }
}

fn check_binary_exists(binary: &str) -> bool {
    std::process::Command::new("which")
        .arg(binary)
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false)
}

fn check_gh_auth() -> bool {
    std::process::Command::new("gh")
        .args(["auth", "status"])
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false)
}