use std::io::IsTerminal;
use crate::config::TuiConfig;
use crate::engine_client::EngineClient;
pub fn run_version() {
let version = env!("CARGO_PKG_VERSION");
println!("complior {version}");
println!("AI Act Compliance Scanner & Fixer");
println!("https://complior.eu");
}
pub async fn run_doctor(config: &TuiConfig) {
println!("Complior Doctor — System Health Check");
println!("=====================================");
println!();
let mut passed = 0u32;
let total = 8u32;
let version = env!("CARGO_PKG_VERSION");
print!(" TUI binary: v{version}");
println!(" OK");
passed += 1;
let engine_url = config
.engine_url_override
.clone()
.unwrap_or_else(|| config.engine_url());
print!(" Engine: ");
let client = EngineClient::from_url(&engine_url);
match client.status().await {
Ok(status) if status.ready => {
let ver = status.version.unwrap_or_else(|| "unknown".into());
println!("v{ver} ({engine_url}) OK");
passed += 1;
}
Ok(_) => println!("NOT READY ({engine_url}) WARN"),
Err(_) => println!("UNREACHABLE ({engine_url}) FAIL"),
}
print!(" Node.js: ");
match std::process::Command::new("node").arg("--version").output() {
Ok(output) if output.status.success() => {
let ver = String::from_utf8_lossy(&output.stdout).trim().to_string();
let major: u32 = ver.trim_start_matches('v').split('.').next()
.and_then(|s| s.parse().ok()).unwrap_or(0);
if major >= 18 {
println!("{ver} (required: >=18) OK");
passed += 1;
} else {
println!("{ver} (required: >=18) FAIL");
}
}
_ => println!("Not found FAIL (install: https://nodejs.org)"),
}
print!(" Disk space: ");
let tmp = std::env::temp_dir();
if tmp.exists() {
println!("OK (temp dir accessible)");
passed += 1;
} else {
println!("WARN (temp dir inaccessible)");
}
print!(" Config: ");
let cwd = std::env::current_dir().unwrap_or_default();
if cwd.join(".complior").exists() {
println!(".complior/ found OK");
passed += 1;
} else {
println!(".complior/ not found WARN (run `complior init`)");
}
print!(" Network: ");
let net_client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.build();
match net_client {
Ok(c) => match c.head("https://github.com/a3ka/complior").send().await {
Ok(resp) if resp.status().is_success() || resp.status().is_redirection() => {
println!("GitHub reachable OK");
passed += 1;
}
_ => println!("GitHub unreachable WARN (offline mode OK)"),
},
Err(_) => println!("Cannot create HTTP client WARN"),
}
print!(" MCP: ");
let mcp_config = dirs::config_dir().map(|d| d.join("complior").join("mcp.json"));
match mcp_config {
Some(p) if p.exists() => {
println!("Configured OK");
passed += 1;
}
_ => println!("Not configured WARN (optional)"),
}
print!(" SaaS Auth: ");
if let Some(tokens) = crate::config::load_tokens() {
if crate::config::is_authenticated() {
let email = tokens.user_email.as_deref().unwrap_or("unknown");
let org = tokens.org_name.as_deref().unwrap_or("unknown");
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let mins_left = tokens.expires_at.saturating_sub(now) / 60;
println!("{email} ({org}) OK");
println!(" Token expires in {mins_left} minutes");
passed += 1;
} else {
println!("Token expired WARN (run `complior login`)");
}
} else {
println!("Not authenticated WARN (run `complior login`)");
}
println!();
println!(" Summary: {passed}/{total} checks passed");
if passed >= 3 {
println!(" Ready to scan!");
}
}
pub async fn run_report(format: &str, output: Option<&str>, path: Option<&str>, config: &TuiConfig) -> i32 {
let engine_url = config
.engine_url_override
.clone()
.unwrap_or_else(|| config.engine_url());
let client = EngineClient::from_url(&engine_url);
match client.status().await {
Ok(status) if status.ready => {}
_ => {
eprintln!("Error: Cannot connect to engine at {engine_url}");
return 1;
}
}
let scan_path = super::common::resolve_project_path(path);
match client.scan(&scan_path).await {
Ok(_) => {}
Err(e) => {
eprintln!("Scan failed: {e}");
return 1;
}
}
let endpoint = match format {
"pdf" => "/report/pdf",
_ => "/report/markdown",
};
match client.post_json(endpoint, &serde_json::json!({})).await {
Ok(resp) => {
let out_path = resp.get("path").and_then(|v| v.as_str()).unwrap_or("report");
if let Some(dest) = output {
println!("Report saved to: {dest}");
} else {
println!("Report generated: {out_path}");
}
0
}
Err(e) => {
eprintln!("Report generation failed: {e}");
1
}
}
}
pub async fn run_init(path: Option<&str>, yes: bool, force: bool, config: &TuiConfig) -> i32 {
use super::common::{ensure_engine_for, resolve_project_path_buf};
use super::format::colors::{bold, bold_red, bold_yellow, check_mark, cyan, dim, diamond, green, red};
use super::format::separator;
use super::interactive;
let base = resolve_project_path_buf(path);
let complior_dir = base.join(".complior");
let project_toml_path = complior_dir.join("project.toml");
if let Err(e) = std::fs::create_dir_all(&complior_dir) {
eprintln!("Failed to create .complior/: {e}");
return 1;
}
if !project_toml_path.exists() {
let toml_content = toml::to_string_pretty(&crate::config::default_project_toml())
.unwrap_or_default();
let _ = std::fs::write(&project_toml_path, toml_content);
}
let env_file_path = complior_dir.join(".env");
if !env_file_path.exists() {
let env_template = r#"# Complior LLM Configuration
# Uncomment ONE provider and set your API key.
# The key will be used for all LLM commands: eval --llm, fix --ai, scan --deep
# ── Provider API Keys ─────────────────────────────────
# Uncomment ONE provider and paste your key:
# OPENROUTER_API_KEY=sk-or-v1-your-key-here
# OPENAI_API_KEY=sk-your-key-here
# ANTHROPIC_API_KEY=sk-ant-your-key-here
# ── Provider Priority (optional) ─────────────────────
# Force a specific provider (default: first available key)
# COMPLIOR_LLM_PROVIDER=openrouter
# ── Model Overrides (optional) ───────────────────────
# Override the default model for each task type.
# Format depends on provider:
# OpenRouter: anthropic/claude-sonnet-4.5, google/gemini-2.0-flash, etc.
# OpenAI: gpt-4o, gpt-4o-mini, etc.
# Anthropic: claude-sonnet-4-5-20250929, claude-haiku-4-5-20251001, etc.
# scan --llm (L5 document quality analysis)
# COMPLIOR_MODEL_CLASSIFY=anthropic/claude-haiku-4.5
# fix --ai (document generation, enrichment)
# COMPLIOR_MODEL_DOCUMENT_GENERATION=anthropic/claude-sonnet-4.5
# eval --llm (LLM judge for compliance tests)
# COMPLIOR_MODEL_QA=anthropic/claude-haiku-4.5
# Other tasks (chat, code generation, reports)
# COMPLIOR_MODEL_CHAT=anthropic/claude-sonnet-4.5
# COMPLIOR_MODEL_CODE=anthropic/claude-sonnet-4.5
# COMPLIOR_MODEL_REPORT=anthropic/claude-sonnet-4.5
"#;
let _ = std::fs::write(&env_file_path, env_template);
}
let gitignore_path = complior_dir.join(".gitignore");
if !gitignore_path.exists() {
let _ = std::fs::write(&gitignore_path, ".env\n");
} else if let Ok(content) = std::fs::read_to_string(&gitignore_path)
&& !content.lines().any(|l| l.trim() == ".env") {
let _ = std::fs::write(&gitignore_path, format!("{content}\n.env\n"));
}
let client = if let Ok(c) = ensure_engine_for(config, &base).await { c } else {
eprintln!("Warning: Could not start engine.");
eprintln!("Run `complior init` again when engine is available.");
return 0;
};
let is_interactive = !yes && std::io::stdin().is_terminal();
let profile_path = complior_dir.join("profile.json");
let profile_exists = profile_path.exists();
let mut profile_role = String::from("deployer");
let mut profile_risk = String::from("limited");
let mut profile_obligations: usize = 15;
let mut profile_storage = String::from("eu");
let mut profile_created = false;
if profile_exists {
println!(" .complior/ already initialized at {}", complior_dir.display());
if let Ok(content) = std::fs::read_to_string(&profile_path)
&& let Ok(profile) = serde_json::from_str::<serde_json::Value>(&content) {
profile_role = profile.pointer("/organization/role")
.and_then(|v| v.as_str()).unwrap_or("deployer").to_string();
profile_risk = profile.pointer("/computed/riskLevel")
.and_then(|v| v.as_str()).unwrap_or("limited").to_string();
profile_obligations = profile.pointer("/computed/applicableObligations")
.and_then(|v| v.as_array()).map_or(15, std::vec::Vec::len);
profile_storage = profile.pointer("/data/storage")
.and_then(|v| v.as_str()).unwrap_or("eu").to_string();
}
profile_created = true;
} else {
println!("\n {}", bold(&format!("{} Complior Setup", diamond())));
println!(" {}", separator());
if let Ok(questions_json) = client.get_json("/onboarding/questions").await {
let answers = if is_interactive {
interactive::run_interactive_onboarding(&questions_json)
} else {
if yes {
println!("\n {} Using defaults (--yes)", dim("*"));
}
interactive::build_default_answers(&questions_json)
};
let body = serde_json::json!({ "answers": answers });
match client.post_json("/onboarding/complete", &body).await {
Ok(result) => {
profile_created = true;
if let Some(profile) = result.get("profile") {
profile_role = profile.pointer("/organization/role")
.and_then(|v| v.as_str()).unwrap_or("deployer").to_string();
profile_risk = profile.pointer("/computed/riskLevel")
.and_then(|v| v.as_str()).unwrap_or("limited").to_string();
profile_obligations = profile.pointer("/computed/applicableObligations")
.and_then(|v| v.as_array()).map_or(15, std::vec::Vec::len);
profile_storage = profile.pointer("/data/storage")
.and_then(|v| v.as_str()).unwrap_or("eu").to_string();
}
}
Err(e) => {
eprintln!(" Warning: Could not save profile: {e}");
let default = serde_json::json!({
"jurisdiction": "EU",
"regulation": "eu-ai-act",
"scanLevels": ["L1", "L2", "L3", "L4"]
});
let _ = std::fs::write(
&profile_path,
serde_json::to_string_pretty(&default).unwrap_or_default(),
);
}
}
} else {
let default = serde_json::json!({
"jurisdiction": "EU",
"regulation": "eu-ai-act",
"scanLevels": ["L1", "L2", "L3", "L4"]
});
let _ = std::fs::write(
&profile_path,
serde_json::to_string_pretty(&default).unwrap_or_default(),
);
profile_created = true;
}
}
let mut body = serde_json::json!({
"path": base.to_string_lossy(),
});
if force {
body["force"] = serde_json::json!(true);
}
let mut agent_list: Vec<(String, String, String, f64)> = Vec::new();
let mut skipped_count: usize = 0;
match client.post_json("/agent/init", &body).await {
Ok(result) => {
let manifests = result.get("manifests").and_then(|v| v.as_array());
let skipped = result.get("skipped").and_then(|v| v.as_array());
skipped_count = skipped.map_or(0, std::vec::Vec::len);
if let Some(agents) = manifests {
for agent in agents {
let name = agent.get("name").and_then(|v| v.as_str()).unwrap_or("unknown").to_string();
let framework = agent.get("framework").and_then(|v| v.as_str()).unwrap_or("unknown").to_string();
let autonomy = agent.get("autonomy_level").and_then(|v| v.as_str()).unwrap_or("?").to_string();
let confidence = agent.get("source")
.and_then(|s| s.get("confidence"))
.and_then(serde_json::Value::as_f64)
.unwrap_or(0.0);
agent_list.push((name, framework, autonomy, confidence));
}
}
}
Err(_) => {
eprintln!(" Warning: Agent discovery failed. Run `complior agent init` later.");
}
}
println!("\n {}", bold(&format!("{} Setup Complete", diamond())));
println!(" {}\n", separator());
if profile_created {
let risk_colored = match profile_risk.as_str() {
"minimal" => green(&profile_risk),
"limited" => bold_yellow(&profile_risk),
"high" => red(&profile_risk),
"unacceptable" => bold_red(&profile_risk),
_ => profile_risk.clone(),
};
let storage_display = match profile_storage.as_str() {
"eu" => "EU only",
"us" => "US only",
"mixed" => "Mixed / Multi-region",
_ => &profile_storage,
};
println!(" {} .complior/profile.json", dim("Profile"));
println!(" {} {}", dim("Role"), cyan(&profile_role));
println!(" {} {}", dim("Risk Level"), risk_colored);
println!(" {} {} applicable", dim("Obligations"), bold(&profile_obligations.to_string()));
println!(" {} {}", dim("Data"), storage_display);
}
let created_count = agent_list.len();
if created_count > 0 {
println!("\n {} {} discovered", dim("Agents"), bold(&created_count.to_string()));
println!(" {}", separator());
for (i, (name, framework, autonomy, confidence)) in agent_list.iter().enumerate() {
let conf_pct = (confidence * 100.0) as u32;
let conf_colored = if conf_pct >= 80 {
green(&format!("{conf_pct}%"))
} else if conf_pct >= 50 {
bold_yellow(&format!("{conf_pct}%"))
} else {
red(&format!("{conf_pct}%"))
};
println!(" {} {:<24} {:<12} {} confidence: {}",
dim(&format!("{}.", i + 1)), name, framework, autonomy, conf_colored);
}
println!(" {}", separator());
if agent_list.iter().any(|(_, _, _, c)| *c < 0.5) {
println!("\n {} Low confidence — fill owner, disclosure, and lifecycle fields:", bold_yellow("⚠"));
println!(" {}", dim("complior agent show <name> — view missing fields"));
println!(" {}", dim("Edit .complior/agents/<name>-manifest.json manually"));
}
println!("\n {} Passports saved to .complior/agents/", check_mark());
println!("\n {} Passports created with score 0/100", bold_yellow("!"));
println!(" Run {} to populate compliance data", bold("complior scan"));
} else if skipped_count > 0 {
println!("\n {} {} already have passports", dim("Agents"), bold(&skipped_count.to_string()));
println!("\n Next: {}", bold("complior scan"));
} else {
println!("\n {} {}", dim("Agents"), dim("none detected"));
println!("\n Next: {}", bold("complior scan"));
}
0
}
pub async fn run_update() {
println!("Checking for updates...");
let current = env!("CARGO_PKG_VERSION");
let client = reqwest::Client::new();
if let Ok(resp) = client
.get("https://api.github.com/repos/a3ka/complior/releases/latest")
.header("User-Agent", "complior-update-check")
.send()
.await
&& let Ok(body) = resp.json::<serde_json::Value>().await
&& let Some(tag) = body.get("tag_name").and_then(|v| v.as_str()) {
let latest = tag.trim_start_matches('v');
if latest == current {
println!("Already up to date: v{current}");
} else {
println!("New version available: v{latest} (current: v{current})");
println!("\nUpdate with:");
println!(" curl -fsSL https://complior.ai/install.sh | sh");
println!(" cargo install complior");
}
return;
}
println!("Could not check for updates. Current version: v{current}");
}