use crate::cli::commands::lifecycle;
use crate::cli::output::{OutputConfig, OutputFormat};
use crate::cli::InitArgs;
use crate::config;
use crate::error::{OlError, ERR_INVALID_CONFIG};
use crate::hooks;
use crate::hooks::DetectedAgent;
pub fn run_init(args: &InitArgs, output: &OutputConfig) -> Result<(), OlError> {
let version = env!("CARGO_PKG_VERSION");
if output.format == OutputFormat::Human && !output.quiet {
eprintln!("openlatch v{version}");
}
let ol_dir = config::openlatch_dir();
std::fs::create_dir_all(&ol_dir).map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!(
"Cannot create openlatch directory '{}': {e}",
ol_dir.display()
),
)
.with_suggestion("Check that you have write permission to your home directory.")
})?;
std::fs::create_dir_all(ol_dir.join("logs")).map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!("Cannot create logs directory: {e}"),
)
})?;
let agent = match hooks::detect_agent() {
Ok(a) => {
let label = agent_label(&a);
output.print_step(&format!("Detected agent: {label}"));
a
}
Err(e) => {
output.print_error(&e);
return Err(e);
}
};
let token_path = ol_dir.join("daemon.token");
let token_existed = token_path.exists();
let new_token = config::generate_token();
std::fs::write(&token_path, &new_token).map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!("Cannot write token file '{}': {e}", token_path.display()),
)
.with_suggestion("Check that you have write permission to the openlatch directory.")
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(&token_path, perms).map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!("Cannot set permissions on token file: {e}"),
)
})?;
}
let token_action = if token_existed {
"(regenerated existing)"
} else {
"(new)"
};
output.print_step(&format!("Generated auth token {token_action}"));
let settings_path = match &agent {
DetectedAgent::ClaudeCode { settings_path, .. } => settings_path.clone(),
};
let config_path = config::openlatch_dir().join("config.toml");
let needs_port_probe = !config_path.exists() || args.reconfig;
let port = if needs_port_probe {
if args.reconfig {
let _ = lifecycle::run_stop(output);
let _ = std::fs::remove_file(config::openlatch_dir().join("daemon.port"));
let _ = std::fs::remove_file(&config_path);
}
let probed = config::probe_free_port(config::PORT_RANGE_START, config::PORT_RANGE_END)?;
output.print_substep(&format!("Selected port {probed} (first available)"));
config::ensure_config(probed)?;
config::write_port_file(probed)?;
probed
} else {
config::Config::load(None, None, false)?.port
};
let cfg = config::Config::load(Some(port), None, false)?;
let hook_result = match hooks::install_hooks(&agent, cfg.port, &new_token) {
Ok(r) => r,
Err(e) => {
output.print_error(&e);
return Err(e);
}
};
output.print_step(&format!("Hooks written to {}", settings_path.display()));
for entry in &hook_result.entries {
let action_label = match entry.action {
hooks::HookAction::Added => "added",
hooks::HookAction::Replaced => "replaced",
};
output.print_substep(&format!("{} ({})", entry.event_type, action_label));
}
let (port, pid) = if args.no_start {
output.print_step("Skipped daemon start (--no-start)");
(cfg.port, 0u32)
} else if args.foreground {
output.print_step(&format!(
"Starting daemon on port {} (foreground)",
cfg.port
));
run_daemon_foreground(cfg.port, &new_token)?;
(cfg.port, std::process::id())
} else {
match lifecycle::spawn_daemon_background(cfg.port, &new_token) {
Ok(pid) => {
let _ = wait_for_health(cfg.port, 5);
output.print_step(&format!("Daemon started on port {} (PID {pid})", cfg.port));
(cfg.port, pid)
}
Err(e) => {
output.print_error(&e);
return Err(e);
}
}
};
let today = chrono::Local::now().format("%Y-%m-%d");
let log_path = config::openlatch_dir()
.join("logs")
.join(format!("events-{today}.jsonl"));
if output.format == OutputFormat::Human && !output.quiet {
eprintln!();
if args.no_start {
eprintln!("Setup complete. Run `openlatch start` to launch the daemon.");
} else {
eprintln!("Ready. Events will appear in: {}", log_path.display());
}
}
if output.format == OutputFormat::Json {
let hooks_list: Vec<&str> = hook_result
.entries
.iter()
.map(|e| e.event_type.as_str())
.collect();
let agent_str = match &agent {
DetectedAgent::ClaudeCode { .. } => "claude-code",
};
let json = serde_json::json!({
"status": "ok",
"agent": agent_str,
"port": port,
"pid": pid,
"hooks": hooks_list,
"log_path": log_path.to_string_lossy(),
"token_action": token_action,
"daemon_started": !args.no_start,
});
output.print_json(&json);
}
Ok(())
}
fn agent_label(agent: &DetectedAgent) -> String {
match agent {
DetectedAgent::ClaudeCode { claude_dir, .. } => {
format!("Claude Code ({})", claude_dir.display())
}
}
}
fn run_daemon_foreground(port: u16, token: &str) -> Result<(), OlError> {
let mut cfg = config::Config::load(Some(port), None, true)?;
cfg.foreground = true;
let rt = tokio::runtime::Runtime::new().map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!("Failed to create async runtime: {e}"),
)
})?;
let token_owned = token.to_string();
rt.block_on(async move {
use crate::envelope;
use crate::logging;
use crate::privacy;
let _guard = logging::daemon_log::init_daemon_logging(&cfg.log_dir);
if let Ok(deleted) = logging::cleanup_old_logs(&cfg.log_dir, cfg.retention_days) {
if deleted > 0 {
tracing::info!(deleted = deleted, "cleaned up old log files");
}
}
privacy::init_filter(&cfg.extra_patterns);
let pid = std::process::id();
let pid_path = config::openlatch_dir().join("daemon.pid");
if let Err(e) = std::fs::write(&pid_path, pid.to_string()) {
tracing::warn!(error = %e, "failed to write PID file");
}
logging::daemon_log::log_startup(
env!("CARGO_PKG_VERSION"),
cfg.port,
pid,
envelope::os_string(),
envelope::arch_string(),
);
match crate::daemon::start_server(cfg.clone(), token_owned).await {
Ok((uptime_secs, events)) => {
eprintln!(
"openlatch daemon stopped \u{2022} uptime {} \u{2022} {} events processed",
crate::daemon::format_uptime(uptime_secs),
events
);
}
Err(e) => {
tracing::error!(error = %e, "daemon exited with error");
eprintln!("Error: daemon exited unexpectedly: {e}");
}
}
let _ = std::fs::remove_file(&pid_path);
});
Ok(())
}
fn wait_for_health(port: u16, timeout_secs: u64) -> bool {
let url = format!("http://127.0.0.1:{port}/health");
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(timeout_secs);
while start.elapsed() < timeout {
if let Ok(resp) = reqwest::blocking::get(&url) {
if resp.status().is_success() {
return true;
}
}
std::thread::sleep(std::time::Duration::from_millis(200));
}
false
}