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: "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
304fn 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 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 assert!(!CATALOG.is_empty());
369 assert!(CATALOG.iter().all(|entry| !entry.command.contains('│')));
370 }
371}