Skip to main content

run_stack/
commands.rs

1//! `rst commands` — every CLI entry in a terminal table.
2
3use anyhow::Result;
4
5/// One row in the command catalogue.
6struct Entry {
7    command: &'static str,
8    aliases: &'static str,
9    group: &'static str,
10    description: &'static str,
11}
12
13const CATALOG: &[Entry] = &[
14    Entry {
15        command: "create",
16        aliases: "new",
17        group: "Global",
18        description: "Write .run/run.config.toml and run init",
19    },
20    Entry {
21        command: "migrate",
22        aliases: "-",
23        group: "Global",
24        description: "Convert legacy run/ to .run/ (config + env)",
25    },
26    Entry {
27        command: "upgrade",
28        aliases: "update",
29        group: "Global",
30        description: "Refresh legacy run/ or self-update",
31    },
32    Entry {
33        command: "self-update",
34        aliases: "-",
35        group: "Global",
36        description: "Install latest from crates.io and migrate configs",
37    },
38    Entry {
39        command: "completion",
40        aliases: "-",
41        group: "Global",
42        description: "Shell / Kiro autocompletion (bash|zsh|fig|install|services)",
43    },
44    Entry {
45        command: "init",
46        aliases: "-",
47        group: "Setup",
48        description: "Write .run/run.config.toml and .run/.env",
49    },
50    Entry {
51        command: "init --update",
52        aliases: "-",
53        group: "Setup",
54        description: "Find and add apps added to the workspace since setup",
55    },
56    Entry {
57        command: "up",
58        aliases: "run",
59        group: "Lifecycle",
60        description: "Build if needed and start (--essential = config essentials)",
61    },
62    Entry {
63        command: "down",
64        aliases: "-",
65        group: "Lifecycle",
66        description: "Stop everything, or named services (keeps data)",
67    },
68    Entry {
69        command: "restart",
70        aliases: "-",
71        group: "Lifecycle",
72        description: "Stop then start (down + up); --build, --essential",
73    },
74    Entry {
75        command: "rebuild",
76        aliases: "-",
77        group: "Lifecycle",
78        description: "Rebuild images from scratch and recreate containers",
79    },
80    Entry {
81        command: "clean",
82        aliases: "-",
83        group: "Lifecycle",
84        description: "Stop and DELETE all volumes",
85    },
86    Entry {
87        command: "commands",
88        aliases: "-",
89        group: "Inspection",
90        description: "List every CLI command in a table",
91    },
92    Entry {
93        command: "apps",
94        aliases: "list",
95        group: "Inspection",
96        description: "List configured apps (backend, web, mobile, …)",
97    },
98    Entry {
99        command: "services",
100        aliases: "-",
101        group: "Inspection",
102        description: "List compose services (and status when Docker is up)",
103    },
104    Entry {
105        command: "ps",
106        aliases: "status",
107        group: "Inspection",
108        description: "Service status",
109    },
110    Entry {
111        command: "ports",
112        aliases: "-",
113        group: "Inspection",
114        description: "Host ports + env URLs table",
115    },
116    Entry {
117        command: "logs",
118        aliases: "-",
119        group: "Inspection",
120        description: "Follow logs",
121    },
122    Entry {
123        command: "shell",
124        aliases: "sh",
125        group: "Inspection",
126        description: "Shell into a container (default: backend)",
127    },
128    Entry {
129        command: "config",
130        aliases: "-",
131        group: "Inspection",
132        description: "Print the resolved configuration",
133    },
134    Entry {
135        command: "doctor",
136        aliases: "-",
137        group: "Inspection",
138        description: "Report workspace faults; --fix repairs the clear ones",
139    },
140    Entry {
141        command: "explain",
142        aliases: "-",
143        group: "Inspection",
144        description: "Print the docker compose command, run nothing",
145    },
146    Entry {
147        command: "env",
148        aliases: "-",
149        group: "Inspection",
150        description: "Print the environment compose is given",
151    },
152    Entry {
153        command: "generate",
154        aliases: "-",
155        group: "Inspection",
156        description: "Write the compose overlays, start nothing",
157    },
158    Entry {
159        command: "backend",
160        aliases: "-",
161        group: "Backend",
162        description: "Run any command inside the backend container",
163    },
164    Entry {
165        command: "migrate",
166        aliases: "-",
167        group: "Backend",
168        description: "Run pending migrations",
169    },
170    Entry {
171        command: "seed",
172        aliases: "-",
173        group: "Backend",
174        description: "Run database seeders",
175    },
176    Entry {
177        command: "fresh",
178        aliases: "-",
179        group: "Backend",
180        description: "Rebuild the schema and seed it (destroys data)",
181    },
182    Entry {
183        command: "artisan",
184        aliases: "-",
185        group: "Backend",
186        description: "php artisan … (BACKEND_STACK=laravel)",
187    },
188    Entry {
189        command: "composer",
190        aliases: "-",
191        group: "Backend",
192        description: "composer … (BACKEND_STACK=laravel)",
193    },
194    Entry {
195        command: "pnpm",
196        aliases: "-",
197        group: "Frontend",
198        description: "pnpm … inside the frontend workspace",
199    },
200    Entry {
201        command: "ios",
202        aliases: "-",
203        group: "Mobile",
204        description: "Open mobile-client in the iOS Simulator",
205    },
206    Entry {
207        command: "android",
208        aliases: "-",
209        group: "Mobile",
210        description: "Open mobile-client on an Android emulator",
211    },
212    Entry {
213        command: "device",
214        aliases: "-",
215        group: "Mobile",
216        description: "Open on a USB phone, or print the Expo Go URL",
217    },
218    Entry {
219        command: "mobile",
220        aliases: "-",
221        group: "Mobile",
222        description: "Restart mobile-deps / mobile-packages / Metro",
223    },
224    Entry {
225        command: "reload",
226        aliases: "-",
227        group: "Mobile",
228        description: "Metro /status + /reload; --clear wipes caches",
229    },
230    Entry {
231        command: "prebuild",
232        aliases: "-",
233        group: "Mobile",
234        description: "expo prebuild (Expo apps only)",
235    },
236    Entry {
237        command: "sync-mobile-port",
238        aliases: "-",
239        group: "Mobile",
240        description: "Sync MOBILE_CLIENT_PORT into the mobile app configs",
241    },
242    Entry {
243        command: "bundler",
244        aliases: "configure-bundler",
245        group: "Mobile",
246        description: "Point the simulator at Metro",
247    },
248    Entry {
249        command: "desktop",
250        aliases: "-",
251        group: "Desktop",
252        description: "Launch the Electron / Tauri shell on the host",
253    },
254    Entry {
255        command: "deploy",
256        aliases: "-",
257        group: "Deploy",
258        description: "Deploy web, mobile or desktop",
259    },
260    Entry {
261        command: "dash",
262        aliases: "dashboard",
263        group: "Dashboard",
264        description: "Open the status dashboard in a browser",
265    },
266];
267
268pub fn run(raw: bool) -> Result<i32> {
269    if raw {
270        let mut seen = std::collections::BTreeSet::new();
271        for entry in CATALOG {
272            if seen.insert(entry.command) {
273                println!("{}", entry.command);
274            }
275        }
276        return Ok(0);
277    }
278    print_table();
279    Ok(0)
280}
281
282fn print_table() {
283    print!("{}", render());
284}
285
286/// The catalogue as a bordered table.
287fn render() -> String {
288    let rows: Vec<Vec<String>> = CATALOG
289        .iter()
290        .map(|entry| {
291            vec![
292                entry.command.to_string(),
293                entry.aliases.to_string(),
294                entry.group.to_string(),
295                entry.description.to_string(),
296            ]
297        })
298        .collect();
299
300    format!(
301        "Commands\n{}",
302        crate::table::render(&["COMMAND", "ALIASES", "GROUP", "DESCRIPTION"], &rows)
303    )
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn doctor_is_in_the_catalogue() {
312        assert!(CATALOG.iter().any(|entry| entry.command == "doctor"));
313    }
314
315    #[test]
316    fn every_row_is_bordered_and_the_same_width() {
317        let table = render();
318        let lines: Vec<&str> = table.lines().skip(1).collect();
319
320        let expected = lines[0].chars().count();
321        for line in &lines {
322            assert_eq!(
323                line.chars().count(),
324                expected,
325                "ragged border on line: {line}"
326            );
327        }
328        assert!(lines.first().unwrap().starts_with('┌'));
329        assert!(lines.last().unwrap().starts_with('└'));
330    }
331
332    #[test]
333    fn no_cell_uses_an_ambiguous_width_character() {
334        // An em dash is one char but renders two columns wide in terminals that
335        // treat East Asian "ambiguous" width as wide, so a row holding one sits
336        // a column past every row that does not.
337        for entry in CATALOG {
338            for cell in [entry.command, entry.aliases, entry.group, entry.description] {
339                assert!(
340                    !cell.contains('\u{2014}'),
341                    "em dash in catalogue cell: {cell}"
342                );
343            }
344        }
345    }
346
347    #[test]
348    fn raw_mode_lists_plain_names() {
349        // `rst commands --raw` feeds shell completion, so it must stay unadorned.
350        assert!(!CATALOG.is_empty());
351        assert!(CATALOG.iter().all(|entry| !entry.command.contains('│')));
352    }
353}