1use anyhow::Result;
4
5struct 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
286fn 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 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 assert!(!CATALOG.is_empty());
351 assert!(CATALOG.iter().all(|entry| !entry.command.contains('│')));
352 }
353}