rok-cli 0.6.1

Developer CLI for rok-based Axum applications
//! `rok down` / `rok up` — application maintenance mode.

use std::{fs, path::Path};

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct MaintenanceState {
    pub secret: String,
    pub message: Option<String>,
    pub retry_at: Option<String>,
}

const STATE_PATH: &str = "storage/framework/down.json";

/// Put the application into maintenance mode.
pub fn activate(secret: Option<String>, message: Option<String>, retry_after: Option<u64>) -> anyhow::Result<()> {
    if Path::new(STATE_PATH).exists() {
        anyhow::bail!("Application is already in maintenance mode. Run `rok up` first.");
    }

    let secret = secret.unwrap_or_else(|| generate_secret());
    let retry_at = retry_after.map(|secs| {
        let now = chrono::Utc::now() + chrono::Duration::seconds(secs as i64);
        now.to_rfc3339()
    });

    let state = MaintenanceState {
        secret,
        message,
        retry_at,
    };

    fs::create_dir_all("storage/framework")?;
    let json = serde_json::to_string_pretty(&state)?;
    fs::write(STATE_PATH, &json)?;

    println!("Application is now in maintenance mode.");
    println!("Secret bypass token: {}", state.secret);
    if let Some(msg) = &state.message {
        println!("Message: {msg}");
    }
    if let Some(retry) = &state.retry_at {
        println!("Retry after: {retry}");
    }
    Ok(())
}

/// Bring the application out of maintenance mode.
pub fn deactivate() -> anyhow::Result<()> {
    if !Path::new(STATE_PATH).exists() {
        anyhow::bail!("Application is not in maintenance mode.");
    }
    fs::remove_file(STATE_PATH)?;
    println!("Maintenance mode disabled.");
    Ok(())
}

/// Check if maintenance mode is active and return the state.
pub fn status() -> anyhow::Result<Option<MaintenanceState>> {
    if !Path::new(STATE_PATH).exists() {
        println!("Application is not in maintenance mode.");
        return Ok(None);
    }
    let json = fs::read_to_string(STATE_PATH)?;
    let state: MaintenanceState = serde_json::from_str(&json)?;
    println!("Application is in maintenance mode.");
    println!("  Secret: {}", state.secret);
    if let Some(msg) = &state.message {
        println!("  Message: {msg}");
    }
    if let Some(retry) = &state.retry_at {
        println!("  Retry after: {retry}");
    }
    Ok(Some(state))
}

fn generate_secret() -> String {
    use rand::Rng;
    const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
    let mut rng = rand::thread_rng();
    (0..32)
        .map(|_| {
            let idx = rng.gen_range(0..CHARSET.len());
            CHARSET[idx] as char
        })
        .collect()
}