use crate::{
Config,
util::{Database, Record, hash_file, iter_files, prompt},
};
use anyhow::{Context, Result};
use argh::FromArgs;
use log::{debug, info, warn};
use semantic_search::ApiClient;
#[derive(FromArgs, PartialEq, Eq, Debug)]
#[argh(subcommand, name = "index", help_triggers("-h", "--help"))]
pub struct Index {
#[argh(switch, short = 'y')]
pub yes: bool,
#[argh(switch, short = 'r')]
pub re_embed: bool,
}
#[derive(Debug, Default)]
pub struct IndexSummary {
pub changed: usize,
pub new: usize,
pub deleted: usize,
}
impl Index {
#[allow(clippy::future_not_send, reason = "Main function")]
pub async fn execute(&self, config: Config) -> Result<IndexSummary> {
if self.yes && self.re_embed {
anyhow::bail!("Options -y and -r should not be used together");
}
let mut db = Database::open(".sense/index.db3", false)
.await
.with_context(|| "Failed to open database")?;
let mut summary = IndexSummary::default();
let api = ApiClient::new(&config.api.key, config.api.model)?;
let cwd = std::env::current_dir()?.canonicalize()?;
summary.deleted = db.clean(&cwd).await?;
let files = iter_files(&cwd, &cwd);
for (path, relative) in files {
let hash = hash_file(&path)?;
let relative = relative.to_string();
let existing = db.get(&relative).await?;
let record = if let Some(mut record) = existing {
let hash_changed = record.file_hash != hash;
if hash_changed {
summary.changed += 1;
debug!("[CHANGED] {relative}: {} -> {hash}", record.file_hash);
warn!("Hash of {relative} has changed, consider relabeling");
record.file_hash = hash;
record.file_id = None;
if self.re_embed {
info!("Re-embedding {relative}");
record.embedding = api.embed(&record.label).await?.into();
} else if !self.yes {
println!("Existing label: {}", record.label);
let label = prompt(&format!("Label for {relative} (empty to keep): "))?;
if label.is_empty() {
println!("Label kept as: {}", record.label);
} else {
record.label = label;
println!("Label updated to: {}", record.label);
record.embedding = api.embed(&relative).await?.into();
}
} else {
info!("Skipping {relative}");
}
} else {
debug!("[SAME] {relative}: {hash}");
continue; }
record
} else {
summary.new += 1;
debug!("[NEW] {hash}: {relative}");
warn!("New file: {relative}, consider labeling");
let (label, embedding) = if self.yes {
let label = path.file_stem().unwrap().to_string_lossy();
(label.to_string(), api.embed(&relative).await?.into())
} else {
let label = prompt(&format!("Label for {relative} (empty to use filename): "))?;
if label.is_empty() {
let label = path.file_stem().unwrap().to_string_lossy();
(label.to_string(), api.embed(&relative).await?.into())
} else {
let embedding = api.embed(&relative).await?;
(label, embedding.into())
}
};
Record {
file_path: relative,
file_hash: hash,
file_id: None,
label,
embedding,
}
};
db.insert(record).await?;
}
Ok(summary)
}
}