use std::fs;
use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc};
use serde_json::{Value, json};
use netsky_core::paths::handoff_archive_dir;
pub fn run(which: &str, arg: Option<&str>, limit: usize, json: bool) -> netsky_core::Result<()> {
let dir = handoff_archive_dir();
if !dir.exists() {
return render_empty(
&format!(
"archive not present: {} (no restart since the archive-feature shipped)",
dir.display()
),
&dir,
json,
);
}
let files = newest_first_jsons(&dir)?;
if files.is_empty() {
return render_empty("(archive empty)", &dir, json);
}
match which {
"list" => list(&files, limit, json),
"last" => dump(&files[0], json),
"show" => {
let target = arg.unwrap_or("last");
if target == "last" {
dump(&files[0], json)
} else if target.chars().all(|c| c.is_ascii_digit()) {
let idx: usize = target.parse()?;
if idx == 0 || idx > files.len() {
netsky_core::bail!("index {idx} out of range (have {} entries)", files.len());
}
dump(&files[idx - 1], json)
} else {
netsky_core::bail!("show takes `last` or a 1-based index, got '{target}'")
}
}
"prune" => prune(&files, limit, json),
s if s.chars().all(|c| c.is_ascii_digit()) => {
let idx: usize = s.parse()?;
if idx == 0 || idx > files.len() {
netsky_core::bail!("index {idx} out of range (have {} entries)", files.len());
}
dump(&files[idx - 1], json)
}
other => netsky_core::bail!(
"unknown selector '{other}' (use: list | show [N|last] | prune | <N>)"
),
}
}
fn prune(files: &[PathBuf], keep: usize, json: bool) -> netsky_core::Result<()> {
let to_drop: Vec<&PathBuf> = files.iter().skip(keep).collect();
let mut removed = Vec::with_capacity(to_drop.len());
for path in &to_drop {
if let Err(err) = fs::remove_file(path) {
netsky_core::bail!("remove {}: {err}", path.display());
}
removed.push(path.display().to_string());
}
if json {
let envelope = json!({
"command": "handoffs",
"status": "green",
"summary": format!("pruned {} (kept {})", removed.len(), keep),
"generated_at": now_utc(),
"data": {
"archive_dir": handoff_archive_dir().display().to_string(),
"kept": files.len().min(keep),
"removed_count": removed.len(),
"removed": removed,
},
});
println!("{}", serde_json::to_string_pretty(&envelope)?);
} else {
println!(
"pruned {} entries; kept newest {}",
removed.len(),
files.len().min(keep)
);
}
Ok(())
}
fn render_empty(message: &str, dir: &Path, json: bool) -> netsky_core::Result<()> {
if json {
let envelope = envelope_list(message, dir, &[]);
println!("{}", serde_json::to_string_pretty(&envelope)?);
} else {
println!("{message}");
}
Ok(())
}
fn newest_first_jsons(dir: &PathBuf) -> netsky_core::Result<Vec<PathBuf>> {
let mut entries: Vec<(std::time::SystemTime, PathBuf)> = Vec::new();
for e in fs::read_dir(dir)? {
let e = e?;
let p = e.path();
if p.extension().and_then(|s| s.to_str()) != Some("json") {
continue;
}
let mtime = e.metadata()?.modified()?;
entries.push((mtime, p));
}
entries.sort_by(|a, b| b.0.cmp(&a.0));
Ok(entries.into_iter().map(|(_, p)| p).collect())
}
fn list(files: &[PathBuf], limit: usize, json: bool) -> netsky_core::Result<()> {
if json {
let items = files
.iter()
.take(limit)
.map(handoff_item)
.collect::<netsky_core::Result<Vec<_>>>()?;
let envelope = envelope_list(
&format!("{} handoff(s) archived", files.len()),
&handoff_archive_dir(),
&items,
);
println!("{}", serde_json::to_string_pretty(&envelope)?);
return Ok(());
}
for f in files.iter().take(limit) {
let md = fs::metadata(f)?;
let ts: DateTime<Utc> = md.modified()?.into();
let size = md.len();
let preview = preview_of(f).unwrap_or_else(|| "(preview unavailable)".to_string());
println!(
" {} {:>5}B {preview}",
ts.format("%Y-%m-%dT%H:%M:%SZ"),
size
);
}
Ok(())
}
fn dump(path: &PathBuf, json: bool) -> netsky_core::Result<()> {
let content = fs::read_to_string(path)?;
let parsed: Option<Value> = serde_json::from_str(&content).ok();
if json {
let envelope = json!({
"command": "handoffs",
"status": "green",
"summary": format!("handoff at {}", path.display()),
"generated_at": now_utc(),
"data": {
"path": path.display().to_string(),
"envelope": parsed.clone().unwrap_or_else(|| Value::String(content.clone())),
},
});
println!("{}", serde_json::to_string_pretty(&envelope)?);
return Ok(());
}
match parsed {
Some(v) => println!("{}", serde_json::to_string_pretty(&v)?),
None => println!("{content}"),
}
Ok(())
}
fn handoff_item(path: &PathBuf) -> netsky_core::Result<Value> {
let md = fs::metadata(path)?;
let ts: DateTime<Utc> = md.modified()?.into();
Ok(json!({
"path": path.display().to_string(),
"ts_utc": ts.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
"size_bytes": md.len(),
"preview": preview_of(path),
}))
}
fn envelope_list(summary: &str, dir: &Path, items: &[Value]) -> Value {
json!({
"command": "handoffs",
"status": "green",
"summary": summary,
"generated_at": now_utc(),
"data": {
"archive_dir": dir.display().to_string(),
"items": items,
"count": items.len(),
},
})
}
fn now_utc() -> String {
Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string()
}
fn preview_of(path: &PathBuf) -> Option<String> {
let content = fs::read_to_string(path).ok()?;
let v: Value = serde_json::from_str(&content).ok()?;
let text = v.get("text")?.as_str()?;
let line: String = text.chars().take(70).collect::<String>().replace('\n', " ");
Some(line)
}