quilt-cli 0.37.0

Local-first data versioning for AI agent workflows: commit immutable data packages on your own machine, then sync them with S3 to share.
use std::path::Path;
use std::path::PathBuf;

#[cfg(test)]
use tempfile::TempDir;

use quilt_rs::lineage::Home;

use crate::cli::Error;
use crate::cli::browse;
use crate::cli::commit;
use crate::cli::create;
use crate::cli::gc;
use crate::cli::history;
use crate::cli::home;
use crate::cli::install;
use crate::cli::list;
use crate::cli::login;
use crate::cli::pull;
use crate::cli::push;
use crate::cli::remove_revisions;
use crate::cli::role;
use crate::cli::status;
use crate::cli::undo_commit;
use crate::cli::uninstall;

pub struct Model {
    local_domain: quilt_rs::LocalDomain,
}

pub trait Commands {
    fn get_local_domain(&self) -> &quilt_rs::LocalDomain;

    async fn browse(&self, args: browse::Input) -> Result<browse::Output, Error> {
        let local_domain = self.get_local_domain();
        browse::model(local_domain, args).await
    }

    async fn create(&self, args: create::Input) -> Result<create::Output, Error> {
        let local_domain = self.get_local_domain();
        create::model(local_domain, args).await
    }

    async fn commit(&self, args: commit::Input) -> Result<commit::Output, Error> {
        let local_domain = self.get_local_domain();
        commit::model(local_domain, args).await
    }

    async fn gc(&self) -> Result<gc::Output, Error> {
        gc::model(self.get_local_domain()).await
    }

    async fn home(&self, args: home::Input) -> Result<home::Output, Error> {
        let local_domain = self.get_local_domain();
        home::model(local_domain, args).await
    }

    async fn install(&self, args: install::Input) -> Result<install::Output, Error> {
        let local_domain = self.get_local_domain();
        install::model(local_domain, args).await
    }

    async fn list(&self, args: list::Input) -> Result<list::Output, Error> {
        let local_domain = self.get_local_domain();
        list::model(local_domain, args).await
    }

    async fn log(&self, args: history::Input) -> Result<history::Output, Error> {
        let local_domain = self.get_local_domain();
        history::model(local_domain, args).await
    }

    async fn login(&self, args: login::Input) -> Result<login::Output, Error> {
        let local_domain = self.get_local_domain();
        login::model(local_domain, args).await
    }

    async fn pull(&self, args: pull::Input) -> Result<pull::Output, Error> {
        let local_domain = self.get_local_domain();
        pull::model(local_domain, args).await
    }

    async fn push(&self, args: push::Input) -> Result<push::Output, Error> {
        let local_domain = self.get_local_domain();
        push::model(local_domain, args).await
    }

    async fn remove_revisions(
        &self,
        args: remove_revisions::Input,
    ) -> Result<remove_revisions::Output, Error> {
        let local_domain = self.get_local_domain();
        remove_revisions::model(local_domain, args).await
    }

    async fn undo_commit(&self, args: undo_commit::Input) -> Result<undo_commit::Output, Error> {
        let local_domain = self.get_local_domain();
        undo_commit::model(local_domain, args).await
    }

    async fn role(&self, args: role::Input) -> Result<role::Output, Error> {
        let local_domain = self.get_local_domain();
        role::model(local_domain, args).await
    }

    async fn status(&self, args: status::Input) -> Result<status::Output, Error> {
        let local_domain = self.get_local_domain();
        status::model(local_domain, args).await
    }

    async fn uninstall(&self, args: uninstall::Input) -> Result<uninstall::Output, Error> {
        let local_domain = self.get_local_domain();
        uninstall::model(local_domain, args).await
    }
}

impl Commands for Model {
    fn get_local_domain(&self) -> &quilt_rs::LocalDomain {
        &self.local_domain
    }
}

impl Model {
    fn new(local_domain: quilt_rs::LocalDomain) -> Self {
        Model { local_domain }
    }

    pub async fn get_home(&self) -> Result<Home, Error> {
        Ok(self.local_domain.get_home().await?)
    }

    pub async fn set_home(&self, dir: impl AsRef<Path>) -> Result<Home, Error> {
        Ok(self.local_domain.set_home(dir).await?)
    }

    #[cfg(test)]
    pub fn from_temp_dir() -> Result<(Self, TempDir), Error> {
        let temp_dir = TempDir::new()?;
        Ok((Model::from(&temp_dir), temp_dir))
    }
}

impl From<PathBuf> for Model {
    fn from(root: PathBuf) -> Self {
        let local_domain = quilt_rs::LocalDomain::new(root);
        Model::new(local_domain)
    }
}

#[cfg(test)]
impl From<&TempDir> for Model {
    fn from(temp_dir: &TempDir) -> Self {
        Model::from(temp_dir.path().to_path_buf())
    }
}

#[cfg(test)]
pub async fn install_package_into_temp_dir(
    uri_str: &str,
) -> Result<(Model, quilt_rs::InstalledPackage, TempDir), Error> {
    install_paths_into_temp_dir(uri_str, None).await
}

/// The same, checking out `paths` as well as the manifest.
///
/// The distinction decides what a later pull can report: install registers the
/// manifest and not the files, so a path this copy does not track falls outside
/// the touch set and a remote change to it moves nothing — correctly silent, and
/// invisible to a test that installed no paths.
#[cfg(test)]
pub async fn install_paths_into_temp_dir(
    uri_str: &str,
    paths: Option<Vec<std::path::PathBuf>>,
) -> Result<(Model, quilt_rs::InstalledPackage, TempDir), Error> {
    let (model, temp_dir) = Model::from_temp_dir()?;

    model.set_home(temp_dir.path()).await?;

    let output = model
        .install(install::Input {
            namespace: None,
            paths,
            uri: uri_str.to_string(),
        })
        .await?;

    let installed_package = output.get_installed_package();

    tracing::log::debug!(
        "Installed package manifest: {:?}",
        installed_package.manifest().await?
    );

    // We must return `temp_dir` because otherwise it will be dropped and removed
    Ok((model, installed_package, temp_dir))
}

#[cfg(test)]
pub async fn create_model_in_temp_dir() -> Result<(Model, TempDir), Error> {
    let (model, temp_dir) = Model::from_temp_dir()?;
    model.set_home(temp_dir.path()).await?;
    Ok((model, temp_dir))
}