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 {
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();
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);
}
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()),
});
}
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()),
});
}
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()),
});
}
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()),
});
}
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()),
});
}
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()),
});
}
}
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()),
});
}
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()),
});
}
println!();
print_section("Required", &required);
println!();
print_section("Recommended", &recommended);
println!();
print_section("Daemon", &daemon_items);
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)
}