1use 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}