slave 0.9.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, that domain and/or path is routed to it. The runtime serves
    /// it in-process, or proxies to it with --separate.
    ///
    /// `--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 Fly.io deployment files for the single runtime
    ///
    /// Writes Dockerfile, .dockerignore, fly.toml and config/apps.fly.toml
    /// (config/apps.toml with `localhost` hosts moved under --domain).
    #[command(name = "install:fly")]
    InstallFly(InstallFlyArgs),

    /// Deploy the single runtime to Fly.io
    ///
    /// Writes the Fly.io files if missing, creates the app when needed,
    /// stages secrets from --secrets (DATABASE_URL, OIDC_*, ...), checks the
    /// required ones are set, then runs `fly deploy` (migrations run as the
    /// release command).
    #[command(name = "deploy:fly")]
    DeployFly(DeployFlyArgs),

    /// 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 runtime and the separate apps, or only some
    ///
    /// With no names, everything is built. Name components to build only
    /// those (e.g. in CI, only what changed): `backend`, `runtime`, or a
    /// separate app (`embed = false`). `--list` shows them with the paths that
    /// affect each.
    Build(BuildArgs),

    /// Run what `slave build` compiled: the runtime, plus each separate app
    Serve(ServeArgs),

    /// Check the setup: database, sign-in, each app's client ID, the build
    ///
    /// Prints what is missing and how to fix it, and the redirect URIs to
    /// register at the OpenID Connect provider. Fails if anything is missing.
    Doctor,

    /// Run an app as a process of its own, proxied by the runtime
    ///
    /// Sets `embed = false` on its entries in config/apps.toml and removes it
    /// from runtime/ (so the runtime no longer compiles it).
    #[command(name = "app:separate")]
    AppSeparate {
        /// The app, as in apps/<name>
        name: String,
    },

    /// Serve a separate app in-process again, inside the runtime
    #[command(name = "app:embed")]
    AppEmbed {
        /// The app, as in apps/<name>
        name: String,
    },
}

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

#[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, e.g. `admin.localhost` or `{toko}.localhost`
    #[arg(long, value_name = "HOST")]
    pub domain: Option<String>,

    /// Path prefix routed to the app, e.g. `/admin`, on any host unless
    /// --domain is given
    #[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>,

    /// Run it as a process of its own (`embed = false`), proxied by the
    /// runtime, instead of inside the runtime
    #[arg(long)]
    pub separate: bool,

    /// 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`, `runtime` or separate apps [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,

    /// Sign in without a real provider: a built-in, insecure OpenID Connect
    /// provider on 127.0.0.1:9000 where anyone signs in as any email (debug
    /// builds only). Overrides the OIDC_* settings of .env
    #[arg(long, conflicts_with = "release")]
    pub dev: bool,
}

#[derive(Debug, Default, Args)]
pub struct InstallFlyArgs {
    /// Fly app name [default: the project name]
    #[arg(long)]
    pub app: Option<String>,

    /// Fly region for the machines [default: sin]
    #[arg(long)]
    pub region: Option<String>,

    /// Public domain that replaces `localhost` in the routes, e.g.
    /// `example.com` [default: <app>.fly.dev]
    #[arg(long)]
    pub domain: Option<String>,

    /// Overwrite existing files
    #[arg(short, long)]
    pub force: bool,
}

#[derive(Debug, Args)]
pub struct DeployFlyArgs {
    /// Env file whose settings become Fly secrets (DATABASE_URL, OIDC_*, <APP>_OIDC_CLIENT_ID, ...)
    #[arg(long, value_name = "FILE")]
    pub secrets: Option<PathBuf>,
}