use anyhow::Result;
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());
}
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() {
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() {
assert!(!CATALOG.is_empty());
assert!(CATALOG.iter().all(|entry| !entry.command.contains('│')));
}
}