use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use anyhow::{Context, Result};
use clap::{CommandFactory, Parser, Subcommand};
use tracing_subscriber::EnvFilter;
use basemind::config::{self, Config, DocumentsCliOverrides};
use basemind::render::{self, Verbosity};
use basemind::store::{LockHolder, Store};
use basemind::watcher::{BatchKind, WatchBatch};
mod comms_cli;
mod doctor_cli;
mod lang_cli;
#[cfg(all(feature = "comms", any(unix, windows)))]
mod stdio_relay;
#[derive(Parser, Debug)]
#[command(
name = "basemind",
version,
about = "File-watcher and code-map generator using tree-sitter",
long_about = None
)]
struct Cli {
#[arg(long, global = true)]
root: Option<PathBuf>,
#[arg(short, long, global = true, conflicts_with = "verbose")]
quiet: bool,
#[arg(short, long, global = true)]
verbose: bool,
#[arg(long, global = true)]
no_color: bool,
#[arg(long, global = true)]
json: bool,
#[arg(long, global = true, default_value_t = basemind::store::VIEW_WORKING.to_string())]
view: String,
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand, Debug)]
enum Cmd {
Init(basemind::cli::init::InitArgs),
Scan(ScanArgs),
Rescan(RescanArgs),
Watch,
#[command(subcommand)]
Code(basemind::cli::code::CodeCmd),
#[command(subcommand)]
Git(basemind::cli::git::GitCmd),
#[command(subcommand)]
Graph(basemind::cli::graph::GraphCmd),
#[command(subcommand)]
Memory(basemind::cli::memory::MemoryCmd),
#[command(subcommand)]
Web(basemind::cli::web::WebCmd),
#[cfg(all(feature = "shells", any(unix, windows)))]
#[command(subcommand)]
Shell(basemind::cli::shell::ShellCmd),
#[command(subcommand)]
Admin(basemind::cli::admin::AdminCmd),
Hook {
#[command(subcommand)]
action: basemind::cli::hook::HookCmd,
},
Doctor,
Status,
Completions {
shell: clap_complete::Shell,
},
Man,
Lang {
#[command(subcommand)]
action: LangCmd,
},
CompressOutput(basemind::textcompress::cli::CompressOutputArgs),
Delta(basemind::textcompress::cli::DeltaArgs),
Checkpoint(basemind::textcompress::cli::CheckpointArgs),
DetectWaste(basemind::textcompress::cli::DetectWasteArgs),
Serve,
Statusline,
#[command(subcommand)]
Cache(basemind::cli::admin::CacheCmd),
#[cfg(all(feature = "comms", any(unix, windows)))]
Comms {
#[command(subcommand)]
action: CommsLifecycleCmd,
},
#[cfg(all(feature = "comms", any(unix, windows)))]
Agents {
#[command(subcommand)]
action: basemind::cli::agents::AgentsCmd,
},
#[cfg(all(feature = "comms", any(unix, windows)))]
Workspace {
#[command(subcommand)]
action: basemind::cli::registry::WorkspaceCmd,
},
#[cfg(all(feature = "comms", any(unix, windows)))]
Daemon {
#[command(subcommand)]
action: DaemonCmd,
},
}
#[cfg(all(feature = "comms", any(unix, windows)))]
#[derive(Subcommand, Debug)]
enum DaemonCmd {
Ensure,
}
#[cfg(all(feature = "comms", any(unix, windows)))]
#[derive(Subcommand, Debug)]
enum CommsLifecycleCmd {
Daemon,
Start,
Stop {
#[arg(long)]
all: bool,
},
Status,
Doctor {
#[arg(long)]
probe: bool,
#[arg(long)]
clear_fatal: bool,
},
}
#[derive(clap::Args, Debug)]
struct ScanArgs {
#[arg(long, conflicts_with = "rev")]
staged: bool,
#[arg(long, value_name = "REV")]
rev: Option<String>,
#[arg(long)]
no_git_history: bool,
#[arg(long)]
rebuild_git_history: bool,
#[command(flatten)]
documents: DocumentsCliOverrides,
}
#[derive(clap::Args, Debug)]
struct RescanArgs {
#[arg(value_name = "PATH")]
paths: Vec<String>,
#[arg(long)]
full: bool,
#[arg(long)]
no_git_history: bool,
#[arg(long)]
rebuild_git_history: bool,
}
#[derive(Subcommand, Debug)]
enum LangCmd {
List,
Install,
Clean,
}
fn default_log_directive(verbosity: Verbosity) -> &'static str {
match verbosity {
Verbosity::Quiet => "warn",
Verbosity::Default => "info",
Verbosity::Verbose => "debug",
}
}
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
fn main() -> std::process::ExitCode {
match run() {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(error) => {
eprintln!("Error: {error:?}");
std::process::ExitCode::from(basemind::cli::exit::exit_code_for(&error))
}
}
}
fn run() -> Result<()> {
basemind::alloc_tag::retag_heap_pages();
let process_started = std::time::Instant::now();
#[cfg(all(feature = "shells", any(unix, windows)))]
if let Some(result) = basemind::shells::intercept_internal_reexec() {
return result;
}
let cli = Cli::parse();
let verbosity = Verbosity::from_flags(cli.quiet, cli.verbose);
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_log_directive(verbosity))),
)
.with_target(false)
.with_writer(std::io::stderr)
.init();
#[cfg(feature = "intelligence")]
{
#[cfg(all(feature = "comms", any(unix, windows)))]
let is_comms = matches!(cli.cmd, Cmd::Comms { .. });
#[cfg(not(all(feature = "comms", any(unix, windows))))]
let is_comms = false;
if !is_comms {
basemind::embeddings::bound_ort_memory(basemind::embeddings::resolve_embed_threads(0));
}
}
let no_color = cli.no_color;
let start = cli
.root
.clone()
.map(|p| p.canonicalize().unwrap_or(p))
.unwrap_or_else(|| std::env::current_dir().expect("cwd"));
let root = basemind::config::discover_root_with_basemind(&start);
if root != start {
tracing::info!(
resolved_root = %root.display(),
from = "ancestor basemind.toml",
"resolved repo root upward"
);
}
let json = cli.json;
let view = cli.view.clone();
warn_ignored_global_flags(&cli.cmd, json, &view);
let dispatch = |tc| {
basemind::cli::run(
&root,
&view,
DocumentsCliOverrides::default(),
json,
process_started,
tc,
)
};
match cli.cmd {
Cmd::Init(args) => basemind::cli::init::run(&basemind::config::init_root(&start), &args),
Cmd::Scan(args) => cmd_scan(&root, &args, verbosity, no_color),
Cmd::Rescan(args) => cmd_rescan(&root, &args, verbosity, no_color),
Cmd::Watch => cmd_watch(&root, verbosity, no_color),
Cmd::Code(c) => {
let _ = basemind::lang::ensure_grammars();
dispatch(basemind::cli::ToolCmd::Code(c))
}
Cmd::Git(g) => dispatch(basemind::cli::ToolCmd::Git(g)),
Cmd::Graph(g) => {
let _ = basemind::lang::ensure_grammars();
dispatch(basemind::cli::ToolCmd::Graph(g))
}
Cmd::Memory(m) => dispatch(basemind::cli::ToolCmd::Memory(m)),
Cmd::Web(w) => dispatch(basemind::cli::ToolCmd::Web(w)),
#[cfg(all(feature = "shells", any(unix, windows)))]
Cmd::Shell(s) => dispatch(basemind::cli::ToolCmd::Shell(s)),
Cmd::Admin(a) => dispatch(basemind::cli::ToolCmd::Admin(a)),
Cmd::Hook { action } => basemind::cli::hook::run(&root, action),
Cmd::Doctor => doctor_cli::cmd_doctor(&root, json),
Cmd::Status => {
let _ = basemind::lang::ensure_grammars();
dispatch(basemind::cli::ToolCmd::Status)
}
Cmd::Completions { shell } => {
let mut script = Vec::new();
clap_complete::generate(shell, &mut Cli::command(), "basemind", &mut script);
std::io::Write::write_all(&mut std::io::stdout(), &script).map_err(Into::into)
}
Cmd::Man => clap_mangen::Man::new(Cli::command())
.render(&mut std::io::stdout())
.map_err(Into::into),
Cmd::Lang { action } => match action {
LangCmd::List => lang_cli::cmd_lang_list(no_color),
LangCmd::Install => lang_cli::cmd_lang_install(&root, verbosity, no_color),
LangCmd::Clean => lang_cli::cmd_lang_clean(),
},
Cmd::CompressOutput(args) => basemind::textcompress::cli::run(&args),
Cmd::Delta(args) => basemind::textcompress::cli::run_delta(&args),
Cmd::Checkpoint(args) => basemind::textcompress::cli::run_checkpoint(&root, &args),
Cmd::DetectWaste(args) => basemind::textcompress::cli::run_detect_waste(&args),
Cmd::Serve => cmd_serve(&root, &view),
Cmd::Cache(action) => basemind::cli::run_cache(&root, action, json),
Cmd::Statusline => comms_cli::cmd_statusline(cli.root.as_ref().map(|_| root.as_path())),
#[cfg(all(feature = "comms", any(unix, windows)))]
Cmd::Comms { action } => comms_cli::cmd_comms(action, json),
#[cfg(all(feature = "comms", any(unix, windows)))]
Cmd::Agents { action } => basemind::cli::agents::run(&root, json, action),
#[cfg(all(feature = "comms", any(unix, windows)))]
Cmd::Workspace { action } => basemind::cli::registry::run(&root, json, action),
#[cfg(all(feature = "comms", any(unix, windows)))]
Cmd::Daemon { action } => comms_cli::cmd_daemon(action, json),
}
}
fn warn_ignored_global_flags(cmd: &Cmd, json: bool, view: &str) {
let consumes_json = matches!(
cmd,
Cmd::Code(_)
| Cmd::Git(_)
| Cmd::Graph(_)
| Cmd::Memory(_)
| Cmd::Web(_)
| Cmd::Admin(_)
| Cmd::Cache(_)
| Cmd::Status
| Cmd::Doctor
);
#[cfg(all(feature = "comms", any(unix, windows)))]
let consumes_json = consumes_json
|| matches!(
cmd,
Cmd::Comms { .. } | Cmd::Agents { .. } | Cmd::Workspace { .. } | Cmd::Daemon { .. }
);
#[cfg(all(feature = "shells", any(unix, windows)))]
let consumes_json = consumes_json || matches!(cmd, Cmd::Shell(_));
let consumes_view = consumes_json || matches!(cmd, Cmd::Serve);
if json && !consumes_json {
tracing::warn!("--json has no effect on this subcommand; ignoring");
}
if view != basemind::store::VIEW_WORKING && !consumes_view {
tracing::warn!(view = %view, "--view has no effect on this subcommand; ignoring");
}
}
fn bootstrap_grammars(verbosity: Verbosity, no_color: bool) -> Result<()> {
let summary = basemind::lang::ensure_grammars().map_err(|e| anyhow::anyhow!("grammar bootstrap failed: {e}"))?;
let mut out = render::stdout(no_color);
render::render_grammar_bootstrap(&mut out, &summary, verbosity);
Ok(())
}
fn load_or_default(root: &std::path::Path) -> Result<Config> {
load_or_default_with(root, None)
}
fn guard_workspace_root(root: &std::path::Path) -> Result<PathBuf> {
config::root_guard::workspace_root_verdict(root)
.map_err(|refusal| anyhow::anyhow!(config::root_guard::refusal_message(root, refusal)))
}
fn load_or_default_with(root: &std::path::Path, cli: Option<DocumentsCliOverrides>) -> Result<Config> {
match config::load_with_overrides(root, None, cli) {
Ok(loaded) => Ok(loaded.config),
Err(config::ConfigError::NotFound(_)) => {
tracing::info!("no basemind.toml; using defaults");
Ok(config::default_for_root(root))
}
Err(e) => Err(anyhow::anyhow!(e)),
}
}
fn open_store_for_write(root: &std::path::Path, view: &str, what: &str, holder: LockHolder) -> Result<Store> {
Store::open_with_holder(root, view, holder).map_err(|err| {
if err.is_lock_contention() {
anyhow::Error::new(err).context(basemind::store::LOCK_CONTENTION_HELP.to_string())
} else {
anyhow::Error::new(err).context(format!("open store ({what})"))
}
})
}
fn writer_collision_notice(root: &std::path::Path) -> Option<String> {
let basemind_dir = basemind::store::workspace_cache_dir(root);
match basemind::store::probe_writer_lock(&basemind_dir) {
basemind::store::WriterProbe::Free => None,
basemind::store::WriterProbe::Held { holder: Some(meta) } => Some(format!(
"`{}` (pid {}) is already running against this repo and keeping the index fresh — \
running this directly is unnecessary and would collide with it. Use `basemind rescan` \
(forwarded to the daemon when one is running) to refresh the index, or stop it first.",
meta.command, meta.pid
)),
basemind::store::WriterProbe::Held { holder: None } => Some(basemind::store::LOCK_CONTENTION_HELP.to_string()),
}
}
fn sync_git_history_after_scan(
root: &std::path::Path,
cli_enabled: bool,
force_rebuild: bool,
out: &mut impl std::io::Write,
) {
if !cli_enabled || !basemind::git_history::index_enabled() {
return;
}
let Ok(repo) = basemind::git::Repo::discover(root) else {
return;
};
let basemind_dir = basemind::git_history::shared_history_basemind_dir(root);
let index = match basemind::git_history::GitHistoryIndex::open(&basemind_dir) {
Ok(index) => index,
Err(error) => {
tracing::warn!(?error, "git-history index unavailable; skipping");
return;
}
};
if force_rebuild && let Err(error) = index.clear(&basemind_dir) {
tracing::warn!(?error, "git-history index clear failed");
}
match basemind::git_history::builder::sync(&index, &repo, &basemind_dir) {
Ok(outcome) => {
let summary = match outcome {
basemind::git_history::builder::RebuildOutcome::Fresh => "git-history index: up to date".to_string(),
basemind::git_history::builder::RebuildOutcome::Skipped { reason } => {
format!("git-history index: not updated ({reason})")
}
basemind::git_history::builder::RebuildOutcome::Incremental { added } => {
format!("git-history index: +{added} commits")
}
basemind::git_history::builder::RebuildOutcome::FullRebuild { reason, commits } => {
format!("git-history index: rebuilt {commits} commits ({reason})")
}
};
let _ = writeln!(out, "{summary}");
}
Err(error) => tracing::warn!(?error, "git-history index sync failed"),
}
std::mem::forget(index);
}
fn scan_streaming(
no_color: bool,
verbosity: Verbosity,
source: basemind::scanner::ScanSource<'_>,
root: &std::path::Path,
store: &mut Store,
config: &basemind::config::Config,
cancel: &basemind::scanner::ScanCancel,
) -> std::result::Result<basemind::scanner::ScanReport, basemind::scanner::ScanError> {
let mut observer = render::ScanLineObserver::new(no_color, verbosity);
basemind::scanner::scan_with_observer(
root,
store,
config,
source,
basemind::scanner::EmbedMode::Inline,
cancel,
&mut observer,
)
}
fn interrupted(
out: &mut anstream::AutoStream<std::io::Stdout>,
report: &basemind::scanner::ScanReport,
verbosity: Verbosity,
what: &str,
) -> Result<()> {
render::render_summary(out, &report.stats, verbosity);
Err(basemind::cli::exit::CliExit::interrupted(format!(
"{what} interrupted: files already indexed are committed and the index is consistent but incomplete; \
rerun to finish"
)))
}
fn cmd_scan(root: &std::path::Path, args: &ScanArgs, verbosity: Verbosity, no_color: bool) -> Result<()> {
let cancel = basemind::cli::interrupt::install();
let root = &guard_workspace_root(root)?;
bootstrap_grammars(verbosity, no_color)?;
let config = load_or_default_with(root, Some(args.documents.clone()))?;
let mut out = render::stdout(no_color);
if args.staged {
let repo = basemind::git::Repo::discover(root).context("`--staged` requires being inside a git repository")?;
let mut store = open_store_for_write(root, basemind::store::VIEW_STAGED, "staged", LockHolder::Scan)?;
render::render_scan_header(&mut out, "staged index", verbosity);
let report = scan_streaming(
no_color,
verbosity,
basemind::scanner::ScanSource::Staged(&repo),
root,
&mut store,
&config,
&cancel,
)
.context("scan staged")?;
if report.cancelled {
return interrupted(&mut out, &report, verbosity, "scan --staged");
}
render::render_summary(&mut out, &report.stats, verbosity);
return Ok(());
}
if let Some(rev_spec) = &args.rev {
let repo = basemind::git::Repo::discover(root).context("`--rev` requires being inside a git repository")?;
let sha = repo.resolve_rev(rev_spec).context("resolve rev")?;
let short = &sha[..7.min(sha.len())];
let view = basemind::store::view_name_for_rev(short);
let mut store = open_store_for_write(root, &view, "rev", LockHolder::Scan)?;
render::render_scan_header(&mut out, &format!("rev {short}"), verbosity);
let report = scan_streaming(
no_color,
verbosity,
basemind::scanner::ScanSource::Rev {
repo: &repo,
sha: sha.clone(),
},
root,
&mut store,
&config,
&cancel,
)
.context("scan rev")?;
if report.cancelled {
return interrupted(&mut out, &report, verbosity, "scan --rev");
}
render::render_summary(&mut out, &report.stats, verbosity);
return Ok(());
}
if let Some(notice) = writer_collision_notice(root) {
return Err(basemind::cli::exit::CliExit::busy(notice));
}
let mut store = open_store_for_write(root, basemind::store::VIEW_WORKING, "scan", LockHolder::Scan)?;
let report = scan_streaming(
no_color,
verbosity,
basemind::scanner::ScanSource::WorkingTree,
root,
&mut store,
&config,
&cancel,
)
.context("scan")?;
if report.cancelled {
return interrupted(&mut out, &report, verbosity, "scan");
}
render::render_summary(&mut out, &report.stats, verbosity);
reclaim_after_scan(&mut out, &report.stats, std::mem::take(&mut store.reset_stale_schema));
sync_git_history_after_scan(root, !args.no_git_history, args.rebuild_git_history, &mut out);
Ok(())
}
fn cmd_rescan(root: &std::path::Path, args: &RescanArgs, verbosity: Verbosity, no_color: bool) -> Result<()> {
let root = &guard_workspace_root(root)?;
let full = args.full || args.paths.is_empty();
let abs = if full {
Vec::new()
} else {
basemind::cli::rescan::resolve_paths(root, &args.paths)?
};
let mut out = render::stdout(no_color);
#[cfg(all(feature = "comms", any(unix, windows)))]
if basemind::cli::rescan::daemon_is_up() {
use std::io::Write as _;
let report = basemind::cli::rescan::rescan_via_daemon(root, (!full).then_some(abs), full)?;
let _ = writeln!(
out,
"rescan via daemon: {} scanned, {} updated, {} docs indexed, {} removed ({} ms)",
report.scanned, report.updated, report.docs_indexed, report.removed, report.elapsed_ms
);
return Ok(());
}
let cancel = basemind::cli::interrupt::install();
bootstrap_grammars(verbosity, no_color)?;
let config = load_or_default(root)?;
if let Some(notice) = writer_collision_notice(root) {
return Err(basemind::cli::exit::CliExit::busy(notice));
}
let mut store = open_store_for_write(root, basemind::store::VIEW_WORKING, "rescan", LockHolder::Rescan)?;
let report = if full {
scan_streaming(
no_color,
verbosity,
basemind::scanner::ScanSource::WorkingTree,
root,
&mut store,
&config,
&cancel,
)
.context("rescan (full)")?
} else {
let mut observer = render::ScanLineObserver::new(no_color, verbosity);
basemind::scanner::scan_paths_with_observer(
root,
&mut store,
&config,
&abs,
basemind::scanner::EmbedMode::Inline,
&cancel,
&mut observer,
)
.context("rescan (paths)")?
};
if report.cancelled {
return interrupted(&mut out, &report, verbosity, "rescan");
}
render::render_summary(&mut out, &report.stats, verbosity);
reclaim_after_scan(&mut out, &report.stats, std::mem::take(&mut store.reset_stale_schema));
sync_git_history_after_scan(root, !args.no_git_history, args.rebuild_git_history, &mut out);
Ok(())
}
fn reclaim_after_scan(
out: &mut anstream::AutoStream<std::io::Stdout>,
stats: &basemind::scanner::ScanStats,
schema_reset: bool,
) {
if !stats.displaced_artifacts() && !schema_reset {
return;
}
match basemind::store_gc::reclaim_displaced_blobs() {
Ok(report) => render::render_cleanup(out, &report),
Err(error) => tracing::warn!(%error, "post-scan blob cleanup failed; the daemon's next sweep will retry"),
}
}
fn cmd_watch(root: &std::path::Path, verbosity: Verbosity, no_color: bool) -> Result<()> {
let root = &guard_workspace_root(root)?;
bootstrap_grammars(verbosity, no_color)?;
let config = Arc::new(load_or_default(root)?);
let store = Arc::new(Mutex::new(
Store::open_with_holder(root, basemind::store::VIEW_WORKING, LockHolder::Watch).context("open store")?,
));
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.context("build tokio runtime")?;
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
let store_w = Arc::clone(&store);
let config_w = Arc::clone(&config);
let root_buf = root.to_path_buf();
let watcher_handle = std::thread::spawn(move || {
let mut stdout = render::stdout(no_color);
let cb: basemind::watcher::BatchCallback = Box::new(move |batch: WatchBatch<'_>| match batch.kind {
BatchKind::InitialScan => {
render::render_files(&mut stdout, batch.results, verbosity);
render::render_summary(&mut stdout, &batch.report.stats, verbosity);
}
BatchKind::Incremental { paths } => {
render::render_batch_header(&mut stdout, paths, verbosity);
render::render_files(&mut stdout, batch.results, verbosity);
}
});
basemind::watcher::watch(&root_buf, store_w, config_w, shutdown_rx, cb)
});
runtime.block_on(async {
let _ = tokio::signal::ctrl_c().await;
tracing::info!("ctrl-c received; shutting down");
let _ = shutdown_tx.send(());
});
match watcher_handle.join() {
Ok(Ok(())) => Ok(()),
Ok(Err(e)) => Err(anyhow::anyhow!(e)),
Err(_) => Err(anyhow::anyhow!("watcher thread panicked")),
}
}
fn cmd_serve(root: &std::path::Path, view: &str) -> Result<()> {
if view != basemind::store::VIEW_WORKING {
let index_path = basemind::store::workspace_cache_dir(root)
.join(basemind::store::VIEWS_DIR)
.join(view)
.join(basemind::store::INDEX_FILE);
if !index_path.exists() {
anyhow::bail!(
"view {view:?} has not been scanned; run `basemind scan --view {view}` first \
(or omit --view to serve the working view)"
);
}
}
let root = &guard_workspace_root(root)?;
#[cfg(all(feature = "comms", any(unix, windows)))]
{
try_serve_relay(root, view).context(
"relay to the basemind daemon failed; run `basemind daemon ensure` to check the daemon, \
then retry",
)
}
#[cfg(not(all(feature = "comms", any(unix, windows))))]
{
let _ = root;
anyhow::bail!(
"`basemind serve` needs the comms daemon to host the MCP server; rebuild with \
`--features comms` (the stdio MCP server is served by relaying to that daemon)."
)
}
}
#[cfg(all(feature = "comms", any(unix, windows)))]
fn try_serve_relay(root: &std::path::Path, view: &str) -> Result<()> {
use basemind::comms::identity;
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.context("build tokio runtime")?;
runtime.block_on(async move {
let agent = identity::mcp_session_agent_id(root);
let canonical_root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let view = view.to_owned();
let stream = open_relay_connection(canonical_root.clone(), view.clone(), agent.clone()).await?;
tracing::info!(
pid = std::process::id(),
view = %view,
root = %canonical_root.display(),
"basemind serve: persistent relay connected"
);
stdio_relay::run(tokio::io::stdin(), tokio::io::stdout(), stream, move || {
open_relay_connection(canonical_root.clone(), view.clone(), agent.clone())
})
.await
.context("run persistent daemon relay")
})
}
#[cfg(all(feature = "comms", unix))]
type RelayStream = tokio::net::UnixStream;
#[cfg(all(feature = "comms", windows))]
type RelayStream = tokio::net::windows::named_pipe::NamedPipeClient;
#[cfg(all(feature = "comms", any(unix, windows)))]
async fn open_relay_connection(
root: PathBuf,
view: String,
agent: basemind::comms::ids::AgentId,
) -> Result<RelayStream> {
use basemind::comms::{relay, singleton};
let paths = singleton::resolve_paths().context("resolve comms paths")?;
singleton::ensure_daemon(&paths).await.context("ensure daemon")?;
let mut stream = relay_connect_stream(&paths.socket_path)
.await
.context("connect to daemon socket")?;
let hello = relay::RelayHello {
relay_proto_ver: relay::RELAY_PROTO_VER,
root,
view,
agent,
};
let welcome = relay::client_handshake(&mut stream, &hello)
.await
.context("relay handshake")?;
if welcome.relay_proto_ver != relay::RELAY_PROTO_VER {
anyhow::bail!(
"daemon relay-proto {} != client {}",
welcome.relay_proto_ver,
relay::RELAY_PROTO_VER
);
}
if !welcome.accepted {
anyhow::bail!("daemon declined relay: {:?}", welcome.code);
}
Ok(stream)
}
#[cfg(all(feature = "comms", unix))]
async fn relay_connect_stream(socket_path: &std::path::Path) -> std::io::Result<tokio::net::UnixStream> {
tokio::net::UnixStream::connect(socket_path).await
}
#[cfg(all(feature = "comms", windows))]
async fn relay_connect_stream(
socket_path: &std::path::Path,
) -> std::io::Result<tokio::net::windows::named_pipe::NamedPipeClient> {
use tokio::net::windows::named_pipe::ClientOptions;
const ERROR_PIPE_BUSY: i32 = 231;
const RETRY_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
let deadline = std::time::Instant::now() + CONNECT_TIMEOUT;
loop {
match ClientOptions::new().open(socket_path) {
Ok(client) => return Ok(client),
Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY) => {
if std::time::Instant::now() >= deadline {
return Err(e);
}
tokio::time::sleep(RETRY_INTERVAL).await;
}
Err(source) => return Err(source),
}
}
}