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