mod alias;
mod attach;
mod cache;
mod cli;
mod collide;
mod config;
mod doctor;
mod fleet;
mod handoff;
mod hook;
mod inject;
mod loader;
mod mcp;
mod notify;
mod pricing;
mod proc;
mod quota;
mod serve;
mod session;
#[cfg(unix)]
mod shim;
#[cfg(not(unix))]
#[path = "shim_stub.rs"]
mod shim;
mod tmux;
mod trace;
mod ui;
mod update;
mod util;
mod watch;
use clap::Parser;
use std::io::IsTerminal;
#[global_allocator]
static ALLOCATOR: mimalloc::MiMalloc = mimalloc::MiMalloc;
fn main() -> anyhow::Result<()> {
if std::env::args().nth(1).as_deref() == Some("hook") {
let argv: Vec<String> = std::env::args().collect();
std::process::exit(hook::emit(&argv[2..]));
}
{
let argv: Vec<String> = std::env::args().collect();
if argv.get(1).map(String::as_str) == Some("doctor") {
std::process::exit(doctor::run(&argv[2..]));
}
}
{
let argv: Vec<String> = std::env::args().collect();
if argv.get(1).map(String::as_str) == Some("serve") {
std::process::exit(serve::run(&argv[2..])?);
}
}
#[cfg(unix)]
let mut agent: Option<Vec<String>> = None;
#[cfg(unix)]
{
let argv: Vec<String> = std::env::args().collect();
if argv.get(1).map(String::as_str) == Some("attach") {
std::process::exit(attach::run_terminal(&argv[2..])?);
}
match argv.get(1).map(String::as_str) {
Some("run") => std::process::exit(shim::run(&argv[2..])?),
Some(word) if !word.starts_with('-') && shim::is_command(word) => {
agent = Some(argv[1..].to_vec());
}
_ => {}
}
}
#[cfg(unix)]
let args = match agent {
Some(_) => cli::Args::parse_from(["cctop"]),
None => cli::Args::parse(),
};
#[cfg(not(unix))]
let args = cli::Args::parse();
if args.trace.is_some() {
trace::enable();
}
if args.update {
return update::run(false);
}
if args.hooks_status {
let cwd = std::env::current_dir().ok();
for (line, problem) in hook::status(cwd.as_deref(), None).lines() {
eprintln!("{} {line}", if problem { "!" } else { "·" });
}
return Ok(());
}
if let Some(scope) = args
.install_hooks
.as_deref()
.or(args.remove_hooks.as_deref())
{
let installing = args.install_hooks.is_some();
let cwd = std::env::current_dir().unwrap_or_default();
let Some(scope) = hook::Scope::parse(scope, &cwd) else {
anyhow::bail!("unknown scope '{scope}'; use `user` or `project`");
};
for line in match installing {
true => hook::install(&scope),
false => hook::remove(&scope),
} {
eprintln!("{line}");
}
eprintln!("Sessions already running keep their old hooks until restarted.");
return Ok(());
}
if args.install_alias || args.remove_alias {
let changed = if args.install_alias {
alias::install()
} else {
alias::remove()
};
match changed.as_slice() {
[] => eprintln!("No shell startup file needed changing."),
files => {
for f in files {
eprintln!("Updated {}", f.display());
}
eprintln!("Restart your shell, or source the file, to pick it up.");
}
}
return Ok(());
}
if args.clear_cache && cache::clear_session_cache()? {
eprintln!("Cleared cctop session extraction cache.");
}
if args.mcp {
return mcp::serve();
}
if let Some(which) = &args.handoff {
let mut loader = loader::Loader::new();
let sessions = loader.load(args.plan);
cli::run_handoff(&sessions, which, &loader)?;
loader.store().save();
finish_trace(&args);
return Ok(());
}
if args.list || args.json {
pricing::refresh_pricing_blocking();
let mut loader = loader::Loader::new();
let sessions = loader.load(args.plan);
if args.json {
cli::run_json(&sessions, args.plan, &loader)?;
} else {
cli::run_list(&sessions, args.plan);
}
loader.store().save();
finish_trace(&args);
return Ok(());
}
if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
#[cfg(unix)]
if let Some(agent) = agent {
std::process::exit(shim::run(&agent)?);
}
anyhow::bail!("the interactive UI needs a TTY; use --list or --json instead");
}
#[cfg(unix)]
let launching_agent = agent.is_some();
#[cfg(not(unix))]
let launching_agent = false;
if !launching_agent && std::env::var_os("CI").is_none() {
alias::ask_on_first_run(&mut cache::UiPrefs::load());
}
pricing::load_cached_pricing();
#[cfg(unix)]
let hosted = agent.map(|agent| shim::host(&agent, None)).transpose()?;
#[cfg(not(unix))]
let hosted = None;
let code = ui::run(&args, hosted)?;
finish_trace(&args);
std::process::exit(code)
}
fn finish_trace(args: &cli::Args) {
let Some(requested) = args.trace.as_deref() else {
return;
};
let path = match requested {
"" => trace::default_path(),
given => std::path::PathBuf::from(given),
};
match trace::write_to(&path) {
Ok(()) => eprintln!("cctop: trace written to {}", path.display()),
Err(error) => eprintln!(
"cctop: could not write trace to {}: {error}",
path.display()
),
}
}