forte-cli 0.3.30

CLI for the Forte fullstack web framework
use anyhow::{Result, anyhow};
use serde::Deserialize;
use std::path::PathBuf;

#[derive(Deserialize)]
struct ForteConfig {
    name: Option<String>,
}

pub async fn run(project_dir: PathBuf, new_name: String) -> Result<()> {
    let config_path = project_dir.join("Forte.toml");
    let content = std::fs::read_to_string(&config_path)
        .map_err(|_| anyhow!("Forte.toml not found. Are you in a Forte project directory?"))?;
    let config: ForteConfig =
        toml::from_str(&content).map_err(|e| anyhow!("Failed to parse Forte.toml: {}", e))?;

    let project_name = config
        .name
        .ok_or_else(|| anyhow!("'name' field missing in Forte.toml"))?;

    if project_name == new_name {
        return Err(anyhow!(
            "new name '{}' is identical to current name",
            new_name
        ));
    }

    fn0_deploy::rename(&project_name, &new_name).await?;

    let updated = rewrite_name_in_toml(&content, &project_name, &new_name)?;
    std::fs::write(&config_path, updated)
        .map_err(|e| anyhow!("Failed to update Forte.toml: {}", e))?;
    println!("Updated {}: name = \"{}\"", config_path.display(), new_name);

    Ok(())
}

fn rewrite_name_in_toml(content: &str, old_name: &str, new_name: &str) -> Result<String> {
    let mut out = String::with_capacity(content.len());
    let mut replaced = false;
    for line in content.lines() {
        if !replaced {
            if let Some(rewritten) = try_rewrite_name_line(line, old_name, new_name) {
                out.push_str(&rewritten);
                out.push('\n');
                replaced = true;
                continue;
            }
        }
        out.push_str(line);
        out.push('\n');
    }
    if !replaced {
        return Err(anyhow!(
            "could not locate `name = \"{}\"` line in Forte.toml",
            old_name
        ));
    }
    Ok(out)
}

fn try_rewrite_name_line(line: &str, old_name: &str, new_name: &str) -> Option<String> {
    let trimmed = line.trim_start();
    if !trimmed.starts_with("name") {
        return None;
    }
    let leading_ws_len = line.len() - trimmed.len();
    let after_key = trimmed.strip_prefix("name")?;
    let after_eq = after_key.trim_start().strip_prefix('=')?;
    let value_part = after_eq.trim_start();
    let (quote, expected) = if let Some(rest) = value_part.strip_prefix('"') {
        ('"', rest)
    } else if let Some(rest) = value_part.strip_prefix('\'') {
        ('\'', rest)
    } else {
        return None;
    };
    let end_idx = expected.find(quote)?;
    let current = &expected[..end_idx];
    if current != old_name {
        return None;
    }
    let leading_ws = &line[..leading_ws_len];
    Some(format!("{leading_ws}name = \"{new_name}\""))
}