cargo-rahti 0.0.6

Create and maintain Rahti projects: cargo rahti new, cargo rahti upgrade.
//! `cargo rahti` — create and maintain Rahti projects.
//!
//! Cargo turns `cargo rahti <args>` into `cargo-rahti rahti <args>`, so the
//! first argument is the subcommand's own name and is skipped. Running the
//! binary directly works too, which is what the tests do.
//!
//! Arguments are read by hand rather than with a parser crate. The surface is
//! a verb, a name and three flags — each of which adds something, so there
//! are no negations to reconcile; a dependency that reads them would be
//! larger than the code that acts on them.

mod new;
mod prompt;
mod templates;
mod upgrade;

use std::process::ExitCode;

/// Written into `createdWith`, so a project records the version that made it.
const VERSION: &str = env!("CARGO_PKG_VERSION");

fn main() -> ExitCode {
    let args: Vec<String> = std::env::args().skip(1).collect();

    // `cargo rahti new app` arrives as `["rahti", "new", "app"]`; running the
    // binary directly gives `["new", "app"]`. Both are supported.
    let args: Vec<&str> = args
        .iter()
        .map(String::as_str)
        .skip_while(|a| *a == "rahti")
        .collect();

    let result = match args.first().copied() {
        Some("new") => new::run(&args[1..]),
        Some("upgrade") => upgrade::run(&args[1..]),
        _ => return other(&args),
    };

    match result {
        Ok(()) => ExitCode::SUCCESS,
        Err(message) => {
            eprintln!("error: {message}");
            ExitCode::FAILURE
        }
    }
}

/// Everything that is not a command that does work.
fn other(args: &[&str]) -> ExitCode {
    match args.first().copied() {
        Some("--version" | "-V") => {
            println!("cargo-rahti {VERSION}");
            ExitCode::SUCCESS
        }
        Some("help" | "--help" | "-h") | None => {
            print!("{}", usage());
            ExitCode::SUCCESS
        }
        Some(other) => {
            eprintln!("error: `{other}` is not a cargo-rahti command.\n");
            eprint!("{}", usage());
            ExitCode::FAILURE
        }
    }
}

fn usage() -> String {
    format!(
        "cargo-rahti {VERSION}
Create and maintain Rahti projects.

USAGE:
    cargo rahti new <name> [options]
    cargo rahti upgrade [--dry-run] [--db [backend]] [--ws]

NEW:
    A flag adds a feature. Leaving it out is how you say no.

    --tailwind         Add Tailwind CSS. Without it, plain CSS.
    --db [backend]     Add a database — sqlite, postgres or mysql. Bare
                       `--db` is sqlite, the one that needs no server.
                       Writes src/models/ and src/migrations/, and adds
                       SeaORM to Cargo.toml.
    --ws               Add WebSockets: the `ws` feature on the rahti
                       dependency, which compiles `rahti::ws` and the
                       `#[socket]` attribute.
    --local <path>     Depend on a Rahti checkout by path rather than by
                       version. For working on the framework itself.

UPGRADE:
    -n, --dry-run      Print what would change, and change nothing.
    --db [backend]     Add a database to a project that has none — the same
                       flag `new` takes. Writes the files, records the
                       backend in rahti.config.json, and prints what to add
                       to Cargo.toml and .env, which stay yours.
    --ws               Add WebSockets to a project without them.

    Rewrites the scaffolded files you have not edited, and leaves the ones
    you have. Which is which is decided by the hashes in rahti.config.json,
    not by guessing. A dry run never asks a question — name the feature to
    preview adding it.

    -h, --help         Print this message.
    -V, --version      Print the version.

An interactive run asks about any feature you did not name. A run with
nowhere to ask — a pipe, a CI job — takes the flags at their word and adds
only what they list.
"
    )
}