use std::time::{SystemTime, UNIX_EPOCH};
use serde::Serialize;
use kindling_service::{ExportBundleOptions, ImportOptions};
use kindling_types::ScopeIds;
use crate::cli::{ExportArgs, ImportArgs};
use crate::output::{format_json, iso8601_utc};
use crate::{open_service, CliError, CliResult};
#[derive(Serialize)]
struct ExportMetaOutput {
success: bool,
#[serde(rename = "outputPath")]
output_path: String,
stats: StatsOutput,
}
#[derive(Serialize)]
struct StatsOutput {
observations: usize,
capsules: usize,
summaries: usize,
pins: usize,
#[serde(rename = "totalSize")]
total_size: usize,
}
fn now_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
pub fn run_export(args: ExportArgs, via_daemon: bool) -> CliResult {
if via_daemon {
return Err(CliError::Invalid(
"export operates on the database file directly and cannot be routed \
through the daemon (--via-daemon is not supported for export)"
.to_string(),
));
}
let (service, _db) = open_service(args.common.db.as_deref())?;
let timestamp = args.timestamp.unwrap_or_else(now_ms);
let scope = match (args.session.as_deref(), args.repo.as_deref()) {
(None, None) => None,
(session, repo) => Some(ScopeIds {
session_id: session.map(str::to_string),
repo_id: repo.map(str::to_string),
..Default::default()
}),
};
let mut metadata = serde_json::Map::new();
metadata.insert(
"description".to_string(),
serde_json::Value::String("Kindling memory export".to_string()),
);
metadata.insert(
"exportedAt".to_string(),
serde_json::Value::String(iso8601_utc(timestamp)),
);
let bundle = service.export(ExportBundleOptions {
scope,
include_redacted: false,
limit: None,
metadata: Some(metadata),
exported_at: timestamp,
})?;
let stats = bundle.stats()?;
let output_path = args
.output
.clone()
.unwrap_or_else(|| format!("kindling-export-{timestamp}.json"));
let json = bundle.to_json(args.pretty)?;
std::fs::write(&output_path, json)?;
if args.common.json {
let out = ExportMetaOutput {
success: true,
output_path: output_path.clone(),
stats: StatsOutput {
observations: stats.observations,
capsules: stats.capsules,
summaries: stats.summaries,
pins: stats.pins,
total_size: stats.total_size,
},
};
println!("{}", format_json(&out, true)?);
} else {
println!("\nExport successful");
println!("Output: {output_path}");
println!("\nStatistics:");
println!(" Observations: {}", stats.observations);
println!(" Capsules: {}", stats.capsules);
println!(" Summaries: {}", stats.summaries);
println!(" Pins: {}", stats.pins);
println!(
" Size: {:.2} KB\n",
stats.total_size as f64 / 1024.0
);
}
Ok(())
}
pub fn run_import(args: ImportArgs, via_daemon: bool) -> CliResult {
if via_daemon {
return Err(CliError::Invalid(
"import operates on the database file directly and cannot be routed \
through the daemon (--via-daemon is not supported for import)"
.to_string(),
));
}
let (service, _db) = open_service(args.common.db.as_deref())?;
let json = std::fs::read_to_string(&args.file)?;
let bundle = kindling_service::ExportBundle::from_json(&json)?;
let result = service.import(
&bundle,
ImportOptions {
dry_run: args.dry_run,
},
)?;
if args.common.json {
println!("{}", format_json(&result, true)?);
} else {
let phase = if args.dry_run { "Dry run" } else { "Import" };
let status = if result.errors.is_empty() {
"successful"
} else {
"completed with errors"
};
println!("\n{phase} {status}");
println!("\nImported:");
println!(" Observations: {}", result.observations);
println!(" Capsules: {}", result.capsules);
println!(" Summaries: {}", result.summaries);
println!(" Pins: {}", result.pins);
if !result.errors.is_empty() {
println!("\nErrors ({}):", result.errors.len());
for error in &result.errors {
println!(" - {error}");
}
}
println!();
}
Ok(())
}