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::PathBuf;

use quilt_uri::Namespace;

use crate::cli::Error;
use crate::cli::model::Commands;
use crate::cli::output::Render;
use crate::cli::output::Std;

#[derive(Debug)]
pub struct Input {
    pub namespace: Namespace,
    pub source: Option<PathBuf>,
    pub message: Option<String>,
}

#[derive(Debug)]
pub struct Output {
    pub installed_package: quilt_rs::InstalledPackage,
    /// What the ignore rules kept out of the source folder.
    pub left_out: quilt_rs::flow::LeftOut,
}

impl std::fmt::Display for Output {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Created package \"{}\"",
            self.installed_package.namespace
        )?;
        if !self.left_out.is_empty() {
            write!(f, "\n{}", self.left_out)?;
        }
        Ok(())
    }
}

impl Render for Output {
    fn to_json(&self) -> serde_json::Value {
        serde_json::json!({
            "namespace": self.installed_package.namespace.to_string(),
            "ignored": {
                "files": self.left_out.files.len(),
                "folders": self.left_out.dirs.len(),
            },
        })
    }
}

pub async fn command(m: impl Commands, args: Input) -> Std {
    Std::from_result(m.create(args).await)
}

pub async fn model(
    local_domain: &quilt_rs::LocalDomain,
    Input {
        namespace,
        source,
        message,
    }: Input,
) -> Result<Output, Error> {
    let (installed_package, left_out) = local_domain
        .create_package(namespace, source, message)
        .await?;
    Ok(Output {
        installed_package,
        left_out,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    use test_log::test;

    use crate::cli::model::create_model_in_temp_dir;
    use crate::cli::output::Render;
    use crate::cli::status;

    #[test(tokio::test)]
    async fn test_create_empty() -> Result<(), Error> {
        let (m, _temp_dir) = create_model_in_temp_dir().await?;

        let output = m
            .create(Input {
                namespace: ("test", "pkg").into(),
                source: None,
                message: None,
            })
            .await?;

        assert_eq!(output.installed_package.namespace.to_string(), "test/pkg");
        Ok(())
    }

    #[test(tokio::test)]
    async fn test_create_then_status_clean() -> Result<(), Error> {
        let (m, _temp_dir) = create_model_in_temp_dir().await?;

        m.create(Input {
            namespace: ("test", "clean").into(),
            source: None,
            message: None,
        })
        .await?;

        let status_output = m
            .status(status::Input {
                namespace: ("test", "clean").into(),
                host_config: None,
                list_ignored: false,
            })
            .await?;

        assert!(
            status_output.status.changes.is_empty(),
            "Freshly created package should have no changes"
        );
        Ok(())
    }

    #[test(tokio::test)]
    async fn test_create_duplicate() -> Result<(), Error> {
        let (m, _temp_dir) = create_model_in_temp_dir().await?;

        m.create(Input {
            namespace: ("test", "dup").into(),
            source: None,
            message: None,
        })
        .await?;

        let result = m
            .create(Input {
                namespace: ("test", "dup").into(),
                source: None,
                message: None,
            })
            .await;

        assert!(result.is_err());
        Ok(())
    }

    #[test(tokio::test)]
    async fn test_create_with_source() -> Result<(), Error> {
        let (m, temp_dir) = create_model_in_temp_dir().await?;

        let source_dir = temp_dir.path().join("my_source");
        std::fs::create_dir_all(&source_dir)?;
        std::fs::write(source_dir.join("data.csv"), "a,b,c\n1,2,3")?;
        std::fs::write(source_dir.join("readme.txt"), "Hello")?;

        let output = m
            .create(Input {
                namespace: ("test", "src").into(),
                source: Some(source_dir),
                message: Some("Import data".to_string()),
            })
            .await?;

        assert_eq!(output.installed_package.namespace.to_string(), "test/src");

        // Verify status is clean after create with source
        let status_output = m
            .status(status::Input {
                namespace: ("test", "src").into(),
                host_config: None,
                list_ignored: false,
            })
            .await?;

        assert!(
            status_output.status.changes.is_empty(),
            "Package created with source should have clean status"
        );
        Ok(())
    }

    #[test(tokio::test)]
    async fn says_what_the_ignore_rules_left_out() -> Result<(), Error> {
        let (m, temp_dir) = create_model_in_temp_dir().await?;

        let source_dir = temp_dir.path().join("my_source");
        std::fs::create_dir_all(source_dir.join(".git"))?;
        std::fs::write(source_dir.join(".git/config"), "x")?;
        std::fs::write(source_dir.join(".DS_Store"), "x")?;
        std::fs::write(source_dir.join("data.csv"), "a,b,c")?;

        let output = m
            .create(Input {
                namespace: ("test", "left").into(),
                source: Some(source_dir),
                message: None,
            })
            .await?;

        assert_eq!(
            output.to_string(),
            "Created package \"test/left\"\n1 file and 1 folder were left out by ignore rules"
        );
        assert_eq!(
            output.to_json()["ignored"],
            serde_json::json!({"files": 1, "folders": 1})
        );
        Ok(())
    }

    #[test(tokio::test)]
    async fn json_carries_the_namespace() -> Result<(), Error> {
        let (m, _temp_dir) = create_model_in_temp_dir().await?;

        let output = m
            .create(Input {
                namespace: ("test", "pkg").into(),
                source: None,
                message: None,
            })
            .await?;

        assert_eq!(
            output.to_json().to_string(),
            r#"{"namespace":"test/pkg","ignored":{"files":0,"folders":0}}"#
        );
        Ok(())
    }
}