mzrs-cli 0.1.21

CLI tool for scaffolding mzrs bot projects
//! Scaffold a new mzrs bot project.

use std::fs;
use std::path::Path;

/// Run the `new` subcommand: create a bot project from a template.
pub fn run(name: &str, template: &str) {
    let project_dir = Path::new(name);

    if project_dir.exists() {
        eprintln!("error: directory `{}` already exists", name);
        std::process::exit(1);
    }

    let src_dir = project_dir.join("src");
    fs::create_dir_all(&src_dir).unwrap_or_else(|e| {
        eprintln!("error: failed to create directory: {e}");
        std::process::exit(1);
    });

    // Cargo.toml
    let cargo_toml = generate_cargo_toml(name);
    write_file(&project_dir.join("Cargo.toml"), &cargo_toml);

    // src/main.rs
    let main_rs = match template {
        "minimal" => generate_minimal_main(),
        "command" => generate_command_main(),
        "full" => generate_full_main(),
        other => {
            eprintln!("error: unknown template `{other}`. Available: minimal, command, full");
            // Clean up
            let _ = fs::remove_dir_all(project_dir);
            std::process::exit(1);
        }
    };
    write_file(&src_dir.join("main.rs"), &main_rs);

    // .env.example
    write_file(
        &project_dir.join(".env.example"),
        "MZRS_BOT_ID=\nMZRS_TOKEN=\n",
    );

    // .gitignore
    write_file(&project_dir.join(".gitignore"), "/target\n.env\n");

    println!("Created new mzrs bot project `{name}` (template: {template})");
    println!();
    println!("  cd {name}");
    println!("  cp .env.example .env   # fill in your credentials");
    println!("  cargo run");
}

fn write_file(path: &Path, content: &str) {
    fs::write(path, content).unwrap_or_else(|e| {
        eprintln!("error: failed to write {}: {e}", path.display());
        std::process::exit(1);
    });
}

fn generate_cargo_toml(name: &str) -> String {
    format!(
        r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2021"

[dependencies]
mzrs-bot = "0.1"
mzrs-macros = "0.1"
tokio = {{ version = "1", features = ["macros", "rt-multi-thread"] }}
async-trait = "0.1"
tracing = "0.1"
tracing-subscriber = "0.3"
dotenvy = "0.15"
"#
    )
}

fn generate_minimal_main() -> String {
    r#"use mzrs_bot::prelude::*;

struct Handler;

#[async_trait]
impl BotHandler for Handler {
    async fn on_channel_message(&self, bot: &Bot, msg: ChannelMessage) {
        if is_own_message(bot, &msg) {
            return;
        }
        if parse_text(&msg.content).trim() == "!ping" {
            let _ = bot.reply_text(&msg, "Pong!").await;
        }
    }
}

#[tokio::main]
async fn main() -> Result<(), BotError> {
    dotenvy::dotenv().ok();
    tracing_subscriber::fmt::init();

    Bot::from_env()?.build()?.run(Handler).await
}
"#
    .to_string()
}

fn generate_command_main() -> String {
    r#"use mzrs_bot::prelude::*;
use mzrs_macros::command;

struct Handler;

#[async_trait]
impl BotHandler for Handler {}

#[command(name = "ping", description = "Check bot status")]
async fn ping(ctx: MessageContext<'_>) -> Result<(), BotError> {
    ctx.reply("Pong!").await
}

#[command(name = "echo", description = "Echo a message")]
async fn echo(ctx: MessageContext<'_>) -> Result<(), BotError> {
    let text = ctx.args().join(" ");
    if text.is_empty() {
        ctx.reply("Usage: !echo <text>").await
    } else {
        ctx.reply(&text).await
    }
}

#[tokio::main]
async fn main() -> Result<(), BotError> {
    dotenvy::dotenv().ok();
    tracing_subscriber::fmt::init();

    Bot::from_env()?
        .prefix("!")
        .command("ping", PingCommand)
        .command("echo", EchoCommand)
        .build()?
        .run(Handler)
        .await
}
"#
    .to_string()
}

fn generate_full_main() -> String {
    r#"use mzrs_bot::prelude::*;
use mzrs_macros::command;

struct Handler;

#[async_trait]
impl BotHandler for Handler {
    async fn on_connected(&self, _bot: &Bot) {
        tracing::info!("Bot connected!");
    }

    async fn on_disconnected(&self, _bot: &Bot) {
        tracing::warn!("Bot disconnected!");
    }

    async fn on_channel_message(&self, bot: &Bot, msg: ChannelMessage) {
        if is_own_message(bot, &msg) {
            return;
        }
        let text = parse_text(&msg.content);
        tracing::info!(sender = msg.sender_id, %text, "received message");
    }
}

#[command(name = "ping", description = "Check bot status")]
async fn ping(ctx: MessageContext<'_>) -> Result<(), BotError> {
    ctx.reply("Pong!").await
}

#[command(name = "echo", description = "Echo a message")]
async fn echo(ctx: MessageContext<'_>) -> Result<(), BotError> {
    let text = ctx.args().join(" ");
    if text.is_empty() {
        ctx.reply("Usage: !echo <text>").await
    } else {
        ctx.reply(&text).await
    }
}

#[command(name = "help", description = "Show available commands")]
async fn help(ctx: MessageContext<'_>) -> Result<(), BotError> {
    ctx.reply("Available commands: !ping, !echo <text>, !help")
        .await
}

#[tokio::main]
async fn main() -> Result<(), BotError> {
    dotenvy::dotenv().ok();
    tracing_subscriber::fmt::init();

    Bot::from_env()?
        .prefix("!")
        .command("ping", PingCommand)
        .command("echo", EchoCommand)
        .command("help", HelpCommand)
        .build()?
        .run(Handler)
        .await
}
"#
    .to_string()
}