use crate::cli::{OutputFormat, ResumeOpts};
use crate::error::{CommandExitCode, Result};
use governor_checkpoint::FileCheckpointStore;
use governor_core::traits::checkpoint_store::{Checkpoint, CheckpointStore};
use serde_json::json;
use std::path::PathBuf;
pub struct ResumeService {
workspace_path: String,
opts: ResumeOpts,
}
impl ResumeService {
pub const fn new(workspace_path: String, opts: ResumeOpts) -> Self {
Self {
workspace_path,
opts,
}
}
pub async fn execute(&self, _format: OutputFormat) -> Result<CommandExitCode> {
let start_time = std::time::Instant::now();
if self.opts.list {
return self.list_checkpoints();
}
if self.opts.clean {
return self.clean_checkpoints();
}
let checkpoint_dir = self.get_checkpoint_dir();
let checkpoint_id = self.get_checkpoint_id().await?;
let store = FileCheckpointStore::new(checkpoint_dir).map_err(|e| {
crate::error::Error::Config(format!("Failed to create checkpoint store: {e}"))
})?;
let checkpoint = self.load_checkpoint(&store).await?;
if checkpoint.id != checkpoint_id {
return Err(crate::error::Error::Config(format!(
"Checkpoint mismatch: expected {}, found {}",
checkpoint_id, checkpoint.id
)));
}
self.print_response(&checkpoint, start_time)?;
Ok(CommandExitCode::Success)
}
fn get_checkpoint_dir(&self) -> PathBuf {
PathBuf::from(&self.workspace_path)
.join(".cargo-governor")
.join("checkpoints")
}
async fn get_checkpoint_id(&self) -> Result<String> {
if let Some(ref id) = self.opts.checkpoint {
return Ok(id.clone());
}
let checkpoint_dir = self.get_checkpoint_dir();
let store = FileCheckpointStore::new(checkpoint_dir).map_err(|e| {
crate::error::Error::Config(format!("Failed to create checkpoint store: {e}"))
})?;
if let Some(checkpoint) = store
.load()
.await
.map_err(|e| crate::error::Error::Config(format!("Failed to load checkpoint: {e}")))?
{
Ok(checkpoint.id)
} else {
Err(crate::error::Error::Config(
"No checkpoint found to resume from".to_string(),
))
}
}
async fn load_checkpoint(&self, store: &FileCheckpointStore) -> Result<Checkpoint> {
store
.load()
.await
.map_err(|e| crate::error::Error::Config(format!("Failed to load checkpoint: {e}")))?
.ok_or_else(|| crate::error::Error::Config("Checkpoint not found".to_string()))
}
fn print_response(
&self,
checkpoint: &Checkpoint,
start_time: std::time::Instant,
) -> Result<()> {
let response = json!({
"success": true,
"command": "resume",
"workspace": self.workspace_path,
"result": {
"checkpoint": {
"id": checkpoint.id,
"workflow_id": checkpoint.workflow_id,
"step_index": checkpoint.step_index,
"completed_steps": checkpoint.completed_steps,
"timestamp": checkpoint.timestamp.to_rfc3339(),
},
"action": "To resume, run: cargo-governor release --resume",
},
"metrics": {
"execution_time_ms": start_time.elapsed().as_millis(),
}
});
println!("{}", serde_json::to_string_pretty(&response).unwrap());
Ok(())
}
fn list_checkpoints(&self) -> Result<CommandExitCode> {
let checkpoint_dir = self.get_checkpoint_dir();
if !checkpoint_dir.exists() {
let response = json!({
"success": true,
"command": "resume",
"result": {
"message": "No checkpoints found",
"checkpoints": [],
}
});
println!("{}", serde_json::to_string_pretty(&response).unwrap());
return Ok(CommandExitCode::Success);
}
let mut checkpoints = self.load_all_checkpoints(&checkpoint_dir)?;
checkpoints.sort_by(|a, b| {
let a_time = a["timestamp"].as_str().unwrap_or("");
let b_time = b["timestamp"].as_str().unwrap_or("");
b_time.cmp(a_time)
});
let response = json!({
"success": true,
"command": "resume",
"result": {
"checkpoints": checkpoints,
"count": checkpoints.len(),
}
});
println!("{}", serde_json::to_string_pretty(&response).unwrap());
Ok(CommandExitCode::Success)
}
fn load_all_checkpoints(&self, checkpoint_dir: &PathBuf) -> Result<Vec<serde_json::Value>> {
let mut checkpoints = Vec::new();
let entries = std::fs::read_dir(checkpoint_dir).map_err(|e| {
crate::error::Error::Io(format!("Failed to read checkpoint directory: {e}"))
})?;
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
let content = std::fs::read_to_string(&path).map_err(|e| {
crate::error::Error::Io(format!("Failed to read checkpoint file: {e}"))
})?;
if let Ok(checkpoint) = serde_json::from_str::<Checkpoint>(&content) {
checkpoints.push(json!({
"id": checkpoint.id,
"workflow_id": checkpoint.workflow_id,
"step_index": checkpoint.step_index,
"completed_steps": checkpoint.completed_steps,
"timestamp": checkpoint.timestamp.to_rfc3339(),
"resume_command": format!("cargo-governor release --resume --checkpoint {}", checkpoint.id),
}));
}
}
}
Ok(checkpoints)
}
fn clean_checkpoints(&self) -> Result<CommandExitCode> {
let checkpoint_dir = self.get_checkpoint_dir();
if !checkpoint_dir.exists() {
let response = json!({
"success": true,
"command": "resume",
"result": {
"message": "No checkpoints to clean",
"removed": 0,
}
});
println!("{}", serde_json::to_string_pretty(&response).unwrap());
return Ok(CommandExitCode::Success);
}
let removed = self.remove_old_checkpoints(&checkpoint_dir)?;
let response = json!({
"success": true,
"command": "resume",
"result": {
"message": format!("Removed {} old checkpoints", removed),
"removed": removed,
}
});
println!("{}", serde_json::to_string_pretty(&response).unwrap());
Ok(CommandExitCode::Success)
}
fn remove_old_checkpoints(&self, checkpoint_dir: &PathBuf) -> Result<usize> {
let mut removed = 0usize;
let entries = std::fs::read_dir(checkpoint_dir).map_err(|e| {
crate::error::Error::Io(format!("Failed to read checkpoint directory: {e}"))
})?;
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json")
&& let Ok(metadata) = std::fs::metadata(&path)
&& let Ok(modified) = metadata.modified()
{
let age = chrono::Utc::now()
.signed_duration_since(chrono::DateTime::<chrono::Utc>::from(modified));
if age.num_days() > 30 {
std::fs::remove_file(&path).map_err(|e| {
crate::error::Error::Io(format!("Failed to remove checkpoint: {e}"))
})?;
removed = removed.saturating_add(1);
}
}
}
Ok(removed)
}
}