proses 0.1.1

Proses – Professional Secure Execution System
use crate::process::Process;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;

/// Every command the CLI can send to the daemon over the Unix socket.
///
/// Serialised as newline-delimited JSON with an internal `"cmd"` tag, e.g.:
/// `{"cmd":"start","name":"myapp","command":"node index.js","cwd":"/app","env":{},"max_restarts":10}`
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "cmd", rename_all = "snake_case")]
pub enum Request {
    /// Launch a new managed process.
    Start {
        name: String,
        command: String,
        cwd: String,
        env: HashMap<String, String>,
        max_restarts: u32,
    },
    /// Gracefully stop a running process and mark it `Stopped`
    /// (no automatic restart).
    Stop { name_or_id: String },
    /// Stop then re-spawn a process, resetting its PID and start timestamp.
    Restart { name_or_id: String },
    /// Stop (if running) and permanently remove a process from the store.
    Delete { name_or_id: String },
    /// Return the full list of managed processes.
    List,
    /// Tail the last `lines` lines of a process's stdout log.
    Logs { name_or_id: String, lines: usize },
    /// Return detailed information about a single process.
    Show { name_or_id: String },
    /// Flush the in-memory store to `~/.proses/store.json`.
    Save,
    /// Re-spawn all processes that were in the `Running` state at last save.
    Resurrect,
    /// Ping the daemon; returns `{"success":true,"message":"ok"}`.
    Health,
    /// Gracefully stop all processes and exit the daemon.
    Shutdown,
}

/// Envelope returned by the daemon for every [`Request`].
#[derive(Debug, Serialize, Deserialize)]
pub struct Response {
    pub success: bool,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub processes: Option<Vec<Process>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub process: Option<Process>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub logs: Option<String>,
}

impl Response {
    /// Construct a successful response with `message` and no payload.
    pub fn ok(message: impl Into<String>) -> Self {
        Self {
            success: true,
            message: message.into(),
            processes: None,
            process: None,
            logs: None,
        }
    }

    /// Construct a failure response with `message`.
    pub fn err(message: impl Into<String>) -> Self {
        Self {
            success: false,
            message: message.into(),
            processes: None,
            process: None,
            logs: None,
        }
    }

    /// Attach a process list payload (used by `List`).
    pub fn with_processes(mut self, procs: Vec<Process>) -> Self {
        self.processes = Some(procs);
        self
    }

    /// Attach a single-process payload (used by `Start`, `Show`, etc.).
    pub fn with_process(mut self, proc: Process) -> Self {
        self.process = Some(proc);
        self
    }

    /// Attach a log-content payload (used by `Logs`).
    pub fn with_logs(mut self, logs: String) -> Self {
        self.logs = Some(logs);
        self
    }
}

/// Blocking IPC client.
///
/// Connects to the daemon's Unix domain socket, writes the JSON-encoded
/// `request` followed by a newline, then reads and deserialises the
/// single-line JSON response.
///
/// This function uses only `std` I/O so it can be called from the
/// synchronous CLI without a Tokio runtime.
pub fn send(request: &Request) -> Result<Response> {
    let sock_path = crate::config::get().sock_path.clone();

    let mut stream = UnixStream::connect(&sock_path).map_err(|e| {
        anyhow::anyhow!(
            "Cannot connect to daemon socket at {:?}: {}\n\
             Hint: run `proses daemon start` first.",
            sock_path,
            e
        )
    })?;

    // ── Write request ──────────────────────────────────────────────────────
    let mut payload = serde_json::to_string(request)?;
    payload.push('\n');
    stream
        .write_all(payload.as_bytes())
        .context("Failed to write request to daemon socket")?;
    stream
        .flush()
        .context("Failed to flush request to daemon socket")?;

    // ── Read response ──────────────────────────────────────────────────────
    // The daemon always writes exactly one JSON line per connection.
    let mut line = String::new();
    BufReader::new(&stream)
        .read_line(&mut line)
        .context("Failed to read response from daemon socket")?;

    let response: Response =
        serde_json::from_str(line.trim()).context("Failed to deserialise daemon response")?;

    Ok(response)
}