use anyhow::{Context, Result};
use std::path::Path;
use tracing::{info, warn};
use crate::config::TurboPropConfig;
use crate::error_classification::ErrorType;
use crate::git::GitignoreFilter;
use crate::incremental::{IncrementalStats, IncrementalUpdater};
use crate::index::PersistentChunkIndex;
use crate::model_validation::{validate_instruction_compatibility, validate_model_selection};
use crate::pipeline::{IndexingPipeline, PipelineConfig};
use crate::storage::PersistentIndex;
use crate::watcher::{FileWatcher, SignalHandler};
const ERROR_ICON: &str = "❌";
const HELP_ICON: &str = "💡";
pub async fn execute_index_command(
path: &Path,
config: &TurboPropConfig,
show_progress: bool,
) -> Result<()> {
info!("Executing index command for path: {}", path.display());
let pipeline_config = PipelineConfig::default()
.with_progress(show_progress)
.with_error_handling(true) .with_batch_size(config.embedding.batch_size);
let mut pipeline = IndexingPipeline::new(config.clone(), pipeline_config);
match pipeline.execute_persistent(path).await {
Ok((persistent_index, stats)) => {
info!("Index command completed successfully");
info!("Final index size: {} chunks", persistent_index.len());
info!(
"Index saved to: {}",
persistent_index.storage_path().display()
);
if stats.files_failed > 0 {
warn!(
"⚠️ {} out of {} files failed to process, but indexing completed successfully",
stats.files_failed, stats.files_discovered
);
warn!("Success rate: {:.1}%", stats.success_rate());
}
Ok(())
}
Err(e) => {
Err(e).with_context(|| format!("Index command failed for path: {}", path.display()))
}
}
}
pub async fn build_index(
repo_path: &Path,
config: &TurboPropConfig,
) -> Result<PersistentChunkIndex> {
execute_index_command(
repo_path, config, false, )
.await?;
PersistentChunkIndex::load(repo_path).context("Failed to load newly created index")
}
pub async fn execute_index_command_cli(
path: &Path,
config: &TurboPropConfig,
show_progress: bool,
) -> Result<()> {
validate_index_inputs(path, config).await?;
match execute_index_command(path, config, show_progress).await {
Ok(()) => Ok(()),
Err(e) => {
let user_message = format_user_error(&e, path);
eprintln!("{}", user_message);
Err(e)
}
}
}
async fn validate_index_inputs(path: &Path, config: &TurboPropConfig) -> Result<()> {
if !path.exists() {
anyhow::bail!("Directory does not exist: {}", path.display());
}
if !path.is_dir() {
anyhow::bail!("Path is not a directory: {}", path.display());
}
config
.validate()
.with_context(|| "Invalid configuration provided")?;
if config.embedding.embedding_dimensions == 0 {
anyhow::bail!("Invalid embedding dimensions: cannot be zero");
}
if config.embedding.batch_size == 0 {
anyhow::bail!("Invalid batch size: cannot be zero");
}
if config.embedding.batch_size > config.embedding.batch_size_warning_threshold {
warn!(
"Large batch size ({}) exceeds threshold ({}), this may cause memory issues",
config.embedding.batch_size, config.embedding.batch_size_warning_threshold
);
}
let model_info = validate_model_selection(&config.embedding.model_name)
.await
.with_context(|| {
format!(
"Model validation failed for '{}'",
config.embedding.model_name
)
})?;
validate_instruction_compatibility(&model_info, config.current_instruction.as_deref())
.with_context(|| "Instruction validation failed")?;
Ok(())
}
fn format_error_message(
main_message: &str,
help_tips: &[&str],
technical_details: Option<&anyhow::Error>,
) -> String {
let mut message = format!("{} {}", ERROR_ICON, main_message);
for tip in help_tips {
message.push_str(&format!("\n{} {}", HELP_ICON, tip));
}
if let Some(details) = technical_details {
message.push_str(&format!("\n\nTechnical details: {}", details));
}
message
}
pub async fn execute_watch_command(
path: &Path,
config: &TurboPropConfig,
show_progress: bool,
) -> Result<()> {
info!("Starting watch mode for path: {}", path.display());
let mut persistent_index = if PersistentIndex::exists(path) {
info!("Loading existing index...");
PersistentIndex::load(path)
.with_context(|| format!("Failed to load existing index for: {}", path.display()))?
} else {
info!("Building initial index...");
execute_index_command(path, config, show_progress)
.await
.context("Failed to build initial index for watch mode")?;
PersistentIndex::load(path).with_context(|| {
format!("Failed to load newly created index for: {}", path.display())
})?
};
let gitignore_filter = GitignoreFilter::new(path)
.with_context(|| format!("Failed to create gitignore filter for: {}", path.display()))?;
let mut file_watcher = FileWatcher::new(path, gitignore_filter)
.with_context(|| format!("Failed to create file watcher for: {}", path.display()))?;
let mut incremental_updater = IncrementalUpdater::new(config.clone(), path)
.await
.context("Failed to initialize incremental updater")?;
let signal_handler = SignalHandler::new().context("Failed to set up signal handler")?;
if show_progress {
println!(
"✓ Watching for changes in {} (Press Ctrl+C to stop)",
path.display()
);
println!(" Initial index: {} chunks", persistent_index.len());
}
let mut total_stats = IncrementalStats::default();
tokio::select! {
_ = signal_handler.wait_for_shutdown() => {
info!("Received shutdown signal, stopping watch mode");
}
_ = async {
while let Some(batch) = file_watcher.next_batch().await {
if show_progress {
println!("🔄 Processing {} file changes...", batch.events.len());
}
match incremental_updater.process_batch(&batch, &mut persistent_index).await {
Ok(stats) => {
if show_progress && (stats.files_added > 0 || stats.files_modified > 0 || stats.files_removed > 0) {
println!(
"✓ Updated index - {} added, {} modified, {} removed ({} chunks)",
stats.files_added,
stats.files_modified,
stats.files_removed,
persistent_index.len()
);
}
total_stats.merge(stats);
}
Err(e) => {
warn!("Failed to process file changes: {}", e);
if show_progress {
eprintln!("⚠️ Error processing changes: {}", e);
}
}
}
}
} => {
info!("File watcher closed, stopping watch mode");
}
}
info!("Watch mode shutting down...");
info!(
"Total changes processed - added: {}, modified: {}, removed: {}",
total_stats.files_added, total_stats.files_modified, total_stats.files_removed
);
if show_progress {
println!("👋 Watch mode stopped");
println!(" Total files processed: {}", total_stats.files_processed);
println!(" Files added: {}", total_stats.files_added);
println!(" Files modified: {}", total_stats.files_modified);
println!(" Files removed: {}", total_stats.files_removed);
println!(" Final index size: {} chunks", persistent_index.len());
}
Ok(())
}
pub async fn execute_watch_command_cli(
path: &Path,
config: &TurboPropConfig,
show_progress: bool,
) -> Result<()> {
validate_index_inputs(path, config).await?;
match execute_watch_command(path, config, show_progress).await {
Ok(()) => Ok(()),
Err(e) => {
let user_message = format_watch_error(&e, path);
eprintln!("{}", user_message);
Err(e)
}
}
}
fn format_watch_error(error: &anyhow::Error, path: &Path) -> String {
let error_str = error.to_string().to_lowercase();
if error_str.contains("permission denied") {
format_error_message(
&format!("Cannot watch directory: {}", path.display()),
&[
"File system watching requires read permissions for the directory and its contents",
"Make sure you have appropriate permissions to watch this directory",
"Try running with elevated permissions if necessary",
],
Some(error),
)
} else if error_str.contains("too many open files") {
format_error_message(
"File system resource limit reached",
&[
"Your system has reached the maximum number of file handles",
"Try watching a smaller directory or increase the system file handle limit",
"On macOS/Linux, you can increase limits with 'ulimit -n'",
],
Some(error),
)
} else if error_str.contains("no such file") || error_str.contains("not found") {
format_error_message(
&format!("Directory not found: {}", path.display()),
&[
"The specified directory does not exist",
"Make sure the path is correct and the directory exists",
"Check that the directory wasn't moved or deleted",
],
Some(error),
)
} else {
format_error_message(
&format!("Watch mode failed for '{}'", path.display()),
&[
"An unexpected error occurred while setting up file watching",
"Try running with --verbose for more detailed error information",
"Make sure the directory is accessible and you have read permissions",
],
Some(error),
)
}
}
fn format_user_error(error: &anyhow::Error, path: &Path) -> String {
let error_type = ErrorType::classify(error);
let error_info = error_type.get_info();
let message = match error_type {
ErrorType::NoFilesFound => format!("No files found to index in '{}'", path.display()),
ErrorType::Generic => format!("Indexing failed for '{}'", path.display()),
_ => error_info.message.to_string(),
};
if matches!(error_type, ErrorType::PermissionDenied) {
let path_help = format!(
"Make sure you have read permissions for the directory: {}",
path.display()
);
let mut help_with_path = vec![path_help.as_str()];
help_with_path.extend_from_slice(error_info.help_texts);
format_error_message(&message, &help_with_path, Some(error))
} else {
let error_ref = if matches!(error_type, ErrorType::NoFilesFound) {
None
} else {
Some(error)
};
format_error_message(&message, error_info.help_texts, error_ref)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::TurboPropConfig;
use std::fs;
use tempfile::TempDir;
fn create_test_file(dir: &Path, name: &str, content: &str) -> std::path::PathBuf {
let file_path = dir.join(name);
fs::write(&file_path, content).unwrap();
file_path
}
#[tokio::test]
async fn test_validate_index_inputs() {
let temp_dir = TempDir::new().unwrap();
let config = TurboPropConfig::default();
assert!(validate_index_inputs(temp_dir.path(), &config)
.await
.is_ok());
let non_existent = temp_dir.path().join("non_existent");
assert!(validate_index_inputs(&non_existent, &config).await.is_err());
let file_path = create_test_file(temp_dir.path(), "test.txt", "content");
assert!(validate_index_inputs(&file_path, &config).await.is_err());
}
#[tokio::test]
async fn test_validate_config() {
let temp_dir = TempDir::new().unwrap();
let mut bad_config = TurboPropConfig::default();
bad_config.embedding.embedding_dimensions = 0;
assert!(validate_index_inputs(temp_dir.path(), &bad_config)
.await
.is_err());
let mut bad_config = TurboPropConfig::default();
bad_config.embedding.batch_size = 0;
assert!(validate_index_inputs(temp_dir.path(), &bad_config)
.await
.is_err());
}
#[tokio::test]
async fn test_execute_index_command_with_empty_dir() {
let temp_dir = TempDir::new().unwrap();
let config = TurboPropConfig::default();
let result = execute_index_command(temp_dir.path(), &config, false).await;
assert!(result.is_err());
let error = result.unwrap_err();
let mut error_chain = vec![error.to_string()];
let mut current = error.source();
while let Some(source) = current {
error_chain.push(source.to_string());
current = source.source();
}
let contains_no_files_found = error_chain.iter().any(|msg| msg.contains("No files found"));
assert!(
contains_no_files_found,
"Expected error chain to contain 'No files found', got: {:?}",
error_chain
);
}
#[test]
fn test_format_user_error() {
let temp_path = Path::new("/test/path");
let error = anyhow::anyhow!("No files found to index");
let formatted = format_user_error(&error, temp_path);
assert!(formatted.contains("No files found"));
assert!(formatted.contains("💡"));
let error = anyhow::anyhow!("Permission denied accessing files");
let formatted = format_user_error(&error, temp_path);
assert!(formatted.contains("Permission denied"));
assert!(formatted.contains("💡"));
let error = anyhow::anyhow!("Some unknown error");
let formatted = format_user_error(&error, temp_path);
assert!(formatted.contains("Indexing failed"));
assert!(formatted.contains("💡"));
}
#[tokio::test]
async fn test_execute_index_command_with_files() {
if std::env::var("OFFLINE_TESTS").is_ok() || std::env::var("CI").is_ok() {
return;
}
let temp_dir = TempDir::new().unwrap();
let config = TurboPropConfig::default();
create_test_file(
temp_dir.path(),
"test1.txt",
"Hello world this is test content",
);
create_test_file(
temp_dir.path(),
"test2.rs",
"fn main() { println!(\"Hello\"); }",
);
let result = execute_index_command(temp_dir.path(), &config, false).await;
if let Err(e) = result {
let error_str = e.to_string();
eprintln!("DEBUG: Actual error message: '{}'", error_str);
assert!(
!error_str.is_empty(),
"Error message should not be empty: {}",
error_str
);
}
}
}