slave 0.8.1

CLI for slave, a Laravel-inspired Rust framework for MCP-first apps: scaffold an MCP backend with auth, roles and Toasty models and migrations, add server-rendered web apps, and route them by domain or path
//! Command-line interface definition.

use std::path::PathBuf;

use clap::{Args, Parser, Subcommand};

/// A Laravel-inspired Rust framework for MCP-first apps.
#[derive(Debug, Parser)]
#[command(name = "slave", version, about, long_about = None)]
#[command(propagate_version = true, arg_required_else_help = true)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Command,
}

#[derive(Debug, Subcommand)]
pub enum Command {
    /// Create a new slave application
    New(NewArgs),

    /// Create a new model in a domain module
    ///
    /// Writes src/modules/<module>/model.rs. The module defaults to the model
    /// name in snake_case (`BlogPost` → `blog_post`) and is created if missing.
    #[command(name = "make:model")]
    MakeModel(MakeArgs),

    /// Create MCP tools for a domain module
    ///
    /// Writes src/modules/<module>/tools.rs with example `<name>_list` and
    /// `<name>_get` tools and registers it in src/modules/mod.rs. MCP tools are
    /// the application's only business interface. The module defaults to the
    /// name without its `Tools` suffix, in snake_case (`BlogPostTools` →
    /// `blog_post`).
    #[command(name = "make:tools")]
    MakeTools(MakeArgs),

    /// Create a web app in apps/<name>
    ///
    /// Without --domain or --path the app is reached directly on its port.
    /// With them, the gateway routes that domain and/or path to it.
    ///
    /// `--stack minijinja` (default), `tera`, `handlebars` or `liquid` render
    /// pages on the server from templates in `apps/<name>/templates/` with that
    /// engine (e.g. a storefront theme); such apps are registered in
    /// config/apps.toml and started by `slave serve`. The app gets its data from
    /// the MCP backend.
    #[command(name = "make:app")]
    MakeApp(MakeAppArgs),

    /// Generate a Toasty migration from your model changes
    ///
    /// Diffs the models against the last snapshot in database/ and writes
    /// database/migrations/<timestamp>_<name>.sql plus a new snapshot. Toasty
    /// asks whether a dropped and an added table or column is a rename.
    #[command(name = "make:migration")]
    MakeMigration {
        /// Migration name in snake_case, e.g. `add_phone_to_users`
        name: String,
    },

    /// Apply every pending migration to the database in DATABASE_URL
    Migrate,

    /// Drop every table in the database, then apply all migrations
    ///
    /// Destroys all data in DATABASE_URL; Toasty asks you to confirm.
    #[command(name = "migrate:fresh")]
    MigrateFresh,

    /// Add the domain/path gateway (Pingora) to this project
    ///
    /// Writes the gateway/ workspace package (using slave-gateway). Not
    /// needed until apps are routed by domain or path; `make:app --domain` and
    /// `--path` install it on demand.
    #[command(name = "install:gateway")]
    InstallGateway,

    /// List the domain routes in config/apps.toml and whether each app is up
    #[command(name = "route:list")]
    RouteList,

    /// Show which route, app and headers a request would get
    #[command(name = "route:match")]
    RouteMatch {
        /// Host and optional path to look up, e.g. `tokox.localhost` or `localhost/admin/users`
        target: String,
    },

    /// Compile the backend, the gateway and the apps, or only some of them
    ///
    /// With no names, everything is built. Name components to build only
    /// those (e.g. in CI, only what changed): `backend`, `gateway`, or an app.
    /// `--list` shows them with the paths that affect each.
    Build(BuildArgs),

    /// Run what `slave build` compiled: the MCP backend, the gateway and every app
    Serve(ServeArgs),
}

#[derive(Debug, Args)]
pub struct NewArgs {
    /// Name of the application (also used as the directory and package name)
    pub name: String,

    /// Depend on a local checkout of slave-framework instead of crates.io
    #[arg(long, value_name = "PATH")]
    pub framework_path: Option<PathBuf>,

    /// Include the domain/path gateway now (otherwise it is installed when
    /// the first app is routed by --domain or --path)
    #[arg(long)]
    pub gateway: bool,
}

#[derive(Debug, Args)]
pub struct MakeArgs {
    /// Type name in PascalCase, e.g. `Post` or `PostTools`
    pub name: String,

    /// Domain module to generate into, in snake_case (default: derived from the name)
    #[arg(short, long, visible_alias = "domain", value_name = "MODULE")]
    pub module: Option<String>,

    /// Overwrite the file with a fresh template if it already exists
    #[arg(short, long)]
    pub force: bool,
}

#[derive(Debug, Args)]
pub struct MakeAppArgs {
    /// App name, used as apps/<name> and its package name, e.g. `dashboard`
    pub name: String,

    /// Host routed to the app through the gateway, e.g. `admin.localhost` or
    /// `{toko}.localhost` (installs the gateway if needed)
    #[arg(long, value_name = "HOST")]
    pub domain: Option<String>,

    /// Path prefix routed to the app through the gateway, e.g. `/admin`, on
    /// any host unless --domain is given (installs the gateway if needed)
    #[arg(long, value_name = "PREFIX")]
    pub path: Option<String>,

    /// Port the app listens on [default: the next free port from 4000]
    #[arg(long)]
    pub port: Option<u16>,

    /// The template engine the app renders its pages with
    #[arg(long, value_enum, default_value_t = Stack::Minijinja)]
    pub stack: Stack,
}

/// The kind of app `make:app` creates.
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum Stack {
    /// Server-rendered pages with MiniJinja templates (Jinja2 syntax): light, fast to build
    Minijinja,
    /// Server-rendered pages with Tera templates (Jinja2/Django-like syntax)
    Tera,
    /// Server-rendered pages with Handlebars templates (`{{value}}`, partials)
    Handlebars,
    /// Server-rendered pages with Liquid templates (Shopify's theme language)
    Liquid,
}

#[derive(Debug, Args)]
pub struct BuildArgs {
    /// Components to build: `backend`, `gateway` or app names [default: all]
    pub components: Vec<String>,

    /// Build optimized binaries (for production or measuring performance)
    #[arg(long)]
    pub release: bool,

    /// List the components and the paths that affect each, then exit
    #[arg(long, conflicts_with = "components")]
    pub list: bool,
}

#[derive(Debug, Args)]
pub struct ServeArgs {
    /// Host address for the MCP backend [default: APP_HOST from the environment or .env, else 127.0.0.1]
    #[arg(long)]
    pub host: Option<String>,

    /// Port for the MCP backend [default: APP_PORT from the environment or .env, else 3000]
    #[arg(short, long)]
    pub port: Option<u16>,

    /// Run the release build (`slave build --release`)
    #[arg(long)]
    pub release: bool,
}