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: "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: "doctor",
148 aliases: "-",
149 group: "Inspection",
150 description: "Report workspace faults; --fix repairs the clear ones",
151 },
152 Entry {
153 command: "explain",
154 aliases: "-",
155 group: "Inspection",
156 description: "Print the docker compose command, run nothing",
157 },
158 Entry {
159 command: "env",
160 aliases: "-",
161 group: "Inspection",
162 description: "Print the environment compose is given",
163 },
164 Entry {
165 command: "generate",
166 aliases: "-",
167 group: "Inspection",
168 description: "Write the compose overlays, start nothing",
169 },
170 Entry {
171 command: "backend",
172 aliases: "-",
173 group: "Backend",
174 description: "Run any command inside the backend container",
175 },
176 Entry {
177 command: "migrate",
178 aliases: "-",
179 group: "Backend",
180 description: "Run pending migrations",
181 },
182 Entry {
183 command: "seed",
184 aliases: "-",
185 group: "Backend",
186 description: "Run database seeders",
187 },
188 Entry {
189 command: "fresh",
190 aliases: "-",
191 group: "Backend",
192 description: "Rebuild the schema and seed it (destroys data)",
193 },
194 Entry {
195 command: "artisan",
196 aliases: "-",
197 group: "Backend",
198 description: "php artisan … (BACKEND_STACK=laravel)",
199 },
200 Entry {
201 command: "composer",
202 aliases: "-",
203 group: "Backend",
204 description: "composer … (BACKEND_STACK=laravel)",
205 },
206 Entry {
207 command: "pnpm",
208 aliases: "-",
209 group: "Frontend",
210 description: "pnpm … inside the frontend workspace",
211 },
212 Entry {
213 command: "ios",
214 aliases: "-",
215 group: "Mobile",
216 description: "Open mobile-client in the iOS Simulator",
217 },
218 Entry {
219 command: "android",
220 aliases: "-",
221 group: "Mobile",
222 description: "Open mobile-client on an Android emulator",
223 },
224 Entry {
225 command: "device",
226 aliases: "-",
227 group: "Mobile",
228 description: "Open on a USB phone, or print the Expo Go URL",
229 },
230 Entry {
231 command: "mobile",
232 aliases: "-",
233 group: "Mobile",
234 description: "Restart mobile-deps / mobile-packages / Metro",
235 },
236 Entry {
237 command: "reload",
238 aliases: "-",
239 group: "Mobile",
240 description: "Metro /status + /reload; --clear wipes caches",
241 },
242 Entry {
243 command: "prebuild",
244 aliases: "-",
245 group: "Mobile",
246 description: "expo prebuild (Expo apps only)",
247 },
248 Entry {
249 command: "sync-mobile-port",
250 aliases: "-",
251 group: "Mobile",
252 description: "Sync MOBILE_CLIENT_PORT into the mobile app configs",
253 },
254 Entry {
255 command: "bundler",
256 aliases: "configure-bundler",
257 group: "Mobile",
258 description: "Point the simulator at Metro",
259 },
260 Entry {
261 command: "desktop",
262 aliases: "-",
263 group: "Desktop",
264 description: "Launch the Electron / Tauri shell on the host",
265 },
266 Entry {
267 command: "deploy",
268 aliases: "-",
269 group: "Deploy",
270 description: "Deploy web, mobile or desktop",
271 },
272 Entry {
273 command: "dash",
274 aliases: "dashboard",
275 group: "Dashboard",
276 description: "Open the status dashboard in a browser",
277 },
278];
279
280pub fn run(raw: bool) -> Result<i32> {
281 if raw {
282 let mut seen = std::collections::BTreeSet::new();
283 for entry in CATALOG {
284 if seen.insert(entry.command) {
285 println!("{}", entry.command);
286 }
287 }
288 return Ok(0);
289 }
290 print_table();
291 Ok(0)
292}
293
294fn print_table() {
295 print!("{}", render());
296}
297
298fn render() -> String {
300 let rows: Vec<Vec<String>> = CATALOG
301 .iter()
302 .map(|entry| {
303 vec![
304 entry.command.to_string(),
305 entry.aliases.to_string(),
306 entry.group.to_string(),
307 entry.description.to_string(),
308 ]
309 })
310 .collect();
311
312 format!(
313 "Commands\n{}",
314 crate::table::render(&["COMMAND", "ALIASES", "GROUP", "DESCRIPTION"], &rows)
315 )
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn doctor_is_in_the_catalogue() {
324 assert!(CATALOG.iter().any(|entry| entry.command == "doctor"));
325 }
326
327 #[test]
328 fn every_row_is_bordered_and_the_same_width() {
329 let table = render();
330 let lines: Vec<&str> = table.lines().skip(1).collect();
331
332 let expected = lines[0].chars().count();
333 for line in &lines {
334 assert_eq!(
335 line.chars().count(),
336 expected,
337 "ragged border on line: {line}"
338 );
339 }
340 assert!(lines.first().unwrap().starts_with('┌'));
341 assert!(lines.last().unwrap().starts_with('└'));
342 }
343
344 #[test]
345 fn no_cell_uses_an_ambiguous_width_character() {
346 for entry in CATALOG {
350 for cell in [entry.command, entry.aliases, entry.group, entry.description] {
351 assert!(
352 !cell.contains('\u{2014}'),
353 "em dash in catalogue cell: {cell}"
354 );
355 }
356 }
357 }
358
359 #[test]
360 fn raw_mode_lists_plain_names() {
361 assert!(!CATALOG.is_empty());
363 assert!(CATALOG.iter().all(|entry| !entry.command.contains('│')));
364 }
365}