use std::collections::HashMap;
use std::path::PathBuf;
use codenexus::kit::{build_kit, KitBootstrapConfig};
use codenexus::service::error::CodeNexusError;
use codenexus::service::init_kit;
const DEFAULT_DB_PATH: &str = "./codenexus.lbug";
#[cfg(feature = "daemon")]
use codenexus::daemon::DEFAULT_DEBOUNCE_MS;
#[cfg(not(feature = "daemon"))]
use codenexus::kit::bootstrap::DEFAULT_DEBOUNCE_MS;
pub fn init_logging() {
init_inklog();
}
fn init_inklog() {
let level = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
let rt = tokio::runtime::Runtime::new().expect("create tokio runtime for inklog");
let logger = rt
.block_on(async {
inklog::LoggerManager::builder()
.level(&level)
.format("{timestamp} [{level}] {target} - {message}")
.console(true)
.console_colored(true)
.file("logs/codenexus.log")
.file_max_size("100MB")
.file_compress(true)
.file_rotation_time("daily")
.file_keep_files(30)
.channel_capacity(10000)
.build()
.await
})
.expect("init inklog");
std::mem::forget(logger);
std::mem::forget(rt);
}
fn main() {
init_logging();
#[cfg(feature = "mcp")]
if std::env::args().nth(1) == Some("mcp".into()) {
run_mcp_server();
return;
}
run_cli();
}
fn run_cli() {
let cmd = sdforge::cli::CliBuilder::new()
.with_name("codenexus")
.with_global_arg(
sdforge::cli::GlobalArg::new("db")
.long("db")
.default_value(DEFAULT_DB_PATH)
.help("Database path"),
)
.with_global_arg(
sdforge::cli::GlobalArg::new("debounce-ms")
.long("debounce-ms")
.default_value(DEFAULT_DEBOUNCE_MS.to_string())
.help("Daemon debounce interval (ms)"),
)
.build();
let matches = cmd.get_matches();
let sub_name = match matches.subcommand_name() {
Some(name) => name,
None => {
println!("Use --help to see available commands");
return;
}
};
let sub_matches = matches.subcommand_matches(sub_name).unwrap();
let db = extract_global_arg(&matches, sub_matches, "db", DEFAULT_DB_PATH);
let debounce_ms = extract_global_arg(
&matches,
sub_matches,
"debounce-ms",
&DEFAULT_DEBOUNCE_MS.to_string(),
)
.parse()
.unwrap_or_else(|_| {
eprintln!("[warn] Invalid --debounce-ms value, using default ({DEFAULT_DEBOUNCE_MS}ms)");
DEFAULT_DEBOUNCE_MS
});
let runtime = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
let config = KitBootstrapConfig::new(PathBuf::from(&db)).with_debounce_ms(debounce_ms);
match runtime.block_on(build_kit(&config)) {
Ok(kit) => {
if let Err(e) = init_kit(kit) {
eprintln!("[warn] Kit already initialized: {e}");
}
}
Err(e) => {
eprintln!("[warn] Failed to build Kit (commands requiring Kit will fail): {e}");
}
}
let args = extract_args(sub_name, sub_matches);
let handler = match sdforge::inventory::iter::<sdforge::cli::CliHandlerRegistration>()
.find(|h| h.name == sub_name)
{
Some(h) => h,
None => {
eprintln!("[error] No handler registered for command: {sub_name}");
std::process::exit(1);
}
};
if let Err(api_error) = runtime.block_on((handler.handler)(args)) {
let cli_error = CodeNexusError::from(api_error);
eprintln!("Error: {cli_error}");
std::process::exit(cli_error.exit_code());
}
}
fn extract_global_arg(
top: &sdforge::clap::ArgMatches,
sub: &sdforge::clap::ArgMatches,
name: &str,
fallback: &str,
) -> String {
top.get_one::<String>(name)
.or_else(|| sub.get_one::<String>(name))
.cloned()
.unwrap_or_else(|| fallback.to_string())
}
fn extract_args(
sub_name: &str,
sub_matches: &sdforge::clap::ArgMatches,
) -> HashMap<String, String> {
let mut args = HashMap::new();
for reg in sdforge::inventory::iter::<sdforge::cli::CliCommandRegistration>() {
if reg.name == sub_name {
for arg_info in reg.args {
if let Some(value) = sub_matches.get_one::<String>(arg_info.name) {
args.insert(arg_info.name.to_string(), value.clone());
}
}
break;
}
}
args
}
#[cfg(feature = "mcp")]
fn run_mcp_server() {
let db = std::env::args()
.skip(2)
.collect::<Vec<_>>()
.chunks(2)
.find(|chunk| chunk.len() == 2 && chunk[0] == "--db")
.map(|chunk| chunk[1].clone())
.unwrap_or_else(|| DEFAULT_DB_PATH.to_string());
let runtime = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
let kit = match runtime.block_on(build_kit(&KitBootstrapConfig::new(PathBuf::from(db)))) {
Ok(kit) => kit,
Err(e) => {
eprintln!("[error] Failed to build Kit: {e}");
std::process::exit(1);
}
};
if let Err(e) = init_kit(kit) {
eprintln!("[error] Failed to init Kit: {e}");
std::process::exit(1);
}
runtime.block_on(async {
let server = sdforge::mcp::build();
sdforge::mcp::serve_stdio(server)
.await
.expect("MCP serve error");
});
}
#[cfg(test)]
mod tests {
use super::*;
use crossbeam_channel::unbounded;
use inklog::domain::core::LoggerSubscriber;
use inklog::{LogRecord, Metrics};
use std::sync::Arc;
use tracing_subscriber::prelude::*;
#[test]
fn inklog_captures_tracing_events() {
let (console_tx, console_rx) = unbounded::<Arc<LogRecord>>();
let (async_tx, _async_rx) = unbounded::<Arc<LogRecord>>();
let metrics = Arc::new(Metrics::new());
let layer = LoggerSubscriber::new(console_tx, async_tx, metrics);
let registry = tracing_subscriber::registry().with(layer);
tracing::subscriber::with_default(registry, || {
tracing::info!("codenexus_test_marker");
});
let record = console_rx
.try_recv()
.expect("should capture tracing event via inklog LoggerSubscriber");
assert!(
record.message.contains("codenexus_test_marker"),
"expected message to contain the marker, got: {:?}",
record.message
);
}
#[test]
fn init_logging_does_not_panic() {
init_logging();
}
fn build_test_cmd() -> sdforge::clap::Command {
sdforge::clap::Command::new("codenexus")
.arg(
sdforge::clap::Arg::new("db")
.long("db")
.global(true)
.default_value(DEFAULT_DB_PATH),
)
.subcommand(sdforge::clap::Command::new("index"))
}
#[test]
fn extract_global_arg_returns_top_value_when_present() {
let cmd = build_test_cmd();
let matches = cmd.get_matches_from(["codenexus", "--db", "/top/db", "index"]);
let sub = matches.subcommand_matches("index").unwrap();
let result = extract_global_arg(&matches, sub, "db", "fallback");
assert_eq!(result, "/top/db");
}
#[test]
fn extract_global_arg_returns_sub_value_when_top_absent() {
let cmd = sdforge::clap::Command::new("codenexus")
.arg(sdforge::clap::Arg::new("db").long("db").global(true))
.subcommand(
sdforge::clap::Command::new("index").arg(sdforge::clap::Arg::new("db").long("db")),
);
let matches = cmd.get_matches_from(["codenexus", "index", "--db", "/sub/db"]);
let sub = matches.subcommand_matches("index").unwrap();
let result = extract_global_arg(&matches, sub, "db", "fallback");
assert_eq!(result, "/sub/db");
}
#[test]
fn extract_global_arg_returns_fallback_when_neither_present() {
let cmd = sdforge::clap::Command::new("codenexus")
.arg(
sdforge::clap::Arg::new("unused")
.long("unused")
.global(true),
)
.subcommand(sdforge::clap::Command::new("index"));
let matches = cmd.get_matches_from(["codenexus", "index"]);
let sub = matches.subcommand_matches("index").unwrap();
let result = extract_global_arg(&matches, sub, "unused", "fallback_value");
assert_eq!(result, "fallback_value");
}
#[test]
fn extract_args_returns_empty_for_unregistered_command() {
let cmd = sdforge::clap::Command::new("codenexus")
.subcommand(sdforge::clap::Command::new("nonexistent_cmd"));
let matches = cmd.get_matches_from(["codenexus", "nonexistent_cmd"]);
let sub = matches.subcommand_matches("nonexistent_cmd").unwrap();
let args = extract_args("nonexistent_cmd", sub);
assert!(
args.is_empty(),
"unregistered command should return empty args"
);
}
}