use super::commands::{Cli, Commands};
use super::output::*;
use crate::api::{BackupStore, GraphStore, MaintenanceStore, PurgeRequest};
use crate::application::AsobiRuntime;
use crate::paths::AsobiPaths;
use anyhow::Result;
use clap::CommandFactory;
use tracing::info;
pub(crate) fn run_cli(cli: Cli) -> Result<()> {
if let Commands::Completions { shell } = cli.command {
let mut command = Cli::command();
let shell: clap_complete::Shell = shell.into();
clap_complete::generate(shell, &mut command, "asobi", &mut std::io::stdout());
return Ok(());
}
if let Commands::Schema { command } = cli.command {
emit_schema(command.as_deref())?;
return Ok(());
}
if let Commands::Init { local } = cli.command {
let cwd = std::env::current_dir()?;
let target = if local {
crate::init::InitTarget::Local
} else {
crate::init::InitTarget::Xdg
};
let report = crate::init::init_workspace(target, &cwd)?;
print_init_report(&report);
return Ok(());
}
let paths = AsobiPaths::resolve();
let runtime = AsobiRuntime::open_default()?;
if let Commands::Restore { ref file, force } = cli.command {
runtime
.into_storage()
.restore(std::path::PathBuf::from(file), force)?;
return Ok(());
}
let backend = runtime.storage();
let json = cli.json;
match cli.command {
Commands::Compact { older_than } => {
let pruned = crate::compact::prune_old_sessions(
&paths.topics_dir.to_string_lossy(),
older_than,
)?;
info!("Pruned {} old session files.", pruned);
let synced = crate::compact::sync_graph_to_markdown(backend)?;
info!("Done. Synced {} entities to Markdown.", synced);
}
Commands::Purge {
entity_types,
statuses,
older_than,
apply,
dry_run,
} => {
let report = backend.purge(PurgeRequest {
entity_types,
statuses,
older_than_days: older_than,
apply: apply && !dry_run,
})?;
if json {
print_json(report)?;
} else {
let action = if report.dry_run {
"would purge"
} else {
"purged"
};
println!(
"Purge {}: {} candidate(s), {} entity/entities {}.",
if report.dry_run {
"preview"
} else {
"complete"
},
report.candidates.len(),
report.deleted,
action
);
for candidate in &report.candidates {
println!(
" {} [{} / {}] last activity {} · {} observations · {} relations",
candidate.name,
candidate.entity_type,
candidate.status,
candidate.last_activity,
candidate.observations,
candidate.relations
);
}
if report.dry_run && !report.candidates.is_empty() {
println!("Re-run with --apply to delete these entities.");
}
}
}
Commands::Tasks { subcommand } => crate::tasks::run(backend, subcommand, json)?,
Commands::Skills { subcommand } => super::skills::run(backend, &paths, subcommand)?,
command => super::graph::run(backend, command, json)?,
}
Ok(())
}
pub(crate) fn import_graph(store: &impl GraphStore, graph: crate::model::Graph) -> Result<()> {
let mut entities = Vec::with_capacity(graph.entities.len());
let mut truths = Vec::new();
for entity in graph.entities {
let name = entity.name;
truths.extend(
entity
.truths
.into_iter()
.map(|(key, value)| (name.clone(), key, value)),
);
entities.push(crate::model::EntityInput {
name,
entity_type: entity.entity_type,
observations: entity.observations,
});
}
if !entities.is_empty() {
store.create_entities(entities)?;
for (name, key, value) in truths {
store.truth_upsert(&name, &key, &value)?;
}
}
if !graph.relations.is_empty() {
store.create_relations(graph.relations)?;
}
Ok(())
}
fn print_init_report(report: &crate::init::InitReport) {
let label = match report.target {
crate::init::InitTarget::Xdg => "Initialised Asobi workspace (XDG)",
crate::init::InitTarget::Local => "Initialised Asobi workspace (project-local)",
};
println!("{}", label);
for dir in &report.created_dirs {
println!(" created {}", dir.display());
}
for dir in &report.skipped_dirs {
println!(" exists {}", dir.display());
}
if let Some(path) = &report.wrote_config {
println!(" wrote {}", path.display());
} else if let Some(path) = &report.config_existed {
println!(" exists {}", path.display());
}
}
#[cfg(test)]
mod tests {
use super::import_graph;
use crate::api::{GraphStore, MaintenanceStore};
use crate::cli::runtime::validate_git_url;
use crate::storage::Storage;
use tempfile::tempdir;
#[test]
fn git_url_validator_rejects_option_and_command_urls() {
assert!(validate_git_url("-upload-pack=x").is_err());
assert!(validate_git_url("ext::sh -c id").is_err());
}
#[test]
fn git_url_validator_accepts_supported_urls() {
for url in [
"https://example.com/repo.git",
"ssh://example.com/repo.git",
"git://example.com/repo.git",
"file:///tmp/repo",
"git@example.com:repo.git",
] {
assert!(validate_git_url(url).is_ok(), "expected valid URL: {url}");
}
}
#[test]
fn import_graph_round_trips_truths() {
let dir = tempdir().unwrap();
unsafe {
std::env::set_var(
crate::paths::ENV_DATABASE_URL,
dir.path().join("test.db").to_str().unwrap(),
);
}
let backend = Storage::open_default().unwrap();
backend
.create_entities(vec![crate::model::EntityInput {
name: "project".to_string(),
entity_type: "task".to_string(),
observations: vec!["ship it".to_string()],
}])
.unwrap();
backend.truth_upsert("project", "status", "READY").unwrap();
let exported = backend.read_graph_full().unwrap();
backend.reset().unwrap();
import_graph(&backend, exported).unwrap();
let imported = backend.read_graph_full().unwrap();
assert_eq!(
imported.entities[0].truths.get("status"),
Some(&"READY".to_string())
);
}
}