void-cli 0.0.4

CLI for void — anonymous encrypted source control
//! Create a new shard from files.
//!
//! Creates a shard containing the specified files and outputs the CID.

use std::path::Path;

use serde::Serialize;
use void_core::shard::ShardWriter;
use void_core::store::{FsStore, ObjectStoreExt};

use crate::context::{open_repo, void_err_to_cli};
use crate::output::{run_command, CliError, CliOptions};

/// Command-line arguments for create-shard.
#[derive(Debug)]
pub struct CreateShardArgs {
    /// Files to include in the shard.
    pub files: Vec<String>,
}

/// JSON output for the create-shard command.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateShardOutput {
    /// CID of the created shard.
    pub cid: String,
    /// Number of files in the shard.
    pub file_count: usize,
    /// Uncompressed size of file contents.
    pub uncompressed_size: usize,
    /// Encrypted shard size.
    pub encrypted_size: usize,
}

/// Run the create-shard command.
///
/// # Arguments
///
/// * `cwd` - Current working directory
/// * `args` - Create-shard arguments
/// * `opts` - CLI options
pub fn run(cwd: &Path, args: CreateShardArgs, opts: &CliOptions) -> Result<(), CliError> {
    run_command("create-shard", opts, |ctx| {
        if args.files.is_empty() {
            return Err(CliError::invalid_args("at least one file is required"));
        }

        ctx.progress("Creating shard...");

        let repo = open_repo(cwd)?;
        let vault = repo.vault();
        let objects_dir = repo.void_dir().join("objects");
        let store = FsStore::new(objects_dir).map_err(void_err_to_cli)?;

        let mut writer = ShardWriter::new();

        for file_path in &args.files {
            let full_path = if Path::new(file_path).is_absolute() {
                Path::new(file_path).to_path_buf()
            } else {
                cwd.join(file_path)
            };

            let content = std::fs::read(&full_path)
                .map_err(|e| CliError::not_found(format!("cannot read {}: {}", file_path, e)))?;

            // Use the relative path as the entry name
            writer
                .add_file(file_path, &content)
                .map_err(void_err_to_cli)?;

            ctx.progress(format!("Added: {}", file_path));
        }

        let file_count = writer.file_count() as usize;
        let uncompressed_size = writer.body_size();

        // Finish and encrypt
        let shard_bytes = writer.finish(3).map_err(void_err_to_cli)?;
        let encrypted = vault.seal_shard(&shard_bytes)
            .map_err(|e| CliError::encryption_error(format!("encryption failed: {}", e)))?;
        let encrypted_size = encrypted.as_bytes().len();

        // Store
        let shard_cid = store.put_blob(&encrypted).map_err(void_err_to_cli)?;
        let cid_str = shard_cid.to_string();

        // Human-readable output
        if !ctx.use_json() {
            ctx.info(format!("Created shard: {}", cid_str));
            ctx.info(format!("Files: {}", file_count));
            ctx.info(format!("Uncompressed: {} bytes", uncompressed_size));
            ctx.info(format!("Encrypted: {} bytes", encrypted_size));
        }

        Ok(CreateShardOutput {
            cid: cid_str,
            file_count,
            uncompressed_size,
            encrypted_size,
        })
    })
}