use std::fmt::Write;
use crate::config::TuiConfig;
use crate::daemon;
use crate::engine_client::EngineClient;
use crate::engine_process::EngineManager;
pub fn url_encode(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
result.push(b as char);
}
_ => {
let _ = write!(result, "%{b:02X}");
}
}
}
result
}
pub fn resolve_client(config: &TuiConfig) -> EngineClient {
let mut dir = std::env::current_dir().unwrap_or_default();
loop {
if let Some(info) = daemon::find_running_daemon(&dir) {
return EngineClient::from_url(&format!("http://127.0.0.1:{}", info.port));
}
if !dir.pop() {
break;
}
}
EngineClient::new(config)
}
pub async fn ensure_engine(config: &TuiConfig) -> Result<EngineClient, i32> {
ensure_engine_for(config, &std::env::current_dir().unwrap_or_default()).await
}
pub async fn ensure_engine_for(config: &TuiConfig, project_path: &std::path::Path) -> Result<EngineClient, i32> {
let project_path = project_path.to_path_buf();
let daemon_exists = daemon::find_running_daemon(&project_path).is_some();
let client = resolve_client(config);
let max_retries: u32 = if daemon_exists { 25 } else { 5 };
let initial_delay_ms = 200u64;
for attempt in 0..max_retries {
match client.status().await {
Ok(status) if status.ready => return Ok(client),
_ => {
if attempt < max_retries - 1 {
let delay = initial_delay_ms * 2u64.pow(attempt.min(4));
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
}
}
}
}
if daemon_exists {
let total_wait: u64 = (0..max_retries)
.map(|a| initial_delay_ms * 2u64.pow(a.min(4)))
.sum();
eprintln!(
"Error: Daemon process found but engine not responding after ~{}s.",
total_wait / 1000
);
eprintln!("Try: complior daemon stop && complior daemon start");
return Err(1);
}
let engine_root = super::agent::find_engine_root(&project_path);
if let Some(root) = engine_root {
eprintln!("Engine not responding. Starting engine...");
let pid_path = daemon::pid_file_path(&project_path);
let mut mgr = if root.join("src").join("server.ts").exists() {
EngineManager::from_engine_dir(&root)
} else {
EngineManager::new(&root)
}
.with_project_path(&project_path);
match mgr.start_with_pid(&pid_path, false) {
Ok(port) => {
let new_client = EngineClient::from_url(&format!("http://127.0.0.1:{port}"));
if mgr.wait_for_ready(&new_client).await {
std::mem::forget(mgr);
return Ok(new_client);
}
eprintln!("Error: Engine started but failed health check.");
}
Err(e) => {
eprintln!("Error: Could not auto-start engine: {e}");
}
}
}
eprintln!("Error: Engine not running. Start with: complior daemon");
Err(1)
}
const LLM_KEY_VARS: &[&str] = &[
"OPENROUTER_API_KEY",
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
];
pub fn check_llm_key(project_path: &str) -> bool {
for var in LLM_KEY_VARS {
if let Ok(val) = std::env::var(var) {
if !val.is_empty() {
return true;
}
}
}
let project_env = std::path::Path::new(project_path).join(".complior/.env");
if check_env_file(&project_env) {
return true;
}
if let Some(home) = dirs::home_dir() {
let global_env = home.join(".config/complior/.env");
if check_env_file(&global_env) {
return true;
}
}
false
}
fn check_env_file(path: &std::path::Path) -> bool {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return false,
};
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
for var in LLM_KEY_VARS {
if let Some(rest) = line.strip_prefix(var) {
if let Some(val) = rest.strip_prefix('=') {
let val = val.trim().trim_matches('"').trim_matches('\'');
if !val.is_empty() {
return true;
}
}
}
}
}
false
}
pub fn print_llm_key_error() {
use super::format::colors::{bold, bold_yellow, cyan, dim, yellow};
use super::format::separator;
eprintln!();
eprintln!(" {}", separator());
eprintln!(" {} LLM API key not configured", bold_yellow("!"));
eprintln!(" {}", separator());
eprintln!();
eprintln!(" This flag uses an LLM model to perform deeper analysis —");
eprintln!(" document quality scoring, semantic gap detection, and");
eprintln!(" AI-enriched compliance content generation.");
eprintln!();
eprintln!(" {} Add your key to {}:", bold("Setup"), cyan(".complior/.env"));
eprintln!();
eprintln!(" {}", dim("# pick one provider, uncomment and paste your key:"));
eprintln!(" {}", yellow("OPENROUTER_API_KEY=sk-or-v1-..."));
eprintln!(" {}", dim("# OPENAI_API_KEY=sk-..."));
eprintln!(" {}", dim("# ANTHROPIC_API_KEY=sk-ant-..."));
eprintln!();
eprintln!(" {} Get a free key:", bold("Keys"));
eprintln!(" OpenRouter {} {}", dim("(recommended)"), cyan("https://openrouter.ai"));
eprintln!(" OpenAI {} {}", dim("(paid)"), cyan("https://platform.openai.com"));
eprintln!(" Anthropic {} {}", dim("(paid)"), cyan("https://console.anthropic.com"));
eprintln!();
eprintln!(" {}", separator());
eprintln!(" {} You can continue without LLM — the base scan (L1-L4)", dim("Tip:"));
eprintln!(" {} and deterministic fixes work fully offline.", dim(" "));
eprintln!(" {} Just run: {}", dim(" "), bold("complior scan"));
eprintln!(" {}", separator());
eprintln!();
}
pub fn resolve_project_path(path: Option<&str>) -> String {
resolve_project_path_buf(path).to_string_lossy().to_string()
}
pub fn resolve_project_path_buf(path: Option<&str>) -> std::path::PathBuf {
let cwd = std::env::current_dir().unwrap_or_default();
match path {
Some(p) => {
let pb = std::path::PathBuf::from(p);
if pb.is_absolute() { pb } else { cwd.join(pb) }
}
None => cwd,
}
}