Skip to main content

life_cli/deploy/
state.rs

1//! Deployment state persistence — tracks deployed agents locally.
2//!
3//! State is saved to ~/.life/deployments/{agent-name}.json so that
4//! `life status` and `life destroy` can reference previous deployments.
5
6use std::collections::HashMap;
7use std::path::PathBuf;
8
9use anyhow::{Context, Result};
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12
13use super::backend::DeployedService;
14
15/// Persisted state of a deployed agent.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct DeploymentState {
18    pub agent_name: String,
19    pub project_name: String,
20    pub target: String,
21    pub project_id: String,
22    pub environment_id: String,
23    pub services: HashMap<String, DeployedService>,
24    pub deployed_at: DateTime<Utc>,
25    pub template_name: String,
26}
27
28impl DeploymentState {
29    /// Directory where deployment states are stored.
30    fn state_dir() -> Result<PathBuf> {
31        let home = dirs::home_dir().context("cannot determine home directory")?;
32        let dir = home.join(".life").join("deployments");
33        std::fs::create_dir_all(&dir)
34            .with_context(|| format!("failed to create {}", dir.display()))?;
35        Ok(dir)
36    }
37
38    /// Path for a specific agent's state file.
39    fn state_path(agent_name: &str) -> Result<PathBuf> {
40        Ok(Self::state_dir()?.join(format!("{agent_name}.json")))
41    }
42
43    /// Save this deployment state to disk.
44    pub fn save(&self) -> Result<()> {
45        let path = Self::state_path(&self.agent_name)?;
46        let json = serde_json::to_string_pretty(self)?;
47        std::fs::write(&path, json)
48            .with_context(|| format!("failed to write {}", path.display()))?;
49        Ok(())
50    }
51
52    /// Load a deployment state from disk.
53    pub fn load(agent_name: &str) -> Result<Self> {
54        let path = Self::state_path(agent_name)?;
55        let json = std::fs::read_to_string(&path)
56            .with_context(|| format!("no deployment state found at {}", path.display()))?;
57        serde_json::from_str(&json).context("failed to parse deployment state")
58    }
59
60    /// Remove the state file after destroy.
61    pub fn remove(&self) -> Result<()> {
62        let path = Self::state_path(&self.agent_name)?;
63        if path.exists() {
64            std::fs::remove_file(&path)?;
65        }
66        Ok(())
67    }
68
69    /// List all saved deployment states.
70    #[allow(dead_code)]
71    pub fn list_all() -> Result<Vec<Self>> {
72        let dir = Self::state_dir()?;
73        let mut states = Vec::new();
74
75        if let Ok(entries) = std::fs::read_dir(dir) {
76            for entry in entries.flatten() {
77                let path = entry.path();
78                if path.extension().is_some_and(|e| e == "json") {
79                    if let Ok(json) = std::fs::read_to_string(&path) {
80                        if let Ok(state) = serde_json::from_str::<DeploymentState>(&json) {
81                            states.push(state);
82                        }
83                    }
84                }
85            }
86        }
87
88        Ok(states)
89    }
90}