1#![deny(unsafe_code)]
8#![deny(missing_docs)]
9#![deny(clippy::all)]
10#![deny(unreachable_pub)]
11#![deny(clippy::unwrap_used)]
12
13use anyhow::{Context, Result};
14use clap::Parser;
15
16#[derive(Parser)]
18#[command(name = "astrid-daemon")]
19#[command(author, version, about)]
20pub struct Args {
21 #[arg(short, long, default_value = "00000000-0000-0000-0000-000000000000")]
23 pub session: String,
24
25 #[arg(short, long)]
27 pub workspace: Option<std::path::PathBuf>,
28
29 #[arg(long)]
31 pub ephemeral: bool,
32
33 #[arg(short, long)]
35 pub verbose: bool,
36}
37
38fn init_logging(verbose: bool) {
39 let workspace_root = std::env::current_dir().ok();
40 let unified_cfg = astrid_config::Config::load(workspace_root.as_deref())
41 .ok()
42 .map(|r| r.config);
43
44 let log_config = if let Some(cfg) = &unified_cfg {
45 let mut lc = astrid_telemetry::log_config_from(cfg);
46 if verbose {
47 "debug".clone_into(&mut lc.level);
48 }
49 if let Ok(home) = astrid_core::dirs::AstridHome::resolve() {
50 lc.target = astrid_telemetry::LogTarget::File(home.log_dir());
51 }
52 lc
53 } else {
54 let level = if verbose { "debug" } else { "info" };
55 let mut lc = astrid_telemetry::LogConfig::new(level)
56 .with_format(astrid_telemetry::LogFormat::Compact);
57 if let Ok(home) = astrid_core::dirs::AstridHome::resolve() {
58 lc.target = astrid_telemetry::LogTarget::File(home.log_dir());
59 }
60 lc
61 };
62
63 if let Err(e) = astrid_telemetry::setup_logging(&log_config) {
64 eprintln!("Failed to initialize logging: {e}");
65 }
66}
67
68pub async fn run() -> Result<()> {
78 let args = Args::parse();
79
80 init_logging(args.verbose);
81
82 let session_id = astrid_core::SessionId::from_uuid(
83 uuid::Uuid::parse_str(&args.session)
84 .map_err(|e| anyhow::anyhow!("Invalid UUID format: {e}"))?,
85 );
86
87 let ws = args.workspace.unwrap_or_else(|| {
88 std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))
89 });
90
91 let kernel = astrid_kernel::Kernel::new(session_id.clone(), ws)
92 .await
93 .map_err(|e| anyhow::anyhow!("Failed to boot Kernel: {e}"))?;
94
95 if args.ephemeral {
97 kernel.set_ephemeral(true);
98 }
99
100 kernel.load_all_capsules().await;
102
103 {
106 let reg = kernel.capsules.read().await;
107 let has_cli_proxy = reg
108 .list()
109 .iter()
110 .any(|id| id.as_str() == "astrid-capsule-cli");
111 if !has_cli_proxy {
112 tracing::error!(
113 "CLI proxy capsule (astrid-capsule-cli) not found - \
114 daemon cannot accept CLI connections"
115 );
116 anyhow::bail!(
117 "CLI proxy capsule (astrid-capsule-cli) not found. \
118 Install it with: astrid capsule install @unicity-astrid/capsule-cli"
119 );
120 }
121 }
122
123 astrid_kernel::socket::write_readiness_file().map_err(|e| {
127 anyhow::anyhow!(
128 "Failed to write readiness file \
129 (daemon is useless without it): {e}"
130 )
131 })?;
132
133 tracing::info!(
134 session = %session_id.0,
135 ephemeral = args.ephemeral,
136 "Kernel booted successfully"
137 );
138
139 let mut shutdown_rx = kernel.shutdown_tx.subscribe();
141
142 #[cfg(unix)]
143 {
144 let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
145 .context("failed to register SIGTERM handler")?;
146 tokio::select! {
147 _ = tokio::signal::ctrl_c() => {
148 tracing::info!("Received SIGINT, shutting down");
149 }
150 _ = sigterm.recv() => {
151 tracing::info!("Received SIGTERM, shutting down");
152 }
153 _ = shutdown_rx.wait_for(|v| *v) => {
154 tracing::info!("Received API shutdown request, shutting down");
155 }
156 }
157 }
158 #[cfg(not(unix))]
159 {
160 tokio::select! {
161 _ = tokio::signal::ctrl_c() => {
162 tracing::info!("Received SIGINT, shutting down");
163 }
164 _ = shutdown_rx.wait_for(|v| *v) => {
165 tracing::info!("Received API shutdown request, shutting down");
166 }
167 }
168 }
169
170 kernel.shutdown(Some("signal".to_string())).await;
171
172 Ok(())
173}