Skip to main content

kaizen/shell/
load.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! `kaizen load` — explicit transcript backfill into local workspace stores.
3
4use crate::core::config;
5use crate::shell::cli::{AgentScanStats, scan_all_agents_with_stats};
6use crate::store::Store;
7use anyhow::Result;
8use serde::Serialize;
9use std::path::{Path, PathBuf};
10
11#[derive(Serialize)]
12struct LoadJson {
13    workspace_count: usize,
14    totals: AgentScanStats,
15    workspaces: Vec<LoadWorkspaceJson>,
16}
17
18#[derive(Serialize)]
19struct LoadWorkspaceJson {
20    workspace: String,
21    #[serde(flatten)]
22    stats: AgentScanStats,
23}
24
25pub fn cmd_load(workspace: Option<&Path>, json: bool) -> Result<()> {
26    print!("{}", load_text(workspace, json)?);
27    Ok(())
28}
29
30pub fn load_text(workspace: Option<&Path>, json: bool) -> Result<String> {
31    let roots = load_roots(workspace)?;
32    let mut totals = AgentScanStats::default();
33    let mut rows = Vec::new();
34    for root in roots {
35        let stats = load_one(&root)?;
36        totals.merge(&stats);
37        rows.push(LoadWorkspaceJson {
38            workspace: root.to_string_lossy().to_string(),
39            stats,
40        });
41    }
42    render_load(rows, totals, json)
43}
44
45fn load_roots(workspace: Option<&Path>) -> Result<Vec<PathBuf>> {
46    if let Some(path) = workspace {
47        return Ok(vec![crate::core::paths::canonical(path)]);
48    }
49    let roots = crate::core::workspace::machine_workspaces(None)?;
50    if roots.is_empty() {
51        return crate::core::workspace::resolve(None).map(|p| vec![p]);
52    }
53    Ok(roots)
54}
55
56fn load_one(workspace: &Path) -> Result<AgentScanStats> {
57    let cfg = config::load(workspace)?;
58    let store = Store::open(&crate::core::workspace::db_path(workspace)?)?;
59    let ws = workspace.to_string_lossy().to_string();
60    scan_all_agents_with_stats(workspace, &cfg, &ws, &store)
61}
62
63fn render_load(rows: Vec<LoadWorkspaceJson>, totals: AgentScanStats, json: bool) -> Result<String> {
64    if json {
65        return Ok(format!(
66            "{}\n",
67            serde_json::to_string_pretty(&LoadJson {
68                workspace_count: rows.len(),
69                totals,
70                workspaces: rows,
71            })?
72        ));
73    }
74    Ok(render_text(&rows, &totals))
75}
76
77fn render_text(rows: &[LoadWorkspaceJson], totals: &AgentScanStats) -> String {
78    use std::fmt::Write;
79    let mut out = String::new();
80    writeln!(&mut out, "Loaded {} workspace(s)", rows.len()).unwrap();
81    for row in rows {
82        writeln!(
83            &mut out,
84            "{}: sessions {} events {} agents {}",
85            row.workspace,
86            row.stats.sessions_upserted,
87            row.stats.events_upserted,
88            agents_label(&row.stats),
89        )
90        .unwrap();
91    }
92    writeln!(
93        &mut out,
94        "Total: sessions {} events {} agents {}",
95        totals.sessions_upserted,
96        totals.events_upserted,
97        agents_label(totals),
98    )
99    .unwrap();
100    out
101}
102
103fn agents_label(stats: &AgentScanStats) -> String {
104    if stats.agents.is_empty() {
105        "-".into()
106    } else {
107        stats.agents.iter().cloned().collect::<Vec<_>>().join(",")
108    }
109}