slave 0.6.0

CLI for slave, a Laravel-inspired Rust framework for MCP-first apps: scaffold an MCP backend with auth, roles and Turso migrations, add Axum or Dioxus 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`. `--stack dioxus` is a
    /// client for web, desktop and mobile (e.g. an admin dashboard) in its own
    /// workspace, run with `dx`. Either way the app gets its data from the MCP
    /// backend.
    #[command(name = "make:app")]
    MakeApp(MakeAppArgs),

    /// Create a new database migration
    ///
    /// Writes database/migrations/<timestamp>_<name>.rs and registers it in
    /// database/migrations/mod.rs. Names like `create_posts_table` start from
    /// a CREATE TABLE template; others start empty and irreversible.
    #[command(name = "make:migration")]
    MakeMigration {
        /// Migration name in snake_case, e.g. `create_users_table`
        name: String,
    },

    /// Run all pending database migrations as a new batch
    Migrate,

    /// Show which migrations have run and which are pending
    #[command(name = "migrate:status")]
    MigrateStatus,

    /// Roll back the most recent batch of migrations
    ///
    /// Refuses, changing nothing, if any migration in the batch is
    /// irreversible (its `down()` returns `None`).
    #[command(name = "migrate:rollback")]
    MigrateRollback,

    /// Drop every table in the database, then run all migrations
    ///
    /// Destroys all data in TURSO_DATABASE_URL, so it requires --force.
    #[command(name = "migrate:fresh")]
    MigrateFresh {
        /// Confirm that every table and all data may be deleted
        #[arg(long)]
        force: bool,
    },

    /// 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 for a server-rendered app, or `dioxus` for a client
    #[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,
    /// A Dioxus client for web, desktop and mobile, e.g. an admin dashboard
    Dioxus,
}

#[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,
}