Skip to main content

kaizen/shell/
prompt.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! `kaizen prompt` subcommands: list, show, diff.
3
4use crate::prompt;
5use crate::store::Store;
6use anyhow::{Context, Result};
7use serde::Serialize;
8use std::path::Path;
9
10#[derive(Serialize)]
11struct SnapshotJson {
12    fingerprint: String,
13    captured_at_ms: u64,
14    total_bytes: u64,
15    files: Vec<FileJson>,
16}
17
18#[derive(Serialize)]
19struct FileJson {
20    path: String,
21    sha256: String,
22    bytes: u64,
23}
24
25pub fn cmd_prompt_list(workspace: Option<&Path>, json: bool) -> Result<()> {
26    let ws = workspace_path(workspace)?;
27    let store = open_store(ws.as_path())?;
28    let snaps = store.list_prompt_snapshots()?;
29    if json {
30        println!("{}", serde_json::to_string_pretty(&to_json_list(&snaps))?);
31        return Ok(());
32    }
33    if snaps.is_empty() {
34        println!("No prompt snapshots recorded yet.");
35        return Ok(());
36    }
37    for s in &snaps {
38        let short = &s.fingerprint[..8.min(s.fingerprint.len())];
39        println!(
40            "{short}  {}  {} bytes",
41            fmt_ts(s.captured_at_ms),
42            s.total_bytes
43        );
44    }
45    Ok(())
46}
47
48pub fn cmd_prompt_show(fingerprint: &str, workspace: Option<&Path>, json: bool) -> Result<()> {
49    let ws = workspace_path(workspace)?;
50    let store = open_store(ws.as_path())?;
51    let snap = store
52        .get_prompt_snapshot(fingerprint)?
53        .with_context(|| format!("snapshot not found: {fingerprint}"))?;
54    if json {
55        println!("{}", serde_json::to_string_pretty(&to_json(&snap))?);
56        return Ok(());
57    }
58    println!("fingerprint: {}", snap.fingerprint);
59    println!("captured:    {}", fmt_ts(snap.captured_at_ms));
60    println!("total_bytes: {}", snap.total_bytes);
61    println!("files:");
62    for f in snap.files() {
63        println!("  {} ({} bytes)", f.path, f.bytes);
64    }
65    Ok(())
66}
67
68pub fn cmd_prompt_diff(a: &str, b: &str, workspace: Option<&Path>) -> Result<()> {
69    let ws = workspace_path(workspace)?;
70    let store = open_store(ws.as_path())?;
71    let snap_a = store
72        .get_prompt_snapshot(a)?
73        .with_context(|| format!("snapshot not found: {a}"))?;
74    let snap_b = store
75        .get_prompt_snapshot(b)?
76        .with_context(|| format!("snapshot not found: {b}"))?;
77    let diff = prompt::diff::diff(&snap_a, &snap_b);
78    if diff.is_empty() {
79        println!("No changes between {a} and {b}.");
80        return Ok(());
81    }
82    for p in &diff.added {
83        println!("+ {p}");
84    }
85    for p in &diff.removed {
86        println!("- {p}");
87    }
88    for p in &diff.changed {
89        println!("~ {p}");
90    }
91    Ok(())
92}
93
94fn to_json(s: &prompt::PromptSnapshot) -> SnapshotJson {
95    SnapshotJson {
96        fingerprint: s.fingerprint.clone(),
97        captured_at_ms: s.captured_at_ms,
98        total_bytes: s.total_bytes,
99        files: s
100            .files()
101            .into_iter()
102            .map(|f| FileJson {
103                path: f.path,
104                sha256: f.sha256,
105                bytes: f.bytes,
106            })
107            .collect(),
108    }
109}
110
111fn to_json_list(snaps: &[prompt::PromptSnapshot]) -> Vec<SnapshotJson> {
112    snaps.iter().map(to_json).collect()
113}
114
115fn open_store(ws: &Path) -> Result<Store> {
116    let db = ws.join(".kaizen/kaizen.db");
117    Store::open(&db).with_context(|| format!("open store: {}", db.display()))
118}
119
120fn workspace_path(ws: Option<&Path>) -> Result<std::path::PathBuf> {
121    Ok(ws
122        .map(|p| p.to_path_buf())
123        .unwrap_or_else(|| std::env::current_dir().expect("cwd")))
124}
125
126fn fmt_ts(ms: u64) -> String {
127    crate::shell::fmt::fmt_ts(ms)
128}