use std::time::Duration;
use atuin_client::database::Sqlite;
use atuin_client::logs::FromSettings;
use atuin_client::record::sqlite_store::SqliteStore;
use atuin_client::settings::Settings;
use atuin_client::theme;
use atuin_common::logs::{self, LogConfig};
use clap::Subcommand;
use eyre::{Result, WrapErr};
use crate::logs::LogCtx;
#[cfg(feature = "sync")]
mod sync;
#[cfg(feature = "sync")]
mod account;
#[cfg(feature = "daemon")]
mod daemon;
mod config;
mod default_config;
mod doctor;
mod dotfiles;
mod history;
mod hook;
mod import;
mod info;
mod init;
mod internal;
mod kv;
mod scripts;
mod search;
mod setup;
mod stats;
mod store;
#[cfg(feature = "self-update")]
mod update;
mod wrapped;
#[derive(Subcommand, Debug)]
#[command(infer_subcommands = true)]
pub enum Cmd {
#[command()]
Setup,
#[command(subcommand)]
History(history::Cmd),
Hook(hook::Cmd),
#[command(subcommand)]
Import(import::Cmd),
Stats(stats::Cmd),
Search(search::Cmd),
#[cfg(feature = "sync")]
#[command(flatten)]
Sync(sync::Cmd),
#[cfg(feature = "sync")]
Account(account::Cmd),
#[command(subcommand)]
Kv(kv::Cmd),
#[command(subcommand)]
Store(store::Cmd),
#[command(subcommand)]
Dotfiles(dotfiles::Cmd),
#[command(subcommand)]
Scripts(scripts::Cmd),
#[command()]
Init(init::Cmd),
#[command()]
Info,
#[command()]
Doctor,
#[cfg(feature = "self-update")]
#[command()]
Update(update::Cmd),
#[command()]
Wrapped {
year: Option<i32>,
},
#[cfg(feature = "daemon")]
#[command()]
Daemon(daemon::Cmd),
#[command()]
DefaultConfig,
#[command(subcommand)]
Config(config::Cmd),
#[cfg(feature = "ai")]
#[command(subcommand)]
Ai(atuin_ai::commands::Command),
#[cfg(feature = "ai")]
#[command()]
Mcp,
#[command(
subcommand,
hide = true,
name = "__internal",
help_template = "error: this command is not meant to be accessed directly",
disable_help_flag = true,
disable_help_subcommand = true
)]
Internal(internal::Cmd),
#[command(
hide = true,
name = "__internal_",
disable_help_flag = true,
disable_help_subcommand = true
)]
InternalDecoy,
}
impl Cmd {
pub fn run(self) -> Result<()> {
#[cfg(all(unix, feature = "daemon"))]
if let Self::Daemon(ref cmd) = self
&& cmd.should_daemonize()
{
daemon::daemonize_current_process()?;
}
#[cfg(feature = "ai")]
let use_multi_thread_runtime = matches!(&self, Self::Ai(_));
#[cfg(not(feature = "ai"))]
let use_multi_thread_runtime = false;
let Some(future) = self.run_inner()? else {
return Ok(());
};
let runtime = if use_multi_thread_runtime {
tokio::runtime::Builder::new_multi_thread()
} else {
tokio::runtime::Builder::new_current_thread()
}
.enable_all()
.build()
.unwrap();
let res = runtime.block_on(future);
runtime.shutdown_timeout(std::time::Duration::from_millis(50));
res
}
fn run_inner(self) -> Result<Option<impl Future<Output = Result<()>>>> {
let run_internal = if let Self::Internal(cmd) = &self {
match cmd.run() {
Some(func) => Some(func),
None => return Ok(None),
}
} else {
None
};
let settings = Settings::new().wrap_err("could not load client settings")?;
let _logging = self
.log_config(&settings)
.map(|c| LogCtx::try_enable("atuin", &c))
.transpose()
.wrap_err("failed to enable logging")?;
Ok(Some(async {
if let Some(func) = run_internal {
func(settings).await
} else {
Box::pin(self.run_async(settings)).await
}
}))
}
#[allow(clippy::too_many_lines)]
#[allow(clippy::future_not_send)]
async fn run_async(self, mut settings: Settings) -> Result<()> {
tracing::trace!(command = ?self, "client command");
let mut theme_manager = theme::ThemeManager::new(settings.theme.debug, None);
match self {
Self::History(history) => return history.run(&settings).await,
Self::Hook(hook) => return hook.run(&settings).await,
Self::Init(init) => return init.run(&settings).await,
Self::Doctor => return doctor::run(&settings).await,
#[cfg(feature = "self-update")]
Self::Update(update) => return update.run(&settings).await,
Self::Config(config) => return config.run(&settings).await,
Self::InternalDecoy => {
eprintln!("error: this command is not meant to be accessed directly");
std::process::exit(1);
}
_ => {}
}
let db_path = &settings.db_path;
let record_store_path = &settings.record_store_path;
let db = Sqlite::new(db_path, Duration::try_from_secs_f64(settings.local_timeout)?).await?;
let sqlite_store = SqliteStore::new(
record_store_path,
Duration::try_from_secs_f64(settings.local_timeout)?,
)
.await?;
let theme_name = settings.theme.name.clone();
let theme = theme_manager.load_theme(theme_name.as_str(), settings.theme.max_depth);
match self {
Self::Setup => setup::run(&settings).await,
Self::Import(import) => import.run(&db).await,
Self::Stats(stats) => stats.run(&db, &settings, theme).await,
Self::Search(search) => search.run(db, &mut settings, sqlite_store, theme).await,
#[cfg(feature = "sync")]
Self::Sync(sync) => sync.run(settings, &db, sqlite_store).await,
#[cfg(feature = "sync")]
Self::Account(account) => account.run(settings, sqlite_store).await,
Self::Kv(kv) => kv.run(&settings, &sqlite_store).await,
Self::Store(store) => store.run(&settings, &db, sqlite_store).await,
Self::Dotfiles(dotfiles) => dotfiles.run(&settings, sqlite_store).await,
Self::Scripts(scripts) => scripts.run(&settings, sqlite_store, &db).await,
Self::Info => {
info::run(&settings);
Ok(())
}
Self::DefaultConfig => {
default_config::run();
Ok(())
}
Self::Wrapped { year } => wrapped::run(year, &db, &settings, sqlite_store, theme).await,
#[cfg(feature = "daemon")]
Self::Daemon(cmd) => cmd.run(settings, sqlite_store, db).await,
Self::History(_)
| Self::Hook(_)
| Self::Init(_)
| Self::Doctor
| Self::Config(_)
| Self::Internal(_)
| Self::InternalDecoy => {
unreachable!()
}
#[cfg(feature = "self-update")]
Self::Update(_) => unreachable!(),
#[cfg(feature = "ai")]
Self::Ai(cli) => atuin_ai::commands::run(cli, &settings).await,
#[cfg(feature = "ai")]
Self::Mcp => atuin_ai::mcp::run(&db).await,
}
}
fn log_config(&self, settings: &Settings) -> Option<LogConfig> {
match self {
Self::History(cmd) => cmd.log_config(),
Self::Search(cmd) if cmd.is_interactive() => {
Some(LogConfig::from_settings(&settings.logs, &settings.logs.search))
}
#[cfg(feature = "daemon")]
Self::Daemon(cmd) => Some(LogConfig {
file: logs::FileConfig::from_settings(&settings.logs, &settings.logs.daemon),
stderr: cmd.show_logs().then(logs::StderrConfig::verbose),
}),
#[cfg(feature = "ai")]
Self::Ai(cmd) => cmd.log_config(settings),
Self::Internal(cmd) => cmd.log_config(),
_ => Some(LogConfig::stderr_only()),
}
}
}