code-moniker 0.4.0

Standalone CLI / linter for the code-moniker symbol graph: file and directory probes, project-wide architecture rules.
Documentation
use std::io::Write;

use code_moniker_daemon_client::{DaemonClient, daemon_workspace_config};
use tracing::info;

use crate::args::McpArgs;
use crate::mcp::{DaemonRuntime, McpContext};
use crate::session::SessionOptions;
use crate::{DEFAULT_SCHEME, Exit, mcp};

pub(crate) fn run<W1: Write, W2: Write>(args: &McpArgs, stdout: &mut W1, stderr: &mut W2) -> Exit {
	match run_inner(args, stdout, stderr) {
		Ok(()) => Exit::Match,
		Err(error) => {
			let _ = writeln!(stderr, "code-moniker: {error:#}");
			Exit::UsageError
		}
	}
}

fn run_inner<W1: Write, W2: Write>(
	args: &McpArgs,
	_stdout: &mut W1,
	_stderr: &mut W2,
) -> anyhow::Result<()> {
	init_logging();
	let scheme = args.scheme.as_deref().unwrap_or(DEFAULT_SCHEME).to_string();
	let paths = args.paths.clone();
	let daemon_config = daemon_workspace_config(
		paths.clone(),
		args.project.to_owned(),
		args.cache.to_owned(),
		Some(live_refresh_label(args).to_string()),
	)?;
	let client = DaemonClient::connect_or_start_config(daemon_config.clone())?;
	let opts = SessionOptions {
		paths,
		project: args.project.to_owned(),
		cache_dir: args.cache.to_owned(),
	};
	let runtime = tokio::runtime::Builder::new_multi_thread()
		.enable_all()
		.thread_name("code-moniker-mcp")
		.build()?;
	runtime.block_on(run_server(
		opts,
		scheme,
		args.port,
		DaemonRuntime::client(client, daemon_config),
	))
}

fn live_refresh_label(args: &McpArgs) -> &'static str {
	match args.live_refresh {
		crate::args::LiveRefresh::OnDemand => "on-demand",
		crate::args::LiveRefresh::Auto => "auto",
	}
}

async fn run_server(
	opts: SessionOptions,
	scheme: String,
	port: u16,
	daemon: DaemonRuntime,
) -> anyhow::Result<()> {
	let paths_label = path_list(&opts);
	let listener = tokio::net::TcpListener::bind(("127.0.0.1", port)).await?;
	let addr = listener.local_addr()?;
	let context = McpContext::new(opts, scheme, daemon);
	let router = mcp::router(context);
	info!(
		event = "http_transport_ready",
		endpoint = %format!("http://{addr}/mcp"),
		paths = %paths_label,
		runtime = "daemon",
		"mcp http transport ready"
	);
	axum::serve(listener, router).await?;
	Ok(())
}

fn init_logging() {
	let _ = tracing_subscriber::fmt()
		.with_writer(std::io::stderr)
		.with_target(false)
		.with_level(true)
		.compact()
		.try_init();
}

fn path_list(opts: &SessionOptions) -> String {
	opts.paths
		.iter()
		.map(|path| path.display().to_string())
		.collect::<Vec<_>>()
		.join(",")
}