guardy 0.2.4

Fast, secure git hooks in Rust with secret scanning and protected file synchronization
Documentation
use anyhow::Result;
use clap::{Parser, Subcommand};

use crate::{
    cli::banner,
    cli::output,
    sync::{
        manager::{REPOS, SyncManager},
        status::StatusDisplay,
    },
};

#[derive(Parser, Clone)]
#[command(about = "File synchronization from remote repositories")]
pub struct SyncArgs {
    #[command(subcommand)]
    pub command: Option<SyncSubcommand>,

    /// Force update, bypass interactive mode and update all changes without prompting
    #[arg(long)]
    pub force: bool,

    /// Bootstrap from a specific repository (initial setup)
    #[arg(long)]
    pub repo: Option<String>,

    /// Specific version to sync (tag, branch, or commit)
    #[arg(long)]
    pub version: Option<String>,

    /// Source directory in the remote repository to sync from (default: ".")
    #[arg(long)]
    pub source_path: Option<String>,

    /// Destination directory to sync files to (default: ".")
    #[arg(long)]
    pub dest_path: Option<String>,

    /// Include patterns for files to sync (can be specified multiple times)
    #[arg(long, action = clap::ArgAction::Append)]
    pub include: Vec<String>,

    /// Exclude patterns for files to skip (can be specified multiple times)
    #[arg(long, action = clap::ArgAction::Append)]
    pub exclude: Vec<String>,
}

#[derive(Subcommand, Clone)]
pub enum SyncSubcommand {
    /// Show sync status and configuration
    Status,

    /// Update files from configured repositories (interactive by default)
    Update {
        /// Force update, bypass interactive mode and update all changes without prompting
        #[arg(long)]
        force: bool,

        /// Bootstrap from a specific repository (initial setup)
        #[arg(long)]
        repo: Option<String>,

        /// Specific version to sync (tag, branch, or commit)
        #[arg(long)]
        version: Option<String>,

        /// Source directory in the remote repository to sync from (default: ".")
        #[arg(long)]
        source_path: Option<String>,

        /// Destination directory to sync files to (default: ".")
        #[arg(long)]
        dest_path: Option<String>,

        /// Include patterns for files to sync (can be specified multiple times)
        #[arg(long, action = clap::ArgAction::Append)]
        include: Vec<String>,

        /// Exclude patterns for files to skip (can be specified multiple times)
        #[arg(long, action = clap::ArgAction::Append)]
        exclude: Vec<String>,
    },

    /// Show differences between local and remote files (what has drifted)
    Diff,
}

pub async fn execute(args: SyncArgs) -> Result<()> {
    match args.command {
        Some(SyncSubcommand::Status) => execute_status().await,
        Some(SyncSubcommand::Update { force, .. }) => {
            // Prefer subcommand args over main args for force flag
            let final_force = force || args.force;
            execute_update(final_force).await
        }
        Some(SyncSubcommand::Diff) => execute_diff().await,
        // Default to update behavior when no subcommand is provided, using main args
        None => execute_update(args.force).await,
    }
}

async fn execute_status() -> Result<()> {
    // Print banner without context
    banner::print_banner(None);

    let manager = SyncManager::new()?;
    let status_display = StatusDisplay::new(&manager);
    status_display.show_detailed_status()
}

async fn execute_diff() -> Result<()> {
    // Print banner without context
    banner::print_banner(None);

    let mut manager = SyncManager::new()?;

    // Check if we have any configuration
    if REPOS.is_empty() {
        output::styled!("{} No sync configuration found", ("⚠️", "warning_symbol"));
        return Ok(());
    }

    output::styled!("{} Checking for differences...", ("🔍", "info_symbol"));

    // Use the dedicated diff-only method (no interactive prompts)
    manager.show_all_diffs().await?;

    Ok(())
}

async fn execute_update(force: bool) -> Result<()> {
    // Print banner without context
    banner::print_banner(None);

    let mut manager = SyncManager::new()?;
    let interactive = !force;

    let updated_files = manager.update_all_repos(interactive).await?;

    // Show results for force mode
    if force {
        if updated_files.is_empty() {
            output::styled!("<info>  No files were updated");
        } else {
            output::styled!(
                "{} Successfully updated {} files:",
                ("", "success_symbol"),
                (updated_files.len().to_string(), "property")
            );

            for file in &updated_files {
                println!("{}", output::file_path(file.display().to_string()));
            }
        }
    }
    // Interactive mode shows its own summary

    Ok(())
}