use clap::Subcommand; use eyre::{Result, WrapErr}; use serde_json::json; use crate::{ atuin_client::{ database::ClientSqlite, encryption, history::store::HistoryStore, record::{sqlite_store::SqliteStore, store::Store, sync}, settings::Settings }, atuin_common::utils, }; mod status; #[derive(Subcommand, Debug)] #[command(infer_subcommands = true)] pub(crate) enum Cmd { /// Sync with the configured server Perform { /// Force re-download everything #[arg(long, short)] force: bool, }, /// Print (or generate) the encryption key and user id for transfer to another machine KeyAndId {}, /// Display the sync status Status, } impl Cmd { pub(crate) async fn run( self, settings: Settings, db: &ClientSqlite, store: SqliteStore, ) -> Result<()> { match self { Self::Perform { force } => run(&settings, force, db, store).await, Self::Status => status::run(&settings).await, Self::KeyAndId {} => { use crate::atuin_client::encryption::{encode_key, load_key}; let key = load_key(&settings).wrap_err("could not load encryption key")?; let user_id = settings .sync .user_id() .wrap_err("Failed to load user-id")? .unwrap_or_else(utils::uuid_v7); let key = encode_key(&key).wrap_err("could not encode encryption key")?; let json = serde_json::to_string_pretty(&json!({ "key": key, "user_id": user_id })) .expect("Will always be formattable"); println!("{json}"); Ok(()) } } } } async fn run( settings: &Settings, force: bool, db: &ClientSqlite, store: SqliteStore, ) -> Result<()> { let encryption_key: [u8; 32] = encryption::load_key(settings) .context("could not load encryption key")? .into(); let host_id = Settings::host_id().await?; let history_store = HistoryStore::new(store.clone(), host_id, encryption_key); let (uploaded, downloaded) = sync::sync(settings, &store, &encryption_key) .await .map_err(crate::print_error::format_sync_error)?; crate::sync::build(settings, &store, db, Some(&downloaded)).await?; println!("{uploaded}/{} up/down to record store", downloaded.len()); let history_length = db.history_count(true).await?; let store_history_length = store.len_tag("history").await?; #[expect(clippy::cast_sign_loss)] if history_length as u64 > store_history_length { println!("{history_length} in history index, but {store_history_length} in history store"); println!("Running automatic history store init..."); // Internally we use the global filter mode, so this context is ignored. // don't recurse or loop here. history_store.init_store(db).await?; println!("Re-running sync due to new records locally"); // we'll want to run sync once more, as there will now be stuff to upload let (uploaded, downloaded) = sync::sync(settings, &store, &encryption_key) .await .map_err(crate::print_error::format_sync_error)?; crate::sync::build(settings, &store, db, Some(&downloaded)).await?; println!("{uploaded}/{} up/down to record store", downloaded.len()); } println!( "Sync complete! {} items in history database, force: {}", db.history_count(true).await?, force ); Ok(()) }