run-stack 0.6.2

One command to boot a full local stack in Docker: API, Vite apps, Expo mobile, desktop renderer, database, mail and a status dashboard.
Documentation
//! `rst commands` — every CLI entry in a terminal table.

use anyhow::Result;

/// One row in the command catalogue.
struct Entry {
    command: &'static str,
    aliases: &'static str,
    group: &'static str,
    description: &'static str,
}

const CATALOG: &[Entry] = &[
    Entry {
        command: "create",
        aliases: "new",
        group: "Global",
        description: "Write .run/run.config.toml and run init",
    },
    Entry {
        command: "migrate",
        aliases: "-",
        group: "Global",
        description: "Convert legacy run/ to .run/ (config + env)",
    },
    Entry {
        command: "upgrade",
        aliases: "update",
        group: "Global",
        description: "Refresh legacy run/ or self-update",
    },
    Entry {
        command: "self-update",
        aliases: "-",
        group: "Global",
        description: "Install latest from crates.io and migrate configs",
    },
    Entry {
        command: "completion",
        aliases: "-",
        group: "Global",
        description: "Shell / Kiro autocompletion (bash|zsh|fig|install|services)",
    },
    Entry {
        command: "init",
        aliases: "-",
        group: "Setup",
        description: "Write .run/run.config.toml and .run/.env",
    },
    Entry {
        command: "init --update",
        aliases: "-",
        group: "Setup",
        description: "Find and add apps added to the workspace since setup",
    },
    Entry {
        command: "up",
        aliases: "run",
        group: "Lifecycle",
        description: "Build if needed and start (--essential = config essentials)",
    },
    Entry {
        command: "up --no-start",
        aliases: "-",
        group: "Lifecycle",
        description: "Create the containers but leave them stopped",
    },
    Entry {
        command: "down",
        aliases: "-",
        group: "Lifecycle",
        description: "Stop everything, or named services (keeps data)",
    },
    Entry {
        command: "restart",
        aliases: "-",
        group: "Lifecycle",
        description: "Stop then start (down + up); --build, --essential",
    },
    Entry {
        command: "rebuild",
        aliases: "-",
        group: "Lifecycle",
        description: "Rebuild images from scratch and recreate containers",
    },
    Entry {
        command: "clean",
        aliases: "-",
        group: "Lifecycle",
        description: "Stop and DELETE all volumes",
    },
    Entry {
        command: "commands",
        aliases: "-",
        group: "Inspection",
        description: "List every CLI command in a table",
    },
    Entry {
        command: "apps",
        aliases: "list",
        group: "Inspection",
        description: "List configured apps (backend, web, mobile, …)",
    },
    Entry {
        command: "services",
        aliases: "-",
        group: "Inspection",
        description: "List compose services (and status when Docker is up)",
    },
    Entry {
        command: "ps",
        aliases: "status",
        group: "Inspection",
        description: "Service status",
    },
    Entry {
        command: "ports",
        aliases: "-",
        group: "Inspection",
        description: "Host ports + env URLs table",
    },
    Entry {
        command: "logs",
        aliases: "-",
        group: "Inspection",
        description: "Follow logs",
    },
    Entry {
        command: "shell",
        aliases: "sh",
        group: "Inspection",
        description: "Shell into a container (default: backend)",
    },
    Entry {
        command: "config",
        aliases: "-",
        group: "Inspection",
        description: "Print the resolved configuration",
    },
    Entry {
        command: "doctor",
        aliases: "-",
        group: "Inspection",
        description: "Report workspace faults; --fix repairs the clear ones",
    },
    Entry {
        command: "explain",
        aliases: "-",
        group: "Inspection",
        description: "Print the docker compose command, run nothing",
    },
    Entry {
        command: "env",
        aliases: "-",
        group: "Inspection",
        description: "Print the environment compose is given",
    },
    Entry {
        command: "generate",
        aliases: "-",
        group: "Inspection",
        description: "Write the compose overlays, start nothing",
    },
    Entry {
        command: "backend",
        aliases: "-",
        group: "Backend",
        description: "Run any command inside the backend container",
    },
    Entry {
        command: "migrate",
        aliases: "-",
        group: "Backend",
        description: "Run pending migrations",
    },
    Entry {
        command: "seed",
        aliases: "-",
        group: "Backend",
        description: "Run database seeders",
    },
    Entry {
        command: "fresh",
        aliases: "-",
        group: "Backend",
        description: "Rebuild the schema and seed it (destroys data)",
    },
    Entry {
        command: "artisan",
        aliases: "-",
        group: "Backend",
        description: "php artisan … (BACKEND_STACK=laravel)",
    },
    Entry {
        command: "composer",
        aliases: "-",
        group: "Backend",
        description: "composer … (BACKEND_STACK=laravel)",
    },
    Entry {
        command: "pnpm",
        aliases: "-",
        group: "Frontend",
        description: "pnpm … inside the frontend workspace",
    },
    Entry {
        command: "ios",
        aliases: "-",
        group: "Mobile",
        description: "Open mobile-client in the iOS Simulator",
    },
    Entry {
        command: "android",
        aliases: "-",
        group: "Mobile",
        description: "Open mobile-client on an Android emulator",
    },
    Entry {
        command: "device",
        aliases: "-",
        group: "Mobile",
        description: "Open on a USB phone, or print the Expo Go URL",
    },
    Entry {
        command: "mobile",
        aliases: "-",
        group: "Mobile",
        description: "Restart mobile-deps / mobile-packages / Metro",
    },
    Entry {
        command: "reload",
        aliases: "-",
        group: "Mobile",
        description: "Metro /status + /reload; --clear wipes caches",
    },
    Entry {
        command: "prebuild",
        aliases: "-",
        group: "Mobile",
        description: "expo prebuild (Expo apps only)",
    },
    Entry {
        command: "sync-mobile-port",
        aliases: "-",
        group: "Mobile",
        description: "Sync MOBILE_CLIENT_PORT into the mobile app configs",
    },
    Entry {
        command: "bundler",
        aliases: "configure-bundler",
        group: "Mobile",
        description: "Point the simulator at Metro",
    },
    Entry {
        command: "desktop",
        aliases: "-",
        group: "Desktop",
        description: "Launch the Electron / Tauri shell on the host",
    },
    Entry {
        command: "deploy",
        aliases: "-",
        group: "Deploy",
        description: "Deploy web, mobile or desktop",
    },
    Entry {
        command: "dash",
        aliases: "dashboard",
        group: "Dashboard",
        description: "Open the status dashboard in a browser",
    },
];

