From dc66b34de11bc2bd2ce73c55cc671cfd9dd6d461 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 11 Sep 2025 01:14:10 +0000 Subject: [PATCH] feat: Add file and index commands to CLI Co-authored-by: ijamespine --- apps/cli/src/main.rs | 233 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 224 insertions(+), 9 deletions(-) diff --git a/apps/cli/src/main.rs b/apps/cli/src/main.rs index f90154e08..3db86f26a 100644 --- a/apps/cli/src/main.rs +++ b/apps/cli/src/main.rs @@ -33,6 +33,9 @@ enum Commands { /// File operations #[command(subcommand)] File(FileCommands), + /// Indexing operations + #[command(subcommand)] + Index(IndexCommands), } #[derive(Subcommand, Debug)] @@ -45,6 +48,12 @@ enum LibraryCommands { enum FileCommands { /// Copy files Copy(FileCopyArgs), + /// Delete files + Delete(FileDeleteArgs), + /// Validate files + Validate(FileValidateArgs), + /// Detect duplicate files + Dedupe(FileDedupeArgs), } #[derive(Parser, Debug, Clone)] @@ -76,19 +85,169 @@ struct FileCopyArgs { impl FileCopyArgs { fn to_input(&self) -> sd_core::ops::files::copy::input::FileCopyInput { use sd_core::ops::files::copy::input::{CopyMethod, FileCopyInput}; - FileCopyInput { - library_id: None, - sources: self.sources.clone(), - destination: self.destination.clone(), - overwrite: self.overwrite, - verify_checksum: self.verify_checksum, - preserve_timestamps: self.preserve_timestamps, - move_files: self.move_files, - copy_method: CopyMethod::Auto, + let mut input = FileCopyInput::new(self.sources.clone(), self.destination.clone()) + .with_overwrite(self.overwrite) + .with_verification(self.verify_checksum) + .with_timestamp_preservation(self.preserve_timestamps) + .with_move(self.move_files) + .with_copy_method(CopyMethod::Auto); + input + } +} + +#[derive(Parser, Debug, Clone)] +struct FileDeleteArgs { + /// Files or directories to delete (one or more) + pub targets: Vec, + + /// Permanently delete instead of moving to trash + #[arg(long, default_value_t = false)] + pub permanent: bool, + + /// Delete directories recursively + #[arg(long, default_value_t = true)] + pub recursive: bool, +} + +impl FileDeleteArgs { + fn to_input(&self) -> sd_core::ops::files::delete::input::FileDeleteInput { + use sd_core::domain::addressing::{SdPath, SdPathBatch}; + use sd_core::ops::files::delete::input::FileDeleteInput; + let paths = self + .targets + .iter() + .cloned() + .map(SdPath::local) + .collect::>(); + FileDeleteInput { + targets: SdPathBatch::new(paths), + permanent: self.permanent, + recursive: self.recursive, } } } +#[derive(Parser, Debug, Clone)] +struct FileValidateArgs { + /// Paths to validate (one or more) + pub paths: Vec, + + /// Verify checksums during validation + #[arg(long, default_value_t = false)] + pub verify_checksums: bool, + + /// Perform deep scan + #[arg(long, default_value_t = false)] + pub deep_scan: bool, +} + +impl FileValidateArgs { + fn to_input(&self) -> sd_core::ops::files::validation::input::FileValidationInput { + use sd_core::ops::files::validation::input::FileValidationInput; + FileValidationInput { + paths: self.paths.clone(), + verify_checksums: self.verify_checksums, + deep_scan: self.deep_scan, + } + } +} + +#[derive(Debug, Clone, ValueEnum)] +enum DedupeAlgorithmArg { + ContentHash, + SizeOnly, + NameAndSize, + DeepScan, +} + +impl DedupeAlgorithmArg { + fn as_str(&self) -> &'static str { + match self { + Self::ContentHash => "content_hash", + Self::SizeOnly => "size_only", + Self::NameAndSize => "name_and_size", + Self::DeepScan => "deep_scan", + } + } +} + +#[derive(Parser, Debug, Clone)] +struct FileDedupeArgs { + /// Paths to scan for duplicates (one or more) + pub paths: Vec, + + /// Detection algorithm + #[arg(long, value_enum, default_value = "content-hash")] + pub algorithm: DedupeAlgorithmArg, + + /// Similarity threshold (0.0 - 1.0) + #[arg(long, default_value_t = 1.0)] + pub threshold: f64, +} + +impl FileDedupeArgs { + fn to_input(&self) -> sd_core::ops::files::duplicate_detection::input::DuplicateDetectionInput { + use sd_core::ops::files::duplicate_detection::input::DuplicateDetectionInput; + DuplicateDetectionInput { + paths: self.paths.clone(), + algorithm: self.algorithm.as_str().to_string(), + threshold: self.threshold, + } + } +} + +#[derive(Subcommand, Debug)] +enum IndexCommands { + /// Start indexing for one or more paths + Start(IndexStartArgs), +} + +#[derive(Debug, Clone, ValueEnum)] +enum IndexModeArg { Shallow, Content, Deep } + +#[derive(Debug, Clone, ValueEnum)] +enum IndexScopeArg { Current, Recursive } + +impl From for sd_core::ops::indexing::job::IndexMode { + fn from(m: IndexModeArg) -> Self { + use sd_core::ops::indexing::job::IndexMode as M; + match m { IndexModeArg::Shallow => M::Shallow, IndexModeArg::Content => M::Content, IndexModeArg::Deep => M::Deep } + } +} + +impl From for sd_core::ops::indexing::job::IndexScope { + fn from(s: IndexScopeArg) -> Self { + use sd_core::ops::indexing::job::IndexScope as S; + match s { IndexScopeArg::Current => S::Current, IndexScopeArg::Recursive => S::Recursive } + } +} + +#[derive(Parser, Debug, Clone)] +struct IndexStartArgs { + /// Paths to index (one or more) + pub paths: Vec, + + /// Library ID to run indexing in (defaults to the only library if just one exists) + #[arg(long)] + pub library: Option, + + /// Indexing mode + #[arg(long, value_enum, default_value = "content")] + pub mode: IndexModeArg, + + /// Indexing scope + #[arg(long, value_enum, default_value = "recursive")] + pub scope: IndexScopeArg, + + /// Include hidden files + #[arg(long, default_value_t = false)] + pub include_hidden: bool, + + /// Persist results to the database instead of in-memory + #[arg(long, default_value_t = false)] + pub persistent: bool, +} + #[tokio::main] async fn main() -> Result<()> { let cli = Cli::parse(); @@ -134,6 +293,62 @@ async fn main() -> Result<()> { core.action(&input).await?; println!("Copy request submitted"); } + Commands::File(FileCommands::Delete(args)) => { + let input = args.to_input(); + if let Err(errors) = input.validate() { + anyhow::bail!(errors.join("; ")); + } + core.action(&input).await?; + println!("Delete request submitted"); + } + Commands::File(FileCommands::Validate(args)) => { + let input = args.to_input(); + core.action(&input).await?; + println!("Validation request submitted"); + } + Commands::File(FileCommands::Dedupe(args)) => { + let input = args.to_input(); + core.action(&input).await?; + println!("Duplicate detection request submitted"); + } + Commands::Index(IndexCommands::Start(args)) => { + use sd_core::ops::indexing::input::IndexInput; + use sd_core::ops::indexing::job::{IndexMode, IndexPersistence, IndexScope}; + + let library_id = if let Some(id) = args.library { + id + } else { + // If only one library exists, use it; otherwise require --library + let libs: Vec = core + .query(&sd_core::ops::libraries::list::query::ListLibrariesQuery::basic()) + .await?; + match libs.len() { + 0 => anyhow::bail!("No libraries found; specify --library after creating one"), + 1 => libs[0].id, + _ => anyhow::bail!("Multiple libraries found; please specify --library "), + } + }; + + let persistence = if args.persistent { + IndexPersistence::Persistent + } else { + IndexPersistence::Ephemeral + }; + + let input = IndexInput::new(library_id, args.paths.clone()) + .with_mode(IndexMode::from(args.mode.clone())) + .with_scope(IndexScope::from(args.scope.clone())) + .with_include_hidden(args.include_hidden) + .with_persistence(persistence); + + // Validate input + if let Err(errors) = input.validate() { + anyhow::bail!(errors.join("; ")); + } + + core.action(&input).await?; + println!("Indexing request submitted"); + } } Ok(())