diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index 0244dee..e62e863 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -1564,8 +1564,74 @@ fn cmd_import(action: ImportAction) -> Result<()> { } } -fn cmd_import_lastpass(_csv: PathBuf) -> Result<()> { - bail!("not implemented yet — Task 8 lands the body") +fn cmd_import_lastpass(csv_path: PathBuf) -> Result<()> { + use std::fs; + use relicario_core::import_lastpass::parse_lastpass_csv; + + let csv_bytes = fs::read(&csv_path) + .with_context(|| format!("failed to read CSV {}", csv_path.display()))?; + + let (items, warnings) = parse_lastpass_csv(&csv_bytes)?; + + if items.is_empty() { + // Print all warnings so the user sees why nothing imported. + for w in &warnings { + print_warning(w); + } + bail!( + "imported 0 items from {} — see warnings above", + csv_path.display() + ); + } + + let vault = crate::session::UnlockedVault::unlock_interactive()?; + let mut manifest = vault.load_manifest()?; + + let total = items.len(); + let mut written_paths: Vec = Vec::with_capacity(items.len() + 1); + + for (idx, item) in items.iter().enumerate() { + vault.save_item(item)?; + manifest.upsert(item); + written_paths.push(format!("items/{}.enc", item.id.as_str())); + + let n = idx + 1; + if n % 50 == 0 || n == total { + eprintln!("[{n}/{total}] importing..."); + } + } + + vault.save_manifest(&manifest)?; + written_paths.push("manifest.enc".into()); + + let path_refs: Vec<&str> = written_paths.iter().map(String::as_str).collect(); + let csv_filename = csv_path + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("lastpass.csv"); + commit_paths( + &vault, + &format!("import: {} items from LastPass ({})", total, csv_filename), + &path_refs, + )?; + + for w in &warnings { + print_warning(w); + } + eprintln!( + "Imported {}, skipped {} (see warnings above)", + total, + warnings.iter().filter(|w| w.message.contains("skipped")).count() + ); + Ok(()) +} + +fn print_warning(w: &relicario_core::import_lastpass::ImportWarning) { + let prefix = match &w.title { + Some(t) => format!("row {} ({}):", w.row, t), + None => format!("row {}:", w.row), + }; + eprintln!("warning: {prefix} {}", w.message); } fn cmd_trash_empty() -> Result<()> {