pub fn run(raw: bool) -> Result<i32> {
    if raw {
        let mut seen = std::collections::BTreeSet::new();
        for entry in CATALOG {
            if seen.insert(entry.command) {
                println!("{}", entry.command);
            }
        }
        return Ok(0);
    }
    print_table();
    Ok(0)
}

fn print_table() {
    print!("{}", render());
}

/// The catalogue as a bordered table.
fn render() -> String {
    let rows: Vec<Vec<String>> = CATALOG
        .iter()
        .map(|entry| {
            vec![
                entry.command.to_string(),
                entry.aliases.to_string(),
                entry.group.to_string(),
                entry.description.to_string(),
            ]
        })
        .collect();

    format!(
        "Commands\n{}",
        crate::table::render(&["COMMAND", "ALIASES", "GROUP", "DESCRIPTION"], &rows)
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn doctor_is_in_the_catalogue() {
        assert!(CATALOG.iter().any(|entry| entry.command == "doctor"));
    }

    #[test]
    fn every_row_is_bordered_and_the_same_width() {
        let table = render();
        let lines: Vec<&str> = table.lines().skip(1).collect();

        let expected = lines[0].chars().count();
        for line in &lines {
            assert_eq!(
                line.chars().count(),
                expected,
                "ragged border on line: {line}"
            );
        }
        assert!(lines.first().unwrap().starts_with(''));
        assert!(lines.last().unwrap().starts_with(''));
    }

    #[test]
    fn no_cell_uses_an_ambiguous_width_character() {
        // An em dash is one char but renders two columns wide in terminals that
        // treat East Asian "ambiguous" width as wide, so a row holding one sits
        // a column past every row that does not.
        for entry in CATALOG {
            for cell in [entry.command, entry.aliases, entry.group, entry.description] {
                assert!(
                    !cell.contains('\u{2014}'),
                    "em dash in catalogue cell: {cell}"
                );
            }
        }
    }

    #[test]
    fn raw_mode_lists_plain_names() {
        // `rst commands --raw` feeds shell completion, so it must stay unadorned.
        assert!(!CATALOG.is_empty());
        assert!(CATALOG.iter().all(|entry| !entry.command.contains('')));
    }
}